Change the recommended model declaration to allow models inheritance.
All checks were successful
/ test (push) Successful in 37s

This commit is contained in:
Madeorsk 2025-06-28 21:45:30 +02:00
parent 5f1e2709bb
commit 72417dd350
Signed by: Madeorsk
GPG key ID: 677E51CA765BB79F
2 changed files with 156 additions and 149 deletions

145
README.md
View file

@ -45,23 +45,23 @@ Then, you can use the defined methods like `serialize`, `parse`, `patch` or `ser
```typescript
class Example {
static model = defineModel({
Class: Example,
properties: {
id: s.property.numeric(),
name: s.property.string(),
},
identifier: "id",
});
id: number;
name: string;
}
const ExampleModel = defineModel({
Class: Example,
properties: {
id: s.property.numeric(),
name: s.property.string(),
},
identifier: "id",
});
```
## Quick start
**Note**: by convention, we define our models in a `model` static variable in the model's class. It is a good way to keep your model declaration near the actual class, and its usage will be more natural.
**Note**: we usually define our models in a `{ModelName}Model` variable next to the model's class.
### Model definition
@ -70,24 +70,27 @@ class Example {
* A person.
*/
class Person {
static model = defineModel({
Class: Person,
properties: {
id: s.property.numeric(),
name: s.property.string(),
email: s.property.string(),
createdAt: s.property.date(),
active: s.property.boolean(),
},
identifier: "id",
});
id: number;
name: string;
email: string;
createdAt: Date;
active: boolean = true;
}
/**
* A person model manager.
*/
const PersonModel = defineModel({
Class: Person,
properties: {
id: s.property.numeric(),
name: s.property.string(),
email: s.property.string(),
createdAt: s.property.date(),
active: s.property.boolean(),
},
identifier: "id",
});
```
```typescript
@ -95,23 +98,6 @@ class Person {
* An article.
*/
class Article {
static model = defineModel({
Class: Article,
properties: {
id: s.property.numeric(),
title: s.property.string(),
authors: s.property.array(s.property.model(Person)),
text: s.property.string(),
evaluation: s.property.decimal(),
tags: s.property.array(
s.property.object({
name: s.property.string(),
}),
),
},
identifier: "id",
});
id: number;
title: string;
authors: Person[] = [];
@ -121,6 +107,26 @@ class Article {
name: string;
}[];
}
/**
* An article model manager.
*/
const ArticleModel = defineModel({
Class: Article,
properties: {
id: s.property.numeric(),
title: s.property.string(),
authors: s.property.array(s.property.model(PersonModel)),
text: s.property.string(),
evaluation: s.property.decimal(),
tags: s.property.array(
s.property.object({
name: s.property.string(),
}),
),
},
identifier: "id",
});
```
```typescript
@ -128,18 +134,21 @@ class Article {
* A model with composite keys.
*/
class CompositeKeys {
static model = defineModel({
Class: CompositeKeys,
properties: {
id1: s.property.numeric(),
id2: s.property.string(),
},
identifier: ["id1", "id2"],
});
id1: number;
id2: string;
}
/**
* A composite keys model manager.
*/
const CompositeKeysModel = defineModel({
Class: CompositeKeys,
properties: {
id1: s.property.numeric(),
id2: s.property.string(),
},
identifier: ["id1", "id2"],
});
```
### Model functions
@ -153,14 +162,14 @@ instance.createdAt = new Date();
instance.name = "John Doe";
instance.email = "john@doe.test";
instance.active = true;
const serialized = Person.model.model(instance).serialize();
const serialized = PersonModel.model(instance).serialize();
console.log(serialized); // { id: 1, createdAt: "YYYY-MM-DDTHH:mm:ss.sssZ", name: "John Doe", email: "john@doe.test", active: true }
```
#### Deserialization
```typescript
const instance = Person.model.parse({
const instance = PersonModel.parse({
id: 1,
createdAt: "2011-10-05T14:48:00.000Z",
name: "John Doe",
@ -174,7 +183,7 @@ console.log(instance.createdAt instanceof Date); // true
#### Patch
```typescript
const instance = Person.model.parse({
const instance = PersonModel.parse({
id: 1,
createdAt: "2011-10-05T14:48:00.000Z",
name: "John Doe",
@ -185,9 +194,9 @@ const instance = Person.model.parse({
instance.name = "Johnny";
// Patch serialized only changed properties and the identifier.
console.log(Person.model.model(instance).patch()); // { id: 1, name: "Johnny" }
console.log(PersonModel.model(instance).patch()); // { id: 1, name: "Johnny" }
// If you run it one more time, already patched properties will not be included again.
console.log(Person.model.model(instance).patch()); // { id: 1 }
console.log(PersonModel.model(instance).patch()); // { id: 1 }
```
#### Identifier
@ -196,7 +205,7 @@ console.log(Person.model.model(instance).patch()); // { id: 1 }
const instance = new CompositeKeys();
instance.id1 = 5;
instance.id2 = "foo";
const instanceIdentifier = CompositeKeys.model.model(instance).getIdentifier();
const instanceIdentifier = CompositeKeysModel.model(instance).getIdentifier();
console.log(instanceIdentifier); // [5, "foo"]
```
@ -222,15 +231,15 @@ When you are defining a property of a Sharkitek model, you must provide its type
```typescript
class Example {
static model = defineModel({
Class: Example,
properties: {
foo: s.property.define(new StringType()),
},
});
foo: string;
}
const ExampleModel = defineModel({
Class: Example,
properties: {
foo: s.property.define(new StringType()),
},
});
```
To ease the use of these classes and reduce read complexity, properties of each type are easily definable with a function for each type.
@ -249,15 +258,15 @@ Type implementers should provide a corresponding function for each defined type.
```typescript
class Example {
static model = defineModel({
Class: Example,
properties: {
foo: s.property.string(),
},
});
foo: string;
}
const ExampleModel = defineModel({
Class: Example,
properties: {
foo: s.property.string(),
},
});
```
### Models

View file

@ -5,18 +5,6 @@ import {circular, defineModel, s} from "../src/library";
* Test class of an account.
*/
class Account {
static model = s.defineModel({
Class: Account,
identifier: "id",
properties: {
id: s.property.numeric(),
createdAt: s.property.date(),
name: s.property.string(),
email: s.property.string(),
active: s.property.boolean(),
},
});
id: number;
createdAt: Date;
name: string;
@ -24,24 +12,22 @@ class Account {
active: boolean;
}
const AccountModel = s.defineModel({
Class: Account,
identifier: "id",
properties: {
id: s.property.numeric(),
createdAt: s.property.date(),
name: s.property.string(),
email: s.property.string(),
active: s.property.boolean(),
},
});
/**
* Test class of an article.
*/
class Article {
static model = s.defineModel({
Class: Article,
identifier: "id",
properties: {
id: s.property.numeric(),
title: s.property.string(),
authors: s.property.array(s.property.model(() => Account.model)),
text: s.property.string(),
evaluation: s.property.decimal(),
tags: s.property.array(s.property.object({name: s.property.string()})),
comments: s.property.array(s.property.model(() => ArticleComment.model)),
},
});
id: number;
title: string;
authors: Account[];
@ -51,27 +37,41 @@ class Article {
comments: ArticleComment[];
}
const ArticleModel = s.defineModel({
Class: Article,
identifier: "id",
properties: {
id: s.property.numeric(),
title: s.property.string(),
authors: s.property.array(s.property.model(() => AccountModel)),
text: s.property.string(),
evaluation: s.property.decimal(),
tags: s.property.array(s.property.object({name: s.property.string()})),
comments: s.property.array(s.property.model(() => ArticleCommentModel)),
},
});
/**
* Test class of a comment on an article.
*/
class ArticleComment {
static model = s.defineModel({
Class: ArticleComment,
identifier: "id",
properties: {
id: s.property.numeric(),
article: s.property.model(circular<Article>(() => Article.model)),
author: s.property.model(() => Account.model),
message: s.property.string(),
},
});
id: number;
article?: Article;
author: Account;
message: string;
}
const ArticleCommentModel = s.defineModel({
Class: ArticleComment,
identifier: "id",
properties: {
id: s.property.numeric(),
article: s.property.model(circular<Article>(() => ArticleModel)),
author: s.property.model(() => AccountModel),
message: s.property.string(),
},
});
/**
* Get a test account instance.
*/
@ -100,17 +100,17 @@ function getTestArticle(): Article {
describe("model", () => {
it("defines a new model, extending an existing one", () => {
class ExtendedAccount extends Account {
static extendedModel = s.extend(Account.model, {
Class: ExtendedAccount,
properties: {
extendedProperty: s.property.string(),
},
});
extendedProperty: string;
}
expect(ExtendedAccount.extendedModel.definition).toEqual({
const ExtendedAccountModel = s.extend(AccountModel, {
Class: ExtendedAccount,
properties: {
extendedProperty: s.property.string(),
},
});
expect(ExtendedAccountModel.definition).toEqual({
Class: ExtendedAccount,
identifier: "id",
properties: {
@ -125,17 +125,17 @@ describe("model", () => {
});
it("initializes a new model", () => {
const article = getTestArticle();
const newModel = Article.model.model(article);
const newModel = ArticleModel.model(article);
expect(newModel.instance).toBe(article);
});
it("gets a model state from its instance", () => {
const article = getTestArticle();
expect(Article.model.model(article).isNew()).toBeTruthy();
expect(Article.model.model(article).isDirty()).toBeFalsy();
expect(ArticleModel.model(article).isNew()).toBeTruthy();
expect(ArticleModel.model(article).isDirty()).toBeFalsy();
});
it("gets a model identifier value", () => {
const article = getTestArticle();
expect(Article.model.model(article).getIdentifier()).toBe(1);
expect(ArticleModel.model(article).getIdentifier()).toBe(1);
});
it("gets a model composite identifier value", () => {
class CompositeModel {
@ -168,11 +168,11 @@ describe("model", () => {
});
it("checks model dirtiness when altered, then reset diff", () => {
const article = getTestArticle();
expect(Article.model.model(article).isDirty()).toBeFalsy();
expect(ArticleModel.model(article).isDirty()).toBeFalsy();
article.title = "new title";
expect(Article.model.model(article).isDirty()).toBeTruthy();
Article.model.model(article).resetDiff();
expect(Article.model.model(article).isDirty()).toBeFalsy();
expect(ArticleModel.model(article).isDirty()).toBeTruthy();
ArticleModel.model(article).resetDiff();
expect(ArticleModel.model(article).isDirty()).toBeFalsy();
});
it("deserializes a model from a serialized form", () => {
@ -213,7 +213,7 @@ describe("model", () => {
],
});
const deserializedArticle = Article.model.parse({
const deserializedArticle = ArticleModel.parse({
id: 1,
title: "this is a test",
authors: [
@ -250,16 +250,14 @@ describe("model", () => {
],
});
const deserializedArticleProperties = Article.model
.model(deserializedArticle)
.getInstanceProperties();
const deserializedArticleProperties =
ArticleModel.model(deserializedArticle).getInstanceProperties();
delete deserializedArticleProperties.authors[0]._sharkitek;
delete deserializedArticleProperties.authors[1]._sharkitek;
delete deserializedArticleProperties.comments[0]._sharkitek;
delete (deserializedArticleProperties.comments[0].author as any)._sharkitek;
const expectedArticleProperties = Article.model
.model(expectedArticle)
.getInstanceProperties();
const expectedArticleProperties =
ArticleModel.model(expectedArticle).getInstanceProperties();
delete expectedArticleProperties.authors[0]._sharkitek;
delete expectedArticleProperties.authors[1]._sharkitek;
delete expectedArticleProperties.comments[0]._sharkitek;
@ -269,7 +267,7 @@ describe("model", () => {
it("serializes an initialized model", () => {
const article = getTestArticle();
expect(Article.model.model(article).serialize()).toEqual({
expect(ArticleModel.model(article).serialize()).toEqual({
id: 1,
title: "this is a test",
text: "this is a long test.",
@ -289,7 +287,7 @@ describe("model", () => {
});
it("deserializes, changes and patches", () => {
const deserializedArticle = Article.model.parse({
const deserializedArticle = ArticleModel.parse({
id: 1,
title: "this is a test",
authors: [
@ -328,21 +326,21 @@ describe("model", () => {
deserializedArticle.text = "A new text for a new life!";
expect(Article.model.model(deserializedArticle).patch()).toStrictEqual({
expect(ArticleModel.model(deserializedArticle).patch()).toStrictEqual({
id: 1,
text: "A new text for a new life!",
});
deserializedArticle.evaluation = 5.24;
expect(Article.model.model(deserializedArticle).patch()).toStrictEqual({
expect(ArticleModel.model(deserializedArticle).patch()).toStrictEqual({
id: 1,
evaluation: "5.24",
});
});
it("patches with modified submodels", () => {
const deserializedArticle = Article.model.parse({
const deserializedArticle = ArticleModel.parse({
id: 1,
title: "this is a test",
authors: [
@ -381,14 +379,14 @@ describe("model", () => {
deserializedArticle.authors[1].active = true;
expect(Article.model.model(deserializedArticle).patch()).toStrictEqual({
expect(ArticleModel.model(deserializedArticle).patch()).toStrictEqual({
id: 1,
authors: [{id: 52}, {id: 4, active: true}],
});
deserializedArticle.comments[0].author.name = "Johnny";
expect(Article.model.model(deserializedArticle).patch()).toStrictEqual({
expect(ArticleModel.model(deserializedArticle).patch()).toStrictEqual({
id: 1,
comments: [
{
@ -454,7 +452,7 @@ describe("model", () => {
});
it("assigns properties, ignoring fields which are not properties", () => {
const deserializedArticle = Article.model.parse({
const deserializedArticle = ArticleModel.parse({
id: 1,
title: "this is a test",
authors: [
@ -492,13 +490,13 @@ describe("model", () => {
});
// Assign title and text, html is silently ignored.
Article.model.model(deserializedArticle).assign({
ArticleModel.model(deserializedArticle).assign({
title: "something else",
text: "fully new text! yes!",
html: "<p>fully new text! yes!</p>",
});
expect((deserializedArticle as any)?.html).toBeUndefined();
expect(Article.model.model(deserializedArticle).patch()).toStrictEqual({
expect(ArticleModel.model(deserializedArticle).patch()).toStrictEqual({
id: 1,
title: "something else",
text: "fully new text! yes!",
@ -506,10 +504,10 @@ describe("model", () => {
});
it("initializes a model from properties values", () => {
const testArticle = Article.model.from({
const testArticle = ArticleModel.from({
title: "this is a test",
authors: [
Account.model.from({
AccountModel.from({
name: "John Doe",
email: "test@test.test",
createdAt: new Date(),
@ -534,11 +532,11 @@ describe("model", () => {
});
it("applies patches to an existing model", () => {
const testArticle = Article.model.from({
const testArticle = ArticleModel.from({
id: 1,
title: "this is a test",
authors: [
Account.model.from({
AccountModel.from({
id: 55,
name: "John Doe",
email: "test@test.test",
@ -553,35 +551,35 @@ describe("model", () => {
unknownField: true,
anotherOne: "test",
});
Article.model.model(testArticle).resetDiff();
ArticleModel.model(testArticle).resetDiff();
// Test simple patch.
Article.model.model(testArticle).applyPatch({
ArticleModel.model(testArticle).applyPatch({
title: "new title",
});
expect(testArticle.title).toBe("new title");
expect(Article.model.model(testArticle).serializeDiff()).toStrictEqual({
expect(ArticleModel.model(testArticle).serializeDiff()).toStrictEqual({
id: 1,
});
// Test originals update propagation.
Article.model.model(testArticle).applyPatch({
ArticleModel.model(testArticle).applyPatch({
authors: [{email: "john@test.test"}],
});
expect(testArticle.authors[0].email).toBe("john@test.test");
expect(Article.model.model(testArticle).serializeDiff()).toStrictEqual({
expect(ArticleModel.model(testArticle).serializeDiff()).toStrictEqual({
id: 1,
});
// Test without originals update.
Article.model.model(testArticle).applyPatch(
ArticleModel.model(testArticle).applyPatch(
{
authors: [{name: "Johnny"}],
},
false,
);
expect(testArticle.authors[0].name).toBe("Johnny");
expect(Article.model.model(testArticle).serializeDiff()).toStrictEqual({
expect(ArticleModel.model(testArticle).serializeDiff()).toStrictEqual({
id: 1,
authors: [{id: 55, name: "Johnny"}],
});