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",
|
||||
"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": {
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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<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 () => {
|
||||
expect(InMemoryRepository).toBeDefined();
|
||||
});
|
||||
|
||||
it("should create something", async () => {
|
||||
const repo = new InMemoryRepository<Test>();
|
||||
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<TestModel> = {
|
||||
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<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 () => {
|
||||
const repo = new InMemoryRepository<Test>();
|
||||
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<Test>();
|
||||
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<Test>();
|
||||
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<Test>();
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -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<TModel extends IdModel>
|
||||
implements IRepository<TModel>
|
||||
{
|
||||
private source: TModel[] = [];
|
||||
|
||||
getBy(filter: (model: TModel) => boolean): Promise<TModel[]> {
|
||||
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<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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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[]> {
|
||||
for (const model of models) {
|
||||
if (!model?.id) {
|
||||
@ -30,6 +77,13 @@ export class InMemoryRepository<TModel extends IdModel>
|
||||
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> {
|
||||
this.source = this.source.filter((x) => !filter(x));
|
||||
return Promise.resolve(true);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user