@decaf-ts/core
Version:
Core persistence module for the decaf framework
256 lines • 11.9 kB
JavaScript
;
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