import DeleteRequest from './requests/delete-request.js';
import GetRequest from './requests/get-request.js';
import OptionsRequest from './requests/options-request.js';
import PatchRequest from './requests/patch-request.js';
import PostRequest from './requests/post-request.js';
import PutRequest from './requests/put-request.js';
import Response from './response.js';
import RESTError from './rest-error.js';
import Filter from './filter.js';
import Logger from './logger.js';
import Sort from './sort.js';
import ModelGenerator from './modeling/model-generator.js';
import ModelUtility from './modeling/model-utility.js';
import StringUtility from './utilities/string-utility.js';
import BaseEngine from './engines/base-engine.js';
import MemoryEngine from './engines/memory-engine.js';
import FetchEngine from './engines/fetch-engine.js';
const SUPPORTED_METHODS = ['all', '*', 'get', 'post', 'put', 'patch', 'delete', 'options'];
const SUPPORTED_STATES = ['log', 'request', 'response', 'done'];
const IS_BROWSER = !(typeof process !== 'undefined' && process.version);
let HttpRequestLoader = null; //see .requestFromObject
/**
* @callback StashKuMiddlewareCallback
* @param {StashKu} stashku - The StashKu instance making the middleware call.
* @param {BaseEngine} engine - The storage engine instance that is processing the request.
* @param {String} method - The name of the request method being carried out, typically "get", "post", "put", "patch",
* "delete", "options".
* @param {GetRequest|PostRequest|PutRequest|PatchRequest|DeleteRequest|OptionsRequest} request - The request object being specified.
* @param {Response} response - The response object (`null` if not prepared yet).
* @param {String} state
* The name of the state of where in the RESTful process the callback is being made, either:
* - "request": Indicates the callback was made before the request has handed off to the engine.
* - "response": Indicates the callback was made after the engine's response has been returned.
* - "done": Indicates the callback was made after the engine has completed the response and the action is about to
* complete (StashKu hands off the storage engine response to the caller).
*/
/**
* @callback StashKuMiddlewareLogCallback
* @param {StashKu} stashku - The StashKu instance making the middleware call.
* @param {BaseEngine} engine - The storage engine instance that is currently loaded.
* @param {String} severity - The severity level of the log message, either "error", "warn", "info", or "debug".
* @param {Array} args - The log message arguments, this can be a mix of types.
*/
/**
* @typedef StashKuMiddleware
* @property {String} [name] - An optional name for the middleware that is used for logging purposes.
* @property {Array.<String>} [states] - The name(s) of the states to target, either "all"/"*", "log", "request",
* "response", or "done".
* @property {Array.<String>} [methods] - The name(s) of the methods to target, either "all"/"*", "get", "post", "put",
* "patch" or "delete".
* @property {StashKuMiddlewareCallback|StashKuMiddlewareLogCallback} callback - The callback to be called when being processed.
*/
/**
* @typedef StashKuModelConfiguration
* @property {Boolean} [header=false] - Instructs StashKu to add a header `model` with the value of the `$stashku`
* definition to all modelled RESTful requests. Certain engines, such as `fetch` may offer advanced features that
* leverage model information in their operation.
*/
/**
* @typedef StashKuConfiguration
* @property {String|BaseEngine} engine - The name or instance of the StashKu engine to use.
* @property {Array.<StashKuMiddleware|StashKuMiddlewareCallback|StashKuMiddlewareLogCallback>} middleware
* @property {StashKuModelConfiguration} model - Configuration describing how StashKu works with models.
* @property {Array.<String>} resources - Optional array of resource names that are allowed through this instance of
* StashKu. Any request asking for a resource not found in this array will throw an error.
*/
/**
* The StashKu class provides the ability to work with a variety of storage mediums through a single, standardized,
* RESTful interface, working much like HTTP - using a request and a response.
*
* StashKu supports middleware which can be configured to pre- and post-process requests and responses.
* @template M
* @template I
*/
class StashKu {
/**
* Creates a new StashKu instance using the provided configuration settings (which override defaults).
* @param {StashKu | StashKuConfiguration} [config] - The configuration settings which override defaults.
*/
constructor(config) {
/**
* The currently active storage engine, or the promise to load it.
* All operations will await the engine if it is a promise.
* @type {BaseEngine|Promise.<BaseEngine>}
*/
this.engine = null;
/**
* Holds onto the last loaded configuration.
* If you wish to change the configuration, you should use the `configure` function.
* @type {StashKuConfiguration}
*/
this.config = null;
/**
* Middleware functions that execute before sending requests or returning responses.
* @type {Array.<StashKuMiddleware>}
*/
this.middleware = [];
/**
* Counter of all requests handled by this instance (or parent instance in the case of a proxy) of stashku.
*/
this.stats = {
requests: {
total: 0,
get: 0,
put: 0,
post: 0,
patch: 0,
delete: 0,
options: 0
}
};
/**
* @type {Logger}
*/
this.log = null;
if (config && config.proxy && config.proxy.model && config.proxy.parent) {
//this instance is being crafted for a call to the `.model` function and is meant to act as
//a proxy to a parent StashKu class.
if ((config.proxy.parent instanceof StashKu) === false) {
throw new RESTError(500, 'StashKu cannot initialize a proxy instance from a non-StashKu parent instance.');
}
this.log = config.proxy.parent.log;
this.engine = config.proxy.parent.engine;
this.middleware = config.proxy.parent.middleware;
this.stats = config.proxy.parent.stats;
this.resources = config.proxy.parent.resources;
//shallow-clone configuration, set proxy
this.config = Object.assign({}, config.proxy.parent.config, { proxy: config.proxy }, { model: config.proxy.parent.config.model });
} else {
this.log = new Logger(this.middlerun.bind(this));
//apply standard configuration
this.configure(config ?? null);
}
}
/**
* Adds the specified middleware to the StashKu instance.
* @param {StashKuMiddleware|StashKuMiddlewareCallback|StashKuMiddlewareLogCallback} middleware - The middleware to use.
* @returns {StashKu}
*/
use(middleware) {
if (typeof middleware === 'function') {
//config specifies a single middleware function.
this.middleware.push({
callback: middleware
});
this.log.debug(`Unnamed middleware loaded at position ${this.middleware.length - 1}.`);
} else if (middleware.callback) {
if (middleware.states) {
if (typeof middleware.states === 'string') {
middleware.states = [middleware.states]; //convert to array
} else if (Array.isArray(middleware.states) === false) {
throw new RESTError(500, 'The middleware specified contains invalid states. The states specified must be a string or array of strings.');
}
if (middleware.states.every(m => typeof m === 'string') === false) {
throw new RESTError(500, 'The middleware specified contains invalid states. All state values specified must be strings.');
} else if (middleware.states.every(m => SUPPORTED_STATES.indexOf(m) >= 0) === false) {
throw new RESTError(500, `The middleware specified contains invalid states. Only "${SUPPORTED_STATES.join('", "')}" states are allowed.`);
}
}
if (middleware.methods) {
if (typeof middleware.methods === 'string') {
middleware.methods = [middleware.methods]; //convert to array
} else if (Array.isArray(middleware.methods) === false) {
throw new RESTError(500, 'The middleware specified contains invalid methods. The methods specified must be a string or array of strings.');
}
if (middleware.methods.every(m => typeof m === 'string') === false) {
throw new RESTError(500, 'The middleware specified contains invalid methods. All method values specified must be strings.');
} else if (middleware.methods.every(m => SUPPORTED_METHODS.indexOf(m) >= 0) === false) {
throw new RESTError(500, `The middleware specified contains invalid methods. Only "${SUPPORTED_METHODS.join('", "')}" methods are allowed.`);
}
}
this.middleware.push(middleware);
if (middleware.name) {
this.log.debug(`"${middleware.name}" middleware loaded at position ${this.middleware.length - 1}.`);
} else {
this.log.debug(`Unnamed middleware loaded at position ${this.middleware.length - 1}.`);
}
}
return this;
}
/**
* Configures this StashKu instance with the specified configuration. If a `null` value is passed, the
* configuration is cleared and reset to the default.
*
* When the `config` parameter is undefined (not specified), this function can be awaited. This will await the
* import and load of the configured StashKu engine. If the engine is already loaded, the function will return.
* @throws Error if the configured storage engine name (`config.engine`) is not registered with StashKu.
* @param {StashKuConfiguration} [config] - The configuration settings which override defaults.
* @returns {StashKu}
*/
configure(config) {
if (typeof config === 'undefined') {
if (this.engine && this.engine.then) {
return this.engine.then((v) => this);
} else {
return this;
}
}
this.log.debug('Configuring StashKu...');
//assign defaults
let engineDefault = 'memory';
let modelDefault = {
header: false
};
if (typeof process !== 'undefined' && typeof process.env === 'object') {
engineDefault = process.env.STASHKU_ENGINE ?? (IS_BROWSER ? 'fetch' : 'memory');
if (typeof process.env.STASHKU_MODEL_HEADER === 'string') {
modelDefault.header = !!process.env.STASHKU_MODEL_HEADER.match(/^[tTyY1]/);
}
} else {
engineDefault = (IS_BROWSER ? 'fetch' : 'memory');
}
this.config = Object.assign({
engine: engineDefault,
middleware: [],
resources: []
}, config, { model: Object.assign(modelDefault, config?.model) });
this.log.debug('Configuration=', this.config);
//load engine
if (this.config.engine === 'memory') {
this.engine = new MemoryEngine();
this.engine.configure(this.config.memory, this.log);
} else if (this.config.engine === 'fetch') {
this.engine = new FetchEngine();
this.engine.configure(this.config.fetch, this.log);
} else if (typeof this.config.engine === 'string' && IS_BROWSER === false) {
let enginePackageName = this.config.engine;
//set import package as variable, so compilers like esbuild ignore the import
let pkg = './node/package-loader.js';
this.engine = import(/* webpackIgnore: true */pkg)
.then(loader => loader.default(enginePackageName))
.then(m => {
this.engine = new m.default();
this.engine.configure(this.config[this.config.engine], this.log);
return this.engine;
});
} else if (this.config.engine instanceof BaseEngine) {
this.engine = this.config.engine;
}
//load middleware
if (Array.isArray(this.config.middleware)) {
//array of possible middleware functions and objects
for (let mw of this.config.middleware) {
this.use(mw);
}
} else if (typeof this.config.middleware === 'function') {
//config specifies a single middleware function.
this.middleware.push({
callback: this.config.middleware
});
}
this.log.debug('Done configuring StashKu.');
return this;
}
/**
* Runs configured middleware callbacks matching the current state and request method.
* @param {String} state
* The name of the state of where in the RESTful process the callback is being made, either:
* - "request": Indicates the callback was made before the request has handed off to the engine.
* - "response": Indicates the callback was made before the engine's response has been considered.
* - "done": Indicates the callback was made after the engine has completed the response and the action is about to
* complete (StashKu hands off the storage engine response to the caller).
* @param {GetRequest|PostRequest|PutRequest|PatchRequest|DeleteRequest|OptionsRequest} request - The request object being specified.
* @param {Promise.<Response>} response - The response object (`null` if not prepared yet).
*/
async middlerun(state, request, response) {
if (SUPPORTED_STATES.indexOf(state) < 0) {
throw new Error(`The "state" argument specified contain an invalid value. Only "${SUPPORTED_STATES.join('", "')}" states are allowed.`);
}
if (this.middleware && this.middleware.length) {
for (let x = 0; x < this.middleware.length; x++) {
let mw = this.middleware[x];
if (mw.callback) {
let stateAllowed = (!mw.states || mw.states.length === 0 || mw.states.indexOf(state) >= 0);
let methodAllowed = (
state === 'log'
|| !mw.methods
|| mw.methods.indexOf('*') >= 0
|| mw.methods.indexOf('all') >= 0
|| mw.methods.indexOf(request.method) >= 0
);
if (stateAllowed && methodAllowed) {
if (state === 'log') {
//under the "log" state...
//request = severity string
//response = args array
await mw.callback(this, this.engine, request, response);
} else {
if (mw.name) {
this.log.debug(`Calling middleware ${x + 1} of ${this.middleware.length} "${mw.name}".`);
} else {
this.log.debug(`Calling middleware ${x + 1} of ${this.middleware.length}.`);
}
await mw.callback(this, this.engine, request.method, request, response, state);
this.log.debug(`Completed middleware call ${x + 1} of ${this.middleware.length}.`);
}
}
}
}
}
}
/**
* Handles the `request` and ensures it is resolved to a response object, even when defined as a callback.
*
* @throws Error if the "request" argument is not a callback function or request-like instance.
* @throws Error if the engine module fails to load.
* @throws Error if the engine is `null`.
* @param {GetRequest|PostRequest|PutRequest|PatchRequest|DeleteRequest|OptionsRequest} request - The request to ensure.
* @param {*} requestType - The expected request instance type.
* @returns {Promise.<Response>} Returns the data objects from storage matching request criteria.
* @private
*/
async _handle(request, requestType) {
//validate arguments
if (!requestType) {
throw new Error('A "requestType" parameter argument is required.');
}
//build request
let reqModel = this.config?.proxy?.model;
if (IS_BROWSER === false && request.url && request.httpVersion) { //looks like we want to process a StashKu request from an HTTP request.
request = await StashKu.requestFromObject(request, reqModel);
} else if (typeof request === 'function') {
//process callback
let tmp = new requestType();
request(tmp, reqModel);
request = tmp;
}
//validate
if ((request instanceof requestType) === false) {
throw new Error(`The "request" argument must be a callback function or ${requestType.name} instance.`);
} else if (!this.engine) {
throw new Error('A StashKu storage engine has not been loaded. An engine must be configured before operations are allowed.');
}
//adjust the request by model, if present
if (reqModel) {
request.model(reqModel, false, this.config?.model?.header);
}
//access restrictions
if (this.config.resources && this.config.resources.length) {
let resource = request.metadata.from || request.metadata.to;
if (this.config.resources.indexOf(resource) < 0) {
throw new RESTError(403, 'Forbidden.');
}
}
//pre-process request
let response = null;
this.stats.requests[request.method]++;
this.stats.requests.total++;
let reqID = this.stats.requests.total.toString().padStart(16, '0');
this.log.debug(`[${reqID}] Processing "${request.method}" request.`);
try {
this.engine = await this.engine; //resolve if a promise.
} catch (err) {
throw new RESTError(500, `The StashKu storage engine "${this.config.engine}" could not be loaded. ${err.toString()}`);
}
this.log.debug(`[${reqID}] Running "${request.method}" request middleware.`);
await this.middlerun('request', request, response);
if (this.engine[request.method]) {
try {
//make the request and get the response
this.log.debug(`[${reqID}] Sending "${request.method}" request to engine "${this.engine.name}".`);
response = await this.engine[request.method](request);
this.log.debug(`[${reqID}] Received response for "${request.method}" request from engine "${this.engine.name}".`);
if (reqModel) {
//convert data
this.log.debug(`[${reqID}] Modelling response data.`);
if (response.data && response.data.length) {
let counter = 0;
for (let m of ModelUtility.model(reqModel, request.method, ...response.data)) {
response.data[counter] = m;
counter++;
}
}
}
this.log.debug(`[${reqID}] Running middleware for response.`);
await this.middlerun('response', request, response);
} catch (err) {
if ((err instanceof RESTError) === false) {
this.log.debug(`[${reqID}] A StashKu internal error occurred while using storage engine "${this.engine.name}": ${err.message}`, err);
throw new RESTError(500, `A StashKu internal error occurred while using storage engine "${this.engine.name}": ${err.message}`, err);
} else {
this.log.debug(`[${reqID}] A StashKu internal REST error occurred while using storage engine "${this.engine.name}": ${err.message}`, err);
}
throw err;
}
} else {
throw new RESTError(501, `The StashKu storage engine "${this.engine.name}" does not know how to handle the method "${request.method}".`);
}
//validate engine response
if (!response) {
throw new RESTError(403, `The StashKu storage engine "${this.engine.name}" did not return a response.`);
} else if ((response instanceof Response) === false) {
throw new RESTError(500, `The StashKu storage engine "${this.engine.name}" returned an invalid response.`);
}
this.log.debug(`[${reqID}] Returning "${request.method}" response with total value of ${response ? response.total : '(null)'}.`);
return response;
}
/**
* This method safely cleans up resources and should be called after the use of the StashKu instance is complete,
* such as just before application exit.
*/
async destroy() {
if (this.engine && this.engine.destroy) {
await this.engine.destroy();
}
}
/**
* @template T
* @typedef {new(...args: Array) | new(...args: Array) => T} Constructor
**/
/**
* Attaches a model to StashKu operations through a new returned (proxy) StashKu instance. The model is attached
* only to the returned proxying instance. This allows the use of the model within the request callback methods
* as the second argument (`model`), and the resulting `Response` data to be instances of the model.
*
* @example
* let sk = new StashKu();
* ...
* //the model object will be exposed as the "m" argument in the callback.
* await sk.model(PersonModel).get((r, m) => r...);
* await sk.model(PersonModel).post((r, m) => r...);
* await sk.model(PersonModel).put((r, m) => r...);
* await sk.model(PersonModel).patch((r, m) => r...);
* await sk.model(PersonModel).delete((r, m) => r...);
*
* @throws Error if the `modelType` argument missing or falsey.
* @throws Error if the `modelType` argument is not a class or constructor function.
* @template MT
* @param {MT} modelType - The model "class" or constructor function.
* @returns {StashKu.<MT, InstanceType.<MT>>}
*/
model(modelType) {
let parent = this;
if (!modelType) {
throw new RESTError(500, 'The "modelType" argument is required.');
}
if (!modelType.constructor || !modelType.prototype) {
throw new RESTError(500, 'The "modelType" argument is invalid and must be a class or constructor function.');
}
if (this.config && this.config.proxy && this.config.proxy.parent) { //prevent parent daisy-chaining
parent = this.config.proxy.parent;
}
return new StashKu({
proxy: {
parent,
model: modelType
}
});
}
/**
* @callback GetRequestCallback
* @param {GetRequest} request
* @param {M} [model]
*/
/**
* Run a GET `request` through StashKu using the configured storage engine.
* This operation returns objects from storage that match the GET `request` criteria.
*
* @example
* let sk = new StashKu();
* ...
* await sk.get(r => r
* .properties('FirstName', 'LastName')
* .from('Contacts')
* .where(f => f.and('FirstName', f.OP.STARTSWITH, 'Rob'))
* .sort('LastName')
* );
*
* @throws Error if the storage engine fails to return a response.
* @throws Error if the storage engine returns an invalid response object.
* @param {GetRequest | GetRequestCallback} [request] - The GET request to send to the storage engine.
* @returns {Promise.<Response.<I>>} Returns the data objects from storage matching request criteria.
*/
async get(request) {
return await this._handle(request ?? new GetRequest(), GetRequest);
}
/**
* @callback PostRequestCallback
* @param {PostRequest} request
* @param {M} [model]
*/
/**
* Run a POST `request` to create objects in a target resource using the StashKu configured storage engine.
* This operation returns an array of the objects that were created.
*
* @example
* let sk = new StashKu();
* ...
* let person1 = { FirstName: 'Robert', LastName: 'Yolo' };
* let person2 = { FirstName: 'Suzy', LastName: 'CraftyMine' };
* await sk.post(r => r
* .objects(person1, person2)
* .to('Contacts')
* );
*
* @throws Error if the storage engine fails to return a response.
* @throws Error if the storage engine returns an invalid response object.
* @param {PostRequest | PostRequestCallback} request - The POST request to send to the storage engine.
* @returns {Promise.<Response.<I>>} Returns the data objects from storage that were created with the request criteria.
*/
async post(request) {
return await this._handle(request, PostRequest);
}
/**
* @callback PutRequestCallback
* @param {PutRequest} request
* @param {M} [model]
*/
/**
* Run a PUT `request` to update objects in the target resource using the StashKu configured storage engine.
* This operation will respond with the objects updated.
*
* @example
* let sk = new StashKu();
* ...
* let person1 = { ID: 1, FirstName: 'Robert', LastName: 'Yolo' };
* let person2 = { ID: 2, FirstName: 'Suzy', LastName: 'CraftyMine' };
* await sk.put(r => r
* .objects(person1, person2)
* .pk('ID')
* .to('Contacts')
* );
*
* @throws Error if the storage engine fails to return a response.
* @throws Error if the storage engine returns an invalid response object.
* @param {PutRequest | PutRequestCallback} request - The PUT request to send to the storage engine.
* @returns {Promise.<Response.<I>>} Returns the data objects from storage that were updated with the request criteria. This
* *__may not__* exactly match the objects requested to be updated, as some may have been deleted from storage or
* some may not match the key criteria.
*/
async put(request) {
return await this._handle(request, PutRequest);
}
/**
* @callback PatchRequestCallback
* @param {PatchRequest} request
* @param {M} [model]
*/
/**
* Run a PATCH `request` to update any matching objects with the specified template properties and values.
* This operation will respond with the objects updated.
*
* @example
* let sk = new StashKu();
* ...
* await sk.patch(r => r
* .template({
* walking: true,
* date_walked: new Date()
* })
* .to('Runners')
* .where(f => f.and('Gender', f.OP.EQUALS, 'Female'))
* );
* @throws Error if the storage engine fails to return a response.
* @throws Error if the storage engine returns an invalid response object.
* @param {PatchRequest | PatchRequestCallback} request - The PATCH request to send to the storage engine.
* @returns {Promise.<Response.<I>>} Returns a response with the total number of the objects affected in storage. No data
* objects are typically returned with this request.
*/
async patch(request) {
return await this._handle(request, PatchRequest);
}
/**
* @callback DeleteRequestCallback
* @param {DeleteRequest} request
* @param {M} [model]
*/
/**
* Run a DELETE `request` to delete any matching objects with the specified template properties and values.
* This operation will respond with the objects deleted.
*
* @example
* let sk = new StashKu();
* ...
* await sk.patch(r => r
* .from('Runners')
* .where(f => f.and('Gender', f.OP.EQUALS, 'Female'))
* );
* @throws Error if the "request" argument is not a callback function or `DeleteRequest` instance.
* @param {DeleteRequest | DeleteRequestCallback} request - The DELETE request to send to the storage engine.
* @returns {Promise.<Response.<I>>} Returns the data objects from storage that were deleted with the request criteria.
*/
async delete(request) {
return await this._handle(request, DeleteRequest);
}
/**
* @callback OptionsRequestCallback
* @param {OptionsRequest} request
* @param {M} [model]
*/
/**
* Run an OPTIONS `request` which returns a dynamically constructed model type which defines how StashKu can
* interact with the target (`from`) resource.
* @example
* let sk = new StashKu();
* ...
* await sk.options(r => r
* .from('Runners')
* );
* @throws Error if the "request" argument is not a callback function or `OptionsRequest` instance.
* @param {OptionsRequest | OptionsRequestCallback} request - The OPTIONS request to send to the storage engine.
* @returns {Promise.<Response.<I>>} Returns a response with a single data object- the dynamically created model
* configuration.
*/
async options(request) {
return await this._handle(request ?? new OptionsRequest(), OptionsRequest);
}
/**
* Instructs StashKu to transform a HTTP request into a StashKu request and run it.
*
* @throws Error when StashKu failed to transform the HTTP request into a valid StashKu request.
* @throws Error when used on an unsupported platform (browser).
* @param {Request} httpRequest - The HTTP request to be transformed and run.
* @param {Modeling.AnyModelType} [modelType] - The model type StashKu can use to discover the proper resource
* of a request. If this is specified, the resource will *always* be derived from the model's appropriate
* resource value.
* If your StashKu chain includes a model, that model will be used when this argument is not specified.
* @returns {Promise.<Response.<I>>} Returns the data objects from storage matching HTTP request criteria.
*/
async http(httpRequest, modelType) {
if (IS_BROWSER === false) {
//support someone handing off a pre-constructed request, just forward to the proper handler.
if (httpRequest instanceof GetRequest) {
return this.get(httpRequest);
}
if (httpRequest instanceof PostRequest) {
return this.post(httpRequest);
}
if (httpRequest instanceof PutRequest) {
return this.put(httpRequest);
}
if (httpRequest instanceof PatchRequest) {
return this.patch(httpRequest);
}
if (httpRequest instanceof DeleteRequest) {
return this.delete(httpRequest);
}
if (httpRequest instanceof OptionsRequest) {
return this.options(httpRequest);
}
if (httpRequest.url && httpRequest.httpVersion) {
let reqModel = modelType || this.config?.proxy?.model;
let request = await StashKu.requestFromObject(httpRequest, reqModel);
if (request) {
switch (request.method) {
case 'get': return this._handle(request, GetRequest);
case 'post': return this._handle(request, PostRequest);
case 'put': return this._handle(request, PutRequest);
case 'patch': return this._handle(request, PatchRequest);
case 'delete': return this._handle(request, DeleteRequest);
case 'options': return this._handle(request, OptionsRequest);
}
}
}
throw new RESTError(400, 'Failed to process request.');
}
throw new RESTError(500, 'The "requestFromFile" function is not supported on this platform.');
}
/**
* @callback ModelNameResolveCallback
* @param {String} name - The model name defined.
* @returns {*} Returns a model type constructor/class associated with the model name.
*/
/**
* Reads a request defintion from file and returns an instance of that request.
*
* @throws Error when used on an unsupported platform (browser).
* @param {fs.PathOrFileDescriptor} jsonFile - The file path to the JSON formatted file defining a single StashKu request.
* @param {{encoding: String, flag: String} | String | fs.BufferEncodingOption} [fsOptions] - File encoding options.
* @param {ModelNameResolveCallback} [modelNameResolver] - Callback function that resolves a model name into a model type (constructor/class).
* @returns {Promise.<DeleteRequest | GetRequest | PatchRequest | PostRequest | PutRequest | OptionsRequest>}
*/
static async requestFromFile(jsonFile, fsOptions, modelNameResolver) {
if (IS_BROWSER === false) {
//set import package as variable, so compilers like esbuild ignore the import
let pkg = 'fs/promises';
let f = await (await import(/* webpackIgnore: true */pkg)).default.readFile(jsonFile, fsOptions);
let obj = JSON.parse(f);
return StashKu.requestFromObject(obj, modelNameResolver);
}
throw new Error(500, 'The "requestFromFile" function is not supported on this platform.');
}
/**
* Attempts to parse (convert) a StashKu request-like object (including http requests) into a `DeleteRequest`.
* If the request-like object does not appear to be parsable into the request type, a value of `null` is
* returned.
* @param {*} reqObj - The untyped request object.
* @param {ModelNameResolveCallback | Modeling.AnyModelType} [modelResolver] - Callback function that resolves
* a model name into a model type (constructor/class). Optionally can be a model type.
* @returns {Promise.<DeleteRequest>}
*/
static async parseDelete(reqObj, modelResolver) {
let r = await StashKu.requestFromObject(reqObj, modelResolver);
if (r instanceof DeleteRequest) {
return r;
}
return null;
}
/**
* Attempts to parse (convert) a StashKu request-like object (including http requests) into a `GetRequest`.
* If the request-like object does not appear to be parsable into the request type, a value of `null` is
* returned.
* @param {*} reqObj - The untyped request object.
* @param {ModelNameResolveCallback | Modeling.AnyModelType} [modelResolver] - Callback function that resolves
* a model name into a model type (constructor/class). Optionally can be a model type.
* @returns {Promise.<GetRequest>}
*/
static async parseGet(reqObj, modelResolver) {
let r = await StashKu.requestFromObject(reqObj, modelResolver);
if (r instanceof GetRequest) {
return r;
}
return null;
}
/**
* Attempts to parse (convert) a StashKu request-like object (including http requests) into a `PatchRequest`.
* If the request-like object does not appear to be parsable into the request type, a value of `null` is
* returned.
* @param {*} reqObj - The untyped request object.
* @param {ModelNameResolveCallback | Modeling.AnyModelType} [modelResolver] - Callback function that resolves
* a model name into a model type (constructor/class). Optionally can be a model type.
* @returns {Promise.<PatchRequest>}
*/
static async parsePatch(reqObj, modelResolver) {
let r = await StashKu.requestFromObject(reqObj, modelResolver);
if (r instanceof PatchRequest) {
return r;
}
return null;
}
/**
* Attempts to parse (convert) a StashKu request-like object (including http requests) into a `PostRequest`.
* If the request-like object does not appear to be parsable into the request type, a value of `null` is
* returned.
* @param {*} reqObj - The untyped request object.
* @template MT
* @param {ModelNameResolveCallback | Modeling.AnyModelType | MT} [modelResolver] - Callback function that resolves
* a model name into a model type (constructor/class). Optionally can be a model type.
* @returns {Promise.<PostRequest.<InstanceType.<MT>>>}
*/
static async parsePost(reqObj, modelResolver) {
let r = await StashKu.requestFromObject(reqObj, modelResolver);
if (r instanceof PostRequest) {
return r;
}
return null;
}
/**
* Attempts to parse (convert) a StashKu request-like object (including http requests) into a `PutRequest`.
* If the request-like object does not appear to be parsable into the request type, a value of `null` is
* returned.
* @param {*} reqObj - The untyped request object.
* @template MT
* @param {ModelNameResolveCallback | Modeling.AnyModelType | MT} [modelResolver] - Callback function that resolves
* a model name into a model type (constructor/class). Optionally can be a model type.
* @returns {Promise.<PutRequest.<InstanceType.<MT>>>}
*/
static async parsePut(reqObj, modelResolver) {
let r = await StashKu.requestFromObject(reqObj, modelResolver);
if (r instanceof PutRequest) {
return r;
}
return null;
}
/**
* Attempts to parse (convert) a StashKu request-like object (including http requests) into a `OptionsRequest`.
* If the request-like object does not appear to be parsable into the request type, a value of `null` is
* returned.
* @param {*} reqObj - The untyped request object.
* @param {ModelNameResolveCallback | Modeling.AnyModelType} [modelResolver] - Callback function that resolves
* a model name into a model type (constructor/class). Optionally can be a model type.
* @returns {Promise.<OptionsRequest>}
*/
static async parseOptions(reqObj, modelResolver) {
let r = await StashKu.requestFromObject(reqObj, modelResolver);
if (r instanceof OptionsRequest) {
return r;
}
return null;
}
/**
* @deprecated Please switch to `StashKu.parse***` functions.
* This function will be removed in a later versions.
* @description
* Converts a StashKu request-like object (including http requests) into an instance of the appropriate StashKu
* request. This can be used in conjunction with `JSON.stringify(...)` and subsequently a `JSON.parse(...)`
* of a request.
*
* If the object is `null` or `undefined`, a `null` value is returned.
* @throws Error if the object is missing a method property.
* @throws Error if the method property value is invalid.
* @param {*} reqObj - The untyped request object.
* @param {ModelNameResolveCallback | Modeling.AnyModelType} [modelNameResolver] - Callback function that resolves
* a model name into a model type (constructor/class). Optionally can be a model type.
* @returns {Promise.<DeleteRequest | GetRequest | PatchRequest | PostRequest | PutRequest | OptionsRequest>}
*/
static async requestFromObject(reqObj, modelNameResolver) {
if (reqObj) {
//get a model type, if available
let mt = null;
if (typeof modelNameResolver === 'function' && reqObj.model && typeof reqObj.model === 'string') {
mt = modelNameResolver(reqObj.model);
} else if (ModelUtility.isValidType(modelNameResolver)) {
mt = modelNameResolver;
}
//handle http.IncomingMessageg
if (IS_BROWSER === false && reqObj.url && reqObj.httpVersion) {
if (!HttpRequestLoader) {
//set import package as variable, so compilers like esbuild ignore the import
let pkg = './node/http-request-loader.js';
HttpRequestLoader = (await import(/* webpackIgnore: true */pkg)).default;
}
return await HttpRequestLoader(reqObj, mt);
}
if (!reqObj.method || /^delete|get|patch|post|put|options$/i.test(reqObj.method) === false) {
throw new Error('The method property value of the object is missing or invalid. Expected a valid request method.');
}
//construct
switch (reqObj.method.toLowerCase()) {
case 'delete': return new DeleteRequest()
.headers(reqObj.headers)
.from(reqObj.from ?? null)
.all(reqObj.all ?? false)
.count(reqObj.count ?? false)
.where(Filter.fromObject(reqObj.where));
case 'get': return new GetRequest()
.headers(reqObj.headers)
.from(reqObj.from ?? null)
.properties(...reqObj.properties)
.distinct(reqObj.distinct ?? false)
.count(reqObj.count ?? false)
.skip(reqObj.skip ?? null)
.take(reqObj.take ?? null)
.sort(...reqObj.sorts)
.where(Filter.fromObject(reqObj.where));
case 'options': return new OptionsRequest()
.from(reqObj.from ?? null)
.headers(reqObj.headers);
case 'patch': return new PatchRequest()
.headers(reqObj.headers)
.to(reqObj.to ?? null)
.template(reqObj.template ?? null)
.all(reqObj.all ?? false)
.count(reqObj.count ?? false)
.where(Filter.fromObject(reqObj.where));
case 'post': return new PostRequest()
.headers(reqObj.headers)
.to(reqObj.to ?? null)
.count(reqObj.count ?? false)
.objects(reqObj.objects ?? null);
case 'put': return new PutRequest()
.headers(reqObj.headers)
.to(reqObj.to ?? null)
.count(reqObj.count ?? false)
.pk(...reqObj.pk)
.objects(reqObj.objects ?? null);
}
}
return null;
}
}
export {
StashKu as default,
GetRequest,
PostRequest,
PutRequest,
PatchRequest,
DeleteRequest,
OptionsRequest,
BaseEngine,
Response,
RESTError,
Filter,
Sort,
ModelGenerator,
ModelUtility,
StringUtility
};
source