update repository implementation

This commit is contained in:
admin 2025-08-25 00:03:51 +02:00
parent f3a82bb27e
commit 475dec8465
4 changed files with 225 additions and 61 deletions

12
package-lock.json generated
View File

@ -1,15 +1,15 @@
{ {
"name": "@apihub24/in-memory-repository", "name": "@apihub24/in-memory-repository",
"version": "1.0.2", "version": "2.0.0-alpha.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "@apihub24/in-memory-repository", "name": "@apihub24/in-memory-repository",
"version": "1.0.2", "version": "2.0.0-alpha.0",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@apihub24/repository": "^1.0.3" "@apihub24/repository": "2.0.0-alpha.0"
}, },
"devDependencies": { "devDependencies": {
"@nestjs/testing": "^11.1.6", "@nestjs/testing": "^11.1.6",
@ -35,9 +35,9 @@
} }
}, },
"node_modules/@apihub24/repository": { "node_modules/@apihub24/repository": {
"version": "1.0.3", "version": "2.0.0-alpha.0",
"resolved": "https://registry.npmjs.org/@apihub24/repository/-/repository-1.0.3.tgz", "resolved": "https://registry.npmjs.org/@apihub24/repository/-/repository-2.0.0-alpha.0.tgz",
"integrity": "sha512-m2twcVPrdnKAcnNQFabGzQ/18/kQUEtuqAuSzVBTEc3mxBKBQ5ex1+Cx4JP/sZ1HqdS4GisFXDa8zfrnpdcLaA==", "integrity": "sha512-Jd+YF4cWUgDxWPdjXnA5nw0BtaDV9sNRkeaq+LFCZtFkLGcUJHbNh32SBrVDRtiAt6pvO0DuBS9nM6wzoxBRvg==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@babel/code-frame": { "node_modules/@babel/code-frame": {

View File

@ -1,6 +1,6 @@
{ {
"name": "@apihub24/in-memory-repository", "name": "@apihub24/in-memory-repository",
"version": "1.0.2", "version": "2.0.0-alpha.0",
"description": "", "description": "",
"main": "dist/index.js", "main": "dist/index.js",
"types": "./dist/index.d.ts", "types": "./dist/index.d.ts",
@ -12,7 +12,7 @@
"rel": "npm i && npm run build && npm run test:coverage" "rel": "npm i && npm run build && npm run test:coverage"
}, },
"dependencies": { "dependencies": {
"@apihub24/repository": "^1.0.3" "@apihub24/repository": "2.0.0-alpha.0"
}, },
"devDependencies": { "devDependencies": {
"rimraf": "^6.0.1", "rimraf": "^6.0.1",

View File

@ -1,67 +1,177 @@
import { IQuery } from "@apihub24/repository";
import { InMemoryRepository } from "./"; import { InMemoryRepository } from "./";
class Test { interface TestModel {
id: string; id: string;
name: string; name: string;
value: number;
} }
describe("InMemoryRepository Tests", () => { describe("InMemoryRepository Tests", () => {
let repository: InMemoryRepository<TestModel>;
let initialData: TestModel[];
beforeEach(() => {
repository = new InMemoryRepository<TestModel>();
initialData = [
{ id: "1", name: "Alpha", value: 10 },
{ id: "2", name: "Beta", value: 20 },
{ id: "3", name: "Gamma", value: 30 },
{ id: "4", name: "Delta", value: 40 },
];
return repository.save(initialData);
});
it("should export InMemoryRepository", async () => { it("should export InMemoryRepository", async () => {
expect(InMemoryRepository).toBeDefined(); expect(InMemoryRepository).toBeDefined();
}); });
it("should create something", async () => { describe("getBy", () => {
const repo = new InMemoryRepository<Test>(); it("should return all models when no options are provided", async () => {
const created = await repo.save([{ id: "1", name: "test" }]); const query: IQuery<TestModel> = {
expect(created).toBeDefined(); filter: () => true,
expect(created.length).toBeGreaterThan(0); skip: 0,
expect(created[0].id).toBe("1"); limit: 0,
expect(created[0].name).toBe("test"); sort: [],
};
const result = await repository.getBy(query);
expect(result).toEqual(initialData);
}); });
it("should generate id", async () => { it("should correctly filter models", async () => {
const repo = new InMemoryRepository<Test>(); const query: IQuery<TestModel> = {
const created = await repo.save([{ id: "", name: "test" }]); filter: (model) => model.value > 25,
expect(created).toBeDefined(); skip: 0,
expect(created.length).toBeGreaterThan(0); limit: 0,
expect(created[0].id).toBeDefined(); sort: [],
expect(created[0].id.length).toBeGreaterThan(3); };
const result = await repository.getBy(query);
expect(result).toEqual([
{ id: "3", name: "Gamma", value: 30 },
{ id: "4", name: "Delta", value: 40 },
]);
}); });
it("should update something", async () => { it("should correctly sort models in ascending order", async () => {
const repo = new InMemoryRepository<Test>(); const query: IQuery<TestModel> = {
const created = await repo.save([{ id: "2", name: "test2" }]); filter: () => true,
expect(created).toBeDefined(); skip: 0,
expect(created.length).toBeGreaterThan(0); limit: 0,
sort: [{ key: (model) => model.name, option: "asc" }],
created[0].name = "test3"; };
const saved = await repo.save([created[0]]); const result = await repository.getBy(query);
expect(saved).toBeDefined(); expect(result.map((m) => m.name)).toEqual([
expect(saved[0].name).toBe("test3"); "Alpha",
"Beta",
"Delta",
"Gamma",
]);
}); });
it("should select something", async () => { it("should correctly sort models in descending order", async () => {
const repo = new InMemoryRepository<Test>(); const query: IQuery<TestModel> = {
const created = await repo.save([{ id: "4", name: "test4" }]); filter: () => true,
expect(created).toBeDefined(); skip: 0,
expect(created.length).toBeGreaterThan(0); limit: 0,
sort: [{ key: (model) => model.value, option: "desc" }],
const selected = await repo.getBy((x) => x.id === "4"); };
expect(selected).toBeDefined(); const result = await repository.getBy(query);
expect(selected.length).toBe(1); expect(result.map((m) => m.value)).toEqual([40, 30, 20, 10]);
expect(selected[0].id).toBe("4");
expect(selected[0].name).toBe("test4");
}); });
it("should delete something", async () => { it("should apply skip and limit for pagination", async () => {
const repo = new InMemoryRepository<Test>(); const query: IQuery<TestModel> = {
const created = await repo.save([{ id: "5", name: "test5" }]); filter: () => true,
expect(created).toBeDefined(); skip: 1,
expect(created.length).toBeGreaterThan(0); limit: 2,
sort: [{ key: (model) => model.value, option: "asc" }],
};
const result = await repository.getBy(query);
expect(result).toEqual([
{ id: "2", name: "Beta", value: 20 },
{ id: "3", name: "Gamma", value: 30 },
]);
});
expect(await repo.deleteBy((x) => x.id === "5")).toBeTruthy(); it("should correctly handle a combined query with filter, sort, and pagination", async () => {
const selected = await repo.getBy((x) => x.id === "5"); const query: IQuery<TestModel> = {
expect(selected).toBeDefined(); filter: (model) => model.value > 10,
expect(selected.length).toBe(0); skip: 1,
limit: 1,
sort: [{ key: (model) => model.value, option: "desc" }],
};
const result = await repository.getBy(query);
expect(result).toEqual([{ id: "3", name: "Gamma", value: 30 }]);
});
});
describe("save", () => {
it("should add a new model to the repository and generate an ID if none is provided", async () => {
const newModel = { id: "", name: "Epsilon", value: 50 };
const result = await repository.save([newModel]);
expect(result.length).toBe(initialData.length + 1);
const savedModel = result.find((m) => m.name === "Epsilon");
expect(savedModel).toBeDefined();
expect(savedModel?.id).toBeDefined();
expect(savedModel?.id).not.toBe("");
});
it("should update an existing model with the same ID", async () => {
const updatedModel = { id: "2", name: "Updated Beta", value: 25 };
await repository.save([updatedModel]);
const result = await repository.getBy({
filter: (model) => model.id === "2",
skip: 0,
limit: 0,
sort: [],
});
expect(result.length).toBe(1);
expect(result[0]).toEqual(updatedModel);
});
it("should handle saving multiple models at once", async () => {
const newModels = [
{ id: "", name: "Epsilon", value: 50 },
{ id: "1", name: "Updated Alpha", value: 15 },
];
const result = await repository.save(newModels);
expect(result.length).toBe(initialData.length + 1); // 4 initial + 1 new = 5
expect(result.find((m) => m.name === "Epsilon")).toBeDefined();
expect(result.find((m) => m.id === "1")?.name).toBe("Updated Alpha");
});
});
describe("deleteBy", () => {
it("should delete models that match the filter", async () => {
const success = await repository.deleteBy((model) => model.value >= 30);
expect(success).toBe(true);
const remainingModels = await repository.getBy({
filter: () => true,
skip: 0,
limit: 0,
sort: [],
});
expect(remainingModels.length).toBe(2);
expect(remainingModels.map((m) => m.id)).toEqual(["1", "2"]);
});
it("should not delete any models if the filter does not match", async () => {
const success = await repository.deleteBy(
(model) => model.name === "Zeta"
);
expect(success).toBe(true);
const remainingModels = await repository.getBy({
filter: () => true,
skip: 0,
limit: 0,
sort: [],
});
expect(remainingModels.length).toBe(initialData.length);
});
}); });
}); });

View File

@ -1,24 +1,71 @@
import { IRepository } from "@apihub24/repository"; import { IQuery, IRepository } from "@apihub24/repository";
/**
* Interface to ensure that a model has a unique 'id' field of type string.
* This is a prerequisite for models managed by the InMemoryRepository.
*/
interface IdModel { interface IdModel {
id: string; id: string;
} }
/**
* A simple, in-memory repository implementation that conforms to the IRepository interface.
* It stores data in a private array and provides methods for filtering, sorting,
* paginating, saving, and deleting models. This is ideal for lightweight applications,
* testing, or scenarios where a persistent database is not required.
*
* @template TModel The type of the model to be stored. Must extend IdModel.
*/
export class InMemoryRepository<TModel extends IdModel> export class InMemoryRepository<TModel extends IdModel>
implements IRepository<TModel> implements IRepository<TModel>
{ {
private source: TModel[] = []; private source: TModel[] = [];
getBy(filter: (model: TModel) => boolean): Promise<TModel[]> { /**
const result: TModel[] = []; * Retrieves models from the repository based on the provided query options.
for (const s of this.source) { * It applies a filter, sorts the results, and then handles pagination (skip and limit).
if (filter(s)) { * The order of operations is crucial: filter -> sort -> slice.
result.push(s); *
* @param options The IQuery object containing the filter, sort, skip, and limit criteria.
* @returns A Promise that resolves with an array of the matching TModel objects.
*/
getBy(options: IQuery<TModel>): Promise<TModel[]> {
let result: TModel[] = this.source.filter(options.filter);
if (options.sort && options.sort.length > 0) {
result.sort((a, b) => {
for (const sortOption of options.sort) {
const keyA = sortOption.key(a);
const keyB = sortOption.key(b);
if (keyA < keyB) {
return sortOption.option === "asc" ? -1 : 1;
} }
if (keyA > keyB) {
return sortOption.option === "asc" ? 1 : -1;
}
}
return 0;
});
}
if (options.skip > 0) {
result = result.slice(options.skip);
}
if (options.limit > 0) {
result = result.slice(0, options.limit);
} }
return Promise.resolve(result); return Promise.resolve(result);
} }
/**
* Saves an array of models to the repository.
* If a model doesn't have an 'id', a new UUID is generated for it.
* Existing models are updated (replaced by ID), and new models are added.
*
* @param models An array of TModel objects to be saved.
* @returns A Promise that resolves with the entire updated repository source.
*/
save(models: TModel[]): Promise<TModel[]> { save(models: TModel[]): Promise<TModel[]> {
for (const model of models) { for (const model of models) {
if (!model?.id) { if (!model?.id) {
@ -30,6 +77,13 @@ export class InMemoryRepository<TModel extends IdModel>
return Promise.resolve(this.source); return Promise.resolve(this.source);
} }
/**
* Deletes models from the repository based on a filter function.
* All models for which the filter returns true will be removed.
*
* @param filter A function that returns `true` for models that should be deleted.
* @returns A Promise that resolves to `true` upon successful deletion.
*/
deleteBy(filter: (model: TModel) => boolean): Promise<boolean> { deleteBy(filter: (model: TModel) => boolean): Promise<boolean> {
this.source = this.source.filter((x) => !filter(x)); this.source = this.source.filter((x) => !filter(x));
return Promise.resolve(true); return Promise.resolve(true);