import { ColumnRef, ComponentDef, EndpointDef, Filter, JoinDef, Order, SystemDef, } from '../systemGenService'; import * as path from 'path'; import * as fs from 'fs'; import pluralize from 'pluralize'; const ncp = require('ncp').ncp; export function createViews(systemDef: SystemDef) { let viewsPromises = []; for (let i in systemDef.components) { viewsPromises.push(createComponent(systemDef.components[i], systemDef)); } return Promise.all(viewsPromises); } function createComponent(component: ComponentDef, systemDef: SystemDef) { return new Promise((componentResolve, componentReject) => { ncp(path.join(process.cwd(), 'frame', 'src', 'components', '{{component}}'), path.join(process.cwd(), systemDef.name, 'src', 'components', component.component), (err: any) => { if (err) { console.log(err); } else { let mapperPromise = initializeComponentFile(path.join(process.cwd(), systemDef.name, 'src', 'components', component.component, '{{component}}Mapper.ets'), path.join(process.cwd(), systemDef.name, 'src', 'components', component.component, `${component.component}Mapper.ts`), component.component, systemDef.name); for (let i in component.endpoints) { mapperPromise = mapperPromise.then(() => { return insertMapperCode(component.endpoints[i], systemDef.name); }); } let routesPromise = initializeComponentFile(path.join(process.cwd(), systemDef.name, 'src', 'components', component.component, '{{component}}Routes.ets'), path.join(process.cwd(), systemDef.name, 'src', 'components', component.component, `${component.component}Routes.ts`), component.component, systemDef.name); addInitializeRoutesCode(component.component, systemDef.name); let serviceFileLocation: string = path.join(process.cwd(), systemDef.name, 'src', 'components', component.component, `${component.component}Service.ts`); let servicePromise = initializeComponentFile(path.join(process.cwd(), systemDef.name, 'src', 'components', component.component, '{{component}}Service.ets'), serviceFileLocation, component.component, systemDef.name); addInitializeServiceCode(component.component, systemDef.name); Promise.all([mapperPromise, routesPromise, servicePromise]).then(() => { console.log(`success creating component ${component.component}`); componentResolve(); }); } }); }); } function initializeComponentFile(sourceFile: string, destinationFile: string, component: string, outDir: string) { return 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(); }); }) } 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 insertMapperCode(view: EndpointDef, outDir: string): Promise { return new Promise((resolve) => { const separator: string = `// SYSTEM-BUILDER-${view.component}-mapper`; let fileLocation = path.join(process.cwd(), outDir, 'src', 'components', view.component, `${view.component}Mapper.ts`); let initMapperFile: string = fs.readFileSync(fileLocation, 'utf8'); let parts = initMapperFile.split(separator); parts[0] = parts[0] + createMapperFunc(view); let newMapperFile = parts.join(separator); fs.writeFileSync(fileLocation, newMapperFile, 'utf8'); resolve(); }); } function createMapperFunc(view: EndpointDef): string { let func: string = ''; if (view.type === 'list' || view.type === 'count') { func = buildGetList(view); } else if (view.type === 'item') { func = buildGetItem(view); } else if (view.type === 'update') { func = buildUpdateFunc(view); } else if (view.type === 'create') { func = buildCreateFunc(view); } else if (view.type === 'delete') { func = buildDeleteItem(view); } return func; } function buildDeleteItem(view: EndpointDef): string { let func: string = ''; let funcName = buildFunctionName(view); let tables: string = `${view.table}`; let filtersObj = buildFilters(view); let filters: string = filtersObj.filters; let requiredFilterValues: string = filtersObj.requiredFilterValues; let optionalFilterParts = filtersObj.optionalFiltersCode; let query: string = `DELETE FROM ${tables}${filters}`; let funcParams = 'params: {[key: string]: string}'; func = ` public ${funcName}(${funcParams}) { let query: string = '${query}'; let queryValues: string[] = [${requiredFilterValues}]; // optional params? ${optionalFilterParts} return super.runQuery(query, [...queryValues]); } `; return func; } function buildGetItem(view: EndpointDef): string { let func: string = ''; let funcName = buildFunctionName(view); let columns: string = `${view.component}.${view.component}_id, ${view.columns.map(c => `${c.table}.${c.name}`).join(', ')}`; let tables: string = view.join?.length ? buildJoin(view) : `${view.table}`; let filtersObj = buildFilters(view); let filters: string = filtersObj.filters; let requiredFilterValues: string = filtersObj.requiredFilterValues; let optionalFilterParts = filtersObj.optionalFiltersCode; let query: string = `SELECT ${columns} FROM ${tables}${filters} LIMIT 1`; let funcParams = 'params: {[key: string]: string}'; func = ` public ${funcName}(${funcParams}) { let query: string = '${query}'; let queryValues: string[] = [${requiredFilterValues}]; // optional params? ${optionalFilterParts} return super.runQuery(query, [...queryValues]); } `; return func; } function buildCreateFunc(view: EndpointDef): string { let func: string = ''; let query: string = `INSERT INTO ${view.table} SET `; func = getCreateOrUpdateFunc(query, view); return func; } function buildUpdateFunc(view: EndpointDef): string { let func: string = ''; let query: string = `UPDATE ${view.table} SET `; func = getCreateOrUpdateFunc(query, view); return func; } function getCreateOrUpdateFunc(query: string, view: EndpointDef): string { let func: string = ''; let funcName = buildFunctionName(view); let requiredQueryValues: string = ''; let optionalValuesCode: string = ''; for (let i in view.columns) { let c: ColumnRef = view.columns[i]; if (c.required) { if (!query.endsWith('SET ')) { query = query + `, `; } query = query + `${c.table}.${c.name} = ?`; if (requiredQueryValues.length) { requiredQueryValues = requiredQueryValues + `, `; } requiredQueryValues = requiredQueryValues + `params.${c.param}`; } else { optionalValuesCode = optionalValuesCode + ` if (params.${c.param}) { query = query + ', ${c.table}.${c.name} = ?'; queryValues.push(params.${c.param}); } `; } } func = ` public ${funcName}(params: {[key: string]: string}) { let query: string = '${query}'; let queryValues: string[] = [${requiredQueryValues}]; // optional params? ${optionalValuesCode} return super.runQuery(query, [...queryValues]); } `; return func; } function buildGetList(view: EndpointDef): string { // TODO: optionally allow a raw query definition with predefined params // This would be so much simpler and easy if it were just the queries wanted let func: string = ''; let funcName: string = ''; let columns: string = view.type === 'count' ? `count(*)` : `${view.component}.${view.component}_id, ${view.columns.map(c => `${c.table}.${c.name}`).join(', ')}`; let query: string = ''; let tables: string = view.join?.length ? buildJoin(view) : `${view.table}`; let filters: string = ''; funcName = buildFunctionName(view); let requiredFilterStrings = []; let requiredQueryValues = ''; let optionalFilterParts = ''; if (view.filters) { for (let i in view.filters) { let f: Filter = view.filters[i]; if (f.required) { requiredFilterStrings.push(`${f.column.table}.${f.column.name} ${f.comparison} ?`); // e.g. 'table.id = ?' if (requiredQueryValues.length) { requiredQueryValues = requiredQueryValues + `, `; } requiredQueryValues = requiredQueryValues + `params.${f.param}`; } else { /* e.g. if (eventLog.app) { query = query + ', app = ?'; queryValues.push(eventLog.app); } */ optionalFilterParts = optionalFilterParts + ` if (params.${f.param}) { query = query + ' AND ${f.column.table}.${f.column.name} ${f.comparison} ?'; queryValues.push(params.${f.param}); }`; } } filters = ` WHERE ${requiredFilterStrings.join(' AND ')}`; } query = `SELECT ${columns} FROM ${tables}${filters}`; let orderByCode = ''; if (view.orderBy) { orderByCode = ` query = query + '${buildOrderBy(view)}'; `; } let funcParams = 'params: {[key: string]: string}, offset: number, limit: number'; let limitOrderCode = `query = query + ' LIMIT ?, ?'; queryValues.push(offset.toString(10)); queryValues.push(limit.toString(10)); ${orderByCode}`; if (view.type === 'count') { funcParams = 'params: {[key: string]: string}'; limitOrderCode = ``; } func = ` public ${funcName}(${funcParams}) { let query: string = '${query}'; let queryValues: string[] = [${requiredQueryValues}]; // optional params? ${optionalFilterParts} ${limitOrderCode} return super.runQuery(query, [...queryValues]); } `; return func; } function buildFilters(view: EndpointDef): {filters: string, requiredFilterValues: string, optionalFiltersCode: string} { let filtersObj: {filters: string, requiredFilterValues: string, optionalFiltersCode: string} = { filters: '', requiredFilterValues: '', optionalFiltersCode: '' }; let filters: string = ''; let requiredFilterStrings = []; let requiredQueryValues = ''; let optionalFilterParts = ''; if (view.filters) { for (let i in view.filters) { let f: Filter = view.filters[i]; if (f.required) { requiredFilterStrings.push(`${f.column.table}.${f.column.name} ${f.comparison} ?`); // e.g. 'table.id = ?' if (requiredQueryValues.length) { requiredQueryValues = requiredQueryValues + `, `; } requiredQueryValues = requiredQueryValues + `params.${f.param}`; } else { /* e.g. if (eventLog.app) { query = query + ', app = ?'; queryValues.push(eventLog.app); } */ optionalFilterParts = optionalFilterParts + ` if (params.${f.param}) { query = query + ' AND ${f.column.table}.${f.column.name} ${f.comparison} ?'; queryValues.push(params.${f.param}); }`; } } filters = ` WHERE ${requiredFilterStrings.join(' AND ')}`; } filtersObj.filters = filters; filtersObj.requiredFilterValues = requiredQueryValues; filtersObj.optionalFiltersCode = optionalFilterParts; return filtersObj; } function buildJoin(view: EndpointDef): string { if (view.join?.length) { let join = `${view.table}`; for (let i in view.join) { let j: JoinDef = view.join[i]; join = join + ` JOIN ${j.table} ON ${j.on.left.table}.${j.on.left.name} = ${j.on.right.table}.${j.on.right.name}`; } return join; } return ''; } function buildOrderBy(view: EndpointDef): string { let orderBy: string = ''; if (view.orderBy?.length) { orderBy = orderBy + ' ORDER BY '; for (let i in view.orderBy) { let order: Order = view.orderBy[i]; if (parseInt(i) !== 0) { orderBy = orderBy + ', '; } orderBy = orderBy + `${order.column.table}.${order.column.name} ${order.direction}`; } } return orderBy; } function uppercaseFirstLetter(input: string) { return input.charAt(0).toUpperCase() + input.slice(1); } function lowercaseFirstLetter(input: string) { return input.charAt(0).toLowerCase() + input.slice(1); } function buildFunctionName(view: EndpointDef): string { let selector: {[type: string]: (view: EndpointDef) => string} = { 'list': (view: EndpointDef) => `get${pluralize(uppercaseFirstLetter(view.component))}` + (view.type === 'count' ? 'Count' : ''), 'count': (view: EndpointDef) => `get${pluralize(uppercaseFirstLetter(view.component))}` + (view.type === 'count' ? 'Count' : ''), 'item': (view: EndpointDef) => `get${uppercaseFirstLetter(view.component)}`, 'update': (view: EndpointDef) => `update${uppercaseFirstLetter(view.component)}`, 'create': (view: EndpointDef) => `create${uppercaseFirstLetter(view.component)}`, 'delete': (view: EndpointDef) => `delete${uppercaseFirstLetter(view.component)}`, 'search': (view: EndpointDef) => `search${uppercaseFirstLetter(view.component)}`, }; return selector[view.type](view); }