Skip to content

Instantly share code, notes, and snippets.

@mkochendorfer
Created June 12, 2018 15:06
Show Gist options
  • Save mkochendorfer/20fdb893461848d3226e07209bc0abfc to your computer and use it in GitHub Desktop.
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
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