import * as fs from 'fs-extra' ; import { IExecutableSchemaDefinition , SchemaDirectiveVisitor , makeExecutableSchema , } from 'graphql-tools' ; const STRING_TYPES = [ 'CHAR' , 'VARCHAR' , 'BINARY' , 'VARBINARY' , 'BLOB' , 'TEXT' , 'ENUM' , 'SET' , ] ; export interface IMakeSqlSchemaInput extends IExecutableSchemaDefinition { schemaDirectives : { [ name ] : typeof SchemaDirectiveVisitor ; } ; outputFilepath : string ; databaseName : string ; tablePrefix ? : string ; } export interface ITable { name : string ; columns : { [ name ] : IColumn ; } ; primaryIndex ? : IColumn ; secondaryIndices ? : IColumn [ ] ; unicode ? : boolean ; constraints ? : string ; } export interface IColumn { name : string ; auto ? : boolean ; default ? : string ; index ? : boolean ; nullable ? : boolean ; primary ? : boolean ; type ? : string ; graphQLType : string ; unicode ? : boolean ; unique ? : boolean ; generated ? : string ; } export interface ISQLAST { [ name ] : ITable ; } let sqlAST = { } ; export function addTable ( table ) { sqlAST [ table . name ] = table ; } export function addColumn ( tableName , column ) { sqlAST [ tableName ] = { ... sqlAST [ tableName ] , name : tableName , columns : { ... ( sqlAST [ tableName ] || { } ) . columns , [ column . name ] : column , } , } ; } export function makeSqlSchema ( options ) { sqlAST = { } ; const outputFilepath = options . outputFilepath ; const databaseName = options . databaseName ; const tablePrefix = options . tablePrefix || '' ; delete options . outputFilepath ; delete options . databaseName ; makeExecutableSchema ( options ) ; setDefaults ( ) ; gatherIndices ( ) ; renderCreateSchemaScript ( databaseName , tablePrefix , outputFilepath ) ; } function setDefaults ( ) { forEachTableDo ( table => { forEachColumnDo ( table , column => { if ( column . primary && column . nullable ) { emitError ( table . name , column . name , '@primary is not allowed with @nullable.' ) ; } if ( ! column . type ) { if ( column . graphQLType === 'String' ) { emitError ( table . name , column . name , `String types must be explicitly defined using @type.` ) ; } else if ( column . graphQLType === 'Int' ) { column . type = 'INT' ; } else if ( column . graphQLType === 'Float' ) { column . type = 'DECIMAL' ; } else if ( column . graphQLType === 'Boolean' ) { column . type = 'BOOLEAN' ; } else if ( column . graphQLType === 'JSON' ) { column . type = 'JSON' ; } else { emitError ( table . name , column . name , `A default SQL type cannot be generated for GraphQL type ${ column . graphQLType }` ) ; } } column . type = ( column . type || '' ) . toUpperCase ( ) ; if ( column . auto ) { if ( ! column . type . includes ( 'INT' ) && column . type !== 'SERIAL' ) { emitError ( table . name , column . name , `Column with "auto" must have INT or SERIAL type.` ) ; } if ( column . default ) { emitError ( table . name , column . name , '"default" is not allowed with "auto".' ) ; } if ( column . unicode ) { emitError ( table . name , column . name , '"unicode" is not allowed with "auto".' ) ; } if ( column . generated ) { emitError ( table . name , column . name , '"generated" is not allowed with "auto".' ) ; } } if ( column . unicode ) { if ( ! STRING_TYPES . includes ( stripLength ( column . type ) ) ) { emitError ( table . name , column . name , '@unicode can only be applied to a string type.' ) ; } if ( ! table . unicode ) { table . unicode = true ; } } if ( column . generated && column . default ) { emitError ( table . name , column . name , '"default" is not allowed with "generated".' ) ; } if ( column . generated && column . generated . startsWith ( 'ALWAYS AS' ) ) { column . generated = `GENERATED ${ column . generated }` ; } if ( column . generated && ! column . generated . startsWith ( 'GENERATED' ) && ! column . generated . startsWith ( 'AS' ) ) { column . generated = `AS ${ column . generated }` ; } } ) ; } ) ; } function gatherIndices ( ) { forEachTableDo ( table => { forEachColumnDo ( table , column => { if ( column . primary ) { if ( ! ! table . primaryIndex ) { emitError ( table . name , column . name , 'More than one column is marked as the primary index.' ) ; } else { table . primaryIndex = column ; } } else if ( column . index ) { table . secondaryIndices = table . secondaryIndices || [ ] ; table . secondaryIndices . push ( column ) ; } } ) ; if ( ! table . primaryIndex ) { emitError ( table . name , '' , `Table ${ table . name } does not have a primary index.` ) ; } } ) ; } function renderCreateSchemaScript ( databaseName , tablePrefix , outputFilepath ) { const tableDefinitions = [ ] ; forEachTableDo ( table => { const columnDefinitions = [ ] ; forEachColumnDo ( table , column => { const unicodeClause = ! ! column . unicode ? 'CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ' : '' ; const generatedClause = ! ! column . generated ? `${ column . generated } ` : '' ; const nullClause = ! ! column . nullable ? 'NULL ' : 'NOT NULL ' ; const defaultClause = ! ! column . default ? `DEFAULT ${ column . default } ` : '' ; const autoClause = ! ! column . auto && column . type !== 'SERIAL' ? 'AUTO_INCREMENT ' : '' ; const uniqueClause = ! ! column . unique ? `UNIQUE ` : '' ; columnDefinitions . push ( `\`${ column . name }\` ${ column . type } ${ unicodeClause }${ generatedClause }${ nullClause }${ defaultClause }${ autoClause }${ uniqueClause }` . trim ( ) ) ; } ) ; const primaryKeyName = ( table . primaryIndex && table . primaryIndex . name ) || '' ; let indexDefinitions = ( table . secondaryIndices || [ ] ) . map ( column => { return `INDEX \`${ column . name . toUpperCase ( ) }INDEX\` (\`${ column . name }\` ASC)` ; } ) ; if ( indexDefinitions . length > 0 ) { indexDefinitions = [ '' ] . concat ( indexDefinitions ) ; } const constraints = table . constraints ? ',\n  ' + table . constraints : '' ; const unicodeModifier = table . unicode ? ' CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci' : '' ; tableDefinitions . push ( `CREATE TABLE \`${ databaseName }\`.\`${ tablePrefix }${ table . name }\` (
  ${ columnDefinitions . join ( ',\n  ' ) },
  PRIMARY KEY (\`${ primaryKeyName }\`)${ indexDefinitions . join ( ',\n  ' ) }${ constraints }
)${ unicodeModifier };` ) ; } ) ; fs . outputFileSync ( `${ outputFilepath }` , tableDefinitions . join ( '\n\n' ) ) ; } function forEachTableDo ( foo ) { for ( const tableName in sqlAST ) { if ( ! sqlAST . hasOwnProperty ( tableName ) ) continue ; const table = sqlAST [ tableName ] ; foo ( table ) ; } } function forEachColumnDo ( table , foo ) { for ( const columnName in table . columns ) { if ( ! table . columns . hasOwnProperty ( columnName ) ) continue ; const column = table . columns [ columnName ] ; foo ( column ) ; } } function emitError ( tableName , columnName , message ) { throw new Error ( `[${ tableName } : ${ columnName }]: ${ message }` ) ; } function stripLength ( type ) { return type . split ( '(' ) [ 0 ] ; }