UNPKG

@goatlab/fluent

Version:

Readable query Interface & API generator for TS and Node

402 lines 12 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.Event = exports.Entity = exports.ValueObject = exports.Model = exports.ModelDefinition = void 0; exports.rejectNavigationalPropertiesInData = rejectNavigationalPropertiesInData; const relation_types_1 = require("./relation.types"); /** * Definition for a model */ class ModelDefinition { name; properties; settings; relations; constructor(nameOrDef) { const definition = typeof nameOrDef === 'string' ? { name: nameOrDef } : nameOrDef; const { name, properties, settings, relations } = definition; this.name = name; this.properties = {}; if (properties) { for (const p in properties) { const prop = properties[p]; if (prop !== undefined) { this.addProperty(p, prop); } } } this.settings = settings ?? new Map(); this.relations = relations ?? {}; } /** * Add a property * @param name - Property definition or name (string) * @param definitionOrType - Definition or property type */ addProperty(name, definitionOrType) { const definition = definitionOrType.type ? definitionOrType : { type: definitionOrType }; if (definition.id === true && definition.generated === true && definition.type !== undefined && definition.useDefaultIdType === undefined) { definition.useDefaultIdType = false; } this.properties[name] = definition; return this; } /** * Add a setting * @param name - Setting name * @param value - Setting value */ addSetting(name, value) { this.settings[name] = value; return this; } /** * Define a new relation. * @param definition - The definition of the new relation. */ addRelation(definition) { this.relations[definition.name] = definition; return this; } /** * Define a new belongsTo relation. * @param name - The name of the belongsTo relation. * @param definition - The definition of the belongsTo relation. */ belongsTo(name, definition) { const meta = { ...definition, name, type: relation_types_1.RelationType.belongsTo, targetsMany: false, }; return this.addRelation(meta); } /** * Define a new hasOne relation. * @param name - The name of the hasOne relation. * @param definition - The definition of the hasOne relation. */ hasOne(name, definition) { const meta = { ...definition, name, type: relation_types_1.RelationType.hasOne, targetsMany: false, }; return this.addRelation(meta); } /** * Define a new hasMany relation. * @param name - The name of the hasMany relation. * @param definition - The definition of the hasMany relation. */ hasMany(name, definition) { const meta = { ...definition, name, type: relation_types_1.RelationType.hasMany, targetsMany: true, }; return this.addRelation(meta); } /** * Get an array of names of ID properties, which are specified in * the model settings or properties with `id` attribute. * * @example * ```ts * { * settings: { * id: ['id'] * } * properties: { * id: { * type: 'string', * id: true * } * } * } * ``` */ idProperties() { if (typeof this.settings.id === 'string') { return [this.settings.id]; } if (Array.isArray(this.settings.id)) { return this.settings.id; } const idProps = Object.keys(this.properties).filter(prop => this.properties[prop]?.id); return idProps; } } exports.ModelDefinition = ModelDefinition; function asJSON(value) { if (value == null) { return value; } if (typeof value.toJSON === 'function') { return value.toJSON(); } // Handle arrays if (Array.isArray(value)) { return value.map(item => asJSON(item)); } return value; } /** * Convert a value to a plain object as DTO. * * - The prototype of the value in primitive types are preserved, * like `Date`, `ObjectId`. * - If the value is an instance of custom model, call `toObject` to convert. * - If the value is an array, convert each element recursively. * * @param value the value to convert * @param options the options */ function asObject(value, options) { if (value == null) { return value; } if (typeof value.toObject === 'function') { return value.toObject(options); } if (Array.isArray(value)) { return value.map(item => asObject(item, options)); } return value; } /** * Base class for models */ // tslint:disable-next-line: max-classes-per-file class Model { static get modelName() { return Model.definition?.name || Model.name; } static definition; /** * Serialize into a plain JSON object */ toJSON() { const def = this.constructor.definition; if (def == null || def.settings.strict === false) { return this.toObject({ ignoreUnknownProperties: false }); } const copyPropertyAsJson = (key) => { const val = asJSON(this[key]); if (val !== undefined) { json[key] = val; } }; const json = {}; const hiddenProperties = def.settings.hiddenProperties || []; for (const p in def.properties) { if (p in this && !hiddenProperties.includes(p)) { copyPropertyAsJson(p); } } for (const r in def.relations) { const rel = def.relations[r]; if (!rel) { continue; } const relName = rel.name; if (relName in this) { copyPropertyAsJson(relName); } } return json; } /** * Convert to a plain object as DTO * * If `ignoreUnknownProperty` is set to false, convert all properties in the * model instance, otherwise only convert the ones defined in the model * definitions. * * See function `asObject` for each property's conversion rules. */ toObject(options) { const def = this.constructor.definition; const obj = {}; if (options?.ignoreUnknownProperties === false) { const hiddenProperties = def?.settings.hiddenProperties || []; for (const p in this) { if (!hiddenProperties.includes(p)) { const val = this[p]; obj[p] = asObject(val, options); } } return obj; } if (def?.relations) { // tslint:disable-next-line: forin for (const r in def.relations) { const rel = def.relations[r]; if (!rel) { continue; } const relName = rel.name; if (relName in this) { obj[relName] = asObject(this[relName], { ...options, ignoreUnknownProperties: false, }); } } } const props = def.properties; const keys = Object.keys(props); // tslint:disable-next-line: forin for (const i in keys) { const propertyName = keys[i]; if (!propertyName) { continue; } const val = this[propertyName]; if (val === undefined) { continue; } obj[propertyName] = asObject(val, options); } return obj; } constructor(data) { Object.assign(this, data); } } exports.Model = Model; /** * Base class for value objects - An object that contains attributes but has no * conceptual identity. They should be treated as immutable. */ class ValueObject extends Model { } exports.ValueObject = ValueObject; /** * Base class for entities which have unique ids */ class Entity extends Model { /** * Get the names of identity properties (primary keys). */ static getIdProperties() { return Entity.definition.idProperties(); } /** * Get the identity value for a given entity instance or entity data object. * * @param entityOrData - The data object for which to determine the identity * value. */ static getIdOf(entityOrData) { if (typeof entityOrData.getId === 'function') { return entityOrData.getId(); } const idName = Entity.getIdProperties()[0]; if (!idName) { return undefined; } return entityOrData[idName]; } /** * Get the identity value. If the identity is a composite key, returns * an object. */ getId() { const { definition } = this.constructor; const idProps = definition.idProperties(); if (idProps.length === 1) { const firstId = idProps[0]; if (!firstId) { return undefined; } return this[firstId]; } if (!idProps.length) { throw new Error(`Invalid Entity ${this.constructor.name}:` + 'missing primary key (id) property'); } return this.getIdObject(); } /** * Get the identity as an object, such as `{id: 1}` or * `{schoolId: 1, studentId: 2}` */ getIdObject() { const { definition } = this.constructor; const idProps = definition.idProperties(); const idObj = {}; for (const idProp of idProps) { idObj[idProp] = this[idProp]; } return idObj; } /** * Build the where object for the given id * @param id - The id value */ static buildWhereForId(id) { const where = {}; const idProps = Entity.definition.idProperties(); if (idProps.length === 1) { const firstId = idProps[0]; if (firstId) { where[firstId] = id; } } else { for (const idProp of idProps) { where[idProp] = id[idProp]; } } return where; } } exports.Entity = Entity; /** * Domain events */ class Event { source; type; } exports.Event = Event; /** * Check model data for navigational properties linking to related models. * Throw a descriptive error if any such property is found. * * @param modelClass Model constructor, e.g. `Product`. * @param entityData Model instance or a plain-data object, * e.g. `{name: 'pen'}`. */ function rejectNavigationalPropertiesInData(modelClass, data) { const def = modelClass.definition; const props = def.properties; for (const r in def.relations) { const rel = def.relations[r]; if (!rel) { continue; } const relName = rel.name; if (!(relName in data)) { continue; } let msg = 'Navigational properties are not allowed in model data ' + `(model "${modelClass.modelName}" property "${relName}"), ` + 'please remove it.'; if (relName in props) { msg += ' The error might be invoked by belongsTo relations, please make' + ' sure the relation name is not the same as the property name.'; } throw new Error(msg); } } //# sourceMappingURL=model.js.map