diff --git a/package-lock.json b/package-lock.json index e663a8b..101e9ce 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,15 +1,15 @@ { "name": "@apihub24/in-memory-repository", - "version": "1.0.2", + "version": "2.0.0-alpha.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@apihub24/in-memory-repository", - "version": "1.0.2", + "version": "2.0.0-alpha.0", "license": "MIT", "dependencies": { - "@apihub24/repository": "^1.0.3" + "@apihub24/repository": "2.0.0-alpha.0" }, "devDependencies": { "@nestjs/testing": "^11.1.6", @@ -35,9 +35,9 @@ } }, "node_modules/@apihub24/repository": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@apihub24/repository/-/repository-1.0.3.tgz", - "integrity": "sha512-m2twcVPrdnKAcnNQFabGzQ/18/kQUEtuqAuSzVBTEc3mxBKBQ5ex1+Cx4JP/sZ1HqdS4GisFXDa8zfrnpdcLaA==", + "version": "2.0.0-alpha.0", + "resolved": "https://registry.npmjs.org/@apihub24/repository/-/repository-2.0.0-alpha.0.tgz", + "integrity": "sha512-Jd+YF4cWUgDxWPdjXnA5nw0BtaDV9sNRkeaq+LFCZtFkLGcUJHbNh32SBrVDRtiAt6pvO0DuBS9nM6wzoxBRvg==", "license": "MIT" }, "node_modules/@babel/code-frame": { diff --git a/package.json b/package.json index 0ec7860..8b3f45d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@apihub24/in-memory-repository", - "version": "1.0.2", + "version": "2.0.0-alpha.0", "description": "", "main": "dist/index.js", "types": "./dist/index.d.ts", @@ -12,7 +12,7 @@ "rel": "npm i && npm run build && npm run test:coverage" }, "dependencies": { - "@apihub24/repository": "^1.0.3" + "@apihub24/repository": "2.0.0-alpha.0" }, "devDependencies": { "rimraf": "^6.0.1", diff --git a/src/in.memory.repository.spec.ts b/src/in.memory.repository.spec.ts index d8302a4..0b9abed 100644 --- a/src/in.memory.repository.spec.ts +++ b/src/in.memory.repository.spec.ts @@ -1,67 +1,177 @@ +import { IQuery } from "@apihub24/repository"; import { InMemoryRepository } from "./"; -class Test { +interface TestModel { id: string; name: string; + value: number; } describe("InMemoryRepository Tests", () => { + let repository: InMemoryRepository; + let initialData: TestModel[]; + + beforeEach(() => { + repository = new InMemoryRepository(); + 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 () => { expect(InMemoryRepository).toBeDefined(); }); - it("should create something", async () => { - const repo = new InMemoryRepository(); - const created = await repo.save([{ id: "1", name: "test" }]); - expect(created).toBeDefined(); - expect(created.length).toBeGreaterThan(0); - expect(created[0].id).toBe("1"); - expect(created[0].name).toBe("test"); + describe("getBy", () => { + it("should return all models when no options are provided", async () => { + const query: IQuery = { + filter: () => true, + skip: 0, + limit: 0, + sort: [], + }; + const result = await repository.getBy(query); + expect(result).toEqual(initialData); + }); + + it("should correctly filter models", async () => { + const query: IQuery = { + filter: (model) => model.value > 25, + skip: 0, + limit: 0, + sort: [], + }; + const result = await repository.getBy(query); + expect(result).toEqual([ + { id: "3", name: "Gamma", value: 30 }, + { id: "4", name: "Delta", value: 40 }, + ]); + }); + + it("should correctly sort models in ascending order", async () => { + const query: IQuery = { + filter: () => true, + skip: 0, + limit: 0, + sort: [{ key: (model) => model.name, option: "asc" }], + }; + const result = await repository.getBy(query); + expect(result.map((m) => m.name)).toEqual([ + "Alpha", + "Beta", + "Delta", + "Gamma", + ]); + }); + + it("should correctly sort models in descending order", async () => { + const query: IQuery = { + filter: () => true, + skip: 0, + limit: 0, + sort: [{ key: (model) => model.value, option: "desc" }], + }; + const result = await repository.getBy(query); + expect(result.map((m) => m.value)).toEqual([40, 30, 20, 10]); + }); + + it("should apply skip and limit for pagination", async () => { + const query: IQuery = { + filter: () => true, + skip: 1, + 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 }, + ]); + }); + + it("should correctly handle a combined query with filter, sort, and pagination", async () => { + const query: IQuery = { + filter: (model) => model.value > 10, + 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 }]); + }); }); - it("should generate id", async () => { - const repo = new InMemoryRepository(); - const created = await repo.save([{ id: "", name: "test" }]); - expect(created).toBeDefined(); - expect(created.length).toBeGreaterThan(0); - expect(created[0].id).toBeDefined(); - expect(created[0].id.length).toBeGreaterThan(3); + 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"); + }); }); - it("should update something", async () => { - const repo = new InMemoryRepository(); - const created = await repo.save([{ id: "2", name: "test2" }]); - expect(created).toBeDefined(); - expect(created.length).toBeGreaterThan(0); + describe("deleteBy", () => { + it("should delete models that match the filter", async () => { + const success = await repository.deleteBy((model) => model.value >= 30); + expect(success).toBe(true); - created[0].name = "test3"; - const saved = await repo.save([created[0]]); - expect(saved).toBeDefined(); - expect(saved[0].name).toBe("test3"); - }); + const remainingModels = await repository.getBy({ + filter: () => true, + skip: 0, + limit: 0, + sort: [], + }); - it("should select something", async () => { - const repo = new InMemoryRepository(); - const created = await repo.save([{ id: "4", name: "test4" }]); - expect(created).toBeDefined(); - expect(created.length).toBeGreaterThan(0); + expect(remainingModels.length).toBe(2); + expect(remainingModels.map((m) => m.id)).toEqual(["1", "2"]); + }); - const selected = await repo.getBy((x) => x.id === "4"); - expect(selected).toBeDefined(); - expect(selected.length).toBe(1); - expect(selected[0].id).toBe("4"); - expect(selected[0].name).toBe("test4"); - }); + 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); - it("should delete something", async () => { - const repo = new InMemoryRepository(); - const created = await repo.save([{ id: "5", name: "test5" }]); - expect(created).toBeDefined(); - expect(created.length).toBeGreaterThan(0); - - expect(await repo.deleteBy((x) => x.id === "5")).toBeTruthy(); - const selected = await repo.getBy((x) => x.id === "5"); - expect(selected).toBeDefined(); - expect(selected.length).toBe(0); + const remainingModels = await repository.getBy({ + filter: () => true, + skip: 0, + limit: 0, + sort: [], + }); + expect(remainingModels.length).toBe(initialData.length); + }); }); }); diff --git a/src/in.memory.repository.ts b/src/in.memory.repository.ts index 154cb70..d097cd8 100644 --- a/src/in.memory.repository.ts +++ b/src/in.memory.repository.ts @@ -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 { 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 implements IRepository { private source: TModel[] = []; - getBy(filter: (model: TModel) => boolean): Promise { - const result: TModel[] = []; - for (const s of this.source) { - if (filter(s)) { - result.push(s); - } + /** + * Retrieves models from the repository based on the provided query options. + * It applies a filter, sorts the results, and then handles pagination (skip and limit). + * The order of operations is crucial: filter -> sort -> slice. + * + * @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): Promise { + 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); } + /** + * 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 { for (const model of models) { if (!model?.id) { @@ -30,6 +77,13 @@ export class InMemoryRepository 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 { this.source = this.source.filter((x) => !filter(x)); return Promise.resolve(true);