diff --git a/dist/example-def.d.ts b/dist/example-def.d.ts index 99665a5..5dcc692 100644 --- a/dist/example-def.d.ts +++ b/dist/example-def.d.ts @@ -1,3 +1,3 @@ -import { SystemDef } from './processDef'; +import { SystemDef } from './systemGenService'; declare let def: SystemDef; export default def; diff --git a/dist/example-def.js b/dist/example-def.js index d636eca..117c302 100644 --- a/dist/example-def.js +++ b/dist/example-def.js @@ -1,6 +1,7 @@ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); var def = { + name: 'Todo', storage: { tables: [ { diff --git a/dist/processDef.js b/dist/processDef.js index a0998c4..6aa31c9 100644 --- a/dist/processDef.js +++ b/dist/processDef.js @@ -103,10 +103,10 @@ function dateString() { } function writeMigrationsToFile(migrations) { var migrationFileContents = "\n--up\n" + migrations.up.join('\n') + "\n--down\n" + migrations.down.join('\n') + "\n "; - var migrationFilePath = path.join(__dirname, outDir, 'src', 'migrationJobs', dateString() + "-create-database.sql"); + var migrationFilePath = path.join(process.cwd(), outDir, 'src', 'migrationJobs', dateString() + "-create-database.sql"); fs.writeFileSync(migrationFilePath, migrationFileContents); } -ncp(path.join(__dirname, 'frame'), path.join(__dirname, outDir), function (err) { +ncp(path.join(process.cwd(), 'frame'), path.join(process.cwd(), outDir), function (err) { if (err) { console.log(err); } diff --git a/frame/src/app.ts b/frame/src/app.ts index 569425e..84c2746 100644 --- a/frame/src/app.ts +++ b/frame/src/app.ts @@ -34,7 +34,6 @@ app.use(express.static(path.join(__dirname, 'public'))); app.use('/', indexRouter); app.use('/api/v1/users', usersRouter); - // SYSTEM-BUILDER-app.use diff --git a/frame/src/components/{{component}}/{{component}}Service.ets b/frame/src/components/{{component}}/{{component}}Service.ets index dce277d..37c0669 100644 --- a/frame/src/components/{{component}}/{{component}}Service.ets +++ b/frame/src/components/{{component}}/{{component}}Service.ets @@ -1,5 +1,5 @@ import { db } from '../../databases'; -import { {{Component}}Mapper } from './componentMapper'; +import { {{Component}}Mapper } from './{{component}}Mapper'; class {{Component}}Service { private {{component}}Mapper = new {{Component}}Mapper(db); diff --git a/frame/src/initializeServices.ts b/frame/src/initializeServices.ts index bab07ad..cb446b0 100644 --- a/frame/src/initializeServices.ts +++ b/frame/src/initializeServices.ts @@ -1,14 +1,14 @@ import { userService } from './components/users/userService'; - // SYSTEM-BUILDER-initialize-import + import { altitude } from './databases'; const appReady = altitude.runMigrations().then(() => { let servicesPromise = [ userService.initialize(), - // SYSTEM-BUILDER-initialize-service + ]; return Promise.all(servicesPromise); }); diff --git a/package.json b/package.json index 77e74ab..4b2623a 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "path": "^0.12.7" }, "scripts": { - "start": "rm -rf dist && tsc && cp -r frame dist/ && node dist/processDef.js" + "start": "rm -rf dist && tsc && cp -r frame dist/ && cd dist && node index.js" }, "name": "system-builder", "version": "1.0.0", diff --git a/src/database/database-creator.ts b/src/database/database-creator.ts new file mode 100644 index 0000000..43c4a00 --- /dev/null +++ b/src/database/database-creator.ts @@ -0,0 +1,115 @@ +import * as path from 'path'; +import * as fs from 'fs'; +import { + SystemDef, + StorageDef, + ViewDef, + Order, + Filter, + TableDef, + ColumnDef, + BelongsToDef, + ManyToManyDef +} from '../systemGenService'; + +let outDir = 'test'; + +export function createDatabase(storageDef: StorageDef) { + let tableCreationQueries: string[] = []; + let tableDeletionQueries: string[] = []; + for (let i in storageDef.tables) { + tableCreationQueries.push(getTableCreationString(storageDef.tables[i])); + tableDeletionQueries.push(getTableDeletionString(storageDef.tables[i])); + } + for (let i in storageDef.relations) { + tableCreationQueries.push(getTableRelationsCreationString(storageDef.relations[i])); + tableDeletionQueries.push(getTableRelationsDeletionString(storageDef.relations[i])) + } + tableCreationQueries.map((query: string) => { + console.log(query); + }); + + tableDeletionQueries.map((query: string) => { + console.log(query); + }); + + return { + up: tableCreationQueries, + down: tableDeletionQueries, + }; +} + +function getTableCreationString(tableDef: TableDef): string { + let primaryKeyString: string = `${tableDef.name}_id VARCHAR(36) PRIMARY KEY`; + let columnString: string = `${primaryKeyString}`; + let defaultColumns: string = `created DATETIME NOT NULL DEFAULT NOW(), modified DATETIME NOT NULL DEFAULT NOW()` + let indexString: string = ``; + + // columns + for (let i in tableDef.columns) { + let column: ColumnDef = tableDef.columns[i]; + columnString = `${columnString}, ${createColumnString(column)}`; + } + + // relations + for (let i in tableDef.relations) { + let relation: BelongsToDef = tableDef.relations[i]; + columnString = `${columnString}, ${relation.table}_id VARCHAR(36)`; + } + + // default created & modified + columnString = `${columnString}, ${defaultColumns}`; + + return `CREATE TABLE IF NOT EXISTS ${tableDef.name} (${columnString}${indexString !== '' ? ', ' + indexString : ''});`; +} + +function getTableRelationsCreationString(relationsDef: ManyToManyDef): string { + let columnString: string = `${relationsDef.left}_id VARCHAR(36) NOT NULL, ${relationsDef.right}_id VARCHAR(36) NOT NULL`; + let indexString: string = `INDEX ${relationsDef.left}_id_index (${relationsDef.left}_id), INDEX ${relationsDef.right}_id_index (${relationsDef.right}_id)`; + + if (relationsDef.columns && relationsDef.columns.length) { + for (let i in relationsDef.columns) { + let column: ColumnDef = relationsDef.columns[i]; + columnString = `${columnString}, ${createColumnString(column)}`; + } + } + + return `CREATE TABLE IF NOT EXISTS ${relationsDef.left}_${relationsDef.right} (${columnString}${indexString !== '' ? ', ' + indexString : ''});`; +} + +function createColumnString(column: ColumnDef) { + const typeMap: {[type: string]: string} = { + "blob": "BLOB", + "boolean": "BOOLEAN", + "date": "DATE", + "dateTIME": "DATETIME", + "number": "INT", + "string": "VARCHAR(255)", + }; + + return `${column.name} ${typeMap[column.type]}${column.nullable ? '' : ' NOT NULL'}${column.default ? ' DEFAULT ' + column.default : ''}${column.autoIncrement ? ' AUTO_INCREMENT' : ''}${column.unique ? ' UNIQUE' : ''}`; +} + +function getTableDeletionString(tableDef: TableDef): string { + return `DROP TABLE IF EXISTS ${tableDef.name};` +} + +function getTableRelationsDeletionString(relationsDef: ManyToManyDef) : string { + return `DROP TABLE IF EXISTS ${relationsDef.left}_${relationsDef.right};` +} + +function dateString() { + let date = new Date(); + return `${date.getFullYear()}${date.getMonth() + 1}${date.getDate()}`; +} + +export function writeMigrationsToFile(migrations: {up: string[], down: string[]}, outDir: string) { + let migrationFileContents = ` +--up +${migrations.up.join('\n')} +--down +${migrations.down.join('\n')} + `; + let migrationFilePath = path.join(process.cwd(), outDir, 'src', 'migrationJobs', `${dateString()}-create-database.sql`); + fs.writeFileSync(migrationFilePath, migrationFileContents); +} diff --git a/src/example-def.ts b/src/example-def.ts index b3d698b..542e53e 100644 --- a/src/example-def.ts +++ b/src/example-def.ts @@ -1,6 +1,7 @@ -import { SystemDef } from './processDef'; +import { SystemDef } from './systemGenService'; let def: SystemDef = { + name: 'Todo', storage: { tables: [ { diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..433be0a --- /dev/null +++ b/src/index.ts @@ -0,0 +1,9 @@ + + +import { systemGenService } from './systemGenService'; + +import defs from './example-def'; +// import simmer from './simmer-def-example'; + + +systemGenService.runSysGen(defs); diff --git a/src/processDef.ts b/src/processDef.ts index 5c834f9..d30e6da 100644 --- a/src/processDef.ts +++ b/src/processDef.ts @@ -104,12 +104,12 @@ ${migrations.up.join('\n')} --down ${migrations.down.join('\n')} `; - let migrationFilePath = path.join(__dirname, outDir, 'src', 'migrationJobs', `${dateString()}-create-database.sql`); + let migrationFilePath = path.join(process.cwd(), outDir, 'src', 'migrationJobs', `${dateString()}-create-database.sql`); fs.writeFileSync(migrationFilePath, migrationFileContents); } -ncp(path.join(__dirname, 'frame'), path.join(__dirname, outDir), (err: any) => { +ncp(path.join(process.cwd(), 'frame'), path.join(process.cwd(), outDir), (err: any) => { if (err) { console.log(err); } else { diff --git a/src/systemGenService.ts b/src/systemGenService.ts new file mode 100644 index 0000000..a54ae17 --- /dev/null +++ b/src/systemGenService.ts @@ -0,0 +1,105 @@ +import * as path from 'path'; +const ncp = require('ncp').ncp; +import { createDatabase, writeMigrationsToFile } from './database/database-creator'; +import { createViews } from './views/views-creator'; + +class SystemGenService { + + public runSysGen(defs: SystemDef) { + ncp(path.join(process.cwd(), 'frame'), path.join(process.cwd(), defs.name), (err: any) => { + if (err) { + console.log(err); + } else { + console.log('success copying files'); + this.buildDatabase(defs.storage, defs.name); + this.buildViews(defs, defs.name); + } + }); + } + + public buildDatabase(storage: StorageDef, outDir: string) { + let creationMigrations = createDatabase(storage); + writeMigrationsToFile(creationMigrations, outDir); + } + + public buildViews(systemDef: SystemDef, outDir: string) { + createViews(systemDef); + } +} + +const systemGenService = new SystemGenService(); + + +interface SystemDef { + name: string; + storage: StorageDef; + views: ViewDef[]; + // TODO: add Views, ACLs, Behaviors, UX +} + +interface StorageDef { + tables: TableDef[]; + relations: ManyToManyDef[]; +} + +interface ViewDef { + component: string; + type: ('list' | 'count' | 'item' | 'distinct')[]; + columns: string[]; + orderBy?: Order[]; + filters?: Filter[]; + // if type is 'list' it will always include skip and limit for pagination +} + +interface Order { + column: string; + direction: 'asc' | 'desc'; +} + +interface Filter { + param: string; // the query param used to get the value + column: string; + comparison: '=' | '!=' | '>' | '<' | 'contains'; + value?: string; + required?: boolean; +} + +interface TableDef { + name: string; + columns: ColumnDef[], + relations: BelongsToDef[], +} + +interface ColumnDef { + name: string; + type: "blob" | "boolean" | "date" | "dateTIME" | "number" | "string"; + nullable: boolean; + unique?: boolean; + autoIncrement?: boolean; + default?: string; +} + +interface BelongsToDef { + type: 'belongs-to'; + table: string; +} + +interface ManyToManyDef { + left: string; + relation: 'many-to-many'; + right: string; + columns?: ColumnDef[]; +} + +export { + SystemDef, + StorageDef, + ViewDef, + Order, + Filter, + TableDef, + ColumnDef, + BelongsToDef, + ManyToManyDef, + systemGenService +} diff --git a/src/views/views-creator.ts b/src/views/views-creator.ts new file mode 100644 index 0000000..2fa26e2 --- /dev/null +++ b/src/views/views-creator.ts @@ -0,0 +1,92 @@ +import { + SystemDef, + ViewDef +} from '../systemGenService'; +import * as path from 'path'; +import * as fs from 'fs'; + +const ncp = require('ncp').ncp; + + +export function createViews(systemDef: SystemDef) { + let viewsPromises = []; + for (let i in systemDef.views) { + viewsPromises.push(createComponent(systemDef.views[i], systemDef.name)); + } + return Promise.all(viewsPromises); +} + +function createComponent(view: ViewDef, outDir: string) { + let componentPromise = new Promise((componentResolve, componentReject) => { + ncp(path.join(process.cwd(), 'frame', 'src', 'components', '{{component}}'), path.join(process.cwd(), outDir, 'src', 'components', view.component), (err: any) => { + if (err) { + console.log(err); + } else { + let mapperPromise = initializeComponentFile(path.join(process.cwd(), outDir, 'src', 'components', view.component, '{{component}}Mapper.ets'), path.join(process.cwd(), outDir, 'src', 'components', view.component, `${view.component}Mapper.ts`), view.component, outDir); + let routesPromise = initializeComponentFile(path.join(process.cwd(), outDir, 'src', 'components', view.component, '{{component}}Routes.ets'), path.join(process.cwd(), outDir, 'src', 'components', view.component, `${view.component}Routes.ts`), view.component, outDir); + addInitializeRoutesCode(view.component, outDir); + let serviceFileLocation: string = path.join(process.cwd(), outDir, 'src', 'components', view.component, `${view.component}Service.ts`); + let servicePromise = initializeComponentFile(path.join(process.cwd(), outDir, 'src', 'components', view.component, '{{component}}Service.ets'), serviceFileLocation, view.component, outDir); + addInitializeServiceCode(view.component, outDir); + Promise.all([mapperPromise, routesPromise, servicePromise]).then(() => { + console.log(`success creating component ${view.component}`); + componentResolve(); + }); + } + }); + }); + return componentPromise; +} + +function initializeComponentFile(sourceFile: string, destinationFile: string, component: string, outDir: string) { + let filePromise = new Promise((resolve, reject) => { + fs.rename(sourceFile, destinationFile, (err: any) => { + let fileContents = fs.readFileSync(destinationFile, 'utf8'); + var uppercaseFirstLetterComponentName = uppercaseFirstLetter(component); + var lowercaseFirstLetterComponentName = lowercaseFirstLetter(component); + let newFileContents = fileContents.split('{{Component}}').join(uppercaseFirstLetterComponentName); + newFileContents = newFileContents.split('{{component}}').join(lowercaseFirstLetterComponentName); + fs.writeFileSync(destinationFile, newFileContents, 'utf8'); + resolve(); + }); + }); + return filePromise +} + +function addInitializeServiceCode(component: string, outDir: string) { + let fileLocation = path.join(process.cwd(), outDir, 'src', 'initializeServices.ts'); + let initServicesFile: string = fs.readFileSync(fileLocation, 'utf8'); + let parts = initServicesFile.split('// SYSTEM-BUILDER-initialize-import'); + parts[0] = parts[0] + `import { ${component}Service } from './components/${component}/${component}Service'; +`; + let newServicesFile = parts.join('// SYSTEM-BUILDER-initialize-import'); + parts = newServicesFile.split('// SYSTEM-BUILDER-initialize-service'); + parts[0] = parts[0] + `${component}Service.initialize(), +`; + newServicesFile = parts.join(' // SYSTEM-BUILDER-initialize-service'); + fs.writeFileSync(fileLocation, newServicesFile, 'utf8'); +} + +function addInitializeRoutesCode(component: string, outDir: string) { + let fileLocation = path.join(process.cwd(), outDir, 'src', 'app.ts'); + let initRoutesFile: string = fs.readFileSync(fileLocation, 'utf8'); + let parts = initRoutesFile.split('// SYSTEM-BUILDER-router.require'); + parts[0] = parts[0] + `var ${component}Router = require('./components/${component}/${component}Routes'); +`; + let newRoutesFile = parts.join('// SYSTEM-BUILDER-router.require'); + parts = newRoutesFile.split('// SYSTEM-BUILDER-app.use'); + parts[0] = parts[0] + `app.use('/api/v1/${component}', ${component}Router); +`; + newRoutesFile = parts.join('// SYSTEM-BUILDER-app.use'); + fs.writeFileSync(fileLocation, newRoutesFile, 'utf8'); +} + +function uppercaseFirstLetter(input: string) +{ + return input.charAt(0).toUpperCase() + input.slice(1); +} + +function lowercaseFirstLetter(input: string) +{ + return input.charAt(0).toLowerCase() + input.slice(1); +}