Compare commits

...

20 commits
v4.0.0 ... main

Author SHA1 Message Date
bf89dc00fe
Add model manager extension type.
All checks were successful
/ test (push) Successful in 40s
2025-06-29 16:41:14 +02:00
f238499f06
Improve jsdoc.
All checks were successful
/ test (push) Successful in 52s
2025-06-29 16:06:27 +02:00
d296658f64
Add model builder.
All checks were successful
/ test (push) Successful in 57s
2025-06-29 15:59:52 +02:00
e9cca58e4e
Version 4.1.0
All checks were successful
/ test (push) Successful in 32s
2025-06-28 23:13:18 +02:00
de27fb7837
Update vite and fix dependencies for build. 2025-06-28 23:12:42 +02:00
18a162c6d7
Fix remaining eslint errors.
All checks were successful
/ test (push) Successful in 38s
2025-06-28 23:01:14 +02:00
784f527a9e
Add eslint to check the code. 2025-06-28 23:00:55 +02:00
a4c1c88138
Refactor types tests.
All checks were successful
/ test (push) Successful in 30s
2025-06-28 22:46:25 +02:00
38c87249b1
Fix test status badge to target main branch.
All checks were successful
/ test (push) Successful in 45s
2025-06-28 21:52:22 +02:00
72417dd350
Change the recommended model declaration to allow models inheritance.
All checks were successful
/ test (push) Successful in 37s
2025-06-28 21:47:15 +02:00
5f1e2709bb
Update yarn to latest stable version.
All checks were successful
/ test (push) Successful in 36s
2025-06-28 19:47:28 +02:00
ed1bfd464a
Update dev dependencies.
Some checks failed
/ test (push) Has been cancelled
2025-06-23 20:41:19 +02:00
97a3c18082
Apply prettier.
All checks were successful
/ test (push) Successful in 25s
2025-06-23 20:39:08 +02:00
ecd8852afa
Add extend function to easily extend an inherited model.
All checks were successful
/ test (push) Successful in 40s
2025-06-22 23:25:43 +02:00
75b7b35dd6
Add map type and document it.
All checks were successful
/ test (push) Successful in 26s
2025-06-22 19:32:25 +02:00
2d86f0fa1a
Add applyPatch function to update models using objects returned by patch function.
All checks were successful
/ test (push) Successful in 56s
2025-06-22 17:25:12 +02:00
7707789bbf
Add from function to initialize a model and assign properties values using any object, silently ignoring fields which are not properties.
All checks were successful
/ test (push) Successful in 51s
2025-04-20 20:45:52 +02:00
fbd2763ea6
Add assign function to assign properties values using any object, silently ignoring fields which are not properties. 2025-04-20 20:38:00 +02:00
40b348862a
Add package installation command in README.
All checks were successful
/ test (push) Successful in 22s
2025-03-30 13:16:00 +02:00
1af46c0aaf
Update badges.
All checks were successful
/ test (push) Successful in 21s
2025-03-30 13:09:31 +02:00
41 changed files with 5337 additions and 1315 deletions

View file

@ -12,4 +12,5 @@ jobs:
with: with:
cache: "yarn" cache: "yarn"
- run: yarn install - run: yarn install
- run: yarn lint
- run: yarn coverage - run: yarn coverage

5
.prettierrc Normal file
View file

@ -0,0 +1,5 @@
{
"useTabs": true,
"trailingComma": "all",
"bracketSpacing": false
}

180
README.md
View file

@ -19,39 +19,49 @@
</p> </p>
<p align="center"> <p align="center">
<a href="https://www.npmjs.com/package/@sharkitek/core" target="_blank"> <img alt="Tests status" src="https://code.zeptotech.net/Sharkitek/Core/badges/workflows/test.yaml/badge.svg?branch=main" />
<img alt="Latest release" src="https://code.zeptotech.net/Sharkitek/Core/badges/release.svg" /> <a href="https://bundlephobia.com/package/@sharkitek/core" target="_blank">
<img alt="Bundle size" src="https://badgen.net/bundlephobia/minzip/@sharkitek/core" />
</a> </a>
<img alt="Tests status" src="https://code.zeptotech.net/Sharkitek/Core/badges/workflows/test.yaml/badge.svg" /> <a href="https://www.npmjs.com/package/@sharkitek/core" target="_blank">
<img alt="Latest release" src="https://badgen.net/npm/v/@sharkitek/core" />
</a>
<a href="https://bundlephobia.com/package/@sharkitek/core" target="_blank">
<img alt="Bundle size" src="https://badgen.net/bundlephobia/dependency-count/@sharkitek/core" />
</a>
<img alt="Latest release" src="https://badgen.net/npm/types/@sharkitek/core" />
</p> </p>
## Introduction ## Introduction
Sharkitek is a lightweight Javascript / TypeScript library designed to ease development of models. Sharkitek is a lightweight Javascript / TypeScript library designed to ease development of models.
```shell
yarn add @sharkitek/core
```
With Sharkitek, you define the architecture of your models by specifying their properties and their types. With Sharkitek, you define the architecture of your models by specifying their properties and their types.
Then, you can use the defined methods like `serialize`, `parse`, `patch` or `serializeDiff`. Then, you can use the defined methods like `serialize`, `parse`, `patch` or `serializeDiff`.
```typescript ```typescript
class Example class Example {
{ id: number;
static model = defineModel({ 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
@ -59,9 +69,18 @@ class Example
/** /**
* A person. * A person.
*/ */
class Person class Person {
{ id: number;
static model = defineModel({ 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(),
@ -71,39 +90,14 @@ 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
/** /**
* 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[] = [];
@ -113,26 +107,48 @@ 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
/** /**
* A model with composite keys. * A model with composite keys.
*/ */
class CompositeKeys class CompositeKeys {
{ id1: number;
static model = defineModel({ 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
@ -146,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",
@ -167,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",
@ -178,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
@ -189,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"]
``` ```
@ -208,22 +224,22 @@ Sharkitek defines some basic types by default, in these classes:
- `DateType`: date in the model, ISO formatted date in the serialized object. - `DateType`: date in the model, ISO formatted date in the serialized object.
- `ArrayType`: array in the model, array in the serialized object. - `ArrayType`: array in the model, array in the serialized object.
- `ObjectType`: object in the model, object in the serialized object. - `ObjectType`: object in the model, object in the serialized object.
- `MapType`: map in the model, record object in the serialized object.
- `ModelType`: instance of a specific class in the model, object in the serialized object. - `ModelType`: instance of a specific class in the model, object in the serialized object.
When you are defining a property of a Sharkitek model, you must provide its type by instantiating one of these classes. When you are defining a property of a Sharkitek model, you must provide its type by instantiating one of these classes.
```typescript ```typescript
class Example class Example {
{ foo: string;
static model = defineModel({ }
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.
@ -235,22 +251,22 @@ To ease the use of these classes and reduce read complexity, properties of each
- `DateType` => `s.property.date` - `DateType` => `s.property.date`
- `ArrayType` => `s.property.array` - `ArrayType` => `s.property.array`
- `ObjectType` => `s.property.object` - `ObjectType` => `s.property.object`
- `MapType` => `s.property.map` or `s.property.stringMap`
- `ModelType` => `s.property.model` - `ModelType` => `s.property.model`
Type implementers should provide a corresponding function for each defined type. They can even provide multiple functions or constants with predefined parameters. For example, we could define `s.property.stringArray()` which would be similar to `s.property.array(s.property.string())`. Type implementers should provide a corresponding function for each defined type. They can even provide multiple functions or constants with predefined parameters. For example, we could define `s.property.stringArray()` which would be similar to `s.property.array(s.property.string())`.
```typescript ```typescript
class Example class Example {
{ foo: string;
static model = defineModel({ }
const ExampleModel = defineModel({
Class: Example, Class: Example,
properties: { properties: {
foo: s.property.string(), foo: s.property.string(),
}, },
}); });
foo: string;
}
``` ```
### Models ### Models
@ -263,6 +279,30 @@ Get a model class (which has all the sharkitek models' functions) from a model i
const model = definedModel.model(modelInstance); const model = definedModel.model(modelInstance);
``` ```
#### `assign(object)`
Assign fields from a provided object to the model instance properties. Fields which are not properties of the target model are silently ignored.
```typescript
const alteredModelInstance = definedModel.model(modelInstance).assign({
anyProperty: "foo",
anotherOne: true,
not_a_property: "will be ignored",
});
```
#### `from(object)`
Initialize a model instance and assign the provided fields to its properties. Fields which are not properties of the target model are silently ignored.
```typescript
const newModelInstance = definedModel.from({
anyProperty: "foo",
anotherOne: true,
not_a_property: "will be ignored",
});
```
#### `serialize()` #### `serialize()`
Serialize the model. Serialize the model.

23
eslint.config.js Normal file
View file

@ -0,0 +1,23 @@
import js from "@eslint/js";
import globals from "globals";
import tseslint from "typescript-eslint";
import {defineConfig, globalIgnores} from "eslint/config";
export default defineConfig([
globalIgnores([".yarn/**", "coverage/**", "lib/**"]),
{
files: ["**/*.{js,mjs,cjs,ts,mts,cts}"],
plugins: {js},
extends: ["js/recommended"],
},
{
files: ["**/*.{js,mjs,cjs,ts,mts,cts}"],
languageOptions: {globals: {...globals.browser, ...globals.node}},
},
tseslint.configs.recommended,
{
rules: {
"@typescript-eslint/no-explicit-any": "off",
},
},
]);

View file

@ -1,6 +1,6 @@
{ {
"name": "@sharkitek/core", "name": "@sharkitek/core",
"version": "4.0.0", "version": "4.1.0",
"description": "TypeScript library for well-designed model architectures.", "description": "TypeScript library for well-designed model architectures.",
"keywords": [ "keywords": [
"deserialization", "deserialization",
@ -25,7 +25,9 @@
"scripts": { "scripts": {
"build": "tsc && vite build", "build": "tsc && vite build",
"test": "vitest", "test": "vitest",
"coverage": "vitest run --coverage" "coverage": "vitest run --coverage",
"format": "prettier . --write",
"lint": "eslint"
}, },
"type": "module", "type": "module",
"source": "src/library.ts", "source": "src/library.ts",
@ -35,13 +37,18 @@
"lib/**/*" "lib/**/*"
], ],
"devDependencies": { "devDependencies": {
"@types/node": "^22.13.14", "@eslint/js": "^9.30.0",
"@vitest/coverage-v8": "^3.0.9", "@types/node": "^24.0.3",
"@vitest/coverage-v8": "^3.2.4",
"eslint": "^9.30.0",
"globals": "^16.2.0",
"prettier": "^3.6.0",
"ts-node": "^10.9.2", "ts-node": "^10.9.2",
"typescript": "^5.8.2", "typescript": "^5.8.3",
"vite": "^6.2.3", "typescript-eslint": "^8.35.0",
"vite-plugin-dts": "^4.5.3", "vite": "^7.0.0",
"vitest": "^3.0.9" "vite-plugin-dts": "^4.5.4",
"vitest": "^3.2.4"
}, },
"packageManager": "yarn@4.6.0" "packageManager": "yarn@4.9.2"
} }

View file

@ -1,4 +1,3 @@
export * from "./sharkitek-error"; export * from "./sharkitek-error";
export * from "./type-error"; export * from "./type-error";
export * from "./invalid-type-value-error"; export * from "./invalid-type-value-error";

View file

@ -4,10 +4,15 @@ import {Type} from "../model/types/type";
/** /**
* A Sharkitek type error when the passed value is invalid. * A Sharkitek type error when the passed value is invalid.
*/ */
export class InvalidTypeValueError<SerializedType, ModelType> extends TypeError<SerializedType, ModelType> export class InvalidTypeValueError<SerializedType, ModelType> extends TypeError<
{ SerializedType,
constructor(public type: Type<SerializedType, ModelType>, public value: any, message?: string) ModelType
{ > {
super(type, message ?? `${JSON.stringify(value)} is an invalid value`) constructor(
public type: Type<SerializedType, ModelType>,
public value: any,
message?: string,
) {
super(type, message ?? `${JSON.stringify(value)} is an invalid value`);
} }
} }

View file

@ -1,6 +1,4 @@
/** /**
* A Sharkitek error. * A Sharkitek error.
*/ */
export class SharkitekError extends Error export class SharkitekError extends Error {}
{
}

View file

@ -4,10 +4,13 @@ import {Type} from "../model/types/type";
/** /**
* A Sharkitek type error. * A Sharkitek type error.
*/ */
export class TypeError<SerializedType, ModelType> extends SharkitekError export class TypeError<SerializedType, ModelType> extends SharkitekError {
{ constructor(
constructor(public type: Type<SerializedType, ModelType>, message?: string) public type: Type<SerializedType, ModelType>,
{ message?: string,
super(`Error in type ${type.constructor.name}${message ? `: ${message}` : ""}`); ) {
super(
`Error in type ${type.constructor.name}${message ? `: ${message}` : ""}`,
);
} }
} }

View file

@ -1,5 +1,5 @@
import * as s from "./model"; import * as s from "./model";
export * from "./model"; export * from "./model";
export * from "./errors"; export * from "./errors";
export { s }; export {s};
export default s; export default s;

77
src/model/builder.ts Normal file
View file

@ -0,0 +1,77 @@
import {
defineModel,
IdentifierDefinition,
ModelDefinition,
ModelShape,
} from "./model";
import {ConstructorOf} from "../utils";
import {Definition} from "./property-definition";
/**
* Model definition builder.
*/
export class ModelBuilder<
T extends object,
Shape extends ModelShape<T>,
Identifier extends IdentifierDefinition<T, Shape>,
> {
/**
* The built model definition.
*/
definition: ModelDefinition<T, Shape, Identifier>;
/**
* Define a new property.
* @param name The new property name.
* @param definition The new property definition.
*/
property<
SerializedType,
PropertyName extends Exclude<keyof T, keyof Shape>,
PropertyDefinition extends Definition<SerializedType, T[PropertyName]>,
>(name: PropertyName, definition: PropertyDefinition) {
(this.definition.properties[name] as Definition<unknown, T[typeof name]>) =
definition;
return this as unknown as ModelBuilder<
T,
Shape & {[k in PropertyName]: PropertyDefinition},
Identifier
>;
}
/**
* Set the model identifier.
* @param identifier The new model identifier.
*/
identifier<NewIdentifier extends IdentifierDefinition<T, Shape>>(
identifier: NewIdentifier,
) {
(this.definition.identifier as unknown) = identifier;
return this as unknown as ModelBuilder<T, Shape, NewIdentifier>;
}
/**
* Define a model using the current model definition.
*/
define() {
return defineModel(this.definition);
}
}
/**
* Initialize a model builder for the provided class.
* @param Class The class for which to build a model.
*/
export function newModel<
T extends object,
Shape extends ModelShape<T> = object,
Identifier extends IdentifierDefinition<T, Shape> = never,
>(Class: ConstructorOf<T>): ModelBuilder<T, Shape, Identifier> {
const builder = new ModelBuilder<T, Shape, Identifier>();
builder.definition = {
Class,
properties: {} as Shape,
};
return builder;
}

View file

@ -1,8 +1,8 @@
import * as property from "./properties"; export * as property from "./properties";
export {property};
export * from "./model"; export * from "./model";
export {Definition} from "./property-definition"; export {Definition} from "./property-definition";
export {newModel, ModelBuilder} from "./builder";
export {ArrayType} from "./types/array"; export {ArrayType} from "./types/array";
export {BooleanType} from "./types/boolean"; export {BooleanType} from "./types/boolean";

View file

@ -1,5 +1,5 @@
import {Definition, UnknownDefinition} from "./property-definition"; import {Definition, UnknownDefinition} from "./property-definition";
import {ConstructorOf} from "../utils"; import {ConstructorOf, Modify} from "../utils";
/** /**
* A model shape. * A model shape.
@ -11,7 +11,10 @@ export type ModelShape<T extends object> = Partial<{
/** /**
* Properties values of a model based on its shape. * Properties values of a model based on its shape.
*/ */
export type ModelPropertiesValues<T extends object, Shape extends ModelShape<T>> = { export type ModelPropertiesValues<
T extends object,
Shape extends ModelShape<T>,
> = {
[k in keyof Shape]: Shape[k]["_sharkitek"]; [k in keyof Shape]: Shape[k]["_sharkitek"];
}; };
@ -26,20 +29,39 @@ export type SerializedModel<T extends object, Shape extends ModelShape<T>> = {
* This is an experimental serialized model type declaration. * This is an experimental serialized model type declaration.
* @deprecated * @deprecated
*/ */
type ExperimentalSerializedModel<T extends object, Shape extends ModelShape<T>> // eslint-disable-next-line @typescript-eslint/no-unused-vars
= Omit<ExperimentalSerializedModelBase<T, Shape>, ExperimentalSerializedModelOptionalKeys<T, Shape>> type ExperimentalSerializedModel<
& Pick<Partial<ExperimentalSerializedModelBase<T, Shape>>, ExperimentalSerializedModelOptionalKeys<T, Shape>>; T extends object,
type ExperimentalSerializedModelBase<T extends object, Shape extends ModelShape<T>> = { Shape extends ModelShape<T>,
> = Omit<
ExperimentalSerializedModelBase<T, Shape>,
ExperimentalSerializedModelOptionalKeys<T, Shape>
> &
Pick<
Partial<ExperimentalSerializedModelBase<T, Shape>>,
ExperimentalSerializedModelOptionalKeys<T, Shape>
>;
type ExperimentalSerializedModelBase<
T extends object,
Shape extends ModelShape<T>,
> = {
[k in keyof Shape]: Shape[k]["_serialized"]; [k in keyof Shape]: Shape[k]["_serialized"];
}; };
type ExperimentalSerializedModelOptionalKeys<T extends object, Shape extends ModelShape<T>> = { type ExperimentalSerializedModelOptionalKeys<
[k in keyof Shape]: Shape[k]["_serialized"] extends undefined ? k : never T extends object,
Shape extends ModelShape<T>,
> = {
[k in keyof Shape]: Shape[k]["_serialized"] extends undefined ? k : never;
}[keyof Shape]; }[keyof Shape];
/** /**
* A sharkitek model instance, with internal model state. * A sharkitek model instance, with internal model state.
*/ */
export type ModelInstance<T extends object, Shape extends ModelShape<T>, Identifier extends IdentifierDefinition<T, Shape>> = T & { export type ModelInstance<
T extends object,
Shape extends ModelShape<T>,
Identifier extends IdentifierDefinition<T, Shape>,
> = T & {
/** /**
* The Sharkitek model state. * The Sharkitek model state.
*/ */
@ -49,19 +71,34 @@ export type ModelInstance<T extends object, Shape extends ModelShape<T>, Identif
/** /**
* Identifier definition type. * Identifier definition type.
*/ */
export type IdentifierDefinition<T extends object, Shape extends ModelShape<T>> = (keyof Shape)|((keyof Shape)[]); export type IdentifierDefinition<
T extends object,
Shape extends ModelShape<T>,
> = keyof Shape | (keyof Shape)[];
/** /**
* Identifier type. * Identifier type.
*/ */
export type IdentifierType<T extends object, Shape extends ModelShape<T>, Identifier extends IdentifierDefinition<T, Shape>> export type IdentifierType<
= Identifier extends keyof Shape ? Shape[Identifier]["_sharkitek"] : { [K in keyof Identifier]: Identifier[K] extends keyof Shape ? Shape[Identifier[K]]["_sharkitek"] : unknown }; T extends object,
Shape extends ModelShape<T>,
Identifier extends IdentifierDefinition<T, Shape>,
> = Identifier extends keyof Shape
? Shape[Identifier]["_sharkitek"]
: {
[K in keyof Identifier]: Identifier[K] extends keyof Shape
? Shape[Identifier[K]]["_sharkitek"]
: unknown;
};
/** /**
* A model definition object. * A model definition object.
*/ */
export interface ModelDefinition<T extends object, Shape extends ModelShape<T>, Identifier extends IdentifierDefinition<T, Shape>> export interface ModelDefinition<
{ T extends object,
Shape extends ModelShape<T>,
Identifier extends IdentifierDefinition<T, Shape>,
> {
/** /**
* Model class. * Model class.
*/ */
@ -83,8 +120,7 @@ export interface ModelDefinition<T extends object, Shape extends ModelShape<T>,
/** /**
* A model property. * A model property.
*/ */
export interface ModelProperty<T extends object, Shape extends ModelShape<T>> export interface ModelProperty<T extends object, Shape extends ModelShape<T>> {
{
/** /**
* Property name. * Property name.
*/ */
@ -104,13 +140,19 @@ export interface ModelProperty<T extends object, Shape extends ModelShape<T>>
/** /**
* Model properties iterator object. * Model properties iterator object.
*/ */
export type ModelProperties<T extends object, Shape extends ModelShape<T>> = ModelProperty<T, Shape>[]; export type ModelProperties<
T extends object,
Shape extends ModelShape<T>,
> = ModelProperty<T, Shape>[];
/** /**
* A Sharkitek model state. * A Sharkitek model state.
*/ */
export class Model<T extends object, Shape extends ModelShape<T>, Identifier extends IdentifierDefinition<T, Shape>> export class Model<
{ T extends object,
Shape extends ModelShape<T>,
Identifier extends IdentifierDefinition<T, Shape>,
> {
/** /**
* The model manager instance. * The model manager instance.
*/ */
@ -144,15 +186,14 @@ export class Model<T extends object, Shape extends ModelShape<T>, Identifier ext
/** /**
* The original serialized object, if there is one. * The original serialized object, if there is one.
*/ */
serialized: SerializedModel<T, Shape>|null; serialized: SerializedModel<T, Shape> | null;
}; };
/** /**
* Initialize a new model state with the defined properties. * Initialize a new model state with the defined properties.
* @param manager The model manager. * @param manager The model manager.
*/ */
constructor(manager: ModelManager<T, Shape, Identifier>) constructor(manager: ModelManager<T, Shape, Identifier>) {
{
this.manager = manager; this.manager = manager;
this.definition = manager.definition; this.definition = manager.definition;
this.properties = manager.properties; this.properties = manager.properties;
@ -161,11 +202,10 @@ export class Model<T extends object, Shape extends ModelShape<T>, Identifier ext
/** /**
* Initialize the Sharkitek model state for a new instance. * Initialize the Sharkitek model state for a new instance.
*/ */
initInstance(): this initInstance(): this {
{
return this.fromInstance( return this.fromInstance(
// Initialize a new model instance. // Initialize a new model instance.
new (this.definition.Class)() new this.definition.Class(),
); );
} }
@ -173,8 +213,7 @@ export class Model<T extends object, Shape extends ModelShape<T>, Identifier ext
* Initialize the Sharkitek model state for the provided instance. * Initialize the Sharkitek model state for the provided instance.
* @param instance The model instance. * @param instance The model instance.
*/ */
fromInstance(instance: T): this fromInstance(instance: T): this {
{
// Initialize the sharkitek model instance. // Initialize the sharkitek model instance.
const sharkitekInstance = instance as ModelInstance<T, Shape, Identifier>; const sharkitekInstance = instance as ModelInstance<T, Shape, Identifier>;
@ -188,8 +227,8 @@ export class Model<T extends object, Shape extends ModelShape<T>, Identifier ext
if (originalInstance) if (originalInstance)
// Share the same original values object. // Share the same original values object.
this.original = originalInstance.original; this.original = originalInstance.original;
else else {
{ // Initialize a new original values object, based on the current values of the instance. // Initialize a new original values object, based on the current values of the instance.
this.original = { this.original = {
properties: undefined, properties: undefined,
serialized: null, serialized: null,
@ -204,14 +243,14 @@ export class Model<T extends object, Shape extends ModelShape<T>, Identifier ext
* Deserialize provided data to a new model instance. * Deserialize provided data to a new model instance.
* @param serialized Serialized model. * @param serialized Serialized model.
*/ */
deserialize(serialized: SerializedModel<T, Shape>): this deserialize(serialized: SerializedModel<T, Shape>): this {
{
// Initialize a new model instance. // Initialize a new model instance.
this.initInstance(); this.initInstance();
for (const property of this.properties) for (const property of this.properties) {
{ // For each defined model property, assigning its deserialized value. // For each defined model property, assigning its deserialized value.
(this.instance[property.name as keyof T] as any) = property.definition.type.deserialize(serialized[property.name]); (this.instance[property.name as keyof T] as any) =
property.definition.type.deserialize(serialized[property.name]);
} }
// Reset original property values. // Reset original property values.
@ -225,29 +264,31 @@ export class Model<T extends object, Shape extends ModelShape<T>, Identifier ext
/** /**
* Get current model instance identifier. * Get current model instance identifier.
*/ */
getIdentifier(): IdentifierType<T, Shape, Identifier> getIdentifier(): IdentifierType<T, Shape, Identifier> {
{ if (Array.isArray(this.definition.identifier)) {
if (Array.isArray(this.definition.identifier)) // The identifier is composite, make an array of properties values.
{ // The identifier is composite, make an array of properties values. return this.definition.identifier.map(
return this.definition.identifier.map(identifier => this.instance?.[identifier as keyof T]) as IdentifierType<T, Shape, Identifier>; (identifier) => this.instance?.[identifier as keyof T],
} ) as IdentifierType<T, Shape, Identifier>;
else } else {
{ // The identifier is a simple property, get its value. // The identifier is a simple property, get its value.
return this.instance?.[this.definition.identifier as keyof Shape as keyof T] as IdentifierType<T, Shape, Identifier>; return this.instance?.[
this.definition.identifier as keyof Shape as keyof T
] as IdentifierType<T, Shape, Identifier>;
} }
} }
/** /**
* Get current model instance properties. * Get current model instance properties.
*/ */
getInstanceProperties(): ModelPropertiesValues<T, Shape> getInstanceProperties(): ModelPropertiesValues<T, Shape> {
{
// Initialize an empty model properties object. // Initialize an empty model properties object.
const instanceProperties: Partial<ModelPropertiesValues<T, Shape>> = {}; const instanceProperties: Partial<ModelPropertiesValues<T, Shape>> = {};
for (const property of this.properties) for (const property of this.properties) {
{ // For each defined model property, adding it to the properties object. // For each defined model property, adding it to the properties object.
instanceProperties[property.name] = this.instance?.[property.name as keyof T]; // keyof Shape is a subset of keyof T. instanceProperties[property.name] =
this.instance?.[property.name as keyof T]; // keyof Shape is a subset of keyof T.
} }
return instanceProperties as ModelPropertiesValues<T, Shape>; // Returning the properties object. return instanceProperties as ModelPropertiesValues<T, Shape>; // Returning the properties object.
@ -256,13 +297,12 @@ export class Model<T extends object, Shape extends ModelShape<T>, Identifier ext
/** /**
* Serialize the model instance. * Serialize the model instance.
*/ */
serialize(): SerializedModel<T, Shape> serialize(): SerializedModel<T, Shape> {
{
// Creating an empty serialized object. // Creating an empty serialized object.
const serializedObject: Partial<SerializedModel<T, Shape>> = {}; const serializedObject: Partial<SerializedModel<T, Shape>> = {};
for (const property of this.properties) for (const property of this.properties) {
{ // For each defined model property, adding it to the serialized object. // For each defined model property, adding it to the serialized object.
serializedObject[property.name] = property.definition.type.serialize( serializedObject[property.name] = property.definition.type.serialize(
// keyof Shape is a subset of keyof T. // keyof Shape is a subset of keyof T.
this.instance?.[property.name as keyof T], this.instance?.[property.name as keyof T],
@ -275,19 +315,22 @@ export class Model<T extends object, Shape extends ModelShape<T>, Identifier ext
/** /**
* Examine if the model is new (never deserialized) or not. * Examine if the model is new (never deserialized) or not.
*/ */
isNew(): boolean isNew(): boolean {
{
return !this.original.serialized; return !this.original.serialized;
} }
/** /**
* Examine if the model is dirty or not. * Examine if the model is dirty or not.
*/ */
isDirty(): boolean isDirty(): boolean {
{ for (const property of this.properties) {
for (const property of this.properties) // For each property, check if it is different.
{ // For each property, check if it is different. if (
if (property.definition.type.hasChanged(this.original.properties?.[property.name], this.instance?.[property.name as keyof T])) property.definition.type.hasChanged(
this.original.properties?.[property.name],
this.instance?.[property.name as keyof T],
)
)
// There is a difference: the model is dirty. // There is a difference: the model is dirty.
return true; return true;
} }
@ -299,19 +342,23 @@ export class Model<T extends object, Shape extends ModelShape<T>, Identifier ext
/** /**
* Serialize the difference between current model state and the original one. * Serialize the difference between current model state and the original one.
*/ */
serializeDiff(): Partial<SerializedModel<T, Shape>> serializeDiff(): Partial<SerializedModel<T, Shape>> {
{
// Creating an empty serialized object. // Creating an empty serialized object.
const serializedObject: Partial<SerializedModel<T, Shape>> = {}; const serializedObject: Partial<SerializedModel<T, Shape>> = {};
for (const property of this.properties) for (const property of this.properties) {
{ // For each defined model property, adding it to the serialized object if it has changed or if it is in the identifier. // For each defined model property, adding it to the serialized object if it has changed or if it is in the identifier.
const instancePropValue = this.instance?.[property.name as keyof T]; // keyof Shape is a subset of keyof T. const instancePropValue = this.instance?.[property.name as keyof T]; // keyof Shape is a subset of keyof T.
if ( if (
property.identifier || property.identifier ||
property.definition.type.hasChanged(this.original.properties?.[property.name], instancePropValue) property.definition.type.hasChanged(
) // The property is part of the identifier or its value has changed. this.original.properties?.[property.name],
serializedObject[property.name] = property.definition.type.serializeDiff(instancePropValue); instancePropValue,
)
)
// The property is part of the identifier or its value has changed.
serializedObject[property.name] =
property.definition.type.serializeDiff(instancePropValue);
} }
return serializedObject; // Returning the serialized object. return serializedObject; // Returning the serialized object.
@ -320,13 +367,13 @@ export class Model<T extends object, Shape extends ModelShape<T>, Identifier ext
/** /**
* Set current model properties as original values. * Set current model properties as original values.
*/ */
resetDiff(): void resetDiff(): void {
{
this.original.properties = {}; this.original.properties = {};
for (const property of this.properties) for (const property of this.properties) {
{ // For each property, set its original value to the current property value. // For each property, set its original value to the current property value.
const instancePropValue = this.instance?.[property.name as keyof T]; const instancePropValue = this.instance?.[property.name as keyof T];
this.original.properties[property.name] = property.definition.type.clone(instancePropValue); this.original.properties[property.name] =
property.definition.type.clone(instancePropValue);
property.definition.type.resetDiff(instancePropValue); property.definition.type.resetDiff(instancePropValue);
} }
} }
@ -335,8 +382,7 @@ export class Model<T extends object, Shape extends ModelShape<T>, Identifier ext
* Get difference between original values and current ones, then reset it. * Get difference between original values and current ones, then reset it.
* Similar to call `serializeDiff()` then `resetDiff()`. * Similar to call `serializeDiff()` then `resetDiff()`.
*/ */
patch(): Partial<SerializedModel<T, Shape>> patch(): Partial<SerializedModel<T, Shape>> {
{
// Get the difference. // Get the difference.
const diff = this.serializeDiff(); const diff = this.serializeDiff();
@ -349,32 +395,36 @@ export class Model<T extends object, Shape extends ModelShape<T>, Identifier ext
/** /**
* Clone the model instance. * Clone the model instance.
*/ */
clone(): ModelInstance<T, Shape, Identifier> clone(): ModelInstance<T, Shape, Identifier> {
{
// Initialize a new instance for the clone. // Initialize a new instance for the clone.
const cloned = this.manager.model(); const cloned = this.manager.model();
// Clone every value of the model instance. // Clone every value of the model instance.
for (const [key, value] of Object.entries(this.instance) as [keyof T, unknown][]) for (const [key, value] of Object.entries(this.instance) as [
{ // For each [key, value], clone the value and put it in the cloned instance. keyof T,
unknown,
][]) {
// For each [key, value], clone the value and put it in the cloned instance.
// Do not clone ourselves. // Do not clone ourselves.
if (key == "_sharkitek") continue; if (key == "_sharkitek") continue;
if (this.definition.properties[key]) if (this.definition.properties[key]) {
{ // The current key is a defined property, clone using the defined type. // The current key is a defined property, clone using the defined type.
(cloned.instance[key] as any) = (this.definition.properties[key] as UnknownDefinition).type.clone(value); (cloned.instance[key] as any) = (
} this.definition.properties[key] as UnknownDefinition
else ).type.clone(value);
{ // Not a property, cloning the raw value. } else {
// Not a property, cloning the raw value.
(cloned.instance[key] as any) = structuredClone(value); (cloned.instance[key] as any) = structuredClone(value);
} }
} }
// Clone original properties. // Clone original properties.
for (const property of this.properties) for (const property of this.properties) {
{ // For each property, clone its original value. // For each property, clone its original value.
cloned.original.properties[property.name] = property.definition.type.clone(this.original.properties[property.name]); cloned.original.properties[property.name] =
property.definition.type.clone(this.original.properties[property.name]);
} }
// Clone original serialized. // Clone original serialized.
@ -382,15 +432,78 @@ export class Model<T extends object, Shape extends ModelShape<T>, Identifier ext
return cloned.instance; // Returning the cloned instance. return cloned.instance; // Returning the cloned instance.
} }
/**
* Assign the provided fields to existing properties.
* Fields that cannot be matched to existing properties are silently ignored.
* @param fields The fields to assign to the model.
*/
assign(
fields: Partial<ModelPropertiesValues<T, Shape>> & {[field: string]: any},
): ModelInstance<T, Shape, Identifier> {
for (const field in fields) {
// For each field, if it's a property, assign its value.
if ((this.definition.properties as any)?.[field])
// Set the instance value.
this.instance[field as keyof T] = fields[field];
}
return this.instance;
}
/**
* Apply a patch to the model instance. All known fields will be deserialized and assigned to the properties.
* @param patch The patch object to apply.
* @param updateOriginals Indicates if the original properties values must be updated or not. By default, they are reset.
*/
applyPatch(
patch: SerializedModel<T, Shape>,
updateOriginals: boolean = true,
): ModelInstance<T, Shape, Identifier> {
if (updateOriginals) {
// If serialized original is null and we need to update it, initialize it.
this.original.serialized = this.serialize();
}
for (const serializedField in patch) {
// For each field, if it's a property, assign its value.
// Get the property definition.
const property =
this.definition.properties[serializedField as keyof Shape];
if (property) {
// Found a matching model property, assigning its deserialized value.
(this.instance[serializedField as keyof Shape as keyof T] as any) = (
property as UnknownDefinition
).type.applyPatch(
this.instance[serializedField as keyof Shape as keyof T],
patch[serializedField],
updateOriginals,
);
if (updateOriginals) {
// Update original values.
// Set original property value.
(this.original.properties[serializedField] as any) = (
property as UnknownDefinition
).type.clone(
this.instance[serializedField as keyof Shape as keyof T],
);
// Set original serialized value.
this.original.serialized[serializedField] = patch[serializedField];
}
}
}
return this.instance;
}
} }
/** /**
* A model manager, created from a model definition. * A model manager, created from a model definition.
*/ */
export class ModelManager<T extends object, Shape extends ModelShape<T>, Identifier extends IdentifierDefinition<T, Shape>> export class ModelManager<
{ T extends object,
Shape extends ModelShape<T>,
Identifier extends IdentifierDefinition<T, Shape>,
> {
/** /**
* Defined properties. * Defined properties.
*/ */
@ -400,8 +513,9 @@ export class ModelManager<T extends object, Shape extends ModelShape<T>, Identif
* Initialize a model manager from a model definition. * Initialize a model manager from a model definition.
* @param definition The model definition. * @param definition The model definition.
*/ */
constructor(public readonly definition: ModelDefinition<T, Shape, Identifier>) constructor(
{ public readonly definition: ModelDefinition<T, Shape, Identifier>,
) {
this.initProperties(); this.initProperties();
} }
@ -409,23 +523,22 @@ export class ModelManager<T extends object, Shape extends ModelShape<T>, Identif
* Initialize properties iterator from current definition. * Initialize properties iterator from current definition.
* @protected * @protected
*/ */
protected initProperties(): void protected initProperties(): void {
{
// Build an array of model properties from the definition. // Build an array of model properties from the definition.
this.properties = []; this.properties = [];
for (const propertyName in this.definition.properties) for (const propertyName in this.definition.properties) {
{ // For each property, build a model property object. // For each property, build a model property object.
this.properties.push({ this.properties.push({
name: propertyName, name: propertyName,
definition: this.definition.properties[propertyName], definition: this.definition.properties[propertyName],
// Find out if the current property is part of the identifier. // Find out if the current property is part of the identifier.
identifier: ( identifier: Array.isArray(this.definition.identifier)
Array.isArray(this.definition.identifier) ? // The identifier is an array, the property must be in the array.
// The identifier is an array, the property must be in the array. this.definition.identifier.includes(
? this.definition.identifier.includes(propertyName as keyof Shape as keyof T) propertyName as keyof Shape as keyof T,
// The identifier is a single string, the property must be the defined identifier. )
: (this.definition.identifier == propertyName as keyof Shape) : // The identifier is a single string, the property must be the defined identifier.
), this.definition.identifier == (propertyName as keyof Shape),
} as ModelProperty<T, Shape>); } as ModelProperty<T, Shape>);
} }
} }
@ -434,49 +547,121 @@ export class ModelManager<T extends object, Shape extends ModelShape<T>, Identif
* Get the model state of the provided model instance. * Get the model state of the provided model instance.
* @param instance The model instance for which to get its state. NULL or undefined to create a new one. * @param instance The model instance for which to get its state. NULL or undefined to create a new one.
*/ */
model(instance: T|ModelInstance<T, Shape, Identifier>|null = null): Model<T, Shape, Identifier> model(
{ // Get the instance model state if there is one, or initialize a new one. instance: T | ModelInstance<T, Shape, Identifier> | null = null,
): Model<T, Shape, Identifier> {
// Get the instance model state if there is one, or initialize a new one.
if (instance) if (instance)
// There is an instance, create a model from it. // There is an instance, create a model from it.
return ((instance as ModelInstance<T, Shape, Identifier>)?._sharkitek ?? (new Model<T, Shape, Identifier>(this))).fromInstance(instance); return (
(instance as ModelInstance<T, Shape, Identifier>)?._sharkitek ??
new Model<T, Shape, Identifier>(this)
).fromInstance(instance);
else else
// No instance, initialize a new one. // No instance, initialize a new one.
return (new Model<T, Shape, Identifier>(this)).initInstance(); return new Model<T, Shape, Identifier>(this).initInstance();
}
/**
* Initialize a new model instance with the provided object properties values.
* Fields that cannot be matched to existing properties are silently ignored.
* @param fields
*/
from(
fields: Partial<ModelPropertiesValues<T, Shape>> & {[field: string]: any},
): ModelInstance<T, Shape, Identifier> {
return this.model().assign(fields);
} }
/** /**
* Parse the serialized model object to a new model instance. * Parse the serialized model object to a new model instance.
* @param serialized The serialized model object. * @param serialized The serialized model object.
*/ */
parse(serialized: SerializedModel<T, Shape>): ModelInstance<T, Shape, Identifier> parse(
{ serialized: SerializedModel<T, Shape>,
): ModelInstance<T, Shape, Identifier> {
return this.model().deserialize(serialized).instance; return this.model().deserialize(serialized).instance;
} }
} }
/**
* A model manager extension is a mixin, building a new model manager class with extended capabilities.
* @see https://www.typescriptlang.org/docs/handbook/mixins.html
*/
export type ModelManagerExtension<
Extension extends object,
T extends object,
Shape extends ModelShape<T>,
Identifier extends IdentifierDefinition<T, Shape>,
> = (
modelManager: ModelManager<T, Shape, Identifier>,
) => ModelManager<T, Shape, Identifier> & Extension;
/** /**
* Define a new model. * Define a new model.
* @param definition The model definition object. * @param definition The model definition object.
*/ */
export function defineModel<T extends object, Shape extends ModelShape<T>, Identifier extends IdentifierDefinition<T, Shape>>( export function defineModel<
definition: ModelDefinition<T, Shape, Identifier> T extends object,
) Shape extends ModelShape<T>,
{ Identifier extends IdentifierDefinition<T, Shape>,
>(definition: ModelDefinition<T, Shape, Identifier>) {
return new ModelManager<T, Shape, Identifier>(definition); return new ModelManager<T, Shape, Identifier>(definition);
} }
/**
* Define a new model, extending an existing one.
* @param extendedModel The extended model manager instance.
* @param definition The extension of the model definition object.
*/
export function extend<
ExtT extends object,
ExtShape extends ModelShape<ExtT>,
ExtIdentifier extends IdentifierDefinition<ExtT, ExtShape>,
T extends ExtT,
Shape extends ModelShape<T>,
Identifier extends IdentifierDefinition<T, Shape>,
ResIdentifier extends IdentifierDefinition<T, ResShape>,
ResShape extends ModelShape<T> = Modify<ExtShape, Shape>,
>(
extendedModel: ModelManager<ExtT, ExtShape, ExtIdentifier>,
definition: ModelDefinition<T, Shape, Identifier>,
) {
const {properties: extendedProperties, ...overridableDefinition} =
extendedModel.definition;
const {properties: propertiesExtension, ...definitionExtension} = definition;
return new ModelManager({
...overridableDefinition,
...definitionExtension,
properties: {
...extendedProperties,
...propertiesExtension,
},
}) as unknown as ModelManager<T, ResShape, ResIdentifier>;
}
/** /**
* A generic model manager for a provided model type, to use in circular dependencies. * A generic model manager for a provided model type, to use in circular dependencies.
*/ */
export type GenericModelManager<T extends object> = ModelManager<T, ModelShape<T>, IdentifierDefinition<T, ModelShape<T>>>; export type GenericModelManager<T extends object> = ModelManager<
T,
ModelShape<T>,
IdentifierDefinition<T, ModelShape<T>>
>;
/** /**
* Function to get a model manager lazily. * Function to get a model manager lazily.
*/ */
export type LazyModelManager<T extends object, Shape extends ModelShape<T>, Identifier extends IdentifierDefinition<T, Shape>> export type LazyModelManager<
= (() => ModelManager<T, Shape, Identifier>); T extends object,
Shape extends ModelShape<T>,
Identifier extends IdentifierDefinition<T, Shape>,
> = () => ModelManager<T, Shape, Identifier>;
/** /**
* A model manager definition that can be lazy. * A model manager definition that can be lazy.
*/ */
export type DeclaredModelManager<T extends object, Shape extends ModelShape<T>, Identifier extends IdentifierDefinition<T, Shape>> export type DeclaredModelManager<
= ModelManager<T, Shape, Identifier>|LazyModelManager<T, Shape, Identifier>; T extends object,
Shape extends ModelShape<T>,
Identifier extends IdentifierDefinition<T, Shape>,
> = ModelManager<T, Shape, Identifier> | LazyModelManager<T, Shape, Identifier>;

View file

@ -8,3 +8,5 @@ export {model} from "./types/model";
export {numeric} from "./types/numeric"; export {numeric} from "./types/numeric";
export {object} from "./types/object"; export {object} from "./types/object";
export {string} from "./types/string"; export {string} from "./types/string";
export {map} from "./types/map";
export {stringMap} from "./types/map";

View file

@ -3,8 +3,7 @@ import {Type} from "./types/type";
/** /**
* Property definition class. * Property definition class.
*/ */
export class Definition<SerializedType, ModelType> export class Definition<SerializedType, ModelType> {
{
readonly _sharkitek: ModelType; readonly _sharkitek: ModelType;
readonly _serialized: SerializedType; readonly _serialized: SerializedType;
@ -12,8 +11,7 @@ export class Definition<SerializedType, ModelType>
* Create a property definer instance. * Create a property definer instance.
* @param type Property type. * @param type Property type.
*/ */
constructor(public readonly type: Type<SerializedType, ModelType>) constructor(public readonly type: Type<SerializedType, ModelType>) {}
{}
} }
/** /**
@ -30,7 +28,8 @@ export type AnyDefinition = Definition<any, any>;
* New definition of a property of the given type. * New definition of a property of the given type.
* @param type Type of the property to define. * @param type Type of the property to define.
*/ */
export function define<SerializedType, ModelType>(type: Type<SerializedType, ModelType>): Definition<SerializedType, ModelType> export function define<SerializedType, ModelType>(
{ type: Type<SerializedType, ModelType>,
): Definition<SerializedType, ModelType> {
return new Definition(type); return new Definition(type);
} }

View file

@ -5,56 +5,70 @@ import {InvalidTypeValueError} from "../../errors";
/** /**
* Type of an array of values. * Type of an array of values.
*/ */
export class ArrayType<SerializedValueType, SharkitekValueType> extends Type<SerializedValueType[], SharkitekValueType[]> export class ArrayType<SerializedValueType, SharkitekValueType> extends Type<
{ SerializedValueType[],
SharkitekValueType[]
> {
/** /**
* Initialize a new array type of a Sharkitek model property. * Initialize a new array type of a Sharkitek model property.
* @param valueDefinition Definition the array values. * @param valueDefinition Definition the array values.
*/ */
constructor(protected valueDefinition: Definition<SerializedValueType, SharkitekValueType>) constructor(
{ protected valueDefinition: Definition<
SerializedValueType,
SharkitekValueType
>,
) {
super(); super();
} }
serialize(value: SharkitekValueType[]|null|undefined): SerializedValueType[]|null|undefined serialize(
{ value: SharkitekValueType[] | null | undefined,
): SerializedValueType[] | null | undefined {
if (value === undefined) return undefined; if (value === undefined) return undefined;
if (value === null) return null; if (value === null) return null;
if (!Array.isArray(value)) throw new InvalidTypeValueError(this, value, "value must be an array"); if (!Array.isArray(value))
throw new InvalidTypeValueError(this, value, "value must be an array");
return value.map((value) => ( return value.map((value) =>
// Serializing each value of the array. // Serializing each value of the array.
this.valueDefinition.type.serialize(value) this.valueDefinition.type.serialize(value),
)); );
} }
deserialize(value: SerializedValueType[]|null|undefined): SharkitekValueType[]|null|undefined deserialize(
{ value: SerializedValueType[] | null | undefined,
): SharkitekValueType[] | null | undefined {
if (value === undefined) return undefined; if (value === undefined) return undefined;
if (value === null) return null; if (value === null) return null;
if (!Array.isArray(value)) throw new InvalidTypeValueError(this, value, "value must be an array"); if (!Array.isArray(value))
throw new InvalidTypeValueError(this, value, "value must be an array");
return value.map((serializedValue) => ( return value.map((serializedValue) =>
// Deserializing each value of the array. // Deserializing each value of the array.
this.valueDefinition.type.deserialize(serializedValue) this.valueDefinition.type.deserialize(serializedValue),
)); );
} }
serializeDiff(value: SharkitekValueType[]|null|undefined): SerializedValueType[]|null|undefined serializeDiff(
{ value: SharkitekValueType[] | null | undefined,
): SerializedValueType[] | null | undefined {
if (value === undefined) return undefined; if (value === undefined) return undefined;
if (value === null) return null; if (value === null) return null;
if (!Array.isArray(value)) throw new InvalidTypeValueError(this, value, "value must be an array"); if (!Array.isArray(value))
throw new InvalidTypeValueError(this, value, "value must be an array");
// Serializing diff of all elements. // Serializing diff of all elements.
return value.map((value) => this.valueDefinition.type.serializeDiff(value) as SerializedValueType); return value.map(
(value) =>
this.valueDefinition.type.serializeDiff(value) as SerializedValueType,
);
} }
resetDiff(value: SharkitekValueType[]|null|undefined): void resetDiff(value: SharkitekValueType[] | null | undefined): void {
{
// Do nothing if it is not an array. // Do nothing if it is not an array.
if (!Array.isArray(value)) return; if (!Array.isArray(value)) return;
@ -62,16 +76,24 @@ export class ArrayType<SerializedValueType, SharkitekValueType> extends Type<Ser
value.forEach((value) => this.valueDefinition.type.resetDiff(value)); value.forEach((value) => this.valueDefinition.type.resetDiff(value));
} }
hasChanged(originalValue: SharkitekValueType[]|null|undefined, currentValue: SharkitekValueType[]|null|undefined): boolean hasChanged(
{ originalValue: SharkitekValueType[] | null | undefined,
currentValue: SharkitekValueType[] | null | undefined,
): boolean {
// If any array length is different, arrays are different. // If any array length is different, arrays are different.
if (originalValue?.length != currentValue?.length) return true; if (originalValue?.length != currentValue?.length) return true;
// If length is undefined, values are probably not arrays. // If length is undefined, values are probably not arrays.
if (originalValue?.length == undefined) return super.hasChanged(originalValue, currentValue); if (originalValue?.length == undefined)
return super.hasChanged(originalValue, currentValue);
for (const key of originalValue.keys()) for (const key of originalValue.keys()) {
{ // Check for any change for each value in the array. // Check for any change for each value in the array.
if (this.valueDefinition.type.hasChanged(originalValue[key], currentValue[key])) if (
this.valueDefinition.type.hasChanged(
originalValue[key],
currentValue[key],
)
)
// The value has changed, the array is different. // The value has changed, the array is different.
return true; return true;
} }
@ -79,16 +101,24 @@ export class ArrayType<SerializedValueType, SharkitekValueType> extends Type<Ser
return false; // No change detected. return false; // No change detected.
} }
serializedHasChanged(originalValue: SerializedValueType[] | null | undefined, currentValue: SerializedValueType[] | null | undefined): boolean serializedHasChanged(
{ originalValue: SerializedValueType[] | null | undefined,
currentValue: SerializedValueType[] | null | undefined,
): boolean {
// If any array length is different, arrays are different. // If any array length is different, arrays are different.
if (originalValue?.length != currentValue?.length) return true; if (originalValue?.length != currentValue?.length) return true;
// If length is undefined, values are probably not arrays. // If length is undefined, values are probably not arrays.
if (originalValue?.length == undefined) return super.serializedHasChanged(originalValue, currentValue); if (originalValue?.length == undefined)
return super.serializedHasChanged(originalValue, currentValue);
for (const key of originalValue.keys()) for (const key of originalValue.keys()) {
{ // Check for any change for each value in the array. // Check for any change for each value in the array.
if (this.valueDefinition.type.serializedHasChanged(originalValue[key], currentValue[key])) if (
this.valueDefinition.type.serializedHasChanged(
originalValue[key],
currentValue[key],
)
)
// The value has changed, the array is different. // The value has changed, the array is different.
return true; return true;
} }
@ -96,30 +126,62 @@ export class ArrayType<SerializedValueType, SharkitekValueType> extends Type<Ser
return false; // No change detected. return false; // No change detected.
} }
clone<T extends SharkitekValueType[]>(array: T|null|undefined): T clone<T extends SharkitekValueType[]>(array: T | null | undefined): T {
{
// Handle NULL / undefined array. // Handle NULL / undefined array.
if (!array) return super.clone(array); if (!array) return super.clone(array);
if (!Array.isArray(array)) throw new InvalidTypeValueError(this, array, "value must be an array"); if (!Array.isArray(array))
throw new InvalidTypeValueError(this, array, "value must be an array");
// Initialize an empty array. // Initialize an empty array.
const cloned = [] as T; const cloned = [] as T;
for (const value of array) for (const value of array) {
{ // Clone each value of the array. // Clone each value of the array.
cloned.push(this.valueDefinition.type.clone(value)); cloned.push(this.valueDefinition.type.clone(value));
} }
return cloned; // Returning cloned array. return cloned; // Returning cloned array.
} }
applyPatch<T extends SharkitekValueType[]>(
currentValue: T | null | undefined,
patchValue: SerializedValueType[] | null | undefined,
updateOriginals: boolean,
): T | null | undefined {
if (patchValue === undefined) return undefined;
if (patchValue === null) return null;
if (!Array.isArray(patchValue))
throw new InvalidTypeValueError(
this,
patchValue,
"value must be an array",
);
currentValue = Array.isArray(currentValue) ? currentValue : ([] as T);
for (let i = 0; i < patchValue.length; i++) {
// Apply the patch to all values of the array.
const patchedElement = this.valueDefinition.type.applyPatch(
currentValue?.[i],
patchValue[i],
updateOriginals,
);
if (i < currentValue.length) currentValue[i] = patchedElement;
else currentValue.push(patchedElement);
}
return currentValue;
}
} }
/** /**
* New array property definition. * New array property definition.
* @param valueDefinition Array values type definition. * @param valueDefinition Array values type definition.
*/ */
export function array<SerializedValueType, SharkitekValueType>(valueDefinition: Definition<SerializedValueType, SharkitekValueType>): Definition<SerializedValueType[], SharkitekValueType[]> export function array<SerializedValueType, SharkitekValueType>(
{ valueDefinition: Definition<SerializedValueType, SharkitekValueType>,
): Definition<SerializedValueType[], SharkitekValueType[]> {
return define(new ArrayType(valueDefinition)); return define(new ArrayType(valueDefinition));
} }

View file

@ -4,10 +4,8 @@ import {define, Definition} from "../property-definition";
/** /**
* Type of any boolean value. * Type of any boolean value.
*/ */
export class BooleanType extends Type<boolean, boolean> export class BooleanType extends Type<boolean, boolean> {
{ deserialize(value: boolean | null | undefined): boolean | null | undefined {
deserialize(value: boolean|null|undefined): boolean|null|undefined
{
// Keep NULL and undefined values. // Keep NULL and undefined values.
if (value === undefined) return undefined; if (value === undefined) return undefined;
if (value === null) return null; if (value === null) return null;
@ -15,8 +13,7 @@ export class BooleanType extends Type<boolean, boolean>
return !!value; // ensure bool type. return !!value; // ensure bool type.
} }
serialize(value: boolean|null|undefined): boolean|null|undefined serialize(value: boolean | null | undefined): boolean | null | undefined {
{
// Keep NULL and undefined values. // Keep NULL and undefined values.
if (value === undefined) return undefined; if (value === undefined) return undefined;
if (value === null) return null; if (value === null) return null;
@ -28,15 +25,13 @@ export class BooleanType extends Type<boolean, boolean>
/** /**
* New boolean property definition. * New boolean property definition.
*/ */
export function boolean(): Definition<boolean, boolean> export function boolean(): Definition<boolean, boolean> {
{
return define(new BooleanType()); return define(new BooleanType());
} }
/** /**
* New boolean property definition. * New boolean property definition.
* Alias of boolean. * Alias of boolean.
*/ */
export function bool(): ReturnType<typeof boolean> export function bool(): ReturnType<typeof boolean> {
{
return boolean(); return boolean();
} }

View file

@ -5,30 +5,30 @@ import {InvalidTypeValueError} from "../../errors";
/** /**
* Type of dates. * Type of dates.
*/ */
export class DateType extends Type<string, Date> export class DateType extends Type<string, Date> {
{ deserialize(value: string | null | undefined): Date | null | undefined {
deserialize(value: string|null|undefined): Date|null|undefined
{
if (value === undefined) return undefined; if (value === undefined) return undefined;
if (value === null) return null; if (value === null) return null;
return new Date(value); return new Date(value);
} }
serialize(value: Date|null|undefined): string|null|undefined serialize(value: Date | null | undefined): string | null | undefined {
{
if (value === undefined) return undefined; if (value === undefined) return undefined;
if (value === null) return null; if (value === null) return null;
if (!(value instanceof Date)) throw new InvalidTypeValueError(this, value, "value must be a date"); if (!(value instanceof Date))
throw new InvalidTypeValueError(this, value, "value must be a date");
if (isNaN(value?.valueOf())) return value?.toString(); if (isNaN(value?.valueOf())) return value?.toString();
return value?.toISOString(); return value?.toISOString();
} }
hasChanged(originalValue: Date|null|undefined, currentValue: Date|null|undefined): boolean hasChanged(
{ originalValue: Date | null | undefined,
if (originalValue instanceof Date && currentValue instanceof Date) currentValue: Date | null | undefined,
{ // Compare dates. ): boolean {
if (originalValue instanceof Date && currentValue instanceof Date) {
// Compare dates.
const originalTime = originalValue.getTime(); const originalTime = originalValue.getTime();
const currentTime = currentValue.getTime(); const currentTime = currentValue.getTime();
@ -37,8 +37,7 @@ export class DateType extends Type<string, Date>
// Timestamps need to be exactly the same. // Timestamps need to be exactly the same.
return originalValue.getTime() !== currentValue.getTime(); return originalValue.getTime() !== currentValue.getTime();
} } else
else
// Compare undefined or null values. // Compare undefined or null values.
return originalValue !== currentValue; return originalValue !== currentValue;
} }
@ -47,7 +46,6 @@ export class DateType extends Type<string, Date>
/** /**
* New date property definition. * New date property definition.
*/ */
export function date(): Definition<string, Date> export function date(): Definition<string, Date> {
{
return define(new DateType()); return define(new DateType());
} }

View file

@ -5,21 +5,19 @@ import {InvalidTypeValueError} from "../../errors";
/** /**
* Type of decimal numbers. * Type of decimal numbers.
*/ */
export class DecimalType extends Type<string, number> export class DecimalType extends Type<string, number> {
{ deserialize(value: string | null | undefined): number | null | undefined {
deserialize(value: string|null|undefined): number|null|undefined
{
if (value === undefined) return undefined; if (value === undefined) return undefined;
if (value === null) return null; if (value === null) return null;
return parseFloat(value); return parseFloat(value);
} }
serialize(value: number|null|undefined): string|null|undefined serialize(value: number | null | undefined): string | null | undefined {
{
if (value === undefined) return undefined; if (value === undefined) return undefined;
if (value === null) return null; if (value === null) return null;
if (typeof value !== "number" && typeof value !== "string") throw new InvalidTypeValueError(this, value, "value must be a number"); if (typeof value !== "number" && typeof value !== "string")
throw new InvalidTypeValueError(this, value, "value must be a number");
return value?.toString(); return value?.toString();
} }
@ -28,7 +26,6 @@ export class DecimalType extends Type<string, number>
/** /**
* New decimal property definition. * New decimal property definition.
*/ */
export function decimal(): Definition<string, number> export function decimal(): Definition<string, number> {
{
return define(new DecimalType()); return define(new DecimalType());
} }

264
src/model/types/map.ts Normal file
View file

@ -0,0 +1,264 @@
import {Type} from "./type";
import {define, Definition} from "../property-definition";
import {InvalidTypeValueError} from "../../errors";
import {string} from "./string";
/**
* Type of a key-value map.
*/
export class MapType<
KeyType,
ValueType,
SerializedValueType,
SerializedMapType extends Record<string, SerializedValueType> = Record<
string,
SerializedValueType
>,
> extends Type<SerializedMapType, Map<KeyType, ValueType>> {
/**
* Initialize a new map type of a Sharkitek model property.
* @param keyDefinition Definition of the map keys.
* @param valueDefinition Definition of the map values.
*/
constructor(
protected keyDefinition: Definition<string, KeyType>,
protected valueDefinition: Definition<SerializedValueType, ValueType>,
) {
super();
}
serialize(
value: Map<KeyType, ValueType> | null | undefined,
): SerializedMapType | null | undefined {
if (value === undefined) return undefined;
if (value === null) return null;
if (!(value instanceof Map))
throw new InvalidTypeValueError(
this,
value,
"value must be an instance of map",
);
return Object.fromEntries(
// Serializing each key-value pair of the map.
value
.entries()
.map(([key, value]) => [
this.keyDefinition.type.serialize(key),
this.valueDefinition.type.serialize(value),
]),
) as SerializedMapType;
}
deserialize(
value: SerializedMapType | null | undefined,
): Map<KeyType, ValueType> | null | undefined {
if (value === undefined) return undefined;
if (value === null) return null;
if (typeof value !== "object" || Array.isArray(value))
throw new InvalidTypeValueError(this, value, "value must be an object");
const map = new Map<KeyType, ValueType>();
for (const [serializedKey, serializedValue] of Object.entries(value)) {
// Deserializing each key-value pair of the map.
map.set(
this.keyDefinition.type.deserialize(serializedKey),
this.valueDefinition.type.deserialize(serializedValue),
);
}
return map;
}
serializeDiff(
value: Map<KeyType, ValueType> | null | undefined,
): SerializedMapType | null | undefined {
if (value === undefined) return undefined;
if (value === null) return null;
if (!(value instanceof Map))
throw new InvalidTypeValueError(
this,
value,
"value must be an instance of map",
);
return Object.fromEntries(
// Serializing the diff of each key-value pair of the map.
value
.entries()
.map(([key, value]) => [
this.keyDefinition.type.serializeDiff(key),
this.valueDefinition.type.serializeDiff(value),
]),
) as SerializedMapType;
}
resetDiff(value: Map<KeyType, ValueType> | null | undefined): void {
// Do nothing if it is not a map.
if (!(value instanceof Map)) return;
// Reset diff of all key-value pairs.
value.forEach((value, key) => {
this.keyDefinition.type.resetDiff(key);
this.valueDefinition.type.resetDiff(value);
});
}
hasChanged(
originalValue: Map<KeyType, ValueType> | null | undefined,
currentValue: Map<KeyType, ValueType> | null | undefined,
): boolean {
// If any map size is different, maps are different.
if (originalValue?.size != currentValue?.size) return true;
// If size is undefined, values are probably not maps.
if (originalValue?.size == undefined)
return super.hasChanged(originalValue, currentValue);
for (const [key, value] of originalValue.entries()) {
// Check for any change for each key-value in the map.
if (this.valueDefinition.type.hasChanged(value, currentValue.get(key)))
// The value has changed, the map is different.
return true;
}
return false; // No change detected.
}
serializedHasChanged(
originalValue: SerializedMapType | null | undefined,
currentValue: SerializedMapType | null | undefined,
): boolean {
// If any value is not a defined object, use the default comparison function.
if (
!originalValue ||
!currentValue ||
typeof originalValue !== "object" ||
typeof currentValue !== "object"
)
return super.serializedHasChanged(originalValue, currentValue);
// If any object size is different, objects are different.
if (Object.keys(originalValue)?.length != Object.keys(currentValue)?.length)
return true;
for (const [key, value] of Object.entries(originalValue)) {
// Check for any change for each key-value pair in the object.
if (
this.valueDefinition.type.serializedHasChanged(value, currentValue[key])
)
// The value has changed, the object is different.
return true;
}
return false; // No change detected.
}
clone<T extends Map<KeyType, ValueType>>(map: T | null | undefined): T {
// Handle NULL / undefined map.
if (!map) return super.clone(map);
if (!(map instanceof Map))
throw new InvalidTypeValueError(
this,
map,
"value must be an instance of map",
);
// Initialize an empty map.
const cloned = new Map<KeyType, ValueType>() as T;
for (const [key, value] of map.entries()) {
// Clone each value of the map.
cloned.set(
this.keyDefinition.type.clone(key),
this.valueDefinition.type.clone(value),
);
}
return cloned; // Returning cloned map.
}
applyPatch<T extends Map<KeyType, ValueType>>(
currentValue: T | null | undefined,
patchValue: SerializedMapType | null | undefined,
updateOriginals: boolean,
): T | null | undefined {
if (patchValue === undefined) return undefined;
if (patchValue === null) return null;
if (typeof patchValue !== "object")
throw new InvalidTypeValueError(
this,
patchValue,
"value must be an object",
);
currentValue =
currentValue instanceof Map
? currentValue
: (new Map<KeyType, ValueType>() as T);
for (const [key, value] of Object.entries(patchValue)) {
// Apply the patch to all values of the map.
const patchedKey = this.keyDefinition.type.deserialize(key);
const patchedElement = this.valueDefinition.type.applyPatch(
currentValue.get(patchedKey),
value,
updateOriginals,
);
currentValue.set(patchedKey, patchedElement);
}
return currentValue;
}
}
/**
* New map property definition.
* @param keyDefinition Definition of the map keys.
* @param valueDefinition Definition of the map values.
*/
export function map<
KeyType,
ValueType,
SerializedValueType,
SerializedMapType extends Record<string, SerializedValueType> = Record<
string,
SerializedValueType
>,
>(
keyDefinition: Definition<string, KeyType>,
valueDefinition: Definition<SerializedValueType, ValueType>,
): Definition<SerializedMapType, Map<KeyType, ValueType>> {
return define(
new MapType<KeyType, ValueType, SerializedValueType, SerializedMapType>(
keyDefinition,
valueDefinition,
),
);
}
/**
* New map property definition, with string as index.
* @param valueDefinition Definition of the map values.
*/
export function stringMap<
ValueType,
SerializedValueType,
SerializedMapType extends Record<string, SerializedValueType> = Record<
string,
SerializedValueType
>,
>(
valueDefinition: Definition<SerializedValueType, ValueType>,
): Definition<SerializedMapType, Map<string, ValueType>> {
return define(
new MapType<string, ValueType, SerializedValueType, SerializedMapType>(
string(),
valueDefinition,
),
);
}

View file

@ -2,50 +2,62 @@ import {Type} from "./type";
import {define, Definition} from "../property-definition"; import {define, Definition} from "../property-definition";
import { import {
GenericModelManager, GenericModelManager,
IdentifierDefinition, DeclaredModelManager, IdentifierDefinition,
DeclaredModelManager,
ModelInstance, ModelInstance,
ModelManager, ModelManager,
ModelShape, ModelShape,
SerializedModel SerializedModel,
} from "../model"; } from "../model";
import {InvalidTypeValueError} from "../../errors"; import {InvalidTypeValueError} from "../../errors";
/** /**
* Type of a Sharkitek model value. * Type of a Sharkitek model value.
*/ */
export class ModelType<T extends object, Shape extends ModelShape<T>, Identifier extends IdentifierDefinition<T, Shape>> extends Type<SerializedModel<T, Shape>, ModelInstance<T, Shape, Identifier>> export class ModelType<
{ T extends object,
Shape extends ModelShape<T>,
Identifier extends IdentifierDefinition<T, Shape>,
> extends Type<SerializedModel<T, Shape>, ModelInstance<T, Shape, Identifier>> {
/** /**
* Initialize a new model type of a Sharkitek model property. * Initialize a new model type of a Sharkitek model property.
* @param declaredModelManager Model manager. * @param declaredModelManager Model manager.
*/ */
constructor(protected declaredModelManager: DeclaredModelManager<T, Shape, Identifier>) constructor(
{ protected declaredModelManager: DeclaredModelManager<T, Shape, Identifier>,
) {
super(); super();
} }
/** /**
* Resolve the defined model using the declared model, that can be defined lazily. * Resolve the defined model using the declared model, that can be defined lazily.
*/ */
get definedModel(): ModelManager<T, Shape, Identifier> get definedModel(): ModelManager<T, Shape, Identifier> {
{ return typeof this.declaredModelManager == "object"
return typeof this.declaredModelManager == "object" ? this.declaredModelManager : this.declaredModelManager(); ? this.declaredModelManager
: this.declaredModelManager();
} }
serialize(value: ModelInstance<T, Shape, Identifier>|null|undefined): SerializedModel<T, Shape>|null|undefined serialize(
{ value: ModelInstance<T, Shape, Identifier> | null | undefined,
): SerializedModel<T, Shape> | null | undefined {
if (value === undefined) return undefined; if (value === undefined) return undefined;
if (value === null) return null; if (value === null) return null;
if (!(value instanceof this.definedModel.definition.Class)) if (!(value instanceof this.definedModel.definition.Class))
throw new InvalidTypeValueError(this, value, `value must be a compatible model (given ${value.constructor.name}, expected ${this.definedModel.definition.Class.name})`); throw new InvalidTypeValueError(
this,
value,
`value must be a compatible model (given ${value.constructor.name}, expected ${this.definedModel.definition.Class.name})`,
);
// Serializing the given model. // Serializing the given model.
return this.definedModel.model(value).serialize(); return this.definedModel.model(value).serialize();
} }
deserialize(value: SerializedModel<T, Shape>|null|undefined): ModelInstance<T, Shape, Identifier>|null|undefined deserialize(
{ value: SerializedModel<T, Shape> | null | undefined,
): ModelInstance<T, Shape, Identifier> | null | undefined {
if (value === undefined) return undefined; if (value === undefined) return undefined;
if (value === null) return null; if (value === null) return null;
@ -56,90 +68,158 @@ export class ModelType<T extends object, Shape extends ModelShape<T>, Identifier
return this.definedModel.parse(value); return this.definedModel.parse(value);
} }
serializeDiff(value: ModelInstance<T, Shape, Identifier>|null|undefined): Partial<SerializedModel<T, Shape>>|null|undefined serializeDiff(
{ value: ModelInstance<T, Shape, Identifier> | null | undefined,
): Partial<SerializedModel<T, Shape>> | null | undefined {
if (value === undefined) return undefined; if (value === undefined) return undefined;
if (value === null) return null; if (value === null) return null;
if (!(value instanceof this.definedModel.definition.Class)) if (!(value instanceof this.definedModel.definition.Class))
throw new InvalidTypeValueError(this, value, `value must be a compatible model (given ${value.constructor.name}, expected ${this.definedModel.definition.Class.name})`); throw new InvalidTypeValueError(
this,
value,
`value must be a compatible model (given ${value.constructor.name}, expected ${this.definedModel.definition.Class.name})`,
);
// Serializing the given model. // Serializing the given model.
return this.definedModel.model(value).serializeDiff(); return this.definedModel.model(value).serializeDiff();
} }
resetDiff(value: ModelInstance<T, Shape, Identifier>|null|undefined): void resetDiff(
{ value: ModelInstance<T, Shape, Identifier> | null | undefined,
): void {
if (value === undefined) return; if (value === undefined) return;
if (value === null) return; if (value === null) return;
if (!(value instanceof this.definedModel.definition.Class)) if (!(value instanceof this.definedModel.definition.Class))
throw new InvalidTypeValueError(this, value, `value must be a compatible model (given ${value.constructor.name}, expected ${this.definedModel.definition.Class.name})`); throw new InvalidTypeValueError(
this,
value,
`value must be a compatible model (given ${value.constructor.name}, expected ${this.definedModel.definition.Class.name})`,
);
// Reset diff of the given model. // Reset diff of the given model.
this.definedModel.model(value).resetDiff(); this.definedModel.model(value).resetDiff();
} }
hasChanged(originalValue: ModelInstance<T, Shape, Identifier>|null|undefined, currentValue: ModelInstance<T, Shape, Identifier>|null|undefined): boolean hasChanged(
{ originalValue: ModelInstance<T, Shape, Identifier> | null | undefined,
currentValue: ModelInstance<T, Shape, Identifier> | null | undefined,
): boolean {
if (originalValue === undefined) return currentValue !== undefined; if (originalValue === undefined) return currentValue !== undefined;
if (originalValue === null) return currentValue !== null; if (originalValue === null) return currentValue !== null;
if (currentValue === undefined) return true; // Original value is not undefined. if (currentValue === undefined) return true; // Original value is not undefined.
if (currentValue === null) return true; // Original value is not null. if (currentValue === null) return true; // Original value is not null.
if (!(originalValue instanceof this.definedModel.definition.Class)) if (!(originalValue instanceof this.definedModel.definition.Class))
throw new InvalidTypeValueError(this, originalValue, `value must be a compatible model (given ${originalValue.constructor.name}, expected ${this.definedModel.definition.Class.name})`); throw new InvalidTypeValueError(
this,
originalValue,
`value must be a compatible model (given ${originalValue.constructor.name}, expected ${this.definedModel.definition.Class.name})`,
);
if (!(currentValue instanceof this.definedModel.definition.Class)) if (!(currentValue instanceof this.definedModel.definition.Class))
throw new InvalidTypeValueError(this, currentValue, `value must be a compatible model (given ${currentValue.constructor.name}, expected ${this.definedModel.definition.Class.name})`); throw new InvalidTypeValueError(
this,
currentValue,
`value must be a compatible model (given ${currentValue.constructor.name}, expected ${this.definedModel.definition.Class.name})`,
);
// If the current value is dirty, it has changed. // If the current value is dirty, it has changed.
return this.definedModel.model(currentValue).isDirty(); return this.definedModel.model(currentValue).isDirty();
} }
serializedHasChanged(originalValue: SerializedModel<T, Shape> | null | undefined, currentValue: SerializedModel<T, Shape> | null | undefined): boolean serializedHasChanged(
{ originalValue: SerializedModel<T, Shape> | null | undefined,
currentValue: SerializedModel<T, Shape> | null | undefined,
): boolean {
if (originalValue === undefined) return currentValue !== undefined; if (originalValue === undefined) return currentValue !== undefined;
if (originalValue === null) return currentValue !== null; if (originalValue === null) return currentValue !== null;
if (currentValue === undefined) return true; // Original value is not undefined. if (currentValue === undefined) return true; // Original value is not undefined.
if (currentValue === null) return true; // Original value is not null. if (currentValue === null) return true; // Original value is not null.
if (typeof originalValue !== "object" || Array.isArray(originalValue)) if (typeof originalValue !== "object" || Array.isArray(originalValue))
throw new InvalidTypeValueError(this, originalValue, "value must be an object"); throw new InvalidTypeValueError(
this,
originalValue,
"value must be an object",
);
if (typeof currentValue !== "object" || Array.isArray(currentValue)) if (typeof currentValue !== "object" || Array.isArray(currentValue))
throw new InvalidTypeValueError(this, currentValue, "value must be an object"); throw new InvalidTypeValueError(
this,
currentValue,
"value must be an object",
);
// If any property has changed, the value has changed. // If any property has changed, the value has changed.
for (const property of this.definedModel.properties) for (const property of this.definedModel.properties)
if (property.definition.type.serializedHasChanged(originalValue?.[property.name], currentValue?.[property.name])) if (
property.definition.type.serializedHasChanged(
originalValue?.[property.name],
currentValue?.[property.name],
)
)
return true; return true;
return false; // No change detected. return false; // No change detected.
} }
clone<Type extends ModelInstance<T, Shape, Identifier>>(value: Type|null|undefined): Type clone<Type extends ModelInstance<T, Shape, Identifier>>(
{ value: Type | null | undefined,
): Type {
// Handle NULL / undefined values. // Handle NULL / undefined values.
if (!value) return super.clone(value); if (!value) return super.clone(value);
if (!(value instanceof this.definedModel.definition.Class)) if (!(value instanceof this.definedModel.definition.Class))
throw new InvalidTypeValueError(this, value, `value must be a compatible model (given ${value.constructor.name}, expected ${this.definedModel.definition.Class.name})`); throw new InvalidTypeValueError(
this,
value,
`value must be a compatible model (given ${value.constructor.name}, expected ${this.definedModel.definition.Class.name})`,
);
return this.definedModel.model(value).clone() as Type; return this.definedModel.model(value).clone() as Type;
} }
applyPatch<Type extends ModelInstance<T, Shape, Identifier>>(
currentValue: Type | null | undefined,
patchValue: SerializedModel<T, Shape> | null | undefined,
updateOriginals: boolean,
): Type | null | undefined {
if (patchValue === undefined) return undefined;
if (patchValue === null) return null;
if (typeof patchValue !== "object" || Array.isArray(patchValue))
throw new InvalidTypeValueError(
this,
patchValue,
"value must be an object",
);
return this.definedModel
.model(currentValue)
.applyPatch(patchValue, updateOriginals) as Type;
}
} }
/** /**
* New model property definition. * New model property definition.
* @param definedModel Model manager. * @param definedModel Model manager.
*/ */
export function model<T extends object, Shape extends ModelShape<T>, Identifier extends IdentifierDefinition<T, Shape>>( export function model<
definedModel: DeclaredModelManager<T, Shape, Identifier> T extends object,
): Definition<SerializedModel<T, Shape>, ModelInstance<T, Shape, Identifier>> Shape extends ModelShape<T>,
{ Identifier extends IdentifierDefinition<T, Shape>,
>(
definedModel: DeclaredModelManager<T, Shape, Identifier>,
): Definition<SerializedModel<T, Shape>, ModelInstance<T, Shape, Identifier>> {
return define(new ModelType(definedModel)); return define(new ModelType(definedModel));
} }
export function circular<T extends object>(definedModel: () => any): () => GenericModelManager<T> /**
{ * Utility function to fix circular dependencies issues.
* @param definedModel A function returning the model to use.
*/
export function circular<T extends object>(
definedModel: () => any,
): () => GenericModelManager<T> {
return definedModel; return definedModel;
} }

View file

@ -5,24 +5,23 @@ import {InvalidTypeValueError} from "../../errors";
/** /**
* Type of any numeric value. * Type of any numeric value.
*/ */
export class NumericType extends Type<number, number> export class NumericType extends Type<number, number> {
{ deserialize(value: number | null | undefined): number | null | undefined {
deserialize(value: number|null|undefined): number|null|undefined
{
if (value === undefined) return undefined; if (value === undefined) return undefined;
if (value === null) return null; if (value === null) return null;
if (typeof value !== "number") throw new InvalidTypeValueError(this, value, "value must be a number"); if (typeof value !== "number")
throw new InvalidTypeValueError(this, value, "value must be a number");
return value; return value;
} }
serialize(value: number|null|undefined): number|null|undefined serialize(value: number | null | undefined): number | null | undefined {
{
if (value === undefined) return undefined; if (value === undefined) return undefined;
if (value === null) return null; if (value === null) return null;
if (typeof value !== "number") throw new InvalidTypeValueError(this, value, "value must be a number"); if (typeof value !== "number")
throw new InvalidTypeValueError(this, value, "value must be a number");
return value; return value;
} }
@ -31,7 +30,6 @@ export class NumericType extends Type<number, number>
/** /**
* New numeric property definition. * New numeric property definition.
*/ */
export function numeric(): Definition<number, number> export function numeric(): Definition<number, number> {
{
return define(new NumericType()); return define(new NumericType());
} }

View file

@ -1,13 +1,21 @@
import {Type} from "./type"; import {Type} from "./type";
import {define, Definition} from "../property-definition"; import {define, Definition, UnknownDefinition} from "../property-definition";
import {ModelProperties, ModelPropertiesValues, ModelProperty, ModelShape, SerializedModel} from "../model"; import {
ModelProperties,
ModelPropertiesValues,
ModelProperty,
ModelShape,
SerializedModel,
} from "../model";
import {InvalidTypeValueError} from "../../errors"; import {InvalidTypeValueError} from "../../errors";
/** /**
* Type of a custom object. * Type of a custom object.
*/ */
export class ObjectType<Shape extends ModelShape<T>, T extends object> extends Type<SerializedModel<T, Shape>, ModelPropertiesValues<T, Shape>> export class ObjectType<
{ Shape extends ModelShape<T>,
T extends object,
> extends Type<SerializedModel<T, Shape>, ModelPropertiesValues<T, Shape>> {
/** /**
* Defined properties. * Defined properties.
*/ */
@ -17,8 +25,7 @@ export class ObjectType<Shape extends ModelShape<T>, T extends object> extends T
* Initialize a new object type of a Sharkitek model property. * Initialize a new object type of a Sharkitek model property.
* @param shape * @param shape
*/ */
constructor(readonly shape: Shape) constructor(readonly shape: Shape) {
{
super(); super();
this.initProperties(); this.initProperties();
} }
@ -27,12 +34,11 @@ export class ObjectType<Shape extends ModelShape<T>, T extends object> extends T
* Initialize properties iterator from the object shape. * Initialize properties iterator from the object shape.
* @protected * @protected
*/ */
protected initProperties(): void protected initProperties(): void {
{
// Build an array of model properties from the object shape. // Build an array of model properties from the object shape.
this.properties = []; this.properties = [];
for (const propertyName in this.shape) for (const propertyName in this.shape) {
{ // For each property, build a model property object. // For each property, build a model property object.
this.properties.push({ this.properties.push({
name: propertyName, name: propertyName,
definition: this.shape[propertyName], definition: this.shape[propertyName],
@ -41,8 +47,9 @@ export class ObjectType<Shape extends ModelShape<T>, T extends object> extends T
} }
} }
deserialize(value: SerializedModel<T, Shape>|null|undefined): ModelPropertiesValues<T, Shape>|null|undefined deserialize(
{ value: SerializedModel<T, Shape> | null | undefined,
): ModelPropertiesValues<T, Shape> | null | undefined {
if (value === undefined) return undefined; if (value === undefined) return undefined;
if (value === null) return null; if (value === null) return null;
@ -52,16 +59,18 @@ export class ObjectType<Shape extends ModelShape<T>, T extends object> extends T
// Initialize an empty object. // Initialize an empty object.
const obj: Partial<ModelPropertiesValues<T, Shape>> = {}; const obj: Partial<ModelPropertiesValues<T, Shape>> = {};
for (const property of this.properties) for (const property of this.properties) {
{ // For each defined property, deserialize its value according to its type. // For each defined property, deserialize its value according to its type.
(obj[property.name as keyof T] as any) = property.definition.type.deserialize(value?.[property.name]); (obj[property.name as keyof T] as any) =
property.definition.type.deserialize(value?.[property.name]);
} }
return obj as ModelPropertiesValues<T, Shape>; // Returning serialized object. return obj as ModelPropertiesValues<T, Shape>; // Returning serialized object.
} }
serialize(value: ModelPropertiesValues<T, Shape>|null|undefined): SerializedModel<T, Shape>|null|undefined serialize(
{ value: ModelPropertiesValues<T, Shape> | null | undefined,
): SerializedModel<T, Shape> | null | undefined {
if (value === undefined) return undefined; if (value === undefined) return undefined;
if (value === null) return null; if (value === null) return null;
@ -71,8 +80,8 @@ export class ObjectType<Shape extends ModelShape<T>, T extends object> extends T
// Creating an empty serialized object. // Creating an empty serialized object.
const serializedObject: Partial<SerializedModel<T, Shape>> = {}; const serializedObject: Partial<SerializedModel<T, Shape>> = {};
for (const property of this.properties) for (const property of this.properties) {
{ // For each property, adding it to the serialized object. // For each property, adding it to the serialized object.
serializedObject[property.name] = property.definition.type.serialize( serializedObject[property.name] = property.definition.type.serialize(
// keyof Shape is a subset of keyof T. // keyof Shape is a subset of keyof T.
value?.[property.name as keyof T], value?.[property.name as keyof T],
@ -82,8 +91,9 @@ export class ObjectType<Shape extends ModelShape<T>, T extends object> extends T
return serializedObject as SerializedModel<T, Shape>; // Returning the serialized object. return serializedObject as SerializedModel<T, Shape>; // Returning the serialized object.
} }
serializeDiff(value: ModelPropertiesValues<T, Shape>|null|undefined): Partial<SerializedModel<T, Shape>>|null|undefined serializeDiff(
{ value: ModelPropertiesValues<T, Shape> | null | undefined,
): Partial<SerializedModel<T, Shape>> | null | undefined {
if (value === undefined) return undefined; if (value === undefined) return undefined;
if (value === null) return null; if (value === null) return null;
@ -93,8 +103,8 @@ export class ObjectType<Shape extends ModelShape<T>, T extends object> extends T
// Creating an empty serialized object. // Creating an empty serialized object.
const serializedObject: Partial<SerializedModel<T, Shape>> = {}; const serializedObject: Partial<SerializedModel<T, Shape>> = {};
for (const property of this.properties) for (const property of this.properties) {
{ // For each property, adding it to the serialized object. // For each property, adding it to the serialized object.
serializedObject[property.name] = property.definition.type.serializeDiff( serializedObject[property.name] = property.definition.type.serializeDiff(
// keyof Shape is a subset of keyof T. // keyof Shape is a subset of keyof T.
value?.[property.name as keyof T], value?.[property.name as keyof T],
@ -104,8 +114,7 @@ export class ObjectType<Shape extends ModelShape<T>, T extends object> extends T
return serializedObject as SerializedModel<T, Shape>; // Returning the serialized object. return serializedObject as SerializedModel<T, Shape>; // Returning the serialized object.
} }
resetDiff(value: ModelPropertiesValues<T, Shape>|null|undefined) resetDiff(value: ModelPropertiesValues<T, Shape> | null | undefined) {
{
if (value === undefined) return; if (value === undefined) return;
if (value === null) return; if (value === null) return;
@ -113,53 +122,84 @@ export class ObjectType<Shape extends ModelShape<T>, T extends object> extends T
throw new InvalidTypeValueError(this, value, "value must be an object"); throw new InvalidTypeValueError(this, value, "value must be an object");
// For each property, reset its diff. // For each property, reset its diff.
for (const property of this.properties)
// keyof Shape is a subset of keyof T. // keyof Shape is a subset of keyof T.
for (const property of this.properties)
property.definition.type.resetDiff(value?.[property.name as keyof T]); property.definition.type.resetDiff(value?.[property.name as keyof T]);
} }
hasChanged(originalValue: ModelPropertiesValues<T, Shape>|null|undefined, currentValue: ModelPropertiesValues<T, Shape>|null|undefined): boolean hasChanged(
{ originalValue: ModelPropertiesValues<T, Shape> | null | undefined,
currentValue: ModelPropertiesValues<T, Shape> | null | undefined,
): boolean {
if (originalValue === undefined) return currentValue !== undefined; if (originalValue === undefined) return currentValue !== undefined;
if (originalValue === null) return currentValue !== null; if (originalValue === null) return currentValue !== null;
if (currentValue === undefined) return true; // Original value is not undefined. if (currentValue === undefined) return true; // Original value is not undefined.
if (currentValue === null) return true; // Original value is not null. if (currentValue === null) return true; // Original value is not null.
if (typeof originalValue !== "object" || Array.isArray(originalValue)) if (typeof originalValue !== "object" || Array.isArray(originalValue))
throw new InvalidTypeValueError(this, originalValue, "value must be an object"); throw new InvalidTypeValueError(
this,
originalValue,
"value must be an object",
);
if (typeof currentValue !== "object" || Array.isArray(currentValue)) if (typeof currentValue !== "object" || Array.isArray(currentValue))
throw new InvalidTypeValueError(this, currentValue, "value must be an object"); throw new InvalidTypeValueError(
this,
currentValue,
"value must be an object",
);
// If any property has changed, the value has changed. // If any property has changed, the value has changed.
for (const property of this.properties) for (const property of this.properties)
if (property.definition.type.hasChanged(originalValue?.[property.name as keyof T], currentValue?.[property.name as keyof T])) if (
property.definition.type.hasChanged(
originalValue?.[property.name as keyof T],
currentValue?.[property.name as keyof T],
)
)
return true; return true;
return false; // No change detected. return false; // No change detected.
} }
serializedHasChanged(originalValue: SerializedModel<T, Shape>|null|undefined, currentValue: SerializedModel<T, Shape>|null|undefined): boolean serializedHasChanged(
{ originalValue: SerializedModel<T, Shape> | null | undefined,
currentValue: SerializedModel<T, Shape> | null | undefined,
): boolean {
if (originalValue === undefined) return currentValue !== undefined; if (originalValue === undefined) return currentValue !== undefined;
if (originalValue === null) return currentValue !== null; if (originalValue === null) return currentValue !== null;
if (currentValue === undefined) return true; // Original value is not undefined. if (currentValue === undefined) return true; // Original value is not undefined.
if (currentValue === null) return true; // Original value is not null. if (currentValue === null) return true; // Original value is not null.
if (typeof originalValue !== "object" || Array.isArray(originalValue)) if (typeof originalValue !== "object" || Array.isArray(originalValue))
throw new InvalidTypeValueError(this, originalValue, "value must be an object"); throw new InvalidTypeValueError(
this,
originalValue,
"value must be an object",
);
if (typeof currentValue !== "object" || Array.isArray(currentValue)) if (typeof currentValue !== "object" || Array.isArray(currentValue))
throw new InvalidTypeValueError(this, currentValue, "value must be an object"); throw new InvalidTypeValueError(
this,
currentValue,
"value must be an object",
);
// If any property has changed, the value has changed. // If any property has changed, the value has changed.
for (const property of this.properties) for (const property of this.properties)
if (property.definition.type.serializedHasChanged(originalValue?.[property.name], currentValue?.[property.name])) if (
property.definition.type.serializedHasChanged(
originalValue?.[property.name],
currentValue?.[property.name],
)
)
return true; return true;
return false; // No change detected. return false; // No change detected.
} }
clone<Type extends ModelPropertiesValues<T, Shape>>(value: Type|null|undefined): Type clone<Type extends ModelPropertiesValues<T, Shape>>(
{ value: Type | null | undefined,
): Type {
// Handle NULL / undefined object. // Handle NULL / undefined object.
if (!value) return super.clone(value); if (!value) return super.clone(value);
@ -169,20 +209,59 @@ export class ObjectType<Shape extends ModelShape<T>, T extends object> extends T
// Initialize an empty object. // Initialize an empty object.
const cloned: Partial<ModelPropertiesValues<T, Shape>> = {}; const cloned: Partial<ModelPropertiesValues<T, Shape>> = {};
for (const property of this.properties) for (const property of this.properties) {
{ // For each defined property, clone it. // For each defined property, clone it.
cloned[property.name as keyof T] = property.definition.type.clone(value?.[property.name]); cloned[property.name as keyof T] = property.definition.type.clone(
value?.[property.name],
);
} }
return cloned as Type; // Returning cloned object. return cloned as Type; // Returning cloned object.
} }
applyPatch<Type extends ModelPropertiesValues<T, Shape>>(
currentValue: Type | null | undefined,
patchValue: SerializedModel<T, Shape> | null | undefined,
updateOriginals: boolean,
): Type | null | undefined {
if (patchValue === undefined) return undefined;
if (patchValue === null) return null;
if (typeof patchValue !== "object" || Array.isArray(patchValue))
throw new InvalidTypeValueError(
this,
patchValue,
"value must be an object",
);
const patchedValue: Partial<Type> =
typeof currentValue === "object" && currentValue !== null
? currentValue
: {};
for (const key in patchValue) {
// Apply the patch to each property of the patch value.
const propertyDef = this.shape[key];
if (propertyDef)
patchedValue[key as keyof Type] = (
propertyDef as UnknownDefinition
).type.applyPatch(
currentValue?.[key as keyof Type],
patchValue[key],
updateOriginals,
);
}
return patchedValue as Type;
}
} }
/** /**
* New object property definition. * New object property definition.
* @param shape Shape of the object. * @param shape Shape of the object.
*/ */
export function object<Shape extends ModelShape<T>, T extends object>(shape: Shape): Definition<SerializedModel<T, Shape>, ModelPropertiesValues<T, Shape>> export function object<Shape extends ModelShape<T>, T extends object>(
{ shape: Shape,
): Definition<SerializedModel<T, Shape>, ModelPropertiesValues<T, Shape>> {
return define(new ObjectType(shape)); return define(new ObjectType(shape));
} }

View file

@ -4,18 +4,15 @@ import {define, Definition} from "../property-definition";
/** /**
* Type of any string value. * Type of any string value.
*/ */
export class StringType extends Type<string, string> export class StringType extends Type<string, string> {
{ deserialize(value: string | null | undefined): string | null | undefined {
deserialize(value: string|null|undefined): string|null|undefined
{
if (value === undefined) return undefined; if (value === undefined) return undefined;
if (value === null) return null; if (value === null) return null;
return String(value); return String(value);
} }
serialize(value: string|null|undefined): string|null|undefined serialize(value: string | null | undefined): string | null | undefined {
{
if (value === undefined) return undefined; if (value === undefined) return undefined;
if (value === null) return null; if (value === null) return null;
@ -26,7 +23,6 @@ export class StringType extends Type<string, string>
/** /**
* New string property definition. * New string property definition.
*/ */
export function string(): Definition<string, string> export function string(): Definition<string, string> {
{
return define(new StringType()); return define(new StringType());
} }

View file

@ -1,26 +1,30 @@
/** /**
* Abstract class of a Sharkitek model property type. * Abstract class of a Sharkitek model property type.
*/ */
export abstract class Type<SerializedType, ModelType> export abstract class Type<SerializedType, ModelType> {
{
/** /**
* Serialize the given value of a Sharkitek model property. * Serialize the given value of a Sharkitek model property.
* @param value Value to serialize. * @param value Value to serialize.
*/ */
abstract serialize(value: ModelType|null|undefined): SerializedType|null|undefined; abstract serialize(
value: ModelType | null | undefined,
): SerializedType | null | undefined;
/** /**
* Deserialize the given value of a serialized Sharkitek model. * Deserialize the given value of a serialized Sharkitek model.
* @param value Value to deserialize. * @param value Value to deserialize.
*/ */
abstract deserialize(value: SerializedType|null|undefined): ModelType|null|undefined; abstract deserialize(
value: SerializedType | null | undefined,
): ModelType | null | undefined;
/** /**
* Serialize the given value only if it has changed. * Serialize the given value only if it has changed.
* @param value Value to deserialize. * @param value Value to deserialize.
*/ */
serializeDiff(value: ModelType|null|undefined): Partial<SerializedType>|null|undefined serializeDiff(
{ value: ModelType | null | undefined,
): Partial<SerializedType> | null | undefined {
return this.serialize(value); // By default, nothing changes. return this.serialize(value); // By default, nothing changes.
} }
@ -28,8 +32,10 @@ export abstract class Type<SerializedType, ModelType>
* Reset the difference between the original value and the current one. * Reset the difference between the original value and the current one.
* @param value Value for which reset diff data. * @param value Value for which reset diff data.
*/ */
resetDiff(value: ModelType|null|undefined): void resetDiff(
{ // eslint-disable-next-line @typescript-eslint/no-unused-vars
value: ModelType | null | undefined,
): void {
// By default, nothing to do. // By default, nothing to do.
} }
@ -38,8 +44,10 @@ export abstract class Type<SerializedType, ModelType>
* @param originalValue Original value. * @param originalValue Original value.
* @param currentValue Current value. * @param currentValue Current value.
*/ */
hasChanged(originalValue: ModelType|null|undefined, currentValue: ModelType|null|undefined): boolean hasChanged(
{ originalValue: ModelType | null | undefined,
currentValue: ModelType | null | undefined,
): boolean {
return originalValue !== currentValue; return originalValue !== currentValue;
} }
@ -48,8 +56,10 @@ export abstract class Type<SerializedType, ModelType>
* @param originalValue Original serialized value. * @param originalValue Original serialized value.
* @param currentValue Current serialized value. * @param currentValue Current serialized value.
*/ */
serializedHasChanged(originalValue: SerializedType|null|undefined, currentValue: SerializedType|null|undefined): boolean serializedHasChanged(
{ originalValue: SerializedType | null | undefined,
currentValue: SerializedType | null | undefined,
): boolean {
return originalValue !== currentValue; return originalValue !== currentValue;
} }
@ -57,8 +67,22 @@ export abstract class Type<SerializedType, ModelType>
* Clone the provided value. * Clone the provided value.
* @param value The to clone. * @param value The to clone.
*/ */
clone<T extends ModelType>(value: T|null|undefined): T clone<T extends ModelType>(value: T | null | undefined): T {
{
return structuredClone(value); return structuredClone(value);
} }
/**
* Apply the patch value.
* @param currentValue The current property value. Its value can be mutated directly.
* @param patchValue The serialized patch value.
* @param updateOriginals Indicates if the original properties values must be updated or not.
*/
applyPatch<T extends ModelType>(
currentValue: T | null | undefined,
patchValue: SerializedType | null | undefined,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
updateOriginals: boolean,
): T | null | undefined {
return this.deserialize(patchValue) as T;
}
} }

View file

@ -1,4 +1,10 @@
/** /**
* Type definition of a class constructor. * Type definition of a class constructor.
*/ */
export type ConstructorOf<T extends object> = { new(): T; }; export type ConstructorOf<T extends object> = {new (): T};
/**
* Type definition of an original object overridden by another.
*/
export type Modify<Original, Override> = Omit<Original, keyof Override> &
Override;

35
tests/builder.test.ts Normal file
View file

@ -0,0 +1,35 @@
import {describe, expect, test} from "vitest";
import {defineModel, newModel, s} from "../src/library";
class Testest {
foo: string;
bar: number;
baz: boolean;
}
describe("model builder", () => {
const modelBuilder = newModel(Testest)
.property("foo", s.property.string())
.property("bar", s.property.decimal())
.property("baz", s.property.boolean())
.identifier("foo");
test("build model definition", () => {
expect(modelBuilder.definition).toStrictEqual({
Class: Testest,
identifier: "foo",
properties: {
foo: s.property.string(),
bar: s.property.decimal(),
baz: s.property.boolean(),
},
});
});
test("build a model", () => {
expect(modelBuilder.define()).toStrictEqual(
defineModel(modelBuilder.definition),
);
});
});

View file

@ -4,12 +4,21 @@ import {s} from "../src/library";
describe("errors", () => { describe("errors", () => {
it("tests type error", () => { it("tests type error", () => {
expect((new TypeError(s.property.string().type)).message).toBe("Error in type StringType"); expect(new TypeError(s.property.string().type).message).toBe(
expect((new TypeError(s.property.string().type, "test")).message).toBe("Error in type StringType: test"); "Error in type StringType",
);
expect(new TypeError(s.property.string().type, "test").message).toBe(
"Error in type StringType: test",
);
}); });
it("tests invalid type value error", () => { it("tests invalid type value error", () => {
expect((new InvalidTypeValueError(s.property.decimal().type, ["value"])).message).toBe("Error in type DecimalType: [\"value\"] is an invalid value"); expect(
expect((new InvalidTypeValueError(s.property.decimal().type, ["value"], "test")).message).toBe("Error in type DecimalType: test"); new InvalidTypeValueError(s.property.decimal().type, ["value"]).message,
).toBe('Error in type DecimalType: ["value"] is an invalid value');
expect(
new InvalidTypeValueError(s.property.decimal().type, ["value"], "test")
.message,
).toBe("Error in type DecimalType: test");
}); });
}); });

View file

@ -4,9 +4,15 @@ import {circular, defineModel, s} from "../src/library";
/** /**
* Test class of an account. * Test class of an account.
*/ */
class Account class Account {
{ id: number;
static model = s.defineModel({ createdAt: Date;
name: string;
email: string;
active: boolean;
}
const AccountModel = s.defineModel({
Class: Account, Class: Account,
identifier: "id", identifier: "id",
properties: { properties: {
@ -16,70 +22,60 @@ 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[];
text: string; text: string;
evaluation: number; evaluation: number;
tags: { name: string }[]; tags: {name: string}[];
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.
*/ */
function getTestAccount(): Account function getTestAccount(): Account {
{
const account = new Account(); const account = new Account();
account.id = 52; account.id = 52;
account.createdAt = new Date(); account.createdAt = new Date();
@ -89,40 +85,60 @@ function getTestAccount(): Account
return account; return account;
} }
function getTestArticle(): Article function getTestArticle(): Article {
{
const article = new Article(); const article = new Article();
article.id = 1; article.id = 1;
article.title = "this is a test"; article.title = "this is a test";
article.text = "this is a long test."; article.text = "this is a long test.";
article.evaluation = 25.23; article.evaluation = 25.23;
article.tags = [ article.tags = [{name: "test"}, {name: "foo"}];
{ name: "test" },
{ name: "foo" },
];
article.authors = [getTestAccount()]; article.authors = [getTestAccount()];
article.comments = []; article.comments = [];
return article; return article;
} }
describe("model", () => { describe("model", () => {
it("defines a new model, extending an existing one", () => {
class ExtendedAccount extends Account {
extendedProperty: string;
}
const ExtendedAccountModel = s.extend(AccountModel, {
Class: ExtendedAccount,
properties: {
extendedProperty: s.property.string(),
},
});
expect(ExtendedAccountModel.definition).toEqual({
Class: ExtendedAccount,
identifier: "id",
properties: {
id: s.property.numeric(),
createdAt: s.property.date(),
name: s.property.string(),
email: s.property.string(),
active: s.property.boolean(),
extendedProperty: s.property.string(),
},
});
});
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 {
{
static model = s.defineModel({ static model = s.defineModel({
Class: CompositeModel, Class: CompositeModel,
properties: { properties: {
@ -131,7 +147,7 @@ describe("model", () => {
label: s.property.string(), label: s.property.string(),
}, },
identifier: ["firstId", "secondId"], identifier: ["firstId", "secondId"],
}) });
firstId: number; firstId: number;
secondId: number; secondId: number;
@ -139,20 +155,24 @@ describe("model", () => {
} }
expect( expect(
CompositeModel.model.model(Object.assign(new CompositeModel(), { CompositeModel.model
.model(
Object.assign(new CompositeModel(), {
firstId: 5, firstId: 5,
secondId: 6, secondId: 6,
label: "test", label: "test",
})).getIdentifier() }),
)
.getIdentifier(),
).toStrictEqual([5, 6]); ).toStrictEqual([5, 6]);
}); });
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", () => {
@ -160,38 +180,84 @@ describe("model", () => {
id: 1, id: 1,
title: "this is a test", title: "this is a test",
authors: [ authors: [
Object.assign(new Account(), { id: 52, name: "John Doe", email: "test@test.test", createdAt: new Date("2022-08-07T08:47:01.000Z"), active: true, }), Object.assign(new Account(), {
Object.assign(new Account(), { id: 4, name: "Tester", email: "another@test.test", createdAt: new Date("2022-09-07T18:32:55.000Z"), active: false, }), id: 52,
name: "John Doe",
email: "test@test.test",
createdAt: new Date("2022-08-07T08:47:01.000Z"),
active: true,
}),
Object.assign(new Account(), {
id: 4,
name: "Tester",
email: "another@test.test",
createdAt: new Date("2022-09-07T18:32:55.000Z"),
active: false,
}),
], ],
text: "this is a long test.", text: "this is a long test.",
evaluation: 8.52, evaluation: 8.52,
tags: [ {name: "test"}, {name: "foo"} ], tags: [{name: "test"}, {name: "foo"}],
comments: [ comments: [
Object.assign(new ArticleComment(), { id: 542, author: Object.assign(new Account(), { id: 52, name: "John Doe", email: "test@test.test", createdAt: new Date("2022-08-07T08:47:01.000Z"), active: true, }), message: "comment content", }), Object.assign(new ArticleComment(), {
id: 542,
author: Object.assign(new Account(), {
id: 52,
name: "John Doe",
email: "test@test.test",
createdAt: new Date("2022-08-07T08:47:01.000Z"),
active: true,
}),
message: "comment content",
}),
], ],
}); });
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: [
{ id: 52, name: "John Doe", email: "test@test.test", createdAt: "2022-08-07T08:47:01.000Z", active: true, }, {
{ id: 4, name: "Tester", email: "another@test.test", createdAt: "2022-09-07T18:32:55.000Z", active: false, }, id: 52,
name: "John Doe",
email: "test@test.test",
createdAt: "2022-08-07T08:47:01.000Z",
active: true,
},
{
id: 4,
name: "Tester",
email: "another@test.test",
createdAt: "2022-09-07T18:32:55.000Z",
active: false,
},
], ],
text: "this is a long test.", text: "this is a long test.",
evaluation: "8.52", evaluation: "8.52",
tags: [ {name: "test"}, {name: "foo"} ], tags: [{name: "test"}, {name: "foo"}],
comments: [ comments: [
{ id: 542, author: { id: 52, name: "John Doe", email: "test@test.test", createdAt: "2022-08-07T08:47:01.000Z", active: true, }, message: "comment content", }, {
id: 542,
author: {
id: 52,
name: "John Doe",
email: "test@test.test",
createdAt: "2022-08-07T08:47:01.000Z",
active: true,
},
message: "comment content",
},
], ],
}); });
const deserializedArticleProperties = Article.model.model(deserializedArticle).getInstanceProperties(); const deserializedArticleProperties =
ArticleModel.model(deserializedArticle).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.model(expectedArticle).getInstanceProperties(); const expectedArticleProperties =
ArticleModel.model(expectedArticle).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;
@ -201,79 +267,126 @@ 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.",
evaluation: "25.23", evaluation: "25.23",
tags: [{ name: "test" }, { name: "foo" }], tags: [{name: "test"}, {name: "foo"}],
authors: [ authors: [
{ id: 52, createdAt: article.authors[0].createdAt.toISOString(), name: "John Doe", email: "john@doe.test", active: true } {
id: 52,
createdAt: article.authors[0].createdAt.toISOString(),
name: "John Doe",
email: "john@doe.test",
active: true,
},
], ],
comments: [], comments: [],
}); });
}); });
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: [
{ id: 52, name: "John Doe", email: "test@test.test", createdAt: "2022-08-07T08:47:01.000Z", active: true, }, {
{ id: 4, name: "Tester", email: "another@test.test", createdAt: "2022-09-07T18:32:55.000Z", active: false, }, id: 52,
name: "John Doe",
email: "test@test.test",
createdAt: "2022-08-07T08:47:01.000Z",
active: true,
},
{
id: 4,
name: "Tester",
email: "another@test.test",
createdAt: "2022-09-07T18:32:55.000Z",
active: false,
},
], ],
text: "this is a long test.", text: "this is a long test.",
evaluation: "8.52", evaluation: "8.52",
tags: [ {name: "test"}, {name: "foo"} ], tags: [{name: "test"}, {name: "foo"}],
comments: [ comments: [
{ id: 542, author: { id: 52, name: "John Doe", email: "test@test.test", createdAt: "2022-08-07T08:47:01.000Z", active: true, }, message: "comment content", }, {
id: 542,
author: {
id: 52,
name: "John Doe",
email: "test@test.test",
createdAt: "2022-08-07T08:47:01.000Z",
active: true,
},
message: "comment content",
},
], ],
}); });
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: [
{ id: 52, name: "John Doe", email: "test@test.test", createdAt: "2022-08-07T08:47:01.000Z", active: true, }, {
{ id: 4, name: "Tester", email: "another@test.test", createdAt: "2022-09-07T18:32:55.000Z", active: false, }, id: 52,
name: "John Doe",
email: "test@test.test",
createdAt: "2022-08-07T08:47:01.000Z",
active: true,
},
{
id: 4,
name: "Tester",
email: "another@test.test",
createdAt: "2022-09-07T18:32:55.000Z",
active: false,
},
], ],
text: "this is a long test.", text: "this is a long test.",
evaluation: "8.52", evaluation: "8.52",
tags: [ {name: "test"}, {name: "foo"} ], tags: [{name: "test"}, {name: "foo"}],
comments: [ comments: [
{ id: 542, author: { id: 52, name: "John Doe", email: "test@test.test", createdAt: "2022-08-07T08:47:01.000Z", active: true, }, message: "comment content", }, {
id: 542,
author: {
id: 52,
name: "John Doe",
email: "test@test.test",
createdAt: "2022-08-07T08:47:01.000Z",
active: true,
},
message: "comment content",
},
], ],
}); });
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: [ authors: [{id: 52}, {id: 4, active: true}],
{ 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: [
{ {
@ -288,8 +401,7 @@ describe("model", () => {
}); });
it("deserializes and patches with fields that are not properties", () => { it("deserializes and patches with fields that are not properties", () => {
class TestModel class TestModel {
{
static model = defineModel({ static model = defineModel({
Class: TestModel, Class: TestModel,
properties: { properties: {
@ -297,12 +409,12 @@ describe("model", () => {
label: s.property.string(), label: s.property.string(),
}, },
identifier: "id", identifier: "id",
}) });
id: number; id: number;
label: string; label: string;
notAProperty: { hello: string } = { hello: "world" }; notAProperty: {hello: string} = {hello: "world"};
} }
const deserializedModel = TestModel.model.parse({ const deserializedModel = TestModel.model.parse({
@ -313,17 +425,163 @@ describe("model", () => {
expect(deserializedModel.label).toBe("testing"); expect(deserializedModel.label).toBe("testing");
expect(deserializedModel.notAProperty?.hello).toBe("world"); expect(deserializedModel.notAProperty?.hello).toBe("world");
const clonedDeserializedModel = TestModel.model.model(deserializedModel).clone(); const clonedDeserializedModel = TestModel.model
.model(deserializedModel)
.clone();
deserializedModel.label = "new!"; deserializedModel.label = "new!";
expect(TestModel.model.model(deserializedModel).patch()).toStrictEqual({ id: 5, label: "new!" }); expect(TestModel.model.model(deserializedModel).patch()).toStrictEqual({
id: 5,
label: "new!",
});
deserializedModel.notAProperty.hello = "monster"; deserializedModel.notAProperty.hello = "monster";
expect(TestModel.model.model(deserializedModel).patch()).toStrictEqual({ id: 5 }); expect(TestModel.model.model(deserializedModel).patch()).toStrictEqual({
id: 5,
});
expect(TestModel.model.model(deserializedModel).serialize()).toStrictEqual({ id: 5, label: "new!" }); expect(TestModel.model.model(deserializedModel).serialize()).toStrictEqual({
id: 5,
label: "new!",
});
expect(TestModel.model.model(clonedDeserializedModel).serialize()).toStrictEqual({ id: 5, label: "testing" }); expect(
TestModel.model.model(clonedDeserializedModel).serialize(),
).toStrictEqual({id: 5, label: "testing"});
expect(clonedDeserializedModel.notAProperty.hello).toEqual("world"); expect(clonedDeserializedModel.notAProperty.hello).toEqual("world");
}); });
it("assigns properties, ignoring fields which are not properties", () => {
const deserializedArticle = ArticleModel.parse({
id: 1,
title: "this is a test",
authors: [
{
id: 52,
name: "John Doe",
email: "test@test.test",
createdAt: "2022-08-07T08:47:01.000Z",
active: true,
},
{
id: 4,
name: "Tester",
email: "another@test.test",
createdAt: "2022-09-07T18:32:55.000Z",
active: false,
},
],
text: "this is a long test.",
evaluation: "8.52",
tags: [{name: "test"}, {name: "foo"}],
comments: [
{
id: 542,
author: {
id: 52,
name: "John Doe",
email: "test@test.test",
createdAt: "2022-08-07T08:47:01.000Z",
active: true,
},
message: "comment content",
},
],
});
// Assign title and text, html is silently ignored.
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(ArticleModel.model(deserializedArticle).patch()).toStrictEqual({
id: 1,
title: "something else",
text: "fully new text! yes!",
});
});
it("initializes a model from properties values", () => {
const testArticle = ArticleModel.from({
title: "this is a test",
authors: [
AccountModel.from({
name: "John Doe",
email: "test@test.test",
createdAt: new Date(),
active: true,
}),
],
text: "this is a long text",
evaluation: 8.52,
tags: [{name: "test"}, {name: "foo"}],
unknownField: true,
anotherOne: "test",
});
expect(testArticle.title).toBe("this is a test");
expect(testArticle.text).toBe("this is a long text");
expect(testArticle.evaluation).toBe(8.52);
expect(testArticle.authors).toHaveLength(1);
expect(testArticle.authors[0]?.name).toBe("John Doe");
expect((testArticle as any).unknownField).toBeUndefined();
expect((testArticle as any).anotherOne).toBeUndefined();
});
it("applies patches to an existing model", () => {
const testArticle = ArticleModel.from({
id: 1,
title: "this is a test",
authors: [
AccountModel.from({
id: 55,
name: "John Doe",
email: "test@test.test",
createdAt: new Date(),
active: true,
}),
],
text: "this is a long text",
evaluation: 8.52,
tags: [{name: "test"}, {name: "foo"}],
unknownField: true,
anotherOne: "test",
});
ArticleModel.model(testArticle).resetDiff();
// Test simple patch.
ArticleModel.model(testArticle).applyPatch({
title: "new title",
});
expect(testArticle.title).toBe("new title");
expect(ArticleModel.model(testArticle).serializeDiff()).toStrictEqual({
id: 1,
});
// Test originals update propagation.
ArticleModel.model(testArticle).applyPatch({
authors: [{email: "john@test.test"}],
});
expect(testArticle.authors[0].email).toBe("john@test.test");
expect(ArticleModel.model(testArticle).serializeDiff()).toStrictEqual({
id: 1,
});
// Test without originals update.
ArticleModel.model(testArticle).applyPatch(
{
authors: [{name: "Johnny"}],
},
false,
);
expect(testArticle.authors[0].name).toBe("Johnny");
expect(ArticleModel.model(testArticle).serializeDiff()).toStrictEqual({
id: 1,
authors: [{id: 55, name: "Johnny"}],
});
});
}); });

View file

@ -1,8 +1,7 @@
import {describe, expect, test} from "vitest"; import {describe, expect, test} from "vitest";
import {ArrayType, InvalidTypeValueError, s} from "../../../src/library"; import {ArrayType, InvalidTypeValueError, s} from "../../../src/library";
class TestModel class TestModel {
{
id: number; id: number;
name: string; name: string;
price: number; price: number;
@ -19,119 +18,423 @@ describe("array type", () => {
identifier: "id", identifier: "id",
}); });
test("array type definition", () => { test("definition", () => {
const arrayType = s.property.array(s.property.model(testModel)); const arrayType = s.property.array(s.property.model(testModel));
expect(arrayType.type).toBeInstanceOf(ArrayType); expect(arrayType.type).toBeInstanceOf(ArrayType);
}); });
const testProperty = s.property.array(s.property.decimal()); const testProperty = s.property.array(s.property.decimal());
test("array type functions", () => { describe("serialize", () => {
expect(testProperty.type.serialize([12.547, 8, -52.11])).toEqual(["12.547", "8", "-52.11"]); test("serialize", () => {
expect(testProperty.type.deserialize(["12.547", "8", "-52.11"])).toEqual([12.547, 8, -52.11]); expect(testProperty.type.serialize([12.547, 8, -52.11])).toEqual([
"12.547",
{ // Try to serialize the difference of an array with one changed model. "8",
const propertyValue = [ "-52.11",
testModel.model(Object.assign(new TestModel(), { id: 1, name: "test", price: 22 })).instance,
testModel.model(Object.assign(new TestModel(), { id: 2, name: "another", price: 12.55 })).instance,
];
propertyValue[0].name = "new";
expect(s.property.array(s.property.model(testModel)).type.serializeDiff(propertyValue)).toEqual([
{ id: 1, name: "new" },
{ id: 2 },
]); ]);
}
expect(testProperty.type.serialize(null)).toBe(null); expect(testProperty.type.serialize(null)).toBe(null);
expect(testProperty.type.deserialize(null)).toBe(null);
expect(testProperty.type.serializeDiff(null)).toBe(null);
expect(testProperty.type.serialize(undefined)).toBe(undefined); expect(testProperty.type.serialize(undefined)).toBe(undefined);
expect(testProperty.type.deserialize(undefined)).toBe(undefined); });
expect(testProperty.type.serializeDiff(undefined)).toBe(undefined);
expect(testProperty.type.hasChanged([12.547, 8, -52.11], [12.547, 8, -52.11])).toBeFalsy(); test("invalid parameters", () => {
expect(() => testProperty.type.serialize({} as any)).toThrowError(
InvalidTypeValueError,
);
});
});
describe("deserialize", () => {
test("deserialize", () => {
expect(testProperty.type.deserialize(["12.547", "8", "-52.11"])).toEqual([
12.547, 8, -52.11,
]);
expect(testProperty.type.deserialize(null)).toBe(null);
expect(testProperty.type.deserialize(undefined)).toBe(undefined);
});
test("invalid parameters", () => {
expect(() => testProperty.type.deserialize({} as any)).toThrowError(
InvalidTypeValueError,
);
});
});
describe("serializeDiff", () => {
test("serializeDiff", () => {
{
// Try to serialize the difference of an array with one changed model.
const propertyValue = [
testModel.model(
Object.assign(new TestModel(), {id: 1, name: "test", price: 22}),
).instance,
testModel.model(
Object.assign(new TestModel(), {
id: 2,
name: "another",
price: 12.55,
}),
).instance,
];
propertyValue[0].name = "new";
expect(
s.property
.array(s.property.model(testModel))
.type.serializeDiff(propertyValue),
).toEqual([{id: 1, name: "new"}, {id: 2}]);
}
expect(testProperty.type.serializeDiff(null)).toBe(null);
expect(testProperty.type.serializeDiff(undefined)).toBe(undefined);
});
test("invalid parameters", () => {
expect(() => testProperty.type.serializeDiff({} as any)).toThrowError(
InvalidTypeValueError,
);
});
});
describe("hasChanged", () => {
test("hasChanged", () => {
expect(
testProperty.type.hasChanged([12.547, 8, -52.11], [12.547, 8, -52.11]),
).toBeFalsy();
expect(testProperty.type.hasChanged(null, null)).toBeFalsy(); expect(testProperty.type.hasChanged(null, null)).toBeFalsy();
expect(testProperty.type.hasChanged(undefined, undefined)).toBeFalsy(); expect(testProperty.type.hasChanged(undefined, undefined)).toBeFalsy();
expect(testProperty.type.hasChanged(null, undefined)).toBeTruthy(); expect(testProperty.type.hasChanged(null, undefined)).toBeTruthy();
expect(testProperty.type.hasChanged(undefined, null)).toBeTruthy(); expect(testProperty.type.hasChanged(undefined, null)).toBeTruthy();
expect(testProperty.type.hasChanged(null, [12.547, 8, -52.11])).toBeTruthy(); expect(
expect(testProperty.type.hasChanged(undefined, [12.547, 8, -52.11])).toBeTruthy(); testProperty.type.hasChanged(null, [12.547, 8, -52.11]),
expect(testProperty.type.hasChanged([12.547, 8, -52.11], null)).toBeTruthy(); ).toBeTruthy();
expect(testProperty.type.hasChanged([12.547, 8, -52.11], undefined)).toBeTruthy(); expect(
expect(testProperty.type.hasChanged([12.547, 8, -52.11], [12.547, -52.11, 8])).toBeTruthy(); testProperty.type.hasChanged(undefined, [12.547, 8, -52.11]),
expect(testProperty.type.hasChanged([12.547, -52.11, 8], [12.547, 8, -52.11])).toBeTruthy(); ).toBeTruthy();
expect(testProperty.type.hasChanged([12.547, 8, -52.11], [12.547, 8])).toBeTruthy(); expect(
expect(testProperty.type.hasChanged([12.547, 8], [12.547, 8, -52.11])).toBeTruthy(); testProperty.type.hasChanged([12.547, 8, -52.11], null),
).toBeTruthy();
expect(
testProperty.type.hasChanged([12.547, 8, -52.11], undefined),
).toBeTruthy();
expect(
testProperty.type.hasChanged([12.547, 8, -52.11], [12.547, -52.11, 8]),
).toBeTruthy();
expect(
testProperty.type.hasChanged([12.547, -52.11, 8], [12.547, 8, -52.11]),
).toBeTruthy();
expect(
testProperty.type.hasChanged([12.547, 8, -52.11], [12.547, 8]),
).toBeTruthy();
expect(
testProperty.type.hasChanged([12.547, 8], [12.547, 8, -52.11]),
).toBeTruthy();
});
expect(testProperty.type.serializedHasChanged(["12.547", "8", "-52.11"], ["12.547", "8", "-52.11"])).toBeFalsy(); test("invalid parameters", () => {
expect(testProperty.type.hasChanged({} as any, {} as any)).toBeTruthy();
expect(
testProperty.type.hasChanged(false as any, false as any),
).toBeFalsy();
});
});
describe("serializedHasChanged", () => {
test("serializedHasChanged", () => {
expect(
testProperty.type.serializedHasChanged(
["12.547", "8", "-52.11"],
["12.547", "8", "-52.11"],
),
).toBeFalsy();
expect(testProperty.type.serializedHasChanged(null, null)).toBeFalsy(); expect(testProperty.type.serializedHasChanged(null, null)).toBeFalsy();
expect(testProperty.type.serializedHasChanged(undefined, undefined)).toBeFalsy(); expect(
expect(testProperty.type.serializedHasChanged(null, undefined)).toBeTruthy(); testProperty.type.serializedHasChanged(undefined, undefined),
expect(testProperty.type.serializedHasChanged(undefined, null)).toBeTruthy(); ).toBeFalsy();
expect(testProperty.type.serializedHasChanged(null, ["12.547", "8", "-52.11"])).toBeTruthy(); expect(
expect(testProperty.type.serializedHasChanged(undefined, ["12.547", "8", "-52.11"])).toBeTruthy(); testProperty.type.serializedHasChanged(null, undefined),
expect(testProperty.type.serializedHasChanged(["12.547", "8", "-52.11"], null)).toBeTruthy(); ).toBeTruthy();
expect(testProperty.type.serializedHasChanged(["12.547", "8", "-52.11"], undefined)).toBeTruthy(); expect(
expect(testProperty.type.serializedHasChanged(["12.547", "8", "-52.11"], ["12.547", "-52.11", "8"])).toBeTruthy(); testProperty.type.serializedHasChanged(undefined, null),
expect(testProperty.type.serializedHasChanged(["12.547", "-52.11", "8"], ["12.547", "8", "-52.11"])).toBeTruthy(); ).toBeTruthy();
expect(testProperty.type.serializedHasChanged(["12.547", "8", "-52.11"], ["12.547", "8"])).toBeTruthy(); expect(
expect(testProperty.type.serializedHasChanged(["12.547", "8"], ["12.547", "8", "-52.11"])).toBeTruthy(); testProperty.type.serializedHasChanged(null, ["12.547", "8", "-52.11"]),
).toBeTruthy();
expect(
testProperty.type.serializedHasChanged(undefined, [
"12.547",
"8",
"-52.11",
]),
).toBeTruthy();
expect(
testProperty.type.serializedHasChanged(["12.547", "8", "-52.11"], null),
).toBeTruthy();
expect(
testProperty.type.serializedHasChanged(
["12.547", "8", "-52.11"],
undefined,
),
).toBeTruthy();
expect(
testProperty.type.serializedHasChanged(
["12.547", "8", "-52.11"],
["12.547", "-52.11", "8"],
),
).toBeTruthy();
expect(
testProperty.type.serializedHasChanged(
["12.547", "-52.11", "8"],
["12.547", "8", "-52.11"],
),
).toBeTruthy();
expect(
testProperty.type.serializedHasChanged(
["12.547", "8", "-52.11"],
["12.547", "8"],
),
).toBeTruthy();
expect(
testProperty.type.serializedHasChanged(
["12.547", "8"],
["12.547", "8", "-52.11"],
),
).toBeTruthy();
});
{ // Try to reset the difference of an array with one changed model. test("invalid parameters", () => {
expect(
testProperty.type.serializedHasChanged({} as any, {} as any),
).toBeTruthy();
expect(
testProperty.type.serializedHasChanged(false as any, false as any),
).toBeFalsy();
});
});
describe("resetDiff", () => {
test("resetDiff", () => {
{
// Try to reset the difference of an array with one changed model.
const propertyValue = [ const propertyValue = [
testModel.model(Object.assign(new TestModel(), { id: 1, name: "test", price: 22 })).instance, testModel.model(
testModel.model(Object.assign(new TestModel(), { id: 2, name: "another", price: 12.55 })).instance, Object.assign(new TestModel(), {id: 1, name: "test", price: 22}),
).instance,
testModel.model(
Object.assign(new TestModel(), {
id: 2,
name: "another",
price: 12.55,
}),
).instance,
]; ];
propertyValue[0].name = "new"; propertyValue[0].name = "new";
expect(s.property.array(s.property.model(testModel)).type.serializeDiff(propertyValue)).toEqual([ expect(
{ id: 1, name: "new" }, s.property
{ id: 2 }, .array(s.property.model(testModel))
]); .type.serializeDiff(propertyValue),
s.property.array(s.property.model(testModel)).type.resetDiff(propertyValue) ).toEqual([{id: 1, name: "new"}, {id: 2}]);
expect(s.property.array(s.property.model(testModel)).type.serializeDiff(propertyValue)).toEqual([ s.property
{ id: 1 }, .array(s.property.model(testModel))
{ id: 2 }, .type.resetDiff(propertyValue);
]); expect(
s.property
.array(s.property.model(testModel))
.type.serializeDiff(propertyValue),
).toEqual([{id: 1}, {id: 2}]);
} }
testProperty.type.resetDiff(undefined); testProperty.type.resetDiff(undefined);
testProperty.type.resetDiff(null); testProperty.type.resetDiff(null);
});
{ // Test that values are cloned in a different array. test("invalid parameters", () => {
expect(() => testProperty.type.resetDiff({} as any)).not.toThrow();
});
});
describe("clone", () => {
test("clone", () => {
{
// Test that values are cloned in a different array.
const propertyValue = [12.547, 8, -52.11]; const propertyValue = [12.547, 8, -52.11];
const clonedPropertyValue = testProperty.type.clone(propertyValue); const clonedPropertyValue = testProperty.type.clone(propertyValue);
expect(clonedPropertyValue).not.toBe(propertyValue); expect(clonedPropertyValue).not.toBe(propertyValue);
expect(clonedPropertyValue).toEqual(propertyValue); expect(clonedPropertyValue).toEqual(propertyValue);
} }
{ // Test that values are cloned recursively. {
// Test that values are cloned recursively.
const propertyValue = [ const propertyValue = [
testModel.model(Object.assign(new TestModel(), { id: 1, name: "test", price: 22 })).instance, testModel.model(
testModel.model(Object.assign(new TestModel(), { id: 2, name: "another", price: 12.55 })).instance, Object.assign(new TestModel(), {id: 1, name: "test", price: 22}),
).instance,
testModel.model(
Object.assign(new TestModel(), {
id: 2,
name: "another",
price: 12.55,
}),
).instance,
]; ];
// The arrays are different. // The arrays are different.
const clonedPropertyValue = s.property.array(s.property.model(testModel)).type.clone(propertyValue); const clonedPropertyValue = s.property
.array(s.property.model(testModel))
.type.clone(propertyValue);
expect(clonedPropertyValue).not.toBe(propertyValue); expect(clonedPropertyValue).not.toBe(propertyValue);
// Array values must be different objects but have the same values. // Array values must be different objects but have the same values.
expect(clonedPropertyValue[0]).not.toBe(propertyValue[0]); expect(clonedPropertyValue[0]).not.toBe(propertyValue[0]);
expect(clonedPropertyValue[1]).not.toBe(propertyValue[1]); expect(clonedPropertyValue[1]).not.toBe(propertyValue[1]);
expect(testModel.model(clonedPropertyValue[0]).getInstanceProperties()).toEqual(testModel.model(propertyValue[0]).getInstanceProperties()); expect(
expect(testModel.model(clonedPropertyValue[1]).getInstanceProperties()).toEqual(testModel.model(propertyValue[1]).getInstanceProperties()); testModel.model(clonedPropertyValue[0]).getInstanceProperties(),
).toEqual(testModel.model(propertyValue[0]).getInstanceProperties());
expect(
testModel.model(clonedPropertyValue[1]).getInstanceProperties(),
).toEqual(testModel.model(propertyValue[1]).getInstanceProperties());
} }
expect(testProperty.type.clone(undefined)).toBe(undefined); expect(testProperty.type.clone(undefined)).toBe(undefined);
expect(testProperty.type.clone(null)).toBe(null); expect(testProperty.type.clone(null)).toBe(null);
}); });
test("invalid parameters types", () => { test("invalid parameters", () => {
expect(() => testProperty.type.serialize({} as any)).toThrowError(InvalidTypeValueError); expect(() => testProperty.type.clone({} as any)).toThrowError(
expect(() => testProperty.type.deserialize({} as any)).toThrowError(InvalidTypeValueError); InvalidTypeValueError,
expect(() => testProperty.type.serializeDiff({} as any)).toThrowError(InvalidTypeValueError); );
expect(() => testProperty.type.resetDiff({} as any)).not.toThrow(); });
expect(testProperty.type.hasChanged({} as any, {} as any)).toBeTruthy(); });
expect(testProperty.type.hasChanged(false as any, false as any)).toBeFalsy();
expect(testProperty.type.serializedHasChanged({} as any, {} as any)).toBeTruthy(); test("applyPatch", () => {
expect(testProperty.type.serializedHasChanged(false as any, false as any)).toBeFalsy(); {
expect(() => testProperty.type.clone({} as any)).toThrowError(InvalidTypeValueError); // Test simple patch.
expect(
testProperty.type.applyPatch(
[12.547, 8, -52.11],
["12.547", "444.34", "-52.11"],
true,
),
).toEqual([12.547, 444.34, -52.11]);
expect(
testProperty.type.applyPatch(
undefined,
["12.547", "444.34", "-52.11"],
false,
),
).toEqual([12.547, 444.34, -52.11]);
expect(
testProperty.type.applyPatch(
null,
["12.547", "444.34", "-52.11"],
false,
),
).toEqual([12.547, 444.34, -52.11]);
expect(
testProperty.type.applyPatch([12.547, 8, -52.11], undefined, false),
).toBeUndefined();
expect(
testProperty.type.applyPatch([12.547, 8, -52.11], null, false),
).toBeNull();
}
{
// Invalid patch.
expect(() =>
testProperty.type.applyPatch([12.547, 8, -52.11], {} as any, false),
).toThrow(InvalidTypeValueError);
}
{
// Test recursive patch.
const propertyValue = [
testModel.model(
Object.assign(new TestModel(), {id: 1, name: "test", price: 22}),
).instance,
testModel.model(
Object.assign(new TestModel(), {
id: 2,
name: "another",
price: 12.55,
}),
).instance,
];
const patched = s.property
.array(s.property.model(testModel))
.type.applyPatch(
propertyValue,
[
{
id: 1,
name: "new",
},
{
id: 2,
price: "13.65",
},
],
true,
);
// Check applied patch.
expect(patched).toEqual([
testModel.parse({id: 1, name: "new", price: "22"}),
testModel.parse({id: 2, name: "another", price: "13.65"}),
]);
// Check that originals have been updated.
expect(testModel.model(patched[0]).serializeDiff()).toEqual({id: 1});
patched[0].name = "test";
expect(testModel.model(patched[0]).serializeDiff()).toEqual({
id: 1,
name: "test",
});
expect(testModel.model(patched[1]).serializeDiff()).toEqual({id: 2});
patched[1].price = 12.55;
expect(testModel.model(patched[1]).serializeDiff()).toEqual({
id: 2,
price: "12.55",
});
}
{
// Test recursive patch without originals update.-
const propertyValue = [
testModel.model(
Object.assign(new TestModel(), {id: 1, name: "test", price: 22}),
).instance,
testModel.model(
Object.assign(new TestModel(), {
id: 2,
name: "another",
price: 12.55,
}),
).instance,
];
const patched = s.property
.array(s.property.model(testModel))
.type.applyPatch(
propertyValue,
[
{
id: 1,
name: "new",
},
{
id: 2,
price: "13.65",
},
],
false,
);
// Check that originals haven't been updated.
expect(testModel.model(patched[0]).serializeDiff()).toEqual({
id: 1,
name: "new",
});
expect(testModel.model(patched[1]).serializeDiff()).toEqual({
id: 2,
price: "13.65",
});
}
}); });
}); });

View file

@ -2,7 +2,7 @@ import {describe, expect, test} from "vitest";
import {BooleanType, s} from "../../../src/library"; import {BooleanType, s} from "../../../src/library";
describe("boolean type", () => { describe("boolean type", () => {
test("boolean type definition", () => { test("definition", () => {
{ {
const booleanType = s.property.boolean(); const booleanType = s.property.boolean();
expect(booleanType.type).toBeInstanceOf(BooleanType); expect(booleanType.type).toBeInstanceOf(BooleanType);
@ -13,56 +13,163 @@ describe("boolean type", () => {
} }
}); });
test("boolean type functions", () => { describe("serialize", () => {
test("serialize", () => {
expect(s.property.boolean().type.serialize(false)).toBe(false); expect(s.property.boolean().type.serialize(false)).toBe(false);
expect(s.property.boolean().type.deserialize(false)).toBe(false);
expect(s.property.boolean().type.serializeDiff(true)).toBe(true);
expect(s.property.boolean().type.serialize(null)).toBe(null); expect(s.property.boolean().type.serialize(null)).toBe(null);
expect(s.property.boolean().type.deserialize(null)).toBe(null);
expect(s.property.boolean().type.serializeDiff(null)).toBe(null);
expect(s.property.boolean().type.serialize(undefined)).toBe(undefined); expect(s.property.boolean().type.serialize(undefined)).toBe(undefined);
expect(s.property.boolean().type.deserialize(undefined)).toBe(undefined); });
expect(s.property.boolean().type.serializeDiff(undefined)).toBe(undefined);
test("invalid parameters", () => {
expect(s.property.boolean().type.serialize(1 as any)).toBeTruthy();
expect(s.property.boolean().type.serialize(0 as any)).toBeFalsy();
});
});
describe("deserialize", () => {
test("deserialize", () => {
expect(s.property.boolean().type.deserialize(false)).toBe(false);
expect(s.property.boolean().type.deserialize(null)).toBe(null);
expect(s.property.boolean().type.deserialize(undefined)).toBe(undefined);
});
test("invalid parameters", () => {
expect(s.property.boolean().type.deserialize(1 as any)).toBeTruthy();
expect(s.property.boolean().type.deserialize(0 as any)).toBeFalsy();
});
});
describe("serializeDiff", () => {
test("serializeDiff", () => {
expect(s.property.boolean().type.serializeDiff(true)).toBe(true);
expect(s.property.boolean().type.serializeDiff(null)).toBe(null);
expect(s.property.boolean().type.serializeDiff(undefined)).toBe(
undefined,
);
});
test("invalid parameters", () => {
expect(s.property.boolean().type.serializeDiff(1 as any)).toBeTruthy();
expect(s.property.boolean().type.serializeDiff(0 as any)).toBeFalsy();
});
});
describe("hasChanged", () => {
test("hasChanged", () => {
expect(s.property.boolean().type.hasChanged(true, true)).toBeFalsy(); expect(s.property.boolean().type.hasChanged(true, true)).toBeFalsy();
expect(s.property.boolean().type.hasChanged(null, null)).toBeFalsy(); expect(s.property.boolean().type.hasChanged(null, null)).toBeFalsy();
expect(s.property.boolean().type.hasChanged(undefined, undefined)).toBeFalsy(); expect(
expect(s.property.boolean().type.hasChanged(null, undefined)).toBeTruthy(); s.property.boolean().type.hasChanged(undefined, undefined),
expect(s.property.boolean().type.hasChanged(undefined, null)).toBeTruthy(); ).toBeFalsy();
expect(
s.property.boolean().type.hasChanged(null, undefined),
).toBeTruthy();
expect(
s.property.boolean().type.hasChanged(undefined, null),
).toBeTruthy();
expect(s.property.boolean().type.hasChanged(null, false)).toBeTruthy(); expect(s.property.boolean().type.hasChanged(null, false)).toBeTruthy();
expect(s.property.boolean().type.hasChanged(undefined, false)).toBeTruthy(); expect(
s.property.boolean().type.hasChanged(undefined, false),
).toBeTruthy();
expect(s.property.boolean().type.hasChanged(false, null)).toBeTruthy(); expect(s.property.boolean().type.hasChanged(false, null)).toBeTruthy();
expect(s.property.boolean().type.hasChanged(false, undefined)).toBeTruthy(); expect(
s.property.boolean().type.hasChanged(false, undefined),
).toBeTruthy();
});
expect(s.property.boolean().type.serializedHasChanged(false, false)).toBeFalsy(); test("invalid parameters", () => {
expect(s.property.boolean().type.serializedHasChanged(null, null)).toBeFalsy(); expect(
expect(s.property.boolean().type.serializedHasChanged(undefined, undefined)).toBeFalsy(); s.property.boolean().type.hasChanged({} as any, {} as any),
expect(s.property.boolean().type.serializedHasChanged(null, undefined)).toBeTruthy(); ).toBeTruthy();
expect(s.property.boolean().type.serializedHasChanged(undefined, null)).toBeTruthy(); expect(
expect(s.property.boolean().type.serializedHasChanged(null, false)).toBeTruthy(); s.property.boolean().type.hasChanged(false as any, false as any),
expect(s.property.boolean().type.serializedHasChanged(undefined, false)).toBeTruthy(); ).toBeFalsy();
expect(s.property.boolean().type.serializedHasChanged(false, null)).toBeTruthy(); });
expect(s.property.boolean().type.serializedHasChanged(false, undefined)).toBeTruthy(); });
describe("serializedHasChanged", () => {
test("serializedHasChanged", () => {
expect(
s.property.boolean().type.serializedHasChanged(false, false),
).toBeFalsy();
expect(
s.property.boolean().type.serializedHasChanged(null, null),
).toBeFalsy();
expect(
s.property.boolean().type.serializedHasChanged(undefined, undefined),
).toBeFalsy();
expect(
s.property.boolean().type.serializedHasChanged(null, undefined),
).toBeTruthy();
expect(
s.property.boolean().type.serializedHasChanged(undefined, null),
).toBeTruthy();
expect(
s.property.boolean().type.serializedHasChanged(null, false),
).toBeTruthy();
expect(
s.property.boolean().type.serializedHasChanged(undefined, false),
).toBeTruthy();
expect(
s.property.boolean().type.serializedHasChanged(false, null),
).toBeTruthy();
expect(
s.property.boolean().type.serializedHasChanged(false, undefined),
).toBeTruthy();
});
test("invalid parameters", () => {
expect(
s.property.boolean().type.serializedHasChanged({} as any, {} as any),
).toBeTruthy();
expect(
s.property
.boolean()
.type.serializedHasChanged(false as any, false as any),
).toBeFalsy();
});
});
describe("resetDiff", () => {
test("resetDiff", () => {
s.property.boolean().type.resetDiff(false); s.property.boolean().type.resetDiff(false);
s.property.boolean().type.resetDiff(undefined); s.property.boolean().type.resetDiff(undefined);
s.property.boolean().type.resetDiff(null); s.property.boolean().type.resetDiff(null);
}); });
test("invalid parameters types", () => { test("resetDiff", () => {
expect(s.property.boolean().type.serialize(1 as any)).toBeTruthy(); expect(() =>
expect(s.property.boolean().type.serialize(0 as any)).toBeFalsy(); s.property.boolean().type.resetDiff({} as any),
expect(s.property.boolean().type.deserialize(1 as any)).toBeTruthy(); ).not.toThrow();
expect(s.property.boolean().type.deserialize(0 as any)).toBeFalsy(); });
expect(s.property.boolean().type.serializeDiff(1 as any)).toBeTruthy(); });
expect(s.property.boolean().type.serializeDiff(0 as any)).toBeFalsy();
expect(() => s.property.boolean().type.resetDiff({} as any)).not.toThrow(); describe("clone", () => {
expect(s.property.boolean().type.hasChanged({} as any, {} as any)).toBeTruthy(); test("invalid parameters", () => {
expect(s.property.boolean().type.hasChanged(false as any, false as any)).toBeFalsy();
expect(s.property.boolean().type.serializedHasChanged({} as any, {} as any)).toBeTruthy();
expect(s.property.boolean().type.serializedHasChanged(false as any, false as any)).toBeFalsy();
expect(s.property.boolean().type.clone({} as any)).toStrictEqual({}); expect(s.property.boolean().type.clone({} as any)).toStrictEqual({});
}); });
});
test("applyPatch", () => {
expect(
s.property.boolean().type.applyPatch(false, true, true),
).toBeTruthy();
expect(
s.property.boolean().type.applyPatch(false, true, false),
).toBeTruthy();
expect(
s.property.boolean().type.applyPatch(true, false, false),
).toBeFalsy();
expect(
s.property.boolean().type.applyPatch(false, undefined, false),
).toBeUndefined();
expect(s.property.boolean().type.applyPatch(false, null, false)).toBeNull();
expect(
s.property.boolean().type.applyPatch(undefined, null, false),
).toBeNull();
expect(s.property.boolean().type.applyPatch(null, null, false)).toBeNull();
expect(
s.property.boolean().type.applyPatch(null, false, false),
).toBeFalsy();
});
}); });

View file

@ -4,71 +4,229 @@ import {DateType, InvalidTypeValueError, s} from "../../../src/library";
describe("date type", () => { describe("date type", () => {
const testDate = new Date(); const testDate = new Date();
test("date type definition", () => { test("definition", () => {
const dateType = s.property.date(); const dateType = s.property.date();
expect(dateType.type).toBeInstanceOf(DateType); expect(dateType.type).toBeInstanceOf(DateType);
}); });
test("date type functions", () => { describe("serialize", () => {
expect(s.property.date().type.serialize(testDate)).toBe(testDate.toISOString()); test("serialize", () => {
expect(s.property.date().type.deserialize(testDate.toISOString())?.getTime()).toBe(testDate.getTime()); expect(s.property.date().type.serialize(testDate)).toBe(
expect(s.property.date().type.serializeDiff(new Date(testDate))).toBe(testDate.toISOString()); testDate.toISOString(),
expect(s.property.date().type.deserialize("2565152-2156121-256123121 5121544175:21515612").valueOf()).toBeNaN(); );
expect(s.property.date().type.serialize(new Date(NaN))).toBe((new Date(NaN)).toString());
expect(s.property.date().type.serialize(new Date(NaN))).toBe(
new Date(NaN).toString(),
);
expect(s.property.date().type.serialize(null)).toBe(null); expect(s.property.date().type.serialize(null)).toBe(null);
expect(s.property.date().type.deserialize(null)).toBe(null);
expect(s.property.date().type.serializeDiff(null)).toBe(null);
expect(s.property.date().type.serialize(undefined)).toBe(undefined); expect(s.property.date().type.serialize(undefined)).toBe(undefined);
expect(s.property.date().type.deserialize(undefined)).toBe(undefined); });
expect(s.property.date().type.serializeDiff(undefined)).toBe(undefined);
expect(s.property.date().type.hasChanged(testDate, new Date(testDate))).toBeFalsy(); test("invalid parameters", () => {
expect(() => s.property.date().type.serialize({} as any)).toThrowError(
InvalidTypeValueError,
);
});
});
describe("deserialize", () => {
test("deserialize", () => {
expect(
s.property.date().type.deserialize(testDate.toISOString())?.getTime(),
).toBe(testDate.getTime());
expect(
s.property
.date()
.type.deserialize("2565152-2156121-256123121 5121544175:21515612")
.valueOf(),
).toBeNaN();
expect(s.property.date().type.deserialize(null)).toBe(null);
expect(s.property.date().type.deserialize(undefined)).toBe(undefined);
});
test("invalid parameters", () => {
expect(
s.property
.date()
.type.deserialize({} as any)
.getTime(),
).toBe(NaN);
});
});
describe("serializeDiff", () => {
test("serializeDiff", () => {
expect(s.property.date().type.serializeDiff(new Date(testDate))).toBe(
testDate.toISOString(),
);
expect(s.property.date().type.serializeDiff(null)).toBe(null);
expect(s.property.date().type.serializeDiff(undefined)).toBe(undefined);
});
test("invalid parameters", () => {
expect(() =>
s.property.date().type.serializeDiff({} as any),
).toThrowError(InvalidTypeValueError);
});
});
describe("hasChanged", () => {
test("hasChanged", () => {
expect(
s.property.date().type.hasChanged(testDate, new Date(testDate)),
).toBeFalsy();
expect(s.property.date().type.hasChanged(null, null)).toBeFalsy(); expect(s.property.date().type.hasChanged(null, null)).toBeFalsy();
expect(s.property.date().type.hasChanged(undefined, undefined)).toBeFalsy(); expect(
s.property.date().type.hasChanged(undefined, undefined),
).toBeFalsy();
expect(s.property.date().type.hasChanged(null, undefined)).toBeTruthy(); expect(s.property.date().type.hasChanged(null, undefined)).toBeTruthy();
expect(s.property.date().type.hasChanged(undefined, null)).toBeTruthy(); expect(s.property.date().type.hasChanged(undefined, null)).toBeTruthy();
expect(s.property.date().type.hasChanged(null, testDate)).toBeTruthy(); expect(s.property.date().type.hasChanged(null, testDate)).toBeTruthy();
expect(s.property.date().type.hasChanged(undefined, testDate)).toBeTruthy(); expect(
s.property.date().type.hasChanged(undefined, testDate),
).toBeTruthy();
expect(s.property.date().type.hasChanged(testDate, null)).toBeTruthy(); expect(s.property.date().type.hasChanged(testDate, null)).toBeTruthy();
expect(s.property.date().type.hasChanged(new Date(NaN), null)).toBeTruthy(); expect(
expect(s.property.date().type.hasChanged(new Date(NaN), undefined)).toBeTruthy(); s.property.date().type.hasChanged(new Date(NaN), null),
expect(s.property.date().type.hasChanged(new Date(NaN), new Date(NaN))).toBeFalsy(); ).toBeTruthy();
expect(
s.property.date().type.hasChanged(new Date(NaN), undefined),
).toBeTruthy();
expect(
s.property.date().type.hasChanged(new Date(NaN), new Date(NaN)),
).toBeFalsy();
});
expect(s.property.date().type.serializedHasChanged(testDate.toISOString(), (new Date(testDate)).toISOString())).toBeFalsy(); test("invalid parameters", () => {
expect(s.property.date().type.serializedHasChanged(null, null)).toBeFalsy(); expect(
expect(s.property.date().type.serializedHasChanged(undefined, undefined)).toBeFalsy(); s.property.date().type.hasChanged({} as any, {} as any),
expect(s.property.date().type.serializedHasChanged(null, undefined)).toBeTruthy(); ).toBeTruthy();
expect(s.property.date().type.serializedHasChanged(undefined, null)).toBeTruthy(); expect(
expect(s.property.date().type.serializedHasChanged(null, testDate.toISOString())).toBeTruthy(); s.property.date().type.hasChanged(false as any, false as any),
expect(s.property.date().type.serializedHasChanged(undefined, testDate.toISOString())).toBeTruthy(); ).toBeFalsy();
expect(s.property.date().type.serializedHasChanged(testDate.toISOString(), null)).toBeTruthy(); });
expect(s.property.date().type.serializedHasChanged((new Date(NaN)).toString(), null)).toBeTruthy(); });
expect(s.property.date().type.serializedHasChanged((new Date(NaN)).toString(), undefined)).toBeTruthy();
expect(s.property.date().type.serializedHasChanged((new Date(NaN)).toString(), (new Date(NaN)).toString())).toBeFalsy();
describe("serializedHasChanged", () => {
test("serializedHasChanged", () => {
expect(
s.property
.date()
.type.serializedHasChanged(
testDate.toISOString(),
new Date(testDate).toISOString(),
),
).toBeFalsy();
expect(
s.property.date().type.serializedHasChanged(null, null),
).toBeFalsy();
expect(
s.property.date().type.serializedHasChanged(undefined, undefined),
).toBeFalsy();
expect(
s.property.date().type.serializedHasChanged(null, undefined),
).toBeTruthy();
expect(
s.property.date().type.serializedHasChanged(undefined, null),
).toBeTruthy();
expect(
s.property
.date()
.type.serializedHasChanged(null, testDate.toISOString()),
).toBeTruthy();
expect(
s.property
.date()
.type.serializedHasChanged(undefined, testDate.toISOString()),
).toBeTruthy();
expect(
s.property
.date()
.type.serializedHasChanged(testDate.toISOString(), null),
).toBeTruthy();
expect(
s.property
.date()
.type.serializedHasChanged(new Date(NaN).toString(), null),
).toBeTruthy();
expect(
s.property
.date()
.type.serializedHasChanged(new Date(NaN).toString(), undefined),
).toBeTruthy();
expect(
s.property
.date()
.type.serializedHasChanged(
new Date(NaN).toString(),
new Date(NaN).toString(),
),
).toBeFalsy();
});
test("invalid parameters", () => {
expect(
s.property.date().type.serializedHasChanged({} as any, {} as any),
).toBeTruthy();
expect(
s.property.date().type.serializedHasChanged(false as any, false as any),
).toBeFalsy();
expect(s.property.date().type.clone({} as any)).toStrictEqual({});
});
});
describe("resetDiff", () => {
test("resetDiff", () => {
s.property.date().type.resetDiff(testDate); s.property.date().type.resetDiff(testDate);
s.property.date().type.resetDiff(undefined); s.property.date().type.resetDiff(undefined);
s.property.date().type.resetDiff(null); s.property.date().type.resetDiff(null);
});
{ // Test that the date is cloned in a different object. test("invalid parameters", () => {
expect(() => s.property.date().type.resetDiff({} as any)).not.toThrow();
});
});
test("clone", () => {
// Test that the date is cloned in a different object.
const propertyValue = new Date(); const propertyValue = new Date();
const clonedPropertyValue = s.property.date().type.clone(propertyValue); const clonedPropertyValue = s.property.date().type.clone(propertyValue);
expect(clonedPropertyValue).not.toBe(propertyValue); expect(clonedPropertyValue).not.toBe(propertyValue);
expect(clonedPropertyValue).toEqual(propertyValue); expect(clonedPropertyValue).toEqual(propertyValue);
}
}); });
test("invalid parameters types", () => { test("applyPatch", () => {
expect(() => s.property.date().type.serialize({} as any)).toThrowError(InvalidTypeValueError); expect(
expect(s.property.date().type.deserialize({} as any).getTime()).toBe(NaN); s.property
expect(() => s.property.date().type.serializeDiff({} as any)).toThrowError(InvalidTypeValueError); .date()
expect(() => s.property.date().type.resetDiff({} as any)).not.toThrow(); .type.applyPatch(new Date("2022-02-22"), testDate.toISOString(), false)
expect(s.property.date().type.hasChanged({} as any, {} as any)).toBeTruthy(); ?.getTime(),
expect(s.property.date().type.hasChanged(false as any, false as any)).toBeFalsy(); ).toBe(testDate.getTime());
expect(s.property.date().type.serializedHasChanged({} as any, {} as any)).toBeTruthy(); expect(
expect(s.property.date().type.serializedHasChanged(false as any, false as any)).toBeFalsy(); s.property
expect(s.property.date().type.clone({} as any)).toStrictEqual({}); .date()
.type.applyPatch(null, testDate.toISOString(), true)
?.getTime(),
).toBe(testDate.getTime());
expect(
s.property
.date()
.type.applyPatch(
undefined,
"2565152-2156121-256123121 5121544175:21515612",
false,
)
.valueOf(),
).toBeNaN();
expect(
s.property.date().type.applyPatch(new Date(), undefined, false),
).toBeUndefined();
expect(
s.property.date().type.applyPatch(new Date(), null, false),
).toBeNull();
}); });
}); });

View file

@ -7,54 +7,158 @@ describe("decimal type", () => {
expect(decimalType.type).toBeInstanceOf(DecimalType); expect(decimalType.type).toBeInstanceOf(DecimalType);
}); });
test("decimal type functions", () => { describe("serialize", () => {
test("serialize", () => {
expect(s.property.decimal().type.serialize(5.257)).toBe("5.257"); expect(s.property.decimal().type.serialize(5.257)).toBe("5.257");
expect(s.property.decimal().type.deserialize("5.257")).toBe(5.257);
expect(s.property.decimal().type.serializeDiff(542)).toBe("542");
expect(s.property.decimal().type.serialize(null)).toBe(null); expect(s.property.decimal().type.serialize(null)).toBe(null);
expect(s.property.decimal().type.deserialize(null)).toBe(null);
expect(s.property.decimal().type.serializeDiff(null)).toBe(null);
expect(s.property.decimal().type.serialize(undefined)).toBe(undefined); expect(s.property.decimal().type.serialize(undefined)).toBe(undefined);
});
test("invalid parameters", () => {
expect(() => s.property.decimal().type.serialize({} as any)).toThrowError(
InvalidTypeValueError,
);
});
});
describe("deserialize", () => {
test("deserialize", () => {
expect(s.property.decimal().type.deserialize("5.257")).toBe(5.257);
expect(s.property.decimal().type.deserialize(null)).toBe(null);
expect(s.property.decimal().type.deserialize(undefined)).toBe(undefined); expect(s.property.decimal().type.deserialize(undefined)).toBe(undefined);
expect(s.property.decimal().type.serializeDiff(undefined)).toBe(undefined); });
expect(s.property.decimal().type.hasChanged(5.257, 5.257)).toBeFalsy(); test("invalid parameters", () => {
expect(s.property.decimal().type.hasChanged(null, null)).toBeFalsy(); expect(s.property.decimal().type.deserialize({} as any)).toBe(NaN);
expect(s.property.decimal().type.hasChanged(undefined, undefined)).toBeFalsy(); expect(s.property.decimal().type.deserialize({} as any)).toBe(NaN);
expect(s.property.decimal().type.hasChanged(null, undefined)).toBeTruthy(); });
expect(s.property.decimal().type.hasChanged(undefined, null)).toBeTruthy(); });
expect(s.property.decimal().type.hasChanged(null, 5.257)).toBeTruthy();
expect(s.property.decimal().type.hasChanged(undefined, 5.257)).toBeTruthy();
expect(s.property.decimal().type.hasChanged(5.257, null)).toBeTruthy();
expect(s.property.decimal().type.hasChanged(5.257, undefined)).toBeTruthy();
expect(s.property.decimal().type.serializedHasChanged("5.257", "5.257")).toBeFalsy(); describe("serializeDiff", () => {
expect(s.property.decimal().type.serializedHasChanged(null, null)).toBeFalsy(); test("serializeDiff", () => {
expect(s.property.decimal().type.serializedHasChanged(undefined, undefined)).toBeFalsy(); expect(s.property.decimal().type.serializeDiff(542)).toBe("542");
expect(s.property.decimal().type.serializedHasChanged(null, undefined)).toBeTruthy();
expect(s.property.decimal().type.serializedHasChanged(undefined, null)).toBeTruthy();
expect(s.property.decimal().type.serializedHasChanged(null, "5.257")).toBeTruthy();
expect(s.property.decimal().type.serializedHasChanged(undefined, "5.257")).toBeTruthy();
expect(s.property.decimal().type.serializedHasChanged("5.257", null)).toBeTruthy();
expect(s.property.decimal().type.serializedHasChanged("5.257", undefined)).toBeTruthy();
expect(s.property.decimal().type.serializeDiff(null)).toBe(null);
expect(s.property.decimal().type.serializeDiff(undefined)).toBe(
undefined,
);
});
test("invalid parameters", () => {
expect(() =>
s.property.decimal().type.serializeDiff({} as any),
).toThrowError(InvalidTypeValueError);
});
});
describe("resetDiff", () => {
test("resetDiff", () => {
s.property.decimal().type.resetDiff(5.257); s.property.decimal().type.resetDiff(5.257);
s.property.decimal().type.resetDiff(undefined); s.property.decimal().type.resetDiff(undefined);
s.property.decimal().type.resetDiff(null); s.property.decimal().type.resetDiff(null);
}); });
test("invalid parameters types", () => { test("invalid parameters", () => {
expect(() => s.property.decimal().type.serialize({} as any)).toThrowError(InvalidTypeValueError); expect(() =>
expect(s.property.decimal().type.deserialize({} as any)).toBe(NaN); s.property.decimal().type.resetDiff({} as any),
expect(s.property.decimal().type.deserialize({} as any)).toBe(NaN); ).not.toThrow();
expect(() => s.property.decimal().type.serializeDiff({} as any)).toThrowError(InvalidTypeValueError); });
expect(() => s.property.decimal().type.resetDiff({} as any)).not.toThrow(); });
expect(s.property.decimal().type.hasChanged({} as any, {} as any)).toBeTruthy();
expect(s.property.decimal().type.hasChanged(false as any, false as any)).toBeFalsy(); describe("hasChanged", () => {
expect(s.property.decimal().type.serializedHasChanged({} as any, {} as any)).toBeTruthy(); test("hasChanged", () => {
expect(s.property.decimal().type.serializedHasChanged(false as any, false as any)).toBeFalsy(); expect(s.property.decimal().type.hasChanged(5.257, 5.257)).toBeFalsy();
expect(s.property.decimal().type.hasChanged(null, null)).toBeFalsy();
expect(
s.property.decimal().type.hasChanged(undefined, undefined),
).toBeFalsy();
expect(
s.property.decimal().type.hasChanged(null, undefined),
).toBeTruthy();
expect(
s.property.decimal().type.hasChanged(undefined, null),
).toBeTruthy();
expect(s.property.decimal().type.hasChanged(null, 5.257)).toBeTruthy();
expect(
s.property.decimal().type.hasChanged(undefined, 5.257),
).toBeTruthy();
expect(s.property.decimal().type.hasChanged(5.257, null)).toBeTruthy();
expect(
s.property.decimal().type.hasChanged(5.257, undefined),
).toBeTruthy();
});
test("invalid parameters", () => {
expect(
s.property.decimal().type.hasChanged({} as any, {} as any),
).toBeTruthy();
expect(
s.property.decimal().type.hasChanged(false as any, false as any),
).toBeFalsy();
});
});
describe("serializedHasChanged", () => {
test("serializedHasChanged", () => {
expect(
s.property.decimal().type.serializedHasChanged("5.257", "5.257"),
).toBeFalsy();
expect(
s.property.decimal().type.serializedHasChanged(null, null),
).toBeFalsy();
expect(
s.property.decimal().type.serializedHasChanged(undefined, undefined),
).toBeFalsy();
expect(
s.property.decimal().type.serializedHasChanged(null, undefined),
).toBeTruthy();
expect(
s.property.decimal().type.serializedHasChanged(undefined, null),
).toBeTruthy();
expect(
s.property.decimal().type.serializedHasChanged(null, "5.257"),
).toBeTruthy();
expect(
s.property.decimal().type.serializedHasChanged(undefined, "5.257"),
).toBeTruthy();
expect(
s.property.decimal().type.serializedHasChanged("5.257", null),
).toBeTruthy();
expect(
s.property.decimal().type.serializedHasChanged("5.257", undefined),
).toBeTruthy();
});
test("invalid parameters", () => {
expect(
s.property.decimal().type.serializedHasChanged({} as any, {} as any),
).toBeTruthy();
expect(
s.property
.decimal()
.type.serializedHasChanged(false as any, false as any),
).toBeFalsy();
});
});
describe("clone", () => {
test("invalid parameters", () => {
expect(s.property.decimal().type.clone({} as any)).toStrictEqual({}); expect(s.property.decimal().type.clone({} as any)).toStrictEqual({});
}); });
});
test("applyPatch", () => {
expect(s.property.decimal().type.applyPatch(1, "5.257", false)).toBe(5.257);
expect(s.property.decimal().type.applyPatch(undefined, "5.257", true)).toBe(
5.257,
);
expect(s.property.decimal().type.applyPatch(null, "5.257", false)).toBe(
5.257,
);
expect(
s.property.decimal().type.applyPatch(5.257, undefined, false),
).toBeUndefined();
expect(s.property.decimal().type.applyPatch(5.257, null, false)).toBeNull();
});
}); });

View file

@ -0,0 +1,283 @@
import {describe, expect, test} from "vitest";
import {InvalidTypeValueError, s} from "../../../src/library";
import {MapType} from "../../../src/model/types/map";
describe("map type", () => {
test("definition", () => {
const mapType = s.property.map(s.property.string(), s.property.numeric());
expect(mapType.type).toBeInstanceOf(MapType);
});
const testProperty = s.property.map(
s.property.string(),
s.property.decimal(),
);
const testMapValue = new Map<string, number>();
testMapValue.set("test", 1.52);
testMapValue.set("another", 55);
describe("serialize", () => {
test("serialize", () => {
expect(testProperty.type.serialize(testMapValue)).toEqual({
test: "1.52",
another: "55",
});
expect(testProperty.type.serialize(null)).toEqual(null);
expect(testProperty.type.serialize(undefined)).toEqual(undefined);
});
test("invalid parameters", () => {
expect(() => testProperty.type.serialize(5 as any)).toThrowError(
InvalidTypeValueError,
);
expect(() => testProperty.type.serialize([] as any)).toThrowError(
InvalidTypeValueError,
);
expect(() => testProperty.type.serialize({} as any)).toThrowError(
InvalidTypeValueError,
);
});
});
describe("deserialize", () => {
test("deserialize", () => {
expect(
testProperty.type.deserialize({
test: "1.52",
another: "55",
}),
).toEqual(testMapValue);
expect(testProperty.type.deserialize(null)).toEqual(null);
expect(testProperty.type.deserialize(undefined)).toEqual(undefined);
});
test("invalid parameters", () => {
expect(() => testProperty.type.deserialize(5 as any)).toThrowError(
InvalidTypeValueError,
);
expect(() => testProperty.type.deserialize([] as any)).toThrowError(
InvalidTypeValueError,
);
});
});
describe("serializeDiff", () => {
test("serializeDiff", () => {
expect(testProperty.type.serializeDiff(testMapValue)).toEqual({
test: "1.52",
another: "55",
});
expect(testProperty.type.serializeDiff(null)).toEqual(null);
expect(testProperty.type.serializeDiff(undefined)).toEqual(undefined);
});
test("invalid parameters", () => {
expect(() => testProperty.type.serializeDiff(5 as any)).toThrowError(
InvalidTypeValueError,
);
expect(() => testProperty.type.serializeDiff([] as any)).toThrowError(
InvalidTypeValueError,
);
expect(() => testProperty.type.serializeDiff({} as any)).toThrowError(
InvalidTypeValueError,
);
});
});
describe("hasChanged", () => {
test("hasChanged", () => {
const anotherTestMapValue = new Map<string, number>();
anotherTestMapValue.set("test", 1.52);
anotherTestMapValue.set("another", 55);
expect(
testProperty.type.hasChanged(testMapValue, anotherTestMapValue),
).toBeFalsy();
anotherTestMapValue.set("test", 1.521);
expect(
testProperty.type.hasChanged(testMapValue, anotherTestMapValue),
).toBeTruthy();
anotherTestMapValue.delete("test");
expect(
testProperty.type.hasChanged(testMapValue, anotherTestMapValue),
).toBeTruthy();
expect(testProperty.type.hasChanged(null, null)).toBeFalsy();
expect(testProperty.type.hasChanged(undefined, undefined)).toBeFalsy();
expect(testProperty.type.hasChanged(null, undefined)).toBeTruthy();
expect(testProperty.type.hasChanged(undefined, null)).toBeTruthy();
expect(testProperty.type.hasChanged(null, testMapValue)).toBeTruthy();
expect(
testProperty.type.hasChanged(undefined, testMapValue),
).toBeTruthy();
expect(testProperty.type.hasChanged(testMapValue, null)).toBeTruthy();
expect(
testProperty.type.hasChanged(testMapValue, undefined),
).toBeTruthy();
});
});
describe("serializedHasChanged", () => {
test("serializedHasChanged", () => {
expect(
testProperty.type.serializedHasChanged(
{test: "1.52", another: "55"},
{test: "1.52", another: "55"},
),
).toBeFalsy();
expect(
testProperty.type.serializedHasChanged(
{test: "1.52", another: "55"},
{test: "1.521", another: "55"},
),
).toBeTruthy();
expect(
testProperty.type.serializedHasChanged(
{test: "1.52", another: "55"},
{another: "55"},
),
).toBeTruthy();
expect(testProperty.type.serializedHasChanged(null, null)).toBeFalsy();
expect(
testProperty.type.serializedHasChanged(undefined, undefined),
).toBeFalsy();
expect(
testProperty.type.serializedHasChanged(null, undefined),
).toBeTruthy();
expect(
testProperty.type.serializedHasChanged(undefined, null),
).toBeTruthy();
expect(
testProperty.type.serializedHasChanged(null, {
test: "1.52",
another: "55",
}),
).toBeTruthy();
expect(
testProperty.type.serializedHasChanged(undefined, {
test: "1.52",
another: "55",
}),
).toBeTruthy();
expect(
testProperty.type.serializedHasChanged(
{test: "1.52", another: "55"},
null,
),
).toBeTruthy();
expect(
testProperty.type.serializedHasChanged(
{test: "1.52", another: "55"},
undefined,
),
).toBeTruthy();
});
});
describe("resetDiff", () => {
test("resetDiff", () => {
testProperty.type.resetDiff(testMapValue);
testProperty.type.resetDiff(undefined);
testProperty.type.resetDiff(null);
});
});
describe("clone", () => {
test("clone", () => {
{
// Test that keys and values are cloned in a different map.
const clonedTestMapValue = testProperty.type.clone(testMapValue);
expect(clonedTestMapValue).not.toBe(testMapValue);
expect(clonedTestMapValue).toEqual(testMapValue);
}
{
// Test that values are cloned in a different object.
const propertyValue = new Map();
propertyValue.set("test", [12, 11]);
const clonedPropertyValue = s.property
.stringMap(s.property.array(s.property.numeric()))
.type.clone(propertyValue);
expect(clonedPropertyValue).not.toBe(propertyValue);
expect(clonedPropertyValue).toEqual(propertyValue);
expect(clonedPropertyValue.get("test")).not.toBe(
propertyValue.get("test"),
);
expect(clonedPropertyValue.get("test")).toEqual(
propertyValue.get("test"),
);
}
expect(testProperty.type.clone(undefined)).toBe(undefined);
expect(testProperty.type.clone(null)).toBe(null);
});
test("invalid parameters", () => {
expect(() => testProperty.type.clone(5 as any)).toThrowError(
InvalidTypeValueError,
);
expect(() => testProperty.type.clone([] as any)).toThrowError(
InvalidTypeValueError,
);
expect(() => testProperty.type.clone({} as any)).toThrowError(
InvalidTypeValueError,
);
});
});
test("applyPatch", () => {
{
// Apply a patch with undefined / NULL values.
expect(
testProperty.type.applyPatch(testMapValue, undefined, false),
).toBeUndefined();
expect(testProperty.type.applyPatch(testMapValue, null, true)).toBeNull();
}
{
// Invalid patch.
expect(() =>
testProperty.type.applyPatch(testMapValue, 5416 as any, false),
).toThrow(InvalidTypeValueError);
}
{
// Apply a patch.
{
const objectInstance = testProperty.type.applyPatch(
testMapValue,
{test: "1.521"},
true,
);
const expectedMapValue = new Map<string, number>();
expectedMapValue.set("test", 1.521);
expectedMapValue.set("another", 55);
expect(objectInstance).toStrictEqual(expectedMapValue);
}
{
const objectInstance = testProperty.type.applyPatch(
undefined,
{test: "1.52"},
false,
);
const expectedMapValue = new Map<string, number>();
expectedMapValue.set("test", 1.52);
expect(objectInstance).toStrictEqual(expectedMapValue);
}
{
const objectInstance = testProperty.type.applyPatch(
null,
{test: "1.52"},
false,
);
const expectedMapValue = new Map<string, number>();
expectedMapValue.set("test", 1.52);
expect(objectInstance).toStrictEqual(expectedMapValue);
}
}
});
});

View file

@ -1,8 +1,7 @@
import {describe, expect, test} from "vitest"; import {describe, expect, test} from "vitest";
import {InvalidTypeValueError, ModelType, s} from "../../../src/library"; import {InvalidTypeValueError, ModelType, s} from "../../../src/library";
class TestModel class TestModel {
{
id: number; id: number;
name: string; name: string;
price: number; price: number;
@ -19,114 +18,565 @@ describe("model type", () => {
identifier: "id", identifier: "id",
}); });
test("model type definition", () => { test("definition", () => {
const modelType = s.property.model(testModel); const modelType = s.property.model(testModel);
expect(modelType.type).toBeInstanceOf(ModelType); expect(modelType.type).toBeInstanceOf(ModelType);
}); });
test("model type functions", () => { describe("serialize", () => {
{ // Try to serialize / deserialize. test("serialize", () => {
const testModelInstance = testModel.model(Object.assign(new TestModel(), { id: 1, name: "test", price: 12.548777 })).instance; const testModelInstance = testModel.model(
expect(s.property.model(testModel).type.serialize(testModelInstance)).toEqual({ id: 1, name: "test", price: "12.548777" }); Object.assign(new TestModel(), {
expect(testModel.model( id: 1,
s.property.model(testModel).type.deserialize({ id: 1, name: "test", price: "12.548777" }) name: "test",
).getInstanceProperties()).toEqual(testModel.model(testModelInstance).getInstanceProperties()); price: 12.548777,
} }),
).instance;
{ // Try to serialize the difference. expect(
const testModelInstance = testModel.model(Object.assign(new TestModel(), { id: 1, name: "test", price: 12.548777 })).instance; s.property.model(testModel).type.serialize(testModelInstance),
testModelInstance.name = "new"; ).toEqual({id: 1, name: "test", price: "12.548777"});
expect(s.property.model(testModel).type.serializeDiff(testModelInstance)).toEqual({ id: 1, name: "new" });
}
expect(s.property.model(testModel).type.serialize(null)).toEqual(null); expect(s.property.model(testModel).type.serialize(null)).toEqual(null);
expect(s.property.model(testModel).type.serialize(undefined)).toEqual(
undefined,
);
});
test("invalid parameters", () => {
expect(() =>
s.property.model(testModel).type.serialize(5 as any),
).toThrowError(InvalidTypeValueError);
expect(() =>
s.property.model(testModel).type.serialize([] as any),
).toThrowError(InvalidTypeValueError);
expect(() =>
s.property.model(testModel).type.serialize(new (class {})() as any),
).toThrowError(InvalidTypeValueError);
});
});
describe("deserialize", () => {
test("deserialize", () => {
const testModelInstance = testModel.model(
Object.assign(new TestModel(), {
id: 1,
name: "test",
price: 12.548777,
}),
).instance;
expect(
testModel
.model(
s.property
.model(testModel)
.type.deserialize({id: 1, name: "test", price: "12.548777"}),
)
.getInstanceProperties(),
).toEqual(testModel.model(testModelInstance).getInstanceProperties());
expect(s.property.model(testModel).type.deserialize(null)).toEqual(null); expect(s.property.model(testModel).type.deserialize(null)).toEqual(null);
expect(s.property.model(testModel).type.serializeDiff(null)).toEqual(null); expect(s.property.model(testModel).type.deserialize(undefined)).toEqual(
undefined,
);
});
expect(s.property.model(testModel).type.serialize(undefined)).toEqual(undefined); test("invalid parameters", () => {
expect(s.property.model(testModel).type.deserialize(undefined)).toEqual(undefined); expect(() =>
expect(s.property.model(testModel).type.serializeDiff(undefined)).toEqual(undefined); s.property.model(testModel).type.deserialize(5 as any),
).toThrowError(InvalidTypeValueError);
expect(() =>
s.property.model(testModel).type.deserialize([] as any),
).toThrowError(InvalidTypeValueError);
});
});
describe("serializeDiff", () => {
test("serializeDiff", () => {
// Try to serialize the difference.
const testModelInstance = testModel.model(
Object.assign(new TestModel(), {
id: 1,
name: "test",
price: 12.548777,
}),
).instance;
testModelInstance.name = "new";
expect(
s.property.model(testModel).type.serializeDiff(testModelInstance),
).toEqual({id: 1, name: "new"});
expect(s.property.model(testModel).type.serializeDiff(null)).toEqual(
null,
);
expect(s.property.model(testModel).type.serializeDiff(undefined)).toEqual(
undefined,
);
});
test("invalid parameters", () => {
expect(() =>
s.property.model(testModel).type.serializeDiff(5 as any),
).toThrowError(InvalidTypeValueError);
expect(() =>
s.property.model(testModel).type.serializeDiff([] as any),
).toThrowError(InvalidTypeValueError);
expect(() =>
s.property.model(testModel).type.serializeDiff(new (class {})() as any),
).toThrowError(InvalidTypeValueError);
});
});
describe("hasChanged", () => {
test("hasChanged", () => {
{ {
const testModelInstance = testModel.model(Object.assign(new TestModel(), { id: 1, name: "test", price: 12.548777 })).instance; const testModelInstance = testModel.model(
expect(s.property.model(testModel).type.hasChanged(testModelInstance, testModelInstance)).toBeFalsy(); Object.assign(new TestModel(), {
id: 1,
name: "test",
price: 12.548777,
}),
).instance;
expect(
s.property
.model(testModel)
.type.hasChanged(testModelInstance, testModelInstance),
).toBeFalsy();
} }
{ {
const testModelInstance = testModel.model(Object.assign(new TestModel(), { id: 1, name: "test", price: 12.548777 })).instance; const testModelInstance = testModel.model(
Object.assign(new TestModel(), {
id: 1,
name: "test",
price: 12.548777,
}),
).instance;
testModelInstance.price = 12.548778; testModelInstance.price = 12.548778;
expect(s.property.model(testModel).type.hasChanged(testModelInstance, testModelInstance)).toBeTruthy(); expect(
s.property
.model(testModel)
.type.hasChanged(testModelInstance, testModelInstance),
).toBeTruthy();
} }
expect(s.property.model(testModel).type.hasChanged(null, null)).toBeFalsy(); expect(
expect(s.property.model(testModel).type.hasChanged(undefined, undefined)).toBeFalsy(); s.property.model(testModel).type.hasChanged(null, null),
expect(s.property.model(testModel).type.hasChanged(null, undefined)).toBeTruthy(); ).toBeFalsy();
expect(s.property.model(testModel).type.hasChanged(undefined, null)).toBeTruthy(); expect(
expect(s.property.model(testModel).type.hasChanged(null, testModel.model(Object.assign(new TestModel(), { id: 1, name: "test", price: 12.548777 })).instance)).toBeTruthy(); s.property.model(testModel).type.hasChanged(undefined, undefined),
expect(s.property.model(testModel).type.hasChanged(undefined, testModel.model(Object.assign(new TestModel(), { id: 1, name: "test", price: 12.548777 })).instance)).toBeTruthy(); ).toBeFalsy();
expect(s.property.model(testModel).type.hasChanged(testModel.model(Object.assign(new TestModel(), { id: 1, name: "test", price: 12.548777 })).instance, null)).toBeTruthy(); expect(
expect(s.property.model(testModel).type.hasChanged(testModel.model(Object.assign(new TestModel(), { id: 1, name: "test", price: 12.548777 })).instance, undefined)).toBeTruthy(); s.property.model(testModel).type.hasChanged(null, undefined),
).toBeTruthy();
expect(
s.property.model(testModel).type.hasChanged(undefined, null),
).toBeTruthy();
expect(
s.property.model(testModel).type.hasChanged(
null,
testModel.model(
Object.assign(new TestModel(), {
id: 1,
name: "test",
price: 12.548777,
}),
).instance,
),
).toBeTruthy();
expect(
s.property.model(testModel).type.hasChanged(
undefined,
testModel.model(
Object.assign(new TestModel(), {
id: 1,
name: "test",
price: 12.548777,
}),
).instance,
),
).toBeTruthy();
expect(
s.property.model(testModel).type.hasChanged(
testModel.model(
Object.assign(new TestModel(), {
id: 1,
name: "test",
price: 12.548777,
}),
).instance,
null,
),
).toBeTruthy();
expect(
s.property.model(testModel).type.hasChanged(
testModel.model(
Object.assign(new TestModel(), {
id: 1,
name: "test",
price: 12.548777,
}),
).instance,
undefined,
),
).toBeTruthy();
});
expect(s.property.model(testModel).type.serializedHasChanged( test("invalid parameters", () => {
{ id: 1, name: "test", price: "12.548777" }, expect(() =>
{ id: 1, price: "12.548777", name: "test" }, s.property.model(testModel).type.hasChanged(5 as any, 5 as any),
)).toBeFalsy(); ).toThrowError(InvalidTypeValueError);
expect(s.property.model(testModel).type.serializedHasChanged( expect(() =>
{ id: 1, name: "test", price: "12.548777" }, s.property
{ id: 1, name: "test", price: "12.548778" }, .model(testModel)
)).toBeTruthy(); .type.hasChanged(
expect(s.property.model(testModel).type.serializedHasChanged(null, null)).toBeFalsy(); testModel.model(new TestModel()).instance,
expect(s.property.model(testModel).type.serializedHasChanged(undefined, undefined)).toBeFalsy(); [] as any,
expect(s.property.model(testModel).type.serializedHasChanged(null, undefined)).toBeTruthy(); ),
expect(s.property.model(testModel).type.serializedHasChanged(undefined, null)).toBeTruthy(); ).toThrowError(InvalidTypeValueError);
expect(s.property.model(testModel).type.serializedHasChanged(null, { id: 1, name: "test", price: "12.548777" })).toBeTruthy(); expect(() =>
expect(s.property.model(testModel).type.serializedHasChanged(undefined, { id: 1, name: "test", price: "12.548777" })).toBeTruthy(); s.property
expect(s.property.model(testModel).type.serializedHasChanged({ id: 1, name: "test", price: "12.548777" }, null)).toBeTruthy(); .model(testModel)
expect(s.property.model(testModel).type.serializedHasChanged({ id: 1, name: "test", price: "12.548777" }, undefined)).toBeTruthy(); .type.hasChanged(
testModel.model(new TestModel()).instance,
new (class {})() as any,
),
).toThrowError(InvalidTypeValueError);
});
});
describe("serializedHasChanged", () => {
test("serializedHasChanged", () => {
expect(
s.property
.model(testModel)
.type.serializedHasChanged(
{id: 1, name: "test", price: "12.548777"},
{id: 1, price: "12.548777", name: "test"},
),
).toBeFalsy();
expect(
s.property
.model(testModel)
.type.serializedHasChanged(
{id: 1, name: "test", price: "12.548777"},
{id: 1, name: "test", price: "12.548778"},
),
).toBeTruthy();
expect(
s.property.model(testModel).type.serializedHasChanged(null, null),
).toBeFalsy();
expect(
s.property
.model(testModel)
.type.serializedHasChanged(undefined, undefined),
).toBeFalsy();
expect(
s.property.model(testModel).type.serializedHasChanged(null, undefined),
).toBeTruthy();
expect(
s.property.model(testModel).type.serializedHasChanged(undefined, null),
).toBeTruthy();
expect(
s.property.model(testModel).type.serializedHasChanged(null, {
id: 1,
name: "test",
price: "12.548777",
}),
).toBeTruthy();
expect(
s.property.model(testModel).type.serializedHasChanged(undefined, {
id: 1,
name: "test",
price: "12.548777",
}),
).toBeTruthy();
expect(
s.property
.model(testModel)
.type.serializedHasChanged(
{id: 1, name: "test", price: "12.548777"},
null,
),
).toBeTruthy();
expect(
s.property
.model(testModel)
.type.serializedHasChanged(
{id: 1, name: "test", price: "12.548777"},
undefined,
),
).toBeTruthy();
});
test("invalid parameters", () => {
expect(() =>
s.property
.model(testModel)
.type.serializedHasChanged(5 as any, 5 as any),
).toThrowError(InvalidTypeValueError);
expect(() =>
s.property
.model(testModel)
.type.serializedHasChanged({} as any, [] as any),
).toThrowError(InvalidTypeValueError);
expect(
s.property
.model(testModel)
.type.serializedHasChanged({} as any, new (class {})() as any),
).toBeFalsy();
});
});
describe("resetDiff", () => {
test("resetDiff", () => {
const testModelInstance = testModel.model(
Object.assign(new TestModel(), {
id: 1,
name: "test",
price: 12.548777,
}),
).instance;
{ // Serializing the difference to check that the difference has been reset.
const testModelInstance = testModel.model(Object.assign(new TestModel(), { id: 1, name: "test", price: 12.548777 })).instance;
testModelInstance.price = 555.555; testModelInstance.price = 555.555;
expect(testModel.model(testModelInstance).serializeDiff()).toEqual({ id: 1, price: "555.555" }); expect(testModel.model(testModelInstance).serializeDiff()).toEqual({
id: 1,
price: "555.555",
});
s.property.model(testModel).type.resetDiff(testModelInstance); s.property.model(testModel).type.resetDiff(testModelInstance);
expect(testModel.model(testModelInstance).serializeDiff()).toEqual({ id: 1 }); expect(testModel.model(testModelInstance).serializeDiff()).toEqual({
} id: 1,
});
s.property.model(testModel).type.resetDiff(undefined); s.property.model(testModel).type.resetDiff(undefined);
s.property.model(testModel).type.resetDiff(null); s.property.model(testModel).type.resetDiff(null);
});
{ // Test that values are cloned in a different model instance. test("invalid parameters", () => {
const testModelInstance = testModel.model(Object.assign(new TestModel(), { id: 1, name: "test", price: 12.548777 })).instance; expect(() =>
s.property.model(testModel).type.resetDiff(5 as any),
).toThrowError(InvalidTypeValueError);
expect(() =>
s.property.model(testModel).type.resetDiff([] as any),
).toThrowError(InvalidTypeValueError);
expect(() =>
s.property.model(testModel).type.resetDiff(new (class {})() as any),
).toThrowError(InvalidTypeValueError);
});
});
describe("clone", () => {
test("clone", () => {
// Test that values are cloned in a different model instance.
const testModelInstance = testModel.model(
Object.assign(new TestModel(), {
id: 1,
name: "test",
price: 12.548777,
}),
).instance;
testModelInstance.price = 555.555; testModelInstance.price = 555.555;
const clonedModelInstance = s.property.model(testModel).type.clone(testModelInstance); const clonedModelInstance = s.property
.model(testModel)
.type.clone(testModelInstance);
expect(clonedModelInstance).not.toBe(testModelInstance); expect(clonedModelInstance).not.toBe(testModelInstance);
expect(testModel.model(clonedModelInstance).getInstanceProperties()).toEqual(testModel.model(testModelInstance).getInstanceProperties()); expect(
expect(testModel.model(clonedModelInstance).serializeDiff()).toEqual(testModel.model(testModelInstance).serializeDiff()); testModel.model(clonedModelInstance).getInstanceProperties(),
} ).toEqual(testModel.model(testModelInstance).getInstanceProperties());
expect(testModel.model(clonedModelInstance).serializeDiff()).toEqual(
testModel.model(testModelInstance).serializeDiff(),
);
expect(s.property.model(testModel).type.clone(undefined)).toBe(undefined); expect(s.property.model(testModel).type.clone(undefined)).toBe(undefined);
expect(s.property.model(testModel).type.clone(null)).toBe(null); expect(s.property.model(testModel).type.clone(null)).toBe(null);
}); });
test("invalid parameters types", () => { test("invalid parameters", () => {
expect(() => s.property.model(testModel).type.serialize(5 as any)).toThrowError(InvalidTypeValueError); expect(() =>
expect(() => s.property.model(testModel).type.deserialize(5 as any)).toThrowError(InvalidTypeValueError); s.property.model(testModel).type.clone(5 as any),
expect(() => s.property.model(testModel).type.serializeDiff(5 as any)).toThrowError(InvalidTypeValueError); ).toThrowError(InvalidTypeValueError);
expect(() => s.property.model(testModel).type.resetDiff(5 as any)).toThrowError(InvalidTypeValueError); expect(() =>
expect(() => s.property.model(testModel).type.hasChanged(5 as any, 5 as any)).toThrowError(InvalidTypeValueError); s.property.model(testModel).type.clone([] as any),
expect(() => s.property.model(testModel).type.serializedHasChanged(5 as any, 5 as any)).toThrowError(InvalidTypeValueError); ).toThrowError(InvalidTypeValueError);
expect(() => s.property.model(testModel).type.clone(5 as any)).toThrowError(InvalidTypeValueError); expect(() =>
s.property.model(testModel).type.clone(new (class {})() as any),
).toThrowError(InvalidTypeValueError);
});
});
expect(() => s.property.model(testModel).type.serialize([] as any)).toThrowError(InvalidTypeValueError); test("applyPatch", () => {
expect(() => s.property.model(testModel).type.deserialize([] as any)).toThrowError(InvalidTypeValueError); {
expect(() => s.property.model(testModel).type.serializeDiff([] as any)).toThrowError(InvalidTypeValueError); // Apply a patch with undefined / NULL values.
expect(() => s.property.model(testModel).type.resetDiff([] as any)).toThrowError(InvalidTypeValueError); expect(
expect(() => s.property.model(testModel).type.hasChanged(testModel.model(new TestModel()).instance, [] as any)).toThrowError(InvalidTypeValueError); s.property.model(testModel).type.applyPatch(
expect(() => s.property.model(testModel).type.serializedHasChanged({} as any, [] as any)).toThrowError(InvalidTypeValueError); testModel.model(
expect(() => s.property.model(testModel).type.clone([] as any)).toThrowError(InvalidTypeValueError); Object.assign(new TestModel(), {
id: 1,
name: "test",
price: 12.548777,
}),
).instance,
undefined,
false,
),
).toBeUndefined();
expect(
s.property.model(testModel).type.applyPatch(
testModel.model(
Object.assign(new TestModel(), {
id: 1,
name: "test",
price: 12.548777,
}),
).instance,
null,
true,
),
).toBeNull();
}
expect(() => s.property.model(testModel).type.serialize(new class{} as any)).toThrowError(InvalidTypeValueError); {
expect(() => s.property.model(testModel).type.serializeDiff(new class{} as any)).toThrowError(InvalidTypeValueError); // Invalid patch.
expect(() => s.property.model(testModel).type.resetDiff(new class{} as any)).toThrowError(InvalidTypeValueError); expect(() =>
expect(() => s.property.model(testModel).type.hasChanged(testModel.model(new TestModel()).instance, new class{} as any)).toThrowError(InvalidTypeValueError); s.property.model(testModel).type.applyPatch(
expect(s.property.model(testModel).type.serializedHasChanged({} as any, new class{} as any)).toBeFalsy(); testModel.model(
expect(() => s.property.model(testModel).type.clone(new class{} as any)).toThrowError(InvalidTypeValueError); Object.assign(new TestModel(), {
id: 1,
name: "test",
price: 12.548777,
}),
).instance,
5416 as any,
false,
),
).toThrow(InvalidTypeValueError);
}
{
// Apply a patch with originals update.
{
const modelInstance = s.property.model(testModel).type.applyPatch(
testModel.model(
Object.assign(new TestModel(), {
id: 1,
name: "test",
price: 12.548777,
}),
).instance,
{id: 1, name: "another"},
true,
);
expect(
testModel.model(modelInstance).getInstanceProperties(),
).toStrictEqual({
id: 1,
name: "another",
price: 12.548777,
});
expect(testModel.model(modelInstance).serializeDiff()).toStrictEqual({
id: 1,
});
}
{
const modelInstance = s.property
.model(testModel)
.type.applyPatch(undefined, {id: 1, name: "test"}, true);
expect(
testModel.model(modelInstance).getInstanceProperties(),
).toStrictEqual({
id: 1,
name: "test",
price: undefined,
});
expect(testModel.model(modelInstance).serializeDiff()).toStrictEqual({
id: 1,
});
}
{
const modelInstance = s.property
.model(testModel)
.type.applyPatch(null, {id: 1, name: "test"}, true);
expect(
testModel.model(modelInstance).getInstanceProperties(),
).toStrictEqual({
id: 1,
name: "test",
price: undefined,
});
expect(testModel.model(modelInstance).serializeDiff()).toStrictEqual({
id: 1,
});
}
}
{
// Apply a patch without originals update.
{
const modelInstance = s.property.model(testModel).type.applyPatch(
testModel.model(
Object.assign(new TestModel(), {
id: 1,
name: "test",
price: 12.548777,
}),
).instance,
{id: 1, name: "another"},
false,
);
expect(
testModel.model(modelInstance).getInstanceProperties(),
).toStrictEqual({
id: 1,
name: "another",
price: 12.548777,
});
expect(testModel.model(modelInstance).serializeDiff()).toStrictEqual({
id: 1,
name: "another",
});
}
{
const modelInstance = s.property
.model(testModel)
.type.applyPatch(undefined, {id: 1, name: "test"}, false);
expect(
testModel.model(modelInstance).getInstanceProperties(),
).toStrictEqual({
id: 1,
name: "test",
price: undefined,
});
expect(testModel.model(modelInstance).serializeDiff()).toStrictEqual({
id: 1,
name: "test",
});
}
{
const modelInstance = s.property
.model(testModel)
.type.applyPatch(null, {id: 1, name: "test"}, false);
expect(
testModel.model(modelInstance).getInstanceProperties(),
).toStrictEqual({
id: 1,
name: "test",
price: undefined,
});
expect(testModel.model(modelInstance).serializeDiff()).toStrictEqual({
id: 1,
name: "test",
});
}
}
}); });
}); });

View file

@ -2,58 +2,163 @@ import {describe, expect, test} from "vitest";
import {InvalidTypeValueError, NumericType, s} from "../../../src/library"; import {InvalidTypeValueError, NumericType, s} from "../../../src/library";
describe("numeric type", () => { describe("numeric type", () => {
test("numeric type definition", () => { test("definition", () => {
const numericType = s.property.numeric(); const numericType = s.property.numeric();
expect(numericType.type).toBeInstanceOf(NumericType); expect(numericType.type).toBeInstanceOf(NumericType);
}); });
test("numeric type functions", () => { describe("serialize", () => {
test("serialize", () => {
expect(s.property.numeric().type.serialize(5.257)).toBe(5.257); expect(s.property.numeric().type.serialize(5.257)).toBe(5.257);
expect(s.property.numeric().type.deserialize(5.257)).toBe(5.257);
expect(s.property.numeric().type.serializeDiff(542)).toBe(542);
expect(s.property.numeric().type.serialize(null)).toBe(null); expect(s.property.numeric().type.serialize(null)).toBe(null);
expect(s.property.numeric().type.deserialize(null)).toBe(null);
expect(s.property.numeric().type.serializeDiff(null)).toBe(null);
expect(s.property.numeric().type.serialize(undefined)).toBe(undefined); expect(s.property.numeric().type.serialize(undefined)).toBe(undefined);
expect(s.property.numeric().type.deserialize(undefined)).toBe(undefined); });
expect(s.property.numeric().type.serializeDiff(undefined)).toBe(undefined);
test("invalid parameters", () => {
expect(() => s.property.numeric().type.serialize({} as any)).toThrowError(
InvalidTypeValueError,
);
});
});
describe("deserialize", () => {
test("deserialize", () => {
expect(s.property.numeric().type.deserialize(5.257)).toBe(5.257);
expect(s.property.numeric().type.deserialize(null)).toBe(null);
expect(s.property.numeric().type.deserialize(undefined)).toBe(undefined);
});
test("invalid parameters", () => {
expect(() =>
s.property.numeric().type.deserialize({} as any),
).toThrowError(InvalidTypeValueError);
});
});
describe("serializeDiff", () => {
test("serializeDiff", () => {
expect(s.property.numeric().type.serializeDiff(542)).toBe(542);
expect(s.property.numeric().type.serializeDiff(null)).toBe(null);
expect(s.property.numeric().type.serializeDiff(undefined)).toBe(
undefined,
);
});
test("invalid parameters", () => {
expect(() =>
s.property.numeric().type.serializeDiff({} as any),
).toThrowError(InvalidTypeValueError);
});
});
describe("hasChanged", () => {
test("hasChanged", () => {
expect(s.property.numeric().type.hasChanged(5.257, 5.257)).toBeFalsy(); expect(s.property.numeric().type.hasChanged(5.257, 5.257)).toBeFalsy();
expect(s.property.numeric().type.hasChanged(null, null)).toBeFalsy(); expect(s.property.numeric().type.hasChanged(null, null)).toBeFalsy();
expect(s.property.numeric().type.hasChanged(undefined, undefined)).toBeFalsy(); expect(
expect(s.property.numeric().type.hasChanged(null, undefined)).toBeTruthy(); s.property.numeric().type.hasChanged(undefined, undefined),
expect(s.property.numeric().type.hasChanged(undefined, null)).toBeTruthy(); ).toBeFalsy();
expect(
s.property.numeric().type.hasChanged(null, undefined),
).toBeTruthy();
expect(
s.property.numeric().type.hasChanged(undefined, null),
).toBeTruthy();
expect(s.property.numeric().type.hasChanged(null, 5.257)).toBeTruthy(); expect(s.property.numeric().type.hasChanged(null, 5.257)).toBeTruthy();
expect(s.property.numeric().type.hasChanged(undefined, 5.257)).toBeTruthy(); expect(
s.property.numeric().type.hasChanged(undefined, 5.257),
).toBeTruthy();
expect(s.property.numeric().type.hasChanged(5.257, null)).toBeTruthy(); expect(s.property.numeric().type.hasChanged(5.257, null)).toBeTruthy();
expect(s.property.numeric().type.hasChanged(5.257, undefined)).toBeTruthy(); expect(
s.property.numeric().type.hasChanged(5.257, undefined),
).toBeTruthy();
});
expect(s.property.numeric().type.serializedHasChanged(5.257, 5.257)).toBeFalsy(); test("invalid parameters", () => {
expect(s.property.numeric().type.serializedHasChanged(null, null)).toBeFalsy(); expect(
expect(s.property.numeric().type.serializedHasChanged(undefined, undefined)).toBeFalsy(); s.property.numeric().type.hasChanged({} as any, {} as any),
expect(s.property.numeric().type.serializedHasChanged(null, undefined)).toBeTruthy(); ).toBeTruthy();
expect(s.property.numeric().type.serializedHasChanged(undefined, null)).toBeTruthy(); expect(
expect(s.property.numeric().type.serializedHasChanged(null, 5.257)).toBeTruthy(); s.property.numeric().type.hasChanged(false as any, false as any),
expect(s.property.numeric().type.serializedHasChanged(undefined, 5.257)).toBeTruthy(); ).toBeFalsy();
expect(s.property.numeric().type.serializedHasChanged(5.257, null)).toBeTruthy(); });
expect(s.property.numeric().type.serializedHasChanged(5.257, undefined)).toBeTruthy(); });
describe("serializedHasChanged", () => {
test("serializedHasChanged", () => {
expect(
s.property.numeric().type.serializedHasChanged(5.257, 5.257),
).toBeFalsy();
expect(
s.property.numeric().type.serializedHasChanged(null, null),
).toBeFalsy();
expect(
s.property.numeric().type.serializedHasChanged(undefined, undefined),
).toBeFalsy();
expect(
s.property.numeric().type.serializedHasChanged(null, undefined),
).toBeTruthy();
expect(
s.property.numeric().type.serializedHasChanged(undefined, null),
).toBeTruthy();
expect(
s.property.numeric().type.serializedHasChanged(null, 5.257),
).toBeTruthy();
expect(
s.property.numeric().type.serializedHasChanged(undefined, 5.257),
).toBeTruthy();
expect(
s.property.numeric().type.serializedHasChanged(5.257, null),
).toBeTruthy();
expect(
s.property.numeric().type.serializedHasChanged(5.257, undefined),
).toBeTruthy();
});
test("invalid parameters", () => {
expect(
s.property.numeric().type.serializedHasChanged({} as any, {} as any),
).toBeTruthy();
expect(
s.property
.numeric()
.type.serializedHasChanged(false as any, false as any),
).toBeFalsy();
});
});
describe("resetDiff", () => {
test("resetDiff", () => {
s.property.numeric().type.resetDiff(5.257); s.property.numeric().type.resetDiff(5.257);
s.property.numeric().type.resetDiff(undefined); s.property.numeric().type.resetDiff(undefined);
s.property.numeric().type.resetDiff(null); s.property.numeric().type.resetDiff(null);
}); });
test("invalid parameters types", () => { test("invalid parameters", () => {
expect(() => s.property.numeric().type.serialize({} as any)).toThrowError(InvalidTypeValueError); expect(() =>
expect(() => s.property.numeric().type.deserialize({} as any)).toThrowError(InvalidTypeValueError) s.property.numeric().type.resetDiff({} as any),
expect(() => s.property.numeric().type.serializeDiff({} as any)).toThrowError(InvalidTypeValueError); ).not.toThrow();
expect(() => s.property.numeric().type.resetDiff({} as any)).not.toThrow(); });
expect(s.property.numeric().type.hasChanged({} as any, {} as any)).toBeTruthy(); });
expect(s.property.numeric().type.hasChanged(false as any, false as any)).toBeFalsy();
expect(s.property.numeric().type.serializedHasChanged({} as any, {} as any)).toBeTruthy(); describe("clone", () => {
expect(s.property.numeric().type.serializedHasChanged(false as any, false as any)).toBeFalsy(); test("invalid parameters", () => {
expect(s.property.numeric().type.clone({} as any)).toStrictEqual({}); expect(s.property.numeric().type.clone({} as any)).toStrictEqual({});
}); });
});
test("applyPatch", () => {
expect(s.property.numeric().type.applyPatch(1, 5.257, false)).toBe(5.257);
expect(s.property.numeric().type.applyPatch(null, 5.257, true)).toBe(5.257);
expect(s.property.numeric().type.applyPatch(undefined, 5.257, false)).toBe(
5.257,
);
expect(
s.property.numeric().type.applyPatch(5.257, undefined, false),
).toBeUndefined();
expect(s.property.numeric().type.applyPatch(5.257, null, false)).toBeNull();
});
}); });

View file

@ -1,8 +1,14 @@
import {describe, expect, test} from "vitest"; import {describe, expect, test} from "vitest";
import {InvalidTypeValueError, NumericType, ObjectType, s, StringType} from "../../../src/library"; import {
InvalidTypeValueError,
NumericType,
ObjectType,
s,
StringType,
} from "../../../src/library";
describe("object type", () => { describe("object type", () => {
test("object type definition", () => { test("definition", () => {
const objectType = s.property.object({ const objectType = s.property.object({
test: s.property.string(), test: s.property.string(),
another: s.property.numeric(), another: s.property.numeric(),
@ -10,10 +16,12 @@ describe("object type", () => {
expect(objectType.type).toBeInstanceOf(ObjectType); expect(objectType.type).toBeInstanceOf(ObjectType);
expect((objectType.type as any).properties).toHaveLength(2); expect((objectType.type as any).properties).toHaveLength(2);
for (const property of (objectType.type as any).properties) for (const property of (objectType.type as any).properties) {
{ // Check all object properties. // Check all object properties.
if (property.name == "test") expect(property.definition.type).toBeInstanceOf(StringType); if (property.name == "test")
else if (property.name == "another") expect(property.definition.type).toBeInstanceOf(NumericType); expect(property.definition.type).toBeInstanceOf(StringType);
else if (property.name == "another")
expect(property.definition.type).toBeInstanceOf(NumericType);
else expect.unreachable(); else expect.unreachable();
} }
}); });
@ -23,54 +31,206 @@ describe("object type", () => {
another: s.property.decimal(), another: s.property.decimal(),
}); });
test("object type functions", () => { describe("serialize", () => {
expect(testProperty.type.serialize({ test: "test", another: 12.548777 })).toEqual({ test: "test", another: "12.548777" }); test("serialize", () => {
expect(testProperty.type.deserialize({ test: "test", another: "12.548777" })).toEqual({ test: "test", another: 12.548777 }); expect(
expect(testProperty.type.serializeDiff({ test: "test", another: 12.548777 })).toEqual({ test: "test", another: "12.548777" }); testProperty.type.serialize({test: "test", another: 12.548777}),
).toEqual({test: "test", another: "12.548777"});
expect(testProperty.type.serialize(null)).toEqual(null); expect(testProperty.type.serialize(null)).toEqual(null);
expect(testProperty.type.deserialize(null)).toEqual(null);
expect(testProperty.type.serializeDiff(null)).toEqual(null);
expect(testProperty.type.serialize(undefined)).toEqual(undefined); expect(testProperty.type.serialize(undefined)).toEqual(undefined);
expect(testProperty.type.deserialize(undefined)).toEqual(undefined); });
expect(testProperty.type.serializeDiff(undefined)).toEqual(undefined);
expect(testProperty.type.hasChanged({ test: "test", another: 12.548777 }, { another: 12.548777, test: "test" })).toBeFalsy(); test("invalid parameters", () => {
expect(testProperty.type.hasChanged({ test: "test", another: 12.548777 }, { test: "test", another: 12.548778 })).toBeTruthy(); expect(() => testProperty.type.serialize(5 as any)).toThrowError(
InvalidTypeValueError,
);
expect(() => testProperty.type.serialize([] as any)).toThrowError(
InvalidTypeValueError,
);
});
});
describe("deserialize", () => {
test("deserialize", () => {
expect(
testProperty.type.deserialize({test: "test", another: "12.548777"}),
).toEqual({test: "test", another: 12.548777});
expect(testProperty.type.deserialize(null)).toEqual(null);
expect(testProperty.type.deserialize(undefined)).toEqual(undefined);
});
test("invalid parameters", () => {
expect(() => testProperty.type.deserialize(5 as any)).toThrowError(
InvalidTypeValueError,
);
expect(() => testProperty.type.deserialize([] as any)).toThrowError(
InvalidTypeValueError,
);
});
});
describe("serializeDiff", () => {
test("serializeDiff", () => {
expect(
testProperty.type.serializeDiff({test: "test", another: 12.548777}),
).toEqual({test: "test", another: "12.548777"});
expect(testProperty.type.serializeDiff(null)).toEqual(null);
expect(testProperty.type.serializeDiff(undefined)).toEqual(undefined);
});
test("invalid parameters", () => {
expect(() => testProperty.type.serializeDiff(5 as any)).toThrowError(
InvalidTypeValueError,
);
expect(() => testProperty.type.serializeDiff([] as any)).toThrowError(
InvalidTypeValueError,
);
});
});
describe("hasChanged", () => {
test("hasChanged", () => {
expect(
testProperty.type.hasChanged(
{test: "test", another: 12.548777},
{another: 12.548777, test: "test"},
),
).toBeFalsy();
expect(
testProperty.type.hasChanged(
{test: "test", another: 12.548777},
{test: "test", another: 12.548778},
),
).toBeTruthy();
expect(testProperty.type.hasChanged(null, null)).toBeFalsy(); expect(testProperty.type.hasChanged(null, null)).toBeFalsy();
expect(testProperty.type.hasChanged(undefined, undefined)).toBeFalsy(); expect(testProperty.type.hasChanged(undefined, undefined)).toBeFalsy();
expect(testProperty.type.hasChanged(null, undefined)).toBeTruthy(); expect(testProperty.type.hasChanged(null, undefined)).toBeTruthy();
expect(testProperty.type.hasChanged(undefined, null)).toBeTruthy(); expect(testProperty.type.hasChanged(undefined, null)).toBeTruthy();
expect(testProperty.type.hasChanged(null, { test: "test", another: 12.548777 })).toBeTruthy(); expect(
expect(testProperty.type.hasChanged(undefined, { test: "test", another: 12.548777 })).toBeTruthy(); testProperty.type.hasChanged(null, {test: "test", another: 12.548777}),
expect(testProperty.type.hasChanged({ test: "test", another: 12.548777 }, null)).toBeTruthy(); ).toBeTruthy();
expect(testProperty.type.hasChanged({ test: "test", another: 12.548777 }, undefined)).toBeTruthy(); expect(
testProperty.type.hasChanged(undefined, {
test: "test",
another: 12.548777,
}),
).toBeTruthy();
expect(
testProperty.type.hasChanged({test: "test", another: 12.548777}, null),
).toBeTruthy();
expect(
testProperty.type.hasChanged(
{test: "test", another: 12.548777},
undefined,
),
).toBeTruthy();
});
expect(testProperty.type.serializedHasChanged({ test: "test", another: "12.548777" }, { another: "12.548777", test: "test" })).toBeFalsy(); test("invalid parameters", () => {
expect(testProperty.type.serializedHasChanged({ test: "test", another: "12.548777" }, { test: "test", another: "12.548778" })).toBeTruthy(); expect(() =>
testProperty.type.hasChanged(5 as any, 5 as any),
).toThrowError(InvalidTypeValueError);
expect(() =>
testProperty.type.hasChanged({} as any, [] as any),
).toThrowError(InvalidTypeValueError);
});
});
describe("serializedHasChanged", () => {
test("serializedHasChanged", () => {
expect(
testProperty.type.serializedHasChanged(
{test: "test", another: "12.548777"},
{another: "12.548777", test: "test"},
),
).toBeFalsy();
expect(
testProperty.type.serializedHasChanged(
{test: "test", another: "12.548777"},
{test: "test", another: "12.548778"},
),
).toBeTruthy();
expect(testProperty.type.serializedHasChanged(null, null)).toBeFalsy(); expect(testProperty.type.serializedHasChanged(null, null)).toBeFalsy();
expect(testProperty.type.serializedHasChanged(undefined, undefined)).toBeFalsy(); expect(
expect(testProperty.type.serializedHasChanged(null, undefined)).toBeTruthy(); testProperty.type.serializedHasChanged(undefined, undefined),
expect(testProperty.type.serializedHasChanged(undefined, null)).toBeTruthy(); ).toBeFalsy();
expect(testProperty.type.serializedHasChanged(null, { test: "test", another: "12.548777" })).toBeTruthy(); expect(
expect(testProperty.type.serializedHasChanged(undefined, { test: "test", another: "12.548777" })).toBeTruthy(); testProperty.type.serializedHasChanged(null, undefined),
expect(testProperty.type.serializedHasChanged({ test: "test", another: "12.548777" }, null)).toBeTruthy(); ).toBeTruthy();
expect(testProperty.type.serializedHasChanged({ test: "test", another: "12.548777" }, undefined)).toBeTruthy(); expect(
testProperty.type.serializedHasChanged(undefined, null),
).toBeTruthy();
expect(
testProperty.type.serializedHasChanged(null, {
test: "test",
another: "12.548777",
}),
).toBeTruthy();
expect(
testProperty.type.serializedHasChanged(undefined, {
test: "test",
another: "12.548777",
}),
).toBeTruthy();
expect(
testProperty.type.serializedHasChanged(
{test: "test", another: "12.548777"},
null,
),
).toBeTruthy();
expect(
testProperty.type.serializedHasChanged(
{test: "test", another: "12.548777"},
undefined,
),
).toBeTruthy();
});
testProperty.type.resetDiff({ test: "test", another: 12.548777 }); test("invalid parameters", () => {
expect(() =>
testProperty.type.serializedHasChanged(5 as any, 5 as any),
).toThrowError(InvalidTypeValueError);
expect(() =>
testProperty.type.serializedHasChanged({} as any, [] as any),
).toThrowError(InvalidTypeValueError);
});
});
describe("resetDiff", () => {
test("resetDiff", () => {
testProperty.type.resetDiff({test: "test", another: 12.548777});
testProperty.type.resetDiff(undefined); testProperty.type.resetDiff(undefined);
testProperty.type.resetDiff(null); testProperty.type.resetDiff(null);
});
{ // Test that values are cloned in a different object. test("invalid parameters", () => {
const propertyValue = { test: "test", another: 12.548777 }; expect(() => testProperty.type.resetDiff(5 as any)).toThrowError(
InvalidTypeValueError,
);
expect(() => testProperty.type.resetDiff([] as any)).toThrowError(
InvalidTypeValueError,
);
});
});
describe("clone", () => {
test("clone", () => {
{
// Test that values are cloned in a different object.
const propertyValue = {test: "test", another: 12.548777};
const clonedPropertyValue = testProperty.type.clone(propertyValue); const clonedPropertyValue = testProperty.type.clone(propertyValue);
expect(clonedPropertyValue).not.toBe(propertyValue); expect(clonedPropertyValue).not.toBe(propertyValue);
expect(clonedPropertyValue).toEqual(propertyValue); expect(clonedPropertyValue).toEqual(propertyValue);
} }
{ // Test that values are cloned in a different object. {
const propertyValue = { arr: [12, 11] }; // Test that values are cloned in a different object.
const clonedPropertyValue = s.property.object({ arr: s.property.array(s.property.numeric()) }).type.clone(propertyValue); const propertyValue = {arr: [12, 11]};
const clonedPropertyValue = s.property
.object({arr: s.property.array(s.property.numeric())})
.type.clone(propertyValue);
expect(clonedPropertyValue).not.toBe(propertyValue); expect(clonedPropertyValue).not.toBe(propertyValue);
expect(clonedPropertyValue).toEqual(propertyValue); expect(clonedPropertyValue).toEqual(propertyValue);
expect(clonedPropertyValue.arr).not.toBe(propertyValue.arr); expect(clonedPropertyValue.arr).not.toBe(propertyValue.arr);
@ -80,21 +240,84 @@ describe("object type", () => {
expect(testProperty.type.clone(null)).toBe(null); expect(testProperty.type.clone(null)).toBe(null);
}); });
test("invalid parameters types", () => { test("invalid parameters", () => {
expect(() => testProperty.type.serialize(5 as any)).toThrowError(InvalidTypeValueError); expect(() => testProperty.type.clone(5 as any)).toThrowError(
expect(() => testProperty.type.deserialize(5 as any)).toThrowError(InvalidTypeValueError); InvalidTypeValueError,
expect(() => testProperty.type.serializeDiff(5 as any)).toThrowError(InvalidTypeValueError); );
expect(() => testProperty.type.resetDiff(5 as any)).toThrowError(InvalidTypeValueError); expect(() => testProperty.type.clone([] as any)).toThrowError(
expect(() => testProperty.type.hasChanged(5 as any, 5 as any)).toThrowError(InvalidTypeValueError); InvalidTypeValueError,
expect(() => testProperty.type.serializedHasChanged(5 as any, 5 as any)).toThrowError(InvalidTypeValueError); );
expect(() => testProperty.type.clone(5 as any)).toThrowError(InvalidTypeValueError); });
});
expect(() => testProperty.type.serialize([] as any)).toThrowError(InvalidTypeValueError); test("applyPatch", () => {
expect(() => testProperty.type.deserialize([] as any)).toThrowError(InvalidTypeValueError); {
expect(() => testProperty.type.serializeDiff([] as any)).toThrowError(InvalidTypeValueError); // Apply a patch with undefined / NULL values.
expect(() => testProperty.type.resetDiff([] as any)).toThrowError(InvalidTypeValueError); expect(
expect(() => testProperty.type.hasChanged({} as any, [] as any)).toThrowError(InvalidTypeValueError); testProperty.type.applyPatch(
expect(() => testProperty.type.serializedHasChanged({} as any, [] as any)).toThrowError(InvalidTypeValueError); {test: "test", another: 12.548777},
expect(() => testProperty.type.clone([] as any)).toThrowError(InvalidTypeValueError); undefined,
false,
),
).toBeUndefined();
expect(
testProperty.type.applyPatch(
{test: "test", another: 12.548777},
null,
true,
),
).toBeNull();
}
{
// Invalid patch.
expect(() =>
testProperty.type.applyPatch(
{test: "test", another: 12.548777},
5416 as any,
false,
),
).toThrow(InvalidTypeValueError);
}
{
// Apply a patch.
{
const objectInstance = testProperty.type.applyPatch(
{test: "test", another: 12.548777},
{test: "another"},
true,
);
expect(objectInstance).toStrictEqual({
test: "another",
another: 12.548777,
});
}
{
const objectInstance = testProperty.type.applyPatch(
undefined,
{test: "test"},
false,
);
expect(objectInstance).toStrictEqual({
test: "test",
});
}
{
const objectInstance = testProperty.type.applyPatch(
null,
{test: "test"},
false,
);
expect(objectInstance).toStrictEqual({
test: "test",
});
}
}
}); });
}); });

View file

@ -2,62 +2,161 @@ import {describe, expect, test} from "vitest";
import {s, StringType} from "../../../src/library"; import {s, StringType} from "../../../src/library";
describe("string type", () => { describe("string type", () => {
test("string type definition", () => { test("definition", () => {
const stringType = s.property.string(); const stringType = s.property.string();
expect(stringType.type).toBeInstanceOf(StringType); expect(stringType.type).toBeInstanceOf(StringType);
}); });
test("string type functions", () => { describe("serialize", () => {
test("serialize", () => {
expect(s.property.string().type.serialize("test")).toBe("test"); expect(s.property.string().type.serialize("test")).toBe("test");
expect(s.property.string().type.deserialize("test")).toBe("test");
expect(s.property.string().type.serializeDiff("test")).toBe("test");
expect(s.property.string().type.serialize(null)).toBe(null); expect(s.property.string().type.serialize(null)).toBe(null);
expect(s.property.string().type.deserialize(null)).toBe(null);
expect(s.property.string().type.serializeDiff(null)).toBe(null);
expect(s.property.string().type.serialize(undefined)).toBe(undefined); expect(s.property.string().type.serialize(undefined)).toBe(undefined);
expect(s.property.string().type.deserialize(undefined)).toBe(undefined); });
expect(s.property.string().type.serializeDiff(undefined)).toBe(undefined);
test("invalid parameters", () => {
const testDate = new Date();
expect(s.property.string().type.serialize({} as any)).toBe(
"[object Object]",
);
expect(s.property.string().type.serialize(2120 as any)).toBe("2120");
expect(s.property.string().type.serialize(testDate as any)).toBe(
testDate.toString(),
);
});
});
describe("deserialize", () => {
test("deserialize", () => {
expect(s.property.string().type.deserialize("test")).toBe("test");
expect(s.property.string().type.deserialize(null)).toBe(null);
expect(s.property.string().type.deserialize(undefined)).toBe(undefined);
});
test("invalid parameters", () => {
expect(s.property.string().type.deserialize({} as any)).toBe(
"[object Object]",
);
expect(s.property.string().type.deserialize(2120 as any)).toBe("2120");
});
});
describe("serializeDiff", () => {
test("serializeDiff", () => {
expect(s.property.string().type.serializeDiff("test")).toBe("test");
expect(s.property.string().type.serializeDiff(null)).toBe(null);
expect(s.property.string().type.serializeDiff(undefined)).toBe(undefined);
});
test("invalid parameters", () => {
expect(s.property.string().type.serializeDiff({} as any)).toBe(
"[object Object]",
);
expect(s.property.string().type.serializeDiff(2120 as any)).toBe("2120");
});
});
describe("hasChanged", () => {
test("hasChanged", () => {
expect(s.property.string().type.hasChanged("test", "test")).toBeFalsy(); expect(s.property.string().type.hasChanged("test", "test")).toBeFalsy();
expect(s.property.string().type.hasChanged(null, null)).toBeFalsy(); expect(s.property.string().type.hasChanged(null, null)).toBeFalsy();
expect(s.property.string().type.hasChanged(undefined, undefined)).toBeFalsy(); expect(
s.property.string().type.hasChanged(undefined, undefined),
).toBeFalsy();
expect(s.property.string().type.hasChanged(null, undefined)).toBeTruthy(); expect(s.property.string().type.hasChanged(null, undefined)).toBeTruthy();
expect(s.property.string().type.hasChanged(undefined, null)).toBeTruthy(); expect(s.property.string().type.hasChanged(undefined, null)).toBeTruthy();
expect(s.property.string().type.hasChanged(null, "test")).toBeTruthy(); expect(s.property.string().type.hasChanged(null, "test")).toBeTruthy();
expect(s.property.string().type.hasChanged(undefined, "test")).toBeTruthy(); expect(
s.property.string().type.hasChanged(undefined, "test"),
).toBeTruthy();
expect(s.property.string().type.hasChanged("test", null)).toBeTruthy(); expect(s.property.string().type.hasChanged("test", null)).toBeTruthy();
expect(s.property.string().type.hasChanged("test", undefined)).toBeTruthy(); expect(
s.property.string().type.hasChanged("test", undefined),
).toBeTruthy();
});
expect(s.property.string().type.serializedHasChanged("test", "test")).toBeFalsy(); test("invalid parameters", () => {
expect(s.property.string().type.serializedHasChanged(null, null)).toBeFalsy(); expect(
expect(s.property.string().type.serializedHasChanged(undefined, undefined)).toBeFalsy(); s.property.string().type.hasChanged({} as any, {} as any),
expect(s.property.string().type.serializedHasChanged(null, undefined)).toBeTruthy(); ).toBeTruthy();
expect(s.property.string().type.serializedHasChanged(undefined, null)).toBeTruthy(); expect(
expect(s.property.string().type.serializedHasChanged(null, "test")).toBeTruthy(); s.property.string().type.hasChanged(false as any, false as any),
expect(s.property.string().type.serializedHasChanged(undefined, "test")).toBeTruthy(); ).toBeFalsy();
expect(s.property.string().type.serializedHasChanged("test", null)).toBeTruthy(); });
expect(s.property.string().type.serializedHasChanged("test", undefined)).toBeTruthy(); });
describe("serializedHasChanged", () => {
test("serializedHasChanged", () => {
expect(
s.property.string().type.serializedHasChanged("test", "test"),
).toBeFalsy();
expect(
s.property.string().type.serializedHasChanged(null, null),
).toBeFalsy();
expect(
s.property.string().type.serializedHasChanged(undefined, undefined),
).toBeFalsy();
expect(
s.property.string().type.serializedHasChanged(null, undefined),
).toBeTruthy();
expect(
s.property.string().type.serializedHasChanged(undefined, null),
).toBeTruthy();
expect(
s.property.string().type.serializedHasChanged(null, "test"),
).toBeTruthy();
expect(
s.property.string().type.serializedHasChanged(undefined, "test"),
).toBeTruthy();
expect(
s.property.string().type.serializedHasChanged("test", null),
).toBeTruthy();
expect(
s.property.string().type.serializedHasChanged("test", undefined),
).toBeTruthy();
});
test("invalid parameters", () => {
expect(
s.property.string().type.serializedHasChanged({} as any, {} as any),
).toBeTruthy();
expect(
s.property
.string()
.type.serializedHasChanged(false as any, false as any),
).toBeFalsy();
});
});
describe("resetDiff", () => {
test("resetDiff", () => {
s.property.string().type.resetDiff("test"); s.property.string().type.resetDiff("test");
s.property.string().type.resetDiff(undefined); s.property.string().type.resetDiff(undefined);
s.property.string().type.resetDiff(null); s.property.string().type.resetDiff(null);
}); });
test("invalid parameters types", () => { test("invalid parameters", () => {
const testDate = new Date(); expect(() => s.property.string().type.resetDiff({} as any)).not.toThrow();
expect(s.property.string().type.serialize({} as any)).toBe("[object Object]"); });
expect(s.property.string().type.serialize(2120 as any)).toBe("2120"); });
expect(s.property.string().type.serialize(testDate as any)).toBe(testDate.toString());
expect(s.property.string().type.deserialize({} as any)).toBe("[object Object]"); test("clone", () => {
expect(s.property.string().type.deserialize(2120 as any)).toBe("2120");
expect(s.property.string().type.serializeDiff({} as any)).toBe("[object Object]");
expect(s.property.string().type.serializeDiff(2120 as any)).toBe("2120");
expect(s.property.string().type.hasChanged({} as any, {} as any)).toBeTruthy();
expect(s.property.string().type.hasChanged(false as any, false as any)).toBeFalsy();
expect(s.property.string().type.serializedHasChanged({} as any, {} as any)).toBeTruthy();
expect(s.property.string().type.serializedHasChanged(false as any, false as any)).toBeFalsy();
expect(s.property.string().type.clone({} as any)).toStrictEqual({}); expect(s.property.string().type.clone({} as any)).toStrictEqual({});
}); });
test("applyPatch", () => {
expect(s.property.string().type.applyPatch("another", "test", false)).toBe(
"test",
);
expect(s.property.string().type.applyPatch(undefined, "test", true)).toBe(
"test",
);
expect(s.property.string().type.applyPatch(null, "test", false)).toBe(
"test",
);
expect(
s.property.string().type.applyPatch("test", undefined, false),
).toBeUndefined();
expect(s.property.string().type.applyPatch("test", null, false)).toBeNull();
});
}); });

View file

@ -2,7 +2,7 @@
"ts-node": { "ts-node": {
"compilerOptions": { "compilerOptions": {
"module": "ESNext", "module": "ESNext",
"types": ["node"], "types": ["node"]
} }
}, },
@ -22,9 +22,6 @@
"module": "ES6", "module": "ES6",
"target": "ES6", "target": "ES6",
"moduleResolution": "Bundler", "moduleResolution": "Bundler",
"lib": [ "lib": ["ESNext", "DOM"]
"ESNext",
"DOM"
]
} }
} }

View file

@ -1,10 +1,10 @@
import {ConfigEnv, defineConfig, UserConfig} from "vite"; import {defineConfig, UserConfig} from "vite";
import dts from "vite-plugin-dts"; import dts from "vite-plugin-dts";
// https://vitejs.dev/config/ // https://vitejs.dev/config/
export default defineConfig(({ mode }: ConfigEnv): UserConfig => { export default defineConfig((): UserConfig => {
return ({ return {
build: { build: {
outDir: "lib", outDir: "lib",
sourcemap: true, sourcemap: true,
@ -22,6 +22,6 @@ export default defineConfig(({ mode }: ConfigEnv): UserConfig => {
rollupTypes: true, rollupTypes: true,
exclude: ["node_modules"], exclude: ["node_modules"],
}), }),
] ],
}); };
}); });

1496
yarn.lock

File diff suppressed because it is too large Load diff