Compare commits
46 commits
Author | SHA1 | Date | |
---|---|---|---|
bf89dc00fe | |||
f238499f06 | |||
d296658f64 | |||
e9cca58e4e | |||
de27fb7837 | |||
18a162c6d7 | |||
784f527a9e | |||
a4c1c88138 | |||
38c87249b1 | |||
72417dd350 | |||
5f1e2709bb | |||
ed1bfd464a | |||
97a3c18082 | |||
ecd8852afa | |||
75b7b35dd6 | |||
2d86f0fa1a | |||
7707789bbf | |||
fbd2763ea6 | |||
40b348862a | |||
1af46c0aaf | |||
8b1a1dabcf | |||
7e86e6fe86 | |||
2debdf5e46 | |||
8afce56b9e | |||
f5502109ac | |||
6eee1b709e | |||
8f8dafed5b | |||
ff9cb91f73 | |||
e373efdd0a | |||
4eb8b7d3bc | |||
576338fa62 | |||
62e62f962e | |||
22bc42acba | |||
6af0da6b55 | |||
6a14623355 | |||
3e291d6bd5 | |||
72df9f6453 | |||
e43e27e2e1 | |||
498d25a909 | |||
9cb2bf1e5c | |||
d96c39d079 | |||
b6411c9401 | |||
f512675906 | |||
bb5eed5162 | |||
eb920a3d6d | |||
0876c59c98 |
58 changed files with 9944 additions and 943 deletions
16
.forgejo/workflows/test.yaml
Normal file
16
.forgejo/workflows/test.yaml
Normal 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
2
.gitignore
vendored
|
@ -13,5 +13,3 @@ lib/
|
|||
yarn-error.log
|
||||
.pnp*
|
||||
node_modules/
|
||||
|
||||
yarn.lock
|
||||
|
|
5
.prettierrc
Normal file
5
.prettierrc
Normal file
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"useTabs": true,
|
||||
"trailingComma": "all",
|
||||
"bracketSpacing": false
|
||||
}
|
2
LICENSE
2
LICENSE
|
@ -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
364
README.md
|
@ -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" }
|
||||
|
||||
```
|
||||
|
|
38
build.ts
38
build.ts
|
@ -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
23
eslint.config.js
Normal 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",
|
||||
},
|
||||
},
|
||||
]);
|
|
@ -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
37
logo.svg
Normal 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 |
66
package.json
66
package.json
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -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.
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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();
|
|
@ -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();
|
|
@ -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();
|
|
@ -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);
|
||||
}
|
|
@ -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();
|
|
@ -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();
|
|
@ -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
3
src/errors/index.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
export * from "./sharkitek-error";
|
||||
export * from "./type-error";
|
||||
export * from "./invalid-type-value-error";
|
18
src/errors/invalid-type-value-error.ts
Normal file
18
src/errors/invalid-type-value-error.ts
Normal 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`);
|
||||
}
|
||||
}
|
4
src/errors/sharkitek-error.ts
Normal file
4
src/errors/sharkitek-error.ts
Normal file
|
@ -0,0 +1,4 @@
|
|||
/**
|
||||
* A Sharkitek error.
|
||||
*/
|
||||
export class SharkitekError extends Error {}
|
16
src/errors/type-error.ts
Normal file
16
src/errors/type-error.ts
Normal 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}` : ""}`,
|
||||
);
|
||||
}
|
||||
}
|
14
src/index.ts
14
src/index.ts
|
@ -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
5
src/library.ts
Normal 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
77
src/model/builder.ts
Normal 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
16
src/model/index.ts
Normal 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
667
src/model/model.ts
Normal 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
12
src/model/properties.ts
Normal 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";
|
35
src/model/property-definition.ts
Normal file
35
src/model/property-definition.ts
Normal 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
187
src/model/types/array.ts
Normal 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));
|
||||
}
|
37
src/model/types/boolean.ts
Normal file
37
src/model/types/boolean.ts
Normal 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
51
src/model/types/date.ts
Normal 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());
|
||||
}
|
31
src/model/types/decimal.ts
Normal file
31
src/model/types/decimal.ts
Normal 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
264
src/model/types/map.ts
Normal 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
225
src/model/types/model.ts
Normal 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;
|
||||
}
|
35
src/model/types/numeric.ts
Normal file
35
src/model/types/numeric.ts
Normal 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
267
src/model/types/object.ts
Normal 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
28
src/model/types/string.ts
Normal 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
88
src/model/types/type.ts
Normal 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
10
src/utils.ts
Normal 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;
|
|
@ -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
35
tests/builder.test.ts
Normal 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
24
tests/errors.test.ts
Normal 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
587
tests/model.test.ts
Normal 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"}],
|
||||
});
|
||||
});
|
||||
});
|
440
tests/model/types/array.test.ts
Normal file
440
tests/model/types/array.test.ts
Normal 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",
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
175
tests/model/types/boolean.test.ts
Normal file
175
tests/model/types/boolean.test.ts
Normal 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();
|
||||
});
|
||||
});
|
232
tests/model/types/date.test.ts
Normal file
232
tests/model/types/date.test.ts
Normal 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();
|
||||
});
|
||||
});
|
164
tests/model/types/decimal.test.ts
Normal file
164
tests/model/types/decimal.test.ts
Normal 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();
|
||||
});
|
||||
});
|
283
tests/model/types/map.test.ts
Normal file
283
tests/model/types/map.test.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
582
tests/model/types/model.test.ts
Normal file
582
tests/model/types/model.test.ts
Normal 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",
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
164
tests/model/types/numeric.test.ts
Normal file
164
tests/model/types/numeric.test.ts
Normal 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();
|
||||
});
|
||||
});
|
323
tests/model/types/object.test.ts
Normal file
323
tests/model/types/object.test.ts
Normal 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",
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
162
tests/model/types/string.test.ts
Normal file
162
tests/model/types/string.test.ts
Normal 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();
|
||||
});
|
||||
});
|
|
@ -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
27
vite.config.ts
Normal 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"],
|
||||
}),
|
||||
],
|
||||
};
|
||||
});
|
Loading…
Add table
Reference in a new issue