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

119
README.md
View file

@ -45,23 +45,23 @@ Then, you can use the defined methods like `serialize`, `parse`, `patch` or `ser
```typescript ```typescript
class Example { class Example {
static model = defineModel({ id: number;
name: string;
}
const ExampleModel = defineModel({
Class: Example, Class: Example,
properties: { properties: {
id: s.property.numeric(), id: s.property.numeric(),
name: s.property.string(), name: s.property.string(),
}, },
identifier: "id", identifier: "id",
}); });
id: number;
name: string;
}
``` ```
## Quick start ## 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 ### Model definition
@ -70,7 +70,17 @@ class Example {
* A person. * A person.
*/ */
class Person { class Person {
static model = defineModel({ id: number;
name: string;
email: string;
createdAt: Date;
active: boolean = true;
}
/**
* A person model manager.
*/
const PersonModel = defineModel({
Class: Person, Class: Person,
properties: { properties: {
id: s.property.numeric(), id: s.property.numeric(),
@ -80,14 +90,7 @@ class Person {
active: s.property.boolean(), active: s.property.boolean(),
}, },
identifier: "id", identifier: "id",
}); });
id: number;
name: string;
email: string;
createdAt: Date;
active: boolean = true;
}
``` ```
```typescript ```typescript
@ -95,23 +98,6 @@ class Person {
* An article. * An article.
*/ */
class 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; id: number;
title: string; title: string;
authors: Person[] = []; authors: Person[] = [];
@ -121,6 +107,26 @@ class Article {
name: string; 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 ```typescript
@ -128,18 +134,21 @@ class Article {
* A model with composite keys. * A model with composite keys.
*/ */
class CompositeKeys { class CompositeKeys {
static model = defineModel({ id1: number;
id2: string;
}
/**
* A composite keys model manager.
*/
const CompositeKeysModel = defineModel({
Class: CompositeKeys, Class: CompositeKeys,
properties: { properties: {
id1: s.property.numeric(), id1: s.property.numeric(),
id2: s.property.string(), id2: s.property.string(),
}, },
identifier: ["id1", "id2"], identifier: ["id1", "id2"],
}); });
id1: number;
id2: string;
}
``` ```
### Model functions ### Model functions
@ -153,14 +162,14 @@ instance.createdAt = new Date();
instance.name = "John Doe"; instance.name = "John Doe";
instance.email = "john@doe.test"; instance.email = "john@doe.test";
instance.active = true; 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 } console.log(serialized); // { id: 1, createdAt: "YYYY-MM-DDTHH:mm:ss.sssZ", name: "John Doe", email: "john@doe.test", active: true }
``` ```
#### Deserialization #### Deserialization
```typescript ```typescript
const instance = Person.model.parse({ const instance = PersonModel.parse({
id: 1, id: 1,
createdAt: "2011-10-05T14:48:00.000Z", createdAt: "2011-10-05T14:48:00.000Z",
name: "John Doe", name: "John Doe",
@ -174,7 +183,7 @@ console.log(instance.createdAt instanceof Date); // true
#### Patch #### Patch
```typescript ```typescript
const instance = Person.model.parse({ const instance = PersonModel.parse({
id: 1, id: 1,
createdAt: "2011-10-05T14:48:00.000Z", createdAt: "2011-10-05T14:48:00.000Z",
name: "John Doe", name: "John Doe",
@ -185,9 +194,9 @@ const instance = Person.model.parse({
instance.name = "Johnny"; instance.name = "Johnny";
// Patch serialized only changed properties and the identifier. // 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. // 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 #### Identifier
@ -196,7 +205,7 @@ console.log(Person.model.model(instance).patch()); // { id: 1 }
const instance = new CompositeKeys(); const instance = new CompositeKeys();
instance.id1 = 5; instance.id1 = 5;
instance.id2 = "foo"; instance.id2 = "foo";
const instanceIdentifier = CompositeKeys.model.model(instance).getIdentifier(); const instanceIdentifier = CompositeKeysModel.model(instance).getIdentifier();
console.log(instanceIdentifier); // [5, "foo"] 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 ```typescript
class Example { class Example {
static model = defineModel({ foo: string;
}
const ExampleModel = defineModel({
Class: Example, Class: Example,
properties: { properties: {
foo: s.property.define(new StringType()), foo: s.property.define(new StringType()),
}, },
}); });
foo: string;
}
``` ```
To ease the use of these classes and reduce read complexity, properties of each type are easily definable with a function for each type. 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 ```typescript
class Example { class Example {
static model = defineModel({ foo: string;
}
const ExampleModel = defineModel({
Class: Example, Class: Example,
properties: { properties: {
foo: s.property.string(), foo: s.property.string(),
}, },
}); });
foo: string;
}
``` ```
### Models ### Models

View file

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