This commit is contained in:
Bastian Wagner
2026-06-09 09:45:33 +02:00
commit 537c7cbbee
124 changed files with 27283 additions and 0 deletions

View File

@@ -0,0 +1,117 @@
type WhereClause<T> = Partial<Record<keyof T, unknown>>;
export class InMemoryRepository<T extends object> {
private readonly records = new Map<string, T>();
constructor(
private readonly keyFields: Array<keyof T & string> = [
'id' as keyof T & string,
'jti' as keyof T & string,
'ownerId' as keyof T & string,
],
) {}
create(entity: Partial<T>): T {
return entity as T;
}
async save(entityOrEntities: T | T[]): Promise<T | T[]> {
if (Array.isArray(entityOrEntities)) {
return Promise.all(entityOrEntities.map((entity) => this.saveOne(entity)));
}
return this.saveOne(entityOrEntities);
}
async find(options: { where?: WhereClause<T>; order?: unknown } = {}): Promise<T[]> {
const records = [...this.records.values()].filter((record) =>
this.matchesWhere(record, options.where),
);
return this.applyOrder(records, options.order);
}
async findOne(options: { where?: WhereClause<T>; order?: unknown }): Promise<T | null> {
const [record] = await this.find(options);
return record ?? null;
}
async delete(criteria: WhereClause<T>): Promise<void> {
for (const [key, record] of this.records.entries()) {
if (this.matchesWhere(record, criteria)) {
this.records.delete(key);
}
}
}
async remove(entityOrEntities: T | T[]): Promise<T | T[]> {
if (Array.isArray(entityOrEntities)) {
entityOrEntities.forEach((entity) => this.records.delete(this.keyOf(entity)));
return entityOrEntities;
}
this.records.delete(this.keyOf(entityOrEntities));
return entityOrEntities;
}
private saveOne(entity: T): T {
const now = new Date();
const timestamped = entity as T & { createdAt?: Date; updatedAt?: Date };
timestamped.createdAt ??= now;
timestamped.updatedAt = now;
this.records.set(this.keyOf(entity), entity);
return entity;
}
private keyOf(entity: T): string {
for (const keyField of this.keyFields) {
const value = (entity as Record<string, unknown>)[keyField];
if (typeof value === 'string' && value) {
return value;
}
}
throw new Error('Entity has no supported in-memory repository key.');
}
private matchesWhere(record: T, where?: WhereClause<T>): boolean {
if (!where) {
return true;
}
return Object.entries(where).every(([key, value]) => {
return (record as Record<string, unknown>)[key] === value;
});
}
private applyOrder(records: T[], order: unknown): T[] {
const sortedRecords = [...records];
const typedOrder = order as { name?: 'ASC' | 'DESC'; items?: { position?: 'ASC' | 'DESC' } };
if (typedOrder?.name) {
sortedRecords.sort((left, right) => {
const leftName = String((left as Record<string, unknown>)['name'] ?? '');
const rightName = String((right as Record<string, unknown>)['name'] ?? '');
return typedOrder.name === 'DESC'
? rightName.localeCompare(leftName)
: leftName.localeCompare(rightName);
});
}
if (typedOrder?.items?.position) {
for (const record of sortedRecords) {
const items = (record as { items?: Array<{ position: number }> }).items;
items?.sort((left, right) =>
typedOrder.items?.position === 'DESC'
? right.position - left.position
: left.position - right.position,
);
}
}
return sortedRecords;
}
}