@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
399 lines (384 loc) • 9.87 kB
JavaScript
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* DS203: Remove `|| {}` from converted for-own loops
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
import * as Bluebird from 'bluebird';
import * as permissions from '../sbvr-api/permissions';
const uiModel = `\
Vocabulary: ui
Term: text
Concept type: Text (Type)
Term: name
Concept type: Short Text (Type)
Term: textarea
--Database id Field: name
Reference Scheme: text
Fact type: textarea is disabled
Fact type: textarea has name
Necessity: Each textarea has exactly 1 name
Necessity: Each name is of exactly 1 textarea
Fact type: textarea has text
Necessity: Each textarea has exactly 1 text`;
// Middleware
const isServerOnAir = (() => {
/** @type { ((thenableOrResult?: import('../sbvr-api/common-types').Resolvable<boolean>) => void) | undefined } */
let resolve;
let promise = new Bluebird(($resolve) => {
resolve = $resolve;
});
return (/** @type {boolean} */ value) => {
if (value != null) {
if (resolve != null) {
resolve(value);
resolve = undefined;
} else {
promise = Bluebird.resolve(value);
}
}
return promise;
};
})();
/** @type { import('express').Handler } */
const serverIsOnAir = async (_req, _res, next) => {
const onAir = await isServerOnAir();
if (onAir) {
next();
} else {
next('route');
}
};
export let config = {
models: [
{
modelName: 'ui',
modelText: uiModel,
apiRoot: 'ui',
customServerCode: { setup },
migrations: {
'11.0.0-modified-at': `\
ALTER TABLE "textarea"
ADD COLUMN IF NOT EXISTS "modified at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL;\
`,
},
},
],
};
/** @type { import('../config-loader/config-loader').SetupFunction } */
export function setup(app, sbvrUtils, db) {
const uiApi = sbvrUtils.api.ui;
const devApi = sbvrUtils.api.dev;
const setupModels = async (
/** @type { import('../database-layer/db').Tx } */ tx,
) => {
try {
const uiApiTx = uiApi.clone({
passthrough: {
tx,
req: permissions.root,
},
});
await uiApiTx
.get({
resource: 'textarea',
options: {
$select: 'id',
$filter: {
name: 'model_area',
},
},
})
.then((/** @type { Array<{ [key: string]: any }> } */ result) => {
if (result.length === 0) {
// Add a model_area entry if it doesn't already exist.
return uiApiTx.post({
resource: 'textarea',
body: {
name: 'model_area',
text: ' ',
},
});
}
});
await devApi
.get({
resource: 'model',
passthrough: {
tx,
req: permissions.rootRead,
},
options: {
$select: ['is_of__vocabulary', 'model_value'],
$filter: {
model_type: 'se',
is_of__vocabulary: 'data',
},
},
})
.then((/** @type { Array<{ [key: string]: any }> } */ result) => {
if (result.length === 0) {
throw new Error('No SE data model found');
}
const instance = result[0];
return sbvrUtils.executeModel(tx, {
apiRoot: instance.is_of__vocabulary,
modelText: instance.model_value,
});
});
await isServerOnAir(true);
} catch {
await isServerOnAir(false);
}
};
app.get('/onAir', async (_req, res) => {
const onAir = await isServerOnAir();
res.json(onAir);
});
app.post(
'/update',
permissions.checkPermissionsMiddleware('all'),
serverIsOnAir,
(_req, res) => {
res.sendStatus(404);
},
);
app.post(
'/execute',
permissions.checkPermissionsMiddleware('all'),
async (_req, res) => {
try {
await uiApi
.get({
resource: 'textarea',
passthrough: { req: permissions.rootRead },
options: {
$select: 'text',
$filter: {
name: 'model_area',
},
},
})
.then(async (
/** @type { Array<{ [key: string]: any }> } */ result,
) => {
if (result.length === 0) {
throw new Error('Could not find the model to execute');
}
const modelText = result[0].text;
await db.transaction(async (tx) => {
await sbvrUtils.executeModel(tx, {
apiRoot: 'data',
modelText,
});
await uiApi.patch({
resource: 'textarea',
passthrough: {
tx,
req: permissions.root,
},
options: {
$filter: {
name: 'model_area',
},
},
body: {
is_disabled: true,
},
});
});
});
await isServerOnAir(true);
res.sendStatus(200);
} catch (err) {
await isServerOnAir(false);
res.status(404).json(err);
}
},
);
app.post(
'/validate',
permissions.checkPermissionsMiddleware('get'),
async (req, res) => {
try {
const results = await sbvrUtils.runRule('data', req.body.rule);
res.json(results);
} catch (err) {
console.log('Error validating', err);
res.sendStatus(404);
}
},
);
app.delete(
'/cleardb',
permissions.checkPermissionsMiddleware('delete'),
async (_req, res) => {
try {
await db.transaction(async (tx) => {
const result = await tx.tableList();
await Bluebird.map(result.rows, (table) => tx.dropTable(table.name));
await sbvrUtils.executeStandardModels(tx);
// TODO: HACK: This is usually done by config-loader and should be done there
// In general cleardb is very destructive and should really go through a full "reboot" procedure to set everything up again.
console.warn(
'DEL /cleardb is very destructive and should really be followed by a full restart/reload.',
);
await sbvrUtils.executeModels(tx, exports.config.models);
await setupModels(tx);
});
res.sendStatus(200);
} catch (err) {
console.error('Error clearing db', err, err.stack);
res.sendStatus(503);
}
},
);
app.put(
'/importdb',
permissions.checkPermissionsMiddleware('set'),
async (req, res) => {
try {
const queries = req.body.split(';');
await db.transaction((tx) =>
Bluebird.each(queries, async (query) => {
query = query.trim();
if (query.length > 0) {
try {
await tx.executeSql(query);
} catch (err) {
throw [query, err];
}
}
}),
);
res.sendStatus(200);
} catch (err) {
console.error('Error importing db', err, err.stack);
res.sendStatus(404);
}
},
);
app.get(
'/exportdb',
permissions.checkPermissionsMiddleware('get'),
async (_req, res) => {
try {
let exported = '';
await db.transaction(async (tx) => {
const tables = await tx.tableList("name NOT LIKE '%_buk'");
await Bluebird.map(tables.rows, async (table) => {
const tableName = table.name;
exported += 'DROP TABLE IF EXISTS "' + tableName + '";\n';
exported += table.sql + ';\n';
const result = await tx.executeSql(
'SELECT * FROM "' + tableName + '";',
);
let insQuery = '';
result.rows.forEach((currRow) => {
let notFirst = false;
insQuery += 'INSERT INTO "' + tableName + '" (';
let valQuery = '';
for (let propName of Object.keys(currRow || {})) {
if (notFirst) {
insQuery += ',';
valQuery += ',';
} else {
notFirst = true;
}
insQuery += '"' + propName + '"';
valQuery += "'" + currRow[propName] + "'";
}
insQuery += ') values (' + valQuery + ');\n';
});
exported += insQuery;
});
});
res.json(exported);
} catch (err) {
console.error('Error exporting db', err, err.stack);
res.sendStatus(503);
}
},
);
app.post(
'/backupdb',
permissions.checkPermissionsMiddleware('all'),
serverIsOnAir,
async (_req, res) => {
try {
await db.transaction(async (tx) => {
const result = await tx.tableList("name NOT LIKE '%_buk'");
await Bluebird.map(result.rows, async (currRow) => {
const tableName = currRow.name;
await tx.dropTable(tableName + '_buk', true);
await tx.executeSql(
'ALTER TABLE "' +
tableName +
'" RENAME TO "' +
tableName +
'_buk";',
);
});
});
res.sendStatus(200);
} catch (err) {
console.error('Error backing up db', err, err.stack);
res.sendStatus(404);
}
},
);
app.post(
'/restoredb',
permissions.checkPermissionsMiddleware('all'),
serverIsOnAir,
async (_req, res) => {
try {
await db.transaction(async (tx) => {
const result = await tx.tableList("name LIKE '%_buk'");
await Bluebird.map(result.rows, async (currRow) => {
const tableName = currRow.name;
await tx.dropTable(tableName.slice(0, -4), true);
await tx.executeSql(
'ALTER TABLE "' +
tableName +
'" RENAME TO "' +
tableName.slice(0, -4) +
'";',
);
});
});
res.sendStatus(200);
} catch (err) {
console.error('Error restoring db', err, err.stack);
res.sendStatus(404);
}
},
);
app.all('/data/*', serverIsOnAir, sbvrUtils.handleODataRequest);
app.get('/Auth/*', serverIsOnAir, sbvrUtils.handleODataRequest);
app.merge('/ui/*', sbvrUtils.handleODataRequest);
app.patch('/ui/*', sbvrUtils.handleODataRequest);
app.delete('/', serverIsOnAir, async (_req, res) => {
await Promise.all([
uiApi.patch({
resource: 'textarea',
passthrough: { req: permissions.root },
options: {
$filter: {
name: 'model_area',
},
},
body: {
text: '',
name: 'model_area',
is_disabled: false,
},
}),
sbvrUtils.deleteModel('data'),
]);
await isServerOnAir(false);
res.sendStatus(200);
});
return db.transaction(setupModels);
}