From 72417dd3500814af8b0c3df228fe7a58d1d0b832 Mon Sep 17 00:00:00 2001 From: Madeorsk Date: Sat, 28 Jun 2025 21:45:30 +0200 Subject: [PATCH] Change the recommended model declaration to allow models inheritance. --- README.md | 145 ++++++++++++++++++++------------------- tests/model.test.ts | 160 ++++++++++++++++++++++---------------------- 2 files changed, 156 insertions(+), 149 deletions(-) diff --git a/README.md b/README.md index 93d76ee..fcfcea3 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/tests/model.test.ts b/tests/model.test.ts index 73eadcb..3f149f8 100644 --- a/tests/model.test.ts +++ b/tests/model.test.ts @@ -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.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
(() => 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: "

fully new text! yes!

", }); 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"}], });