@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
text/typescript
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,
};
};