@decaf-ts/for-couchdb
Version:
decaf-ts couchdb wrappers
463 lines • 20.5 kB
JavaScript
;
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