@softvisio/core
Version:
Softisio core
374 lines (316 loc) • 10.6 kB
JavaScript
import ejs from "#lib/ejs";
import * as utils from "#lib/utils";
const TYPES = {
"null": new Set( [ "=", "!=" ] ),
"boolean": new Set( [ "=", "!=", "in", "not in" ] ),
"string": new Set( [
//
"=",
"!=",
"like",
"not like",
"ilike",
"not ilike",
"includes",
"not includes",
"includes case insensitive",
"not includes case insensitive",
"starts with",
"not starts with",
"starts with case insensitive",
"not starts with case insensitive",
"ends with",
"not ends with",
"ends with case insensitive",
"not ends with case insensitive",
"glob",
"glob case insensitive",
"in",
"not in",
"~",
"~*",
"!~",
"!~*",
] ),
"number": new Set( [ "=", "!=", "<", ">", "<=", ">=", "in", "not in" ] ),
"integer": new Set( [ "=", "!=", "<", ">", "<=", ">=", "in", "not in" ] ),
};
const ARRAY_OPERATORS = new Set( [
//
"in",
"not in",
] );
const GLOB_OPERATORS = new Set( [
//
"glob",
"glob case insensitive",
] );
const ALL_OPERATORS = [
...new Set( [
//
...TYPES.null,
...TYPES.boolean,
...TYPES.string,
...TYPES.number,
...TYPES.integer,
] ),
];
function generateMacro ( type, format, operators, _const, _enum, aclResolver ) {
if ( !Array.isArray( operators ) ) operators = [ operators ];
var scalarOperators = [],
arrayOperators = [],
globOperators = [];
// validate operators
for ( const operator of operators ) {
if ( !TYPES[ type ].has( operator ) ) throw `Operator "${ operator }" is invalid for type "${ type }"`;
// group operators
if ( ARRAY_OPERATORS.has( operator ) ) {
arrayOperators.push( operator );
}
else if ( GLOB_OPERATORS.has( operator ) ) {
globOperators.push( operator );
}
else {
scalarOperators.push( operator );
}
}
const macros = [];
// scalar
if ( scalarOperators.length ) {
macros.push( {
"type": "array",
"items": [
//
scalarOperators.length === 1
? { "const": scalarOperators[ 0 ] }
: { "enum": scalarOperators },
generateType( type, format, _const, _enum, aclResolver ),
],
"minItems": 2,
"maxItems": 2,
} );
}
// array
if ( arrayOperators.length ) {
macros.push( {
"type": "array",
"items": [
arrayOperators.length === 1
? { "const": arrayOperators[ 0 ] }
: { "enum": arrayOperators },
{
"type": "array",
"items": generateType( type, format, _const, _enum, aclResolver ),
"minItems": 1,
},
],
"minItems": 2,
"maxItems": 2,
} );
}
// glob
if ( globOperators.length ) {
macros.push( {
"type": "array",
"items": [
// glob operator
globOperators.length === 1
? { "const": globOperators[ 0 ] }
: { "enum": globOperators },
// glob patterns
{
"anyOf": [
generateType( type, format, _const, _enum, aclResolver ),
{
"type": "array",
"items": generateType( type, format, _const, _enum, aclResolver ),
"minItems": 1,
},
],
},
// glob options
{
"type": "object",
"properties": {
"prefix": {
"type": "string",
},
},
"additionalProperties": false,
"required": [ "prefix" ],
},
],
"minItems": 2,
"maxItems": 3,
} );
}
if ( macros.length === 1 ) {
return macros[ 0 ];
}
else {
return { "anyOf": macros };
}
}
function generateType ( type, format, _const, _enum, aclResolver ) {
const macro = {};
if ( _const !== undefined ) {
macro.const = _const;
}
else if ( _enum ) {
macro.enum = _enum;
}
else {
macro.type = type;
if ( format ) macro.format = format;
}
if ( aclResolver ) macro.aclResolver = aclResolver;
return macro;
}
const keyword = {
"keyword": "read",
"metaSchema": {
"type": "object",
"properties": {
// fields
"fields": {
"type": "object",
"propertyNames": { "type": "string", "format": "snake-case" },
"additionalProperties": {
"type": "object",
"properties": {
"description": { "type": "string" },
"type": { "enum": [ "null", "boolean", "string", "number", "integer" ] },
"format": { "type": "string" },
"operator": {
"anyOf": [
{
"enum": ALL_OPERATORS,
},
{
"type": "array",
"items": {
"enum": ALL_OPERATORS,
},
"minItems": 1,
"uniqueItems": true,
},
],
},
"const": {},
"enum": { "type": "array", "minItems": 1, "uniqueItems": true },
"required": { "type": "boolean" },
"sortable": { "type": "boolean" },
"aclResolver": { "type": "string" },
},
"additionalProperties": false,
},
"errorMessage": "API read method fields must be in the snake_case",
},
// default order by
"order_by": {
"type": "array",
"items": {
"type": "array",
"items": [
{
"type": "string",
"format": "snake-case",
},
{ "enum": [ "asc", "desc" ] },
],
"minItems": 1,
"maxItems": 2,
},
"minItems": 1,
},
// offset
"offset": { "const": false },
// limit
"limit": {
"anyOf": [
{ "const": false },
{
"type": "object",
"properties": {
"defaultLimit": { "type": "integer", "minimum": 1 },
"maxLimit": { "type": "integer", "minimum": 1 },
"maxResults": { "type": "integer", "minimum": 1 },
},
"additionalProperties": false,
},
],
},
},
},
"macro": ( schema, parentSchema, it ) => {
const macro = {
"type": "object",
"properties": {},
"additionalProperties": false,
};
// offset
if ( schema.offset !== false ) {
macro.properties.offset = { "type": "integer", "minimum": 0 };
}
// limit
if ( schema.limit !== false ) {
const limit = { "type": "integer", "minimum": 1 };
if ( schema.limit?.maxLimit ) limit.maximum = schema.limit.maxLimit;
macro.properties.limit = limit;
}
const required = [];
const sortable = [];
for ( const fieldName in schema.fields ) {
const field = schema.fields[ fieldName ];
if ( field.required ) required.push( fieldName );
if ( field.sortable ) sortable.push( fieldName );
if ( field.operator ) {
macro.properties.where ||= { "type": "object", "additionalProperties": false, "properties": {} };
macro.properties.where.properties[ fieldName ] = generateMacro( field.type, field.format, field.operator, field.const, field.enum, field[ "aclResolver" ] );
}
}
// required
if ( required.length ) macro.properties.where.required = required;
// order by
if ( sortable.length ) {
macro.properties[ "order_by" ] = {
"type": "array",
"items": {
"type": "array",
"items": [
{
"enum": sortable,
},
{
"enum": [ "asc", "desc" ],
},
],
"minItems": 1,
"maxItems": 2,
},
"minItems": 1,
};
}
if ( required.length ) {
macro.required = [ "where" ];
}
return macro;
},
};
class readKeyword {
get keyword () {
return keyword;
}
async getDescription ( param ) {
const template = utils.resolve( "#resources/templates/read-keyword.md.ejs", import.meta.url );
return ejs.renderFile( template, {
"schema": param.schema.read,
"fields": param.schema.read.fields,
"where": Object.keys( param.schema.read.fields || {} ).filter( field => param.schema.read.fields[ field ].operator ),
"order_by": Object.keys( param.schema.read.fields || {} ).filter( field => param.schema.read.fields[ field ].sortable ),
"offset": param.schema.read.offset,
"limit": param.schema.read.limit,
} );
}
}
export default new readKeyword();