diff --git a/.gitignore b/.gitignore index ad46b30..f76c83d 100644 --- a/.gitignore +++ b/.gitignore @@ -59,3 +59,6 @@ typings/ # next.js build output .next + +# IDEs +.idea diff --git a/bin/cli.js b/bin/cli.js index 97db1c5..65fd693 100755 --- a/bin/cli.js +++ b/bin/cli.js @@ -1,8 +1,9 @@ #! /usr/bin/env node -const fs = require('fs') +const fs = require('fs'); const fetchSchema = require('../fetchSchema'); -var program = require('commander'); +const AirtableGraphQL = require('../index'); +const program = require('commander'); program .command('pull') @@ -17,6 +18,15 @@ program }).then(schema => { fs.writeFileSync('./schema.json', JSON.stringify(schema, null, 2), 'utf-8'); }) - }) + }); + +program + .command('start') + .option('-s --schema [path]', 'Path of the file containing Airtable schema', './schema.json') + .option('-p --port [port]', 'Port for the adapter to listen on', '8765') + .action(function (cmd) { + const api = new AirtableGraphQL(process.env.AIRTABLE_API_KEY, {schemaPath: cmd.schema}); + api.listen({port: cmd.port}); + }); program.parse(process.argv); \ No newline at end of file diff --git a/columns/number.js b/columns/number.js index f6def12..5b3bb5c 100644 --- a/columns/number.js +++ b/columns/number.js @@ -8,15 +8,15 @@ module.exports = airtableGraphql => { } if (column.options.format === "currency") { - return { type: graphql.GraphQLString }; + return { type: graphql.GraphQLFloat }; } if (column.options.format === "percent") { - return { type: graphql.GraphQLString }; + return { type: graphql.GraphQLInt }; } if (column.options.format === "duration") { - return { type: graphql.GraphQLString }; + return { type: graphql.GraphQLInt }; } return { type: graphql.GraphQLInt }; @@ -25,14 +25,6 @@ module.exports = airtableGraphql => { resolver: (column, api) => (obj) => { let value = obj.fields[column.name]; - if (column.options.format === "currency") { - value = `${column.options.symbol}${value}`; - } - - if (column.options.format === "percent") { - value = `${value}%`; - } - return value; } }); diff --git a/convertSchema.js b/convertSchema.js index d912741..5d36ef9 100644 --- a/convertSchema.js +++ b/convertSchema.js @@ -11,7 +11,68 @@ const { } = require("graphql"); const sanitize = require("./sanitize"); -module.exports = (airtableSchema, columnSupport) => { +function reformatSchema(airtableSchema) { + let tablesById = {}; + + airtableSchema.tables.map(table => { + tablesById[table.id] = table; + }); + + return { + tables: airtableSchema.tables.map(table => ({ + name: table.name, + columns: table.columns.filter(column => !['lookup', 'formula', 'rollup'].includes(column.type)).map(column => { + let options = {}; + + if (column.type === "select") { + options = { + choices: Object.values(column.typeOptions.choices).map(c => { + return c.name; + }) + }; + } + + if (column.type === 'foreignKey') { + let tableName; + if (column.foreignTable && column.foreignTable.name) { + tableName = column.foreignTable.name; + } + else { + tableName = tablesById[column.typeOptions.foreignTableId].name; + } + options = { + relationship: column.typeOptions.relationship, + table: tableName + } + } + + if (column.type === 'multiSelect') { + options = { + choices: Object.values(column.typeOptions.choices).map(c => { + return c.name + }) + } + } + + if (column.type === 'number') { + options = { + format: column.typeOptions.format, + symbol: column.typeOptions.symbol + } + } + + return { + name: column.name, + type: column.type, + options: options + } + }) + })) + }; +} + +function convertSchema(airtableSchema, columnSupport) { + const TYPES = []; const queryType = { @@ -61,4 +122,9 @@ module.exports = (airtableSchema, columnSupport) => { return new GraphQLSchema({ query: new GraphQLObjectType(queryType) }); +} + +module.exports = { + reformatSchema, + convertSchema }; diff --git a/fetchSchema.js b/fetchSchema.js index 58cc757..cf2378f 100644 --- a/fetchSchema.js +++ b/fetchSchema.js @@ -1,5 +1,29 @@ const puppeteer = require("puppeteer"); +function removeForeignTables(obj) { + console.log(typeof obj); + if (typeof obj === 'object') { + let result = {}; + for (let key in obj) { + if (obj.hasOwnProperty(key) && key !== 'foreignTable') { + result[key] = removeForeignTables(obj[key]); + } + } + if (Array.isArray(obj)) { + { + let arrayResult = []; + for (let i = 0; i < obj.length; i++) { + arrayResult.push(result[String(i)]); + } + result = arrayResult; + } + } + return result; + } else { + return obj; + } +} + module.exports = async config => { console.log("Fetching schema..."); const browser = await puppeteer.launch({args: ['--no-sandbox', '--disable-setuid-sandbox']}); @@ -10,55 +34,7 @@ module.exports = async config => { await page.keyboard.press("Enter"); await page.waitForNavigation(); const schema = await page.evaluate(() => { - return { - tables: application.tables.map(table => ({ - name: table.name, - columns: table.columns.map(column => { - let options = {}; - - if (column.type === "select") { - options = { - choices: Object.values(column.typeOptions.choices).map(c => { - return c.name; - }) - }; - } - - if (column.type === 'foreignKey') { - options = { - relationship: column.typeOptions.relationship, - table: column.foreignTable.name - } - } - - if (column.type === 'multiSelect') { - options = { - choices: Object.values(column.typeOptions.choices).map(c => { - return c.name - }) - } - } - - if (column.type === 'number') { - options = { - format: column.typeOptions.format - } - } - - if (column.type === 'number') { - options = { - format: column.typeOptions.format - } - } - - return { - name: column.name, - type: column.type, - options: options - } - }) - })) - }; + return removeForeignTables(application); }); await browser.close(); return { diff --git a/index.js b/index.js index ad61eae..4625e08 100644 --- a/index.js +++ b/index.js @@ -1,24 +1,26 @@ const { printSchema } = require("graphql"); const { ApolloServer } = require("apollo-server"); -const convertSchema = require("./convertSchema"); +const { convertSchema, reformatSchema } = require("./convertSchema"); const createResolvers = require("./createResolvers"); const airtable = require("airtable"); -const fs = require('fs') +const fs = require('fs'); class AirtableGraphQL { constructor(apiKey, config = {}) { this.columns = {}; airtable.configure({ apiKey }); - const schema = JSON.parse(fs.readFileSync(config.schemaPath || "./schema.json", "utf8")); - var normalizedPath = require("path").join(__dirname, "columns"); + const rawSchema = JSON.parse(fs.readFileSync(config.schemaPath || "./schema.json", "utf8")); + const schema = reformatSchema(rawSchema); + + const normalizedPath = require("path").join(__dirname, "columns"); require("fs") .readdirSync(normalizedPath) .forEach(file => { require("./columns/" + file)(this); }); - this.api = airtable.base(schema.id); + this.api = airtable.base(rawSchema.id); this.schema = convertSchema(schema, this.columns); this.resolvers = createResolvers( diff --git a/readme.md b/readme.md index 880abb0..111ae86 100644 --- a/readme.md +++ b/readme.md @@ -22,14 +22,12 @@ $ airtable-graphql pull --email=[your_email] --password=[your_password] --base=[ This will create a `schema.json` file which describes all of your bases tables and columns. -Create a file called `index.js` and add the following. +Use the `airtable-graphql start` command to start the adapter -```js -const AirtableGraphQL = require("airtable-graphql"); -api = new AirtableGraphQL("airtable_api_key"); -api.listen(); +``` +$ AIRTABLE_API_KEY={{api_key}} airtable-graphql start -s schema.json -p 8765 ``` -Run `node index.js` +Open your browser to localhost:8765 to start writing GraphQL queries against your Airtable data. That's it!