Initial
This commit is contained in:
117
listify-api/src/testing/in-memory-repository.ts
Normal file
117
listify-api/src/testing/in-memory-repository.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user