diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d2e47b8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,16 @@ +# IDEA + +.idea/ +*.iml + +# JS library + +coverage/ +lib/ +.parcel-cache/ +.yarn/ +.yarnrc* +yarn-error.log +.pnp* + +yarn.lock diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 0000000..c66b70d --- /dev/null +++ b/jest.config.js @@ -0,0 +1,9 @@ +/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */ +module.exports = { + preset: "ts-jest", + testEnvironment: "node", + + roots: [ + "./tests", + ], +}; \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..7e01894 --- /dev/null +++ b/package.json @@ -0,0 +1,34 @@ +{ + "name": "@sharkitek/repositories", + "version": "1.0.0", + "description": "Sharkitek models repositories extension.", + "repository": "https://git.madeorsk.com/Sharkitek/repositories", + "author": "Madeorsk ", + "license": "MIT", + "scripts": { + "build": "parcel build", + "dev": "parcel watch", + "test": "jest" + }, + "main": "lib/index.js", + "source": "src/index.ts", + "types": "lib/index.d.ts", + "files": [ + "lib/**/*" + ], + "dependencies": { + "@sharkitek/core": "^1.0.1", + "reflect-metadata": "^0.1.13" + }, + "devDependencies": { + "@parcel/packager-ts": "2.6.2", + "@parcel/transformer-typescript-types": "2.6.2", + "@types/jest": "^28.1.6", + "jest": "^28.1.3", + "parcel": "^2.6.2", + "process": "^0.11.10", + "ts-jest": "^28.0.7", + "typescript": "^4.7.4" + }, + "packageManager": "yarn@3.2.2" +} diff --git a/src/Model/Repositories/AutoRetriever.ts b/src/Model/Repositories/AutoRetriever.ts new file mode 100644 index 0000000..c32d0f2 --- /dev/null +++ b/src/Model/Repositories/AutoRetriever.ts @@ -0,0 +1,13 @@ +import {ConstructorOf, Model} from "@sharkitek/core"; + +/** + * Auto retriever interface. + */ +export interface AutoRetriever +{ + /** + * Auto retrieve a model for the given identifier. + * @param identifier - Identifier for which to retrieve the model. + */ + autoRetrieve(identifier: unknown): Promise; +} diff --git a/src/Model/Repositories/ModelRepository.ts b/src/Model/Repositories/ModelRepository.ts new file mode 100644 index 0000000..1e848cc --- /dev/null +++ b/src/Model/Repositories/ModelRepository.ts @@ -0,0 +1,165 @@ +import "reflect-metadata"; +import {ConstructorOf, Model} from "@sharkitek/core"; +import {AutoRetriever} from "./AutoRetriever"; + +/** + * Interface of a model with a repository. + */ +export interface ModelWithRepository +{ + /** + * Get the model repository name. + */ + getRepositoryName(): string; + + /** + * Get the model repository. + */ + getRepository(): ModelRepository; + + /** + * Store the model in its repository. + */ + store(): void; +} + +/** + * Find a model in the corresponding repository or try to retrieve it. + * @param modelClass - Class of the model to find. + * @param identifier - Identifier of the object to find. + * @param retriever - Function called when a model with the given identifier could not be found. + */ +export async function find(modelClass: ConstructorOf, identifier: IdentifierType, retriever: (identifier: IdentifierType) => Promise = async () => null): Promise +{ + // Getting model repository. + const repository = (modelClass as any).getRepository() as ModelRepository; + + // Trying to get the model from repository. + let model = repository.get(String(identifier)); + + if (!model) + { // If there is no model with the given identifier in the repository, trying to get one and registering it if found. + model = await retriever(identifier); // Trying to retrieve the model. + + if (!model && modelClass.prototype.autoRetrieve) + // If there is not model after calling the custom retriever, trying to get one with the auto retriever if there is one. + model = await (new modelClass() as unknown as AutoRetriever).autoRetrieve(identifier); + + // The model has been found, registering it. + if (model) repository.register(model); + } + + return model; // Returning found model, if there is one. +} + +/** + * Add repository capability to Sharkitek models. + * @param repositoryName - Model class to extend. + */ +export function WithRepository(repositoryName: string): (modelClass: typeof Model) => ConstructorOf +{ + return (modelClass) => ( + class WithRepository extends modelClass implements ModelWithRepository { + /** + * Get the model repository name. + */ + static getRepositoryName(): string + { + return repositoryName; + } + + /** + * Get the model repository. + */ + static getRepository(): ModelRepository + { + return ModelsRepositories.get().getRepository(WithRepository.getRepositoryName()); + } + + getRepositoryName(): string + { + return WithRepository.getRepositoryName(); + } + + getRepository(): ModelRepository + { + return WithRepository.getRepository() as ModelRepository; + } + + store(): void + { + WithRepository.getRepository().register(this); + } + } + ); +} + +/** + * Models repositories. + */ +export class ModelsRepositories +{ + private static instance: ModelsRepositories; + + /** + * Get the singleton instance. + */ + static get(): ModelsRepositories + { + // If there is no instance, creating one. + if (!this.instance) this.instance = new ModelsRepositories(); + + return this.instance; // Return the main instance. + } + + protected constructor() + {} + + /** + * List of all repositories. + */ + protected repositories: Record = {}; + + /** + * Get repository for the given name. + * @param repositoryName - Name of the repository to get. + */ + getRepository(repositoryName: string): ModelRepository + { + if (!this.repositories[repositoryName]) + // The repository for the given name does not exists, initializing it. + this.repositories[repositoryName] = new ModelRepository(); + + return this.repositories[repositoryName] as ModelRepository; // Return the repository. + } +} + +/** + * A model repository. + */ +export class ModelRepository +{ + models: Record = {}; + + /** + * Register the given model in the repository. + * @param model - Model to register. + */ + register(model: T): void + { + this.models[String(model.getIdentifier())] = model; + } + + /** + * Get the model identified by its identifier. + * @param identifier - Identifier of the model. + */ + get(identifier: string): T|null + { + return typeof this.models?.[identifier] !== "undefined" + // Model exists, returning it. + ? this.models[identifier] + // Model does not exists, returning NULL. + : null; + } +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..89326bf --- /dev/null +++ b/src/index.ts @@ -0,0 +1,5 @@ + + +export * from "./Model/Repositories/ModelRepository"; +export * from "./Model/Repositories/AutoRetriever"; + diff --git a/tests/ModelRepository.test.ts b/tests/ModelRepository.test.ts new file mode 100644 index 0000000..a6845d6 --- /dev/null +++ b/tests/ModelRepository.test.ts @@ -0,0 +1,113 @@ +import {Model, SString, SNumeric, SDecimal, SArray, SModel, Identifier, Property} from "@sharkitek/core"; +import {find, ModelRepository, WithRepository, AutoRetriever} from "../src"; + +/** + * Another test model. + */ +class Author extends WithRepository("Author")(Model) implements AutoRetriever +{ + async autoRetrieve(identifier: unknown): Promise + { + return (new Author()).deserialize({ + name: "autoretrieved", + firstName: "autoretrieved", + email: identifier, + }); + } + + @Property(SString) + name: string = undefined; + + @Property(SString) + firstName: string = undefined; + + @Property(SString) + @Identifier + email: string = undefined; + + constructor(name: string = undefined, firstName: string = undefined, email: string = undefined) + { + super(); + + this.name = name; + this.firstName = firstName; + this.email = email; + } +} + +/** + * A test model. + */ +class Article extends Model +{ + @Property(SNumeric) + @Identifier + id: number = undefined; + + @Property(SString) + title: string = undefined; + + @Property(SArray(SModel(Author))) + authors: Author[] = []; + + @Property(SString) + text: string = undefined; + + @Property(SDecimal) + evaluation: number = undefined; +} + +it("find in empty repository", async () => { + const author = await find(Author, "test@test.test", async (email: string) => ( + (new Author()).deserialize({ + name: "TEST", + firstName: "Test", + email: email, + }) + )); + + expect(author.serialize()).toStrictEqual({ + name: "TEST", + firstName: "Test", + email: "test@test.test", + }); +}); + +it("find in repository", async () => { + (new Author()).deserialize({ + name: "TEST", + firstName: "Test", + email: "test@test.test", + }).store(); + + const author = await find(Author, "test@test.test"); + + expect(author.serialize()).toStrictEqual({ + name: "TEST", + firstName: "Test", + email: "test@test.test", + }); +}); + +it("get repository data", () => { + const author = (new Author()).deserialize({ + name: "TEST", + firstName: "Test", + email: "test@test.test", + }); + + expect(author.getRepositoryName()).toStrictEqual("Author"); + expect(author.getRepository()).toBeInstanceOf(ModelRepository); +}); + +it("test auto retriever", async () => { + const identifier = "autoretrieve@test.test"; + + expect( + (await find(Author, identifier)).serialize() + ).toStrictEqual({ + name: "autoretrieved", + firstName: "autoretrieved", + email: identifier, + }); +}); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..46259e1 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,21 @@ +{ + "files": ["src/index.ts"], + "compilerOptions": { + "outDir": "./lib/", + "noImplicitAny": true, + "allowSyntheticDefaultImports": true, + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "module": "ES6", + "moduleResolution": "Node", + "target": "ES6", + + "lib": [ + "ESNext", + "DOM" + ] + } +}