Compare commits
7 Commits
Author | SHA1 | Date | |
---|---|---|---|
3acc564dee | |||
ca58569011 | |||
6f7b2fa81d | |||
cb40f95a52 | |||
15de42f7ca | |||
44ba66b39f | |||
e7050a2fc0 |
140
README.md
140
README.md
@ -1,3 +1,139 @@
|
||||
# repositories
|
||||
# Sharkitek Repositories
|
||||
|
||||
Sharkitek repositories extension.
|
||||
The _Sharkitek Repositories_ extension is designed to add global repositories of models. It helps to deduplicate instances
|
||||
of the same objects (with the same identifier) in an application. It could be thought as a tiny local database.
|
||||
|
||||
It adds a model class extension and a special model type that tries to use (and save) the models from
|
||||
the corresponding repository on deserialization.
|
||||
|
||||
## Documentation
|
||||
|
||||
To link models to repositories, you can use the `WithRepository` Sharkitek extension.
|
||||
|
||||
```typescript
|
||||
class Author extends WithRepository("Author")(Model)
|
||||
{
|
||||
@Property(SString)
|
||||
name: string = undefined;
|
||||
|
||||
@Property(SString)
|
||||
firstName: string = undefined;
|
||||
|
||||
@Property(SString)
|
||||
@Identifier
|
||||
email: string = undefined;
|
||||
}
|
||||
```
|
||||
|
||||
The only argument of `WithRepository` is the name of the repository to use. The easier way to use _Sharkitek Repositories_ is
|
||||
to always use class name as repository name.
|
||||
|
||||
`WithRepository` declares 3 methods in the model, which can be used to get the repository or store the model in it.
|
||||
These 3 methods are defined in the `ModelWithRepository` interface:
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* Interface of a model with a repository.
|
||||
*/
|
||||
interface ModelWithRepository
|
||||
{
|
||||
/**
|
||||
* Get the model repository name.
|
||||
*/
|
||||
getRepositoryName(): string;
|
||||
|
||||
/**
|
||||
* Get the model repository.
|
||||
*/
|
||||
getRepository(): ModelRepository;
|
||||
|
||||
/**
|
||||
* Store the model in its repository.
|
||||
*/
|
||||
store(): void;
|
||||
}
|
||||
```
|
||||
|
||||
When a model is linked to a repository, you can use `find` to get instances from it or retrieve it elsewhere (from an API, for example).
|
||||
|
||||
```typescript
|
||||
const author = await find(Author, "example@example.example", async (email: string) => {
|
||||
// Make an API call or something like that to retrieve the model and store it in the repository.
|
||||
|
||||
// ...
|
||||
|
||||
return model;
|
||||
});
|
||||
```
|
||||
|
||||
To help generalization of API calls for a given model, you can implement an automatic retriever of a model.
|
||||
|
||||
```typescript
|
||||
class Author extends WithRepository("Author")(Model) implements AutoRetriever<Author>
|
||||
{
|
||||
async autoRetrieve(identifier: unknown): Promise<Author>
|
||||
{
|
||||
return (new Author()).deserialize({
|
||||
// ... make an API call to retrieve serialized data, for example ...
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// ... the rest of the definition ...
|
||||
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
_Sharkitek Repositories_ also provide a special model property type, that automatically uses repositories
|
||||
to store and retrieve instances. When you use `SModelFromRepository` property type, on deserialization, Sharkitek
|
||||
will first try to get an instance matching the model identifier in its repository. If it is not found, then it will use
|
||||
the currently serialized instance of the model as the main instance and store it in the repository.
|
||||
Here is an example:
|
||||
|
||||
```typescript
|
||||
class A extends WithRepository("A")(Model)
|
||||
{
|
||||
@Property(SNumeric)
|
||||
@Identifier
|
||||
id: number = undefined;
|
||||
|
||||
@Property(SString)
|
||||
foo: string = undefined;
|
||||
}
|
||||
|
||||
class B extends Model
|
||||
{
|
||||
@Property(SNumeric)
|
||||
@Identifier
|
||||
id: number = undefined;
|
||||
|
||||
@Property(SModelFromRepository(A))
|
||||
a: A = undefined;
|
||||
}
|
||||
|
||||
// First deserialization, that should store the A instance.
|
||||
const firstB = (new B()).deserialize({
|
||||
id: 5,
|
||||
a: {
|
||||
id: 3, // No instance with ID 3 is stored in the repository, using this instance and storing it.
|
||||
foo: "first",
|
||||
}
|
||||
});
|
||||
|
||||
// To check that the first instance of "A" is used, we change the value of "foo". If a.foo == "first", then the first object has been used.
|
||||
const secondB = (new B()).deserialize({
|
||||
id: 7,
|
||||
a: {
|
||||
id: 3, // Same ID as the previous object, using the instance from repository.
|
||||
foo: "second",
|
||||
}
|
||||
});
|
||||
|
||||
console.log(firstB.a.foo); // Result: "first".
|
||||
console.log(secondB.a.foo); // Result: "first".
|
||||
|
||||
// firstB.a and secondB.a are the same object.
|
||||
firstB.a.foo = "test";
|
||||
console.log(secondB.a.foo); // Result: "test".
|
||||
```
|
||||
|
@ -1,7 +1,8 @@
|
||||
{
|
||||
"name": "@sharkitek/repositories",
|
||||
"version": "1.0.0",
|
||||
"version": "1.1.0",
|
||||
"description": "Sharkitek models repositories extension.",
|
||||
"keywords": ["sharkitek", "model", "storage", "repository"],
|
||||
"repository": "https://git.madeorsk.com/Sharkitek/repositories",
|
||||
"author": "Madeorsk <madeorsk@protonmail.com>",
|
||||
"license": "MIT",
|
||||
@ -17,7 +18,7 @@
|
||||
"lib/**/*"
|
||||
],
|
||||
"dependencies": {
|
||||
"@sharkitek/core": "^1.0.1",
|
||||
"@sharkitek/core": "^1.0.2",
|
||||
"reflect-metadata": "^0.1.13"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
@ -1,4 +1,3 @@
|
||||
import {ConstructorOf, Model} from "@sharkitek/core";
|
||||
|
||||
/**
|
||||
* Auto retriever interface.
|
||||
|
@ -153,13 +153,14 @@ export class ModelRepository<T extends Model = Model>
|
||||
/**
|
||||
* Get the model identified by its identifier.
|
||||
* @param identifier - Identifier of the model.
|
||||
* @param defaultValue - Function called when there is no value matching the given identifier in the repository. Return null by default.
|
||||
*/
|
||||
get(identifier: string): T|null
|
||||
get(identifier: string, defaultValue: (identifier: string) => T|null = () => null): T|null
|
||||
{
|
||||
return typeof this.models?.[identifier] !== "undefined"
|
||||
// Model exists, returning it.
|
||||
? this.models[identifier]
|
||||
// Model does not exists, returning NULL.
|
||||
: null;
|
||||
: defaultValue(identifier);
|
||||
}
|
||||
}
|
||||
|
30
src/Model/Types/ModelFromRepositoryType.ts
Normal file
30
src/Model/Types/ModelFromRepositoryType.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import {ConstructorOf, Model, ModelType} from "@sharkitek/core";
|
||||
import {ModelWithRepository} from "../Repositories/ModelRepository";
|
||||
|
||||
/**
|
||||
* Type of a Sharkitek model (from repository) value.
|
||||
*/
|
||||
export class ModelFromRepositoryType<M extends Model&ModelWithRepository> extends ModelType<M>
|
||||
{
|
||||
deserialize(value: any): M
|
||||
{
|
||||
// Deserializing the given object in a new model.
|
||||
let model = (new this.modelConstructor()).deserialize(value);
|
||||
|
||||
// Getting the object matching the current model identifier, if there is one, or the current model.
|
||||
model = model.getRepository().get(String(model.getIdentifier()), () => model) as M;
|
||||
|
||||
model?.store(); // Storing the current model in the repository if it was not.
|
||||
|
||||
return model; // Returning the model.
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Type of a Sharkitek model (from repository) value.
|
||||
* @param modelConstructor - Constructor of the model.
|
||||
*/
|
||||
export function SModelFromRepository<M extends Model&ModelWithRepository>(modelConstructor: ConstructorOf<M>)
|
||||
{
|
||||
return new ModelFromRepositoryType(modelConstructor);
|
||||
}
|
@ -3,3 +3,5 @@
|
||||
export * from "./Model/Repositories/ModelRepository";
|
||||
export * from "./Model/Repositories/AutoRetriever";
|
||||
|
||||
export * from "./Model/Types/ModelFromRepositoryType";
|
||||
|
||||
|
50
tests/ModelFromRepositoryType.test.ts
Normal file
50
tests/ModelFromRepositoryType.test.ts
Normal file
@ -0,0 +1,50 @@
|
||||
import {Identifier, Model, Property, SNumeric, SString} from "@sharkitek/core";
|
||||
import {WithRepository, SModelFromRepository} from "../src";
|
||||
|
||||
class A extends WithRepository("A")(Model)
|
||||
{
|
||||
@Property(SNumeric)
|
||||
@Identifier
|
||||
id: number = undefined;
|
||||
|
||||
@Property(SString)
|
||||
foo: string = undefined;
|
||||
}
|
||||
|
||||
class B extends Model
|
||||
{
|
||||
@Property(SNumeric)
|
||||
@Identifier
|
||||
id: number = undefined;
|
||||
|
||||
@Property(SModelFromRepository(A))
|
||||
a: A = undefined;
|
||||
}
|
||||
|
||||
it("get the same object after two deserializations", () => {
|
||||
// First deserialization, that should store the A instance.
|
||||
const firstB = (new B()).deserialize({
|
||||
id: 5,
|
||||
a: {
|
||||
id: 3,
|
||||
foo: "first",
|
||||
}
|
||||
});
|
||||
|
||||
// To check that the first instance of "A" is used, we change the value of "foo". If a.foo == "first", then the first object has been used.
|
||||
const secondB = (new B()).deserialize({
|
||||
id: 7,
|
||||
a: {
|
||||
id: 3,
|
||||
foo: "second",
|
||||
}
|
||||
});
|
||||
|
||||
// `a` of `secondB` should be `a` of `firstB`.
|
||||
expect(secondB.a).toBe(firstB.a);
|
||||
|
||||
// If something changes in A instance of firstB,
|
||||
// A instance of secondB should have the same modification (as it should be the same instance).
|
||||
firstB.a.foo = "test";
|
||||
expect(secondB.a.foo).toStrictEqual("test");
|
||||
});
|
Reference in New Issue
Block a user