@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
220 lines (211 loc) • 5.61 kB
JavaScript
import * as Bluebird from 'bluebird';
import * as _ from 'lodash';
/**
* @typedef {Partial<Window> & {GLOBAL_PERMISSIONS?: string[]}} ExtendedWindow
*/
/** @type {ExtendedWindow} */
const w = typeof window !== 'undefined' && window !== null ? window : {};
w.GLOBAL_PERMISSIONS = ['resource.all'];
const app = (function () {
/** @type Function */
let ready;
const enabled = new Bluebird((resolve) => {
ready = resolve;
});
/** @type {{[key: string]: any}} */
const appVars = { env: 'development' };
/** @type {{[key: string]: any}} */
const handlers = {
// USE is a list of middleware to run before any request.
USE: [],
POST: [],
PUT: [],
DELETE: [],
GET: [],
PATCH: [],
MERGE: [],
OPTIONS: [],
};
const addHandler = function (
/** @type string */ handlerName,
/** @type {string} */ match,
/** @type import('express').Handler[] */ ...middleware
) {
//Strip wildcard
let paramName;
match = match.toLowerCase();
const newMatch = match.replace(/[\/\*]*$/, '');
if (newMatch !== match) {
match = newMatch;
paramName = '*';
} else {
const paramMatch = /:(.*)$/.exec(match);
paramName = paramMatch?.[1];
}
handlers[handlerName].push({
match,
paramName,
// Flatten middleware list to handle arrays of middleware in the arg list.
middleware: _.flattenDeep(middleware),
});
};
const process = function (
/** @type string */ method,
/** @type string */ uri,
/** @type {{[key: string]: any}} */ headers,
/** @type any */ body,
) {
if (body == null) {
body = '';
}
if (!handlers[method]) {
return Bluebird.reject([404, null, null]);
}
const req = {
// Have a default user for in-browser with all permissions
user: {
permissions: w.GLOBAL_PERMISSIONS,
},
method,
body,
headers,
url: uri,
/** @type {{[key: string]: any}} */
params: {},
query: {},
login(/** @type any */ _user, /** @type Function */ callback) {
callback();
},
};
console.log(method, uri, body);
if (uri.slice(-1) === '/') {
uri = uri.slice(0, uri.length - 1);
}
uri = uri.toLowerCase();
return new Bluebird(function (resolve, reject) {
const res = {
statusCode: 200,
status(/** @type number */ statusCode) {
this.statusCode = statusCode;
return this;
},
json(/** @type any */ obj) {
// Stringify and parse to emulate passing over network.
obj = JSON.parse(JSON.stringify(obj));
if (this.statusCode >= 400) {
reject([this.statusCode, obj, null]);
} else {
resolve([this.statusCode, obj, null]);
}
},
send(/** @type any */ data) {
data = _.cloneDeep(data);
if (this.statusCode >= 400) {
reject([this.statusCode, data, null]);
} else {
resolve([this.statusCode, data, null]);
}
},
sendStatus(/** @type undefined | number */ statusCode) {
if (statusCode == null) {
({ statusCode } = this);
}
if (statusCode >= 400) {
reject([statusCode, null, null]);
} else {
resolve([statusCode, null, null]);
}
},
redirect() {
reject([307]);
},
set() {
// noop
},
type() {
// noop
},
};
const methodHandlers = handlers.USE.concat(handlers[method]);
let i = -1;
let j = -1;
const next = function (/** @type {undefined | 'route'} */ route) {
j++;
if (route === 'route' || j >= methodHandlers[i].middleware.length) {
checkMethodHandlers();
} else {
methodHandlers[i].middleware[j](req, res, next);
}
};
const checkMethodHandlers = () => {
i++;
if (i < methodHandlers.length) {
if (
uri.slice(0, methodHandlers[i].match.length) ===
methodHandlers[i].match
) {
j = -1;
// Reset params that may have been added on previous routes that failed in middleware
req.params = {};
if (methodHandlers[i].paramName != null) {
req.params[methodHandlers[i].paramName] = uri.slice(
methodHandlers[i].match.length,
);
next();
} else if (uri.length !== methodHandlers[i].match.length) {
// Not an exact match and no parameter matching
checkMethodHandlers();
} else {
next();
}
} else {
checkMethodHandlers();
}
} else {
res.sendStatus(404);
}
};
checkMethodHandlers();
});
};
return {
use: _.partial(addHandler, 'USE', '/*'),
get(/** @type string */ name) {
const callback = arguments[arguments.length - 1];
if (typeof callback === 'function') {
addHandler('GET', ...arguments);
} else {
return appVars[name];
}
},
post: _.partial(addHandler, 'POST'),
put: _.partial(addHandler, 'PUT'),
delete: _.partial(addHandler, 'DELETE'),
patch: _.partial(addHandler, 'PATCH'),
merge: _.partial(addHandler, 'MERGE'),
options: _.partial(addHandler, 'OPTIONS'),
all(/** @type any[] */ ...args) {
this.post(...args);
this.get(...args);
this.put(...args);
this.delete(...args);
},
process(/** @type any[] */ ...args) {
// The promise will run the real process function asynchronously once the app is enabled,
// which matches somewhat more closely to an AJAX call than doing it synchronously.
return enabled.then(() => process(...args));
},
listen() {
const callback = arguments[arguments.length - 1];
ready();
if (typeof callback === 'function') {
return enabled.then(callback);
}
},
set(/** @type string */ name, /** @type any */ value) {
appVars[name] = value;
},
};
})();
const express = () => app;
module.exports = express;