UNPKG

@resin/pinejs

Version:

Pine.js is a sophisticated rules-driven API engine that enables you to define rules in a structured subset of English. Those rules are used in order for Pine.js to generate a database schema and the associated [OData](http://www.odata.org/) API. This make

311 lines (287 loc) • 8.18 kB
import type * as Express from 'express'; import type { AbstractSqlModel } from '@resin/abstract-sql-compiler'; import type { Database } from '../database-layer/db'; import type { Migration } from '../migrator/migrator'; import type { AnyObject, Resolvable } from '../sbvr-api/common-types'; import * as Bluebird from 'bluebird'; import * as fs from 'fs'; import * as _ from 'lodash'; import * as path from 'path'; import * as sbvrUtils from '../sbvr-api/sbvr-utils'; import * as permissions from '../sbvr-api/permissions'; export type SetupFunction = ( app: Express.Application, sbvrUtilsInstance: typeof sbvrUtils, db: Database, ) => Resolvable<void>; export interface Model { apiRoot?: string; modelName?: string; modelFile?: string; modelText?: string; abstractSql?: AbstractSqlModel; migrationsPath?: string; migrations?: { [index: string]: Migration; }; initSqlPath?: string; initSql?: string; customServerCode?: | string | { setup: SetupFunction; }; logging?: { [key in keyof Console | 'default']?: boolean }; } export interface User { username: string; password: string; permissions: string[]; } export interface Config { models: Model[]; users?: User[]; } const getOrCreate = async ( authApiTx: sbvrUtils.PinejsClient, resource: string, uniqueFields: AnyObject, extraFields?: AnyObject, ) => { const [result] = (await authApiTx.get({ resource, options: { $select: 'id', $filter: uniqueFields, }, })) as Array<{ id: number }>; if (result != null) { return result.id; } const { id } = (await authApiTx.post({ resource, body: { ...uniqueFields, ...extraFields }, options: { returnResource: false }, })) as { id: number }; return id; }; const getOrCreatePermission = async ( authApiTx: sbvrUtils.PinejsClient, permissionName: string, ) => { try { return getOrCreate(authApiTx, 'permission', { name: permissionName }); } catch (e) { e.message = `Could not create or find permission "${permissionName}": ${e.message}`; throw e; } }; // Setup function export const setup = (app: Express.Application) => { const loadConfig = (data: Config): Bluebird<void> => sbvrUtils.db.transaction(async (tx) => { const authApiTx = sbvrUtils.api.Auth.clone({ passthrough: { tx, req: permissions.root, }, }); const { users } = data; if (users != null) { const permissionsCache: { [index: string]: Promise<number>; } = {}; users.forEach((user) => { if (user.permissions == null) { return; } user.permissions.forEach((permissionName) => { if (permissionsCache[permissionName] != null) { return; } permissionsCache[permissionName] = getOrCreatePermission( authApiTx, permissionName, ); }); }); await Bluebird.map(users, async (user) => { try { const userID = await getOrCreate( authApiTx, 'user', { username: user.username, }, { password: user.password, }, ); if (user.permissions != null) { await Bluebird.map(user.permissions, async (permissionName) => { const permissionID = await permissionsCache[permissionName]; await getOrCreate(authApiTx, 'user__has__permission', { user: userID, permission: permissionID, }); }); } } catch (e) { e.message = `Could not create or find user "${user.username}": ${e.message}`; throw e; } }); } await Bluebird.map(data.models, async (model) => { if ( (model.abstractSql != null || model.modelText != null) && model.apiRoot != null ) { try { await sbvrUtils.executeModel( tx, model as sbvrUtils.ExecutableModel, ); const apiRoute = `/${model.apiRoot}/*`; app.options(apiRoute, (_req, res) => res.sendStatus(200)); app.all(apiRoute, sbvrUtils.handleODataRequest); console.info( 'Successfully executed ' + model.modelName + ' model.', ); } catch (err) { const message = `Failed to execute ${model.modelName} model from ${model.modelFile}`; if (_.isError(err)) { err.message = message; throw err; } throw new Error(message); } } if (model.customServerCode != null) { let customCode: SetupFunction; if (typeof model.customServerCode === 'string') { try { customCode = nodeRequire(model.customServerCode).setup; } catch (e) { e.message = `Error loading custom server code: '${e.message}'`; throw e; } } else if (_.isObject(model.customServerCode)) { customCode = model.customServerCode.setup; } else { throw new Error( `Invalid type for customServerCode '${typeof model.customServerCode}'`, ); } if (typeof customCode !== 'function') { return; } return customCode(app, sbvrUtils, sbvrUtils.db); } }); }); const loadConfigFile = (configPath: string): Bluebird<Config> => { console.info('Loading config:', configPath); return Bluebird.resolve(import(configPath)); }; const loadApplicationConfig = Bluebird.method( async (config: string | Config | undefined) => { try { if (require.extensions['.coffee'] == null) { try { // Try to register the coffeescript loader if it doesn't exist // We ignore if it fails though, since that probably just means it is not available/needed. require('coffeescript/register'); } catch (e) { // Ignore errors } } if (require.extensions['.ts'] == null) { try { require('ts-node/register/transpile-only'); } catch (e) { // Ignore errors } } console.info('Loading application config'); let root: string; let configObj: Config; if (config == null) { root = path.resolve(process.argv[2]) || __dirname; configObj = await loadConfigFile(path.join(root, 'config.json')); } else if (typeof config === 'string') { root = path.dirname(config); configObj = await loadConfigFile(config); } else if (_.isObject(config)) { root = process.cwd(); configObj = config; } else { throw new Error(`Invalid type for config '${typeof config}'`); } const resolvePath = (s: string): string => { if (path.isAbsolute(s)) { return s; } return path.join(root, s); }; await Bluebird.map(configObj.models, async (model) => { if (model.modelFile != null) { model.modelText = await fs.promises.readFile( resolvePath(model.modelFile), 'utf8', ); } if (typeof model.customServerCode === 'string') { model.customServerCode = resolvePath(model.customServerCode); } if (model.migrations == null) { model.migrations = {}; } const migrations = model.migrations; if (model.migrationsPath) { const migrationsPath = resolvePath(model.migrationsPath); delete model.migrationsPath; await Bluebird.map( fs.promises.readdir(migrationsPath), async (filename) => { const filePath = path.join(migrationsPath, filename); const [migrationKey] = filename.split('-', 1); switch (path.extname(filename)) { case '.coffee': case '.ts': case '.js': migrations[migrationKey] = nodeRequire(filePath); break; case '.sql': migrations[migrationKey] = await fs.promises.readFile( filePath, 'utf8', ); break; default: console.error( `Unrecognised migration file extension, skipping: ${path.extname( filename, )}`, ); } }, ); } if (model.initSqlPath) { const initSqlPath = resolvePath(model.initSqlPath); model.initSql = await fs.promises.readFile(initSqlPath, 'utf8'); } }); await loadConfig(configObj); } catch (err) { console.error('Error loading application config', err, err.stack); process.exit(1); } }, ); return { loadConfig, loadApplicationConfig, }; };