import { RamFlags, RawRamQuery, RamStorage, RamRepository } from "./types";
import { RamStatement } from "./RamStatement";
import { RamContext } from "./RamContext";
import { Repository } from "../repository/Repository";
import { Adapter, PersistenceKeys, Sequence } from "../persistence";
import { SequenceOptions } from "../interfaces";
import { Lock } from "@decaf-ts/transactional-decorators";
import {
Constructor,
Decoration,
Model,
propMetadata,
} from "@decaf-ts/decorator-validation";
import {
BaseError,
ConflictError,
findPrimaryKey,
InternalError,
NotFoundError,
onCreate,
OperationKeys,
} from "@decaf-ts/db-decorators";
import { RamSequence } from "./RamSequence";
import { createdByOnRamCreateUpdate } from "./handlers";
import { RamFlavour } from "./constants";
/**
* @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
*/
export class RamAdapter extends Adapter<
RamStorage,
RawRamQuery<any>,
RamFlags,
RamContext
> {
constructor(alias?: string) {
super(new Map<string, Map<string, any>>(), RamFlavour, alias);
}
/**
* @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
*/
override repository<M extends Model>(): Constructor<RamRepository<M>> {
return super.repository<M>() as Constructor<RamRepository<M>>;
}
/**
* @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 {RamFlags} Complete flags with UUID
*/
override flags<M extends Model>(
operation: OperationKeys,
model: Constructor<M>,
flags: Partial<RamFlags>
): RamFlags {
return Object.assign(super.flags(operation, model, flags), {
UUID: crypto.randomUUID(),
}) as RamFlags;
}
override Context = RamContext;
private indexes: Record<
string,
Record<string | number, Record<string, any>>
> = {};
private lock = new Lock();
/**
* @description Initializes the RAM adapter
* @summary A no-op initialization method for the RAM adapter.
* Since RAM adapter doesn't require any setup, this method simply resolves immediately.
* @param {...any[]} args - Initialization arguments (unused)
* @return {Promise<void>} A promise that resolves when initialization is complete
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async initialize(...args: any[]): Promise<void> {
return Promise.resolve(undefined);
}
/**
* @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: Record<string, any>[]): Promise<any> {
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
*/
override prepare<M extends Model>(
model: M,
pk: keyof M
): { record: Record<string, any>; id: string } {
const prepared = super.prepare(model, pk);
delete prepared.record[pk as string];
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 {string | Constructor<M>} clazz - The model class or name
* @param pk - The primary key property name
* @param {string | number} id - The primary key value
* @return {M} The reconstructed model instance
*/
override revert<M extends Model>(
obj: Record<string, any>,
clazz: string | Constructor<M>,
pk: keyof M,
id: string | number
): M {
const res = super.revert(obj, clazz, pk, id);
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} tableName - 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(
tableName: string,
id: string | number,
model: Record<string, any>
): Promise<Record<string, any>> {
await this.lock.acquire();
if (!this.native.has(tableName)) this.native.set(tableName, new Map());
if (this.native.get(tableName) && this.native.get(tableName)?.has(id))
throw new ConflictError(
`Record with id ${id} already exists in table ${tableName}`
);
this.native.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 {string} tableName - The name of the table to retrieve from
* @param {string | number} 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(
tableName: string,
id: string | number
): Promise<Record<string, any>> {
if (!this.native.has(tableName))
throw new NotFoundError(`Table ${tableName} not found`);
if (!this.native.get(tableName)?.has(id))
throw new NotFoundError(
`Record with id ${id} not found in table ${tableName}`
);
return this.native.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(
tableName: string,
id: string | number,
model: Record<string, any>
): Promise<Record<string, any>> {
await this.lock.acquire();
if (!this.native.has(tableName))
throw new NotFoundError(`Table ${tableName} not found`);
if (!this.native.get(tableName)?.has(id))
throw new NotFoundError(
`Record with id ${id} not found in table ${tableName}`
);
this.native.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(
tableName: string,
id: string | number
): Promise<Record<string, any>> {
await this.lock.acquire();
if (!this.native.has(tableName))
throw new NotFoundError(`Table ${tableName} not found`);
if (!this.native.get(tableName)?.has(id))
throw new NotFoundError(
`Record with id ${id} not found in table ${tableName}`
);
const natived = this.native.get(tableName)?.get(id);
this.native.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
*/
protected tableFor<M extends Model>(from: string | Constructor<M>) {
if (typeof from === "string") from = Model.get(from) as Constructor<M>;
const table = Repository.table(from);
if (!this.native.has(table)) this.native.set(table, new Map());
return this.native.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<R>(rawInput: RawRamQuery<any>): Promise<R> {
const { where, sort, limit, skip, from } = rawInput;
let { select } = rawInput;
const collection = this.tableFor(from);
if (!collection)
throw new InternalError(`Table ${from} not found in RamAdapter`);
const { id, props } = findPrimaryKey(new from());
let result: any[] = Array.from(collection.entries()).map(([pk, r]) =>
this.revert(
r,
from,
id as any,
Sequence.parseValue(props.type as any, pk as string) as string
)
);
result = where ? result.filter(where) : result;
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: Record<string, any>, [key, val]) => {
if ((select as string[]).includes(key)) acc[key] = val;
return acc;
}, {})
);
}
return result as unknown as R;
}
/**
* @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<V extends BaseError>(err: Error): V {
if (err instanceof BaseError) return err as V;
return new InternalError(err) as V;
}
/**
* @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<M extends Model>(): RamStatement<M, any> {
return new RamStatement<M, any>(this as any);
}
/**
* @description Creates a new sequence for generating sequential IDs
* @summary Factory method that creates a new RamSequence instance for ID generation.
* This method provides a way to create auto-incrementing sequences for entity IDs.
* @param {SequenceOptions} options - Configuration options for the sequence
* @return {Promise<Sequence>} A promise that resolves to the new sequence instance
*/
async Sequence(options: SequenceOptions): Promise<Sequence> {
return new RamSequence(options, this);
}
/**
* @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() {
const createdByKey = Repository.key(PersistenceKeys.CREATED_BY);
const updatedByKey = Repository.key(PersistenceKeys.UPDATED_BY);
Decoration.flavouredAs(RamFlavour)
.for(createdByKey)
.define(
onCreate(createdByOnRamCreateUpdate),
propMetadata(createdByKey, {})
)
.apply();
Decoration.flavouredAs(RamFlavour)
.for(updatedByKey)
.define(
onCreate(createdByOnRamCreateUpdate),
propMetadata(updatedByKey, {})
)
.apply();
}
}
RamAdapter.decoration();
Source