UNPKG

acebase-core

Version:

Shared AceBase core components, no need to install manually

363 lines 15.6 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.SchemaDefinition = void 0; // parses a typestring, creates checker functions function parse(definition) { // tokenize let pos = 0; function consumeSpaces() { let c; while (c = definition[pos], [' ', '\r', '\n', '\t'].includes(c)) { pos++; } } function consumeCharacter(c) { if (definition[pos] !== c) { throw new Error(`Unexpected character at position ${pos}. Expected: '${c}', found '${definition[pos]}'`); } pos++; } function readProperty() { consumeSpaces(); const prop = { name: '', optional: false, wildcard: false }; let c; while (c = definition[pos], c === '_' || c === '$' || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (prop.name.length > 0 && c >= '0' && c <= '9') || (prop.name.length === 0 && c === '*')) { prop.name += c; pos++; } if (prop.name.length === 0) { throw new Error(`Property name expected at position ${pos}, found: ${definition.slice(pos, pos + 10)}..`); } if (definition[pos] === '?') { prop.optional = true; pos++; } if (prop.name === '*' || prop.name[0] === '$') { prop.optional = true; prop.wildcard = true; } consumeSpaces(); consumeCharacter(':'); return prop; } function readType() { consumeSpaces(); let type = { typeOf: 'any' }, c; // try reading simple type first: (string,number,boolean,Date etc) let name = ''; while (c = definition[pos], (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')) { name += c; pos++; } if (name.length === 0) { if (definition[pos] === '*') { // any value consumeCharacter('*'); type.typeOf = 'any'; } else if (['\'', '"', '`'].includes(definition[pos])) { // Read string value type.typeOf = 'string'; type.value = ''; const quote = definition[pos]; consumeCharacter(quote); while (c = definition[pos], c && c !== quote) { type.value += c; pos++; } consumeCharacter(quote); } else if (definition[pos] >= '0' && definition[pos] <= '9') { // read numeric value type.typeOf = 'number'; let nr = ''; while (c = definition[pos], c === '.' || c === 'n' || (c >= '0' && c <= '9')) { nr += c; pos++; } if (nr.endsWith('n')) { type.value = BigInt(nr); } else if (nr.includes('.')) { type.value = parseFloat(nr); } else { type.value = parseInt(nr); } } else if (definition[pos] === '{') { // Read object (interface) definition consumeCharacter('{'); type.typeOf = 'object'; type.instanceOf = Object; // Read children: type.children = []; while (true) { const prop = readProperty(); const types = readTypes(); type.children.push({ name: prop.name, optional: prop.optional, wildcard: prop.wildcard, types }); consumeSpaces(); if (definition[pos] === ';' || definition[pos] === ',') { consumeCharacter(definition[pos]); consumeSpaces(); } if (definition[pos] === '}') { break; } } consumeCharacter('}'); } else if (definition[pos] === '/') { // Read regular expression definition consumeCharacter('/'); let pattern = '', flags = ''; while (c = definition[pos], c !== '/' || pattern.endsWith('\\')) { pattern += c; pos++; } consumeCharacter('/'); while (c = definition[pos], ['g', 'i', 'm', 's', 'u', 'y', 'd'].includes(c)) { flags += c; pos++; } type.typeOf = 'string'; type.matches = new RegExp(pattern, flags); } else { throw new Error(`Expected a type definition at position ${pos}, found character '${definition[pos]}'`); } } else if (['string', 'number', 'boolean', 'bigint', 'undefined', 'String', 'Number', 'Boolean', 'BigInt'].includes(name)) { type.typeOf = name.toLowerCase(); } else if (name === 'Object' || name === 'object') { type.typeOf = 'object'; type.instanceOf = Object; } else if (name === 'Date') { type.typeOf = 'object'; type.instanceOf = Date; } else if (name === 'Binary' || name === 'binary') { type.typeOf = 'object'; type.instanceOf = ArrayBuffer; } else if (name === 'any') { type.typeOf = 'any'; } else if (name === 'null') { // This is ignored, null values are not stored in the db (null indicates deletion) type.typeOf = 'object'; type.value = null; } else if (name === 'Array') { // Read generic Array defintion consumeCharacter('<'); type.typeOf = 'object'; type.instanceOf = Array; //name; type.genericTypes = readTypes(); consumeCharacter('>'); } else if (['true', 'false'].includes(name)) { type.typeOf = 'boolean'; type.value = name === 'true'; } else { throw new Error(`Unknown type at position ${pos}: "${type}"`); } // Check if it's an Array of given type (eg: string[] or string[][]) // Also converts to generics, string[] becomes Array<string>, string[][] becomes Array<Array<string>> consumeSpaces(); while (definition[pos] === '[') { consumeCharacter('['); consumeCharacter(']'); type = { typeOf: 'object', instanceOf: Array, genericTypes: [type] }; } return type; } function readTypes() { consumeSpaces(); const types = [readType()]; while (definition[pos] === '|') { consumeCharacter('|'); types.push(readType()); consumeSpaces(); } return types; } return readType(); } function checkObject(path, properties, obj, partial) { // Are there any properties that should not be in there? const invalidProperties = properties.find(prop => prop.name === '*' || prop.name[0] === '$') // Only if no wildcard properties are allowed ? [] : Object.keys(obj).filter(key => ![null, undefined].includes(obj[key]) // Ignore null or undefined values && !properties.find(prop => prop.name === key)); if (invalidProperties.length > 0) { return { ok: false, reason: `Object at path "${path}" cannot have propert${invalidProperties.length === 1 ? 'y' : 'ies'} ${invalidProperties.map(p => `"${p}"`).join(', ')}` }; } // Loop through properties that should be present function checkProperty(property) { const hasValue = ![null, undefined].includes(obj[property.name]); if (!property.optional && (partial ? obj[property.name] === null : !hasValue)) { return { ok: false, reason: `Property at path "${path}/${property.name}" is not optional` }; } if (hasValue && property.types.length === 1) { return checkType(`${path}/${property.name}`, property.types[0], obj[property.name], false); } if (hasValue && !property.types.some(type => checkType(`${path}/${property.name}`, type, obj[property.name], false).ok)) { return { ok: false, reason: `Property at path "${path}/${property.name}" does not match any of ${property.types.length} allowed types` }; } return { ok: true }; } const namedProperties = properties.filter(prop => !prop.wildcard); const failedProperty = namedProperties.find(prop => !checkProperty(prop).ok); if (failedProperty) { const reason = checkProperty(failedProperty).reason; return { ok: false, reason }; } const wildcardProperty = properties.find(prop => prop.wildcard); if (!wildcardProperty) { return { ok: true }; } const wildcardChildKeys = Object.keys(obj).filter(key => !namedProperties.find(prop => prop.name === key)); let result = { ok: true }; for (let i = 0; i < wildcardChildKeys.length && result.ok; i++) { const childKey = wildcardChildKeys[i]; result = checkProperty({ name: childKey, types: wildcardProperty.types, optional: true, wildcard: true }); } return result; } function checkType(path, type, value, partial, trailKeys) { const ok = { ok: true }; if (type.typeOf === 'any') { return ok; } if (trailKeys instanceof Array && trailKeys.length > 0) { // The value to check resides in a descendant path of given type definition. // Recursivly check child type definitions to find a match if (type.typeOf !== 'object') { return { ok: false, reason: `path "${path}" must be typeof ${type.typeOf}` }; // given value resides in a child path, but parent is not allowed be an object. } if (!type.children) { return ok; } const childKey = trailKeys[0]; let property = type.children.find(prop => prop.name === childKey); if (!property) { property = type.children.find(prop => prop.name === '*' || prop.name[0] === '$'); } if (!property) { return { ok: false, reason: `Object at path "${path}" cannot have property "${childKey}"` }; } if (property.optional && value === null && trailKeys.length === 1) { return ok; } let result; property.types.some(type => { const childPath = typeof childKey === 'number' ? `${path}[${childKey}]` : `${path}/${childKey}`; result = checkType(childPath, type, value, partial, trailKeys.slice(1)); return result.ok; }); return result; } if (value === null) { return ok; } if (type.instanceOf === Object && (typeof value !== 'object' || value instanceof Array || value instanceof Date)) { return { ok: false, reason: `path "${path}" must be an object collection` }; } if (type.instanceOf && (typeof value !== 'object' || value.constructor !== type.instanceOf)) { // !(value instanceof type.instanceOf) // value.constructor.name !== type.instanceOf return { ok: false, reason: `path "${path}" must be an instance of ${type.instanceOf.name}` }; } if ('value' in type && value !== type.value) { return { ok: false, reason: `path "${path}" must be value: ${type.value}` }; } if (typeof value !== type.typeOf) { return { ok: false, reason: `path "${path}" must be typeof ${type.typeOf}` }; } if (type.instanceOf === Array && type.genericTypes && !value.every(v => type.genericTypes.some(t => checkType(path, t, v, false).ok))) { return { ok: false, reason: `every array value of path "${path}" must match one of the specified types` }; } if (type.typeOf === 'object' && type.children) { return checkObject(path, type.children, value, partial); } if (type.matches && !type.matches.test(value)) { return { ok: false, reason: `path "${path}" must match regular expression /${type.matches.source}/${type.matches.flags}` }; } return ok; } // eslint-disable-next-line @typescript-eslint/ban-types function getConstructorType(val) { switch (val) { case String: return 'string'; case Number: return 'number'; case Boolean: return 'boolean'; case Date: return 'Date'; case BigInt: return 'bigint'; case Array: throw new Error('Schema error: Array cannot be used without a type. Use string[] or Array<string> instead'); default: throw new Error(`Schema error: unknown type used: ${val.name}`); } } class SchemaDefinition { constructor(definition, handling = { warnOnly: false }) { this.handling = handling; this.source = definition; if (typeof definition === 'object') { // Turn object into typescript definitions // eg: // const example = { // name: String, // born: Date, // instrument: "'guitar'|'piano'", // "address?": { // street: String // } // }; // Resulting ts: "{name:string,born:Date,instrument:'guitar'|'piano',address?:{street:string}}" const toTS = (obj) => { return '{' + Object.keys(obj) .map(key => { let val = obj[key]; if (val === undefined) { val = 'undefined'; } else if (val instanceof RegExp) { val = `/${val.source}/${val.flags}`; } else if (typeof val === 'object') { val = toTS(val); } else if (typeof val === 'function') { val = getConstructorType(val); } else if (!['string', 'number', 'boolean', 'bigint'].includes(typeof val)) { throw new Error(`Type definition for key "${key}" must be a string, number, boolean, bigint, object, regular expression, or one of these classes: String, Number, Boolean, Date, BigInt`); } return `${key}:${val}`; }) .join(',') + '}'; }; this.text = toTS(definition); } else if (typeof definition === 'string') { this.text = definition; } else { throw new Error('Type definiton must be a string or an object'); } this.type = parse(this.text); } check(path, value, partial, trailKeys) { const result = checkType(path, this.type, value, partial, trailKeys); if (!result.ok && this.handling.warnOnly) { // Only issue a warning, allows schema definitions to be added to a production db to monitor if they are accurate before enforcing them. result.warning = `${partial ? 'Partial schema' : 'Schema'} check on path "${path}"${trailKeys ? ` for child "${trailKeys.join('/')}"` : ''} failed: ${result.reason}`; result.ok = true; this.handling.warnCallback(result.warning); } return result; } } exports.SchemaDefinition = SchemaDefinition; //# sourceMappingURL=schema.js.map