UNPKG

@decaf-ts/core

Version:

Core persistence module for the decaf framework

256 lines 11.9 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.Sequence = void 0; const decorator_validation_1 = require("@decaf-ts/decorator-validation"); const errors_1 = require("./errors.cjs"); const db_decorators_1 = require("@decaf-ts/db-decorators"); const ContextualLoggedClass_1 = require("./../utils/ContextualLoggedClass.cjs"); const Adapter_1 = require("./Adapter.cjs"); const Repository_1 = require("./../repository/Repository.cjs"); const SequenceModel_1 = require("./../model/SequenceModel.cjs"); const generators_1 = require("./generators.cjs"); const Context_1 = require("./Context.cjs"); const transactional_decorators_1 = require("@decaf-ts/transactional-decorators"); /** * @description Abstract base class for sequence generation * @summary Provides a framework for generating sequential values (like primary keys) in the persistence layer. * Implementations of this class handle the specifics of how sequences are stored and incremented in different * database systems. * @param {SequenceOptions} options - Configuration options for the sequence generator * @class Sequence * @example * ```typescript * // Example implementation for a specific database * class PostgresSequence extends Sequence { * constructor(options: SequenceOptions) { * super(options); * } * * async next(): Promise<number> { * // Implementation to get next value from PostgreSQL sequence * const result = await this.options.executor.raw(`SELECT nextval('${this.options.name}')`); * return parseInt(result.rows[0].nextval); * } * * async current(): Promise<number> { * // Implementation to get current value from PostgreSQL sequence * const result = await this.options.executor.raw(`SELECT currval('${this.options.name}')`); * return parseInt(result.rows[0].currval); * } * * async range(count: number): Promise<number[]> { * // Implementation to get a range of values * const values: number[] = []; * for (let i = 0; i < count; i++) { * values.push(await this.next()); * } * return values; * } * } * * // Usage * const sequence = new PostgresSequence({ * name: 'user_id_seq', * executor: dbExecutor * }); * * const nextId = await sequence.next(); * ``` */ class Sequence extends ContextualLoggedClass_1.ContextualLoggedClass { static { this.lock = new transactional_decorators_1.MultiLock(); } /** * @description Creates a new sequence instance * @summary Protected constructor that initializes the sequence with the provided options */ constructor(options, adapter) { super(); this.options = options; this.adapter = adapter; this.repo = Repository_1.Repository.forModel(SequenceModel_1.SequenceModel, adapter.alias); } /** * @description Retrieves the current value of the sequence * @summary Gets the current value of the sequence from storage. If the sequence * doesn't exist yet, it returns the configured starting value. * @return A promise that resolves to the current sequence value */ async current(...args) { const contextArgs = await Context_1.Context.args(db_decorators_1.OperationKeys.READ, SequenceModel_1.SequenceModel, args, this.adapter); const ctx = contextArgs.context; const { name, startWith } = this.options; try { const sequence = await this.repo.read(name, ctx); return this.parse(sequence.current); } catch (e) { const log = ctx.logger.for(this.current); if (e instanceof db_decorators_1.NotFoundError) { log.debug(`Sequence.current missing ${name}, returning startWith=${startWith}`); if (typeof startWith === "undefined") throw new db_decorators_1.InternalError("Starting value is not defined for a non existing sequence"); try { return this.parse(startWith); } catch (e) { throw new db_decorators_1.InternalError(`Failed to parse initial value for sequence ${startWith}: ${e}`); } } throw new db_decorators_1.InternalError(`Failed to retrieve current value for sequence ${name}: ${e}`); } } /** * @description Increments the sequence value * @summary Increases the current sequence value by the specified amount and persists * the new value to storage. This method handles both numeric and BigInt sequence types. * @param {string | number | bigint} current - The current value of the sequence * @param {number} [count] - Optional amount to increment by, defaults to the sequence's incrementBy value * @return A promise that resolves to the new sequence value after incrementing */ async increment(count, ctx) { const log = ctx.logger.for(this.increment); const { type, incrementBy, name } = this.options; if (!name) throw new db_decorators_1.InternalError("Sequence name is required"); return Sequence.lock.execute(async () => { const toIncrementBy = count || incrementBy; if (toIncrementBy % incrementBy !== 0) throw new db_decorators_1.InternalError(`Value to increment does not consider the incrementBy setting: ${incrementBy}`); const typeName = typeof type === "function" && type?.name ? type.name : type; const currentValue = await this.current(ctx); const performUpsert = async (next) => { try { return await this.repo.update(new SequenceModel_1.SequenceModel({ id: name, current: next }), ctx); } catch (e) { if (e instanceof db_decorators_1.NotFoundError) { log.debug(`Sequence create ${name} current=${currentValue} next=${next}`); return this.repo.create(new SequenceModel_1.SequenceModel({ id: name, current: next }), ctx); } throw e; } }; const incrementSerial = (base) => { switch (typeName) { case Number.name: return this.parse(base) + toIncrementBy; case BigInt.name: return this.parse(base) + BigInt(toIncrementBy); case String.name: return this.parse(base); case "serial": return generators_1.Serial.instance.generate(base); default: throw new db_decorators_1.InternalError("Should never happen"); } }; if (typeName === "uuid") { while (true) { const next = generators_1.UUID.instance.generate(currentValue); try { const result = await performUpsert(next); log.debug(`Sequence uuid increment ${name} current=${currentValue} next=${next}`); return result.current; } catch (e) { if (e instanceof db_decorators_1.ConflictError) continue; throw e; } } } const next = incrementSerial(currentValue); const seq = await performUpsert(next); log.debug(`Sequence.increment ${name} current=${currentValue} next=${next}`); return seq.current; }, name); } /** * @description Gets the next value in the sequence * @summary Retrieves the current value of the sequence and increments it by the * configured increment amount. This is the main method used to get a new sequential value. * @return A promise that resolves to the next value in the sequence */ async next(...argz) { const contextArgs = await Context_1.Context.args(db_decorators_1.OperationKeys.UPDATE, SequenceModel_1.SequenceModel, argz, this.adapter); const { context } = contextArgs; return this.increment(undefined, context); } /** * @description Generates a range of sequential values * @summary Retrieves a specified number of sequential values from the sequence. * This is useful when you need to allocate multiple IDs at once. * The method increments the sequence by the total amount needed and returns all values in the range. * @param {number} count - The number of sequential values to generate * @return A promise that resolves to an array of sequential values */ async range(count, ...argz) { const contextArgs = await Context_1.Context.args(db_decorators_1.OperationKeys.UPDATE, SequenceModel_1.SequenceModel, argz, this.adapter); const { context, args } = contextArgs; const current = (await this.current(...args)); const incrementBy = this.parse(this.options.incrementBy); const next = await this.increment(this.parse(count) * incrementBy, context); const range = []; for (let i = 1; i <= count; i++) { range.push(current + incrementBy * this.parse(i)); } if (this.options.type === "uuid" || this.options.type === "serial") throw new errors_1.UnsupportedError(// TODO just generate valid uuids/serials `type ${this.options.type} is currently not suppported for this adapter`); const typeName = typeof this.options.type === "function" && this.options.type?.name ? this.options.type.name : this.options.type; if (range[range.length - 1] !== next && typeName !== "String") throw new db_decorators_1.InternalError("Miscalculation of range"); return range; } parse(value) { return Sequence.parseValue(this.options.type, value); } /** * @description Gets the primary key sequence name for a model * @summary Utility method that returns the standardized sequence name for a model's primary key * @template M - The model type * @param {M|Constructor<M>} model - The model instance or constructor * @return {string} The sequence name for the model's primary key */ static pk(model) { return decorator_validation_1.Model.sequenceName(model, "pk"); } /** * @description Parses a sequence value to the appropriate type * @summary Converts a sequence value to the specified type (Number or BigInt) * @param {"Number"|"BigInt"|undefined} type - The target type to convert to * @param {string|number|bigint} value - The value to convert * @return {string|number|bigint} The converted value */ static parseValue(type, value) { const typeName = typeof type === "function" && type?.name ? type.name : type; switch (typeName) { case Number.name || Number.name.toLowerCase(): return typeof value === "string" ? parseInt(value) : typeof value === "number" ? value : BigInt(value); case BigInt.name || BigInt.name.toLowerCase(): return BigInt(value); case String.name || String.name.toLowerCase(): return value.toString(); case undefined: case "uuid": case "serial": return value; default: throw new errors_1.UnsupportedError(`Unsupported sequence type: ${type} for adapter ${this}`); } } } exports.Sequence = Sequence; Adapter_1.Adapter["_baseSequence"] = Sequence; //# sourceMappingURL=Sequence.js.map