Compare commits

...

46 commits
v2.0.1 ... 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
8b1a1dabcf
Update README for the new version and add tests status badge.
All checks were successful
/ test (push) Successful in 19s
2025-03-30 12:52:14 +02:00
7e86e6fe86
Add yarn.lock.
All checks were successful
/ test (push) Successful in 19s
2025-03-30 12:44:43 +02:00
2debdf5e46
Add test workflow. 2025-03-30 12:44:22 +02:00
8afce56b9e
Add runtime type checking and errors when invalid values are provided. 2025-03-30 11:33:22 +02:00
f5502109ac
Rewrite model system, solve circular dependencies issues, better testing, clone function. 2025-03-29 22:59:13 +01:00
6eee1b709e
Rename save function to patch to be less confusing (it doesn't actually save anything). 2024-10-05 17:36:03 +02:00
8f8dafed5b
Objects, arrays and models changes deep checks when checking if a model is dirty. 2024-10-05 16:55:09 +02:00
ff9cb91f73
Fix date property change detection. 2024-10-05 16:03:58 +02:00
e373efdd0a
Add a way to get the identifier name of a model. 2024-10-05 14:16:15 +02:00
4eb8b7d3bc
Add models extension system. 2024-10-04 21:24:11 +02:00
576338fa62
Improve returned model type for deserialize function. 2024-10-04 17:41:25 +02:00
62e62f962e
Add a new model class type. 2024-10-04 17:07:40 +02:00
22bc42acba
Change description in package.json. 2024-10-04 16:22:06 +02:00
6af0da6b55
More welcoming README and add logo. 2024-10-04 16:11:49 +02:00
6a14623355
Update repository and keywords. 2024-10-04 15:21:31 +02:00
3e291d6bd5
Change license info. 2024-10-04 15:00:29 +02:00
72df9f6453
Change README for 3.0.0. 2024-10-04 14:58:00 +02:00
e43e27e2e1
Models rewrite with new API for better typings and extensibility. 2024-10-03 23:33:00 +02:00
498d25a909
Fix undefined date serialization. 2024-09-30 22:58:17 +02:00
9cb2bf1e5c
Fix undefined decimal type serialization. 2024-09-30 22:53:42 +02:00
d96c39d079
Allow undefined values in simple objects. 2024-09-28 17:18:12 +02:00
b6411c9401
Add default public access as publish config. 2024-09-28 17:13:18 +02:00
f512675906
Version 2.1.0 2024-09-28 17:03:46 +02:00
bb5eed5162
Add a simple object type and other improvements. 2024-09-28 17:02:52 +02:00
eb920a3d6d
Remove reflect-metadata dependency. 2024-09-28 17:01:14 +02:00
0876c59c98
Update library build and test configuration. 2024-09-28 15:13:22 +02:00
58 changed files with 9944 additions and 943 deletions

View file

@ -0,0 +1,16 @@
on: [push]
jobs:
test:
runs-on: docker
container:
image: node:latest
steps:
- uses: actions/checkout@v4
- run: corepack enable
- uses: actions/setup-node@v4
with:
cache: "yarn"
- run: yarn install
- run: yarn lint
- run: yarn coverage

2
.gitignore vendored
View file

@ -13,5 +13,3 @@ lib/
yarn-error.log
.pnp*
node_modules/
yarn.lock

5
.prettierrc Normal file
View file

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

View file

@ -1,6 +1,6 @@
MIT License
Copyright (c) <year> <copyright holders>
Copyright (c) 2024 Zeptotech
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

364
README.md
View file

@ -1,91 +1,212 @@
# Sharkitek Core
<p align="center">
<a href="https://code.zeptotech.net/Sharkitek/Core">
<picture>
<img alt="Sharkitek logo" width="200" src="https://code.zeptotech.net/Sharkitek/Core/raw/branch/main/logo.svg" />
</picture>
</a>
</p>
<h1 align="center">
Sharkitek
</h1>
<h4 align="center">
<a href="https://code.zeptotech.net/Sharkitek/Core">Documentation</a> |
<a href="https://code.zeptotech.net/Sharkitek/Core">Website</a>
</h4>
<p align="center">
TypeScript library for well-designed model architectures
</p>
<p align="center">
<img alt="Tests status" src="https://code.zeptotech.net/Sharkitek/Core/badges/workflows/test.yaml/badge.svg?branch=main" />
<a href="https://bundlephobia.com/package/@sharkitek/core" target="_blank">
<img alt="Bundle size" src="https://badgen.net/bundlephobia/minzip/@sharkitek/core" />
</a>
<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>
## Introduction
Sharkitek is a Javascript / TypeScript library designed to ease development of client-side models.
Sharkitek is a lightweight Javascript / TypeScript library designed to ease development of models.
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`, `deserialize` or `serializeDiff`.
```typescript
class Example extends Model<Example>
{
id: number;
name: string;
protected SDefinition(): ModelDefinition<Example>
{
return {
id: SDefine(SNumeric),
name: SDefine(SString),
};
}
}
```shell
yarn add @sharkitek/core
```
## Examples
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`.
### Simple model definition
```typescript
class Example {
id: number;
name: string;
}
const ExampleModel = defineModel({
Class: Example,
properties: {
id: s.property.numeric(),
name: s.property.string(),
},
identifier: "id",
});
```
## Quick start
**Note**: we usually define our models in a `{ModelName}Model` variable next to the model's class.
### Model definition
```typescript
/**
* A person.
*/
class Person extends Model<Person>
{
class Person {
id: number;
name: string;
firstName: string;
email: string;
createdAt: Date;
active: boolean = true;
protected SIdentifier(): ModelIdentifier<Person>
{
return "id";
}
protected SDefinition(): ModelDefinition<Person>
{
return {
name: SDefine(SString),
firstName: SDefine(SString),
email: SDefine(SString),
createdAt: SDefine(SDate),
active: SDefine(SBool),
};
}
}
/**
* A person model manager.
*/
const PersonModel = defineModel({
Class: Person,
properties: {
id: s.property.numeric(),
name: s.property.string(),
email: s.property.string(),
createdAt: s.property.date(),
active: s.property.boolean(),
},
identifier: "id",
});
```
```typescript
/**
* An article.
*/
class Article extends Model<Article>
{
class Article {
id: number;
title: string;
authors: Author[] = [];
authors: Person[] = [];
text: string;
evaluation: number;
protected SIdentifier(): ModelIdentifier<Article>
{
return "id";
tags: {
name: string;
}[];
}
protected SDefinition(): ModelDefinition<Article>
{
return {
id: SDefine(SNumeric),
title: SDefine(SString),
authors: SDefine(SArray(SModel(Author))),
text: SDefine(SString),
evaluation: SDefine(SDecimal),
};
}
/**
* 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
/**
* A model with composite keys.
*/
class CompositeKeys {
id1: number;
id2: string;
}
/**
* A composite keys model manager.
*/
const CompositeKeysModel = defineModel({
Class: CompositeKeys,
properties: {
id1: s.property.numeric(),
id2: s.property.string(),
},
identifier: ["id1", "id2"],
});
```
### Model functions
#### Serialization
```typescript
const instance = new Person();
instance.id = 1;
instance.createdAt = new Date();
instance.name = "John Doe";
instance.email = "john@doe.test";
instance.active = true;
const serialized = PersonModel.model(instance).serialize();
console.log(serialized); // { id: 1, createdAt: "YYYY-MM-DDTHH:mm:ss.sssZ", name: "John Doe", email: "john@doe.test", active: true }
```
#### Deserialization
```typescript
const instance = PersonModel.parse({
id: 1,
createdAt: "2011-10-05T14:48:00.000Z",
name: "John Doe",
email: "john@doe.test",
active: true,
});
console.log(instance instanceof Person); // true
console.log(instance.createdAt instanceof Date); // true
```
#### Patch
```typescript
const instance = PersonModel.parse({
id: 1,
createdAt: "2011-10-05T14:48:00.000Z",
name: "John Doe",
email: "john@doe.test",
active: true,
});
instance.name = "Johnny";
// Patch serialized only changed properties and the identifier.
console.log(PersonModel.model(instance).patch()); // { id: 1, name: "Johnny" }
// If you run it one more time, already patched properties will not be included again.
console.log(PersonModel.model(instance).patch()); // { id: 1 }
```
#### Identifier
```typescript
const instance = new CompositeKeys();
instance.id1 = 5;
instance.id2 = "foo";
const instanceIdentifier = CompositeKeysModel.model(instance).getIdentifier();
console.log(instanceIdentifier); // [5, "foo"]
```
## API
@ -96,64 +217,92 @@ Types are defined by a class extending `Type`.
Sharkitek defines some basic types by default, in these classes:
- `BoolType`: boolean value in the model, boolean value in the serialized object.
- `BooleanType`: boolean value in the model, boolean value in the serialized object.
- `StringType`: string in the model, string in the serialized object.
- `NumericType`: number in the model, number in the serialized object.
- `DecimalType`: number in the model, formatted string 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.
- `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.
When you are defining a Sharkitek property, 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
class Example extends Model<Example>
{
class Example {
foo: string;
}
protected SDefinition(): ModelDefinition<Example>
{
return {
foo: new Definition(new StringType()),
};
}
}
const ExampleModel = defineModel({
Class: Example,
properties: {
foo: s.property.define(new StringType()),
},
});
```
To ease the use of these classes and reduce read complexity, some constant variables and functions are defined in the library,
following a certain naming convention: "S{type_name}".
To ease the use of these classes and reduce read complexity, properties of each type are easily definable with a function for each type.
- `BoolType` => `SBool`
- `StringType` => `SString`
- `NumericType` => `SNumeric`
- `DecimalType` => `SDecimal`
- `DateType` => `SDate`
- `ArrayType` => `SArray`
- `ModelType` => `SModel`
- `BooleanType` => `s.property.boolean`
- `StringType` => `s.property.string`
- `NumericType` => `s.property.numeric`
- `DecimalType` => `s.property.decimal`
- `DateType` => `s.property.date`
- `ArrayType` => `s.property.array`
- `ObjectType` => `s.property.object`
- `MapType` => `s.property.map` or `s.property.stringMap`
- `ModelType` => `s.property.model`
When the types require parameters, the constant is defined as a function. If there is no parameter, then a simple
variable is enough.
Type implementers should provide a corresponding variable or function for each defined type. They can even provide
multiple functions or constants when predefined parameters. (For example, we could define `SStringArray` which would
be a variable similar to `SArray(SString)`.)
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
class Example extends Model<Example>
{
foo: string = undefined;
class Example {
foo: string;
}
protected SDefinition(): ModelDefinition<Example>
{
return {
foo: SDefine(SString),
};
}
}
const ExampleModel = defineModel({
Class: Example,
properties: {
foo: s.property.string(),
},
});
```
### Models
#### `model(instance)`
Get a model class (which has all the sharkitek models' functions) from a model instance.
```typescript
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 the model.
@ -161,17 +310,17 @@ Serialize the model.
Example:
```typescript
const serializedObject = model.serialize();
const serializedObject = definedModel.model(modelInstance).serialize();
```
#### `deserialize(serializedObject)`
#### `parse(serializedObject)`
Deserialize the model.
Example:
```typescript
const model = (new TestModel()).deserialize({
const modelInstance = definedModel.parse({
id: 5,
title: "Hello World!",
users: [
@ -190,7 +339,7 @@ Serialize the difference between current model state and original one.
Example:
```typescript
const model = (new TestModel()).deserialize({
const modelInstance = definedModel.parse({
id: 5,
title: "Hello World!",
users: [
@ -201,14 +350,13 @@ const model = (new TestModel()).deserialize({
],
});
model.title = "A new title for a new world";
modelInstance.title = "A new title for a new world";
const result = model.serializeDiff();
const result = definedModel.model(modelInstance).serializeDiff();
// if `id` is defined as the model identifier:
// result = { id: 5, title: "A new title for a new world" }
// if `id` is not defined as the model identifier:
// result = { title: "A new title for a new world" }
```
#### `resetDiff()`
@ -218,7 +366,7 @@ Set current properties values as original values.
Example:
```typescript
const model = (new TestModel()).deserialize({
const modelInstance = definedModel.parse({
id: 5,
title: "Hello World!",
users: [
@ -229,25 +377,24 @@ const model = (new TestModel()).deserialize({
],
});
model.title = "A new title for a new world";
modelInstance.title = "A new title for a new world";
model.resetDiff();
definedModel.model(modelInstance).resetDiff();
const result = model.serializeDiff();
const result = definedModel.model(modelInstance).serializeDiff();
// if `id` is defined as the model identifier:
// result = { id: 5 }
// if `id` is not defined as the model identifier:
// result = {}
```
#### `save()`
#### `patch()`
Get difference between original values and current ones, then reset it.
Similar to call `serializeDiff()` then `resetDiff()`.
```typescript
const model = (new TestModel()).deserialize({
const modelInstance = definedModel.parse({
id: 5,
title: "Hello World!",
users: [
@ -258,12 +405,11 @@ const model = (new TestModel()).deserialize({
],
});
model.title = "A new title for a new world";
modelInstance.title = "A new title for a new world";
const result = model.save();
const result = definedModel.model(modelInstance).patch();
// if `id` is defined as the model identifier:
// result = { id: 5, title: "A new title for a new world" }
// if `id` is not defined as the model identifier:
// result = { title: "A new title for a new world" }
```

View file

@ -1,38 +0,0 @@
import {build} from "esbuild";
/**
* Build the library.
* @param devMode - Dev mode.
*/
function buildLibrary(devMode: boolean = false): void
{
// Compilation de l'application.
build({
entryPoints: ["src/index.ts"],
outfile: "lib/index.js",
bundle: true,
minify: true,
sourcemap: true,
format: "esm",
loader: {
".ts": "ts",
},
watch: devMode ? {
// Affichage suite à une recompilation.
onRebuild(error, result) {
console.log(new Date());
if (!error && result.errors.length == 0)
console.log("Successfully built.");
else
console.error("Error!");
}
} : false,
})
// Fonction lancée pour une compilation réussie.
.then(() => { console.log(new Date()); console.log("Success."); })
// Fonction lancée pour une compilation échouée.
.catch((e) => console.error(e.message));
}
// @ts-ignore
buildLibrary(process.argv?.[2] == "dev");

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,9 +0,0 @@
/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */
module.exports = {
preset: "ts-jest",
testEnvironment: "node",
roots: [
"./tests",
],
};

37
logo.svg Normal file
View file

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg
fill="#000000"
width="900"
height="900"
viewBox="0 0 36 36"
version="1.1"
id="svg1"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<defs
id="defs1" />
<title
id="title1">shark</title>
<path
d="M 18.033615,1.4513338 C 10.403464,6.2983278 6.2910007,18.59117 6.2910007,34.205527 H 29.708999 c 0,-15.613237 -4.045235,-27.9071992 -11.675384,-32.7541932 z m -7.467688,15.9532972 1.284018,1.073376 -0.440332,-2.462712 c 0.5154,-0.767498 1.064412,-1.447601 1.641435,-2.029105 l 1.345642,1.913699 0.275626,-3.236931 c 0.657695,-0.416801 1.342279,-0.713715 2.045911,-0.877298 l 1.281776,2.534419 1.281775,-2.534419 c 0.69915,0.162462 1.378133,0.456016 2.032467,0.869455 l 0.28907,3.393792 1.406144,-2.001093 c 0.554614,0.568059 1.082339,1.226874 1.579811,1.96636 l -0.43921,2.462712 1.284016,-1.073375 c 1.706421,3.099118 2.943378,7.246962 3.466621,11.948299 -0.06683,3.889694 -21.7515603,2.320881 -21.8036319,-0.0011 0.5221219,-4.700217 1.7590797,-8.849181 3.4666199,-11.947179 z"
id="path2"
style="display:inline;fill:#1c4878;fill-opacity:1;stroke-width:1.12043" />
<path
d="M 18.030001,4.0195273 C 11.220001,8.3455277 6.2910007,20.269527 6.2910007,34.205527 H 29.708999 c 0,-13.935 -4.869,-25.8599993 -11.678998,-30.1859997 z m -6.664999,15.1909997 1.146,0.958 -0.393,-2.198 c 0.46,-0.685 0.949999,-1.292 1.465,-1.811 l 1.200997,1.708 0.246,-2.889 c 0.587,-0.372 1.198,-0.637 1.826,-0.783 l 1.144,2.262 1.144,-2.262 c 0.624,0.145 1.23,0.407 1.814,0.776 l 0.258,3.029 1.255,-1.786 c 0.495,0.507 0.966,1.095 1.41,1.755 l -0.392,2.198 1.146,-0.958 c 1.523,2.766 2.627,6.468 3.094,10.664 -0.626,-2.009 -1.659,-3.774 -2.975,-5.146 l 0.25,-2.235 -1.6,1.042 c -0.381,-0.283 -0.777,-0.537 -1.188,-0.759 l -0.208,-2.556 -1.641,1.801 c -0.456,-0.13 -0.924,-0.222 -1.401,-0.276 l -0.968,-2.074 -0.968,2.074 c -0.508,0.057 -1.006,0.159 -1.49,0.302 l -1.549999,-1.701 -0.197,2.425 c -0.415,0.224 -0.815,0.479 -1.199,0.765 l -1.6,-1.042 0.25,2.234 c -1.3159993,1.371 -2.3489993,3.136 -2.9749993,5.145 0.466,-4.195 1.57,-7.898 3.0939993,-10.663 z"
id="path1"
style="display:inline;fill:#3178c6;fill-opacity:1" />
<metadata
id="metadata1">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:title>shark</dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

View file

@ -1,38 +1,54 @@
{
"name": "@sharkitek/core",
"version": "2.0.1",
"description": "Sharkitek core models library.",
"version": "4.1.0",
"description": "TypeScript library for well-designed model architectures.",
"keywords": [
"sharkitek",
"model",
"serialization",
"deserialization",
"diff",
"dirty",
"deserialization",
"property"
"model",
"object",
"property",
"serialization",
"sharkitek",
"typescript"
],
"repository": "https://git.madeorsk.com/Sharkitek/core",
"author": "Madeorsk <madeorsk@protonmail.com>",
"license": "MIT",
"scripts": {
"build": "parcel build",
"test": "jest"
"repository": "https://code.zeptotech.net/Sharkitek/Core",
"author": {
"name": "Madeorsk",
"email": "m@deor.sk"
},
"source": "src/index.ts",
"main": "lib/index.js",
"license": "MIT",
"publishConfig": {
"access": "public"
},
"scripts": {
"build": "tsc && vite build",
"test": "vitest",
"coverage": "vitest run --coverage",
"format": "prettier . --write",
"lint": "eslint"
},
"type": "module",
"source": "src/library.ts",
"types": "lib/index.d.ts",
"main": "lib/index.js",
"files": [
"lib/**/*"
],
"devDependencies": {
"@parcel/packager-ts": "2.7.0",
"@parcel/transformer-typescript-types": "2.7.0",
"@types/jest": "^28.1.6",
"esbuild": "^0.15.8",
"jest": "^28.1.3",
"parcel": "^2.7.0",
"ts-jest": "^28.0.7",
"ts-node": "^10.9.1",
"typescript": "^4.7.4"
}
"@eslint/js": "^9.30.0",
"@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",
"typescript": "^5.8.3",
"typescript-eslint": "^8.35.0",
"vite": "^7.0.0",
"vite-plugin-dts": "^4.5.4",
"vitest": "^3.2.4"
},
"packageManager": "yarn@4.9.2"
}

View file

@ -1,34 +0,0 @@
import {Type} from "./Types/Type";
/**
* Options of a definition.
*/
export interface DefinitionOptions<SerializedType, SharkitekType>
{ //TODO implement some options, like `mandatory`.
}
/**
* A Sharkitek model property definition.
*/
export class Definition<SerializedType, SharkitekType>
{
/**
* Initialize a property definition with the given type and options.
* @param type - The model property type.
* @param options - Property definition options.
*/
constructor(
public type: Type<SerializedType, SharkitekType>,
public options: DefinitionOptions<SerializedType, SharkitekType> = {},
) {}
}
/**
* Initialize a property definition with the given type and options.
* @param type - The model property type.
* @param options - Property definition options.
*/
export function SDefine<SerializedType, SharkitekType>(type: Type<SerializedType, SharkitekType>, options: DefinitionOptions<SerializedType, SharkitekType> = {})
{
return new Definition(type, options);
}

View file

@ -1,217 +0,0 @@
import {Definition} from "./Definition";
/**
* Model properties definition type.
*/
export type ModelDefinition<T> = Partial<Record<keyof T, Definition<unknown, unknown>>>;
/**
* Model identifier type.
*/
export type ModelIdentifier<T> = keyof T;
/**
* A Sharkitek model.
*/
export abstract class Model<THIS>
{
/**
* Model properties definition function.
*/
protected abstract SDefinition(): ModelDefinition<THIS>;
/**
* Return the name of the model identifier property.
*/
protected SIdentifier(): ModelIdentifier<THIS>
{
return undefined;
}
/**
* Get given property definition.
* @protected
*/
protected getPropertyDefinition(propertyName: string): Definition<unknown, unknown>
{
return (this.SDefinition() as any)?.[propertyName];
}
/**
* Get the list of the model properties.
* @protected
*/
protected getProperties(): string[]
{
return Object.keys(this.SDefinition());
}
/**
* Calling a function for a defined property.
* @param propertyName - The property for which to check definition.
* @param callback - The function called when the property is defined.
* @param notProperty - The function called when the property is not defined.
* @protected
*/
protected propertyWithDefinition(propertyName: string, callback: (propertyDefinition: Definition<unknown, unknown>) => void, notProperty: () => void = () => {}): unknown
{
// Getting the current property definition.
const propertyDefinition = this.getPropertyDefinition(propertyName);
if (propertyDefinition)
// There is a definition for the current property, calling the right callback.
return callback(propertyDefinition);
else
// No definition for the given property, calling the right callback.
return notProperty();
}
/**
* Calling a function for each defined property.
* @param callback - The function to call.
* @protected
*/
protected forEachModelProperty(callback: (propertyName: string, propertyDefinition: Definition<unknown, unknown>) => unknown): any|void
{
for (const propertyName of this.getProperties())
{ // For each property, checking that its type is defined and calling the callback with its type.
const result = this.propertyWithDefinition(propertyName, (propertyDefinition) => {
// If the property is defined, calling the function with the property name and definition.
const result = callback(propertyName, propertyDefinition);
// If there is a return value, returning it directly (loop is broken).
if (typeof result !== "undefined") return result;
});
// If there is a return value, returning it directly (loop is broken).
if (typeof result !== "undefined") return result;
}
}
/**
* The original properties values.
* @protected
*/
protected _originalProperties: Record<string, any> = {};
/**
* The original (serialized) object.
* @protected
*/
protected _originalObject: any = null;
/**
* Determine if the model is new or not.
*/
isNew(): boolean
{
return !this._originalObject;
}
/**
* Determine if the model is dirty or not.
*/
isDirty(): boolean
{
return this.forEachModelProperty((propertyName, propertyDefinition) => (
// For each property, checking if it is different.
propertyDefinition.type.propertyHasChanged(this._originalProperties[propertyName], (this as any)[propertyName])
// There is a difference, we should return false.
? true
// There is no difference, returning nothing.
: undefined
)) === true;
}
/**
* Get model identifier.
*/
getIdentifier(): unknown
{
return (this as any)[this.SIdentifier()];
}
/**
* Set current properties values as original values.
*/
resetDiff()
{
this.forEachModelProperty((propertyName, propertyDefinition) => {
// For each property, set its original value to its current property value.
this._originalProperties[propertyName] = (this as any)[propertyName];
propertyDefinition.type.resetDiff((this as any)[propertyName]);
});
}
/**
* Serialize the difference between current model state and original one.
*/
serializeDiff(): any
{
// Creating a serialized object.
const serializedDiff: any = {};
this.forEachModelProperty((propertyName, propertyDefinition) => {
// For each defined model property, adding it to the serialized object if it has changed.
if (this.SIdentifier() == propertyName
|| propertyDefinition.type.propertyHasChanged(this._originalProperties[propertyName], (this as any)[propertyName]))
// Adding the current property to the serialized object if it is the identifier or its value has changed.
serializedDiff[propertyName] = propertyDefinition.type.serializeDiff((this as any)[propertyName]);
})
return serializedDiff; // Returning the serialized object.
}
/**
* Get difference between original values and current ones, then reset it.
* Similar to call `serializeDiff()` then `resetDiff()`.
*/
save(): any
{
// Get the difference.
const diff = this.serializeDiff();
// Once the difference has been gotten, reset it.
this.resetDiff();
return diff; // Return the difference.
}
/**
* Serialize the model.
*/
serialize(): any
{
// Creating a serialized object.
const serializedObject: any = {};
this.forEachModelProperty((propertyName, propertyDefinition) => {
// For each defined model property, adding it to the serialized object.
serializedObject[propertyName] = propertyDefinition.type.serialize((this as any)[propertyName]);
});
return serializedObject; // Returning the serialized object.
}
/**
* Special operations on parse.
* @protected
*/
protected parse(): void
{} // Nothing by default. TODO: create an event system to create functions like "beforeDeserialization" or "afterDeserialization".
/**
* Deserialize the model.
*/
deserialize(serializedObject: any): THIS
{
this.forEachModelProperty((propertyName, propertyDefinition) => {
// For each defined model property, assigning its deserialized value to the model.
(this as any)[propertyName] = propertyDefinition.type.deserialize(serializedObject[propertyName]);
});
// Reset original property values.
this.resetDiff();
this._originalObject = serializedObject; // The model is not a new one, but loaded from a deserialized one.
return this as unknown as THIS; // Returning this, after deserialization.
}
}

View file

@ -1,65 +0,0 @@
import {Type} from "./Type";
/**
* Type of an array of values.
*/
export class ArrayType<SerializedValueType, SharkitekValueType> extends Type<SerializedValueType[], SharkitekValueType[]>
{
/**
* Constructs a new array type of Sharkitek model property.
* @param valueType - Type of the array values.
*/
constructor(protected valueType: Type<SerializedValueType, SharkitekValueType>)
{
super();
}
serialize(value: SharkitekValueType[]): SerializedValueType[]
{
if (value === undefined) return undefined;
if (value === null) return null;
return value.map((value) => (
// Serializing each value of the array.
this.valueType.serialize(value)
));
}
deserialize(value: SerializedValueType[]): SharkitekValueType[]
{
if (value === undefined) return undefined;
if (value === null) return null;
return value.map((serializedValue) => (
// Deserializing each value of the array.
this.valueType.deserialize(serializedValue)
));
}
serializeDiff(value: SharkitekValueType[]): any
{
if (value === undefined) return undefined;
if (value === null) return null;
// Serializing diff of all elements.
return value.map((value) => this.valueType.serializeDiff(value));
}
resetDiff(value: SharkitekValueType[]): void
{
// Do nothing if it is not an array.
if (!Array.isArray(value)) return;
// Reset diff of all elements.
value.forEach((value) => this.valueType.resetDiff(value));
}
}
/**
* Type of an array of values.
* @param valueType - Type of the array values.
*/
export function SArray<SerializedValueType, SharkitekValueType>(valueType: Type<SerializedValueType, SharkitekValueType>)
{
return new ArrayType<SerializedValueType, SharkitekValueType>(valueType);
}

View file

@ -1,22 +0,0 @@
import {Type} from "./Type";
/**
* Type of any boolean value.
*/
export class BoolType extends Type<boolean, boolean>
{
deserialize(value: boolean): boolean
{
return !!value; // ensure bool type.
}
serialize(value: boolean): boolean
{
return !!value; // ensure bool type.
}
}
/**
* Type of any boolean value.
*/
export const SBool = new BoolType();

View file

@ -1,22 +0,0 @@
import {Type} from "./Type";
/**
* Type of dates.
*/
export class DateType extends Type<string, Date>
{
deserialize(value: string): Date
{
return new Date(value);
}
serialize(value: Date): string
{
return value.toISOString();
}
}
/**
* Type of dates.
*/
export const SDate = new DateType();

View file

@ -1,22 +0,0 @@
import {Type} from "./Type";
/**
* Type of decimal numbers.
*/
export class DecimalType extends Type<string, number>
{
deserialize(value: string): number
{
return parseFloat(value);
}
serialize(value: number): string
{
return value.toString();
}
}
/**
* Type of decimal numbers.
*/
export const SDecimal = new DecimalType();

View file

@ -1,55 +0,0 @@
import {Type} from "./Type";
import {Model} from "../Model";
/**
* Type definition of the constructor of a specific type.
*/
export type ConstructorOf<T> = { new(): T; }
/**
* Type of a Sharkitek model value.
*/
export class ModelType<M extends Model<M>> extends Type<any, M>
{
/**
* Constructs a new model type of a Sharkitek model property.
* @param modelConstructor - Constructor of the model.
*/
constructor(protected modelConstructor: ConstructorOf<M>)
{
super();
}
serialize(value: M|null): any
{
// Serializing the given model.
return value ? value.serialize() : null;
}
deserialize(value: any): M|null
{
// Deserializing the given object in the new model.
return value ? (new this.modelConstructor()).deserialize(value) : null;
}
serializeDiff(value: M): any
{
// Serializing the given model.
return value ? value.serializeDiff() : null;
}
resetDiff(value: M): void
{
// Reset diff of the given model.
value?.resetDiff();
}
}
/**
* Type of a Sharkitek model value.
* @param modelConstructor - Constructor of the model.
*/
export function SModel<M extends Model<M>>(modelConstructor: ConstructorOf<M>)
{
return new ModelType(modelConstructor);
}

View file

@ -1,22 +0,0 @@
import {Type} from "./Type";
/**
* Type of any numeric value.
*/
export class NumericType extends Type<number, number>
{
deserialize(value: number): number
{
return value;
}
serialize(value: number): number
{
return value;
}
}
/**
* Type of any numeric value.
*/
export const SNumeric = new NumericType();

View file

@ -1,22 +0,0 @@
import {Type} from "./Type";
/**
* Type of any string value.
*/
export class StringType extends Type<string, string>
{
deserialize(value: string): string
{
return value;
}
serialize(value: string): string
{
return value;
}
}
/**
* Type of any string value.
*/
export const SString = new StringType();

View file

@ -1,55 +0,0 @@
/**
* Abstract class of a Sharkitek model property type.
*/
export abstract class Type<SerializedType, SharkitekType>
{
/**
* Serialize the given value of a Sharkitek model property.
* @param value - Value to serialize.
*/
abstract serialize(value: SharkitekType): SerializedType;
/**
* Deserialize the given value of a serialized Sharkitek model.
* @param value - Value to deserialize.
*/
abstract deserialize(value: SerializedType): SharkitekType;
/**
* Serialize the given value only if it has changed.
* @param value - Value to deserialize.
*/
serializeDiff(value: SharkitekType): SerializedType|null
{
return this.serialize(value); // By default, nothing changes.
}
/**
* Reset the difference between the original value and the current one.
* @param value - Value for which reset diff data.
*/
resetDiff(value: SharkitekType): void
{
// By default, nothing to do.
}
/**
* Determine if the property value has changed.
* @param originalValue - Original property value.
* @param currentValue - Current property value.
*/
propertyHasChanged(originalValue: SharkitekType, currentValue: SharkitekType): boolean
{
return originalValue != currentValue;
}
/**
* Determine if the serialized property value has changed.
* @param originalValue - Original serialized property value.
* @param currentValue - Current serialized property value.
*/
serializedPropertyHasChanged(originalValue: SerializedType, currentValue: SerializedType): boolean
{
return originalValue != currentValue;
}
}

3
src/errors/index.ts Normal file
View file

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

View file

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

View file

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

16
src/errors/type-error.ts Normal file
View file

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

View file

@ -1,14 +0,0 @@
export * from "./Model/Model";
export * from "./Model/Definition";
export * from "./Model/Types/Type";
export * from "./Model/Types/ArrayType";
export * from "./Model/Types/BoolType";
export * from "./Model/Types/DateType";
export * from "./Model/Types/DecimalType";
export * from "./Model/Types/ModelType";
export * from "./Model/Types/NumericType";
export * from "./Model/Types/StringType";

5
src/library.ts Normal file
View file

@ -0,0 +1,5 @@
import * as s from "./model";
export * from "./model";
export * from "./errors";
export {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;
}

16
src/model/index.ts Normal file
View file

@ -0,0 +1,16 @@
export * as property from "./properties";
export * from "./model";
export {Definition} from "./property-definition";
export {newModel, ModelBuilder} from "./builder";
export {ArrayType} from "./types/array";
export {BooleanType} from "./types/boolean";
export {DateType} from "./types/date";
export {DecimalType} from "./types/decimal";
export {ModelType} from "./types/model";
export {NumericType} from "./types/numeric";
export {ObjectType} from "./types/object";
export {StringType} from "./types/string";
export {circular} from "./types/model";

667
src/model/model.ts Normal file
View file

@ -0,0 +1,667 @@
import {Definition, UnknownDefinition} from "./property-definition";
import {ConstructorOf, Modify} from "../utils";
/**
* A model shape.
*/
export type ModelShape<T extends object> = Partial<{
[k in keyof T]: Definition<unknown, T[k]>;
}>;
/**
* Properties values of a model based on its shape.
*/
export type ModelPropertiesValues<
T extends object,
Shape extends ModelShape<T>,
> = {
[k in keyof Shape]: Shape[k]["_sharkitek"];
};
/**
* Serialized object type based on model shape.
*/
export type SerializedModel<T extends object, Shape extends ModelShape<T>> = {
[k in keyof Shape]?: Shape[k]["_serialized"];
};
/**
* This is an experimental serialized model type declaration.
* @deprecated
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
type ExperimentalSerializedModel<
T extends object,
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"];
};
type ExperimentalSerializedModelOptionalKeys<
T extends object,
Shape extends ModelShape<T>,
> = {
[k in keyof Shape]: Shape[k]["_serialized"] extends undefined ? k : never;
}[keyof Shape];
/**
* A sharkitek model instance, with internal model state.
*/
export type ModelInstance<
T extends object,
Shape extends ModelShape<T>,
Identifier extends IdentifierDefinition<T, Shape>,
> = T & {
/**
* The Sharkitek model state.
*/
_sharkitek: Model<T, Shape, Identifier>;
};
/**
* Identifier definition type.
*/
export type IdentifierDefinition<
T extends object,
Shape extends ModelShape<T>,
> = keyof Shape | (keyof Shape)[];
/**
* Identifier type.
*/
export type IdentifierType<
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.
*/
export interface ModelDefinition<
T extends object,
Shape extends ModelShape<T>,
Identifier extends IdentifierDefinition<T, Shape>,
> {
/**
* Model class.
*/
Class: ConstructorOf<T>;
/**
* Properties names of the model identifier.
* Can be a single of a composite identifier.
*/
identifier?: Identifier;
/**
* Model properties definition.
* Set properties types (serialized and deserialized).
*/
properties: Shape;
}
/**
* A model property.
*/
export interface ModelProperty<T extends object, Shape extends ModelShape<T>> {
/**
* Property name.
*/
name: keyof Shape;
/**
* Property definition.
*/
definition: UnknownDefinition;
/**
* Set if the property is part of the identifier or not.
*/
identifier: boolean;
}
/**
* Model properties iterator object.
*/
export type ModelProperties<
T extends object,
Shape extends ModelShape<T>,
> = ModelProperty<T, Shape>[];
/**
* A Sharkitek model state.
*/
export class Model<
T extends object,
Shape extends ModelShape<T>,
Identifier extends IdentifierDefinition<T, Shape>,
> {
/**
* The model manager instance.
*/
readonly manager: ModelManager<T, Shape, Identifier>;
/**
* The model definition object.
*/
readonly definition: ModelDefinition<T, Shape, Identifier>;
/**
* Iterable properties.
*/
readonly properties: ModelProperties<T, Shape>;
/**
* The actual instance of the model.
*/
instance: ModelInstance<T, Shape, Identifier>;
/**
* Original values, to keep track of the changes on the model properties.
* @protected
*/
protected original: {
/**
* The original properties values.
*/
properties: Partial<ModelPropertiesValues<T, Shape>>;
/**
* The original serialized object, if there is one.
*/
serialized: SerializedModel<T, Shape> | null;
};
/**
* Initialize a new model state with the defined properties.
* @param manager The model manager.
*/
constructor(manager: ModelManager<T, Shape, Identifier>) {
this.manager = manager;
this.definition = manager.definition;
this.properties = manager.properties;
}
/**
* Initialize the Sharkitek model state for a new instance.
*/
initInstance(): this {
return this.fromInstance(
// Initialize a new model instance.
new this.definition.Class(),
);
}
/**
* Initialize the Sharkitek model state for the provided instance.
* @param instance The model instance.
*/
fromInstance(instance: T): this {
// Initialize the sharkitek model instance.
const sharkitekInstance = instance as ModelInstance<T, Shape, Identifier>;
// Keep the original instance, if it exists.
const originalInstance = sharkitekInstance._sharkitek;
// Set references to instance / model state.
sharkitekInstance._sharkitek = this;
this.instance = sharkitekInstance;
if (originalInstance)
// Share the same original values object.
this.original = originalInstance.original;
else {
// Initialize a new original values object, based on the current values of the instance.
this.original = {
properties: undefined,
serialized: null,
};
this.resetDiff();
}
return this;
}
/**
* Deserialize provided data to a new model instance.
* @param serialized Serialized model.
*/
deserialize(serialized: SerializedModel<T, Shape>): this {
// Initialize a new model instance.
this.initInstance();
for (const property of this.properties) {
// 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]);
}
// Reset original property values.
this.resetDiff();
// Store the original serialized object.
this.original.serialized = serialized;
return this;
}
/**
* Get current model instance identifier.
*/
getIdentifier(): IdentifierType<T, Shape, Identifier> {
if (Array.isArray(this.definition.identifier)) {
// The identifier is composite, make an array of properties values.
return this.definition.identifier.map(
(identifier) => this.instance?.[identifier as keyof T],
) as IdentifierType<T, Shape, Identifier>;
} else {
// 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>;
}
}
/**
* Get current model instance properties.
*/
getInstanceProperties(): ModelPropertiesValues<T, Shape> {
// Initialize an empty model properties object.
const instanceProperties: Partial<ModelPropertiesValues<T, Shape>> = {};
for (const property of this.properties) {
// 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.
}
return instanceProperties as ModelPropertiesValues<T, Shape>; // Returning the properties object.
}
/**
* Serialize the model instance.
*/
serialize(): SerializedModel<T, Shape> {
// Creating an empty serialized object.
const serializedObject: Partial<SerializedModel<T, Shape>> = {};
for (const property of this.properties) {
// For each defined model property, adding it to the serialized object.
serializedObject[property.name] = property.definition.type.serialize(
// keyof Shape is a subset of keyof T.
this.instance?.[property.name as keyof T],
);
}
return serializedObject as SerializedModel<T, Shape>; // Returning the serialized object.
}
/**
* Examine if the model is new (never deserialized) or not.
*/
isNew(): boolean {
return !this.original.serialized;
}
/**
* Examine if the model is dirty or not.
*/
isDirty(): boolean {
for (const property of this.properties) {
// For each property, check if it is different.
if (
property.definition.type.hasChanged(
this.original.properties?.[property.name],
this.instance?.[property.name as keyof T],
)
)
// There is a difference: the model is dirty.
return true;
}
// No difference.
return false;
}
/**
* Serialize the difference between current model state and the original one.
*/
serializeDiff(): Partial<SerializedModel<T, Shape>> {
// Creating an empty serialized object.
const serializedObject: Partial<SerializedModel<T, Shape>> = {};
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.
const instancePropValue = this.instance?.[property.name as keyof T]; // keyof Shape is a subset of keyof T.
if (
property.identifier ||
property.definition.type.hasChanged(
this.original.properties?.[property.name],
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.
}
/**
* Set current model properties as original values.
*/
resetDiff(): void {
this.original.properties = {};
for (const property of this.properties) {
// For each property, set its original value to the current property value.
const instancePropValue = this.instance?.[property.name as keyof T];
this.original.properties[property.name] =
property.definition.type.clone(instancePropValue);
property.definition.type.resetDiff(instancePropValue);
}
}
/**
* Get difference between original values and current ones, then reset it.
* Similar to call `serializeDiff()` then `resetDiff()`.
*/
patch(): Partial<SerializedModel<T, Shape>> {
// Get the difference.
const diff = this.serializeDiff();
// Once the difference has been obtained, reset it.
this.resetDiff();
return diff; // Return the difference.
}
/**
* Clone the model instance.
*/
clone(): ModelInstance<T, Shape, Identifier> {
// Initialize a new instance for the clone.
const cloned = this.manager.model();
// Clone every value of the model instance.
for (const [key, value] of Object.entries(this.instance) as [
keyof T,
unknown,
][]) {
// For each [key, value], clone the value and put it in the cloned instance.
// Do not clone ourselves.
if (key == "_sharkitek") continue;
if (this.definition.properties[key]) {
// 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);
} else {
// Not a property, cloning the raw value.
(cloned.instance[key] as any) = structuredClone(value);
}
}
// Clone original properties.
for (const property of this.properties) {
// For each property, clone its original value.
cloned.original.properties[property.name] =
property.definition.type.clone(this.original.properties[property.name]);
}
// Clone original serialized.
cloned.original.serialized = structuredClone(this.original.serialized);
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.
*/
export class ModelManager<
T extends object,
Shape extends ModelShape<T>,
Identifier extends IdentifierDefinition<T, Shape>,
> {
/**
* Defined properties.
*/
properties: ModelProperties<T, Shape>;
/**
* Initialize a model manager from a model definition.
* @param definition The model definition.
*/
constructor(
public readonly definition: ModelDefinition<T, Shape, Identifier>,
) {
this.initProperties();
}
/**
* Initialize properties iterator from current definition.
* @protected
*/
protected initProperties(): void {
// Build an array of model properties from the definition.
this.properties = [];
for (const propertyName in this.definition.properties) {
// For each property, build a model property object.
this.properties.push({
name: propertyName,
definition: this.definition.properties[propertyName],
// Find out if the current property is part of the identifier.
identifier: Array.isArray(this.definition.identifier)
? // The identifier is an array, the property must be in the array.
this.definition.identifier.includes(
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),
} as ModelProperty<T, Shape>);
}
}
/**
* 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.
*/
model(
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)
// 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);
else
// No instance, initialize a new one.
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.
* @param serialized The serialized model object.
*/
parse(
serialized: SerializedModel<T, Shape>,
): ModelInstance<T, Shape, Identifier> {
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.
* @param definition The model definition object.
*/
export function defineModel<
T extends object,
Shape extends ModelShape<T>,
Identifier extends IdentifierDefinition<T, Shape>,
>(definition: ModelDefinition<T, Shape, Identifier>) {
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.
*/
export type GenericModelManager<T extends object> = ModelManager<
T,
ModelShape<T>,
IdentifierDefinition<T, ModelShape<T>>
>;
/**
* Function to get a model manager lazily.
*/
export type LazyModelManager<
T extends object,
Shape extends ModelShape<T>,
Identifier extends IdentifierDefinition<T, Shape>,
> = () => ModelManager<T, Shape, Identifier>;
/**
* A model manager definition that can be lazy.
*/
export type DeclaredModelManager<
T extends object,
Shape extends ModelShape<T>,
Identifier extends IdentifierDefinition<T, Shape>,
> = ModelManager<T, Shape, Identifier> | LazyModelManager<T, Shape, Identifier>;

12
src/model/properties.ts Normal file
View file

@ -0,0 +1,12 @@
export {define} from "./property-definition";
export {array} from "./types/array";
export {bool, boolean} from "./types/boolean";
export {date} from "./types/date";
export {decimal} from "./types/decimal";
export {model} from "./types/model";
export {numeric} from "./types/numeric";
export {object} from "./types/object";
export {string} from "./types/string";
export {map} from "./types/map";
export {stringMap} from "./types/map";

View file

@ -0,0 +1,35 @@
import {Type} from "./types/type";
/**
* Property definition class.
*/
export class Definition<SerializedType, ModelType> {
readonly _sharkitek: ModelType;
readonly _serialized: SerializedType;
/**
* Create a property definer instance.
* @param type Property type.
*/
constructor(public readonly type: Type<SerializedType, ModelType>) {}
}
/**
* Unknown property definition.
*/
export type UnknownDefinition = Definition<unknown, unknown>;
/**
* Any property definition.
*/
export type AnyDefinition = Definition<any, any>;
/**
* New definition of a property of the given type.
* @param type Type of the property to define.
*/
export function define<SerializedType, ModelType>(
type: Type<SerializedType, ModelType>,
): Definition<SerializedType, ModelType> {
return new Definition(type);
}

187
src/model/types/array.ts Normal file
View file

@ -0,0 +1,187 @@
import {Type} from "./type";
import {define, Definition} from "../property-definition";
import {InvalidTypeValueError} from "../../errors";
/**
* Type of an array of values.
*/
export class ArrayType<SerializedValueType, SharkitekValueType> extends Type<
SerializedValueType[],
SharkitekValueType[]
> {
/**
* Initialize a new array type of a Sharkitek model property.
* @param valueDefinition Definition the array values.
*/
constructor(
protected valueDefinition: Definition<
SerializedValueType,
SharkitekValueType
>,
) {
super();
}
serialize(
value: SharkitekValueType[] | null | undefined,
): SerializedValueType[] | null | undefined {
if (value === undefined) return undefined;
if (value === null) return null;
if (!Array.isArray(value))
throw new InvalidTypeValueError(this, value, "value must be an array");
return value.map((value) =>
// Serializing each value of the array.
this.valueDefinition.type.serialize(value),
);
}
deserialize(
value: SerializedValueType[] | null | undefined,
): SharkitekValueType[] | null | undefined {
if (value === undefined) return undefined;
if (value === null) return null;
if (!Array.isArray(value))
throw new InvalidTypeValueError(this, value, "value must be an array");
return value.map((serializedValue) =>
// Deserializing each value of the array.
this.valueDefinition.type.deserialize(serializedValue),
);
}
serializeDiff(
value: SharkitekValueType[] | null | undefined,
): SerializedValueType[] | null | undefined {
if (value === undefined) return undefined;
if (value === null) return null;
if (!Array.isArray(value))
throw new InvalidTypeValueError(this, value, "value must be an array");
// Serializing diff of all elements.
return value.map(
(value) =>
this.valueDefinition.type.serializeDiff(value) as SerializedValueType,
);
}
resetDiff(value: SharkitekValueType[] | null | undefined): void {
// Do nothing if it is not an array.
if (!Array.isArray(value)) return;
// Reset diff of all elements.
value.forEach((value) => this.valueDefinition.type.resetDiff(value));
}
hasChanged(
originalValue: SharkitekValueType[] | null | undefined,
currentValue: SharkitekValueType[] | null | undefined,
): boolean {
// If any array length is different, arrays are different.
if (originalValue?.length != currentValue?.length) return true;
// If length is undefined, values are probably not arrays.
if (originalValue?.length == undefined)
return super.hasChanged(originalValue, currentValue);
for (const key of originalValue.keys()) {
// Check for any change for each value in the array.
if (
this.valueDefinition.type.hasChanged(
originalValue[key],
currentValue[key],
)
)
// The value has changed, the array is different.
return true;
}
return false; // No change detected.
}
serializedHasChanged(
originalValue: SerializedValueType[] | null | undefined,
currentValue: SerializedValueType[] | null | undefined,
): boolean {
// If any array length is different, arrays are different.
if (originalValue?.length != currentValue?.length) return true;
// If length is undefined, values are probably not arrays.
if (originalValue?.length == undefined)
return super.serializedHasChanged(originalValue, currentValue);
for (const key of originalValue.keys()) {
// Check for any change for each value in the array.
if (
this.valueDefinition.type.serializedHasChanged(
originalValue[key],
currentValue[key],
)
)
// The value has changed, the array is different.
return true;
}
return false; // No change detected.
}
clone<T extends SharkitekValueType[]>(array: T | null | undefined): T {
// Handle NULL / undefined array.
if (!array) return super.clone(array);
if (!Array.isArray(array))
throw new InvalidTypeValueError(this, array, "value must be an array");
// Initialize an empty array.
const cloned = [] as T;
for (const value of array) {
// Clone each value of the array.
cloned.push(this.valueDefinition.type.clone(value));
}
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.
* @param valueDefinition Array values type definition.
*/
export function array<SerializedValueType, SharkitekValueType>(
valueDefinition: Definition<SerializedValueType, SharkitekValueType>,
): Definition<SerializedValueType[], SharkitekValueType[]> {
return define(new ArrayType(valueDefinition));
}

View file

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

51
src/model/types/date.ts Normal file
View file

@ -0,0 +1,51 @@
import {Type} from "./type";
import {define, Definition} from "../property-definition";
import {InvalidTypeValueError} from "../../errors";
/**
* Type of dates.
*/
export class DateType extends Type<string, Date> {
deserialize(value: string | null | undefined): Date | null | undefined {
if (value === undefined) return undefined;
if (value === null) return null;
return new Date(value);
}
serialize(value: Date | null | undefined): string | null | undefined {
if (value === undefined) return undefined;
if (value === null) return null;
if (!(value instanceof Date))
throw new InvalidTypeValueError(this, value, "value must be a date");
if (isNaN(value?.valueOf())) return value?.toString();
return value?.toISOString();
}
hasChanged(
originalValue: Date | null | undefined,
currentValue: Date | null | undefined,
): boolean {
if (originalValue instanceof Date && currentValue instanceof Date) {
// Compare dates.
const originalTime = originalValue.getTime();
const currentTime = currentValue.getTime();
// The two values are not numbers, nothing has changed.
if (isNaN(originalTime) && isNaN(currentTime)) return false;
// Timestamps need to be exactly the same.
return originalValue.getTime() !== currentValue.getTime();
} else
// Compare undefined or null values.
return originalValue !== currentValue;
}
}
/**
* New date property definition.
*/
export function date(): Definition<string, Date> {
return define(new DateType());
}

View file

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

225
src/model/types/model.ts Normal file
View file

@ -0,0 +1,225 @@
import {Type} from "./type";
import {define, Definition} from "../property-definition";
import {
GenericModelManager,
IdentifierDefinition,
DeclaredModelManager,
ModelInstance,
ModelManager,
ModelShape,
SerializedModel,
} from "../model";
import {InvalidTypeValueError} from "../../errors";
/**
* 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>> {
/**
* Initialize a new model type of a Sharkitek model property.
* @param declaredModelManager Model manager.
*/
constructor(
protected declaredModelManager: DeclaredModelManager<T, Shape, Identifier>,
) {
super();
}
/**
* Resolve the defined model using the declared model, that can be defined lazily.
*/
get definedModel(): ModelManager<T, Shape, Identifier> {
return typeof this.declaredModelManager == "object"
? this.declaredModelManager
: this.declaredModelManager();
}
serialize(
value: ModelInstance<T, Shape, Identifier> | null | undefined,
): SerializedModel<T, Shape> | null | undefined {
if (value === undefined) return undefined;
if (value === null) return null;
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})`,
);
// Serializing the given model.
return this.definedModel.model(value).serialize();
}
deserialize(
value: SerializedModel<T, Shape> | null | undefined,
): ModelInstance<T, Shape, Identifier> | 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");
// Parse the given object in the new model.
return this.definedModel.parse(value);
}
serializeDiff(
value: ModelInstance<T, Shape, Identifier> | null | undefined,
): Partial<SerializedModel<T, Shape>> | null | undefined {
if (value === undefined) return undefined;
if (value === null) return null;
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})`,
);
// Serializing the given model.
return this.definedModel.model(value).serializeDiff();
}
resetDiff(
value: ModelInstance<T, Shape, Identifier> | null | undefined,
): void {
if (value === undefined) return;
if (value === null) return;
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})`,
);
// Reset diff of the given model.
this.definedModel.model(value).resetDiff();
}
hasChanged(
originalValue: ModelInstance<T, Shape, Identifier> | null | undefined,
currentValue: ModelInstance<T, Shape, Identifier> | null | undefined,
): boolean {
if (originalValue === undefined) return currentValue !== undefined;
if (originalValue === null) return currentValue !== null;
if (currentValue === undefined) return true; // Original value is not undefined.
if (currentValue === null) return true; // Original value is not null.
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})`,
);
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})`,
);
// If the current value is dirty, it has changed.
return this.definedModel.model(currentValue).isDirty();
}
serializedHasChanged(
originalValue: SerializedModel<T, Shape> | null | undefined,
currentValue: SerializedModel<T, Shape> | null | undefined,
): boolean {
if (originalValue === undefined) return currentValue !== undefined;
if (originalValue === null) return currentValue !== null;
if (currentValue === undefined) return true; // Original value is not undefined.
if (currentValue === null) return true; // Original value is not null.
if (typeof originalValue !== "object" || Array.isArray(originalValue))
throw new InvalidTypeValueError(
this,
originalValue,
"value must be an object",
);
if (typeof currentValue !== "object" || Array.isArray(currentValue))
throw new InvalidTypeValueError(
this,
currentValue,
"value must be an object",
);
// If any property has changed, the value has changed.
for (const property of this.definedModel.properties)
if (
property.definition.type.serializedHasChanged(
originalValue?.[property.name],
currentValue?.[property.name],
)
)
return true;
return false; // No change detected.
}
clone<Type extends ModelInstance<T, Shape, Identifier>>(
value: Type | null | undefined,
): Type {
// Handle NULL / undefined values.
if (!value) return super.clone(value);
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})`,
);
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.
* @param definedModel Model manager.
*/
export function model<
T extends object,
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));
}
/**
* 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;
}

View file

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

267
src/model/types/object.ts Normal file
View file

@ -0,0 +1,267 @@
import {Type} from "./type";
import {define, Definition, UnknownDefinition} from "../property-definition";
import {
ModelProperties,
ModelPropertiesValues,
ModelProperty,
ModelShape,
SerializedModel,
} from "../model";
import {InvalidTypeValueError} from "../../errors";
/**
* Type of a custom object.
*/
export class ObjectType<
Shape extends ModelShape<T>,
T extends object,
> extends Type<SerializedModel<T, Shape>, ModelPropertiesValues<T, Shape>> {
/**
* Defined properties.
*/
properties: ModelProperties<T, Shape>;
/**
* Initialize a new object type of a Sharkitek model property.
* @param shape
*/
constructor(readonly shape: Shape) {
super();
this.initProperties();
}
/**
* Initialize properties iterator from the object shape.
* @protected
*/
protected initProperties(): void {
// Build an array of model properties from the object shape.
this.properties = [];
for (const propertyName in this.shape) {
// For each property, build a model property object.
this.properties.push({
name: propertyName,
definition: this.shape[propertyName],
identifier: false,
} as ModelProperty<T, Shape>);
}
}
deserialize(
value: SerializedModel<T, Shape> | null | undefined,
): ModelPropertiesValues<T, Shape> | 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");
// Initialize an empty object.
const obj: Partial<ModelPropertiesValues<T, Shape>> = {};
for (const property of this.properties) {
// 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]);
}
return obj as ModelPropertiesValues<T, Shape>; // Returning serialized object.
}
serialize(
value: ModelPropertiesValues<T, Shape> | null | undefined,
): SerializedModel<T, Shape> | 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");
// Creating an empty serialized object.
const serializedObject: Partial<SerializedModel<T, Shape>> = {};
for (const property of this.properties) {
// For each property, adding it to the serialized object.
serializedObject[property.name] = property.definition.type.serialize(
// keyof Shape is a subset of keyof T.
value?.[property.name as keyof T],
);
}
return serializedObject as SerializedModel<T, Shape>; // Returning the serialized object.
}
serializeDiff(
value: ModelPropertiesValues<T, Shape> | null | undefined,
): Partial<SerializedModel<T, Shape>> | 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");
// Creating an empty serialized object.
const serializedObject: Partial<SerializedModel<T, Shape>> = {};
for (const property of this.properties) {
// For each property, adding it to the serialized object.
serializedObject[property.name] = property.definition.type.serializeDiff(
// keyof Shape is a subset of keyof T.
value?.[property.name as keyof T],
);
}
return serializedObject as SerializedModel<T, Shape>; // Returning the serialized object.
}
resetDiff(value: ModelPropertiesValues<T, Shape> | null | undefined) {
if (value === undefined) return;
if (value === null) return;
if (typeof value !== "object" || Array.isArray(value))
throw new InvalidTypeValueError(this, value, "value must be an object");
// For each property, reset its diff.
// keyof Shape is a subset of keyof T.
for (const property of this.properties)
property.definition.type.resetDiff(value?.[property.name as keyof T]);
}
hasChanged(
originalValue: ModelPropertiesValues<T, Shape> | null | undefined,
currentValue: ModelPropertiesValues<T, Shape> | null | undefined,
): boolean {
if (originalValue === undefined) return currentValue !== undefined;
if (originalValue === null) return currentValue !== null;
if (currentValue === undefined) return true; // Original value is not undefined.
if (currentValue === null) return true; // Original value is not null.
if (typeof originalValue !== "object" || Array.isArray(originalValue))
throw new InvalidTypeValueError(
this,
originalValue,
"value must be an object",
);
if (typeof currentValue !== "object" || Array.isArray(currentValue))
throw new InvalidTypeValueError(
this,
currentValue,
"value must be an object",
);
// If any property has changed, the value has changed.
for (const property of this.properties)
if (
property.definition.type.hasChanged(
originalValue?.[property.name as keyof T],
currentValue?.[property.name as keyof T],
)
)
return true;
return false; // No change detected.
}
serializedHasChanged(
originalValue: SerializedModel<T, Shape> | null | undefined,
currentValue: SerializedModel<T, Shape> | null | undefined,
): boolean {
if (originalValue === undefined) return currentValue !== undefined;
if (originalValue === null) return currentValue !== null;
if (currentValue === undefined) return true; // Original value is not undefined.
if (currentValue === null) return true; // Original value is not null.
if (typeof originalValue !== "object" || Array.isArray(originalValue))
throw new InvalidTypeValueError(
this,
originalValue,
"value must be an object",
);
if (typeof currentValue !== "object" || Array.isArray(currentValue))
throw new InvalidTypeValueError(
this,
currentValue,
"value must be an object",
);
// If any property has changed, the value has changed.
for (const property of this.properties)
if (
property.definition.type.serializedHasChanged(
originalValue?.[property.name],
currentValue?.[property.name],
)
)
return true;
return false; // No change detected.
}
clone<Type extends ModelPropertiesValues<T, Shape>>(
value: Type | null | undefined,
): Type {
// Handle NULL / undefined object.
if (!value) return super.clone(value);
if (typeof value !== "object" || Array.isArray(value))
throw new InvalidTypeValueError(this, value, "value must be an object");
// Initialize an empty object.
const cloned: Partial<ModelPropertiesValues<T, Shape>> = {};
for (const property of this.properties) {
// For each defined property, clone it.
cloned[property.name as keyof T] = property.definition.type.clone(
value?.[property.name],
);
}
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.
* @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>> {
return define(new ObjectType(shape));
}

28
src/model/types/string.ts Normal file
View file

@ -0,0 +1,28 @@
import {Type} from "./type";
import {define, Definition} from "../property-definition";
/**
* Type of any string value.
*/
export class StringType extends Type<string, string> {
deserialize(value: string | null | undefined): string | null | undefined {
if (value === undefined) return undefined;
if (value === null) return null;
return String(value);
}
serialize(value: string | null | undefined): string | null | undefined {
if (value === undefined) return undefined;
if (value === null) return null;
return String(value);
}
}
/**
* New string property definition.
*/
export function string(): Definition<string, string> {
return define(new StringType());
}

88
src/model/types/type.ts Normal file
View file

@ -0,0 +1,88 @@
/**
* Abstract class of a Sharkitek model property type.
*/
export abstract class Type<SerializedType, ModelType> {
/**
* Serialize the given value of a Sharkitek model property.
* @param value Value to serialize.
*/
abstract serialize(
value: ModelType | null | undefined,
): SerializedType | null | undefined;
/**
* Deserialize the given value of a serialized Sharkitek model.
* @param value Value to deserialize.
*/
abstract deserialize(
value: SerializedType | null | undefined,
): ModelType | null | undefined;
/**
* Serialize the given value only if it has changed.
* @param value Value to deserialize.
*/
serializeDiff(
value: ModelType | null | undefined,
): Partial<SerializedType> | null | undefined {
return this.serialize(value); // By default, nothing changes.
}
/**
* Reset the difference between the original value and the current one.
* @param value Value for which reset diff data.
*/
resetDiff(
// eslint-disable-next-line @typescript-eslint/no-unused-vars
value: ModelType | null | undefined,
): void {
// By default, nothing to do.
}
/**
* Determine if the value has changed.
* @param originalValue Original value.
* @param currentValue Current value.
*/
hasChanged(
originalValue: ModelType | null | undefined,
currentValue: ModelType | null | undefined,
): boolean {
return originalValue !== currentValue;
}
/**
* Determine if the serialized value has changed.
* @param originalValue Original serialized value.
* @param currentValue Current serialized value.
*/
serializedHasChanged(
originalValue: SerializedType | null | undefined,
currentValue: SerializedType | null | undefined,
): boolean {
return originalValue !== currentValue;
}
/**
* Clone the provided value.
* @param value The to clone.
*/
clone<T extends ModelType>(value: T | null | undefined): T {
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;
}
}

10
src/utils.ts Normal file
View file

@ -0,0 +1,10 @@
/**
* Type definition of a class constructor.
*/
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;

View file

@ -1,173 +0,0 @@
import {
SArray,
SDecimal,
SModel,
SNumeric,
SString,
SDate,
SBool,
Model,
ModelDefinition,
SDefine, ModelIdentifier
} from "../src";
/**
* Another test model.
*/
class Author extends Model<Author>
{
name: string;
firstName: string;
email: string;
createdAt: Date;
active: boolean = true;
protected SDefinition(): ModelDefinition<Author>
{
return {
name: SDefine(SString),
firstName: SDefine(SString),
email: SDefine(SString),
createdAt: SDefine(SDate),
active: SDefine(SBool),
};
}
constructor(name: string = undefined, firstName: string = undefined, email: string = undefined, createdAt: Date = undefined)
{
super();
this.name = name;
this.firstName = firstName;
this.email = email;
this.createdAt = createdAt;
}
}
/**
* A test model.
*/
class Article extends Model<Article>
{
id: number;
title: string;
authors: Author[] = [];
text: string;
evaluation: number;
protected SIdentifier(): ModelIdentifier<Article>
{
return "id";
}
protected SDefinition(): ModelDefinition<Article>
{
return {
id: SDefine(SNumeric),
title: SDefine(SString),
authors: SDefine(SArray(SModel(Author))),
text: SDefine(SString),
evaluation: SDefine(SDecimal),
};
}
}
it("deserialize", () => {
expect((new Article()).deserialize({
id: 1,
title: "this is a test",
authors: [
{ name: "DOE", firstName: "John", email: "test@test.test", createdAt: "2022-08-07T08:47:01.000Z", active: true, },
{ name: "TEST", firstName: "Another", email: "another@test.test", createdAt: "2022-09-07T18:32:55.000Z", active: false, },
],
text: "this is a long test.",
evaluation: "25.23",
}).serialize()).toStrictEqual({
id: 1,
title: "this is a test",
authors: [
{ name: "DOE", firstName: "John", email: "test@test.test", createdAt: "2022-08-07T08:47:01.000Z", active: true, },
{ name: "TEST", firstName: "Another", email: "another@test.test", createdAt: "2022-09-07T18:32:55.000Z", active: false, },
],
text: "this is a long test.",
evaluation: "25.23",
});
});
it("create and check state then serialize", () => {
const now = new Date();
const article = new Article();
article.id = 1;
article.title = "this is a test";
article.authors = [
new Author("DOE", "John", "test@test.test", now),
];
article.text = "this is a long test.";
article.evaluation = 25.23;
expect(article.isNew()).toBeTruthy();
expect(article.getIdentifier()).toStrictEqual(1);
expect(article.serialize()).toStrictEqual({
id: 1,
title: "this is a test",
authors: [
{ name: "DOE", firstName: "John", email: "test@test.test", createdAt: now.toISOString(), active: true, },
],
text: "this is a long test.",
evaluation: "25.23",
});
});
it("deserialize then save", () => {
const article = (new Article()).deserialize({
id: 1,
title: "this is a test",
authors: [
{ name: "DOE", firstName: "John", email: "test@test.test", createdAt: new Date(), active: true, },
{ name: "TEST", firstName: "Another", email: "another@test.test", createdAt: new Date(), active: false, },
],
text: "this is a long test.",
evaluation: "25.23",
});
expect(article.isNew()).toBeFalsy();
expect(article.isDirty()).toBeFalsy();
expect(article.evaluation).toStrictEqual(25.23);
article.text = "Modified text.";
expect(article.isDirty()).toBeTruthy();
expect(article.save()).toStrictEqual({
id: 1,
text: "Modified text.",
});
});
it("save with modified submodels", () => {
const article = (new Article()).deserialize({
id: 1,
title: "this is a test",
authors: [
{ name: "DOE", firstName: "John", email: "test@test.test", createdAt: new Date(), active: true, },
{ name: "TEST", firstName: "Another", email: "another@test.test", createdAt: new Date(), active: false, },
],
text: "this is a long test.",
evaluation: "25.23",
});
article.authors = article.authors.map((author) => {
author.name = "TEST";
return author;
});
expect(article.save()).toStrictEqual({
id: 1,
authors: [
{ name: "TEST", },
{}, //{ name: "TEST", firstName: "Another", email: "another@test.test" },
],
});
});

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

24
tests/errors.test.ts Normal file
View file

@ -0,0 +1,24 @@
import {describe, expect, it} from "vitest";
import {InvalidTypeValueError, TypeError} from "../src/errors";
import {s} from "../src/library";
describe("errors", () => {
it("tests type error", () => {
expect(new TypeError(s.property.string().type).message).toBe(
"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", () => {
expect(
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");
});
});

587
tests/model.test.ts Normal file
View file

@ -0,0 +1,587 @@
import {describe, expect, it} from "vitest";
import {circular, defineModel, s} from "../src/library";
/**
* Test class of an account.
*/
class Account {
id: number;
createdAt: Date;
name: string;
email: string;
active: boolean;
}
const AccountModel = s.defineModel({
Class: Account,
identifier: "id",
properties: {
id: s.property.numeric(),
createdAt: s.property.date(),
name: s.property.string(),
email: s.property.string(),
active: s.property.boolean(),
},
});
/**
* Test class of an article.
*/
class Article {
id: number;
title: string;
authors: Account[];
text: string;
evaluation: number;
tags: {name: string}[];
comments: ArticleComment[];
}
const ArticleModel = s.defineModel({
Class: Article,
identifier: "id",
properties: {
id: s.property.numeric(),
title: s.property.string(),
authors: s.property.array(s.property.model(() => AccountModel)),
text: s.property.string(),
evaluation: s.property.decimal(),
tags: s.property.array(s.property.object({name: s.property.string()})),
comments: s.property.array(s.property.model(() => ArticleCommentModel)),
},
});
/**
* Test class of a comment on an article.
*/
class ArticleComment {
id: number;
article?: Article;
author: Account;
message: string;
}
const ArticleCommentModel = s.defineModel({
Class: ArticleComment,
identifier: "id",
properties: {
id: s.property.numeric(),
article: s.property.model(circular<Article>(() => ArticleModel)),
author: s.property.model(() => AccountModel),
message: s.property.string(),
},
});
/**
* Get a test account instance.
*/
function getTestAccount(): Account {
const account = new Account();
account.id = 52;
account.createdAt = new Date();
account.name = "John Doe";
account.email = "john@doe.test";
account.active = true;
return account;
}
function getTestArticle(): Article {
const article = new Article();
article.id = 1;
article.title = "this is a test";
article.text = "this is a long test.";
article.evaluation = 25.23;
article.tags = [{name: "test"}, {name: "foo"}];
article.authors = [getTestAccount()];
article.comments = [];
return article;
}
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", () => {
const article = getTestArticle();
const newModel = ArticleModel.model(article);
expect(newModel.instance).toBe(article);
});
it("gets a model state from its instance", () => {
const article = getTestArticle();
expect(ArticleModel.model(article).isNew()).toBeTruthy();
expect(ArticleModel.model(article).isDirty()).toBeFalsy();
});
it("gets a model identifier value", () => {
const article = getTestArticle();
expect(ArticleModel.model(article).getIdentifier()).toBe(1);
});
it("gets a model composite identifier value", () => {
class CompositeModel {
static model = s.defineModel({
Class: CompositeModel,
properties: {
firstId: s.property.numeric(),
secondId: s.property.numeric(),
label: s.property.string(),
},
identifier: ["firstId", "secondId"],
});
firstId: number;
secondId: number;
label: string;
}
expect(
CompositeModel.model
.model(
Object.assign(new CompositeModel(), {
firstId: 5,
secondId: 6,
label: "test",
}),
)
.getIdentifier(),
).toStrictEqual([5, 6]);
});
it("checks model dirtiness when altered, then reset diff", () => {
const article = getTestArticle();
expect(ArticleModel.model(article).isDirty()).toBeFalsy();
article.title = "new title";
expect(ArticleModel.model(article).isDirty()).toBeTruthy();
ArticleModel.model(article).resetDiff();
expect(ArticleModel.model(article).isDirty()).toBeFalsy();
});
it("deserializes a model from a serialized form", () => {
const expectedArticle = Object.assign(new Article(), {
id: 1,
title: "this is a test",
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(), {
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.",
evaluation: 8.52,
tags: [{name: "test"}, {name: "foo"}],
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",
}),
],
});
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",
},
],
});
const deserializedArticleProperties =
ArticleModel.model(deserializedArticle).getInstanceProperties();
delete deserializedArticleProperties.authors[0]._sharkitek;
delete deserializedArticleProperties.authors[1]._sharkitek;
delete deserializedArticleProperties.comments[0]._sharkitek;
delete (deserializedArticleProperties.comments[0].author as any)._sharkitek;
const expectedArticleProperties =
ArticleModel.model(expectedArticle).getInstanceProperties();
delete expectedArticleProperties.authors[0]._sharkitek;
delete expectedArticleProperties.authors[1]._sharkitek;
delete expectedArticleProperties.comments[0]._sharkitek;
delete (expectedArticleProperties.comments[0].author as any)._sharkitek;
expect(deserializedArticleProperties).toEqual(expectedArticleProperties);
});
it("serializes an initialized model", () => {
const article = getTestArticle();
expect(ArticleModel.model(article).serialize()).toEqual({
id: 1,
title: "this is a test",
text: "this is a long test.",
evaluation: "25.23",
tags: [{name: "test"}, {name: "foo"}],
authors: [
{
id: 52,
createdAt: article.authors[0].createdAt.toISOString(),
name: "John Doe",
email: "john@doe.test",
active: true,
},
],
comments: [],
});
});
it("deserializes, changes and patches", () => {
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",
},
],
});
deserializedArticle.text = "A new text for a new life!";
expect(ArticleModel.model(deserializedArticle).patch()).toStrictEqual({
id: 1,
text: "A new text for a new life!",
});
deserializedArticle.evaluation = 5.24;
expect(ArticleModel.model(deserializedArticle).patch()).toStrictEqual({
id: 1,
evaluation: "5.24",
});
});
it("patches with modified submodels", () => {
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",
},
],
});
deserializedArticle.authors[1].active = true;
expect(ArticleModel.model(deserializedArticle).patch()).toStrictEqual({
id: 1,
authors: [{id: 52}, {id: 4, active: true}],
});
deserializedArticle.comments[0].author.name = "Johnny";
expect(ArticleModel.model(deserializedArticle).patch()).toStrictEqual({
id: 1,
comments: [
{
id: 542,
author: {
id: 52,
name: "Johnny",
},
},
],
});
});
it("deserializes and patches with fields that are not properties", () => {
class TestModel {
static model = defineModel({
Class: TestModel,
properties: {
id: s.property.numeric(),
label: s.property.string(),
},
identifier: "id",
});
id: number;
label: string;
notAProperty: {hello: string} = {hello: "world"};
}
const deserializedModel = TestModel.model.parse({
id: 5,
label: "testing",
});
expect(deserializedModel.id).toBe(5);
expect(deserializedModel.label).toBe("testing");
expect(deserializedModel.notAProperty?.hello).toBe("world");
const clonedDeserializedModel = TestModel.model
.model(deserializedModel)
.clone();
deserializedModel.label = "new!";
expect(TestModel.model.model(deserializedModel).patch()).toStrictEqual({
id: 5,
label: "new!",
});
deserializedModel.notAProperty.hello = "monster";
expect(TestModel.model.model(deserializedModel).patch()).toStrictEqual({
id: 5,
});
expect(TestModel.model.model(deserializedModel).serialize()).toStrictEqual({
id: 5,
label: "new!",
});
expect(
TestModel.model.model(clonedDeserializedModel).serialize(),
).toStrictEqual({id: 5, label: "testing"});
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

@ -0,0 +1,440 @@
import {describe, expect, test} from "vitest";
import {ArrayType, InvalidTypeValueError, s} from "../../../src/library";
class TestModel {
id: number;
name: string;
price: number;
}
describe("array type", () => {
const testModel = s.defineModel({
Class: TestModel,
properties: {
id: s.property.numeric(),
name: s.property.string(),
price: s.property.decimal(),
},
identifier: "id",
});
test("definition", () => {
const arrayType = s.property.array(s.property.model(testModel));
expect(arrayType.type).toBeInstanceOf(ArrayType);
});
const testProperty = s.property.array(s.property.decimal());
describe("serialize", () => {
test("serialize", () => {
expect(testProperty.type.serialize([12.547, 8, -52.11])).toEqual([
"12.547",
"8",
"-52.11",
]);
expect(testProperty.type.serialize(null)).toBe(null);
expect(testProperty.type.serialize(undefined)).toBe(undefined);
});
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(undefined, undefined)).toBeFalsy();
expect(testProperty.type.hasChanged(null, undefined)).toBeTruthy();
expect(testProperty.type.hasChanged(undefined, null)).toBeTruthy();
expect(
testProperty.type.hasChanged(null, [12.547, 8, -52.11]),
).toBeTruthy();
expect(
testProperty.type.hasChanged(undefined, [12.547, 8, -52.11]),
).toBeTruthy();
expect(
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();
});
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(undefined, undefined),
).toBeFalsy();
expect(
testProperty.type.serializedHasChanged(null, undefined),
).toBeTruthy();
expect(
testProperty.type.serializedHasChanged(undefined, null),
).toBeTruthy();
expect(
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();
});
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 = [
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}]);
s.property
.array(s.property.model(testModel))
.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(null);
});
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 clonedPropertyValue = testProperty.type.clone(propertyValue);
expect(clonedPropertyValue).not.toBe(propertyValue);
expect(clonedPropertyValue).toEqual(propertyValue);
}
{
// Test that values are cloned recursively.
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,
];
// The arrays are different.
const clonedPropertyValue = s.property
.array(s.property.model(testModel))
.type.clone(propertyValue);
expect(clonedPropertyValue).not.toBe(propertyValue);
// Array values must be different objects but have the same values.
expect(clonedPropertyValue[0]).not.toBe(propertyValue[0]);
expect(clonedPropertyValue[1]).not.toBe(propertyValue[1]);
expect(
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(null)).toBe(null);
});
test("invalid parameters", () => {
expect(() => testProperty.type.clone({} as any)).toThrowError(
InvalidTypeValueError,
);
});
});
test("applyPatch", () => {
{
// 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

@ -0,0 +1,175 @@
import {describe, expect, test} from "vitest";
import {BooleanType, s} from "../../../src/library";
describe("boolean type", () => {
test("definition", () => {
{
const booleanType = s.property.boolean();
expect(booleanType.type).toBeInstanceOf(BooleanType);
}
{
const boolType = s.property.bool();
expect(boolType.type).toBeInstanceOf(BooleanType);
}
});
describe("serialize", () => {
test("serialize", () => {
expect(s.property.boolean().type.serialize(false)).toBe(false);
expect(s.property.boolean().type.serialize(null)).toBe(null);
expect(s.property.boolean().type.serialize(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(null, null)).toBeFalsy();
expect(
s.property.boolean().type.hasChanged(undefined, undefined),
).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(undefined, false),
).toBeTruthy();
expect(s.property.boolean().type.hasChanged(false, null)).toBeTruthy();
expect(
s.property.boolean().type.hasChanged(false, undefined),
).toBeTruthy();
});
test("invalid parameters", () => {
expect(
s.property.boolean().type.hasChanged({} as any, {} as any),
).toBeTruthy();
expect(
s.property.boolean().type.hasChanged(false as any, false as any),
).toBeFalsy();
});
});
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(undefined);
s.property.boolean().type.resetDiff(null);
});
test("resetDiff", () => {
expect(() =>
s.property.boolean().type.resetDiff({} as any),
).not.toThrow();
});
});
describe("clone", () => {
test("invalid parameters", () => {
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

@ -0,0 +1,232 @@
import {describe, expect, test} from "vitest";
import {DateType, InvalidTypeValueError, s} from "../../../src/library";
describe("date type", () => {
const testDate = new Date();
test("definition", () => {
const dateType = s.property.date();
expect(dateType.type).toBeInstanceOf(DateType);
});
describe("serialize", () => {
test("serialize", () => {
expect(s.property.date().type.serialize(testDate)).toBe(
testDate.toISOString(),
);
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(undefined)).toBe(undefined);
});
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(undefined, undefined),
).toBeFalsy();
expect(s.property.date().type.hasChanged(null, undefined)).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(undefined, testDate),
).toBeTruthy();
expect(s.property.date().type.hasChanged(testDate, null)).toBeTruthy();
expect(
s.property.date().type.hasChanged(new Date(NaN), null),
).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();
});
test("invalid parameters", () => {
expect(
s.property.date().type.hasChanged({} as any, {} as any),
).toBeTruthy();
expect(
s.property.date().type.hasChanged(false as any, false as any),
).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(undefined);
s.property.date().type.resetDiff(null);
});
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 clonedPropertyValue = s.property.date().type.clone(propertyValue);
expect(clonedPropertyValue).not.toBe(propertyValue);
expect(clonedPropertyValue).toEqual(propertyValue);
});
test("applyPatch", () => {
expect(
s.property
.date()
.type.applyPatch(new Date("2022-02-22"), testDate.toISOString(), false)
?.getTime(),
).toBe(testDate.getTime());
expect(
s.property
.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

@ -0,0 +1,164 @@
import {describe, expect, test} from "vitest";
import {DecimalType, InvalidTypeValueError, s} from "../../../src/library";
describe("decimal type", () => {
test("decimal type definition", () => {
const decimalType = s.property.decimal();
expect(decimalType.type).toBeInstanceOf(DecimalType);
});
describe("serialize", () => {
test("serialize", () => {
expect(s.property.decimal().type.serialize(5.257)).toBe("5.257");
expect(s.property.decimal().type.serialize(null)).toBe(null);
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);
});
test("invalid parameters", () => {
expect(s.property.decimal().type.deserialize({} as any)).toBe(NaN);
expect(s.property.decimal().type.deserialize({} as any)).toBe(NaN);
});
});
describe("serializeDiff", () => {
test("serializeDiff", () => {
expect(s.property.decimal().type.serializeDiff(542)).toBe("542");
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(undefined);
s.property.decimal().type.resetDiff(null);
});
test("invalid parameters", () => {
expect(() =>
s.property.decimal().type.resetDiff({} as any),
).not.toThrow();
});
});
describe("hasChanged", () => {
test("hasChanged", () => {
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({});
});
});
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

@ -0,0 +1,582 @@
import {describe, expect, test} from "vitest";
import {InvalidTypeValueError, ModelType, s} from "../../../src/library";
class TestModel {
id: number;
name: string;
price: number;
}
describe("model type", () => {
const testModel = s.defineModel({
Class: TestModel,
properties: {
id: s.property.numeric(),
name: s.property.string(),
price: s.property.decimal(),
},
identifier: "id",
});
test("definition", () => {
const modelType = s.property.model(testModel);
expect(modelType.type).toBeInstanceOf(ModelType);
});
describe("serialize", () => {
test("serialize", () => {
const testModelInstance = testModel.model(
Object.assign(new TestModel(), {
id: 1,
name: "test",
price: 12.548777,
}),
).instance;
expect(
s.property.model(testModel).type.serialize(testModelInstance),
).toEqual({id: 1, name: "test", price: "12.548777"});
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(undefined)).toEqual(
undefined,
);
});
test("invalid parameters", () => {
expect(() =>
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;
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;
testModelInstance.price = 12.548778;
expect(
s.property
.model(testModel)
.type.hasChanged(testModelInstance, testModelInstance),
).toBeTruthy();
}
expect(
s.property.model(testModel).type.hasChanged(null, null),
).toBeFalsy();
expect(
s.property.model(testModel).type.hasChanged(undefined, undefined),
).toBeFalsy();
expect(
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();
});
test("invalid parameters", () => {
expect(() =>
s.property.model(testModel).type.hasChanged(5 as any, 5 as any),
).toThrowError(InvalidTypeValueError);
expect(() =>
s.property
.model(testModel)
.type.hasChanged(
testModel.model(new TestModel()).instance,
[] as any,
),
).toThrowError(InvalidTypeValueError);
expect(() =>
s.property
.model(testModel)
.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;
testModelInstance.price = 555.555;
expect(testModel.model(testModelInstance).serializeDiff()).toEqual({
id: 1,
price: "555.555",
});
s.property.model(testModel).type.resetDiff(testModelInstance);
expect(testModel.model(testModelInstance).serializeDiff()).toEqual({
id: 1,
});
s.property.model(testModel).type.resetDiff(undefined);
s.property.model(testModel).type.resetDiff(null);
});
test("invalid parameters", () => {
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;
const clonedModelInstance = s.property
.model(testModel)
.type.clone(testModelInstance);
expect(clonedModelInstance).not.toBe(testModelInstance);
expect(
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(null)).toBe(null);
});
test("invalid parameters", () => {
expect(() =>
s.property.model(testModel).type.clone(5 as any),
).toThrowError(InvalidTypeValueError);
expect(() =>
s.property.model(testModel).type.clone([] as any),
).toThrowError(InvalidTypeValueError);
expect(() =>
s.property.model(testModel).type.clone(new (class {})() as any),
).toThrowError(InvalidTypeValueError);
});
});
test("applyPatch", () => {
{
// Apply a patch with undefined / NULL values.
expect(
s.property.model(testModel).type.applyPatch(
testModel.model(
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();
}
{
// Invalid patch.
expect(() =>
s.property.model(testModel).type.applyPatch(
testModel.model(
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

@ -0,0 +1,164 @@
import {describe, expect, test} from "vitest";
import {InvalidTypeValueError, NumericType, s} from "../../../src/library";
describe("numeric type", () => {
test("definition", () => {
const numericType = s.property.numeric();
expect(numericType.type).toBeInstanceOf(NumericType);
});
describe("serialize", () => {
test("serialize", () => {
expect(s.property.numeric().type.serialize(5.257)).toBe(5.257);
expect(s.property.numeric().type.serialize(null)).toBe(null);
expect(s.property.numeric().type.serialize(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(null, null)).toBeFalsy();
expect(
s.property.numeric().type.hasChanged(undefined, undefined),
).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(undefined, 5.257),
).toBeTruthy();
expect(s.property.numeric().type.hasChanged(5.257, null)).toBeTruthy();
expect(
s.property.numeric().type.hasChanged(5.257, undefined),
).toBeTruthy();
});
test("invalid parameters", () => {
expect(
s.property.numeric().type.hasChanged({} as any, {} as any),
).toBeTruthy();
expect(
s.property.numeric().type.hasChanged(false as any, false as any),
).toBeFalsy();
});
});
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(undefined);
s.property.numeric().type.resetDiff(null);
});
test("invalid parameters", () => {
expect(() =>
s.property.numeric().type.resetDiff({} as any),
).not.toThrow();
});
});
describe("clone", () => {
test("invalid parameters", () => {
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

@ -0,0 +1,323 @@
import {describe, expect, test} from "vitest";
import {
InvalidTypeValueError,
NumericType,
ObjectType,
s,
StringType,
} from "../../../src/library";
describe("object type", () => {
test("definition", () => {
const objectType = s.property.object({
test: s.property.string(),
another: s.property.numeric(),
});
expect(objectType.type).toBeInstanceOf(ObjectType);
expect((objectType.type as any).properties).toHaveLength(2);
for (const property of (objectType.type as any).properties) {
// Check all object properties.
if (property.name == "test")
expect(property.definition.type).toBeInstanceOf(StringType);
else if (property.name == "another")
expect(property.definition.type).toBeInstanceOf(NumericType);
else expect.unreachable();
}
});
const testProperty = s.property.object({
test: s.property.string(),
another: s.property.decimal(),
});
describe("serialize", () => {
test("serialize", () => {
expect(
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(undefined)).toEqual(undefined);
});
test("invalid parameters", () => {
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(undefined, undefined)).toBeFalsy();
expect(testProperty.type.hasChanged(null, undefined)).toBeTruthy();
expect(testProperty.type.hasChanged(undefined, null)).toBeTruthy();
expect(
testProperty.type.hasChanged(null, {test: "test", another: 12.548777}),
).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();
});
test("invalid parameters", () => {
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(undefined, undefined),
).toBeFalsy();
expect(
testProperty.type.serializedHasChanged(null, 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();
});
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(null);
});
test("invalid parameters", () => {
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);
expect(clonedPropertyValue).not.toBe(propertyValue);
expect(clonedPropertyValue).toEqual(propertyValue);
}
{
// Test that values are cloned in a different object.
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).toEqual(propertyValue);
expect(clonedPropertyValue.arr).not.toBe(propertyValue.arr);
expect(clonedPropertyValue.arr).toEqual(propertyValue.arr);
}
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,
);
});
});
test("applyPatch", () => {
{
// Apply a patch with undefined / NULL values.
expect(
testProperty.type.applyPatch(
{test: "test", another: 12.548777},
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

@ -0,0 +1,162 @@
import {describe, expect, test} from "vitest";
import {s, StringType} from "../../../src/library";
describe("string type", () => {
test("definition", () => {
const stringType = s.property.string();
expect(stringType.type).toBeInstanceOf(StringType);
});
describe("serialize", () => {
test("serialize", () => {
expect(s.property.string().type.serialize("test")).toBe("test");
expect(s.property.string().type.serialize(null)).toBe(null);
expect(s.property.string().type.serialize(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(null, null)).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(undefined, null)).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("test", null)).toBeTruthy();
expect(
s.property.string().type.hasChanged("test", undefined),
).toBeTruthy();
});
test("invalid parameters", () => {
expect(
s.property.string().type.hasChanged({} as any, {} as any),
).toBeTruthy();
expect(
s.property.string().type.hasChanged(false as any, false as any),
).toBeFalsy();
});
});
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(undefined);
s.property.string().type.resetDiff(null);
});
test("invalid parameters", () => {
expect(() => s.property.string().type.resetDiff({} as any)).not.toThrow();
});
});
test("clone", () => {
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

@ -1,25 +1,27 @@
{
"ts-node": {
"compilerOptions": {
"module": "CommonJS"
"module": "ESNext",
"types": ["node"]
}
},
"files": ["src/index.ts"],
"compilerOptions": {
"outDir": "./lib/",
"incremental": true,
"sourceMap": true,
"noImplicitAny": true,
"noImplicitThis": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"resolveJsonModule": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"module": "ES6",
"moduleResolution": "Node",
"target": "ES6",
"lib": [
"ESNext",
"DOM"
]
"moduleResolution": "Bundler",
"lib": ["ESNext", "DOM"]
}
}

27
vite.config.ts Normal file
View file

@ -0,0 +1,27 @@
import {defineConfig, UserConfig} from "vite";
import dts from "vite-plugin-dts";
// https://vitejs.dev/config/
export default defineConfig((): UserConfig => {
return {
build: {
outDir: "lib",
sourcemap: true,
minify: "esbuild",
lib: {
entry: "src/library.ts",
formats: ["es"],
fileName: "index",
},
},
plugins: [
dts({
insertTypesEntry: true,
rollupTypes: true,
exclude: ["node_modules"],
}),
],
};
});

4254
yarn.lock Normal file

File diff suppressed because it is too large Load diff