update repository implementation
This commit is contained in:
parent
f3a82bb27e
commit
475dec8465
12
package-lock.json
generated
12
package-lock.json
generated
@ -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": {
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
@ -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 correctly filter models", async () => {
|
||||||
|
const query: IQuery<TestModel> = {
|
||||||
|
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<TestModel> = {
|
||||||
|
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<TestModel> = {
|
||||||
|
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<TestModel> = {
|
||||||
|
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<TestModel> = {
|
||||||
|
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 () => {
|
describe("save", () => {
|
||||||
const repo = new InMemoryRepository<Test>();
|
it("should add a new model to the repository and generate an ID if none is provided", async () => {
|
||||||
const created = await repo.save([{ id: "", name: "test" }]);
|
const newModel = { id: "", name: "Epsilon", value: 50 };
|
||||||
expect(created).toBeDefined();
|
const result = await repository.save([newModel]);
|
||||||
expect(created.length).toBeGreaterThan(0);
|
|
||||||
expect(created[0].id).toBeDefined();
|
expect(result.length).toBe(initialData.length + 1);
|
||||||
expect(created[0].id.length).toBeGreaterThan(3);
|
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 () => {
|
describe("deleteBy", () => {
|
||||||
const repo = new InMemoryRepository<Test>();
|
it("should delete models that match the filter", async () => {
|
||||||
const created = await repo.save([{ id: "2", name: "test2" }]);
|
const success = await repository.deleteBy((model) => model.value >= 30);
|
||||||
expect(created).toBeDefined();
|
expect(success).toBe(true);
|
||||||
expect(created.length).toBeGreaterThan(0);
|
|
||||||
|
|
||||||
created[0].name = "test3";
|
const remainingModels = await repository.getBy({
|
||||||
const saved = await repo.save([created[0]]);
|
filter: () => true,
|
||||||
expect(saved).toBeDefined();
|
skip: 0,
|
||||||
expect(saved[0].name).toBe("test3");
|
limit: 0,
|
||||||
});
|
sort: [],
|
||||||
|
});
|
||||||
|
|
||||||
it("should select something", async () => {
|
expect(remainingModels.length).toBe(2);
|
||||||
const repo = new InMemoryRepository<Test>();
|
expect(remainingModels.map((m) => m.id)).toEqual(["1", "2"]);
|
||||||
const created = await repo.save([{ id: "4", name: "test4" }]);
|
});
|
||||||
expect(created).toBeDefined();
|
|
||||||
expect(created.length).toBeGreaterThan(0);
|
|
||||||
|
|
||||||
const selected = await repo.getBy((x) => x.id === "4");
|
it("should not delete any models if the filter does not match", async () => {
|
||||||
expect(selected).toBeDefined();
|
const success = await repository.deleteBy(
|
||||||
expect(selected.length).toBe(1);
|
(model) => model.name === "Zeta"
|
||||||
expect(selected[0].id).toBe("4");
|
);
|
||||||
expect(selected[0].name).toBe("test4");
|
expect(success).toBe(true);
|
||||||
});
|
|
||||||
|
|
||||||
it("should delete something", async () => {
|
const remainingModels = await repository.getBy({
|
||||||
const repo = new InMemoryRepository<Test>();
|
filter: () => true,
|
||||||
const created = await repo.save([{ id: "5", name: "test5" }]);
|
skip: 0,
|
||||||
expect(created).toBeDefined();
|
limit: 0,
|
||||||
expect(created.length).toBeGreaterThan(0);
|
sort: [],
|
||||||
|
});
|
||||||
expect(await repo.deleteBy((x) => x.id === "5")).toBeTruthy();
|
expect(remainingModels.length).toBe(initialData.length);
|
||||||
const selected = await repo.getBy((x) => x.id === "5");
|
});
|
||||||
expect(selected).toBeDefined();
|
|
||||||
expect(selected.length).toBe(0);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user