@decaf-ts/core
Version:
Core persistence module for the decaf framework
482 lines • 21.4 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.RamAdapter = void 0;
const RamStatement_1 = require("./RamStatement.cjs");
const persistence_1 = require("./../persistence/index.cjs");
const transactional_decorators_1 = require("@decaf-ts/transactional-decorators");
const decorator_validation_1 = require("@decaf-ts/decorator-validation");
const db_decorators_1 = require("@decaf-ts/db-decorators");
const handlers_1 = require("./handlers.cjs");
const constants_1 = require("./constants.cjs");
const decoration_1 = require("@decaf-ts/decoration");
const RamPaginator_1 = require("./RamPaginator.cjs");
/**
* @description In-memory adapter for data persistence
* @summary The RamAdapter provides an in-memory implementation of the persistence layer.
* It stores data in JavaScript Maps and provides CRUD operations and query capabilities.
* This adapter is useful for testing, prototyping, and applications that don't require
* persistent storage across application restarts.
* @class RamAdapter
* @category Ram
* @example
* ```typescript
* // Create a new RAM adapter
* const adapter = new RamAdapter('myRamAdapter');
*
* // Create a repository for a model
* const userRepo = new (adapter.repository<User>())(User, adapter);
*
* // Perform CRUD operations
* const user = new User({ name: 'John', email: 'john@example.com' });
* await userRepo.create(user);
* const retrievedUser = await userRepo.findById(user.id);
* ```
* @mermaid
* sequenceDiagram
* participant Client
* participant Repository
* participant RamAdapter
* participant Storage as In-Memory Storage
*
* Client->>Repository: create(model)
* Repository->>RamAdapter: create(tableName, id, model)
* RamAdapter->>RamAdapter: lock.acquire()
* RamAdapter->>Storage: set(id, model)
* RamAdapter->>RamAdapter: lock.release()
* RamAdapter-->>Repository: model
* Repository-->>Client: model
*
* Client->>Repository: findById(id)
* Repository->>RamAdapter: read(tableName, id)
* RamAdapter->>Storage: get(id)
* Storage-->>RamAdapter: model
* RamAdapter-->>Repository: model
* Repository-->>Client: model
*/
class RamAdapter extends persistence_1.Adapter {
constructor(conf = {}, alias) {
super(conf, constants_1.RamFlavour, alias);
this.indexes = {};
this.lock = new transactional_decorators_1.Lock();
}
/**
* @description Gets the repository constructor for a model
* @summary Returns a constructor for creating repositories that work with the specified model type.
* This method overrides the base implementation to provide RAM-specific repository functionality.
* @template M - The model type for the repository
* @return {Constructor<RamRepository<M>>} A constructor for creating RAM repositories
*/
repository() {
return super.repository();
}
/**
* @description Creates operation flags with UUID
* @summary Extends the base flags with a UUID for user identification.
* This method ensures that all operations have a unique identifier for tracking purposes.
* @template M - The model type for the operation
* @param {OperationKeys} operation - The type of operation being performed
* @param {Constructor<M>} model - The model constructor
* @param {Partial<RamFlags>} flags - Partial flags to be extended
* @return {Promise<RamFlags>} Complete flags with UUID
*/
async flags(operation, model, flags) {
return Object.assign(await super.flags(operation, model, flags), {
UUID: this.config.user || "" + Date.now(),
});
}
Dispatch() {
return super.Dispatch();
}
/**
* @description Indexes models in the RAM adapter
* @summary A no-op indexing method for the RAM adapter.
* Since RAM adapter doesn't require explicit indexing, this method simply resolves immediately.
* @param models - Models to be indexed (unused)
* @return {Promise<any>} A promise that resolves when indexing is complete
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async index(...models) {
return Promise.resolve(undefined);
}
/**
* @description Prepares a model for storage
* @summary Converts a model instance to a format suitable for storage in the RAM adapter.
* This method extracts the primary key and creates a record without the primary key field.
* @template M - The model type being prepared
* @param {M} model - The model instance to prepare
* @param pk - The primary key property name
* @return Object containing the record and ID
*/
prepare(model, ...args) {
const ctx = args.pop();
const prepared = super.prepare(model, ...args, ctx);
return prepared;
}
/**
* @description Converts a stored record back to a model instance
* @summary Reconstructs a model instance from a stored record by adding back the primary key.
* This method is the inverse of the prepare method.
* @template M - The model type to revert to
* @param {Record<string, any>} obj - The stored record
* @param {Constructor<M>} clazz - The model class or name
* @param {PrimaryKeyType} id - The primary key value
* @return {M} The reconstructed model instance
*/
revert(obj, clazz, id, transient, ...args) {
const res = super.revert(obj, clazz, id, transient, ...args);
return res;
}
/**
* @description Creates a new record in the in-memory storage
* @summary Stores a new record in the specified table with the given ID.
* This method acquires a lock to ensure thread safety, creates the table if it doesn't exist,
* checks for conflicts, and stores the model.
* @param {string} clazz - The name of the table to store the record in
* @param {string | number} id - The unique identifier for the record
* @param {Record<string, any>} model - The record data to store
* @return {Promise<Record<string, any>>} A promise that resolves to the stored record
* @mermaid
* sequenceDiagram
* participant Caller
* participant RamAdapter
* participant Storage as In-Memory Storage
*
* Caller->>RamAdapter: create(tableName, id, model)
* RamAdapter->>RamAdapter: lock.acquire()
* RamAdapter->>Storage: has(tableName)
* alt Table doesn't exist
* RamAdapter->>Storage: set(tableName, new Map())
* end
* RamAdapter->>Storage: has(id)
* alt Record exists
* RamAdapter-->>Caller: throw ConflictError
* end
* RamAdapter->>Storage: set(id, model)
* RamAdapter->>RamAdapter: lock.release()
* RamAdapter-->>Caller: model
*/
async create(clazz, id, model, ctx) {
const log = ctx.logger.for(this.create);
const tableName = decorator_validation_1.Model.tableName(clazz);
log.debug(`creating record in table ${tableName} with id ${id}`);
if (!this.client.has(tableName))
this.client.set(tableName, new Map());
if (this.client.get(tableName) &&
this.client.get(tableName)?.has(id))
throw new db_decorators_1.ConflictError(`Record with id ${id} already exists in table ${tableName}`);
await this.lock.acquire();
this.client.get(tableName)?.set(id, model);
this.lock.release();
return model;
}
/**
* @description Retrieves a record from in-memory storage
* @summary Fetches a record with the specified ID from the given table.
* This method checks if the table and record exist and throws appropriate errors if not.
* @param {Constructor} clazz - The name of the table to retrieve from
* @param {PrimaryKeyType} id - The unique identifier of the record to retrieve
* @return {Promise<Record<string, any>>} A promise that resolves to the retrieved record
* @mermaid
* sequenceDiagram
* participant Caller
* participant RamAdapter
* participant Storage as In-Memory Storage
*
* Caller->>RamAdapter: read(tableName, id)
* RamAdapter->>Storage: has(tableName)
* alt Table doesn't exist
* RamAdapter-->>Caller: throw NotFoundError
* end
* RamAdapter->>Storage: has(id)
* alt Record doesn't exist
* RamAdapter-->>Caller: throw NotFoundError
* end
* RamAdapter->>Storage: get(id)
* Storage-->>RamAdapter: record
* RamAdapter-->>Caller: record
*/
async read(clazz, id,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
ctx) {
const tableName = decorator_validation_1.Model.tableName(clazz);
if (!this.client.has(tableName))
throw new db_decorators_1.NotFoundError(`Table ${tableName} not found`);
if (!this.client.get(tableName)?.has(id))
throw new db_decorators_1.NotFoundError(`Record with id ${id} not found in table ${tableName}`);
return this.client.get(tableName)?.get(id);
}
/**
* @description Updates an existing record in the in-memory storage
* @summary Updates a record with the specified ID in the given table.
* This method acquires a lock to ensure thread safety, checks if the table and record exist,
* and updates the record with the new data.
* @param {string} tableName - The name of the table containing the record
* @param {string | number} id - The unique identifier of the record to update
* @param {Record<string, any>} model - The new record data
* @return {Promise<Record<string, any>>} A promise that resolves to the updated record
* @mermaid
* sequenceDiagram
* participant Caller
* participant RamAdapter
* participant Storage as In-Memory Storage
*
* Caller->>RamAdapter: update(tableName, id, model)
* RamAdapter->>RamAdapter: lock.acquire()
* RamAdapter->>Storage: has(tableName)
* alt Table doesn't exist
* RamAdapter-->>Caller: throw NotFoundError
* end
* RamAdapter->>Storage: has(id)
* alt Record doesn't exist
* RamAdapter-->>Caller: throw NotFoundError
* end
* RamAdapter->>Storage: set(id, model)
* RamAdapter->>RamAdapter: lock.release()
* RamAdapter-->>Caller: model
*/
async update(clazz, id, model, ctx) {
const log = ctx.logger.for(this.update);
const tableName = decorator_validation_1.Model.tableName(clazz);
log.debug(`updating record in table ${tableName} with id ${id}`);
if (!this.client.has(tableName))
throw new db_decorators_1.NotFoundError(`Table ${tableName} not found`);
if (!this.client.get(tableName)?.has(id))
throw new db_decorators_1.NotFoundError(`Record with id ${id} not found in table ${tableName}`);
await this.lock.acquire();
this.client.get(tableName)?.set(id, model);
this.lock.release();
return model;
}
/**
* @description Deletes a record from the in-memory storage
* @summary Removes a record with the specified ID from the given table.
* This method acquires a lock to ensure thread safety, checks if the table and record exist,
* retrieves the record before deletion, and then removes it from storage.
* @param {string} tableName - The name of the table containing the record
* @param {string | number} id - The unique identifier of the record to delete
* @return {Promise<Record<string, any>>} A promise that resolves to the deleted record
* @mermaid
* sequenceDiagram
* participant Caller
* participant RamAdapter
* participant Storage as In-Memory Storage
*
* Caller->>RamAdapter: delete(tableName, id)
* RamAdapter->>RamAdapter: lock.acquire()
* RamAdapter->>Storage: has(tableName)
* alt Table doesn't exist
* RamAdapter-->>Caller: throw NotFoundError
* end
* RamAdapter->>Storage: has(id)
* alt Record doesn't exist
* RamAdapter-->>Caller: throw NotFoundError
* end
* RamAdapter->>Storage: get(id)
* Storage-->>RamAdapter: record
* RamAdapter->>Storage: delete(id)
* RamAdapter->>RamAdapter: lock.release()
* RamAdapter-->>Caller: record
*/
async delete(clazz, id, ctx) {
const log = ctx.logger.for(this.delete);
const tableName = decorator_validation_1.Model.tableName(clazz);
log.debug(`deleting record from table ${tableName} with pk ${id}`);
if (!this.client.has(tableName))
throw new db_decorators_1.NotFoundError(`Table ${tableName} not found`);
if (!this.client.get(tableName)?.has(id))
throw new db_decorators_1.NotFoundError(`Record with id ${id} not found in table ${tableName}`);
await this.lock.acquire();
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const table = this.client.get(tableName);
const natived = this.client.get(tableName)?.get(id);
this.client.get(tableName)?.delete(id);
this.lock.release();
return natived;
}
/**
* @description Gets or creates a table in the in-memory storage
* @summary Retrieves the Map representing a table for a given model or table name.
* If the table doesn't exist, it creates a new one. This is a helper method used
* by other methods to access the correct storage location.
* @template M - The model type for the table
* @param {string | Constructor<M>} from - The model class or table name
* @return {Map<string | number, any> | undefined} The table Map or undefined
*/
tableFor(from) {
if (typeof from === "string")
from = decorator_validation_1.Model.get(from);
const table = decorator_validation_1.Model.tableName(from);
if (!this.client.has(table))
this.client.set(table, new Map());
return this.client.get(table);
}
/**
* @description Executes a raw query against the in-memory storage
* @summary Performs a query operation on the in-memory data store using the provided query specification.
* This method supports filtering, sorting, pagination, and field selection.
* @template R - The return type of the query
* @param {RawRamQuery<any>} rawInput - The query specification
* @return {Promise<R>} A promise that resolves to the query results
* @mermaid
* sequenceDiagram
* participant Caller
* participant RamAdapter
* participant Storage as In-Memory Storage
*
* Caller->>RamAdapter: raw(rawInput)
* RamAdapter->>RamAdapter: tableFor(from)
* alt Table doesn't exist
* RamAdapter-->>Caller: throw InternalError
* end
* RamAdapter->>RamAdapter: findPrimaryKey(new from())
* RamAdapter->>Storage: entries()
* Storage-->>RamAdapter: entries
* loop For each entry
* RamAdapter->>RamAdapter: revert(r, from, id, pk)
* end
* alt Where condition exists
* RamAdapter->>RamAdapter: result.filter(where)
* end
* alt Sort condition exists
* RamAdapter->>RamAdapter: result.sort(sort)
* end
* alt Skip specified
* RamAdapter->>RamAdapter: result.slice(skip)
* end
* alt Limit specified
* RamAdapter->>RamAdapter: result.slice(0, limit)
* end
* alt Select fields specified
* loop For each result
* RamAdapter->>RamAdapter: Filter to selected fields
* end
* end
* RamAdapter-->>Caller: result
*/
async raw(rawInput, docsOnly = true, ctx) {
const log = ctx.logger.for(this.raw);
log.debug(`performing raw query: ${JSON.stringify(rawInput)}`);
const { where, sort, limit, skip, from } = rawInput;
let { select } = rawInput;
const collection = this.tableFor(from);
if (!collection)
throw new db_decorators_1.InternalError(`Table ${from} not found in RamAdapter`);
const id = decorator_validation_1.Model.pk(from);
const props = decoration_1.Metadata.get(from, decoration_1.Metadata.key(db_decorators_1.DBKeys.ID, id));
let result = Array.from(collection.entries()).map(([pk, r]) => this.revert(r, from, persistence_1.Sequence.parseValue(props.type, pk), undefined, ctx));
result = where ? result.filter(where) : result;
const count = result.length;
if (sort)
result = result.sort(sort);
if (skip)
result = result.slice(skip);
if (limit)
result = result.slice(0, limit);
if (select) {
select = Array.isArray(select) ? select : [select];
result = result.map((r) => Object.entries(r).reduce((acc, [key, val]) => {
if (select.includes(key))
acc[key] = val;
return acc;
}, {}));
}
if (docsOnly)
return result;
return {
data: result,
count: count,
};
}
/**
* @description Parses and converts errors to appropriate types
* @summary Ensures that errors are of the correct type for consistent error handling.
* If the error is already a BaseError, it's returned as is; otherwise, it's wrapped in an InternalError.
* @template V - The expected error type, extending BaseError
* @param {Error} err - The error to parse
* @return {V} The parsed error of the expected type
*/
parseError(err) {
if (err instanceof db_decorators_1.BaseError)
return err;
return new db_decorators_1.InternalError(err);
}
/**
* @description Creates a new statement builder for queries
* @summary Factory method that creates a new RamStatement instance for building queries.
* This method allows for fluent query construction against the RAM adapter.
* @template M - The model type for the statement
* @return {RamStatement<M, any>} A new statement builder instance
*/
Statement(overrides) {
return new RamStatement_1.RamStatement(this, overrides);
}
Paginator(query, size, clazz) {
return new RamPaginator_1.RamPaginator(this, query, size, clazz);
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
for(config, ...args) {
if (!this.proxies)
this.proxies = {};
const key = `${this.alias} - ${(0, decorator_validation_1.hashObj)(config)}`;
if (key in this.proxies)
return this.proxies[key];
const proxy = new Proxy(this, {
get: (target, p, receiver) => {
if (p === "_config") {
const originalConf = Reflect.get(target, p, receiver);
return Object.assign({}, originalConf, config);
}
return Reflect.get(target, p, receiver);
},
});
this.proxies[key] = proxy;
return proxy;
}
/**
* @description Sets up RAM-specific decorations for model properties
* @summary Configures decorations for createdBy and updatedBy fields in the RAM adapter.
* This static method is called during initialization to set up handlers that automatically
* populate these fields with the current user's UUID during create and update operations.
* @return {void}
* @mermaid
* sequenceDiagram
* participant RamAdapter
* participant Decoration
* participant Repository
*
* RamAdapter->>Repository: key(PersistenceKeys.CREATED_BY)
* Repository-->>RamAdapter: createdByKey
* RamAdapter->>Repository: key(PersistenceKeys.UPDATED_BY)
* Repository-->>RamAdapter: updatedByKey
*
* RamAdapter->>Decoration: flavouredAs(RamFlavour)
* Decoration-->>RamAdapter: DecoratorBuilder
* RamAdapter->>Decoration: for(createdByKey)
* RamAdapter->>Decoration: define(onCreate, propMetadata)
* RamAdapter->>Decoration: apply()
*
* RamAdapter->>Decoration: flavouredAs(RamFlavour)
* Decoration-->>RamAdapter: DecoratorBuilder
* RamAdapter->>Decoration: for(updatedByKey)
* RamAdapter->>Decoration: define(onCreate, propMetadata)
* RamAdapter->>Decoration: apply()
*/
static decoration() {
super.decoration();
const createdByKey = persistence_1.PersistenceKeys.CREATED_BY;
const updatedByKey = persistence_1.PersistenceKeys.UPDATED_BY;
decoration_1.Decoration.flavouredAs(constants_1.RamFlavour)
.for(createdByKey)
.define((0, db_decorators_1.onCreate)(handlers_1.createdByOnRamCreateUpdate), (0, decoration_1.propMetadata)(createdByKey, {}))
.apply();
decoration_1.Decoration.flavouredAs(constants_1.RamFlavour)
.for(updatedByKey)
.define((0, db_decorators_1.onCreateUpdate)(handlers_1.createdByOnRamCreateUpdate), (0, decoration_1.propMetadata)(updatedByKey, {}))
.apply();
}
getClient() {
return new Map();
}
}
exports.RamAdapter = RamAdapter;
//# sourceMappingURL=RamAdapter.js.map