Created
June 12, 2018 15:06
-
-
Save mkochendorfer/20fdb893461848d3226e07209bc0abfc to your computer and use it in GitHub Desktop.
A Launchpad demo GraphQL service built for a P2Con workshop: https://launchpad.graphql.com/new
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { makeExecutableSchema } from 'graphql-tools'; | |
import Airtable from 'airtable'; | |
class AirtableHelper { | |
constructor(apiKey, baseId) { | |
// Init the Airtable integration. | |
this.base = new Airtable({ | |
endpointUrl: 'https://api.airtable.com', | |
apiKey, | |
}).base(baseId); | |
} | |
getAllRecords(table) { | |
return new Promise((resolve, reject) => { | |
this.base(table).select({ | |
maxRecords: 200, | |
view: "Grid view" | |
}).all((err, records) => { | |
if (err) { | |
reject(err); | |
return; | |
} | |
resolve(records); | |
}); | |
}); | |
} | |
getAllRecordsWithFilter(table, filterByFormula, maxRecords = 200) { | |
return new Promise((resolve, reject) => { | |
this.base(table).select({ | |
maxRecords: 200, | |
filterByFormula, | |
view: "Grid view" | |
}).all((err, records) => { | |
if (err) { | |
reject(err); | |
return; | |
} | |
resolve(records); | |
}); | |
}); | |
} | |
getRecordByKeyValue(table, key, value) { | |
const filter = `{${key}} = "${value}"`; | |
return this.getAllRecordsWithFilter(table, filter, 1) | |
.then(records => records[0]); | |
} | |
getRecordById(table, id) { | |
return new Promise((resolve, reject) => { | |
this.base(table).find(id, (err, record) => { | |
if (err) { | |
reject(err); | |
return; | |
} | |
resolve(record); | |
}); | |
}); | |
} | |
//The maximum is exclusive and the minimum is inclusive | |
getRandomInt(min, max) { | |
min = Math.ceil(min); | |
max = Math.floor(max); | |
return Math.floor(Math.random() * (max - min)) + min; | |
} | |
getRandomRecord(table) { | |
return this.getAllRecords(table).then(records => { | |
if (records.length <= 0) { | |
return null; | |
} | |
const randomIndex = this.getRandomInt(0, records.length); | |
return records[randomIndex]; | |
}); | |
} | |
search(table, column, searchText) { | |
return new Promise((resolve, reject) => { | |
this.base(table).select({ | |
maxRecords: 100, | |
filterByFormula: `SEARCH("${searchText}", {${column}})`, | |
view: "Grid view" | |
}).all((err, records) => { | |
if (err) { | |
reject(err); | |
return; | |
} | |
resolve(records); | |
}); | |
}); | |
} | |
createRecord(table, data) { | |
return new Promise((resolve, reject) => { | |
this.base(table).create(data, (err, record) => { | |
if (err) { | |
reject(err); | |
return; | |
} | |
resolve(record.getId()); | |
}); | |
}); | |
} | |
getValue(id) { | |
return this.getRecordByKeyValue('Store', 'id', id).then(record => record.fields.value); | |
} | |
setValue(id, value) { | |
return new Promise((resolve, reject) => { | |
this.getRecordByKeyValue('Store', 'id', id).then(record => { | |
record.set('value', `${value}`); | |
record.save((err) => { | |
if (err) { | |
reject(err); | |
return; | |
} | |
resolve(value); | |
}); | |
}); | |
}); | |
} | |
} | |
// Will be constructured in the context function so it can use secrets. | |
let airtable = null; | |
const schemaString = ` | |
schema { | |
query: Query | |
mutation: Mutation | |
} | |
# The query type, represents all of the entry points into our object graph | |
type Query { | |
quote: String | |
character(id: ID!): Character | |
characters: [Character] | |
droid(id: ID!): Droid | |
human(id: ID!): Human | |
jediMasters: [Human] | |
starship(id: ID!): Starship | |
starships: [Starship] | |
search(text: String): [SearchResult] | |
hanShotFirst: Boolean | |
greedoShotFirst: Boolean | |
} | |
# The mutation type, represents all updates we can make to our data | |
type Mutation { | |
hanShotFirst: Int | |
greedoShotFirst: Int | |
createQuote(text: String): ID | |
createHuman(human: HumanInput): ID | |
} | |
input HumanInput { | |
name: String! | |
height: Float | |
mass: Float | |
species: String | |
friends: [ID] | |
starships: [ID] | |
jediMaster: Boolean | |
} | |
# Units | |
enum Unit { | |
METRIC | |
IMPERIAL | |
} | |
# A character from the Star Wars universe | |
interface Character { | |
id: ID! | |
name: String! | |
height(unit: Unit = METRIC): Float | |
mass(unit: Unit = METRIC): Float | |
friends: [Character] | |
} | |
# A humanoid creature from the Star Wars universe | |
type Human implements Character { | |
id: ID! | |
name: String! | |
height(unit: Unit = METRIC): Float | |
mass(unit: Unit = METRIC): Float | |
species: String | |
friends: [Character] | |
starships: [Starship] | |
jediMaster: Boolean | |
} | |
# An autonomous mechanical character in the Star Wars universe | |
type Droid implements Character { | |
id: ID! | |
name: String! | |
height(unit: Unit = METRIC): Float | |
mass(unit: Unit = METRIC): Float | |
friends: [Character] | |
primaryFunction: String | |
} | |
type Starship { | |
id: ID! | |
name: String! | |
# Length of the starship, along the longest axis | |
length(unit: Unit = METRIC): Float | |
cost: Float | |
class: String | |
crew: Int | |
pilots: [Character] | |
} | |
union SearchResult = Human | Droid | Starship | |
`; | |
function getQuote() { | |
return airtable.getRandomRecord('Quotes'); | |
} | |
function getCharacter(id) { | |
return airtable.getRecordById('Characters', id); | |
} | |
function getCharacters() { | |
return airtable.getAllRecords('Characters'); | |
} | |
function getJedi() { | |
return airtable.getAllRecordsWithFilter('Characters', '{jediMaster}'); | |
} | |
function getCharacterOfType(id, type) { | |
return getCharacter(id).then(character => { | |
if (character.fields.type === type) { | |
return character; | |
} | |
return null; | |
}); | |
} | |
function getHuman(id) { | |
return getCharacterOfType(id, 'Human'); | |
} | |
function getDroid(id) { | |
return getCharacterOfType(id, 'Droid'); | |
} | |
function getStarship(id) { | |
console.log('getStarship', id); | |
return airtable.getRecordById('Starships', id); | |
} | |
function getStarships() { | |
return airtable.getAllRecords('Starships'); | |
} | |
function hanShotFirst() { | |
return airtable.getValue('hanShotFirst') | |
.then(hanShotFirst => airtable.getValue('greedoShotFirst') | |
.then(greedoShotFirst => hanShotFirst > greedoShotFirst) | |
); | |
} | |
function search(text) { | |
const characters = airtable.search('Characters', 'name', text); | |
const starships = airtable.search('Starships', 'name', text); | |
return Promise.all([characters, starships]).then(([characters, starships]) => { | |
return characters.concat(starships); | |
}); | |
} | |
function modifyValueByDelta(key, delta) { | |
return airtable.getValue(key).then(value => airtable.setValue(key, parseInt(value, 10) + delta)); | |
} | |
function createQuote(quote) { | |
return airtable.createRecord('Quotes', { | |
quote, | |
}); | |
} | |
function createCharacter(data) { | |
return airtable.createRecord('Characters', data); | |
} | |
function createHuman(human) { | |
return createCharacter({ | |
...human, | |
type: 'Human', | |
}); | |
} | |
function mapRecordsToFields(records) { | |
return records.map(mapRecordToFields); | |
} | |
function mapRecordToFields(record) { | |
const { | |
id, | |
fields, | |
} = record; | |
return { | |
id, | |
...fields, | |
}; | |
} | |
function mapReferences(references, getter) { | |
if (!references) { | |
return []; | |
} | |
return references.map(id => getter(id).then(mapRecordToFields)); | |
} | |
function metersToFeet(meters) { | |
return meters * 3.28084; | |
} | |
function kgToLB(kg) { | |
return kg * 2.20462; | |
} | |
const resolvers = { | |
Query: { | |
quote: () => getQuote().then(quote => quote.fields.quote), | |
character: (root, { id }) => getCharacter(id).then(mapRecordToFields), | |
characters: () => getCharacters().then(mapRecordsToFields), | |
jediMasters: () => getJedi().then(mapRecordsToFields), | |
human: (root, { id }) => getHuman(id).then(mapRecordToFields), | |
droid: (root, { id }) => getDroid(id).then(mapRecordToFields), | |
starship: (root, { id }) => getStarship(id).then(mapRecordToFields), | |
starships: () => getStarships().then(mapRecordsToFields), | |
search: (root, { text }) => search(text).then(mapRecordsToFields), | |
hanShotFirst: () => hanShotFirst(), | |
greedoShotFirst: () => hanShotFirst().then(hanShotFirst => !hanShotFirst), | |
}, | |
Mutation: { | |
hanShotFirst: () => modifyValueByDelta('hanShotFirst', 1), | |
greedoShotFirst: () => modifyValueByDelta('greedoShotFirst', 1), | |
createQuote: (root, { text }) => createQuote(text), | |
createHuman: (root, { human }) => createHuman(human), | |
}, | |
Character: { | |
__resolveType(data, context, info) { | |
return data.type; | |
}, | |
}, | |
Human: { | |
height: ({ height }, { unit }) => { | |
if (unit === 'IMPERIAL') { | |
return metersToFeet(height); | |
} | |
return height; | |
}, | |
mass: ({ mass }, { unit }) => { | |
if (unit === 'IMPERIAL') { | |
return kgToLB(mass); | |
} | |
return mass; | |
}, | |
jediMaster: ({ jediMaster }) => !!jediMaster, | |
friends: ({ friends }) => mapReferences(friends, getCharacter), | |
starships: ({ starships }) => mapReferences(starships, getStarship), | |
}, | |
Droid: { | |
height: ({ height }, { unit }) => { | |
if (unit === 'IMPERIAL') { | |
return metersToFeet(height); | |
} | |
return height; | |
}, | |
mass: ({ mass }, { unit }) => { | |
if (unit === 'IMPERIAL') { | |
return kgToLB(mass); | |
} | |
return mass; | |
}, | |
friends: ({ friends }) => mapReferences(friends, getCharacter), | |
}, | |
Starship: { | |
length: ({ length }, { unit }) => { | |
if (unit === 'IMPERIAL') { | |
return metersToFeet(length); | |
} | |
return length; | |
}, | |
pilots: ({ pilots }) => mapReferences(pilots, getCharacter), | |
}, | |
SearchResult: { | |
__resolveType(data, context, info) { | |
return data.type || 'Starship'; | |
}, | |
}, | |
}; | |
/** | |
* Finally, we construct our schema (whose starting query type is the query | |
* type we defined above) and export it. | |
*/ | |
export const schema = makeExecutableSchema({ | |
typeDefs: [schemaString], | |
resolvers, | |
}); | |
// Optional: Export a function to get context from the request. It accepts two | |
// parameters - headers (lowercased http headers) and secrets (secrets defined | |
// in secrets section). It must return an object (or a promise resolving to it). | |
export function context(headers, secrets) { | |
secrets.apiKey = secrets.apiKey; | |
secrets.baseId = secrets.baseId; | |
// Create the Airtable helper class instance with keys. | |
airtable = airtable || new AirtableHelper(secrets.apiKey, secrets.baseId); | |
return { | |
headers, | |
secrets, | |
}; | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment