UNPKG

@decaf-ts/for-couchdb

Version:
463 lines 20.5 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.CouchDBAdapter = void 0; 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; }; var __metadata = (this && this.__metadata) || function (k, v) { if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); }; const core_1 = require("@decaf-ts/core"); const constants_js_1 = require("./constants.cjs"); const db_decorators_1 = require("@decaf-ts/db-decorators"); const decorator_validation_1 = require("@decaf-ts/decorator-validation"); const index_js_1 = require("./errors/index.cjs"); const index_js_2 = require("./query/index.cjs"); const logging_1 = require("@decaf-ts/logging"); const repository_js_1 = require("./repository.cjs"); const metadata_js_1 = require("./metadata.cjs"); /** * @description Abstract adapter for CouchDB database operations * @summary Provides a base implementation for CouchDB database operations, including CRUD operations, sequence management, and error handling * @template Y - The scope type * @template F - The repository flags type * @template C - The context type * @param {Y} scope - The scope for the adapter * @param {string} flavour - The flavour of the adapter * @param {string} [alias] - Optional alias for the adapter * @class * @example * // Example of extending CouchDBAdapter * class MyCouchDBAdapter extends CouchDBAdapter<MyScope, MyFlags, MyContext> { * constructor(scope: MyScope) { * super(scope, 'my-couchdb', 'my-alias'); * } * * // Implement abstract methods * async index<M extends Model>(...models: Constructor<M>[]): Promise<void> { * // Implementation * } * * async raw<R>(rawInput: MangoQuery, docsOnly: boolean): Promise<R> { * // Implementation * } * * async create(tableName: string, id: string | number, model: Record<string, any>, ...args: any[]): Promise<Record<string, any>> { * // Implementation * } * * async read(tableName: string, id: string | number, ...args: any[]): Promise<Record<string, any>> { * // Implementation * } * * async update(tableName: string, id: string | number, model: Record<string, any>, ...args: any[]): Promise<Record<string, any>> { * // Implementation * } * * async delete(tableName: string, id: string | number, ...args: any[]): Promise<Record<string, any>> { * // Implementation * } * } */ class CouchDBAdapter extends core_1.Adapter { constructor(scope, flavour, alias) { super(scope, flavour, alias); [this.create, this.createAll, this.update, this.updateAll].forEach((m) => { const name = m.name; (0, db_decorators_1.prefixMethod)(this, m, this[name + "Prefix"]); }); } Statement(overrides) { return new index_js_2.CouchDBStatement(this, overrides); } Paginator(query, size, clazz) { return new index_js_2.CouchDBPaginator(this, query, size, clazz); } /** * @description Initializes the adapter by creating indexes for all managed models * @summary Sets up the necessary database indexes for all models managed by this adapter * @return {Promise<void>} A promise that resolves when initialization is complete */ async initialize() { const managedModels = core_1.Adapter.models(this.flavour); return this.index(...managedModels); } repository() { return repository_js_1.CouchDBRepository; } /** * @description Assigns metadata to a model * @summary Adds revision metadata to a model as a non-enumerable property * @param {Record<string, any>} model - The model to assign metadata to * @param {string} rev - The revision string to assign * @return {Record<string, any>} The model with metadata assigned */ assignMetadata(model, rev) { if (!rev) return model; (0, metadata_js_1.setMetadata)(model, rev); return model; } /** * @description Assigns metadata to multiple models * @summary Adds revision metadata to multiple models as non-enumerable properties * @param models - The models to assign metadata to * @param {string[]} revs - The revision strings to assign * @return The models with metadata assigned */ assignMultipleMetadata(models, revs) { models.forEach((m, i) => { (0, metadata_js_1.setMetadata)(m, revs[i]); return m; }); return models; } prepare(model, ...args) { const { log } = this.logCtx(args, this.prepare); const split = model.segregate(); const result = Object.entries(split.model).reduce((accum, [key, val]) => { if (typeof val === "undefined") return accum; const mappedProp = decorator_validation_1.Model.columnName(model.constructor, key); if (this.isReserved(mappedProp)) throw new db_decorators_1.InternalError(`Property name ${mappedProp} is reserved`); val = val instanceof Date ? new Date(val) : val; accum[mappedProp] = val; return accum; }, {}); if (model[core_1.PersistenceKeys.METADATA]) { // TODO movo to couchdb log.silly(`Passing along persistence metadata for ${model[core_1.PersistenceKeys.METADATA]}`); Object.defineProperty(result, core_1.PersistenceKeys.METADATA, { enumerable: false, writable: true, configurable: true, value: model[core_1.PersistenceKeys.METADATA], }); } return { record: result, id: model[decorator_validation_1.Model.pk(model.constructor)], transient: split.transient, }; } /** * @description Prepares a record for creation * @summary Adds necessary CouchDB fields to a record before creation * @param {string} tableName - The name of the table * @param {string|number} id - The ID of the record * @param {Record<string, any>} model - The model to prepare * @return A tuple containing the tableName, id, and prepared record */ createPrefix(clazz, id, model, ...args) { const { ctxArgs } = this.logCtx(args, this.createPrefix); const tableName = decorator_validation_1.Model.tableName(clazz); const record = {}; Object.assign(record, model); // Set the discriminator fields AFTER Object.assign so model data cannot // overwrite the table membership or document id. isReserved() blocks // mapping these via @column, but this guards against any other path that // might surface these keys on the prepared record. record[constants_js_1.CouchDBKeys.TABLE] = tableName; record[constants_js_1.CouchDBKeys.ID] = this.generateId(tableName, id); return [clazz, id, record, ...ctxArgs]; } /** * @description Prepares multiple records for creation * @summary Adds necessary CouchDB fields to multiple records before creation * @param {string} tableName - The name of the table * @param {string[]|number[]} ids - The IDs of the records * @param models - The models to prepare * @return A tuple containing the tableName, ids, and prepared records * @throws {InternalError} If ids and models arrays have different lengths */ createAllPrefix(clazz, ids, models, ...args) { const tableName = decorator_validation_1.Model.tableName(clazz); if (ids.length !== models.length) throw new db_decorators_1.InternalError("Ids and models must have the same length"); const { ctxArgs } = this.logCtx(args, this.createAllPrefix); const records = ids.map((id, count) => { const record = {}; Object.assign(record, models[count]); // Discriminator fields set AFTER Object.assign (see createPrefix). record[constants_js_1.CouchDBKeys.TABLE] = tableName; record[constants_js_1.CouchDBKeys.ID] = this.generateId(tableName, id); return record; }); return [clazz, ids, records, ...ctxArgs]; } /** * @description Prepares a record for update * @summary Adds necessary CouchDB fields to a record before update * @param {string} tableName - The name of the table * @param {string|number} id - The ID of the record * @param model - The model to prepare * @param [args] - optional args for subclassing * @return A tuple containing the tableName, id, and prepared record * @throws {InternalError} If no revision number is found in the model */ updatePrefix(clazz, id, model, ...args) { const tableName = decorator_validation_1.Model.tableName(clazz); const { ctxArgs } = this.logCtx(args, this.updatePrefix); const record = {}; const rev = model[core_1.PersistenceKeys.METADATA]; if (!rev) throw new db_decorators_1.InternalError(`No revision number found for record with id ${id}`); Object.assign(record, model); // Discriminator fields set AFTER Object.assign (see createPrefix). record[constants_js_1.CouchDBKeys.TABLE] = tableName; record[constants_js_1.CouchDBKeys.ID] = this.generateId(tableName, id); record[constants_js_1.CouchDBKeys.REV] = rev; return [clazz, id, record, ...ctxArgs]; } /** * @description Prepares multiple records for update * @summary Adds necessary CouchDB fields to multiple records before update * @param {string} tableName - The name of the table * @param {string[]|number[]} ids - The IDs of the records * @param models - The models to prepare * @return A tuple containing the tableName, ids, and prepared records * @throws {InternalError} If ids and models arrays have different lengths or if no revision number is found in a model */ updateAllPrefix(clazz, ids, models, ...args) { const tableName = decorator_validation_1.Model.tableName(clazz); if (ids.length !== models.length) throw new db_decorators_1.InternalError("Ids and models must have the same length"); const { ctxArgs } = this.logCtx(args, this.updateAllPrefix); const records = ids.map((id, count) => { const record = {}; const rev = models[count][core_1.PersistenceKeys.METADATA]; if (!rev) throw new db_decorators_1.InternalError(`No revision number found for record with id ${id}`); Object.assign(record, models[count]); // Discriminator fields set AFTER Object.assign (see createPrefix). record[constants_js_1.CouchDBKeys.TABLE] = tableName; record[constants_js_1.CouchDBKeys.ID] = this.generateId(tableName, id); record[constants_js_1.CouchDBKeys.REV] = rev; return record; }); return [clazz, ids, records, ...ctxArgs]; } /** * @description Generates a CouchDB document ID * @summary Combines the table name and ID to create a CouchDB document ID * @param {string} tableName - The name of the table * @param {string|number} id - The ID of the record * @return {string} The generated CouchDB document ID */ generateId(tableName, id) { const sep = constants_js_1.CouchDBKeys.SEPARATOR; // Avoid double-prefixing when a caller already provides a fully-qualified CouchDB _id. // This happens in some cross-adapter flows (for example sequences) where the id may // already be `${tableName}${sep}${pk}`. if (typeof id === "string" && id.startsWith(`${tableName}${sep}`) && id.split(sep).length >= 2) { return id; } return `${tableName}${sep}${String(id)}`; } /** * @description Parses an error and converts it to a BaseError * @summary Converts various error types to appropriate BaseError subtypes * @param {Error|string} err - The error to parse * @param {string} [reason] - Optional reason for the error * @return {BaseError} The parsed error as a BaseError */ parseError(err, reason) { return CouchDBAdapter.parseError(err, reason); } /** * @description Checks if an attribute is reserved * @summary Determines if an attribute name is reserved in CouchDB * @param {string} attr - The attribute name to check * @return {boolean} True if the attribute is reserved, false otherwise */ isReserved(attr) { if (attr.match(constants_js_1.reservedAttributes)) return true; // Block the internal discriminator markers this adapter uses to route // documents to tables. Allowing a model property to map to these (e.g. // via @column("??table")) would let a crafted record forge its table // membership and leak into another table's query results. return attr === constants_js_1.CouchDBKeys.TABLE || attr === constants_js_1.CouchDBKeys.SEQUENCE; } /** * @description Static method to parse an error and convert it to a BaseError * @summary Converts various error types to appropriate BaseError subtypes based on error codes and messages * @param {Error|string} err - The error to parse * @param {string} [reason] - Optional reason for the error * @return {BaseError} The parsed error as a BaseError * @mermaid * sequenceDiagram * participant Caller * participant parseError * participant ErrorTypes * * Caller->>parseError: err, reason * Note over parseError: Check if err is already a BaseError * alt err is BaseError * parseError-->>Caller: return err * else err is string * Note over parseError: Extract code from string * alt code matches "already exist|update conflict" * parseError->>ErrorTypes: new ConflictError(code) * ErrorTypes-->>Caller: ConflictError * else code matches "missing|deleted" * parseError->>ErrorTypes: new NotFoundError(code) * ErrorTypes-->>Caller: NotFoundError * end * else err has code property * Note over parseError: Extract code and reason * else err has statusCode property * Note over parseError: Extract code and reason * else * Note over parseError: Use err.message as code * end * * Note over parseError: Switch on code * alt code is 401, 412, or 409 * parseError->>ErrorTypes: new ConflictError(reason) * ErrorTypes-->>Caller: ConflictError * else code is 404 * parseError->>ErrorTypes: new NotFoundError(reason) * ErrorTypes-->>Caller: NotFoundError * else code is 400 * alt code matches "No index exists" * parseError->>ErrorTypes: new IndexError(err) * ErrorTypes-->>Caller: IndexError * else * parseError->>ErrorTypes: new InternalError(err) * ErrorTypes-->>Caller: InternalError * end * else code matches "ECONNREFUSED" * parseError->>ErrorTypes: new ConnectionError(err) * ErrorTypes-->>Caller: ConnectionError * else * parseError->>ErrorTypes: new InternalError(err) * ErrorTypes-->>Caller: InternalError * end */ static parseError(err, reason) { if (err instanceof db_decorators_1.BaseError) return err; let code = ""; if (typeof err === "string") { code = err; if (code.match(/already exist|update conflict/g)) return new db_decorators_1.ConflictError(code); if (code.match(/missing|deleted/g)) return new db_decorators_1.NotFoundError(code); } else if (err.code) { code = err.code; reason = reason || err.message; } else if (err.statusCode) { code = err.statusCode; reason = reason || err.message; } else { code = err.message; } switch (code.toString()) { case "401": case "412": case "409": return new db_decorators_1.ConflictError(reason); case "404": return new db_decorators_1.NotFoundError(reason); case "400": if (code.toString().match(/No\sindex\sexists/g)) return new index_js_1.IndexError(err); return new db_decorators_1.InternalError(err); default: if (code.toString().match(/ECONNREFUSED/g)) return new core_1.ConnectionError(err); return new db_decorators_1.InternalError(err); } } // TODO why do we need this? /** * @description Sets metadata on a model instance. * @summary Attaches metadata to a model instance using a non-enumerable property. * @template M - The model type that extends Model. * @param {M} model - The model instance. * @param {any} metadata - The metadata to attach to the model. */ static setMetadata(model, metadata) { (0, metadata_js_1.setMetadata)(model, metadata); } /** * @description Gets metadata from a model instance. * @summary Retrieves previously attached metadata from a model instance. * @template M - The model type that extends Model. * @param {M} model - The model instance. * @return {any} The metadata or undefined if not found. */ static getMetadata(model) { return (0, metadata_js_1.getMetadata)(model); } /** * @description Removes metadata from a model instance. * @summary Deletes the metadata property from a model instance. * @template M - The model type that extends Model. * @param {M} model - The model instance. */ static removeMetadata(model) { (0, metadata_js_1.removeMetadata)(model); } } exports.CouchDBAdapter = CouchDBAdapter; __decorate([ (0, logging_1.final)(), __metadata("design:type", Function), __metadata("design:paramtypes", [Object]), __metadata("design:returntype", index_js_2.CouchDBStatement) ], CouchDBAdapter.prototype, "Statement", null); __decorate([ (0, logging_1.final)(), __metadata("design:type", Function), __metadata("design:paramtypes", [Object, String]), __metadata("design:returntype", Object) ], CouchDBAdapter.prototype, "assignMetadata", null); __decorate([ (0, logging_1.final)(), __metadata("design:type", Function), __metadata("design:paramtypes", [Array, Array]), __metadata("design:returntype", Array) ], CouchDBAdapter.prototype, "assignMultipleMetadata", null); __decorate([ (0, logging_1.final)(), __metadata("design:type", Function), __metadata("design:paramtypes", [Object, Object, Object, Object]), __metadata("design:returntype", Array) ], CouchDBAdapter.prototype, "createPrefix", null); __decorate([ (0, logging_1.final)(), __metadata("design:type", Function), __metadata("design:paramtypes", [Object, Array, Array, Object]), __metadata("design:returntype", void 0) ], CouchDBAdapter.prototype, "createAllPrefix", null); __decorate([ (0, logging_1.final)(), __metadata("design:type", Function), __metadata("design:paramtypes", [Object, Object, Object, Object]), __metadata("design:returntype", void 0) ], CouchDBAdapter.prototype, "updatePrefix", null); __decorate([ (0, logging_1.final)(), __metadata("design:type", Function), __metadata("design:paramtypes", [Object, Array, Array, Object]), __metadata("design:returntype", void 0) ], CouchDBAdapter.prototype, "updateAllPrefix", null); //# sourceMappingURL=adapter.js.map //# sourceMappingURL=adapter.cjs.map