├── .editorconfig ├── .eslintignore ├── .gitignore ├── LICENSE ├── README.md ├── backends ├── index.js ├── mysql.js ├── postgres.js └── sqlite.js ├── cli.js ├── cli ├── args.js └── prompts.js ├── package.json ├── steps ├── ast-builders │ ├── config.js │ ├── exports-schema.js │ ├── exports.js │ ├── field-wrapper-function.js │ ├── node-definitions.js │ ├── object.js │ ├── query.js │ ├── resolve-map.js │ ├── resolver.js │ ├── schema-imports.js │ ├── schema-module.js │ ├── schema.js │ ├── templates │ │ ├── node-def-cjs.js │ │ └── node-def-es6.js │ ├── type-imports.js │ ├── type-index.js │ ├── type.js │ ├── use-strict.js │ └── variable.js ├── column-to-object.js ├── copy-templates.js ├── find-one-to-many-rels.js ├── find-references.js ├── generate-types.js ├── output-data.js ├── table-comment.js ├── table-list.js ├── table-structure.js ├── table-to-object.js └── update-package.js ├── templates ├── cjs │ ├── db.js │ ├── handlers │ │ ├── graphql.js │ │ └── schema-printer.js │ ├── package.json │ ├── server.js │ └── util │ │ ├── connection-resolver.js │ │ ├── entity-resolver.js │ │ ├── get-selection-set.js │ │ └── get-unaliased-name.js ├── es6 │ ├── .eslintrc │ ├── db.js │ ├── handlers │ │ ├── graphql.js │ │ └── schema-printer.js │ ├── index.js │ ├── package.json │ ├── server.js │ └── util │ │ ├── connection-resolver.js │ │ ├── entity-resolver.js │ │ ├── get-selection-set.js │ │ └── get-unaliased-name.js └── public │ ├── css │ ├── playground.css │ └── pure.css │ ├── favicon.ico │ ├── index.html │ └── js │ ├── JSXTransformer.js │ ├── ace.js │ ├── mode-json.js │ ├── playground.js │ ├── react.js │ ├── reqwest.min.js │ └── theme-monokai.js └── util ├── get-primary-key.js └── log.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | 9 | indent_style = space 10 | indent_size = 4 11 | end_of_line = lf 12 | charset = utf-8 13 | trim_trailing_whitespace = true 14 | insert_final_newline = true 15 | 16 | [*.md] 17 | trim_trailing_whitespace = false 18 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | templates/public/js 2 | steps/ast-builders/templates/node-def-es6.js 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | test.js 3 | 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2015, VaffelNinja AS 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to 7 | deal in the Software without restriction, including without limitation the 8 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 9 | sell copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | * The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 21 | IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # sql-to-graphql 2 | 3 | Generate GraphQL schemas and server based on SQL table structure. 4 | 5 | ## :warning: Unmaintained 6 | 7 | If you want to help maintain or develop this, let me know! 8 | 9 | ## What? 10 | 11 | [GraphQL](https://facebook.github.io/graphql/) is pretty awesome, but getting started can be difficult - especially if you are unfamiliar with the concepts it introduces. 12 | 13 | `sql-to-graphql` is a command-line utility that can help you get started. You give it the credentials to an SQL database (MySQL, PostgreSQL and SQLite currently) and it will inspect the tables it finds and do the following: 14 | 15 | - Generate GraphQL-types for each table (including resolvers) 16 | - Generate an HTTP-server based on Hapi that accepts GraphQL queries 17 | - Sets up a basic web-frontend that lets you query the server 18 | 19 | ## Disclaimer 20 | 21 | This utility is intended to help people get started with GraphQL. It is **NOT** intended to be used in production. 22 | 23 | ## Installation 24 | 25 | `npm install -g sql-to-graphql` 26 | 27 | ## Usage 28 | 29 | `sql2graphql [options]` 30 | 31 | ### Options: 32 | 33 | - `--relay`, `-r` - Generate Relay-style schema *`(boolean [default: false])`* 34 | - `--output-dir`, `-o` - Directory to use when generating app *`(string [required])`* 35 | - `--es6` - Use ES6 for generated code *`(boolean [default: false])`* 36 | - `--database`, `--db` - Database name *`(string [required])`* 37 | - `--db-filename` - Database filename, used for SQLite *`(string)`* 38 | - `--host`, `-h` - Hostname of database server *`(string [default: "localhost"])`* 39 | - `--port`, `-P` - Port number of database server *`(number)`* 40 | - `--user`, `-u` - Username to use when connecting *`(string [default: "root"])`* 41 | - `--password`, `-p` - Password to use when connecting *`(string [default: ""])`* 42 | - `--table`, `-t` - Tables to generate type schemas for *`(array [default: "*"])`* 43 | - `--backend`, `-b` - Type of database *`(string [default: "mysql"])`* 44 | - `--strip-suffix` - Remove a suffix from table names when generating types *`(array)`* 45 | - `--strip-prefix` - Remove a prefix from table names when generating types *`(array)`* 46 | - `--interactive`, `-i` - Interactive mode *`(boolean [default: false])`* 47 | - `--colors`, `-c` - Colorize the code output *`(boolean [default: false])`* 48 | - `--use-tabs` - Use tabs for indentation *`(boolean [default: false])`* 49 | - `--tab-width` - Width of tabs *`(number [default: 2])`* 50 | - `--quote` - Quote style (single/double) *`(string [default: "single"])`* 51 | - `--default-description` - The description to use for columns without a comment *`(string [default: "@TODO DESCRIBE ME"])`* 52 | - `--unaliased-primary-keys` Disable aliasing of primary key fields to "id" for each type *`(boolean [default: false])`* 53 | - `--help` - Show help *`(boolean)`* 54 | 55 | ## A note about connections 56 | 57 | At the moment, sql-to-graphql tries to guess connections between tables based on naming conventions. This is far from fool-proof, but here's how it works: 58 | 59 | Given you have a table called `users` (or `user`) and a table called `posts` (or `post`) and the `posts` table have a column called `user_id`, it will create a connection called `user` on the `Post`-type, giving you the `User` this post belongs to. It will also create a connection on the `User`-type named `posts`, which will return all the posts belonging to this `User`. 60 | 61 | How these connections look depends on the options used (namely, `--relay`). 62 | 63 | ## License 64 | 65 | MIT-licensed. See LICENSE. 66 | -------------------------------------------------------------------------------- /backends/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var adapters = { 4 | mysql: require('./mysql'), 5 | postgres: require('./postgres'), 6 | pg: require('./postgres'), 7 | sqlite: require('./sqlite') 8 | }; 9 | 10 | module.exports = function getBackendAdapter(db) { 11 | var backend = (db || '').toLowerCase(); 12 | return adapters[backend]; 13 | }; 14 | -------------------------------------------------------------------------------- /backends/mysql.js: -------------------------------------------------------------------------------- 1 | /* eslint camelcase: 0 */ 2 | 'use strict'; 3 | 4 | var knex = require('knex'); 5 | var pluck = require('lodash/collection/pluck'); 6 | var mapKeys = require('lodash/object/mapKeys'); 7 | var contains = require('lodash/collection/includes'); 8 | var camelCase = require('lodash/string/camelCase'); 9 | var undef; 10 | 11 | module.exports = function mysqlBackend(opts, callback) { 12 | var mysql = knex({ 13 | client: 'mysql', 14 | connection: opts 15 | }); 16 | 17 | process.nextTick(callback); 18 | 19 | return { 20 | getTables: function(tableNames, cb) { 21 | var matchAll = tableNames.length === 1 && tableNames[0] === '*'; 22 | 23 | mysql 24 | .select('table_name') 25 | .from('information_schema.tables') 26 | .where('table_schema', opts.database) 27 | .where('table_type', 'BASE TABLE') 28 | .catch(cb) 29 | .then(function(tbls) { 30 | tbls = pluck(tbls, 'table_name'); 31 | 32 | if (!matchAll) { 33 | tbls = tbls.filter(function(tbl) { 34 | return contains(tableNames, tbl); 35 | }); 36 | } 37 | 38 | cb(null, tbls); 39 | }); 40 | }, 41 | 42 | getTableComment: function(tableName, cb) { 43 | mysql 44 | .first('table_comment AS comment') 45 | .from('information_schema.tables') 46 | .where({ 47 | table_schema: opts.database, 48 | table_name: tableName 49 | }) 50 | .catch(cb) 51 | .then(function(info) { 52 | cb(null, info ? info.comment || undef : undef); 53 | }); 54 | }, 55 | 56 | getTableStructure: function(tableName, cb) { 57 | mysql 58 | .select([ 59 | 'table_name', 60 | 'column_name', 61 | 'ordinal_position', 62 | 'is_nullable', 63 | 'data_type', 64 | 'column_key', 65 | 'column_type', 66 | 'column_comment' 67 | ]) 68 | .from('information_schema.columns') 69 | .where({ 70 | table_schema: opts.database, 71 | table_name: tableName 72 | }) 73 | .orderBy('ordinal_position', 'asc') 74 | .catch(cb) 75 | .then(function(info) { 76 | cb(null, (info || []).map(camelCaseKeys)); 77 | }); 78 | }, 79 | 80 | hasDuplicateValues: function(table, column, cb) { 81 | mysql 82 | .count(column + ' as hasSameValues') 83 | .from(table) 84 | .groupBy(column) 85 | .having('hasSameValues', '>', 1) 86 | .limit(1) 87 | .catch(cb) 88 | .then(function(info) { 89 | cb(null, (info || []).length > 0); 90 | }); 91 | }, 92 | 93 | close: function(cb) { 94 | mysql.destroy(cb); 95 | } 96 | }; 97 | }; 98 | 99 | function camelCaseKeys(obj) { 100 | return mapKeys(obj, function(val, key) { 101 | return camelCase(key); 102 | }); 103 | } 104 | -------------------------------------------------------------------------------- /backends/postgres.js: -------------------------------------------------------------------------------- 1 | /* eslint camelcase: 0 */ 2 | 'use strict'; 3 | 4 | var knex = require('knex'); 5 | var uniq = require('lodash/array/uniq'); 6 | var pluck = require('lodash/collection/pluck'); 7 | var mapKeys = require('lodash/object/mapKeys'); 8 | var contains = require('lodash/collection/includes'); 9 | var camelCase = require('lodash/string/camelCase'); 10 | var undef; 11 | 12 | module.exports = function postgresBackend(opts, cb) { 13 | var up = ''; 14 | if (opts.user !== 'root' || opts.password) { 15 | up = opts.user + ':' + opts.password + '@'; 16 | } 17 | 18 | var port = opts.port === 3306 ? '' : ':' + opts.port; 19 | var conString = 'postgres://' + up + opts.host + port + '/' + opts.db; 20 | 21 | var pgSchemas = ['pg_catalog', 'pg_statistic', 'information_schema']; 22 | var pg = knex({ 23 | client: 'pg', 24 | connection: conString 25 | }); 26 | 27 | process.nextTick(cb); 28 | 29 | return { 30 | getTables: function(tableNames, tblCb) { 31 | var matchAll = tableNames.length === 1 && tableNames[0] === '*'; 32 | 33 | pg('information_schema.tables') 34 | .distinct('table_name') 35 | .where({ 36 | table_catalog: opts.db, 37 | table_type: 'BASE TABLE' 38 | }) 39 | .whereNotIn('table_schema', pgSchemas) 40 | .then(function(tbls) { 41 | tbls = pluck(tbls, 'table_name'); 42 | 43 | if (!matchAll) { 44 | tbls = tbls.filter(function(tbl) { 45 | return contains(tableNames, tbl); 46 | }); 47 | } 48 | 49 | tblCb(null, tbls); 50 | }) 51 | .catch(tblCb); 52 | }, 53 | 54 | getTableComment: function(tableName, tblCb) { 55 | var q = 'SELECT obj_description(?::regclass, \'pg_class\') AS table_comment'; 56 | pg.raw(q, [tableName]).then(function(info) { 57 | tblCb(null, ((info || [])[0] || {}).table_comment || undef); 58 | }).catch(tblCb); 59 | }, 60 | 61 | getTableStructure: function(tableName, tblCb) { 62 | pg.select('table_name', 'column_name', 'ordinal_position', 'is_nullable', 'data_type', 'udt_name') 63 | .from('information_schema.columns AS c') 64 | .where({ 65 | table_catalog: opts.db, 66 | table_name: tableName 67 | }) 68 | .whereNotIn('table_schema', pgSchemas) 69 | .orderBy('ordinal_position', 'asc') 70 | .catch(tblCb) 71 | .then(function(columns) { 72 | var enumQueries = uniq(columns.filter(function(col) { 73 | return col.data_type === 'USER-DEFINED'; 74 | }).map(function(col) { 75 | return 'enum_range(NULL::' + col.udt_name + ') AS ' + col.udt_name; 76 | })).join(', '); 77 | 78 | pg.raw('SELECT ' + (enumQueries || '1 AS "1"')).then(function(enumRes) { 79 | var enums = enumRes.rows[0]; 80 | 81 | var subQuery = pg.select('constraint_name') 82 | .from('information_schema.table_constraints') 83 | .where({ 84 | table_catalog: opts.db, 85 | table_name: tableName, 86 | constraint_type: 'PRIMARY KEY' 87 | }) 88 | .whereNotIn('table_schema', pgSchemas); 89 | 90 | pg.first('column_name AS primary_key') 91 | .from('information_schema.key_column_usage') 92 | .where({ 93 | table_catalog: opts.db, 94 | table_name: tableName, 95 | constraint_name: subQuery 96 | }) 97 | .whereNotIn('table_schema', pgSchemas) 98 | .then(function(pk) { 99 | var pkCol = (pk || {}).primary_key; 100 | columns = columns.map(function(col) { 101 | var isUserDefined = col.data_type === 'USER-DEFINED'; 102 | col.columnKey = col.column_name === pkCol ? 'PRI' : null; 103 | col.columnType = isUserDefined ? enums[col.udt_name] : null; 104 | return col; 105 | }); 106 | 107 | tblCb(null, (columns || []).map(camelCaseKeys)); 108 | }); 109 | }).catch(tblCb); 110 | }); 111 | }, 112 | 113 | hasDuplicateValues: function(table, column, callback) { 114 | pg 115 | .select(column) 116 | .from(table) 117 | .groupBy(column) 118 | .havingRaw('count(' + column + ') > 1') 119 | .limit(1) 120 | .catch(callback) 121 | .then(function(info) { 122 | callback(null, (info || []).length > 0); 123 | }); 124 | }, 125 | 126 | close: function(tblCb) { 127 | pg.destroy(tblCb); 128 | } 129 | }; 130 | }; 131 | 132 | function camelCaseKeys(obj) { 133 | return mapKeys(obj, function(val, key) { 134 | return camelCase(key); 135 | }); 136 | } 137 | -------------------------------------------------------------------------------- /backends/sqlite.js: -------------------------------------------------------------------------------- 1 | /* eslint camelcase: 0 */ 2 | 'use strict'; 3 | 4 | var knex = require('knex'); 5 | var knexString = require('knex/src/query/string'); 6 | var pluck = require('lodash/collection/pluck'); 7 | var contains = require('lodash/collection/includes'); 8 | 9 | module.exports = function sqliteBackend(opts, callback) { 10 | var sqlite = knex({ 11 | client: 'sqlite', 12 | connection: { 13 | filename: opts.dbFilename 14 | } 15 | }); 16 | 17 | process.nextTick(callback); 18 | 19 | return { 20 | getTables: function(tableNames, cb) { 21 | var matchAll = tableNames.length === 1 && tableNames[0] === '*'; 22 | sqlite 23 | .select('name') 24 | .from('sqlite_master') 25 | .whereIn('type', ['table', 'views']) 26 | .andWhere('name', 'not like', 'sqlite_%') 27 | .orderBy('id', 'asc') 28 | .catch(cb) 29 | .then(function(tbls) { 30 | tbls = pluck(tbls, 'name'); 31 | if (!matchAll) { 32 | tbls = tbls.filter(function(tbl) { 33 | return contains(tableNames, tbl); 34 | }); 35 | } 36 | cb(null, tbls); 37 | }); 38 | }, 39 | 40 | getTableComment: function(tableName, cb) { 41 | cb(null, ''); 42 | }, 43 | 44 | getTableStructure: function(tableName, cb) { 45 | var dbName = opts.database || 'main'; 46 | var rawSql = 'pragma ' 47 | + knexString.escape(dbName) 48 | + '.table_info(' 49 | + knexString.escape(tableName) 50 | + ');'; 51 | sqlite 52 | .raw(rawSql) 53 | .catch(cb) 54 | .then(function(info) { 55 | var structure = info.map(function(col) { 56 | var parensAndContents = /\(.+\)/; 57 | var sanitizedType = col.type 58 | .toLowerCase() 59 | .replace(parensAndContents, ''); 60 | return { 61 | columnName: col.name, 62 | isNullable: col.notnull !== 1, 63 | columnKey: col.pk === 1 ? 'PRI' : null, 64 | dataType: sanitizedType 65 | }; 66 | }); 67 | cb(null, structure); 68 | }); 69 | }, 70 | 71 | hasDuplicateValues: function(table, column, cb) { 72 | sqlite 73 | .count(column + ' as hasSameValues') 74 | .from(table) 75 | .groupBy(column) 76 | .having('hasSameValues', '>', 1) 77 | .limit(1) 78 | .catch(cb) 79 | .then(function(info) { 80 | cb(null, (info || []).length > 0); 81 | }); 82 | }, 83 | 84 | close: function(cb) { 85 | sqlite.destroy(cb); 86 | } 87 | }; 88 | }; 89 | -------------------------------------------------------------------------------- /cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 'use strict'; 3 | 4 | var path = require('path'); 5 | var async = require('async'); 6 | var opts = require('./cli/args'); 7 | var prompts = require('./cli/prompts'); 8 | var merge = require('lodash/object/merge'); 9 | var partial = require('lodash/function/partial'); 10 | var backends = require('./backends'); 11 | var mapValues = require('lodash/object/mapValues'); 12 | var steps = { 13 | getTables: require('./steps/table-list'), 14 | tableToObject: require('./steps/table-to-object'), 15 | findReferences: require('./steps/find-references'), 16 | findOneToManyReferences: require('./steps/find-one-to-many-rels'), 17 | generateTypes: require('./steps/generate-types'), 18 | outputData: require('./steps/output-data'), 19 | 20 | collect: { 21 | tableStructure: require('./steps/table-structure'), 22 | tableComments: require('./steps/table-comment') 23 | } 24 | }; 25 | 26 | // Force recast to throw away whitespace information 27 | opts.reuseWhitespace = false; 28 | 29 | if (opts.backend === 'sqlite' && !opts.database) { 30 | opts.database = 'main'; 31 | } 32 | 33 | if (opts.backend === 'sqlite' && !opts.dbFilename) { 34 | return bail(new Error('You need to specify a database filename (--db-filename) when using the \'sqlite\' backend')); 35 | } 36 | 37 | if (!opts.interactive && !opts.database) { 38 | return bail(new Error('You need to specify a database (--database)')); 39 | } 40 | 41 | if (!opts.interactive && !opts.outputDir) { 42 | return bail(new Error('You need to provide an output directory (--output-dir=) to generate an application')); 43 | } 44 | 45 | if (opts.interactive) { 46 | prompts.dbCredentials(opts, function(options) { 47 | opts = merge({}, opts, options); 48 | 49 | initOutputPath(); 50 | }); 51 | } else { 52 | initOutputPath(); 53 | } 54 | 55 | function initOutputPath() { 56 | if (opts.outputDir) { 57 | return initStyleOpts(); 58 | } 59 | 60 | prompts.outputPath(function(outPath) { 61 | opts.outputDir = outPath; 62 | 63 | initStyleOpts(); 64 | }); 65 | } 66 | 67 | function initStyleOpts() { 68 | if (!opts.interactive) { 69 | return instantiate(); 70 | } 71 | 72 | prompts.styleOptions(opts, function(options) { 73 | opts = options; 74 | 75 | instantiate(); 76 | }); 77 | } 78 | 79 | var adapter; 80 | function instantiate() { 81 | // Do we support the given backend? 82 | var backend = backends(opts.backend); 83 | if (!backend) { 84 | return bail(new Error('Database backend "' + opts.backend + '" not supported')); 85 | } 86 | 87 | // Instantiate the adapter for the given backend 88 | adapter = backend(opts, function(err) { 89 | bailOnError(err); 90 | 91 | setTimeout(getTables, 1000); 92 | }); 93 | } 94 | 95 | function getTables() { 96 | // Collect a list of available tables 97 | steps.getTables(adapter, opts, function(err, tableNames) { 98 | bailOnError(err); 99 | 100 | // If we're in interactive mode, prompt the user to select from the list of available tables 101 | if (opts.interactive) { 102 | return prompts.tableSelection(tableNames, onTablesSelected); 103 | } 104 | 105 | // Use the found tables (or a filtered set if --table is used) 106 | return onTablesSelected(tableNames); 107 | }); 108 | } 109 | 110 | // When tables have been selected, fetch data for those tables 111 | function onTablesSelected(tables) { 112 | // Assign partialed functions to make the code slightly more readable 113 | steps.collect = mapValues(steps.collect, function(method) { 114 | return partial(method, adapter, { tables: tables }); 115 | }); 116 | 117 | // Collect the data in parallel 118 | async.parallel(steps.collect, onTableDataCollected); 119 | } 120 | 121 | // When table data has been collected, build an object representation of them 122 | function onTableDataCollected(err, data) { 123 | bailOnError(err); 124 | 125 | var tableName, models = {}, model; 126 | for (tableName in data.tableStructure) { 127 | model = steps.tableToObject({ 128 | name: tableName, 129 | columns: data.tableStructure[tableName], 130 | comment: data.tableComments[tableName] 131 | }, opts); 132 | 133 | models[model.name] = model; 134 | } 135 | 136 | data.models = steps.findReferences(models); 137 | 138 | // Note: This mutates the models - sorry. PRs are welcome. 139 | steps.findOneToManyReferences(adapter, data.models, function(refErr) { 140 | if (refErr) { 141 | throw refErr; 142 | } 143 | 144 | data.types = steps.generateTypes(data, opts); 145 | 146 | adapter.close(); 147 | steps.outputData(data, opts, onDataOutput); 148 | }); 149 | } 150 | 151 | // When the data has been written to stdout/files 152 | function onDataOutput() { 153 | if (!opts.outputDir) { 154 | return; 155 | } 156 | 157 | if (opts.interactive) { 158 | console.log('\n\n\n'); 159 | } 160 | 161 | var dir = path.resolve(opts.outputDir); 162 | console.log('Demo app generated in ' + dir + '. To run:'); 163 | console.log('cd ' + dir); 164 | console.log('npm install'); 165 | console.log('npm start'); 166 | console.log(); 167 | console.log('Then point your browser at http://localhost:3000'); 168 | } 169 | 170 | function bail(err) { 171 | console.error(err.message ? err.message : err.toString()); 172 | process.exit(1); 173 | } 174 | 175 | function bailOnError(err) { 176 | if (err) { 177 | return bail(err); 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /cli/args.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = require('yargs') 4 | .usage('Usage: $0 [options]') 5 | .option('database', { 6 | alias: 'db', 7 | describe: 'Database name', 8 | type: 'string' 9 | }) 10 | .option('db-filename', { 11 | describe: 'full path to the sqlite db file', 12 | type: 'string' 13 | }) 14 | .option('host', { 15 | alias: 'h', 16 | describe: 'Hostname of database server', 17 | type: 'string', 18 | default: 'localhost' 19 | }) 20 | .option('port', { 21 | alias: 'P', 22 | describe: 'Port number of database server', 23 | type: 'number', 24 | default: 3306 25 | }) 26 | .option('user', { 27 | alias: 'u', 28 | describe: 'Username to use when connecting', 29 | type: 'string', 30 | default: 'root' 31 | }) 32 | .option('password', { 33 | alias: 'p', 34 | describe: 'Password to use when connecting', 35 | type: 'string', 36 | default: '' 37 | }) 38 | .option('table', { 39 | alias: 't', 40 | describe: 'Tables to generate type schemas for', 41 | type: 'array', 42 | default: '*' 43 | }) 44 | .option('backend', { 45 | alias: 'b', 46 | describe: 'Type of database (mysql, postgres, sqlite)', 47 | type: 'string', 48 | default: 'mysql' 49 | }) 50 | .option('relay', { 51 | alias: 'r', 52 | describe: 'Generate Relay-style schema', 53 | type: 'boolean' 54 | }) 55 | .option('strip-suffix', { 56 | describe: 'Remove a suffix from table names when generating', 57 | type: 'array' 58 | }) 59 | .option('strip-prefix', { 60 | describe: 'Remove a prefix from table names when generating', 61 | type: 'array' 62 | }) 63 | .option('interactive', { 64 | alias: 'i', 65 | describe: 'Interactive mode (prompt for confirmations)', 66 | type: 'boolean', 67 | default: false 68 | }) 69 | .option('output-dir', { 70 | alias: 'o', 71 | describe: 'Print output into separate files within the given directory', 72 | type: 'string' 73 | }) 74 | .option('es6', { 75 | describe: 'Output in ES6 format (const, import et all)', 76 | type: 'boolean', 77 | default: false 78 | }) 79 | .option('use-tabs', { 80 | describe: 'Use tabs for indentation', 81 | type: 'boolean', 82 | default: false 83 | }) 84 | .option('tab-width', { 85 | describe: 'Width of tabs', 86 | type: 'number', 87 | default: 4 88 | }) 89 | .option('quote', { 90 | describe: 'Quote style (single/double)', 91 | type: 'string', 92 | default: 'single' 93 | }) 94 | .option('default-description', { 95 | describe: 'The description to use for columns without a comment', 96 | type: 'string', 97 | default: '@TODO DESCRIBE ME' 98 | }) 99 | .option('unaliased-primary-keys', { 100 | describe: 'Disable aliasing of primary key fields to "id" for each type', 101 | type: 'boolean', 102 | default: false 103 | }) 104 | .help('help') 105 | .argv; 106 | -------------------------------------------------------------------------------- /cli/prompts.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var inquirer = require('inquirer'); 4 | var merge = require('lodash/object/merge'); 5 | 6 | module.exports = { 7 | outputPath: function(cb) { 8 | prompt({ 9 | message: 'Output path of the application', 10 | name: 'outputPath' 11 | }, cb); 12 | }, 13 | 14 | styleOptions: function(opts, cb) { 15 | return inquirer.prompt([{ 16 | type: 'confirm', 17 | message: 'Do you want to use babel + ES6?', 18 | name: 'es6', 19 | default: opts.es6 20 | }, { 21 | type: 'confirm', 22 | name: 'relay', 23 | message: 'Do you want a Relay-style GraphQL schema?', 24 | default: opts.relay 25 | }], function(answers) { 26 | cb(merge({}, opts, answers)); 27 | }); 28 | }, 29 | 30 | tableSelection: function(tables, cb) { 31 | prompt({ 32 | type: 'checkbox', 33 | message: 'Select tables to include', 34 | name: 'tables', 35 | choices: [ 36 | new inquirer.Separator('Available tables:') 37 | ].concat(tables.map(function(tbl) { 38 | return { name: tbl, checked: true }; 39 | })), 40 | validate: function(selected) { 41 | if (selected.length < 1) { 42 | return 'You must select at least one table.'; 43 | } 44 | return true; 45 | } 46 | }, cb); 47 | }, 48 | 49 | dbCredentials: function(opts, cb) { 50 | var prompts = []; 51 | 52 | if (!opts.backend || opts.backend === 'mysql') { 53 | prompts.push({ 54 | type: 'list', 55 | name: 'backend', 56 | message: 'What kind of database is it?', 57 | choices: ['mysql', 'postgres'], 58 | default: 'mysql' 59 | }); 60 | } 61 | 62 | if (!opts.host || opts.host === 'localhost') { 63 | prompts.push({ 64 | message: 'What is the hostname of your database server?', 65 | name: 'host', 66 | validate: Boolean, 67 | default: 'localhost' 68 | }); 69 | } 70 | 71 | if (!opts.database) { 72 | prompts.push({ 73 | message: 'What is the name of the database?', 74 | name: 'database', 75 | validate: Boolean 76 | }); 77 | } 78 | 79 | if (!opts.user || opts.user === 'root') { 80 | prompts.push({ 81 | message: 'What is the database username?', 82 | name: 'user', 83 | validate: Boolean, 84 | default: 'root' 85 | }); 86 | } 87 | 88 | if (!opts.password) { 89 | prompts.push({ 90 | message: 'What is the database password?', 91 | name: 'password', 92 | type: 'password' 93 | }); 94 | } 95 | 96 | inquirer.prompt(prompts, function(answers) { 97 | if (!opts.port || opts.port === 3306) { 98 | prompt({ 99 | message: 'What is the port number of the ' + answers.backend + ' database?', 100 | name: 'port', 101 | default: answers.backend === 'mysql' ? 3306 : 5432, 102 | validate: function(num) { 103 | num = parseInt(num, 10); 104 | return !isNaN(num) && num > 0; 105 | } 106 | }, function(port) { 107 | cb(merge({}, answers, { port: port })); 108 | }); 109 | } 110 | }); 111 | } 112 | }; 113 | 114 | function prompt(opts, cb) { 115 | return inquirer.prompt([opts], function(answers) { 116 | cb(answers[opts.name]); 117 | }); 118 | } 119 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sql-to-graphql", 3 | "version": "2.0.9", 4 | "description": "Generate a GraphQL API based on your SQL database structure", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "eslint ." 8 | }, 9 | "bin": { 10 | "sql2graphql": "./cli.js" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+ssh://git@github.com/vaffel/sql-to-graphql.git" 15 | }, 16 | "keywords": [ 17 | "generator", 18 | "graphql", 19 | "schemas", 20 | "sql", 21 | "type" 22 | ], 23 | "preferGlobal": true, 24 | "author": "Espen Hovlandsdal ", 25 | "contributors": [ 26 | { 27 | "name": "Kristoffer Brabrand", 28 | "email": "kristoffer@vaffel.ninja" 29 | } 30 | ], 31 | "license": "MIT", 32 | "bugs": { 33 | "url": "https://github.com/vaffel/sql-to-graphql/issues" 34 | }, 35 | "homepage": "https://github.com/vaffel/sql-to-graphql#readme", 36 | "dependencies": { 37 | "ast-types": "^0.8.4", 38 | "async": "^1.3.0", 39 | "copy-dir": "0.0.8", 40 | "graphql": "^0.2.5", 41 | "inquirer": "^0.9.0", 42 | "knex": "^0.8.6", 43 | "lodash": "^3.10.0", 44 | "mkdirp": "^0.5.1", 45 | "mysql": "^2.8.0", 46 | "pg": "^4.4.1", 47 | "pluralize": "^1.1.2", 48 | "recast": "^0.10.26", 49 | "sqlite3": "^3.1.0", 50 | "yargs": "^3.15.0" 51 | }, 52 | "devDependencies": { 53 | "eslint": "^1.2.1", 54 | "eslint-config-vaffel": "^2.0.0" 55 | }, 56 | "eslintConfig": { 57 | "extends": "vaffel", 58 | "rules": { 59 | "new-cap": 0, 60 | "no-console": 0, 61 | "no-process-exit": 0, 62 | "no-underscore-dangle": 0 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /steps/ast-builders/config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var b = require('ast-types').builders; 4 | var buildObject = require('./object'); 5 | var buildStrict = require('./use-strict'); 6 | var buildExports = require('./exports'); 7 | 8 | module.exports = function buildConfig(opts) { 9 | var program = [] 10 | .concat(buildStrict(opts)) 11 | .concat([buildExports(getConfigAst(opts), opts)]); 12 | 13 | return b.program(program); 14 | }; 15 | 16 | function getConfigAst(opts) { 17 | return b.objectExpression([ 18 | b.property('init', b.identifier('client'), b.literal(opts.backend)), 19 | b.property('init', b.identifier('connection'), buildObject({ 20 | host: opts.host, 21 | user: opts.user, 22 | password: opts.password, 23 | database: opts.db 24 | })) 25 | ]); 26 | } 27 | -------------------------------------------------------------------------------- /steps/ast-builders/exports-schema.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var b = require('ast-types').builders; 4 | var buildExport = require('./exports'); 5 | 6 | module.exports = function buildExportsQuery(opts) { 7 | return buildExport( 8 | b.identifier('schema'), 9 | opts 10 | ); 11 | }; 12 | -------------------------------------------------------------------------------- /steps/ast-builders/exports.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var b = require('ast-types').builders; 4 | 5 | module.exports = function buildExports(val, opts) { 6 | if (opts.es6) { 7 | return b.exportDeclaration(true, val); 8 | } 9 | 10 | return b.expressionStatement( 11 | b.assignmentExpression( 12 | '=', 13 | b.memberExpression( 14 | b.identifier('module'), 15 | b.identifier('exports'), 16 | false 17 | ), 18 | val 19 | ) 20 | ); 21 | }; 22 | -------------------------------------------------------------------------------- /steps/ast-builders/field-wrapper-function.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var b = require('ast-types').builders; 4 | 5 | module.exports = function buildFieldWrapperFunc(name, fields, opts) { 6 | if (opts.es6) { 7 | return b.arrowFunctionExpression([], fields); 8 | } 9 | 10 | return b.functionExpression( 11 | b.identifier('get' + name + 'Fields'), 12 | [], 13 | b.blockStatement([ 14 | b.returnStatement(fields) 15 | ]) 16 | ); 17 | }; 18 | -------------------------------------------------------------------------------- /steps/ast-builders/node-definitions.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var fs = require('fs'); 4 | var path = require('path'); 5 | var recast = require('recast'); 6 | 7 | module.exports = function buildNodeDefinitions(opts) { 8 | var tplPath = path.join(__dirname, 'templates'); 9 | var tpl = opts.es6 ? 'node-def-es6.js' : 'node-def-cjs.js'; 10 | var ast = recast.parse(fs.readFileSync(path.join(tplPath, tpl))); 11 | 12 | return ast; 13 | }; 14 | -------------------------------------------------------------------------------- /steps/ast-builders/object.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var b = require('ast-types').builders; 4 | var isPlainObject = require('lodash/lang/isPlainObject'); 5 | 6 | function buildObject(obj) { 7 | var fields = [], key; 8 | for (key in obj) { 9 | fields.push(b.property('init', b.literal(key), castValue(obj[key]))); 10 | } 11 | 12 | return b.objectExpression(fields); 13 | } 14 | 15 | function castValue(val) { 16 | if (isPlainObject(val)) { 17 | return buildObject(val); 18 | } else if (val === null) { 19 | return b.identifier('null'); 20 | } else if (typeof val === 'undefined') { 21 | return b.identifier('undefined'); 22 | } 23 | 24 | return b.literal(val); 25 | } 26 | 27 | module.exports = buildObject; 28 | -------------------------------------------------------------------------------- /steps/ast-builders/query.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var b = require('ast-types').builders; 4 | var buildResolver = require('./resolver'); 5 | var getPrimaryKey = require('../../util/get-primary-key'); 6 | var typeMap = { 7 | string: 'GraphQLString', 8 | integer: 'GraphQLInt', 9 | float: 'GraphQLFloat' 10 | }; 11 | 12 | module.exports = function buildQuery(type, data, opts) { 13 | var model = data.models[type.name]; 14 | var primaryKey = getPrimaryKey(model) || {}; 15 | var keyName = primaryKey.name; 16 | var keyType = typeMap[primaryKey.type]; 17 | 18 | return b.objectExpression([ 19 | b.property('init', b.identifier('type'), b.identifier(type.varName)), 20 | b.property('init', b.identifier('args'), b.objectExpression(keyName ? [ 21 | b.property('init', b.identifier('id'), b.objectExpression([ 22 | b.property('init', b.identifier('name'), b.literal(keyName)), 23 | b.property('init', b.identifier('type'), b.newExpression( 24 | b.identifier('GraphQLNonNull'), 25 | [b.identifier(keyType)] 26 | )) 27 | ])) 28 | ] : [])) 29 | ].concat(opts.outputDir ? [b.property( 30 | 'init', 31 | b.identifier('resolve'), 32 | buildResolver(type) 33 | )] : [])); 34 | }; 35 | -------------------------------------------------------------------------------- /steps/ast-builders/resolve-map.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var b = require('ast-types').builders; 4 | var reduce = require('lodash/collection/reduce'); 5 | var buildObject = require('./object'); 6 | var buildStrict = require('./use-strict'); 7 | var buildVariable = require('./variable'); 8 | var getPrimaryKey = require('../../util/get-primary-key'); 9 | 10 | module.exports = function buildResolveMap(data, opts) { 11 | var map = getResolveMap(data.models, opts); 12 | 13 | var program = [] 14 | .concat(buildStrict(opts)) 15 | .concat(buildImports(opts)) 16 | .concat(buildResolveMapExport(map, opts)) 17 | .concat(buildConnectionsVar(opts)) 18 | .concat(buildExportedFunc(opts, getTypeRegisterAst(), 'registerType')) 19 | .concat(buildExportedFunc(opts, getTypeGetterAst(), 'getType')); 20 | 21 | if (opts.relay) { 22 | program = program.concat(buildExportedFunc( 23 | opts, 24 | getConnectionGetterAst(opts), 25 | 'getConnection' 26 | )); 27 | } 28 | 29 | return b.program(program); 30 | }; 31 | 32 | function getResolveMap(models, opts) { 33 | var resolveMap = {}; 34 | for (var type in models) { 35 | resolveMap[models[type].name] = getTypeResolver(models[type], opts); 36 | } 37 | 38 | return buildObject(resolveMap, opts); 39 | } 40 | 41 | function getTypeResolver(model) { 42 | return { 43 | name: model.name, 44 | table: model.table, 45 | primaryKey: getPrimaryKeyArg(model), 46 | aliases: model.aliasedFields, 47 | referenceMap: getRefFieldMapArg(model), 48 | listReferences: getListRefFieldMapArg(model) 49 | }; 50 | } 51 | 52 | function getListRefFieldMapArg(model) { 53 | return reduce(model.listReferences, buildReferenceMap, {}); 54 | } 55 | 56 | function getRefFieldMapArg(model) { 57 | return reduce(model.references, buildReferenceMap, {}); 58 | } 59 | 60 | function getPrimaryKeyArg(model) { 61 | var primaryKey = getPrimaryKey(model); 62 | return primaryKey ? primaryKey.originalName : null; 63 | } 64 | 65 | function buildReferenceMap(refMap, reference) { 66 | refMap[reference.field] = reference.refField; 67 | return refMap; 68 | } 69 | 70 | function buildResolveMapExport(map, opts) { 71 | if (opts.es6) { 72 | return [b.exportDeclaration(false, buildVariable('resolveMap', map, opts.es6))]; 73 | } 74 | 75 | return [ 76 | buildVariable('resolveMap', map, opts.es6), 77 | b.expressionStatement( 78 | b.assignmentExpression( 79 | '=', 80 | b.memberExpression( 81 | b.identifier('exports'), 82 | b.identifier('resolveMap'), 83 | false 84 | ), 85 | b.identifier('resolveMap') 86 | ) 87 | ) 88 | ]; 89 | } 90 | 91 | function buildExportedFunc(opts, ast, name) { 92 | var func = (opts.es6 ? b.functionDeclaration : b.functionExpression)( 93 | b.identifier(name), 94 | [b.identifier('type')], 95 | b.blockStatement(ast) 96 | ); 97 | 98 | if (opts.es6) { 99 | return [b.exportDeclaration(false, func)]; 100 | } 101 | 102 | return [ 103 | b.expressionStatement(b.assignmentExpression( 104 | '=', 105 | b.memberExpression( 106 | b.identifier('exports'), 107 | b.identifier(name), 108 | false 109 | ), 110 | func 111 | )) 112 | ]; 113 | } 114 | 115 | function buildConnectionsVar(opts) { 116 | if (!opts.relay) { 117 | return []; 118 | } 119 | 120 | return [buildVariable('connections', b.objectExpression([]), opts.es6)]; 121 | } 122 | 123 | function buildImports(opts) { 124 | if (!opts.relay) { 125 | return []; 126 | } 127 | 128 | if (opts.es6) { 129 | return [ 130 | b.importDeclaration( 131 | [importSpecifier('connectionDefinitions')], 132 | b.literal('graphql-relay') 133 | ) 134 | ]; 135 | } 136 | 137 | return [ 138 | b.variableDeclaration('var', 139 | [b.variableDeclarator( 140 | b.identifier('GraphQLRelay'), 141 | b.callExpression( 142 | b.identifier('require'), 143 | [b.literal('graphql-relay')] 144 | ) 145 | )] 146 | ), 147 | 148 | b.variableDeclaration('var', 149 | [b.variableDeclarator( 150 | b.identifier('connectionDefinitions'), 151 | b.memberExpression( 152 | b.identifier('GraphQLRelay'), 153 | b.identifier('connectionDefinitions'), 154 | false 155 | ) 156 | )] 157 | ) 158 | ]; 159 | } 160 | 161 | function importSpecifier(name) { 162 | return { 163 | type: 'ImportSpecifier', 164 | id: { 165 | type: 'Identifier', 166 | name: name 167 | }, 168 | name: null 169 | }; 170 | } 171 | 172 | /* eslint quote-props: 0 */ 173 | 174 | // Can't be bothered trying replicate this with the builder right now :x 175 | // On a totally unrelated side-note, someone should make a thing that takes 176 | // AST and generates code that can build it with the `ast-types` builder 177 | // Then you would be able to easily make parts of it dynamic etc. Much like 178 | // this project, I guess. 179 | function getTypeRegisterAst() { 180 | return [ 181 | { 182 | 'type': 'IfStatement', 183 | 'test': { 184 | 'type': 'UnaryExpression', 185 | 'operator': '!', 186 | 'argument': { 187 | 'type': 'MemberExpression', 188 | 'computed': true, 189 | 'object': { 190 | 'type': 'Identifier', 191 | 'name': 'resolveMap' 192 | }, 193 | 'property': { 194 | 'type': 'MemberExpression', 195 | 'computed': false, 196 | 'object': { 197 | 'type': 'Identifier', 198 | 'name': 'type' 199 | }, 200 | 'property': { 201 | 'type': 'Identifier', 202 | 'name': 'name' 203 | } 204 | } 205 | }, 206 | 'prefix': true 207 | }, 208 | 'consequent': { 209 | 'type': 'BlockStatement', 210 | 'body': [ 211 | { 212 | 'type': 'ThrowStatement', 213 | 'argument': { 214 | 'type': 'NewExpression', 215 | 'callee': { 216 | 'type': 'Identifier', 217 | 'name': 'Error' 218 | }, 219 | 'arguments': [ 220 | { 221 | 'type': 'BinaryExpression', 222 | 'operator': '+', 223 | 'left': { 224 | 'type': 'BinaryExpression', 225 | 'operator': '+', 226 | 'left': { 227 | 'type': 'Literal', 228 | 'value': 'Cannot register type "' 229 | }, 230 | 'right': { 231 | 'type': 'MemberExpression', 232 | 'computed': false, 233 | 'object': { 234 | 'type': 'Identifier', 235 | 'name': 'type' 236 | }, 237 | 'property': { 238 | 'type': 'Identifier', 239 | 'name': 'name' 240 | } 241 | } 242 | }, 243 | 'right': { 244 | 'type': 'Literal', 245 | 'value': '" - resolve map does not exist for that type' 246 | } 247 | } 248 | ] 249 | } 250 | } 251 | ] 252 | }, 253 | 'alternate': null 254 | }, 255 | { 256 | 'type': 'ExpressionStatement', 257 | 'expression': { 258 | 'type': 'AssignmentExpression', 259 | 'operator': '=', 260 | 'left': { 261 | 'type': 'MemberExpression', 262 | 'computed': false, 263 | 'object': { 264 | 'type': 'MemberExpression', 265 | 'computed': true, 266 | 'object': { 267 | 'type': 'Identifier', 268 | 'name': 'resolveMap' 269 | }, 270 | 'property': { 271 | 'type': 'MemberExpression', 272 | 'computed': false, 273 | 'object': { 274 | 'type': 'Identifier', 275 | 'name': 'type' 276 | }, 277 | 'property': { 278 | 'type': 'Identifier', 279 | 'name': 'name' 280 | } 281 | } 282 | }, 283 | 'property': { 284 | 'type': 'Identifier', 285 | 'name': 'type' 286 | } 287 | }, 288 | 'right': { 289 | 'type': 'Identifier', 290 | 'name': 'type' 291 | } 292 | } 293 | } 294 | ]; 295 | } 296 | 297 | function getTypeGetterAst() { 298 | return [{ 299 | 'type': 'IfStatement', 300 | 'test': { 301 | 'type': 'LogicalExpression', 302 | 'operator': '||', 303 | 'left': { 304 | 'type': 'UnaryExpression', 305 | 'operator': '!', 306 | 'argument': { 307 | 'type': 'MemberExpression', 308 | 'computed': true, 309 | 'object': { 310 | 'type': 'Identifier', 311 | 'name': 'resolveMap' 312 | }, 313 | 'property': { 314 | 'type': 'Identifier', 315 | 'name': 'type' 316 | } 317 | }, 318 | 'prefix': true 319 | }, 320 | 'right': { 321 | 'type': 'UnaryExpression', 322 | 'operator': '!', 323 | 'argument': { 324 | 'type': 'MemberExpression', 325 | 'computed': false, 326 | 'object': { 327 | 'type': 'MemberExpression', 328 | 'computed': true, 329 | 'object': { 330 | 'type': 'Identifier', 331 | 'name': 'resolveMap' 332 | }, 333 | 'property': { 334 | 'type': 'Identifier', 335 | 'name': 'type' 336 | } 337 | }, 338 | 'property': { 339 | 'type': 'Identifier', 340 | 'name': 'type' 341 | } 342 | }, 343 | 'prefix': true 344 | } 345 | }, 346 | 'consequent': { 347 | 'type': 'BlockStatement', 348 | 'body': [ 349 | { 350 | 'type': 'ThrowStatement', 351 | 'argument': { 352 | 'type': 'NewExpression', 353 | 'callee': { 354 | 'type': 'Identifier', 355 | 'name': 'Error' 356 | }, 357 | 'arguments': [ 358 | { 359 | 'type': 'BinaryExpression', 360 | 'operator': '+', 361 | 'left': { 362 | 'type': 'BinaryExpression', 363 | 'operator': '+', 364 | 'left': { 365 | 'type': 'Literal', 366 | 'value': 'No type registered for type \'' 367 | }, 368 | 'right': { 369 | 'type': 'Identifier', 370 | 'name': 'type' 371 | } 372 | }, 373 | 'right': { 374 | 'type': 'Literal', 375 | 'value': '\'' 376 | } 377 | } 378 | ] 379 | } 380 | } 381 | ] 382 | }, 383 | 'alternate': null 384 | }, 385 | { 386 | 'type': 'ReturnStatement', 387 | 'argument': { 388 | 'type': 'MemberExpression', 389 | 'computed': false, 390 | 'object': { 391 | 'type': 'MemberExpression', 392 | 'computed': true, 393 | 'object': { 394 | 'type': 'Identifier', 395 | 'name': 'resolveMap' 396 | }, 397 | 'property': { 398 | 'type': 'Identifier', 399 | 'name': 'type' 400 | } 401 | }, 402 | 'property': { 403 | 'type': 'Identifier', 404 | 'name': 'type' 405 | } 406 | } 407 | }]; 408 | } 409 | 410 | function getConnectionGetterAst(opts) { 411 | return [ 412 | { 413 | 'type': 'IfStatement', 414 | 'test': { 415 | 'type': 'UnaryExpression', 416 | 'operator': '!', 417 | 'argument': { 418 | 'type': 'MemberExpression', 419 | 'computed': true, 420 | 'object': { 421 | 'type': 'Identifier', 422 | 'name': 'connections' 423 | }, 424 | 'property': { 425 | 'type': 'Identifier', 426 | 'name': 'type' 427 | } 428 | }, 429 | 'prefix': true 430 | }, 431 | 'consequent': { 432 | 'type': 'BlockStatement', 433 | 'body': [ 434 | { 435 | 'type': 'ExpressionStatement', 436 | 'expression': { 437 | 'type': 'AssignmentExpression', 438 | 'operator': '=', 439 | 'left': { 440 | 'type': 'MemberExpression', 441 | 'computed': true, 442 | 'object': { 443 | 'type': 'Identifier', 444 | 'name': 'connections' 445 | }, 446 | 'property': { 447 | 'type': 'Identifier', 448 | 'name': 'type' 449 | } 450 | }, 451 | 'right': { 452 | 'type': 'MemberExpression', 453 | 'computed': false, 454 | 'object': { 455 | 'type': 'CallExpression', 456 | 'callee': { 457 | 'type': 'Identifier', 458 | 'name': 'connectionDefinitions' 459 | }, 460 | 'arguments': [ 461 | { 462 | 'type': 'ObjectExpression', 463 | 'properties': [ 464 | { 465 | 'type': 'Property', 466 | 'key': { 467 | 'type': 'Identifier', 468 | 'name': 'name' 469 | }, 470 | 'value': { 471 | 'type': 'Identifier', 472 | 'name': 'type' 473 | }, 474 | 'kind': 'init', 475 | 'method': false, 476 | 'shorthand': false, 477 | 'computed': false 478 | }, 479 | { 480 | 'type': 'Property', 481 | 'key': { 482 | 'type': 'Identifier', 483 | 'name': 'nodeType' 484 | }, 485 | 'value': { 486 | 'type': 'CallExpression', 487 | 'callee': opts.es6 ? { 488 | 'type': 'Identifier', 489 | 'name': 'getType' 490 | } : { 491 | 'type': 'MemberExpression', 492 | 'computed': false, 493 | 'object': { 494 | 'type': 'Identifier', 495 | 'name': 'exports' 496 | }, 497 | 'property': { 498 | 'type': 'Identifier', 499 | 'name': 'getType' 500 | } 501 | }, 502 | 'arguments': [ 503 | { 504 | 'type': 'Identifier', 505 | 'name': 'type' 506 | } 507 | ] 508 | }, 509 | 'kind': 'init', 510 | 'method': false, 511 | 'shorthand': false, 512 | 'computed': false 513 | } 514 | ] 515 | } 516 | ] 517 | }, 518 | 'property': { 519 | 'type': 'Identifier', 520 | 'name': 'connectionType' 521 | } 522 | } 523 | } 524 | } 525 | ] 526 | }, 527 | 'alternate': null 528 | }, 529 | { 530 | 'type': 'ReturnStatement', 531 | 'argument': { 532 | 'type': 'MemberExpression', 533 | 'computed': true, 534 | 'object': { 535 | 'type': 'Identifier', 536 | 'name': 'connections' 537 | }, 538 | 'property': { 539 | 'type': 'Identifier', 540 | 'name': 'type' 541 | } 542 | } 543 | } 544 | ]; 545 | } 546 | -------------------------------------------------------------------------------- /steps/ast-builders/resolver.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var b = require('ast-types').builders; 4 | 5 | module.exports = function buildResolver(model) { 6 | return b.callExpression( 7 | b.identifier('getEntityResolver'), 8 | [b.literal(model.name)] 9 | ); 10 | }; 11 | -------------------------------------------------------------------------------- /steps/ast-builders/schema-imports.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var b = require('ast-types').builders; 4 | var uniq = require('lodash/array/uniq'); 5 | var flatten = require('lodash/array/flatten'); 6 | var reduce = require('lodash/collection/reduce'); 7 | var pluck = require('lodash/collection/pluck'); 8 | var getPrimaryKey = require('../../util/get-primary-key'); 9 | var typeMap = { 10 | string: 'GraphQLString', 11 | integer: 'GraphQLInt', 12 | float: 'GraphQLFloat' 13 | }; 14 | 15 | function generateSchemaImports(data, opts) { 16 | var imports = []; 17 | if (!opts.relay) { 18 | imports = uniq( 19 | flatten(pluck(data.types, 'imports')).concat( 20 | pluck(data.types, 'varName') 21 | ) 22 | ); 23 | } 24 | 25 | var types = imports.filter(not(isGraphQL)); 26 | var graphql = [ 27 | 'GraphQLObjectType', 28 | 'GraphQLSchema' 29 | ]; 30 | 31 | if (!opts.relay) { 32 | graphql = graphql 33 | .concat(['GraphQLNonNull']) 34 | .concat(reduceGraphQLTypes(data.models)); 35 | } 36 | 37 | return opts.es6 ? 38 | es6Import(graphql, types, opts) : 39 | cjsImport(graphql, types, opts); 40 | } 41 | 42 | function reduceGraphQLTypes(models) { 43 | return reduce(models, function(types, model) { 44 | var primaryKey = getPrimaryKey(model) || {}; 45 | var keyType = typeMap[primaryKey.type]; 46 | 47 | if (keyType && types.indexOf(keyType) === -1) { 48 | types.push(keyType); 49 | } 50 | 51 | return types; 52 | }, []); 53 | } 54 | 55 | function cjsImport(graphql, types, opts) { 56 | var declarations = [ 57 | b.variableDeclaration('var', 58 | [b.variableDeclarator( 59 | b.identifier('getEntityResolver'), 60 | b.callExpression( 61 | b.identifier('require'), 62 | [b.literal('./util/entity-resolver')] 63 | ) 64 | )] 65 | ) 66 | ]; 67 | 68 | if (graphql.length) { 69 | declarations.push(b.variableDeclaration('var', 70 | [b.variableDeclarator( 71 | b.identifier('GraphQL'), 72 | b.callExpression( 73 | b.identifier('require'), 74 | [b.literal('graphql')] 75 | ) 76 | )] 77 | )); 78 | } 79 | 80 | if (opts.relay) { 81 | declarations.push(b.variableDeclaration('var', 82 | [b.variableDeclarator( 83 | b.identifier('Node'), 84 | b.callExpression( 85 | b.identifier('require'), 86 | [b.literal('./types/Node')] 87 | ) 88 | )] 89 | )); 90 | } 91 | 92 | declarations = declarations.concat(types.map(function(item) { 93 | return b.variableDeclaration('var', 94 | [b.variableDeclarator( 95 | b.identifier(item), 96 | b.callExpression( 97 | b.identifier('require'), 98 | [b.literal('./types/' + item)] 99 | ) 100 | )] 101 | ); 102 | })); 103 | 104 | declarations.push(b.variableDeclaration('var', 105 | [b.variableDeclarator( 106 | b.identifier('resolveMap'), 107 | b.callExpression( 108 | b.identifier('require'), 109 | [b.literal('./resolve-map')] 110 | ) 111 | )] 112 | )); 113 | 114 | declarations.push(b.variableDeclaration('var', 115 | [b.variableDeclarator( 116 | b.identifier('types'), 117 | b.callExpression( 118 | b.identifier('require'), 119 | [b.literal('./types')] 120 | ) 121 | )] 122 | )); 123 | 124 | declarations = declarations.concat(graphql.map(function(item) { 125 | return b.variableDeclaration('var', 126 | [b.variableDeclarator( 127 | b.identifier(item), 128 | b.memberExpression( 129 | b.identifier('GraphQL'), 130 | b.identifier(item), 131 | false 132 | ) 133 | )] 134 | ); 135 | })); 136 | 137 | if (opts.relay) { 138 | declarations.push( 139 | b.variableDeclaration('var', 140 | [b.variableDeclarator( 141 | b.identifier('nodeField'), 142 | b.memberExpression( 143 | b.identifier('Node'), 144 | b.identifier('nodeField'), 145 | false 146 | ) 147 | )] 148 | ) 149 | ); 150 | } 151 | 152 | declarations.push( 153 | b.variableDeclaration('var', 154 | [b.variableDeclarator( 155 | b.identifier('registerType'), 156 | b.memberExpression( 157 | b.identifier('resolveMap'), 158 | b.identifier('registerType'), 159 | false 160 | ) 161 | )] 162 | ) 163 | ); 164 | 165 | return declarations; 166 | } 167 | 168 | function es6Import(graphql, types, opts) { 169 | var declarations = [ 170 | b.importDeclaration( 171 | [importSpecifier('getEntityResolver', true)], 172 | b.literal('./util/entity-resolver') 173 | ) 174 | ]; 175 | 176 | if (graphql.length) { 177 | declarations.push(b.importDeclaration( 178 | graphql.map(importSpecifier), 179 | b.literal('graphql') 180 | )); 181 | } 182 | 183 | if (opts.relay && opts.isFromSchema) { 184 | declarations.push(b.importDeclaration( 185 | [importSpecifier('nodeField')], 186 | b.literal('./types/Node') 187 | )); 188 | } 189 | 190 | declarations = declarations.concat( 191 | types.map(function(item) { 192 | return b.importDeclaration( 193 | [importSpecifier(item, true)], 194 | b.literal('./types/' + item) 195 | ); 196 | }) 197 | ); 198 | 199 | declarations.push(b.importDeclaration( 200 | [importSpecifier('registerType')], 201 | b.literal('./resolve-map') 202 | )); 203 | 204 | declarations.push(b.importDeclaration( 205 | [{ 206 | type: 'ImportNamespaceSpecifier', 207 | id: { 208 | type: 'Identifier', 209 | name: 'types' 210 | } 211 | }], 212 | b.literal('./types') 213 | )); 214 | 215 | return declarations; 216 | } 217 | 218 | // Couldn't figure out b.importSpecifier 219 | function importSpecifier(name, def) { 220 | return { 221 | type: def === true ? 'ImportDefaultSpecifier' : 'ImportSpecifier', 222 | id: { 223 | type: 'Identifier', 224 | name: name 225 | }, 226 | name: null 227 | }; 228 | } 229 | 230 | function isGraphQL(name) { 231 | return name.indexOf('GraphQL') === 0; 232 | } 233 | 234 | function not(fn) { 235 | return function(item) { 236 | return !fn(item); 237 | }; 238 | } 239 | 240 | module.exports = generateSchemaImports; 241 | -------------------------------------------------------------------------------- /steps/ast-builders/schema-module.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var b = require('ast-types').builders; 4 | var buildSchema = require('./schema'); 5 | var buildStrict = require('./use-strict'); 6 | var buildSchemaExport = require('./exports-schema'); 7 | var buildSchemaImports = require('./schema-imports'); 8 | 9 | module.exports = function buildSchemaModule(data, opts) { 10 | var program = [] 11 | .concat(buildStrict(opts)) 12 | .concat(buildSchemaImports(data, opts)) 13 | .concat([buildSchema(data, opts)]) 14 | .concat([buildSchemaExport(data, opts)]); 15 | 16 | return b.program(program); 17 | }; 18 | -------------------------------------------------------------------------------- /steps/ast-builders/schema.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var camelCase = require('lodash/string/camelCase'); 4 | var map = require('lodash/collection/map'); 5 | var b = require('ast-types').builders; 6 | var buildVar = require('./variable'); 7 | var buildQuery = require('./query'); 8 | var buildFieldWrapperFunction = require('./field-wrapper-function'); 9 | 10 | module.exports = function(data, opts) { 11 | var queryFields = []; 12 | if (opts.relay) { 13 | queryFields.push(b.property( 14 | 'init', 15 | b.identifier('node'), 16 | b.identifier('nodeField') 17 | )); 18 | } else { 19 | queryFields = map(data.types, function(type) { 20 | return b.property( 21 | 'init', 22 | b.identifier(camelCase(type.name)), 23 | buildQuery(type, data, opts) 24 | ); 25 | }); 26 | } 27 | 28 | return buildVar('schema', 29 | b.newExpression( 30 | b.identifier('GraphQLSchema'), 31 | [b.objectExpression([ 32 | b.property( 33 | 'init', 34 | b.identifier('query'), 35 | b.newExpression( 36 | b.identifier('GraphQLObjectType'), 37 | [b.objectExpression([ 38 | b.property('init', b.identifier('name'), b.literal('RootQueryType')), 39 | b.property('init', b.identifier('fields'), buildFieldWrapperFunction( 40 | 'RootQuery', 41 | b.objectExpression(queryFields), 42 | opts 43 | )) 44 | ])] 45 | ) 46 | ) 47 | ])] 48 | ) 49 | ); 50 | }; 51 | -------------------------------------------------------------------------------- /steps/ast-builders/templates/node-def-cjs.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var relay = require('graphql-relay'); 4 | var getEntityResolver = require('../util/entity-resolver'); 5 | 6 | var nodeDefinitions = relay.nodeDefinitions; 7 | var fromGlobalId = relay.fromGlobalId; 8 | 9 | var node = nodeDefinitions( 10 | function(globalId, ast) { 11 | var gid = fromGlobalId(globalId); 12 | return getEntityResolver(gid.type)(null, { id: gid.id }, ast); 13 | }, 14 | function(data) { 15 | return data.__type; 16 | } 17 | ); 18 | 19 | exports.nodeInterface = node.nodeInterface; 20 | exports.nodeField = node.nodeField; 21 | -------------------------------------------------------------------------------- /steps/ast-builders/templates/node-def-es6.js: -------------------------------------------------------------------------------- 1 | import {nodeDefinitions, fromGlobalId} from 'graphql-relay'; 2 | import getEntityResolver from '../util/entity-resolver'; 3 | 4 | const {nodeInterface, nodeField} = nodeDefinitions( 5 | function(globalId, ast) { 6 | let {type, id} = fromGlobalId(globalId); 7 | return getEntityResolver(type)(null, { id }, ast); 8 | }, 9 | data => data.__type 10 | ); 11 | 12 | export { nodeInterface, nodeField }; 13 | -------------------------------------------------------------------------------- /steps/ast-builders/type-imports.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var b = require('ast-types').builders; 4 | 5 | function buildTypeImports(type, opts) { 6 | var imports = type.imports; 7 | var others = imports.filter(not(isGraphQL)); 8 | var graphql = ['GraphQLObjectType'].concat(imports.filter(isGraphQL)); 9 | 10 | return opts.es6 ? 11 | es6Import(graphql, others, opts) : 12 | cjsImport(graphql, others, opts); 13 | } 14 | 15 | function cjsImport(graphql, others, opts) { 16 | // Require the entity resolver 17 | var declarations = [ 18 | b.variableDeclaration('var', 19 | [b.variableDeclarator( 20 | b.identifier('getEntityResolver'), 21 | b.callExpression( 22 | b.identifier('require'), 23 | [b.literal('../util/entity-resolver')] 24 | ) 25 | )] 26 | ) 27 | ]; 28 | 29 | if (opts.relay && graphql.indexOf('GraphQLList') >= 0) { 30 | declarations.push( 31 | b.variableDeclaration('var', 32 | [b.variableDeclarator( 33 | b.identifier('getConnectionResolver'), 34 | b.callExpression( 35 | b.identifier('require'), 36 | [b.literal('../util/connection-resolver')] 37 | ) 38 | )] 39 | ) 40 | ); 41 | } 42 | 43 | // Require the resolve map 44 | declarations.push( 45 | b.variableDeclaration('var', 46 | [b.variableDeclarator( 47 | b.identifier('resolveMap'), 48 | b.callExpression( 49 | b.identifier('require'), 50 | [b.literal('../resolve-map')] 51 | ) 52 | )] 53 | ) 54 | ); 55 | 56 | // Require the graphql library 57 | if (graphql.length) { 58 | declarations.push(b.variableDeclaration('var', 59 | [b.variableDeclarator( 60 | b.identifier('GraphQL'), 61 | b.callExpression( 62 | b.identifier('require'), 63 | [b.literal('graphql')] 64 | ) 65 | )] 66 | )); 67 | } 68 | 69 | // Relay needs the Node interface along with the globalIdField 70 | if (opts.relay) { 71 | declarations.push(b.variableDeclaration('var', 72 | [b.variableDeclarator( 73 | b.identifier('GraphQLRelay'), 74 | b.callExpression( 75 | b.identifier('require'), 76 | [b.literal('graphql-relay')] 77 | ) 78 | )] 79 | )); 80 | 81 | declarations.push(b.variableDeclaration('var', 82 | [b.variableDeclarator( 83 | b.identifier('Node'), 84 | b.callExpression( 85 | b.identifier('require'), 86 | [b.literal('./Node')] 87 | ) 88 | )] 89 | )); 90 | } 91 | 92 | // Other types 93 | declarations = declarations.concat(others.map(function(item) { 94 | return b.variableDeclaration('var', 95 | [b.variableDeclarator( 96 | b.identifier(item), 97 | b.callExpression( 98 | b.identifier('require'), 99 | [b.literal('./' + item)] 100 | ) 101 | )] 102 | ); 103 | })); 104 | 105 | // Declare local variables for the different GraphQL types 106 | declarations = declarations.concat(graphql.map(function(item) { 107 | return b.variableDeclaration('var', 108 | [b.variableDeclarator( 109 | b.identifier(item), 110 | b.memberExpression( 111 | b.identifier('GraphQL'), 112 | b.identifier(item), 113 | false 114 | ) 115 | )] 116 | ); 117 | })); 118 | 119 | // We need the nodeInterface from the Node type and the globalIdField from relay 120 | // If we have any list references, we also will need the connection stuff 121 | if (opts.relay) { 122 | declarations.push( 123 | b.variableDeclaration('var', 124 | [b.variableDeclarator( 125 | b.identifier('nodeInterface'), 126 | b.memberExpression( 127 | b.identifier('Node'), 128 | b.identifier('nodeInterface'), 129 | false 130 | ) 131 | )] 132 | ) 133 | ); 134 | 135 | declarations.push( 136 | b.variableDeclaration('var', 137 | [b.variableDeclarator( 138 | b.identifier('globalIdField'), 139 | b.memberExpression( 140 | b.identifier('GraphQLRelay'), 141 | b.identifier('globalIdField'), 142 | false 143 | ) 144 | )] 145 | ) 146 | ); 147 | 148 | if (graphql.indexOf('GraphQLList') >= 0) { 149 | declarations.push(b.variableDeclaration('var', 150 | [b.variableDeclarator( 151 | b.identifier('connectionArgs'), 152 | b.memberExpression( 153 | b.identifier('GraphQLRelay'), 154 | b.identifier('connectionArgs'), 155 | false 156 | ) 157 | )] 158 | )); 159 | } 160 | } 161 | 162 | declarations.push( 163 | b.variableDeclaration('var', 164 | [b.variableDeclarator( 165 | b.identifier('getType'), 166 | b.memberExpression( 167 | b.identifier('resolveMap'), 168 | b.identifier('getType'), 169 | false 170 | ) 171 | )] 172 | ) 173 | ); 174 | 175 | declarations.push( 176 | b.variableDeclaration('var', 177 | [b.variableDeclarator( 178 | b.identifier('registerType'), 179 | b.memberExpression( 180 | b.identifier('resolveMap'), 181 | b.identifier('registerType'), 182 | false 183 | ) 184 | )] 185 | ) 186 | ); 187 | 188 | if (opts.relay && graphql.indexOf('GraphQLList') >= 0) { 189 | declarations.push( 190 | b.variableDeclaration('var', 191 | [b.variableDeclarator( 192 | b.identifier('getConnection'), 193 | b.memberExpression( 194 | b.identifier('resolveMap'), 195 | b.identifier('getConnection'), 196 | false 197 | ) 198 | )] 199 | ) 200 | ); 201 | } 202 | 203 | return declarations; 204 | } 205 | 206 | function es6Import(graphql, others, opts) { 207 | var resolveImports = [ 208 | importSpecifier('getType'), 209 | importSpecifier('registerType') 210 | ]; 211 | 212 | if (opts.relay && graphql.indexOf('GraphQLList') >= 0) { 213 | resolveImports.push(importSpecifier('getConnection')); 214 | } 215 | 216 | var declarations = [ 217 | b.importDeclaration( 218 | [importSpecifier('getEntityResolver', true)], 219 | b.literal('../util/entity-resolver') 220 | ) 221 | ]; 222 | 223 | if (opts.relay && graphql.indexOf('GraphQLList') >= 0) { 224 | declarations.push( 225 | b.importDeclaration( 226 | [importSpecifier('getConnectionResolver', true)], 227 | b.literal('../util/connection-resolver') 228 | ) 229 | ); 230 | } 231 | 232 | declarations.push( 233 | b.importDeclaration( 234 | resolveImports, 235 | b.literal('../resolve-map') 236 | ) 237 | ); 238 | 239 | declarations.push(b.importDeclaration( 240 | graphql.map(importSpecifier), 241 | b.literal('graphql') 242 | )); 243 | 244 | if (opts.relay) { 245 | var relayStuff = ['globalIdField']; 246 | 247 | if (graphql.indexOf('GraphQLList') >= 0) { 248 | relayStuff.push('connectionArgs'); 249 | } 250 | 251 | declarations.push(b.importDeclaration( 252 | relayStuff.map(function(thing) { 253 | return importSpecifier(thing); 254 | }), 255 | b.literal('graphql-relay') 256 | )); 257 | 258 | declarations.push(b.importDeclaration( 259 | [importSpecifier('nodeInterface')], 260 | b.literal('./Node') 261 | )); 262 | } 263 | 264 | declarations = declarations.concat( 265 | others.map(function(item) { 266 | return importDeclaration(item, opts); 267 | }) 268 | ); 269 | 270 | return declarations; 271 | } 272 | 273 | // Couldn't figure out b.importSpecifier 274 | function importSpecifier(name, def) { 275 | return { 276 | type: def === true ? 'ImportDefaultSpecifier' : 'ImportSpecifier', 277 | id: { 278 | type: 'Identifier', 279 | name: name 280 | }, 281 | name: null 282 | }; 283 | } 284 | 285 | function importDeclaration(item) { 286 | return b.importDeclaration( 287 | [importSpecifier(item, true)], 288 | b.literal('./' + item) 289 | ); 290 | } 291 | 292 | function isGraphQL(name) { 293 | return name.indexOf('GraphQL') === 0; 294 | } 295 | 296 | function not(fn) { 297 | return function(item) { 298 | return !fn(item); 299 | }; 300 | } 301 | 302 | module.exports = buildTypeImports; 303 | -------------------------------------------------------------------------------- /steps/ast-builders/type-index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var b = require('ast-types').builders; 4 | var buildStrict = require('./use-strict'); 5 | var pluck = require('lodash/collection/pluck'); 6 | var buildExports = require('./exports'); 7 | 8 | module.exports = function buildTypeIndex(data, opts) { 9 | var types = pluck(data.types, 'varName'); 10 | var theImports = types.map(opts.es6 ? es6Import : cjsImport); 11 | var theExports = (opts.es6 ? es6Export : cjsExport)(types); 12 | 13 | var program = [] 14 | .concat(buildStrict(opts)) 15 | .concat(theImports) 16 | .concat(theExports); 17 | 18 | return b.program(program); 19 | }; 20 | 21 | function cjsImport(type) { 22 | return b.variableDeclaration('var', 23 | [b.variableDeclarator( 24 | b.identifier(type), 25 | b.callExpression( 26 | b.identifier('require'), 27 | [b.literal('./' + type)] 28 | ) 29 | )] 30 | ); 31 | } 32 | 33 | function es6Import(type) { 34 | return b.importDeclaration( 35 | [{ 36 | type: 'ImportDefaultSpecifier', 37 | id: { 38 | type: 'Identifier', 39 | name: type 40 | }, 41 | name: null 42 | }], 43 | b.literal('./' + type) 44 | ); 45 | } 46 | 47 | function es6Export(types) { 48 | return { 49 | type: 'ExportDeclaration', 50 | default: false, 51 | declaration: null, 52 | specifiers: types.map(function(type) { 53 | return { 54 | type: 'ExportSpecifier', 55 | id: { 56 | type: 'Identifier', 57 | name: type 58 | }, 59 | name: null 60 | }; 61 | }) 62 | }; 63 | } 64 | 65 | function cjsExport(types) { 66 | return buildExports(b.objectExpression(types.map(function(type) { 67 | return b.property('init', b.literal(type), b.identifier(type)); 68 | })), { es6: false }); 69 | } 70 | -------------------------------------------------------------------------------- /steps/ast-builders/type.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var b = require('ast-types').builders; 4 | var buildTypeImports = require('./type-imports'); 5 | var buildExports = require('./exports'); 6 | 7 | module.exports = function buildType(type, opts) { 8 | var imports = buildTypeImports(type, opts); 9 | var typeAst = type.ast; 10 | var typeExport = [buildExports(b.identifier(type.varName), opts)]; 11 | var register = [b.expressionStatement( 12 | b.callExpression( 13 | b.identifier('registerType'), 14 | [b.identifier(type.varName)] 15 | ) 16 | )]; 17 | 18 | return b.program([] 19 | .concat(imports) 20 | .concat(typeAst) 21 | .concat(register) 22 | .concat(typeExport) 23 | ); 24 | }; 25 | -------------------------------------------------------------------------------- /steps/ast-builders/use-strict.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var b = require('ast-types').builders; 4 | 5 | module.exports = function buildStrict(options) { 6 | if (options.es6) { 7 | return []; 8 | } 9 | 10 | return b.expressionStatement( 11 | b.literal('use strict') 12 | ); 13 | }; 14 | -------------------------------------------------------------------------------- /steps/ast-builders/variable.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var b = require('ast-types').builders; 4 | 5 | module.exports = function buildVar(name, val, es6) { 6 | var varStyle = es6 ? 'const' : 'var'; 7 | return b.variableDeclaration(varStyle, [ 8 | b.variableDeclarator( 9 | b.identifier(name), 10 | val 11 | ) 12 | ]); 13 | }; 14 | -------------------------------------------------------------------------------- /steps/column-to-object.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var merge = require('lodash/object/merge'); 4 | var indexBy = require('lodash/collection/indexBy'); 5 | var camelCase = require('lodash/string/camelCase'); 6 | var capitalize = require('lodash/string/capitalize'); 7 | var generics = ['type'], undef; 8 | 9 | function columnToObject(col, opts) { 10 | var column = merge({ 11 | name: getColName(col, opts), 12 | originalName: col.columnName, 13 | description: col.columnComment || undef, 14 | isNullable: col.isNullable === 'YES', 15 | isPrimaryKey: col.columnKey === 'PRI' 16 | }, getType(col)); 17 | 18 | if (column.isPrimaryKey && !opts.unaliasedPrimaryKeys) { 19 | column.name = 'id'; 20 | } 21 | 22 | return column; 23 | } 24 | 25 | function getColName(col) { 26 | if (generics.indexOf(col.columnName.toLowerCase()) > -1) { 27 | return camelCase(col.tableName) + capitalize(camelCase(col.columnName)); 28 | } 29 | 30 | return camelCase(col.columnName); 31 | } 32 | 33 | function getEnumValueMap(col) { 34 | var values = col.columnType 35 | .replace(/^enum\((.*)\)/, '$1') 36 | .replace(/^\{|\}$/g, '') 37 | .split(',') 38 | .map(unquote) 39 | .map(function(val) { 40 | return { 41 | value: val, 42 | description: undef 43 | }; 44 | }); 45 | 46 | return indexBy(values, 'value'); 47 | } 48 | 49 | function unquote(str) { 50 | return str.replace(/^'+|'+$/g, ''); 51 | } 52 | 53 | function getType(col) { 54 | switch (col.dataType) { 55 | // Dates represented as strings 56 | case 'time': 57 | case 'date': 58 | case 'datetime': 59 | // pg 60 | case 'timestamp with time zone': 61 | 62 | // Buffers represented as strings 63 | case 'bit': 64 | case 'blob': 65 | case 'tinyblob': 66 | case 'longblob': 67 | case 'mediumblob': 68 | case 'binary': 69 | case 'varbinary': 70 | 71 | // Numbers that may exceed float precision, repesent as string 72 | case 'bigint': 73 | case 'decimal': 74 | case 'numeric': 75 | case 'geometry': 76 | case 'bigserial': 77 | 78 | // Network addresses represented as strings 79 | case 'cidr': 80 | case 'inet': 81 | case 'macaddr': 82 | 83 | // Strings 84 | case 'set': 85 | case 'char': 86 | case 'text': 87 | case 'uuid': 88 | case 'varchar': 89 | case 'nvarchar': 90 | case 'tinytext': 91 | case 'longtext': 92 | case 'character': 93 | case 'mediumtext': 94 | // pg 95 | case 'character varying': 96 | case 'jsonb': 97 | return { type: 'string' }; 98 | 99 | // Integers 100 | case 'int': 101 | case 'year': 102 | case 'serial': 103 | case 'integer': 104 | case 'tinyint': 105 | case 'smallint': 106 | case 'mediumint': 107 | case 'timestamp': 108 | return { type: 'integer' }; 109 | 110 | // Floats 111 | case 'real': 112 | case 'float': 113 | case 'double': 114 | case 'double precision': 115 | return { type: 'float' }; 116 | 117 | // Booleans 118 | case 'boolean': 119 | return { type: 'boolean' }; 120 | 121 | // Enum special case 122 | case 'enum': 123 | 124 | // As well as postgres enums 125 | case 'USER-DEFINED': 126 | return { 127 | type: 'enum', 128 | values: getEnumValueMap(col) 129 | }; 130 | default: 131 | console.log(col); 132 | throw new Error('Type "' + col.dataType + '" not recognized'); 133 | } 134 | } 135 | 136 | module.exports = columnToObject; 137 | -------------------------------------------------------------------------------- /steps/copy-templates.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var path = require('path'); 4 | var copy = require('copy-dir'); 5 | 6 | module.exports = function copyTemplates(type, toDir) { 7 | var fromDir = path.resolve(path.join(__dirname, '..', 'templates', type)); 8 | var publicDir = path.resolve(path.join(__dirname, '..', 'templates', 'public')); 9 | copy.sync(fromDir, toDir); 10 | copy.sync(publicDir, path.join(toDir, 'public')); 11 | }; 12 | -------------------------------------------------------------------------------- /steps/find-one-to-many-rels.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var async = require('async'); 4 | var after = require('lodash/function/after'); 5 | var find = require('lodash/collection/find'); 6 | var camelCase = require('lodash/string/camelCase'); 7 | var capitalize = require('lodash/string/capitalize'); 8 | var pluralize = require('pluralize'); 9 | 10 | module.exports = function findOneToManyRelationships(adapter, models, callback) { 11 | var tasks = Object.keys(models).reduce(function(tasklist, model) { 12 | tasklist[model] = function(cb) { 13 | findRelationships(adapter, models[model], models, cb); 14 | }; 15 | return tasklist; 16 | }, {}); 17 | 18 | async.parallel(tasks, callback); 19 | }; 20 | 21 | function findRelationships(adapter, model, models, callback) { 22 | if (!model.references.length) { 23 | return setImmediate(callback, null, model); 24 | } 25 | 26 | var done = after(model.references.length, callback); 27 | model.references.forEach(function(ref) { 28 | var referenceColumn = getUnaliasedField(ref.refField, model); 29 | adapter.hasDuplicateValues(model.table, referenceColumn, function(err, hasDupes) { 30 | if (err) { 31 | return callback(err); 32 | } 33 | 34 | if (!hasDupes) { 35 | return done(null, model); 36 | } 37 | 38 | var reverseRefs = ref.model.listReferences; 39 | var refName = camelCase(pluralize(model.name)); 40 | var description = pluralize(model.name) + ' belonging to this ' + ref.model.name; 41 | if (find(reverseRefs, { field: refName }) || ref.model.fields[refName]) { 42 | // @TODO find a better name algo resolve mechanism 43 | // `thread_id` should naturally be `threads`, while `old_thread_id` should be.. something else 44 | refName += capitalize(camelCase(referenceColumn)).replace(/Id$/, ''); 45 | description += '..? (' + referenceColumn + ')'; 46 | } 47 | 48 | reverseRefs.push({ 49 | model: model, 50 | description: description, 51 | field: refName, 52 | refField: referenceColumn, 53 | isList: true 54 | }); 55 | 56 | done(null, model); 57 | }); 58 | }); 59 | } 60 | 61 | function getUnaliasedField(field, model) { 62 | for (var unaliased in model.aliasedFields) { 63 | if (model.aliasedFields[unaliased] === field) { 64 | return unaliased; 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /steps/find-references.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var filter = require('lodash/collection/filter'); 4 | var snakeCase = require('lodash/string/snakeCase'); 5 | var capitalize = require('lodash/string/capitalize'); 6 | 7 | function findReferences(models) { 8 | for (var type in models) { 9 | models[type].references = findReferencesForModel(models[type], models); 10 | models[type].listReferences = []; 11 | } 12 | 13 | return models; 14 | } 15 | 16 | function findReferencesForModel(model, models) { 17 | // Find columns that end with "Id" 18 | var refs = filter(model.fields, isIdColumn); 19 | var fields = Object.keys(model.fields); 20 | 21 | // Filter the columns that have a corresponding model 22 | return refs.reduce(function(references, col) { 23 | var colName = col.name.substr(0, col.name.length - 2).replace(/^parent/, ''); 24 | var parts = snakeCase(colName).split('_'), fieldName; 25 | 26 | do { 27 | var name = parts.map(capitalize).join(''); 28 | 29 | // Do we have a match for this? 30 | if (models[name]) { 31 | fieldName = col.name.replace(/Id$/, ''); 32 | 33 | // If we collide with a different field name, add a "Ref"-suffix 34 | if (fields.indexOf(fieldName) !== -1) { 35 | fieldName += 'Ref'; 36 | } 37 | 38 | references.push({ 39 | model: models[name], 40 | field: fieldName, 41 | refField: col.name 42 | }); 43 | 44 | return references; 45 | } 46 | 47 | parts.shift(); 48 | } while (parts.length > 0); 49 | 50 | return references; 51 | }, []); 52 | } 53 | 54 | function isIdColumn(col) { 55 | return !col.isPrimaryKey && col.name.substr(-2) === 'Id'; 56 | } 57 | 58 | module.exports = findReferences; 59 | -------------------------------------------------------------------------------- /steps/generate-types.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var find = require('lodash/collection/find'); 4 | var where = require('lodash/collection/where'); 5 | var pluck = require('lodash/collection/pluck'); 6 | var capitalize = require('lodash/string/capitalize'); 7 | var snakeCase = require('lodash/string/snakeCase'); 8 | var b = require('ast-types').builders; 9 | var buildVar = require('./ast-builders/variable'); 10 | var buildResolver = require('./ast-builders/resolver'); 11 | var buildFieldWrapperFunction = require('./ast-builders/field-wrapper-function'); 12 | var enumRegex = /^[_a-zA-Z][_a-zA-Z0-9]*$/; 13 | 14 | var typeMap = { 15 | id: 'GraphQLID', 16 | string: 'GraphQLString', 17 | integer: 'GraphQLInt', 18 | float: 'GraphQLFloat', 19 | boolean: 'GraphQLBoolean' 20 | }; 21 | 22 | function exclude(arr, val) { 23 | return arr.filter(function(type) { 24 | return type !== val; 25 | }); 26 | } 27 | 28 | function generateTypes(data, opts) { 29 | var types = {}, typesUsed; 30 | for (var typeName in data.models) { 31 | typesUsed = []; 32 | types[typeName] = generateType(typeName, data.models[typeName]); 33 | types[typeName].varName = typeName + 'Type'; 34 | types[typeName].name = typeName; 35 | types[typeName].imports = exclude(typesUsed, types[typeName].varName); 36 | } 37 | 38 | return types; 39 | 40 | function addUsedType(type) { 41 | if (typesUsed.indexOf(type) === -1) { 42 | typesUsed.push(type); 43 | } 44 | } 45 | 46 | function generateType(name, model) { 47 | var fields = [], refs; 48 | 49 | for (var fieldName in model.fields) { 50 | fields.push(generateField(model.fields[fieldName], null, name, model)); 51 | 52 | refs = where(model.references, { refField: fieldName }); 53 | for (var i = 0; i < refs.length; i++) { 54 | fields.push(generateReferenceField(model.fields[fieldName], refs[i])); 55 | } 56 | } 57 | 58 | model.listReferences.forEach(function(ref) { 59 | fields.push(generateListReferenceField(ref)); 60 | }); 61 | 62 | var interfaces = opts.relay && b.property( 63 | 'init', 64 | b.identifier('interfaces'), 65 | b.arrayExpression([b.identifier('nodeInterface')]) 66 | ); 67 | 68 | var typeDeclaration = b.objectExpression([ 69 | b.property('init', b.identifier('name'), b.literal(name)), 70 | generateDescription(model.description), 71 | b.property( 72 | 'init', 73 | b.identifier('fields'), 74 | buildFieldWrapperFunction(name, b.objectExpression(fields), opts) 75 | ) 76 | ].concat(interfaces || [])); 77 | 78 | return { 79 | ast: buildVar( 80 | name + 'Type', 81 | b.newExpression( 82 | b.identifier('GraphQLObjectType'), 83 | [typeDeclaration] 84 | ), 85 | opts.es6 86 | ) 87 | }; 88 | } 89 | 90 | function generateDescription(description) { 91 | return b.property( 92 | 'init', 93 | b.identifier('description'), 94 | b.literal(description || opts.defaultDescription) 95 | ); 96 | } 97 | 98 | function generateField(field, type, parentName, parentModel) { 99 | if (field.isPrimaryKey && opts.relay) { 100 | return b.property('init', b.identifier('id'), b.callExpression( 101 | b.identifier('globalIdField'), 102 | [b.literal(parentName)] 103 | )); 104 | } 105 | 106 | var props = [ 107 | b.property('init', b.identifier('type'), type || getType(field, parentModel)), 108 | generateDescription(field.description) 109 | ]; 110 | 111 | if (field.resolve) { 112 | props.push(b.property('init', b.identifier('resolve'), field.resolve)); 113 | } 114 | 115 | if (field.args) { 116 | props.push(field.args); 117 | } 118 | 119 | return b.property( 120 | 'init', 121 | b.identifier(field.name), 122 | b.objectExpression(props) 123 | ); 124 | } 125 | 126 | function generateReferenceField(refField, refersTo) { 127 | var description = opts.defaultDescription; 128 | if (refersTo.field.indexOf('parent') === 0) { 129 | description += ' (parent ' + refersTo.model.name.toLowerCase() + ')'; 130 | } else { 131 | description += ' (reference)'; 132 | } 133 | 134 | return generateField({ 135 | name: refersTo.field, 136 | description: description, 137 | resolve: buildResolver(refersTo.model, refField.originalName) 138 | }, b.callExpression( 139 | b.identifier('getType'), 140 | [b.literal(refersTo.model.name)] 141 | )); 142 | } 143 | 144 | function generateListReferenceField(reference) { 145 | addUsedType('GraphQLList'); 146 | 147 | if (opts.relay) { 148 | return generateRelayReferenceField(reference); 149 | } 150 | 151 | return generateField({ 152 | name: reference.field, 153 | description: reference.description || opts.defaultDescription + ' (reference)', 154 | resolve: buildResolver(reference.model, find(reference.model.fields, { isPrimaryKey: true }).originalName), 155 | args: generateLimitOffsetArgs() 156 | }, b.newExpression( 157 | b.identifier('GraphQLList'), 158 | [b.callExpression(b.identifier('getType'), [b.literal(reference.model.name)])] 159 | )); 160 | } 161 | 162 | function generateRelayReferenceField(reference) { 163 | return generateField({ 164 | name: reference.field, 165 | description: reference.description || opts.defaultDescription + ' (reference)', 166 | resolve: b.callExpression( 167 | b.identifier('getConnectionResolver'), 168 | [b.literal(reference.model.name)] 169 | ), 170 | args: b.property('init', b.identifier('args'), b.identifier('connectionArgs')) 171 | }, b.callExpression( 172 | b.identifier('getConnection'), 173 | [b.literal(reference.model.name)] 174 | )); 175 | } 176 | 177 | function generateLimitOffsetArgs() { 178 | return b.property('init', b.identifier('args'), b.objectExpression([ 179 | b.property('init', b.identifier('limit'), b.objectExpression([ 180 | b.property('init', b.identifier('name'), b.literal('limit')), 181 | b.property('init', b.identifier('type'), b.identifier('GraphQLInt')) 182 | ])), 183 | b.property('init', b.identifier('offset'), b.objectExpression([ 184 | b.property('init', b.identifier('name'), b.literal('offset')), 185 | b.property('init', b.identifier('type'), b.identifier('GraphQLInt')) 186 | ])) 187 | ])); 188 | } 189 | 190 | function getType(field, model) { 191 | if (field.type === 'enum') { 192 | addUsedType('GraphQLEnumType'); 193 | return getEnum(field, model); 194 | } 195 | 196 | var type = typeMap[field.type]; 197 | var identifier = b.identifier(type); 198 | 199 | addUsedType(type); 200 | 201 | if (!field.isNullable) { 202 | addUsedType('GraphQLNonNull'); 203 | return b.newExpression(b.identifier('GraphQLNonNull'), [identifier]); 204 | } 205 | 206 | return identifier; 207 | } 208 | 209 | function getEnum(field, model) { 210 | var values = [], enumKey, enumValue; 211 | 212 | var fieldValues = field.values; 213 | var numVals = pluck(fieldValues, 'value').map(Number); 214 | if (numVals.length === 2 && numVals.indexOf(0) > -1 && numVals.indexOf(1) > -1) { 215 | fieldValues = { 216 | TRUE: { value: '1' }, 217 | FALSE: { value: '0' } 218 | }; 219 | } 220 | 221 | for (var name in fieldValues) { 222 | enumValue = fieldValues[name]; 223 | enumKey = snakeCase(name).toUpperCase(); 224 | 225 | if (!enumKey.match(enumRegex)) { 226 | enumKey = 'ENUM_' + enumKey; 227 | } 228 | 229 | values.push(b.property( 230 | 'init', 231 | b.literal(enumKey), 232 | b.objectExpression([ 233 | b.property('init', b.identifier('value'), b.literal(enumValue.value)), 234 | generateDescription(enumValue.description) 235 | ]) 236 | )); 237 | } 238 | 239 | var typeDeclaration = b.objectExpression([ 240 | b.property('init', b.identifier('name'), b.literal(model.name + capitalize(field.name))), 241 | generateDescription(field.description), 242 | b.property('init', b.identifier('values'), b.objectExpression(values)) 243 | ]); 244 | 245 | return b.newExpression( 246 | b.identifier('GraphQLEnumType'), 247 | [typeDeclaration] 248 | ); 249 | } 250 | } 251 | 252 | module.exports = generateTypes; 253 | -------------------------------------------------------------------------------- /steps/output-data.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var fs = require('fs'); 4 | var path = require('path'); 5 | var merge = require('lodash/object/merge'); 6 | var recast = require('recast'); 7 | var mkdirp = require('mkdirp'); 8 | var buildType = require('./ast-builders/type'); 9 | var buildConfig = require('./ast-builders/config'); 10 | var buildTypeIndex = require('./ast-builders/type-index'); 11 | var buildResolveMap = require('./ast-builders/resolve-map'); 12 | var buildSchemaModule = require('./ast-builders/schema-module'); 13 | var buildNodeDefinitions = require('./ast-builders/node-definitions'); 14 | var updatePackageManifest = require('./update-package'); 15 | var copyTemplates = require('./copy-templates'); 16 | 17 | function outputData(data, opts, callback) { 18 | if (opts.relay) { 19 | opts = merge({}, opts, { isFromSchema: true }); 20 | } 21 | 22 | // Output to a directory, in other words: split stuff up 23 | var outputDir = path.resolve(opts.outputDir); 24 | var typesDir = path.join(outputDir, 'types'); 25 | var configDir = path.join(outputDir, 'config'); 26 | mkdirp(configDir, function(err) { 27 | if (err) { 28 | throw err; 29 | } 30 | 31 | // Write the configuration file 32 | var conf = recast.prettyPrint(buildConfig(opts), opts).code; 33 | fs.writeFileSync(path.join(configDir, 'config.js'), conf); 34 | 35 | // Write types 36 | mkdirp(typesDir, function(typesErr) { 37 | if (typesErr) { 38 | throw typesErr; 39 | } 40 | 41 | // Build the type AST and write the code to separate files 42 | var type, ast, code; 43 | for (type in data.types) { 44 | ast = buildType(data.types[type], opts); 45 | code = recast.prettyPrint(ast, opts).code; 46 | 47 | fs.writeFileSync(path.join(typesDir, data.types[type].varName + '.js'), code); 48 | } 49 | 50 | // Write a type index 51 | ast = buildTypeIndex(data, opts); 52 | code = recast.prettyPrint(ast, opts).code; 53 | fs.writeFileSync(path.join(typesDir, 'index.js'), code); 54 | 55 | // If this is a relay app, write the Node interface 56 | if (opts.relay) { 57 | ast = buildNodeDefinitions(opts); 58 | code = recast.prettyPrint(ast, opts).code; 59 | fs.writeFileSync(path.join(typesDir, 'Node.js'), code); 60 | } 61 | }); 62 | 63 | // Build and write the resolve map 64 | var resolveMap = recast.prettyPrint(buildResolveMap(data, opts), opts).code; 65 | fs.writeFileSync(path.join(outputDir, 'resolve-map.js'), resolveMap); 66 | 67 | // Copy templates ("static" ones, should probably be named something else) 68 | copyTemplates(opts.es6 ? 'es6' : 'cjs', outputDir); 69 | 70 | // Write the schema! 71 | var schemaCode = recast.prettyPrint(buildSchemaModule(data, opts), opts).code; 72 | fs.writeFileSync(path.join(outputDir, 'schema.js'), schemaCode); 73 | 74 | // Update package.json file with any necessary changes 75 | updatePackageManifest(opts); 76 | 77 | callback(); 78 | }); 79 | } 80 | 81 | module.exports = outputData; 82 | -------------------------------------------------------------------------------- /steps/table-comment.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var async = require('async'); 4 | 5 | function getTableComments(adapter, opts, cb) { 6 | var tables = opts.tables.reduce(function(map, tbl) { 7 | map[tbl] = getTableCommentTask(adapter, tbl); 8 | return map; 9 | }, {}); 10 | 11 | async.parallel(tables, cb); 12 | } 13 | 14 | function getTableCommentTask(adapter, tblName) { 15 | return function getTableComment(cb) { 16 | adapter.getTableComment(tblName, cb); 17 | }; 18 | } 19 | 20 | module.exports = getTableComments; 21 | -------------------------------------------------------------------------------- /steps/table-list.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var diff = require('lodash/array/difference'); 4 | 5 | // Get available tables/verify specified tables 6 | module.exports = function getTableList(adapter, opts, cb) { 7 | var tableList = opts.table; 8 | adapter.getTables(tableList, function(err, tables) { 9 | if (err) { 10 | return cb(err); 11 | } 12 | 13 | // Check for missing tables 14 | var matchAll = tableList.length === 1 && tableList[0] === '*'; 15 | if (!matchAll && tableList.length !== tables.length) { 16 | return cb(new Error( 17 | 'Did not find specified table(s): ' + diff(tableList, tables).join(', ') 18 | )); 19 | } 20 | 21 | cb(null, tables); 22 | }); 23 | }; 24 | -------------------------------------------------------------------------------- /steps/table-structure.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var async = require('async'); 4 | 5 | function getTableStructures(adapter, opts, cb) { 6 | var tables = opts.tables.reduce(function(map, tbl) { 7 | map[tbl] = getTableStructureTask(adapter, tbl); 8 | return map; 9 | }, {}); 10 | 11 | async.parallel(tables, cb); 12 | } 13 | 14 | function getTableStructureTask(adapter, tblName) { 15 | return function getTableStructure(cb) { 16 | adapter.getTableStructure(tblName, cb); 17 | }; 18 | } 19 | 20 | module.exports = getTableStructures; 21 | -------------------------------------------------------------------------------- /steps/table-to-object.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var pluralize = require('pluralize'); 4 | var camelCase = require('lodash/string/camelCase'); 5 | var capitalize = require('lodash/string/capitalize'); 6 | var indexBy = require('lodash/collection/indexBy'); 7 | var columnToObject = require('./column-to-object'); 8 | 9 | function tableToObject(table, opts) { 10 | var normalized = normalizeTableName(table.name, { 11 | suffix: opts.stripSuffix, 12 | prefix: opts.stripPrefix 13 | }); 14 | 15 | var fields = table.columns.map(function(column) { 16 | return columnToObject(column, opts); 17 | }); 18 | 19 | var model = { 20 | name: getTypeName(normalized), 21 | description: table.comment, 22 | table: table.name, 23 | normalizedTable: normalized, 24 | fields: indexBy(fields, 'name'), 25 | aliasedFields: fields.reduce(function(aliases, field) { 26 | if (field.name !== field.originalName) { 27 | aliases[field.originalName] = field.name; 28 | } 29 | 30 | return aliases; 31 | }, {}) 32 | }; 33 | 34 | return model; 35 | } 36 | 37 | function getTypeName(item) { 38 | return pluralize(capitalize(camelCase(item)), 1); 39 | } 40 | 41 | function normalizeTableName(name, strip) { 42 | (strip.suffix || []).forEach(function(suffix) { 43 | name = name.replace(new RegExp(escapeRegExp(suffix) + '$'), ''); 44 | }); 45 | 46 | (strip.prefix || []).forEach(function(prefix) { 47 | name = name.replace(new RegExp('^' + escapeRegExp(prefix)), ''); 48 | }); 49 | 50 | return name; 51 | } 52 | 53 | function escapeRegExp(str) { 54 | return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&'); 55 | } 56 | 57 | module.exports = tableToObject; 58 | -------------------------------------------------------------------------------- /steps/update-package.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var fs = require('fs'); 4 | var path = require('path'); 5 | 6 | module.exports = function updatePackageJson(opts) { 7 | var pkgPath = path.join(opts.outputDir, 'package.json'); 8 | var pkgFile = fs.readFileSync(pkgPath); 9 | var pkgContent = JSON.parse(pkgFile); 10 | 11 | if (opts.relay) { 12 | pkgContent.dependencies['graphql-relay'] = '^0.3.0'; 13 | } 14 | 15 | fs.writeFileSync(pkgPath, JSON.stringify(pkgContent, null, 2)); 16 | }; 17 | -------------------------------------------------------------------------------- /templates/cjs/db.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var knex = require('knex'); 4 | var config = require('./config/config'); 5 | var db; 6 | 7 | function getDb() { 8 | return db || getDb.reconnect(); 9 | } 10 | 11 | getDb.reconnect = function() { 12 | db = knex(config); 13 | return db; 14 | }; 15 | 16 | module.exports = getDb; 17 | -------------------------------------------------------------------------------- /templates/cjs/handlers/graphql.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Boom = require('boom'); 4 | var graphql = require('graphql').graphql; 5 | var schema = require('../schema'); 6 | 7 | module.exports = function graphqlHandler(request, reply) { 8 | var payload = (request.payload || '').toString(); 9 | graphql(schema, payload).then(function(result) { 10 | if (result.errors) { 11 | logErrors(result.errors); 12 | 13 | return reply(Boom.badRequest( 14 | result.errors.reduce(reduceErrors, []) 15 | )); 16 | } 17 | 18 | return reply(result); 19 | }); 20 | }; 21 | 22 | function reduceErrors(errs, err) { 23 | // Hacky, but knex sucks at errors and graphql swallows errors. 24 | var isDbErr = err.message.indexOf('Pool') === 0; 25 | 26 | errs.push((isDbErr ? '[Database] ' : '') + err.message); 27 | return errs; 28 | } 29 | 30 | function logErrors(errs) { 31 | errs.forEach(function(err) { 32 | console.log(err.message); 33 | 34 | if (err.stack) { 35 | console.log(err.stack); 36 | } 37 | }); 38 | } 39 | -------------------------------------------------------------------------------- /templates/cjs/handlers/schema-printer.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var printSchema = require('graphql/utilities/schemaPrinter').printSchema; 4 | var schema = require('../schema'); 5 | 6 | module.exports = function schemaPrintHandler(request, reply) { 7 | reply(printSchema(schema)); 8 | }; 9 | -------------------------------------------------------------------------------- /templates/cjs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "scaffolded-app", 3 | "version": "1.0.0", 4 | "private": true, 5 | "description": "Demo app generated by sql-to-graphql", 6 | "main": "server.js", 7 | "scripts": { 8 | "start": "node server.js", 9 | "test": "echo \"Error: no test specified\" && exit 1" 10 | }, 11 | "author": "VaffelNinja AS", 12 | "license": "MIT", 13 | "dependencies": { 14 | "boom": "^2.8.0", 15 | "graphql": "^0.4.2", 16 | "hapi": "^8.8.1", 17 | "knex": "^0.8.6", 18 | "mysql": "^2.8.0" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /templates/cjs/server.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Hapi = require('hapi'); 4 | 5 | var server = new Hapi.Server(); 6 | server.connection({ port: 3000 }); 7 | 8 | server.route({ 9 | method: 'POST', 10 | path: '/graphql', 11 | handler: require('./handlers/graphql'), 12 | config: { 13 | payload: { 14 | parse: false, 15 | allow: 'application/graphql' 16 | } 17 | } 18 | }); 19 | 20 | server.route({ 21 | method: 'GET', 22 | path: '/schema', 23 | handler: require('./handlers/schema-printer') 24 | }); 25 | 26 | server.route({ 27 | method: 'GET', 28 | path: '/{param*}', 29 | handler: { 30 | directory: { 31 | path: 'public' 32 | } 33 | } 34 | }); 35 | 36 | server.start(function() { 37 | console.log('Server running at:', server.info.uri); 38 | }); 39 | -------------------------------------------------------------------------------- /templates/cjs/util/connection-resolver.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var resolveMap = require('../resolve-map').resolveMap; 4 | var getSelectionSet = require('./get-selection-set'); 5 | var db = require('../db'); 6 | var config = require('../config/config'); 7 | 8 | module.exports = function getConnectionResolver(type) { 9 | var typeData = resolveMap[type]; 10 | 11 | if (!typeData) { 12 | throw new Error('Type "' + type + '" not a recognized type'); 13 | } 14 | 15 | return function resolveConnection(parent, args, ast) { 16 | var parentTypeData = resolveMap[ast.parentType.name]; 17 | var listRefField = parentTypeData.listReferences[ast.fieldName]; 18 | var parentPk = parentTypeData.aliases[parentTypeData.primaryKey] || parentTypeData.primaryKey; 19 | var edgeSelection = ast.fieldASTs[0].selectionSet.selections.reduce(edgeReducer, null); 20 | var selection = getSelectionSet(type, edgeSelection, typeData.aliases, typeData.referenceMap); 21 | var clauses = {}; 22 | clauses[listRefField] = parent[parentPk]; 23 | 24 | var before = args.before, 25 | after = args.after, 26 | first = args.first, 27 | last = args.last, 28 | offset = 0, 29 | limit = config.edgeSize || 25; 30 | 31 | if (before && after) { 32 | throw new Error('Combining `before` and `after` is not supported'); 33 | } 34 | 35 | if (after) { 36 | offset = getOffset(after) || 0; 37 | limit = parseInt(first || config.edgeSize || 25, 10); 38 | } else if (before) { 39 | limit = parseInt(last || config.edgeSize || 25, 10); 40 | offset = Math.max(0, (getOffset(before) || 0) - limit); 41 | } 42 | 43 | var query = db() 44 | .select(selection) 45 | .from(typeData.table) 46 | .where(clauses) 47 | .offset(offset) 48 | .limit(limit + 1); 49 | 50 | if (config.debug) { 51 | console.log(query.toSQL()); 52 | } 53 | 54 | return query.then(function(result) { 55 | var hasNextPage = result.length > limit; 56 | if (hasNextPage) { 57 | result.pop(); 58 | } 59 | 60 | if (result.length === 0) { 61 | return emptyConnection(); 62 | } 63 | 64 | var startIndex = after ? offset + 1 : offset; 65 | var edges = result.map(function(value, index) { 66 | return { 67 | cursor: offsetToCursor(startIndex + index), 68 | node: value 69 | }; 70 | }); 71 | 72 | if (first) { 73 | edges = edges.slice(0, first); 74 | } 75 | 76 | if (last) { 77 | edges = edges.slice(-last); 78 | } 79 | 80 | if (edges.length === 0) { 81 | return emptyConnection(); 82 | } 83 | 84 | return { 85 | edges: edges, 86 | pageInfo: { 87 | startCursor: edges[0].cursor, 88 | endCursor: edges[edges.length - 1].cursor, 89 | hasPreviousPage: cursorToOffset(edges[0].cursor) > 0, 90 | hasNextPage: hasNextPage 91 | } 92 | }; 93 | }); 94 | }; 95 | }; 96 | 97 | var PREFIX = 'Connection:'; 98 | function offsetToCursor(offset) { 99 | return new Buffer(PREFIX + offset).toString('base64'); 100 | } 101 | 102 | function cursorToOffset(cursor) { 103 | return parseInt(new Buffer(cursor, 'base64').toString('ascii').substring(PREFIX.length), 10); 104 | } 105 | 106 | // Given an optional cursor and a default offset, returns the offset 107 | // to use; if the cursor contains a valid offset, that will be used, 108 | // otherwise it will be the default. 109 | function getOffset(cursor, defaultOffset) { 110 | if (typeof cursor === 'undefined' || cursor === null) { 111 | return defaultOffset; 112 | } 113 | 114 | var offset = cursorToOffset(cursor); 115 | if (isNaN(offset)) { 116 | return defaultOffset; 117 | } 118 | 119 | return offset; 120 | } 121 | 122 | function emptyConnection() { 123 | return { 124 | edges: [], 125 | pageInfo: { 126 | startCursor: null, 127 | endCursor: null, 128 | hasPreviousPage: false, 129 | hasNextPage: false 130 | } 131 | }; 132 | } 133 | 134 | function edgeReducer(edges, selection) { 135 | if (selection.name.value !== 'edges') { 136 | return edges; 137 | } 138 | 139 | return selection.selectionSet.selections.reduce(nodeReducer); 140 | } 141 | 142 | function nodeReducer(node, selection) { 143 | if (selection.name.value !== 'node') { 144 | return node; 145 | } 146 | 147 | return selection; 148 | } 149 | -------------------------------------------------------------------------------- /templates/cjs/util/entity-resolver.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var GraphQL = require('graphql'); 4 | var resolveMap = require('../resolve-map').resolveMap; 5 | var getSelectionSet = require('./get-selection-set'); 6 | var getUnaliasedName = require('./get-unaliased-name'); 7 | var db = require('../db'); 8 | var config = require('../config/config'); 9 | 10 | function getResolver(type) { 11 | var typeData = resolveMap[type]; 12 | 13 | if (!typeData) { 14 | throw new Error('Type "' + type + '" not a recognized type'); 15 | } 16 | 17 | var pkAlias = typeData.primaryKey ? typeData.aliases[typeData.primaryKey] : null; 18 | return function resolveEntity(parent, args, ast) { 19 | var isList = ast.returnType instanceof GraphQL.GraphQLList; 20 | var clauses = getClauses(ast, args, typeData.aliases); 21 | var selection = getSelectionSet(type, ast.fieldASTs[0], typeData.aliases, typeData.referenceMap); 22 | var hasPkSelected = 23 | typeData.primaryKey && 24 | selection.some(function(item) { 25 | return item.indexOf(typeData.primaryKey) === 0; 26 | }); 27 | 28 | if (typeData.primaryKey && !hasPkSelected) { 29 | selection.unshift(getAliasSelection(typeData.primaryKey, pkAlias)); 30 | } 31 | 32 | if (parent) { 33 | var parentTypeData = resolveMap[ast.parentType.name]; 34 | var refField = parentTypeData.referenceMap[ast.fieldName]; 35 | var listRefField = parentTypeData.listReferences[ast.fieldName]; 36 | 37 | if (refField) { 38 | var unliasedRef = getUnaliasedName(refField, parentTypeData.aliases); 39 | clauses[typeData.primaryKey] = parent[refField] || parent[unliasedRef]; 40 | } else if (listRefField) { 41 | var parentPk = parentTypeData.aliases[parentTypeData.primaryKey] || parentTypeData.primaryKey; 42 | clauses[listRefField] = parent[parentPk]; 43 | } 44 | } 45 | 46 | var query = ( 47 | isList ? db().select(selection) : db().first(selection) 48 | ).from(typeData.table).where(clauses).limit(25); 49 | 50 | if (isList) { 51 | query.limit(args.limit || 25).offset(args.offset || 0); 52 | } 53 | 54 | if (config.debug) { 55 | console.log(query.toSQL()); 56 | } 57 | 58 | // @TODO Find a much less hacky and error prone to handle this 59 | // Ties together with the Node type in Relay! 60 | return query.then(function(result) { 61 | if (result) { 62 | result.__type = typeData.type; 63 | } 64 | 65 | return result; 66 | }); 67 | }; 68 | } 69 | 70 | function getClauses(ast, args, aliases) { 71 | return Object.keys(args).reduce(function(query, alias) { 72 | if (alias === 'limit' || alias === 'offset') { 73 | return query; 74 | } 75 | 76 | var field = getUnaliasedName(alias, aliases); 77 | query[field || alias] = args[alias]; 78 | return query; 79 | }, {}); 80 | } 81 | 82 | function getAliasSelection(field, alias) { 83 | if (alias) { 84 | return field + ' AS ' + alias; 85 | } 86 | 87 | return field; 88 | } 89 | 90 | module.exports = getResolver; 91 | -------------------------------------------------------------------------------- /templates/cjs/util/get-selection-set.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var getUnaliasedName = require('./get-unaliased-name'); 4 | 5 | module.exports = function getSelectionSet(type, ast, aliases, referenceMap) { 6 | if (!ast || !ast.selectionSet) { 7 | return []; 8 | } 9 | 10 | return ast.selectionSet.selections.reduce(function reduceSelectionSet(set, selection) { 11 | // If we encounter a selection with a type condition, make sure it's the correct type 12 | if (selection.typeCondition && selection.typeCondition.name.value !== type) { 13 | return set; 14 | } 15 | 16 | var alias, field; 17 | if (selection.kind === 'Field' && selection.selectionSet && referenceMap) { 18 | // For fields with its own selection set, we need to fetch the reference ID 19 | alias = referenceMap[selection.name.value]; 20 | field = getUnaliasedName(alias, aliases); 21 | if (field || alias) { 22 | set.push(field || alias); 23 | } 24 | return set; 25 | } else if (selection.kind === 'InlineFragment' && selection.selectionSet) { 26 | // And for inline fragments, we need to recurse down and combine the set 27 | return set.concat(getSelectionSet(type, selection, aliases, referenceMap)); 28 | } else if (selection.selectionSet) { 29 | return set; 30 | } 31 | 32 | alias = selection.name.value; 33 | field = getUnaliasedName(alias, aliases); 34 | set.push(field ? field + ' AS ' + alias : alias); 35 | return set; 36 | }, []); 37 | }; 38 | -------------------------------------------------------------------------------- /templates/cjs/util/get-unaliased-name.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function getUnaliasedName(alias, aliases) { 4 | for (var key in aliases) { 5 | if (aliases[key] === alias) { 6 | return key; 7 | } 8 | } 9 | }; 10 | -------------------------------------------------------------------------------- /templates/es6/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "es6": true 4 | }, 5 | 6 | "ecmaFeatures": { 7 | "modules": true 8 | } 9 | } -------------------------------------------------------------------------------- /templates/es6/db.js: -------------------------------------------------------------------------------- 1 | import knex from 'knex'; 2 | import config from './config/config'; 3 | 4 | var db; 5 | 6 | export default function getDb() { 7 | return db || getDb.reconnect(); 8 | } 9 | 10 | getDb.reconnect = function() { 11 | db = knex(config); 12 | return db; 13 | }; 14 | -------------------------------------------------------------------------------- /templates/es6/handlers/graphql.js: -------------------------------------------------------------------------------- 1 | import Boom from 'boom'; 2 | import { graphql } from 'graphql'; 3 | import schema from '../schema'; 4 | 5 | export default function graphqlHandler(request, reply) { 6 | const payload = (request.payload || '').toString(); 7 | graphql(schema, payload).then(function(result) { 8 | if (result.errors) { 9 | logErrors(result.errors); 10 | 11 | return reply(Boom.badRequest( 12 | result.errors.reduce(reduceErrors, []) 13 | )); 14 | } 15 | 16 | return reply(result); 17 | }); 18 | } 19 | 20 | function reduceErrors(errs, err) { 21 | // Hacky, but knex sucks at errors and graphql swallows errors. 22 | const isDbErr = err.message.indexOf('Pool') === 0; 23 | 24 | errs.push((isDbErr ? '[Database] ' : '') + err.message); 25 | return errs; 26 | } 27 | 28 | function logErrors(errs) { 29 | errs.forEach(function(err) { 30 | console.log(err.message); 31 | 32 | if (err.stack) { 33 | console.log(err.stack); 34 | } 35 | }); 36 | } 37 | -------------------------------------------------------------------------------- /templates/es6/handlers/schema-printer.js: -------------------------------------------------------------------------------- 1 | import { printSchema } from 'graphql/utilities/schemaPrinter'; 2 | import schema from '../schema'; 3 | 4 | export default function schemaPrintHandler(request, reply) { 5 | reply(printSchema(schema)); 6 | } 7 | -------------------------------------------------------------------------------- /templates/es6/index.js: -------------------------------------------------------------------------------- 1 | require('babel/register'); 2 | require('./server'); 3 | -------------------------------------------------------------------------------- /templates/es6/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "scaffolded-app", 3 | "version": "1.0.0", 4 | "private": true, 5 | "description": "Demo app generated by sql-to-graphql", 6 | "main": "index.js", 7 | "scripts": { 8 | "start": "node index.js", 9 | "test": "echo \"Error: no test specified\" && exit 1" 10 | }, 11 | "author": "VaffelNinja AS", 12 | "license": "MIT", 13 | "dependencies": { 14 | "babel": "^5.8.20", 15 | "boom": "^2.8.0", 16 | "graphql": "^0.4.2", 17 | "hapi": "^8.8.1", 18 | "knex": "^0.8.6", 19 | "mysql": "^2.8.0" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /templates/es6/server.js: -------------------------------------------------------------------------------- 1 | import Hapi from 'hapi'; 2 | 3 | const server = new Hapi.Server(); 4 | server.connection({ port: 3000 }); 5 | 6 | server.route({ 7 | method: 'POST', 8 | path: '/graphql', 9 | handler: require('./handlers/graphql'), 10 | config: { 11 | payload: { 12 | parse: false, 13 | allow: 'application/graphql' 14 | } 15 | } 16 | }); 17 | 18 | server.route({ 19 | method: 'GET', 20 | path: '/schema', 21 | handler: require('./handlers/schema-printer') 22 | }); 23 | 24 | server.route({ 25 | method: 'GET', 26 | path: '/{param*}', 27 | handler: { 28 | directory: { 29 | path: 'public' 30 | } 31 | } 32 | }); 33 | 34 | server.start(function() { 35 | console.log('Server running at:', server.info.uri); 36 | }); 37 | -------------------------------------------------------------------------------- /templates/es6/util/connection-resolver.js: -------------------------------------------------------------------------------- 1 | import { resolveMap } from '../resolve-map'; 2 | import getSelectionSet from './get-selection-set'; 3 | import db from '../db'; 4 | import config from '../config/config'; 5 | 6 | export default function getConnectionResolver(type) { 7 | const typeData = resolveMap[type]; 8 | 9 | if (!typeData) { 10 | throw new Error('Type "' + type + '" not a recognized type'); 11 | } 12 | 13 | return function resolveConnection(parent, args, ast) { 14 | const parentTypeData = resolveMap[ast.parentType.name]; 15 | const listRefField = parentTypeData.listReferences[ast.fieldName]; 16 | const parentPk = parentTypeData.aliases[parentTypeData.primaryKey] || parentTypeData.primaryKey; 17 | const edgeSelection = ast.fieldASTs[0].selectionSet.selections.reduce(edgeReducer, null); 18 | const selection = getSelectionSet(type, edgeSelection, typeData.aliases, typeData.referenceMap); 19 | const clauses = { [listRefField]: parent[parentPk] }; 20 | 21 | const {before, after, first, last} = args; 22 | let offset = 0, limit = config.edgeSize || 25; 23 | 24 | if (before && after) { 25 | throw new Error('Combining `before` and `after` is not supported'); 26 | } 27 | 28 | if (after) { 29 | offset = getOffset(after) || 0; 30 | limit = parseInt(first || config.edgeSize || 25, 10); 31 | } else if (before) { 32 | limit = parseInt(last || config.edgeSize || 25, 10); 33 | offset = Math.max(0, (getOffset(before) || 0) - limit); 34 | } 35 | 36 | const query = db() 37 | .select(selection) 38 | .from(typeData.table) 39 | .where(clauses) 40 | .offset(offset) 41 | .limit(limit + 1); 42 | 43 | if (config.debug) { 44 | console.log(query.toSQL()); 45 | } 46 | 47 | return query.then(function(result) { 48 | let hasNextPage = result.length > limit; 49 | if (hasNextPage) { 50 | result.pop(); 51 | } 52 | 53 | if (result.length === 0) { 54 | return emptyConnection(); 55 | } 56 | 57 | let startIndex = after ? offset + 1 : offset; 58 | let edges = result.map( 59 | (value, index) => ({ 60 | cursor: offsetToCursor(startIndex + index), 61 | node: value 62 | }) 63 | ); 64 | 65 | if (first) { 66 | edges = edges.slice(0, first); 67 | } 68 | 69 | if (last) { 70 | edges = edges.slice(-last); 71 | } 72 | 73 | if (edges.length === 0) { 74 | return emptyConnection(); 75 | } 76 | 77 | return { 78 | edges: edges, 79 | pageInfo: { 80 | startCursor: edges[0].cursor, 81 | endCursor: edges[edges.length - 1].cursor, 82 | hasPreviousPage: cursorToOffset(edges[0].cursor) > 0, 83 | hasNextPage: hasNextPage 84 | } 85 | }; 86 | }); 87 | }; 88 | } 89 | 90 | var PREFIX = 'Connection:'; 91 | function offsetToCursor(offset) { 92 | return new Buffer(PREFIX + offset).toString('base64'); 93 | } 94 | 95 | function cursorToOffset(cursor) { 96 | return parseInt(new Buffer(cursor, 'base64').toString('ascii').substring(PREFIX.length), 10); 97 | } 98 | 99 | // Given an optional cursor and a default offset, returns the offset 100 | // to use; if the cursor contains a valid offset, that will be used, 101 | // otherwise it will be the default. 102 | function getOffset(cursor, defaultOffset) { 103 | if (typeof cursor === 'undefined' || cursor === null) { 104 | return defaultOffset; 105 | } 106 | 107 | let offset = cursorToOffset(cursor); 108 | if (isNaN(offset)) { 109 | return defaultOffset; 110 | } 111 | 112 | return offset; 113 | } 114 | 115 | function emptyConnection() { 116 | return { 117 | edges: [], 118 | pageInfo: { 119 | startCursor: null, 120 | endCursor: null, 121 | hasPreviousPage: false, 122 | hasNextPage: false 123 | } 124 | }; 125 | } 126 | 127 | function edgeReducer(edges, selection) { 128 | if (selection.name.value !== 'edges') { 129 | return edges; 130 | } 131 | 132 | return selection.selectionSet.selections.reduce(nodeReducer); 133 | } 134 | 135 | function nodeReducer(node, selection) { 136 | if (selection.name.value !== 'node') { 137 | return node; 138 | } 139 | 140 | return selection; 141 | } 142 | -------------------------------------------------------------------------------- /templates/es6/util/entity-resolver.js: -------------------------------------------------------------------------------- 1 | import { GraphQLList } from 'graphql'; 2 | import { resolveMap } from '../resolve-map'; 3 | import getSelectionSet from './get-selection-set'; 4 | import getUnaliasedName from './get-unaliased-name'; 5 | import db from '../db'; 6 | import config from '../config/config'; 7 | 8 | export default function getResolver(type) { 9 | const typeData = resolveMap[type]; 10 | 11 | if (!typeData) { 12 | throw new Error('Type "' + type + '" not a recognized type'); 13 | } 14 | 15 | const pkAlias = typeData.primaryKey ? typeData.aliases[typeData.primaryKey] : null; 16 | return function resolveEntity(parent, args, ast) { 17 | const isList = ast.returnType instanceof GraphQLList; 18 | const clauses = getClauses(ast, args, typeData.aliases); 19 | const selection = getSelectionSet(type, ast.fieldASTs[0], typeData.aliases, typeData.referenceMap); 20 | const hasPkSelected = ( 21 | typeData.primaryKey && 22 | selection.some(item => item.indexOf(typeData.primaryKey) === 0) 23 | ); 24 | 25 | if (typeData.primaryKey && !hasPkSelected) { 26 | selection.unshift(getAliasSelection(typeData.primaryKey, pkAlias)); 27 | } 28 | 29 | if (parent) { 30 | const parentTypeData = resolveMap[ast.parentType.name]; 31 | const refField = parentTypeData.referenceMap[ast.fieldName]; 32 | const listRefField = parentTypeData.listReferences[ast.fieldName]; 33 | 34 | if (refField) { 35 | const unliasedRef = getUnaliasedName(refField, parentTypeData.aliases); 36 | clauses[typeData.primaryKey] = parent[refField] || parent[unliasedRef]; 37 | } else if (listRefField) { 38 | const parentPk = parentTypeData.aliases[parentTypeData.primaryKey] || parentTypeData.primaryKey; 39 | clauses[listRefField] = parent[parentPk]; 40 | } 41 | } 42 | 43 | const query = ( 44 | isList ? db().select(selection) : db().first(selection) 45 | ).from(typeData.table).where(clauses).limit(25); 46 | 47 | if (isList) { 48 | query.limit(args.limit || 25).offset(args.offset || 0); 49 | } 50 | 51 | if (config.debug) { 52 | console.log(query.toSQL()); 53 | } 54 | 55 | // @TODO Find a much less hacky and error prone to handle this 56 | // Ties together with the Node type in Relay! 57 | return query.then(function(result) { 58 | if (result) { 59 | result.__type = typeData.type; 60 | } 61 | 62 | return result; 63 | }); 64 | }; 65 | } 66 | 67 | function getClauses(ast, args, aliases) { 68 | return Object.keys(args).reduce(function(query, alias) { 69 | if (alias === 'limit' || alias === 'offset') { 70 | return query; 71 | } 72 | 73 | let field = getUnaliasedName(alias, aliases); 74 | query[field || alias] = args[alias]; 75 | return query; 76 | }, {}); 77 | } 78 | 79 | function getAliasSelection(field, alias) { 80 | if (alias) { 81 | return field + ' AS ' + alias; 82 | } 83 | 84 | return field; 85 | } 86 | -------------------------------------------------------------------------------- /templates/es6/util/get-selection-set.js: -------------------------------------------------------------------------------- 1 | import getUnaliasedName from './get-unaliased-name'; 2 | 3 | export default function getSelectionSet(type, ast, aliases, referenceMap) { 4 | if (!ast || !ast.selectionSet) { 5 | return []; 6 | } 7 | 8 | return ast.selectionSet.selections.reduce(function reduceSelectionSet(set, selection) { 9 | // If we encounter a selection with a type condition, make sure it's the correct type 10 | if (selection.typeCondition && selection.typeCondition.name.value !== type) { 11 | return set; 12 | } 13 | 14 | var alias, field; 15 | if (selection.kind === 'Field' && selection.selectionSet && referenceMap) { 16 | // For fields with its own selection set, we need to fetch the reference ID 17 | alias = referenceMap[selection.name.value]; 18 | field = getUnaliasedName(alias, aliases); 19 | if (field || alias) { 20 | set.push(field || alias); 21 | } 22 | return set; 23 | } else if (selection.kind === 'InlineFragment' && selection.selectionSet) { 24 | // And for inline fragments, we need to recurse down and combine the set 25 | return set.concat(getSelectionSet(type, selection, aliases, referenceMap)); 26 | } else if (selection.selectionSet) { 27 | return set; 28 | } 29 | 30 | alias = selection.name.value; 31 | field = getUnaliasedName(alias, aliases); 32 | set.push(field ? field + ' AS ' + alias : alias); 33 | return set; 34 | }, []); 35 | } 36 | -------------------------------------------------------------------------------- /templates/es6/util/get-unaliased-name.js: -------------------------------------------------------------------------------- 1 | export default function getUnaliasedName(alias, aliases) { 2 | for (var key in aliases) { 3 | if (aliases[key] === alias) { 4 | return key; 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /templates/public/css/playground.css: -------------------------------------------------------------------------------- 1 | h1 { 2 | margin: 0; 3 | } 4 | 5 | h2 { 6 | margin: 10px 0; 7 | } 8 | 9 | #container { 10 | margin: 20px; 11 | } 12 | 13 | #editor-wrapper { 14 | position: relative; 15 | } 16 | 17 | .editor { 18 | width: 100%; 19 | height: 400px; 20 | font-size: 20px !important; 21 | margin-bottom: 10px; 22 | } 23 | 24 | .response { 25 | padding: 15px; 26 | margin-top: 10px; 27 | position: relative; 28 | } 29 | 30 | .wrapper { 31 | padding: 10px; 32 | } 33 | 34 | .schema pre { 35 | border: 1px solid #eee; 36 | background: #fafafa; 37 | margin: 0; 38 | padding: 10px; 39 | } 40 | 41 | .errors { 42 | background: #DC7676; 43 | color: #fff; 44 | } 45 | 46 | .loading { 47 | position: absolute; 48 | right: 15px; 49 | display: block; 50 | z-index: 5; 51 | width: 40px; 52 | height: 40px; 53 | } 54 | 55 | .loading:before { 56 | content: ''; 57 | width: 40px; 58 | height: 5px; 59 | background: #000; 60 | opacity: 0.1; 61 | position: absolute; 62 | top: 59px; 63 | left: -10px; 64 | border-radius: 50%; 65 | animation: shadow .5s linear infinite; 66 | } 67 | .loading:after { 68 | content: ''; 69 | width: 40px; 70 | height: 40px; 71 | background: #a6dc2a; 72 | animation: animate .5s linear infinite; 73 | position: absolute; 74 | top: 0; 75 | left: -10px; 76 | border-radius: 3px; 77 | } 78 | @keyframes animate { 79 | 17% { 80 | border-bottom-right-radius: 3px; 81 | } 82 | 25% { 83 | transform: translateY(9px) rotate(22.5deg); 84 | } 85 | 50% { 86 | transform: translateY(18px) scale(1, 0.9) rotate(45deg); 87 | border-bottom-right-radius: 40px; 88 | } 89 | 75% { 90 | transform: translateY(9px) rotate(67.5deg); 91 | } 92 | 100% { 93 | transform: translateY(0) rotate(90deg); 94 | } 95 | } 96 | @keyframes shadow { 97 | 0%, 98 | 100% { 99 | transform: scale(1, 1); 100 | } 101 | 50% { 102 | transform: scale(1.2, 1); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /templates/public/css/pure.css: -------------------------------------------------------------------------------- 1 | /*! 2 | Pure v0.6.0 3 | Copyright 2014 Yahoo! Inc. All rights reserved. 4 | Licensed under the BSD License. 5 | https://github.com/yahoo/pure/blob/master/LICENSE.md 6 | */ 7 | /*! 8 | normalize.css v^3.0 | MIT License | git.io/normalize 9 | Copyright (c) Nicolas Gallagher and Jonathan Neal 10 | */ 11 | /*! normalize.css v3.0.2 | MIT License | git.io/normalize */ 12 | 13 | /** 14 | * 1. Set default font family to sans-serif. 15 | * 2. Prevent iOS text size adjust after orientation change, without disabling 16 | * user zoom. 17 | */ 18 | 19 | html { 20 | font-family: sans-serif; /* 1 */ 21 | -ms-text-size-adjust: 100%; /* 2 */ 22 | -webkit-text-size-adjust: 100%; /* 2 */ 23 | } 24 | 25 | /** 26 | * Remove default margin. 27 | */ 28 | 29 | body { 30 | margin: 0; 31 | } 32 | 33 | /* HTML5 display definitions 34 | ========================================================================== */ 35 | 36 | /** 37 | * Correct `block` display not defined for any HTML5 element in IE 8/9. 38 | * Correct `block` display not defined for `details` or `summary` in IE 10/11 39 | * and Firefox. 40 | * Correct `block` display not defined for `main` in IE 11. 41 | */ 42 | 43 | article, 44 | aside, 45 | details, 46 | figcaption, 47 | figure, 48 | footer, 49 | header, 50 | hgroup, 51 | main, 52 | menu, 53 | nav, 54 | section, 55 | summary { 56 | display: block; 57 | } 58 | 59 | /** 60 | * 1. Correct `inline-block` display not defined in IE 8/9. 61 | * 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera. 62 | */ 63 | 64 | audio, 65 | canvas, 66 | progress, 67 | video { 68 | display: inline-block; /* 1 */ 69 | vertical-align: baseline; /* 2 */ 70 | } 71 | 72 | /** 73 | * Prevent modern browsers from displaying `audio` without controls. 74 | * Remove excess height in iOS 5 devices. 75 | */ 76 | 77 | audio:not([controls]) { 78 | display: none; 79 | height: 0; 80 | } 81 | 82 | /** 83 | * Address `[hidden]` styling not present in IE 8/9/10. 84 | * Hide the `template` element in IE 8/9/11, Safari, and Firefox < 22. 85 | */ 86 | 87 | [hidden], 88 | template { 89 | display: none; 90 | } 91 | 92 | /* Links 93 | ========================================================================== */ 94 | 95 | /** 96 | * Remove the gray background color from active links in IE 10. 97 | */ 98 | 99 | a { 100 | background-color: transparent; 101 | } 102 | 103 | /** 104 | * Improve readability when focused and also mouse hovered in all browsers. 105 | */ 106 | 107 | a:active, 108 | a:hover { 109 | outline: 0; 110 | } 111 | 112 | /* Text-level semantics 113 | ========================================================================== */ 114 | 115 | /** 116 | * Address styling not present in IE 8/9/10/11, Safari, and Chrome. 117 | */ 118 | 119 | abbr[title] { 120 | border-bottom: 1px dotted; 121 | } 122 | 123 | /** 124 | * Address style set to `bolder` in Firefox 4+, Safari, and Chrome. 125 | */ 126 | 127 | b, 128 | strong { 129 | font-weight: bold; 130 | } 131 | 132 | /** 133 | * Address styling not present in Safari and Chrome. 134 | */ 135 | 136 | dfn { 137 | font-style: italic; 138 | } 139 | 140 | /** 141 | * Address variable `h1` font-size and margin within `section` and `article` 142 | * contexts in Firefox 4+, Safari, and Chrome. 143 | */ 144 | 145 | h1 { 146 | font-size: 2em; 147 | margin: 0.67em 0; 148 | } 149 | 150 | /** 151 | * Address styling not present in IE 8/9. 152 | */ 153 | 154 | mark { 155 | background: #ff0; 156 | color: #000; 157 | } 158 | 159 | /** 160 | * Address inconsistent and variable font size in all browsers. 161 | */ 162 | 163 | small { 164 | font-size: 80%; 165 | } 166 | 167 | /** 168 | * Prevent `sub` and `sup` affecting `line-height` in all browsers. 169 | */ 170 | 171 | sub, 172 | sup { 173 | font-size: 75%; 174 | line-height: 0; 175 | position: relative; 176 | vertical-align: baseline; 177 | } 178 | 179 | sup { 180 | top: -0.5em; 181 | } 182 | 183 | sub { 184 | bottom: -0.25em; 185 | } 186 | 187 | /* Embedded content 188 | ========================================================================== */ 189 | 190 | /** 191 | * Remove border when inside `a` element in IE 8/9/10. 192 | */ 193 | 194 | img { 195 | border: 0; 196 | } 197 | 198 | /** 199 | * Correct overflow not hidden in IE 9/10/11. 200 | */ 201 | 202 | svg:not(:root) { 203 | overflow: hidden; 204 | } 205 | 206 | /* Grouping content 207 | ========================================================================== */ 208 | 209 | /** 210 | * Address margin not present in IE 8/9 and Safari. 211 | */ 212 | 213 | figure { 214 | margin: 1em 40px; 215 | } 216 | 217 | /** 218 | * Address differences between Firefox and other browsers. 219 | */ 220 | 221 | hr { 222 | -moz-box-sizing: content-box; 223 | box-sizing: content-box; 224 | height: 0; 225 | } 226 | 227 | /** 228 | * Contain overflow in all browsers. 229 | */ 230 | 231 | pre { 232 | overflow: auto; 233 | } 234 | 235 | /** 236 | * Address odd `em`-unit font size rendering in all browsers. 237 | */ 238 | 239 | code, 240 | kbd, 241 | pre, 242 | samp { 243 | font-family: monospace, monospace; 244 | font-size: 1em; 245 | } 246 | 247 | /* Forms 248 | ========================================================================== */ 249 | 250 | /** 251 | * Known limitation: by default, Chrome and Safari on OS X allow very limited 252 | * styling of `select`, unless a `border` property is set. 253 | */ 254 | 255 | /** 256 | * 1. Correct color not being inherited. 257 | * Known issue: affects color of disabled elements. 258 | * 2. Correct font properties not being inherited. 259 | * 3. Address margins set differently in Firefox 4+, Safari, and Chrome. 260 | */ 261 | 262 | button, 263 | input, 264 | optgroup, 265 | select, 266 | textarea { 267 | color: inherit; /* 1 */ 268 | font: inherit; /* 2 */ 269 | margin: 0; /* 3 */ 270 | } 271 | 272 | /** 273 | * Address `overflow` set to `hidden` in IE 8/9/10/11. 274 | */ 275 | 276 | button { 277 | overflow: visible; 278 | } 279 | 280 | /** 281 | * Address inconsistent `text-transform` inheritance for `button` and `select`. 282 | * All other form control elements do not inherit `text-transform` values. 283 | * Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera. 284 | * Correct `select` style inheritance in Firefox. 285 | */ 286 | 287 | button, 288 | select { 289 | text-transform: none; 290 | } 291 | 292 | /** 293 | * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio` 294 | * and `video` controls. 295 | * 2. Correct inability to style clickable `input` types in iOS. 296 | * 3. Improve usability and consistency of cursor style between image-type 297 | * `input` and others. 298 | */ 299 | 300 | button, 301 | html input[type="button"], /* 1 */ 302 | input[type="reset"], 303 | input[type="submit"] { 304 | -webkit-appearance: button; /* 2 */ 305 | cursor: pointer; /* 3 */ 306 | } 307 | 308 | /** 309 | * Re-set default cursor for disabled elements. 310 | */ 311 | 312 | button[disabled], 313 | html input[disabled] { 314 | cursor: default; 315 | } 316 | 317 | /** 318 | * Remove inner padding and border in Firefox 4+. 319 | */ 320 | 321 | button::-moz-focus-inner, 322 | input::-moz-focus-inner { 323 | border: 0; 324 | padding: 0; 325 | } 326 | 327 | /** 328 | * Address Firefox 4+ setting `line-height` on `input` using `!important` in 329 | * the UA stylesheet. 330 | */ 331 | 332 | input { 333 | line-height: normal; 334 | } 335 | 336 | /** 337 | * It's recommended that you don't attempt to style these elements. 338 | * Firefox's implementation doesn't respect box-sizing, padding, or width. 339 | * 340 | * 1. Address box sizing set to `content-box` in IE 8/9/10. 341 | * 2. Remove excess padding in IE 8/9/10. 342 | */ 343 | 344 | input[type="checkbox"], 345 | input[type="radio"] { 346 | box-sizing: border-box; /* 1 */ 347 | padding: 0; /* 2 */ 348 | } 349 | 350 | /** 351 | * Fix the cursor style for Chrome's increment/decrement buttons. For certain 352 | * `font-size` values of the `input`, it causes the cursor style of the 353 | * decrement button to change from `default` to `text`. 354 | */ 355 | 356 | input[type="number"]::-webkit-inner-spin-button, 357 | input[type="number"]::-webkit-outer-spin-button { 358 | height: auto; 359 | } 360 | 361 | /** 362 | * 1. Address `appearance` set to `searchfield` in Safari and Chrome. 363 | * 2. Address `box-sizing` set to `border-box` in Safari and Chrome 364 | * (include `-moz` to future-proof). 365 | */ 366 | 367 | input[type="search"] { 368 | -webkit-appearance: textfield; /* 1 */ 369 | -moz-box-sizing: content-box; 370 | -webkit-box-sizing: content-box; /* 2 */ 371 | box-sizing: content-box; 372 | } 373 | 374 | /** 375 | * Remove inner padding and search cancel button in Safari and Chrome on OS X. 376 | * Safari (but not Chrome) clips the cancel button when the search input has 377 | * padding (and `textfield` appearance). 378 | */ 379 | 380 | input[type="search"]::-webkit-search-cancel-button, 381 | input[type="search"]::-webkit-search-decoration { 382 | -webkit-appearance: none; 383 | } 384 | 385 | /** 386 | * Define consistent border, margin, and padding. 387 | */ 388 | 389 | fieldset { 390 | border: 1px solid #c0c0c0; 391 | margin: 0 2px; 392 | padding: 0.35em 0.625em 0.75em; 393 | } 394 | 395 | /** 396 | * 1. Correct `color` not being inherited in IE 8/9/10/11. 397 | * 2. Remove padding so people aren't caught out if they zero out fieldsets. 398 | */ 399 | 400 | legend { 401 | border: 0; /* 1 */ 402 | padding: 0; /* 2 */ 403 | } 404 | 405 | /** 406 | * Remove default vertical scrollbar in IE 8/9/10/11. 407 | */ 408 | 409 | textarea { 410 | overflow: auto; 411 | } 412 | 413 | /** 414 | * Don't inherit the `font-weight` (applied by a rule above). 415 | * NOTE: the default cannot safely be changed in Chrome and Safari on OS X. 416 | */ 417 | 418 | optgroup { 419 | font-weight: bold; 420 | } 421 | 422 | /* Tables 423 | ========================================================================== */ 424 | 425 | /** 426 | * Remove most spacing between table cells. 427 | */ 428 | 429 | table { 430 | border-collapse: collapse; 431 | border-spacing: 0; 432 | } 433 | 434 | td, 435 | th { 436 | padding: 0; 437 | } 438 | 439 | /*csslint important:false*/ 440 | 441 | /* ========================================================================== 442 | Pure Base Extras 443 | ========================================================================== */ 444 | 445 | /** 446 | * Extra rules that Pure adds on top of Normalize.css 447 | */ 448 | 449 | /** 450 | * Always hide an element when it has the `hidden` HTML attribute. 451 | */ 452 | 453 | .hidden, 454 | [hidden] { 455 | display: none !important; 456 | } 457 | 458 | /** 459 | * Add this class to an image to make it fit within it's fluid parent wrapper while maintaining 460 | * aspect ratio. 461 | */ 462 | .pure-img { 463 | max-width: 100%; 464 | height: auto; 465 | display: block; 466 | } 467 | 468 | /*csslint regex-selectors:false, known-properties:false, duplicate-properties:false*/ 469 | 470 | .pure-g { 471 | letter-spacing: -0.31em; /* Webkit: collapse white-space between units */ 472 | *letter-spacing: normal; /* reset IE < 8 */ 473 | *word-spacing: -0.43em; /* IE < 8: collapse white-space between units */ 474 | text-rendering: optimizespeed; /* Webkit: fixes text-rendering: optimizeLegibility */ 475 | 476 | /* 477 | Sets the font stack to fonts known to work properly with the above letter 478 | and word spacings. See: https://github.com/yahoo/pure/issues/41/ 479 | 480 | The following font stack makes Pure Grids work on all known environments. 481 | 482 | * FreeSans: Ships with many Linux distros, including Ubuntu 483 | 484 | * Arimo: Ships with Chrome OS. Arimo has to be defined before Helvetica and 485 | Arial to get picked up by the browser, even though neither is available 486 | in Chrome OS. 487 | 488 | * Droid Sans: Ships with all versions of Android. 489 | 490 | * Helvetica, Arial, sans-serif: Common font stack on OS X and Windows. 491 | */ 492 | font-family: FreeSans, Arimo, "Droid Sans", Helvetica, Arial, sans-serif; 493 | 494 | /* 495 | Use flexbox when possible to avoid `letter-spacing` side-effects. 496 | 497 | NOTE: Firefox (as of 25) does not currently support flex-wrap, so the 498 | `-moz-` prefix version is omitted. 499 | */ 500 | 501 | display: -webkit-flex; 502 | -webkit-flex-flow: row wrap; 503 | 504 | /* IE10 uses display: flexbox */ 505 | display: -ms-flexbox; 506 | -ms-flex-flow: row wrap; 507 | 508 | /* Prevents distributing space between rows */ 509 | -ms-align-content: flex-start; 510 | -webkit-align-content: flex-start; 511 | align-content: flex-start; 512 | } 513 | 514 | /* Opera as of 12 on Windows needs word-spacing. 515 | The ".opera-only" selector is used to prevent actual prefocus styling 516 | and is not required in markup. 517 | */ 518 | .opera-only :-o-prefocus, 519 | .pure-g { 520 | word-spacing: -0.43em; 521 | } 522 | 523 | .pure-u { 524 | display: inline-block; 525 | *display: inline; /* IE < 8: fake inline-block */ 526 | zoom: 1; 527 | letter-spacing: normal; 528 | word-spacing: normal; 529 | vertical-align: top; 530 | text-rendering: auto; 531 | } 532 | 533 | /* 534 | Resets the font family back to the OS/browser's default sans-serif font, 535 | this the same font stack that Normalize.css sets for the `body`. 536 | */ 537 | .pure-g [class *= "pure-u"] { 538 | font-family: sans-serif; 539 | } 540 | 541 | .pure-u-1, 542 | .pure-u-1-1, 543 | .pure-u-1-2, 544 | .pure-u-1-3, 545 | .pure-u-2-3, 546 | .pure-u-1-4, 547 | .pure-u-3-4, 548 | .pure-u-1-5, 549 | .pure-u-2-5, 550 | .pure-u-3-5, 551 | .pure-u-4-5, 552 | .pure-u-5-5, 553 | .pure-u-1-6, 554 | .pure-u-5-6, 555 | .pure-u-1-8, 556 | .pure-u-3-8, 557 | .pure-u-5-8, 558 | .pure-u-7-8, 559 | .pure-u-1-12, 560 | .pure-u-5-12, 561 | .pure-u-7-12, 562 | .pure-u-11-12, 563 | .pure-u-1-24, 564 | .pure-u-2-24, 565 | .pure-u-3-24, 566 | .pure-u-4-24, 567 | .pure-u-5-24, 568 | .pure-u-6-24, 569 | .pure-u-7-24, 570 | .pure-u-8-24, 571 | .pure-u-9-24, 572 | .pure-u-10-24, 573 | .pure-u-11-24, 574 | .pure-u-12-24, 575 | .pure-u-13-24, 576 | .pure-u-14-24, 577 | .pure-u-15-24, 578 | .pure-u-16-24, 579 | .pure-u-17-24, 580 | .pure-u-18-24, 581 | .pure-u-19-24, 582 | .pure-u-20-24, 583 | .pure-u-21-24, 584 | .pure-u-22-24, 585 | .pure-u-23-24, 586 | .pure-u-24-24 { 587 | display: inline-block; 588 | *display: inline; 589 | zoom: 1; 590 | letter-spacing: normal; 591 | word-spacing: normal; 592 | vertical-align: top; 593 | text-rendering: auto; 594 | } 595 | 596 | .pure-u-1-24 { 597 | width: 4.1667%; 598 | *width: 4.1357%; 599 | } 600 | 601 | .pure-u-1-12, 602 | .pure-u-2-24 { 603 | width: 8.3333%; 604 | *width: 8.3023%; 605 | } 606 | 607 | .pure-u-1-8, 608 | .pure-u-3-24 { 609 | width: 12.5000%; 610 | *width: 12.4690%; 611 | } 612 | 613 | .pure-u-1-6, 614 | .pure-u-4-24 { 615 | width: 16.6667%; 616 | *width: 16.6357%; 617 | } 618 | 619 | .pure-u-1-5 { 620 | width: 20%; 621 | *width: 19.9690%; 622 | } 623 | 624 | .pure-u-5-24 { 625 | width: 20.8333%; 626 | *width: 20.8023%; 627 | } 628 | 629 | .pure-u-1-4, 630 | .pure-u-6-24 { 631 | width: 25%; 632 | *width: 24.9690%; 633 | } 634 | 635 | .pure-u-7-24 { 636 | width: 29.1667%; 637 | *width: 29.1357%; 638 | } 639 | 640 | .pure-u-1-3, 641 | .pure-u-8-24 { 642 | width: 33.3333%; 643 | *width: 33.3023%; 644 | } 645 | 646 | .pure-u-3-8, 647 | .pure-u-9-24 { 648 | width: 37.5000%; 649 | *width: 37.4690%; 650 | } 651 | 652 | .pure-u-2-5 { 653 | width: 40%; 654 | *width: 39.9690%; 655 | } 656 | 657 | .pure-u-5-12, 658 | .pure-u-10-24 { 659 | width: 41.6667%; 660 | *width: 41.6357%; 661 | } 662 | 663 | .pure-u-11-24 { 664 | width: 45.8333%; 665 | *width: 45.8023%; 666 | } 667 | 668 | .pure-u-1-2, 669 | .pure-u-12-24 { 670 | width: 50%; 671 | *width: 49.9690%; 672 | } 673 | 674 | .pure-u-13-24 { 675 | width: 54.1667%; 676 | *width: 54.1357%; 677 | } 678 | 679 | .pure-u-7-12, 680 | .pure-u-14-24 { 681 | width: 58.3333%; 682 | *width: 58.3023%; 683 | } 684 | 685 | .pure-u-3-5 { 686 | width: 60%; 687 | *width: 59.9690%; 688 | } 689 | 690 | .pure-u-5-8, 691 | .pure-u-15-24 { 692 | width: 62.5000%; 693 | *width: 62.4690%; 694 | } 695 | 696 | .pure-u-2-3, 697 | .pure-u-16-24 { 698 | width: 66.6667%; 699 | *width: 66.6357%; 700 | } 701 | 702 | .pure-u-17-24 { 703 | width: 70.8333%; 704 | *width: 70.8023%; 705 | } 706 | 707 | .pure-u-3-4, 708 | .pure-u-18-24 { 709 | width: 75%; 710 | *width: 74.9690%; 711 | } 712 | 713 | .pure-u-19-24 { 714 | width: 79.1667%; 715 | *width: 79.1357%; 716 | } 717 | 718 | .pure-u-4-5 { 719 | width: 80%; 720 | *width: 79.9690%; 721 | } 722 | 723 | .pure-u-5-6, 724 | .pure-u-20-24 { 725 | width: 83.3333%; 726 | *width: 83.3023%; 727 | } 728 | 729 | .pure-u-7-8, 730 | .pure-u-21-24 { 731 | width: 87.5000%; 732 | *width: 87.4690%; 733 | } 734 | 735 | .pure-u-11-12, 736 | .pure-u-22-24 { 737 | width: 91.6667%; 738 | *width: 91.6357%; 739 | } 740 | 741 | .pure-u-23-24 { 742 | width: 95.8333%; 743 | *width: 95.8023%; 744 | } 745 | 746 | .pure-u-1, 747 | .pure-u-1-1, 748 | .pure-u-5-5, 749 | .pure-u-24-24 { 750 | width: 100%; 751 | } 752 | .pure-button { 753 | /* Structure */ 754 | display: inline-block; 755 | zoom: 1; 756 | line-height: normal; 757 | white-space: nowrap; 758 | vertical-align: middle; 759 | text-align: center; 760 | cursor: pointer; 761 | -webkit-user-drag: none; 762 | -webkit-user-select: none; 763 | -moz-user-select: none; 764 | -ms-user-select: none; 765 | user-select: none; 766 | -webkit-box-sizing: border-box; 767 | -moz-box-sizing: border-box; 768 | box-sizing: border-box; 769 | } 770 | 771 | /* Firefox: Get rid of the inner focus border */ 772 | .pure-button::-moz-focus-inner { 773 | padding: 0; 774 | border: 0; 775 | } 776 | 777 | /*csslint outline-none:false*/ 778 | 779 | .pure-button { 780 | font-family: inherit; 781 | font-size: 100%; 782 | padding: 0.5em 1em; 783 | color: #444; /* rgba not supported (IE 8) */ 784 | color: rgba(0, 0, 0, 0.80); /* rgba supported */ 785 | border: 1px solid #999; /*IE 6/7/8*/ 786 | border: none rgba(0, 0, 0, 0); /*IE9 + everything else*/ 787 | background-color: #E6E6E6; 788 | text-decoration: none; 789 | border-radius: 2px; 790 | } 791 | 792 | .pure-button-hover, 793 | .pure-button:hover, 794 | .pure-button:focus { 795 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#00000000', endColorstr='#1a000000',GradientType=0); 796 | background-image: -webkit-gradient(linear, 0 0, 0 100%, from(transparent), color-stop(40%, rgba(0,0,0, 0.05)), to(rgba(0,0,0, 0.10))); 797 | background-image: -webkit-linear-gradient(transparent, rgba(0,0,0, 0.05) 40%, rgba(0,0,0, 0.10)); 798 | background-image: -moz-linear-gradient(top, rgba(0,0,0, 0.05) 0%, rgba(0,0,0, 0.10)); 799 | background-image: -o-linear-gradient(transparent, rgba(0,0,0, 0.05) 40%, rgba(0,0,0, 0.10)); 800 | background-image: linear-gradient(transparent, rgba(0,0,0, 0.05) 40%, rgba(0,0,0, 0.10)); 801 | } 802 | .pure-button:focus { 803 | outline: 0; 804 | } 805 | .pure-button-active, 806 | .pure-button:active { 807 | box-shadow: 0 0 0 1px rgba(0,0,0, 0.15) inset, 0 0 6px rgba(0,0,0, 0.20) inset; 808 | border-color: #000\9; 809 | } 810 | 811 | .pure-button[disabled], 812 | .pure-button-disabled, 813 | .pure-button-disabled:hover, 814 | .pure-button-disabled:focus, 815 | .pure-button-disabled:active { 816 | border: none; 817 | background-image: none; 818 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 819 | filter: alpha(opacity=40); 820 | -khtml-opacity: 0.40; 821 | -moz-opacity: 0.40; 822 | opacity: 0.40; 823 | cursor: not-allowed; 824 | box-shadow: none; 825 | } 826 | 827 | .pure-button-hidden { 828 | display: none; 829 | } 830 | 831 | /* Firefox: Get rid of the inner focus border */ 832 | .pure-button::-moz-focus-inner{ 833 | padding: 0; 834 | border: 0; 835 | } 836 | 837 | .pure-button-primary, 838 | .pure-button-selected, 839 | a.pure-button-primary, 840 | a.pure-button-selected { 841 | background-color: rgb(0, 120, 231); 842 | color: #fff; 843 | } 844 | 845 | /*csslint box-model:false*/ 846 | /* 847 | Box-model set to false because we're setting a height on select elements, which 848 | also have border and padding. This is done because some browsers don't render 849 | the padding. We explicitly set the box-model for select elements to border-box, 850 | so we can ignore the csslint warning. 851 | */ 852 | 853 | .pure-form input[type="text"], 854 | .pure-form input[type="password"], 855 | .pure-form input[type="email"], 856 | .pure-form input[type="url"], 857 | .pure-form input[type="date"], 858 | .pure-form input[type="month"], 859 | .pure-form input[type="time"], 860 | .pure-form input[type="datetime"], 861 | .pure-form input[type="datetime-local"], 862 | .pure-form input[type="week"], 863 | .pure-form input[type="number"], 864 | .pure-form input[type="search"], 865 | .pure-form input[type="tel"], 866 | .pure-form input[type="color"], 867 | .pure-form select, 868 | .pure-form textarea { 869 | padding: 0.5em 0.6em; 870 | display: inline-block; 871 | border: 1px solid #ccc; 872 | box-shadow: inset 0 1px 3px #ddd; 873 | border-radius: 4px; 874 | vertical-align: middle; 875 | -webkit-box-sizing: border-box; 876 | -moz-box-sizing: border-box; 877 | box-sizing: border-box; 878 | } 879 | 880 | /* 881 | Need to separate out the :not() selector from the rest of the CSS 2.1 selectors 882 | since IE8 won't execute CSS that contains a CSS3 selector. 883 | */ 884 | .pure-form input:not([type]) { 885 | padding: 0.5em 0.6em; 886 | display: inline-block; 887 | border: 1px solid #ccc; 888 | box-shadow: inset 0 1px 3px #ddd; 889 | border-radius: 4px; 890 | -webkit-box-sizing: border-box; 891 | -moz-box-sizing: border-box; 892 | box-sizing: border-box; 893 | } 894 | 895 | 896 | /* Chrome (as of v.32/34 on OS X) needs additional room for color to display. */ 897 | /* May be able to remove this tweak as color inputs become more standardized across browsers. */ 898 | .pure-form input[type="color"] { 899 | padding: 0.2em 0.5em; 900 | } 901 | 902 | 903 | .pure-form input[type="text"]:focus, 904 | .pure-form input[type="password"]:focus, 905 | .pure-form input[type="email"]:focus, 906 | .pure-form input[type="url"]:focus, 907 | .pure-form input[type="date"]:focus, 908 | .pure-form input[type="month"]:focus, 909 | .pure-form input[type="time"]:focus, 910 | .pure-form input[type="datetime"]:focus, 911 | .pure-form input[type="datetime-local"]:focus, 912 | .pure-form input[type="week"]:focus, 913 | .pure-form input[type="number"]:focus, 914 | .pure-form input[type="search"]:focus, 915 | .pure-form input[type="tel"]:focus, 916 | .pure-form input[type="color"]:focus, 917 | .pure-form select:focus, 918 | .pure-form textarea:focus { 919 | outline: 0; 920 | border-color: #129FEA; 921 | } 922 | 923 | /* 924 | Need to separate out the :not() selector from the rest of the CSS 2.1 selectors 925 | since IE8 won't execute CSS that contains a CSS3 selector. 926 | */ 927 | .pure-form input:not([type]):focus { 928 | outline: 0; 929 | border-color: #129FEA; 930 | } 931 | 932 | .pure-form input[type="file"]:focus, 933 | .pure-form input[type="radio"]:focus, 934 | .pure-form input[type="checkbox"]:focus { 935 | outline: thin solid #129FEA; 936 | outline: 1px auto #129FEA; 937 | } 938 | .pure-form .pure-checkbox, 939 | .pure-form .pure-radio { 940 | margin: 0.5em 0; 941 | display: block; 942 | } 943 | 944 | .pure-form input[type="text"][disabled], 945 | .pure-form input[type="password"][disabled], 946 | .pure-form input[type="email"][disabled], 947 | .pure-form input[type="url"][disabled], 948 | .pure-form input[type="date"][disabled], 949 | .pure-form input[type="month"][disabled], 950 | .pure-form input[type="time"][disabled], 951 | .pure-form input[type="datetime"][disabled], 952 | .pure-form input[type="datetime-local"][disabled], 953 | .pure-form input[type="week"][disabled], 954 | .pure-form input[type="number"][disabled], 955 | .pure-form input[type="search"][disabled], 956 | .pure-form input[type="tel"][disabled], 957 | .pure-form input[type="color"][disabled], 958 | .pure-form select[disabled], 959 | .pure-form textarea[disabled] { 960 | cursor: not-allowed; 961 | background-color: #eaeded; 962 | color: #cad2d3; 963 | } 964 | 965 | /* 966 | Need to separate out the :not() selector from the rest of the CSS 2.1 selectors 967 | since IE8 won't execute CSS that contains a CSS3 selector. 968 | */ 969 | .pure-form input:not([type])[disabled] { 970 | cursor: not-allowed; 971 | background-color: #eaeded; 972 | color: #cad2d3; 973 | } 974 | .pure-form input[readonly], 975 | .pure-form select[readonly], 976 | .pure-form textarea[readonly] { 977 | background-color: #eee; /* menu hover bg color */ 978 | color: #777; /* menu text color */ 979 | border-color: #ccc; 980 | } 981 | 982 | .pure-form input:focus:invalid, 983 | .pure-form textarea:focus:invalid, 984 | .pure-form select:focus:invalid { 985 | color: #b94a48; 986 | border-color: #e9322d; 987 | } 988 | .pure-form input[type="file"]:focus:invalid:focus, 989 | .pure-form input[type="radio"]:focus:invalid:focus, 990 | .pure-form input[type="checkbox"]:focus:invalid:focus { 991 | outline-color: #e9322d; 992 | } 993 | .pure-form select { 994 | /* Normalizes the height; padding is not sufficient. */ 995 | height: 2.25em; 996 | border: 1px solid #ccc; 997 | background-color: white; 998 | } 999 | .pure-form select[multiple] { 1000 | height: auto; 1001 | } 1002 | .pure-form label { 1003 | margin: 0.5em 0 0.2em; 1004 | } 1005 | .pure-form fieldset { 1006 | margin: 0; 1007 | padding: 0.35em 0 0.75em; 1008 | border: 0; 1009 | } 1010 | .pure-form legend { 1011 | display: block; 1012 | width: 100%; 1013 | padding: 0.3em 0; 1014 | margin-bottom: 0.3em; 1015 | color: #333; 1016 | border-bottom: 1px solid #e5e5e5; 1017 | } 1018 | 1019 | .pure-form-stacked input[type="text"], 1020 | .pure-form-stacked input[type="password"], 1021 | .pure-form-stacked input[type="email"], 1022 | .pure-form-stacked input[type="url"], 1023 | .pure-form-stacked input[type="date"], 1024 | .pure-form-stacked input[type="month"], 1025 | .pure-form-stacked input[type="time"], 1026 | .pure-form-stacked input[type="datetime"], 1027 | .pure-form-stacked input[type="datetime-local"], 1028 | .pure-form-stacked input[type="week"], 1029 | .pure-form-stacked input[type="number"], 1030 | .pure-form-stacked input[type="search"], 1031 | .pure-form-stacked input[type="tel"], 1032 | .pure-form-stacked input[type="color"], 1033 | .pure-form-stacked input[type="file"], 1034 | .pure-form-stacked select, 1035 | .pure-form-stacked label, 1036 | .pure-form-stacked textarea { 1037 | display: block; 1038 | margin: 0.25em 0; 1039 | } 1040 | 1041 | /* 1042 | Need to separate out the :not() selector from the rest of the CSS 2.1 selectors 1043 | since IE8 won't execute CSS that contains a CSS3 selector. 1044 | */ 1045 | .pure-form-stacked input:not([type]) { 1046 | display: block; 1047 | margin: 0.25em 0; 1048 | } 1049 | .pure-form-aligned input, 1050 | .pure-form-aligned textarea, 1051 | .pure-form-aligned select, 1052 | /* NOTE: pure-help-inline is deprecated. Use .pure-form-message-inline instead. */ 1053 | .pure-form-aligned .pure-help-inline, 1054 | .pure-form-message-inline { 1055 | display: inline-block; 1056 | *display: inline; 1057 | *zoom: 1; 1058 | vertical-align: middle; 1059 | } 1060 | .pure-form-aligned textarea { 1061 | vertical-align: top; 1062 | } 1063 | 1064 | /* Aligned Forms */ 1065 | .pure-form-aligned .pure-control-group { 1066 | margin-bottom: 0.5em; 1067 | } 1068 | .pure-form-aligned .pure-control-group label { 1069 | text-align: right; 1070 | display: inline-block; 1071 | vertical-align: middle; 1072 | width: 10em; 1073 | margin: 0 1em 0 0; 1074 | } 1075 | .pure-form-aligned .pure-controls { 1076 | margin: 1.5em 0 0 11em; 1077 | } 1078 | 1079 | /* Rounded Inputs */ 1080 | .pure-form input.pure-input-rounded, 1081 | .pure-form .pure-input-rounded { 1082 | border-radius: 2em; 1083 | padding: 0.5em 1em; 1084 | } 1085 | 1086 | /* Grouped Inputs */ 1087 | .pure-form .pure-group fieldset { 1088 | margin-bottom: 10px; 1089 | } 1090 | .pure-form .pure-group input, 1091 | .pure-form .pure-group textarea { 1092 | display: block; 1093 | padding: 10px; 1094 | margin: 0 0 -1px; 1095 | border-radius: 0; 1096 | position: relative; 1097 | top: -1px; 1098 | } 1099 | .pure-form .pure-group input:focus, 1100 | .pure-form .pure-group textarea:focus { 1101 | z-index: 3; 1102 | } 1103 | .pure-form .pure-group input:first-child, 1104 | .pure-form .pure-group textarea:first-child { 1105 | top: 1px; 1106 | border-radius: 4px 4px 0 0; 1107 | margin: 0; 1108 | } 1109 | .pure-form .pure-group input:first-child:last-child, 1110 | .pure-form .pure-group textarea:first-child:last-child { 1111 | top: 1px; 1112 | border-radius: 4px; 1113 | margin: 0; 1114 | } 1115 | .pure-form .pure-group input:last-child, 1116 | .pure-form .pure-group textarea:last-child { 1117 | top: -2px; 1118 | border-radius: 0 0 4px 4px; 1119 | margin: 0; 1120 | } 1121 | .pure-form .pure-group button { 1122 | margin: 0.35em 0; 1123 | } 1124 | 1125 | .pure-form .pure-input-1 { 1126 | width: 100%; 1127 | } 1128 | .pure-form .pure-input-2-3 { 1129 | width: 66%; 1130 | } 1131 | .pure-form .pure-input-1-2 { 1132 | width: 50%; 1133 | } 1134 | .pure-form .pure-input-1-3 { 1135 | width: 33%; 1136 | } 1137 | .pure-form .pure-input-1-4 { 1138 | width: 25%; 1139 | } 1140 | 1141 | /* Inline help for forms */ 1142 | /* NOTE: pure-help-inline is deprecated. Use .pure-form-message-inline instead. */ 1143 | .pure-form .pure-help-inline, 1144 | .pure-form-message-inline { 1145 | display: inline-block; 1146 | padding-left: 0.3em; 1147 | color: #666; 1148 | vertical-align: middle; 1149 | font-size: 0.875em; 1150 | } 1151 | 1152 | /* Block help for forms */ 1153 | .pure-form-message { 1154 | display: block; 1155 | color: #666; 1156 | font-size: 0.875em; 1157 | } 1158 | 1159 | @media only screen and (max-width : 480px) { 1160 | .pure-form button[type="submit"] { 1161 | margin: 0.7em 0 0; 1162 | } 1163 | 1164 | .pure-form input:not([type]), 1165 | .pure-form input[type="text"], 1166 | .pure-form input[type="password"], 1167 | .pure-form input[type="email"], 1168 | .pure-form input[type="url"], 1169 | .pure-form input[type="date"], 1170 | .pure-form input[type="month"], 1171 | .pure-form input[type="time"], 1172 | .pure-form input[type="datetime"], 1173 | .pure-form input[type="datetime-local"], 1174 | .pure-form input[type="week"], 1175 | .pure-form input[type="number"], 1176 | .pure-form input[type="search"], 1177 | .pure-form input[type="tel"], 1178 | .pure-form input[type="color"], 1179 | .pure-form label { 1180 | margin-bottom: 0.3em; 1181 | display: block; 1182 | } 1183 | 1184 | .pure-group input:not([type]), 1185 | .pure-group input[type="text"], 1186 | .pure-group input[type="password"], 1187 | .pure-group input[type="email"], 1188 | .pure-group input[type="url"], 1189 | .pure-group input[type="date"], 1190 | .pure-group input[type="month"], 1191 | .pure-group input[type="time"], 1192 | .pure-group input[type="datetime"], 1193 | .pure-group input[type="datetime-local"], 1194 | .pure-group input[type="week"], 1195 | .pure-group input[type="number"], 1196 | .pure-group input[type="search"], 1197 | .pure-group input[type="tel"], 1198 | .pure-group input[type="color"] { 1199 | margin-bottom: 0; 1200 | } 1201 | 1202 | .pure-form-aligned .pure-control-group label { 1203 | margin-bottom: 0.3em; 1204 | text-align: left; 1205 | display: block; 1206 | width: 100%; 1207 | } 1208 | 1209 | .pure-form-aligned .pure-controls { 1210 | margin: 1.5em 0 0 0; 1211 | } 1212 | 1213 | /* NOTE: pure-help-inline is deprecated. Use .pure-form-message-inline instead. */ 1214 | .pure-form .pure-help-inline, 1215 | .pure-form-message-inline, 1216 | .pure-form-message { 1217 | display: block; 1218 | font-size: 0.75em; 1219 | /* Increased bottom padding to make it group with its related input element. */ 1220 | padding: 0.2em 0 0.8em; 1221 | } 1222 | } 1223 | 1224 | /*csslint adjoining-classes: false, box-model:false*/ 1225 | .pure-menu { 1226 | -webkit-box-sizing: border-box; 1227 | -moz-box-sizing: border-box; 1228 | box-sizing: border-box; 1229 | } 1230 | 1231 | .pure-menu-fixed { 1232 | position: fixed; 1233 | left: 0; 1234 | top: 0; 1235 | z-index: 3; 1236 | } 1237 | 1238 | .pure-menu-list, 1239 | .pure-menu-item { 1240 | position: relative; 1241 | } 1242 | 1243 | .pure-menu-list { 1244 | list-style: none; 1245 | margin: 0; 1246 | padding: 0; 1247 | } 1248 | 1249 | .pure-menu-item { 1250 | padding: 0; 1251 | margin: 0; 1252 | height: 100%; 1253 | } 1254 | 1255 | .pure-menu-link, 1256 | .pure-menu-heading { 1257 | display: block; 1258 | text-decoration: none; 1259 | white-space: nowrap; 1260 | } 1261 | 1262 | /* HORIZONTAL MENU */ 1263 | .pure-menu-horizontal { 1264 | width: 100%; 1265 | white-space: nowrap; 1266 | } 1267 | 1268 | .pure-menu-horizontal .pure-menu-list { 1269 | display: inline-block; 1270 | } 1271 | 1272 | /* Initial menus should be inline-block so that they are horizontal */ 1273 | .pure-menu-horizontal .pure-menu-item, 1274 | .pure-menu-horizontal .pure-menu-heading, 1275 | .pure-menu-horizontal .pure-menu-separator { 1276 | display: inline-block; 1277 | *display: inline; 1278 | zoom: 1; 1279 | vertical-align: middle; 1280 | } 1281 | 1282 | /* Submenus should still be display: block; */ 1283 | .pure-menu-item .pure-menu-item { 1284 | display: block; 1285 | } 1286 | 1287 | .pure-menu-children { 1288 | display: none; 1289 | position: absolute; 1290 | left: 100%; 1291 | top: 0; 1292 | margin: 0; 1293 | padding: 0; 1294 | z-index: 3; 1295 | } 1296 | 1297 | .pure-menu-horizontal .pure-menu-children { 1298 | left: 0; 1299 | top: auto; 1300 | width: inherit; 1301 | } 1302 | 1303 | .pure-menu-allow-hover:hover > .pure-menu-children, 1304 | .pure-menu-active > .pure-menu-children { 1305 | display: block; 1306 | position: absolute; 1307 | } 1308 | 1309 | /* Vertical Menus - show the dropdown arrow */ 1310 | .pure-menu-has-children > .pure-menu-link:after { 1311 | padding-left: 0.5em; 1312 | content: "\25B8"; 1313 | font-size: small; 1314 | } 1315 | 1316 | /* Horizontal Menus - show the dropdown arrow */ 1317 | .pure-menu-horizontal .pure-menu-has-children > .pure-menu-link:after { 1318 | content: "\25BE"; 1319 | } 1320 | 1321 | /* scrollable menus */ 1322 | .pure-menu-scrollable { 1323 | overflow-y: scroll; 1324 | overflow-x: hidden; 1325 | } 1326 | 1327 | .pure-menu-scrollable .pure-menu-list { 1328 | display: block; 1329 | } 1330 | 1331 | .pure-menu-horizontal.pure-menu-scrollable .pure-menu-list { 1332 | display: inline-block; 1333 | } 1334 | 1335 | .pure-menu-horizontal.pure-menu-scrollable { 1336 | white-space: nowrap; 1337 | overflow-y: hidden; 1338 | overflow-x: auto; 1339 | -ms-overflow-style: none; 1340 | -webkit-overflow-scrolling: touch; 1341 | /* a little extra padding for this style to allow for scrollbars */ 1342 | padding: .5em 0; 1343 | } 1344 | 1345 | .pure-menu-horizontal.pure-menu-scrollable::-webkit-scrollbar { 1346 | display: none; 1347 | } 1348 | 1349 | /* misc default styling */ 1350 | 1351 | .pure-menu-separator { 1352 | background-color: #ccc; 1353 | height: 1px; 1354 | margin: .3em 0; 1355 | } 1356 | 1357 | .pure-menu-horizontal .pure-menu-separator { 1358 | width: 1px; 1359 | height: 1.3em; 1360 | margin: 0 .3em ; 1361 | } 1362 | 1363 | .pure-menu-heading { 1364 | text-transform: uppercase; 1365 | color: #565d64; 1366 | } 1367 | 1368 | .pure-menu-link { 1369 | color: #777; 1370 | } 1371 | 1372 | .pure-menu-children { 1373 | background-color: #fff; 1374 | } 1375 | 1376 | .pure-menu-link, 1377 | .pure-menu-disabled, 1378 | .pure-menu-heading { 1379 | padding: .5em 1em; 1380 | } 1381 | 1382 | .pure-menu-disabled { 1383 | opacity: .5; 1384 | } 1385 | 1386 | .pure-menu-disabled .pure-menu-link:hover { 1387 | background-color: transparent; 1388 | } 1389 | 1390 | .pure-menu-active > .pure-menu-link, 1391 | .pure-menu-link:hover, 1392 | .pure-menu-link:focus { 1393 | background-color: #eee; 1394 | } 1395 | 1396 | .pure-menu-selected .pure-menu-link, 1397 | .pure-menu-selected .pure-menu-link:visited { 1398 | color: #000; 1399 | } 1400 | 1401 | .pure-table { 1402 | /* Remove spacing between table cells (from Normalize.css) */ 1403 | border-collapse: collapse; 1404 | border-spacing: 0; 1405 | empty-cells: show; 1406 | border: 1px solid #cbcbcb; 1407 | } 1408 | 1409 | .pure-table caption { 1410 | color: #000; 1411 | font: italic 85%/1 arial, sans-serif; 1412 | padding: 1em 0; 1413 | text-align: center; 1414 | } 1415 | 1416 | .pure-table td, 1417 | .pure-table th { 1418 | border-left: 1px solid #cbcbcb;/* inner column border */ 1419 | border-width: 0 0 0 1px; 1420 | font-size: inherit; 1421 | margin: 0; 1422 | overflow: visible; /*to make ths where the title is really long work*/ 1423 | padding: 0.5em 1em; /* cell padding */ 1424 | } 1425 | 1426 | /* Consider removing this next declaration block, as it causes problems when 1427 | there's a rowspan on the first cell. Case added to the tests. issue#432 */ 1428 | .pure-table td:first-child, 1429 | .pure-table th:first-child { 1430 | border-left-width: 0; 1431 | } 1432 | 1433 | .pure-table thead { 1434 | background-color: #e0e0e0; 1435 | color: #000; 1436 | text-align: left; 1437 | vertical-align: bottom; 1438 | } 1439 | 1440 | /* 1441 | striping: 1442 | even - #fff (white) 1443 | odd - #f2f2f2 (light gray) 1444 | */ 1445 | .pure-table td { 1446 | background-color: transparent; 1447 | } 1448 | .pure-table-odd td { 1449 | background-color: #f2f2f2; 1450 | } 1451 | 1452 | /* nth-child selector for modern browsers */ 1453 | .pure-table-striped tr:nth-child(2n-1) td { 1454 | background-color: #f2f2f2; 1455 | } 1456 | 1457 | /* BORDERED TABLES */ 1458 | .pure-table-bordered td { 1459 | border-bottom: 1px solid #cbcbcb; 1460 | } 1461 | .pure-table-bordered tbody > tr:last-child > td { 1462 | border-bottom-width: 0; 1463 | } 1464 | 1465 | 1466 | /* HORIZONTAL BORDERED TABLES */ 1467 | 1468 | .pure-table-horizontal td, 1469 | .pure-table-horizontal th { 1470 | border-width: 0 0 1px 0; 1471 | border-bottom: 1px solid #cbcbcb; 1472 | } 1473 | .pure-table-horizontal tbody > tr:last-child > td { 1474 | border-bottom-width: 0; 1475 | } 1476 | -------------------------------------------------------------------------------- /templates/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rexxars/sql-to-graphql/b159d82759a2c8ace38e26b6caa858a011282995/templates/public/favicon.ico -------------------------------------------------------------------------------- /templates/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | sql-to-graphql playground 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /templates/public/js/mode-json.js: -------------------------------------------------------------------------------- 1 | define("ace/mode/json_highlight_rules",["require","exports","module","ace/lib/oop","ace/mode/text_highlight_rules"],function(e,t,n){"use strict";var r=e("../lib/oop"),i=e("./text_highlight_rules").TextHighlightRules,s=function(){this.$rules={start:[{token:"variable",regex:'["](?:(?:\\\\.)|(?:[^"\\\\]))*?["]\\s*(?=:)'},{token:"string",regex:'"',next:"string"},{token:"constant.numeric",regex:"0[xX][0-9a-fA-F]+\\b"},{token:"constant.numeric",regex:"[+-]?\\d+(?:(?:\\.\\d*)?(?:[eE][+-]?\\d+)?)?\\b"},{token:"constant.language.boolean",regex:"(?:true|false)\\b"},{token:"invalid.illegal",regex:"['](?:(?:\\\\.)|(?:[^'\\\\]))*?[']"},{token:"invalid.illegal",regex:"\\/\\/.*$"},{token:"paren.lparen",regex:"[[({]"},{token:"paren.rparen",regex:"[\\])}]"},{token:"text",regex:"\\s+"}],string:[{token:"constant.language.escape",regex:/\\(?:x[0-9a-fA-F]{2}|u[0-9a-fA-F]{4}|["\\\/bfnrt])/},{token:"string",regex:'[^"\\\\]+'},{token:"string",regex:'"',next:"start"},{token:"string",regex:"",next:"start"}]}};r.inherits(s,i),t.JsonHighlightRules=s}),define("ace/mode/matching_brace_outdent",["require","exports","module","ace/range"],function(e,t,n){"use strict";var r=e("../range").Range,i=function(){};(function(){this.checkOutdent=function(e,t){return/^\s+$/.test(e)?/^\s*\}/.test(t):!1},this.autoOutdent=function(e,t){var n=e.getLine(t),i=n.match(/^(\s*\})/);if(!i)return 0;var s=i[1].length,o=e.findMatchingBracket({row:t,column:s});if(!o||o.row==t)return 0;var u=this.$getIndent(e.getLine(o.row));e.replace(new r(t,0,t,s-1),u)},this.$getIndent=function(e){return e.match(/^\s*/)[0]}}).call(i.prototype),t.MatchingBraceOutdent=i}),define("ace/mode/behaviour/cstyle",["require","exports","module","ace/lib/oop","ace/mode/behaviour","ace/token_iterator","ace/lib/lang"],function(e,t,n){"use strict";var r=e("../../lib/oop"),i=e("../behaviour").Behaviour,s=e("../../token_iterator").TokenIterator,o=e("../../lib/lang"),u=["text","paren.rparen","punctuation.operator"],a=["text","paren.rparen","punctuation.operator","comment"],f,l={},c=function(e){var t=-1;e.multiSelect&&(t=e.selection.index,l.rangeCount!=e.multiSelect.rangeCount&&(l={rangeCount:e.multiSelect.rangeCount}));if(l[t])return f=l[t];f=l[t]={autoInsertedBrackets:0,autoInsertedRow:-1,autoInsertedLineEnd:"",maybeInsertedBrackets:0,maybeInsertedRow:-1,maybeInsertedLineStart:"",maybeInsertedLineEnd:""}},h=function(e,t,n,r){var i=e.end.row-e.start.row;return{text:n+t+r,selection:[0,e.start.column+1,i,e.end.column+(i?0:1)]}},p=function(){this.add("braces","insertion",function(e,t,n,r,i){var s=n.getCursorPosition(),u=r.doc.getLine(s.row);if(i=="{"){c(n);var a=n.getSelectionRange(),l=r.doc.getTextRange(a);if(l!==""&&l!=="{"&&n.getWrapBehavioursEnabled())return h(a,l,"{","}");if(p.isSaneInsertion(n,r))return/[\]\}\)]/.test(u[s.column])||n.inMultiSelectMode?(p.recordAutoInsert(n,r,"}"),{text:"{}",selection:[1,1]}):(p.recordMaybeInsert(n,r,"{"),{text:"{",selection:[1,1]})}else if(i=="}"){c(n);var d=u.substring(s.column,s.column+1);if(d=="}"){var v=r.$findOpeningBracket("}",{column:s.column+1,row:s.row});if(v!==null&&p.isAutoInsertedClosing(s,u,i))return p.popAutoInsertedClosing(),{text:"",selection:[1,1]}}}else{if(i=="\n"||i=="\r\n"){c(n);var m="";p.isMaybeInsertedClosing(s,u)&&(m=o.stringRepeat("}",f.maybeInsertedBrackets),p.clearMaybeInsertedClosing());var d=u.substring(s.column,s.column+1);if(d==="}"){var g=r.findMatchingBracket({row:s.row,column:s.column+1},"}");if(!g)return null;var y=this.$getIndent(r.getLine(g.row))}else{if(!m){p.clearMaybeInsertedClosing();return}var y=this.$getIndent(u)}var b=y+r.getTabString();return{text:"\n"+b+"\n"+y+m,selection:[1,b.length,1,b.length]}}p.clearMaybeInsertedClosing()}}),this.add("braces","deletion",function(e,t,n,r,i){var s=r.doc.getTextRange(i);if(!i.isMultiLine()&&s=="{"){c(n);var o=r.doc.getLine(i.start.row),u=o.substring(i.end.column,i.end.column+1);if(u=="}")return i.end.column++,i;f.maybeInsertedBrackets--}}),this.add("parens","insertion",function(e,t,n,r,i){if(i=="("){c(n);var s=n.getSelectionRange(),o=r.doc.getTextRange(s);if(o!==""&&n.getWrapBehavioursEnabled())return h(s,o,"(",")");if(p.isSaneInsertion(n,r))return p.recordAutoInsert(n,r,")"),{text:"()",selection:[1,1]}}else if(i==")"){c(n);var u=n.getCursorPosition(),a=r.doc.getLine(u.row),f=a.substring(u.column,u.column+1);if(f==")"){var l=r.$findOpeningBracket(")",{column:u.column+1,row:u.row});if(l!==null&&p.isAutoInsertedClosing(u,a,i))return p.popAutoInsertedClosing(),{text:"",selection:[1,1]}}}}),this.add("parens","deletion",function(e,t,n,r,i){var s=r.doc.getTextRange(i);if(!i.isMultiLine()&&s=="("){c(n);var o=r.doc.getLine(i.start.row),u=o.substring(i.start.column+1,i.start.column+2);if(u==")")return i.end.column++,i}}),this.add("brackets","insertion",function(e,t,n,r,i){if(i=="["){c(n);var s=n.getSelectionRange(),o=r.doc.getTextRange(s);if(o!==""&&n.getWrapBehavioursEnabled())return h(s,o,"[","]");if(p.isSaneInsertion(n,r))return p.recordAutoInsert(n,r,"]"),{text:"[]",selection:[1,1]}}else if(i=="]"){c(n);var u=n.getCursorPosition(),a=r.doc.getLine(u.row),f=a.substring(u.column,u.column+1);if(f=="]"){var l=r.$findOpeningBracket("]",{column:u.column+1,row:u.row});if(l!==null&&p.isAutoInsertedClosing(u,a,i))return p.popAutoInsertedClosing(),{text:"",selection:[1,1]}}}}),this.add("brackets","deletion",function(e,t,n,r,i){var s=r.doc.getTextRange(i);if(!i.isMultiLine()&&s=="["){c(n);var o=r.doc.getLine(i.start.row),u=o.substring(i.start.column+1,i.start.column+2);if(u=="]")return i.end.column++,i}}),this.add("string_dquotes","insertion",function(e,t,n,r,i){if(i=='"'||i=="'"){c(n);var s=i,o=n.getSelectionRange(),u=r.doc.getTextRange(o);if(u!==""&&u!=="'"&&u!='"'&&n.getWrapBehavioursEnabled())return h(o,u,s,s);if(!u){var a=n.getCursorPosition(),f=r.doc.getLine(a.row),l=f.substring(a.column-1,a.column),p=f.substring(a.column,a.column+1),d=r.getTokenAt(a.row,a.column),v=r.getTokenAt(a.row,a.column+1);if(l=="\\"&&d&&/escape/.test(d.type))return null;var m=d&&/string|escape/.test(d.type),g=!v||/string|escape/.test(v.type),y;if(p==s)y=m!==g;else{if(m&&!g)return null;if(m&&g)return null;var b=r.$mode.tokenRe;b.lastIndex=0;var w=b.test(l);b.lastIndex=0;var E=b.test(l);if(w||E)return null;if(p&&!/[\s;,.})\]\\]/.test(p))return null;y=!0}return{text:y?s+s:"",selection:[1,1]}}}}),this.add("string_dquotes","deletion",function(e,t,n,r,i){var s=r.doc.getTextRange(i);if(!i.isMultiLine()&&(s=='"'||s=="'")){c(n);var o=r.doc.getLine(i.start.row),u=o.substring(i.start.column+1,i.start.column+2);if(u==s)return i.end.column++,i}})};p.isSaneInsertion=function(e,t){var n=e.getCursorPosition(),r=new s(t,n.row,n.column);if(!this.$matchTokenType(r.getCurrentToken()||"text",u)){var i=new s(t,n.row,n.column+1);if(!this.$matchTokenType(i.getCurrentToken()||"text",u))return!1}return r.stepForward(),r.getCurrentTokenRow()!==n.row||this.$matchTokenType(r.getCurrentToken()||"text",a)},p.$matchTokenType=function(e,t){return t.indexOf(e.type||e)>-1},p.recordAutoInsert=function(e,t,n){var r=e.getCursorPosition(),i=t.doc.getLine(r.row);this.isAutoInsertedClosing(r,i,f.autoInsertedLineEnd[0])||(f.autoInsertedBrackets=0),f.autoInsertedRow=r.row,f.autoInsertedLineEnd=n+i.substr(r.column),f.autoInsertedBrackets++},p.recordMaybeInsert=function(e,t,n){var r=e.getCursorPosition(),i=t.doc.getLine(r.row);this.isMaybeInsertedClosing(r,i)||(f.maybeInsertedBrackets=0),f.maybeInsertedRow=r.row,f.maybeInsertedLineStart=i.substr(0,r.column)+n,f.maybeInsertedLineEnd=i.substr(r.column),f.maybeInsertedBrackets++},p.isAutoInsertedClosing=function(e,t,n){return f.autoInsertedBrackets>0&&e.row===f.autoInsertedRow&&n===f.autoInsertedLineEnd[0]&&t.substr(e.column)===f.autoInsertedLineEnd},p.isMaybeInsertedClosing=function(e,t){return f.maybeInsertedBrackets>0&&e.row===f.maybeInsertedRow&&t.substr(e.column)===f.maybeInsertedLineEnd&&t.substr(0,e.column)==f.maybeInsertedLineStart},p.popAutoInsertedClosing=function(){f.autoInsertedLineEnd=f.autoInsertedLineEnd.substr(1),f.autoInsertedBrackets--},p.clearMaybeInsertedClosing=function(){f&&(f.maybeInsertedBrackets=0,f.maybeInsertedRow=-1)},r.inherits(p,i),t.CstyleBehaviour=p}),define("ace/mode/folding/cstyle",["require","exports","module","ace/lib/oop","ace/range","ace/mode/folding/fold_mode"],function(e,t,n){"use strict";var r=e("../../lib/oop"),i=e("../../range").Range,s=e("./fold_mode").FoldMode,o=t.FoldMode=function(e){e&&(this.foldingStartMarker=new RegExp(this.foldingStartMarker.source.replace(/\|[^|]*?$/,"|"+e.start)),this.foldingStopMarker=new RegExp(this.foldingStopMarker.source.replace(/\|[^|]*?$/,"|"+e.end)))};r.inherits(o,s),function(){this.foldingStartMarker=/(\{|\[)[^\}\]]*$|^\s*(\/\*)/,this.foldingStopMarker=/^[^\[\{]*(\}|\])|^[\s\*]*(\*\/)/,this.singleLineBlockCommentRe=/^\s*(\/\*).*\*\/\s*$/,this.tripleStarBlockCommentRe=/^\s*(\/\*\*\*).*\*\/\s*$/,this.startRegionRe=/^\s*(\/\*|\/\/)#?region\b/,this._getFoldWidgetBase=this.getFoldWidget,this.getFoldWidget=function(e,t,n){var r=e.getLine(n);if(this.singleLineBlockCommentRe.test(r)&&!this.startRegionRe.test(r)&&!this.tripleStarBlockCommentRe.test(r))return"";var i=this._getFoldWidgetBase(e,t,n);return!i&&this.startRegionRe.test(r)?"start":i},this.getFoldWidgetRange=function(e,t,n,r){var i=e.getLine(n);if(this.startRegionRe.test(i))return this.getCommentRegionBlock(e,i,n);var s=i.match(this.foldingStartMarker);if(s){var o=s.index;if(s[1])return this.openingBracketBlock(e,s[1],n,o);var u=e.getCommentFoldRange(n,o+s[0].length,1);return u&&!u.isMultiLine()&&(r?u=this.getSectionRange(e,n):t!="all"&&(u=null)),u}if(t==="markbegin")return;var s=i.match(this.foldingStopMarker);if(s){var o=s.index+s[0].length;return s[1]?this.closingBracketBlock(e,s[1],n,o):e.getCommentFoldRange(n,o,-1)}},this.getSectionRange=function(e,t){var n=e.getLine(t),r=n.search(/\S/),s=t,o=n.length;t+=1;var u=t,a=e.getLength();while(++tf)break;var l=this.getFoldWidgetRange(e,"all",t);if(l){if(l.start.row<=s)break;if(l.isMultiLine())t=l.end.row;else if(r==f)break}u=t}return new i(s,o,u,e.getLine(u).length)},this.getCommentRegionBlock=function(e,t,n){var r=t.search(/\s*$/),s=e.getLength(),o=n,u=/^\s*(?:\/\*|\/\/|--)#?(end)?region\b/,a=1;while(++no)return new i(o,r,l,t.length)}}.call(o.prototype)}),define("ace/mode/json",["require","exports","module","ace/lib/oop","ace/mode/text","ace/mode/json_highlight_rules","ace/mode/matching_brace_outdent","ace/mode/behaviour/cstyle","ace/mode/folding/cstyle","ace/worker/worker_client"],function(e,t,n){"use strict";var r=e("../lib/oop"),i=e("./text").Mode,s=e("./json_highlight_rules").JsonHighlightRules,o=e("./matching_brace_outdent").MatchingBraceOutdent,u=e("./behaviour/cstyle").CstyleBehaviour,a=e("./folding/cstyle").FoldMode,f=e("../worker/worker_client").WorkerClient,l=function(){this.HighlightRules=s,this.$outdent=new o,this.$behaviour=new u,this.foldingRules=new a};r.inherits(l,i),function(){this.getNextLineIndent=function(e,t,n){var r=this.$getIndent(t);if(e=="start"){var i=t.match(/^.*[\{\(\[]\s*$/);i&&(r+=n)}return r},this.checkOutdent=function(e,t,n){return this.$outdent.checkOutdent(t,n)},this.autoOutdent=function(e,t,n){this.$outdent.autoOutdent(t,n)},this.createWorker=function(e){var t=new f(["ace"],"ace/mode/json_worker","JsonWorker");return t.attachToDocument(e.getDocument()),t.on("annotate",function(t){e.setAnnotations(t.data)}),t.on("terminate",function(){e.clearAnnotations()}),t},this.$id="ace/mode/json"}.call(l.prototype),t.Mode=l}) -------------------------------------------------------------------------------- /templates/public/js/playground.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var editor = null; 4 | var defaultQuery = ''; 5 | 6 | var App = React.createClass({ 7 | runQuery: function() { 8 | if (!editor) { 9 | return; 10 | } 11 | 12 | this.setState({ loading: true }); 13 | 14 | var query = editor.getValue(); 15 | localStorage.editorValue = query; 16 | 17 | reqwest({ 18 | url: '/graphql', 19 | type: 'json', 20 | method: 'POST', 21 | data: query, 22 | contentType: 'application/graphql', 23 | error: this.onError, 24 | success: this.onSuccess 25 | }); 26 | }, 27 | 28 | onError: function(res) { 29 | this.setState({ 30 | error: JSON.parse(res.responseText).message, 31 | response: null, 32 | loading: false 33 | }); 34 | }, 35 | 36 | onSuccess: function(res) { 37 | this.setState({ 38 | error: null, 39 | response: res, 40 | loading: false 41 | }); 42 | }, 43 | 44 | renderFeedback: function() { 45 | if (!this.state) { 46 | return null; 47 | } 48 | 49 | if (this.state.error) { 50 | return ; 51 | } 52 | 53 | return ( 54 | 58 | ); 59 | }, 60 | 61 | getKeyBinding: function() { 62 | return (navigator.userAgent || '').indexOf('Macintosh') >= 0 ? 63 | 'command + enter' : 'ctrl + enter'; 64 | }, 65 | 66 | render: function() { 67 | return ( 68 |
69 |

sql-to-graphql playground

70 | 71 |
72 |
73 |
74 |

Query ({this.getKeyBinding()} to run)

75 | 78 | 79 | 82 | 83 | {this.renderFeedback()} 84 |
85 |
86 |
87 |
88 |

Schema

89 | 90 |
91 |
92 |
93 |
94 | ); 95 | } 96 | }); 97 | 98 | var Schema = React.createClass({ 99 | componentDidMount: function() { 100 | reqwest('/schema', this.onData); 101 | }, 102 | 103 | onData: function(res) { 104 | this.setState({ schema: res }); 105 | }, 106 | 107 | render: function() { 108 | return this.state ?
{this.state.schema}
:
...
; 109 | } 110 | }); 111 | 112 | var QueryError = React.createClass({ 113 | render: function() { 114 | return ( 115 |
116 |

Errors

117 | {this.props.errors} 118 |
119 | ) 120 | } 121 | }); 122 | 123 | var QueryResponse = React.createClass({ 124 | componentDidMount: function() { 125 | this.setValue(this.props); 126 | }, 127 | 128 | componentDidUpdate: function() { 129 | this.setValue(this.props); 130 | }, 131 | 132 | setValue: function(props) { 133 | if (!this.editor) { 134 | var el = React.findDOMNode(this.refs.res); 135 | this.editor = ace.edit(el); 136 | this.editor.setTheme('ace/theme/monokai'); 137 | this.editor.setShowPrintMargin(false); 138 | this.editor.getSession().setMode('ace/mode/json'); 139 | this.editor.getSession().setUseWrapMode(true); 140 | this.editor.setReadOnly(true); 141 | } 142 | 143 | var response = (props && props.response) || {}; 144 | if (response.data) { 145 | this.editor.setValue( 146 | JSON.stringify(response.data, null, 4), 147 | 1 148 | ); 149 | } 150 | }, 151 | 152 | render: function() { 153 | return ( 154 |
155 |

Response

156 | {this.props.loading ?
: null} 157 |
158 |
159 | ) 160 | } 161 | }); 162 | 163 | var Editor = React.createClass({ 164 | componentDidMount: function() { 165 | var el = React.findDOMNode(this.refs.editor); 166 | editor = ace.edit(el); 167 | editor.setTheme('ace/theme/monokai'); 168 | editor.setShowPrintMargin(false); 169 | editor.commands.addCommand({ 170 | name: 'runQuery', 171 | bindKey: {win: 'Ctrl-Enter', mac: 'Command-Enter'}, 172 | exec: this.props.runQuery 173 | }); 174 | }, 175 | 176 | shouldComponentUpdate: function() { 177 | return false; 178 | }, 179 | 180 | render: function() { 181 | return ( 182 |
{this.props.initialValue}
183 | ); 184 | } 185 | }); 186 | 187 | React.render( 188 | , 189 | document.getElementById('container') 190 | ); 191 | 192 | function getInitialEditorValue() { 193 | return localStorage.editorValue || defaultQuery; 194 | } 195 | -------------------------------------------------------------------------------- /templates/public/js/reqwest.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Reqwest! A general purpose XHR connection manager 3 | * license MIT (c) Dustin Diaz 2015 4 | * https://github.com/ded/reqwest 5 | */ 6 | !function(e,t,n){typeof module!="undefined"&&module.exports?module.exports=n():typeof define=="function"&&define.amd?define(n):t[e]=n()}("reqwest",this,function(){function succeed(e){var t=protocolRe.exec(e.url);return t=t&&t[1]||context.location.protocol,httpsRe.test(t)?twoHundo.test(e.request.status):!!e.request.response}function handleReadyState(e,t,n){return function(){if(e._aborted)return n(e.request);if(e._timedOut)return n(e.request,"Request is aborted: timeout");e.request&&e.request[readyState]==4&&(e.request.onreadystatechange=noop,succeed(e)?t(e.request):n(e.request))}}function setHeaders(e,t){var n=t.headers||{},r;n.Accept=n.Accept||defaultHeaders.accept[t.type]||defaultHeaders.accept["*"];var i=typeof FormData=="function"&&t.data instanceof FormData;!t.crossOrigin&&!n[requestedWith]&&(n[requestedWith]=defaultHeaders.requestedWith),!n[contentType]&&!i&&(n[contentType]=t.contentType||defaultHeaders.contentType);for(r in n)n.hasOwnProperty(r)&&"setRequestHeader"in e&&e.setRequestHeader(r,n[r])}function setCredentials(e,t){typeof t.withCredentials!="undefined"&&typeof e.withCredentials!="undefined"&&(e.withCredentials=!!t.withCredentials)}function generalCallback(e){lastValue=e}function urlappend(e,t){return e+(/\?/.test(e)?"&":"?")+t}function handleJsonp(e,t,n,r){var i=uniqid++,s=e.jsonpCallback||"callback",o=e.jsonpCallbackName||reqwest.getcallbackPrefix(i),u=new RegExp("((^|\\?|&)"+s+")=([^&]+)"),a=r.match(u),f=doc.createElement("script"),l=0,c=navigator.userAgent.indexOf("MSIE 10.0")!==-1;return a?a[3]==="?"?r=r.replace(u,"$1="+o):o=a[3]:r=urlappend(r,s+"="+o),context[o]=generalCallback,f.type="text/javascript",f.src=r,f.async=!0,typeof f.onreadystatechange!="undefined"&&!c&&(f.htmlFor=f.id="_reqwest_"+i),f.onload=f.onreadystatechange=function(){if(f[readyState]&&f[readyState]!=="complete"&&f[readyState]!=="loaded"||l)return!1;f.onload=f.onreadystatechange=null,f.onclick&&f.onclick(),t(lastValue),lastValue=undefined,head.removeChild(f),l=1},head.appendChild(f),{abort:function(){f.onload=f.onreadystatechange=null,n({},"Request is aborted: timeout",{}),lastValue=undefined,head.removeChild(f),l=1}}}function getRequest(e,t){var n=this.o,r=(n.method||"GET").toUpperCase(),i=typeof n=="string"?n:n.url,s=n.processData!==!1&&n.data&&typeof n.data!="string"?reqwest.toQueryString(n.data):n.data||null,o,u=!1;return(n["type"]=="jsonp"||r=="GET")&&s&&(i=urlappend(i,s),s=null),n["type"]=="jsonp"?handleJsonp(n,e,t,i):(o=n.xhr&&n.xhr(n)||xhr(n),o.open(r,i,n.async===!1?!1:!0),setHeaders(o,n),setCredentials(o,n),context[xDomainRequest]&&o instanceof context[xDomainRequest]?(o.onload=e,o.onerror=t,o.onprogress=function(){},u=!0):o.onreadystatechange=handleReadyState(this,e,t),n.before&&n.before(o),u?setTimeout(function(){o.send(s)},200):o.send(s),o)}function Reqwest(e,t){this.o=e,this.fn=t,init.apply(this,arguments)}function setType(e){if(e.match("json"))return"json";if(e.match("javascript"))return"js";if(e.match("text"))return"html";if(e.match("xml"))return"xml"}function init(o,fn){function complete(e){o.timeout&&clearTimeout(self.timeout),self.timeout=null;while(self._completeHandlers.length>0)self._completeHandlers.shift()(e)}function success(resp){var type=o.type||resp&&setType(resp.getResponseHeader("Content-Type"));resp=type!=="jsonp"?self.request:resp;var filteredResponse=globalSetupOptions.dataFilter(resp.responseText,type),r=filteredResponse;try{resp.responseText=r}catch(e){}if(r)switch(type){case"json":try{resp=context.JSON?context.JSON.parse(r):eval("("+r+")")}catch(err){return error(resp,"Could not parse JSON in response",err)}break;case"js":resp=eval(r);break;case"html":resp=r;break;case"xml":resp=resp.responseXML&&resp.responseXML.parseError&&resp.responseXML.parseError.errorCode&&resp.responseXML.parseError.reason?null:resp.responseXML}self._responseArgs.resp=resp,self._fulfilled=!0,fn(resp),self._successHandler(resp);while(self._fulfillmentHandlers.length>0)resp=self._fulfillmentHandlers.shift()(resp);complete(resp)}function timedOut(){self._timedOut=!0,self.request.abort()}function error(e,t,n){e=self.request,self._responseArgs.resp=e,self._responseArgs.msg=t,self._responseArgs.t=n,self._erred=!0;while(self._errorHandlers.length>0)self._errorHandlers.shift()(e,t,n);complete(e)}this.url=typeof o=="string"?o:o.url,this.timeout=null,this._fulfilled=!1,this._successHandler=function(){},this._fulfillmentHandlers=[],this._errorHandlers=[],this._completeHandlers=[],this._erred=!1,this._responseArgs={};var self=this;fn=fn||function(){},o.timeout&&(this.timeout=setTimeout(function(){timedOut()},o.timeout)),o.success&&(this._successHandler=function(){o.success.apply(o,arguments)}),o.error&&this._errorHandlers.push(function(){o.error.apply(o,arguments)}),o.complete&&this._completeHandlers.push(function(){o.complete.apply(o,arguments)}),this.request=getRequest.call(this,success,error)}function reqwest(e,t){return new Reqwest(e,t)}function normalize(e){return e?e.replace(/\r?\n/g,"\r\n"):""}function serial(e,t){var n=e.name,r=e.tagName.toLowerCase(),i=function(e){e&&!e.disabled&&t(n,normalize(e.attributes.value&&e.attributes.value.specified?e.value:e.text))},s,o,u,a;if(e.disabled||!n)return;switch(r){case"input":/reset|button|image|file/i.test(e.type)||(s=/checkbox/i.test(e.type),o=/radio/i.test(e.type),u=e.value,(!s&&!o||e.checked)&&t(n,normalize(s&&u===""?"on":u)));break;case"textarea":t(n,normalize(e.value));break;case"select":if(e.type.toLowerCase()==="select-one")i(e.selectedIndex>=0?e.options[e.selectedIndex]:null);else for(a=0;e.length&&a