UNPKG

@webda/core

Version:

Expose API with Lambda

1,207 lines 34.4 kB
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; import { EventEmitter } from "events"; import util from "util"; import { v4 as uuidv4 } from "uuid"; import { Core, EventEmitterUtils } from "../core.js"; import { WebdaError } from "../errors.js"; import { BinariesImpl, Binary } from "../services/binary.js"; import { WebdaQL } from "../stores/webdaql/query.js"; import { Throttler } from "../utils/throttler.js"; import { ModelLinksArray, ModelLinksSimpleArray, ModelMapLoaderImplementation, createModelLinksMap } from "./relations.js"; /** * Expose the model through API or GraphQL if it exists * The model will be exposed using its class name + 's' * If you need to have a specific plural, use the annotation WebdaPlural * to define the plural name * * @returns */ export function Expose(params = {}) { return function (target) { params.restrict ?? (params.restrict = {}); target.Expose = params; }; } /** * */ export class CoreModelQuery { constructor(type, model, attribute) { this.attribute = attribute; this.type = type; this.model = model; } /** * Retrieve target model definition * @returns */ getTargetModel() { this.targetModel ?? (this.targetModel = Core.get().getModel(this.type)); return this.targetModel; } /** * Query the object * @param query * @returns */ query(query, context) { return this.getTargetModel().query(this.completeQuery(query), true, context); } /** * Complete the query with condition * @param query * @returns */ completeQuery(query) { return WebdaQL.PrependCondition(query, `${this.attribute} = '${this.model.getUuid()}'`); } /** * * @param callback * @param context */ async forEach(callback, query, context, parallelism = 3) { const throttler = new Throttler(); throttler.setConcurrency(parallelism); for await (const model of this.iterate(query, context)) { throttler.execute(() => callback(model)); } return throttler.wait(); } /** * Iterate through all * @param context * @returns */ iterate(query, context) { return Core.get().getModelStore(this.getTargetModel()).iterate(this.completeQuery(query), context); } /** * Get all the objects * @returns */ async getAll(context) { let res = []; for await (const item of this.iterate(this.completeQuery(), context)) { res.push(item); } return res; } } __decorate([ NotEnumerable ], CoreModelQuery.prototype, "type", void 0); __decorate([ NotEnumerable ], CoreModelQuery.prototype, "model", void 0); __decorate([ NotEnumerable ], CoreModelQuery.prototype, "attribute", void 0); __decorate([ NotEnumerable ], CoreModelQuery.prototype, "targetModel", void 0); /** * Make a property hidden from json and schema * * This property will not be saved in the store * Nor it will be exposed in the API * * @param target * @param propertyKey */ export function NotEnumerable(target, propertyKey) { Object.defineProperty(target, propertyKey, { set(value) { Object.defineProperty(this, propertyKey, { value, writable: true, configurable: true }); }, configurable: true }); } const ActionsAnnotated = new Map(); /** * Define an object method as an action * @param target * @param propertyKey */ export function Action(options = {}) { return function (target, propertyKey) { let custom = target; const global = typeof target === "function"; if (!global) { custom = target.constructor; } if (!ActionsAnnotated.has(custom)) { ActionsAnnotated.set(custom, {}); } const actions = ActionsAnnotated.get(custom); actions[options.name || propertyKey] = { ...options, global, method: propertyKey }; }; } export class ModelRef { constructor(uuid, model, parent) { this.uuid = uuid; this.model = model; this.uuid = uuid === "" ? undefined : model.completeUid(uuid); this.store = Core.get().getModelStore(model); } async get(context) { return (await this.store.get(this.uuid))?.setContext(context || this.parent?.getContext()); } set(id) { this.uuid = id instanceof CoreModel ? id.getUuid() : id; this.parent?.__dirty.add(Object.keys(this.parent).find(k => this.parent[k] === this)); } toString() { return this.uuid; } toJSON() { return this.uuid; } getUuid() { return this.uuid; } async deleteItemFromCollection(prop, index, itemWriteCondition, itemWriteConditionField) { const updateDate = await this.store.deleteItemFromCollection(this.uuid, prop, index, itemWriteCondition, itemWriteConditionField); await this.model.emitSync("Store.PartialUpdated", { object_id: this.uuid, store: this.store, updateDate, partial_update: { deleteItem: { property: prop, index: index } } }); return this; } async upsertItemToCollection(prop, item, index, itemWriteCondition, itemWriteConditionField) { const updateDate = await this.store.upsertItemToCollection(this.uuid, prop, item, index, itemWriteCondition, itemWriteConditionField); await this.model.emitSync("Store.PartialUpdated", { object_id: this.uuid, store: this.store, updateDate, partial_update: { addItem: { value: item, property: prop, index: index } } }); return this; } exists() { return this.store.exists(this.uuid); } delete() { return this.store.delete(this.uuid); } conditionalPatch(updates, conditionField, condition) { return this.store.conditionalPatch(this.uuid, updates, conditionField, condition); } patch(updates) { return this.store.conditionalPatch(this.uuid, updates, null, undefined); } async setAttribute(attribute, value) { await this.store.setAttribute(this.uuid, attribute, value); return this; } async removeAttribute(attribute, itemWriteCondition, itemWriteConditionField) { await this.store.removeAttribute(this.uuid, attribute, itemWriteCondition, itemWriteConditionField); await this.model?.emitSync("Store.PartialUpdated", { object_id: this.uuid, store: this.store, partial_update: { deleteAttribute: attribute } }); return this; } async incrementAttributes(info) { const updateDate = await this.store.incrementAttributes(this.uuid, info); await this.model.emitSync("Store.PartialUpdated", { object_id: this.uuid, store: this.store, updateDate, partial_update: { increments: info } }); return this; } } __decorate([ NotEnumerable ], ModelRef.prototype, "store", void 0); __decorate([ NotEnumerable ], ModelRef.prototype, "model", void 0); __decorate([ NotEnumerable ], ModelRef.prototype, "parent", void 0); export class ModelRefWithCreate extends ModelRef { /** * Allow to create a model * @param defaultValue * @param context * @param withSave * @returns */ async create(defaultValue, context, withSave = true) { let result = new this.model().setContext(context).load(defaultValue, true).setUuid(this.uuid); if (withSave) { await result.save(); } return result; } /** * Load a model from the known store * * @param this the class from which the static is called * @param id of the object to load * @param defaultValue if object not found return a default object * @param context to set on the object * @returns */ async getOrCreate(defaultValue, context, withSave = true) { return (await this.get()) || this.create(defaultValue, context, withSave); } } export class ModelRefCustom extends ModelRef { constructor(uuid, model, data, parent) { super(uuid, model, parent); this.uuid = uuid; Object.assign(this, data); } toJSON() { return this; } getUuid() { return this.uuid; } } export const Emitters = new WeakMap(); /** * Basic Object in Webda * * It is used to define a data stored * Any variable starting with _ can only be set by the server * Any variable starting with __ won't be exported outside of the server * * @class * @WebdaModel */ class CoreModel { constructor() { this.__dirty = new Set(); this.__class = new.target; // Get the store automatically now this.__store = Core.get()?.getModelStore(new.target); // Get the type automatically now this.__type = process.env.WEBDA_V2_COMPATIBLE ? Core.get()?.getApplication().getModelFromInstance(this) : Core.get()?.getApplication().getShortId(Core.get()?.getApplication().getModelFromInstance(this)); } /** * Listen to events on the model * @param event * @param listener * @param async */ static on(event, listener, async = false) { if (!Emitters.has(this)) { Emitters.set(this, new EventEmitter()); } // TODO Manage async if (async) { Core.get() .getService("AsyncEvents") .bindAsyncListener(this, event, listener); } else { Emitters.get(this).on(event, listener); } return this; } /** * Emit an event for this class * @param this * @param event * @param evt */ static emit(event, evt) { let clazz = this; // @ts-ignore while (clazz) { // Emit for all parent class if (Emitters.has(clazz)) { EventEmitterUtils.emit(Emitters.get(clazz), event, evt); } // @ts-ignore if (clazz === CoreModel) { break; } clazz = Object.getPrototypeOf(clazz); } } /** * Emit an event for this class and wait for all listeners to finish * @param this * @param event * @param evt */ static async emitSync(event, evt) { let clazz = this; let p = []; // @ts-ignore while (clazz) { // Emit for all parent class if (Emitters.has(clazz)) { p.push(EventEmitterUtils.emitSync(Emitters.get(clazz), event, evt)); } // @ts-ignore if (clazz === CoreModel) { break; } clazz = Object.getPrototypeOf(clazz); } await Promise.all(p); } /** * Listen to events on the model asynchronously * @param event * @param listener */ static onAsync(event, listener, queue) { return this.on(event, listener, true); } /** * * @param event * @param listener * @returns */ static addListener(event, listener) { return this.on(event, listener); } static emitter(method, ...args) { if (!Emitters.has(this)) { Emitters.set(this, new EventEmitter()); } // @ts-ignore return Emitters.get(this)[method](...args); } static removeListener(...args) { return this.emitter("removeListener", ...args); } static off(...args) { return this.emitter("off", ...args); } static once(...args) { return this.emitter("once", ...args); } static removeAllListeners(...args) { return this.emitter("removeAllListeners", ...args); } static setMaxListeners(...args) { return this.emitter("setMaxListeners", ...args); } static getMaxListeners(...args) { return this.emitter("getMaxListeners", ...args); } static listeners(...args) { return this.emitter("listeners", ...args); } static rawListeners(...args) { return this.emitter("rawListeners", ...args); } static listenerCount(...args) { return this.emitter("listenerCount", ...args); } static prependListener(...args) { return this.emitter("prependListener", ...args); } static prependOnceListener(...args) { return this.emitter("prependOnceListener", ...args); } static eventNames(...args) { return this.emitter("eventNames", ...args); } /** * * @returns */ static getRelations() { return Core.get()?.getApplication().getRelations(this); } /** * Do not declare any public events by default * @returns */ static getClientEvents() { return []; } /** * Does not allow any event by default * @param _event * @param _context * @returns */ static authorizeClientEvent(_event, _context, _model) { return false; } /** * Get Store for this model * @param this * @returns */ static store() { if (!Core.get()) { throw new Error("Webda not initialized"); } return Core.get().getModelStore(this); } /** * Complete the uid with prefix if any * * Useful when object are stored with a prefix for full uuid * @param uid * @returns */ static completeUid(uid) { return uid; } /** * Return the known schema * @returns */ static getSchema() { const app = Core.get()?.getApplication(); return app?.getSchema(app.getModelFromConstructor(this)); } /** * Get a reference to a model * @param this * @param uid * @returns */ static ref(uid) { return new ModelRefWithCreate(uid, this); } /** * Get a reference to a model * @param this * @param uid * @returns */ static create(data) { return new this().load(data, true).save(); } /** * Get identifier for this model * @returns */ static getIdentifier(short = true) { const res = Core.get().getApplication().getModelName(this); if (short) { return Core.get().getApplication().getShortId(res); } else { return res; } } /** * Get the model hierarchy * * Ancestors will contain every model it inherits from * Children will contain every model that inherits from this model in a tree structure */ static getHierarchy() { return Core.get().getApplication().getModelHierarchy(this); } /** * Unflat an object * @param data * @param split * @returns */ static unflat(data, split = "#") { const res = {}; for (let i in data) { const attrs = i.split(split); let attr = attrs.shift(); let cur = res; while (attr) { if (attrs.length) { cur[attr] ?? (cur[attr] = {}); cur = cur[attr]; } else { cur[attr] = data[i]; } attr = attrs.shift(); } } return res; } /** * Create subobject for a model * * Useful for counters * * @param split * @returns */ unflat(split = "#") { return CoreModel.unflat(this, split); } /** * Flat an object into another * * { * a: { * b: 1 * }, * c: 1 * } * * become * * { * "a#b": 1 * "c": 1 * } * * @param target * @param data * @param split * @param prefix */ static flat(target, data, split = "#", prefix = "") { for (let i in data) { if (typeof data[i] === "object") { CoreModel.flat(target, data[i], split, i + split); } else { target[prefix + i] = data[i]; } } } /** * Complete the query with __type/__types * @param query * @param includeSubclass * @returns */ static completeQuery(query, includeSubclass = true) { if (!query.includes("__type")) { let condition; const app = Core.get().getApplication(); const name = app.getShortId(app.getModelName(this)); if (includeSubclass) { condition = `__types CONTAINS "${name}"`; } else { condition = `__type = "${name}"`; } return WebdaQL.PrependCondition(query, condition); } return query; } /** * Iterate through the model * * How to use a iterator is: * * ``` * for await (const model of CoreModel.iterate()) { * // Do something with my model * } * ``` * * @param this * @param query * @param includeSubclass * @param context * @returns */ static iterate(query = "", includeSubclass = true, context) { // @ts-ignore return this.store().iterate(this.completeQuery(query, includeSubclass), context); } /** * Query for models * @param this * @param id * @returns */ static async query(query = "", includeSubclass = true, context) { // @ts-ignore return this.store().query(this.completeQuery(query, includeSubclass), context); } /** * Return a proxy to the object to detect if dirty * @returns */ getProxy() { const subProxier = prop => { return { set: (target, p, value) => { this.__dirty.add(prop); target[p] = value; return true; }, get: (target, p) => { if (Array.isArray(target[p]) || target[p] instanceof Object) { return new Proxy(target[p], subProxier(prop)); } return target[p]; }, deleteProperty: (t, property) => { delete t[property]; this.__dirty.add(prop); return true; } }; }; const proxier = { deleteProperty: (t, property) => { delete t[property]; this.__dirty.add(property); return true; }, set: (target, p, value) => { if (p !== "__dirty") { target.__dirty.add(p); } target[p] = value; return true; }, get: (target, p) => { if (typeof p === "string" && p.startsWith("__")) { return target[p]; } if (Array.isArray(target[p]) || target[p] instanceof Object) { return new Proxy(target[p], subProxier(p)); } return target[p]; } }; return new Proxy(this, proxier); } /** * Return true if needs a save * @returns */ isDirty() { return this.__dirty.size > 0; } /** * * @returns the uuid of the object */ getUuid() { // @ts-ignore return this[this.__class.getUuidField()]; } /** * * @param uuid * @param target */ setUuid(uuid, target = this) { target[this.__class.getUuidField()] = uuid; return target; } /** * Get actions callable on an object * * This will expose them by the Store with /storeUrl/{uuid}/{action} */ static getActions() { const actions = []; // Explore all parent classes to collect all known actions let clazz = this; // @ts-ignore while (clazz !== CoreModel) { if (ActionsAnnotated.has(clazz)) { actions.push(ActionsAnnotated.get(clazz)); } clazz = Object.getPrototypeOf(clazz); } // Reduce right to give priority to the last class: return actions.reduceRight((v, c) => ({ ...v, ...c }), {}); } /** * Return the expressable query for permission * * @param context of the query * @returns */ static getPermissionQuery(_ctx) { return null; } async checkAct(context, action) { let msg = await this.canAct(context, action); if (msg !== true) { throw new WebdaError.Forbidden(msg === false ? "No permission" : msg); } } /** * By default nothing is permitted on a CoreModel * @returns */ async canAct(_context, _action) { return "This model does not support any action: override canAct"; } /** * Get the UUID property */ static getUuidField() { return "uuid"; } /** * Create an object * @returns */ static factory(object, context) { return object instanceof this ? object : new this().setContext(context).load(object, context === undefined); } /** * Detect what looks like a CoreModel but can be from different version * @param object * @returns */ static instanceOf(object) { return (typeof object.toStoredJSON === "function" && object.__class && object.__class.factory && object.__class.instanceOf); } /** * Return a unique reference within the application to the object * * It contains the Store containing it * @returns */ getFullUuid() { return `${this.__type.replace(/\//, "-")}$${this.getUuid()}`; } /** * Get an object from the full uuid * @param core * @param fullUuid * @param partials * @returns */ static async fromFullUuid(fullUuid, core = Core.get(), partials) { const [model, uuid] = fullUuid.split("$"); let modelObject = core.getApplication().getModel(model.replace("-", "/")); if (partials) { return new modelObject().load(partials, true).setUuid(uuid); } return new modelObject().setUuid(uuid).get(); } /** * Allow to define custom permission per attribute * * This method allows you to do permission based attribute * But also a mask destructive attribute * * @param key * @param value * @param mode * @param context * @returns updated value */ attributePermission(key, value, mode, context) { if (mode === "WRITE") { return key.startsWith("_") ? undefined : value; } else { return !key.startsWith("__") ? value : undefined; } } /** * Load an object from RAW * * @param raw data * @param secure if false will ignore any _ variable */ load(raw, secure = false, relations = true) { // Object assign with filter for (let prop in raw) { let val = raw[prop]; if (!secure) { val = this.attributePermission(prop, raw[prop], "WRITE"); if (val === undefined) { continue; } } // @ts-ignore this[prop] = val; } if (this._creationDate) { this._creationDate = new Date(this._creationDate); } if (this._lastUpdate) { this._lastUpdate = new Date(this._lastUpdate); } if (!this.getUuid()) { this.setUuid(this.generateUid(raw)); } if (relations) { this.handleRelations(); } return this; } /** * Patch every attribute that is based on a relation * to add all the helpers */ handleRelations() { var _a, _b; const rel = Core.get() ?.getApplication() ?.getRelations(this) || {}; for (let link of rel.links || []) { const model = Core.get().getModel(link.model); if (link.type === "LINK") { this[_a = link.attribute] ?? (this[_a] = ""); if (typeof this[link.attribute] === "string") { this[link.attribute] = new ModelRef(this[link.attribute], model, this); } } else if (link.type === "LINKS_ARRAY") { this[link.attribute] = new ModelLinksArray(model, this[link.attribute], this); } else if (link.type === "LINKS_SIMPLE_ARRAY") { this[link.attribute] = new ModelLinksSimpleArray(model, this[link.attribute], this); } else if (link.type === "LINKS_MAP") { this[link.attribute] = createModelLinksMap(model, this[link.attribute], this); } } for (let link of rel.maps || []) { this[link.attribute] = (this[link.attribute] || []).map(el => new ModelMapLoaderImplementation(Core.get().getModel(link.model), el, this)); } for (let query of rel.queries || []) { this[query.attribute] = new CoreModelQuery(query.model, this, query.targetAttribute); } if (rel.parent) { this[_b = rel.parent.attribute] ?? (this[_b] = ""); if (typeof this[rel.parent.attribute] === "string") { this[rel.parent.attribute] = new ModelRef(this[rel.parent.attribute], Core.get().getModel(rel.parent.model), this); } } for (let binary of rel.binaries || []) { if (binary.cardinality === "ONE") { this[binary.attribute] = new Binary(binary.attribute, this); } else { this[binary.attribute] = new BinariesImpl().assign(this, binary.attribute); } } } /** * Context of the request */ setContext(ctx) { this.__ctx = ctx; return this; } /** * Get object context * * Global object does not belong to a request */ getContext() { return this.__ctx || Core.get().getGlobalContext(); } /** * Return the object registered store */ getStore() { return this.__store; } /** * Get the object * @returns */ async get() { return this.refresh(); } /** * Get the object again * * @throws Error if the object is not coming from a store */ async refresh() { let obj = await this.__store.get(this.getUuid()); if (obj) { Object.assign(this, obj); for (let i in this) { // @ts-ignore if (obj[i] !== this[i]) { delete this[i]; } } this.handleRelations(); } return this; } /** * Delete this object * * @throws Error if the object is not coming from a store */ async delete() { return this.__store.delete(this); } /** * Patch current object with this update * @param obj * @param conditionField if null no condition used otherwise fallback to lastUpdate * @param conditionValue */ async patch(obj, conditionField, conditionValue) { await this.__store.patch({ [this.__class.getUuidField()]: this.getUuid(), ...obj }, true, conditionField, conditionValue); Object.assign(this, obj); } /** * Save this object * * @throws Error if the object is not coming from a store */ async save(full, ...args) { // If proxy is not used and not field specified call save if ((!util.types.isProxy(this) && full === undefined) || full === true) { if (!this._creationDate || !this._lastUpdate) { await this.__store.create(this, this.getContext()); } else { await this.__store.update(this); } return this; } const patch = { [this.__class.getUuidField()]: this.getUuid() }; if (typeof full === "string") { [full, ...args].forEach(k => { patch[k] = this[k]; }); } else { for (let entry of this.__dirty.entries()) { patch[entry[0]] = this[entry[0]]; } } await this.__store.patch(patch); this.__dirty.clear(); return this; } /** * Validate objet modification * * @param ctx * @param updates */ async validate(ctx, updates, ignoreRequired = false) { ctx.getWebda().validateSchema(this, updates, ignoreRequired); return true; } /** * Generate uuid for the object * * @param object * @returns */ generateUid(_object = undefined) { return uuidv4().toString(); } /** * Return the object to be serialized without the __store * * @param stringify * @returns */ toStoredJSON(stringify = false) { let obj = this._toJSON(true); if (stringify) { return JSON.stringify(obj); } return obj; } /** * Get a pre typed service * * @param service to retrieve * WARNING: Only object attached to a store can retrieve service */ getService(service) { return this.__store.getService(service); } /** * Remove the specific attributes if not secure * * * * @param secure serialize server fields also * @returns filtered object to be serialized */ _toJSON(secure) { let obj = {}; for (let i in this) { let value = this[i]; if (!secure) { value = this.attributePermission(i, value, "READ"); } if (value === undefined) continue; if (value instanceof ModelRef) { obj[i] = value.toString(); } else if (value instanceof Binary) { obj[i] = value.toJSON(); } else { obj[i] = value; } } return obj; } /** * Return the object without sensitive attributes * * @returns Object to serialize */ toJSON() { return this._toJSON(false); } /** * Called when object is about to be deleted */ async _onDelete() { // Empty to be overriden } /** * Called when object has been deleted */ async _onDeleted() { // Empty to be overriden } /** * Called when object is retrieved */ async _onGet() { // Empty to be overriden } /** * Called when object is about to be saved */ async _onSave() { // Empty to be overriden } /** * Called when object is saved */ async _onSaved() { // Empty to be overriden } /** * Called when object is about to be updates * * @param updates to be send */ async _onUpdate(_updates) { // Empty to be overriden } /** * Called when object is updated */ async _onUpdated() { // Empty to be overriden } /** * Set attribute on the object and database * @param property * @param value */ async setAttribute(property, value) { await this.getRef().setAttribute(property, value); this[property] = value; } /** * Remove attribute from both the object and db * @param property */ async removeAttribute(property) { await this.getRef().removeAttribute(property); delete this[property]; } /** * Increment an attribute both in store and object * @param property * @param value */ async incrementAttribute(property, value) { return this.incrementAttributes([{ property, value }]); } /** * Return a model ref * @returns */ getRef() { return new ModelRef(this.getUuid(), this.__class); } /** * Increment a attributes both in store and object * @param info */ async incrementAttributes(info) { var _a; await this.getRef().incrementAttributes(info); for (let inc of info) { this[_a = inc.property] ?? (this[_a] = 0); this[inc.property] += inc.value; } } } __decorate([ NotEnumerable ], CoreModel.prototype, "__class", void 0); __decorate([ NotEnumerable ], CoreModel.prototype, "__ctx", void 0); __decorate([ NotEnumerable ], CoreModel.prototype, "__store", void 0); __decorate([ NotEnumerable ], CoreModel.prototype, "__dirty", void 0); /** * CoreModel with a uuid */ class UuidModel extends CoreModel { /** * @override */ validate(ctx, updates, ignoreRequired) { updates.uuid ?? (updates.uuid = this.generateUid()); return super.validate(ctx, updates, ignoreRequired); } } export { CoreModel, UuidModel }; //# sourceMappingURL=coremodel.js.map