Compare commits

..

No commits in common. "main" and "v3.0.2" have entirely different histories.
main ... v3.0.2

58 changed files with 987 additions and 9817 deletions

View file

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

2
.gitignore vendored
View file

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

View file

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

277
README.md
View file

@ -19,194 +19,72 @@
</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" />
<img alt="Version 3.0.2" src="https://img.shields.io/badge/version-3.0.2-blue" />
</p>
## Introduction
Sharkitek is a lightweight Javascript / TypeScript library designed to ease development of models.
```shell
yarn add @sharkitek/core
```
Sharkitek is a Javascript / TypeScript library designed to ease development of client-side 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`, `parse`, `patch` or `serializeDiff`.
Then, you can use the defined methods like `serialize`, `deserialize`, `save` or `serializeDiff`.
```typescript
class Example {
id: number;
name: string;
}
const ExampleModel = defineModel({
Class: Example,
properties: {
class Example extends s.model({
id: s.property.numeric(),
name: s.property.string(),
},
identifier: "id",
});
})
{
}
```
## Quick start
## Examples
**Note**: we usually define our models in a `{ModelName}Model` variable next to the model's class.
### Model definition
### Simple model definition
```typescript
/**
* A person.
*/
class Person {
id: number;
name: string;
email: string;
createdAt: Date;
active: boolean = true;
}
/**
* A person model manager.
*/
const PersonModel = defineModel({
Class: Person,
properties: {
class Person extends s.model({
id: s.property.numeric(),
name: s.property.string(),
firstName: s.property.string(),
email: s.property.string(),
createdAt: s.property.date(),
active: s.property.boolean(),
},
identifier: "id",
});
}, "id")
{
active: boolean = true;
}
```
```typescript
/**
* An article.
*/
class Article {
class Article extends s.model({
id: s.property.numeric(),
title: s.property.string(),
authors: s.property.array(s.property.model(Author)),
text: s.property.string(),
evaluation: s.property.decimal(),
tags: s.property.array(
s.property.object({
name: s.property.string(),
})
),
}, "id")
{
id: number;
title: string;
authors: Person[] = [];
authors: Author[] = [];
text: string;
evaluation: number;
tags: {
name: string;
}[];
}
/**
* An article model manager.
*/
const ArticleModel = defineModel({
Class: Article,
properties: {
id: s.property.numeric(),
title: s.property.string(),
authors: s.property.array(s.property.model(PersonModel)),
text: s.property.string(),
evaluation: s.property.decimal(),
tags: s.property.array(
s.property.object({
name: s.property.string(),
}),
),
},
identifier: "id",
});
```
```typescript
/**
* 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
@ -217,92 +95,53 @@ Types are defined by a class extending `Type`.
Sharkitek defines some basic types by default, in these classes:
- `BooleanType`: boolean value in the model, boolean value in the serialized object.
- `BoolType`: 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 property of a Sharkitek model, you must provide its type by instantiating one of these classes.
```typescript
class Example {
class Example extends s.model({
foo: s.property.define(new StringType()),
})
{
foo: string;
}
const ExampleModel = defineModel({
Class: Example,
properties: {
foo: s.property.define(new StringType()),
},
});
```
To ease the use of these classes and reduce read complexity, properties of each type are easily definable with a function for each type.
To ease the use of these classes and reduce read complexity,
properties of each type are easily definable with a function for each type.
- `BooleanType` => `s.property.boolean`
- `BoolType` => `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`
Type implementers should provide a corresponding function for each defined type. They can even provide multiple functions or constants with predefined parameters. For example, we could define `s.property.stringArray()` which would be similar to `s.property.array(s.property.string())`.
Type implementers should provide a corresponding function for each defined type. They can even provide
multiple functions or constants with predefined parameters.
(For example, we could define `s.property.stringArray()` which would be similar to `s.property.array(s.property.string())`.)
```typescript
class Example {
class Example extends s.model({
foo: s.property.string(),
})
{
foo: string;
}
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.
@ -310,17 +149,17 @@ Serialize the model.
Example:
```typescript
const serializedObject = definedModel.model(modelInstance).serialize();
const serializedObject = model.serialize();
```
#### `parse(serializedObject)`
#### `deserialize(serializedObject)`
Deserialize the model.
Example:
```typescript
const modelInstance = definedModel.parse({
const model = (new TestModel()).deserialize({
id: 5,
title: "Hello World!",
users: [
@ -339,7 +178,7 @@ Serialize the difference between current model state and original one.
Example:
```typescript
const modelInstance = definedModel.parse({
const model = (new TestModel()).deserialize({
id: 5,
title: "Hello World!",
users: [
@ -350,9 +189,9 @@ const modelInstance = definedModel.parse({
],
});
modelInstance.title = "A new title for a new world";
model.title = "A new title for a new world";
const result = definedModel.model(modelInstance).serializeDiff();
const result = model.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:
@ -366,7 +205,7 @@ Set current properties values as original values.
Example:
```typescript
const modelInstance = definedModel.parse({
const model = (new TestModel()).deserialize({
id: 5,
title: "Hello World!",
users: [
@ -377,24 +216,24 @@ const modelInstance = definedModel.parse({
],
});
modelInstance.title = "A new title for a new world";
model.title = "A new title for a new world";
definedModel.model(modelInstance).resetDiff();
model.resetDiff();
const result = definedModel.model(modelInstance).serializeDiff();
const result = model.serializeDiff();
// if `id` is defined as the model identifier:
// result = { id: 5 }
// if `id` is not defined as the model identifier:
// result = {}
```
#### `patch()`
#### `save()`
Get difference between original values and current ones, then reset it.
Similar to call `serializeDiff()` then `resetDiff()`.
```typescript
const modelInstance = definedModel.parse({
const model = (new TestModel()).deserialize({
id: 5,
title: "Hello World!",
users: [
@ -405,9 +244,9 @@ const modelInstance = definedModel.parse({
],
});
modelInstance.title = "A new title for a new world";
model.title = "A new title for a new world";
const result = definedModel.model(modelInstance).patch();
const result = model.save();
// 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:

View file

@ -1,23 +0,0 @@
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",
},
},
]);

10
jest.config.js Normal file
View file

@ -0,0 +1,10 @@
/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */
export default {
preset: "ts-jest",
testEnvironment: "node",
roots: [
"./tests",
],
};

View file

@ -1,6 +1,6 @@
{
"name": "@sharkitek/core",
"version": "4.1.0",
"version": "3.0.2",
"description": "TypeScript library for well-designed model architectures.",
"keywords": [
"deserialization",
@ -16,7 +16,7 @@
"repository": "https://code.zeptotech.net/Sharkitek/Core",
"author": {
"name": "Madeorsk",
"email": "m@deor.sk"
"email": "madeorsk@protonmail.com"
},
"license": "MIT",
"publishConfig": {
@ -24,31 +24,24 @@
},
"scripts": {
"build": "tsc && vite build",
"test": "vitest",
"coverage": "vitest run --coverage",
"format": "prettier . --write",
"lint": "eslint"
"test": "jest"
},
"type": "module",
"source": "src/library.ts",
"source": "src/index.ts",
"types": "lib/index.d.ts",
"main": "lib/index.js",
"files": [
"lib/**/*"
],
"devDependencies": {
"@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",
"@types/jest": "^29.5.13",
"@types/node": "^22.7.4",
"jest": "^29.7.0",
"ts-jest": "^29.2.5",
"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"
"typescript": "^5.6.2",
"vite": "^5.4.8",
"vite-plugin-dts": "^4.2.2"
},
"packageManager": "yarn@4.9.2"
"packageManager": "yarn@4.5.0"
}

236
src/Model/Model.ts Normal file
View file

@ -0,0 +1,236 @@
import {Definition} from "./PropertyDefinition";
/**
* Type definition of a model constructor.
*/
export type ConstructorOf<T extends object> = { new(): T; };
/**
* Unknown property definition.
*/
export type UnknownDefinition = Definition<unknown, unknown>;
/**
* A model shape.
*/
export type ModelShape = Record<string, UnknownDefinition>;
/**
* Properties of a model based on its shape.
*/
export type PropertiesModel<Shape extends ModelShape> = {
[k in keyof Shape]: Shape[k]["_sharkitek"];
};
/**
* Serialized object type based on model shape.
*/
export type SerializedModel<Shape extends ModelShape> = {
[k in keyof Shape]: Shape[k]["_serialized"];
};
/**
* Type of a model object.
*/
export type Model<Shape extends ModelShape, IdentifierType = unknown> = ModelDefinition<Shape, IdentifierType> & PropertiesModel<Shape>;
/**
* Type of a model class.
*/
export type ModelClass<Shape extends ModelShape, Identifier extends keyof Shape = any> = ConstructorOf<Model<Shape, IdentifierType<Shape, Identifier>>>;
/**
* Identifier type.
*/
export type IdentifierType<Shape extends ModelShape, K extends keyof Shape> = Shape[K]["_sharkitek"];
/**
* Interface of a Sharkitek model definition.
*/
export interface ModelDefinition<Shape extends ModelShape, IdentifierType, ModelType extends Model<Shape, IdentifierType> = Model<Shape, IdentifierType>>
{
/**
* Get model identifier.
*/
getIdentifier(): IdentifierType;
/**
* Serialize the model.
*/
serialize(): SerializedModel<Shape>;
/**
* Deserialize the model.
* @param obj Serialized object.
*/
deserialize(obj: SerializedModel<Shape>): ModelType;
/**
* Find out if the model is new (never deserialized) or not.
*/
isNew(): boolean;
/**
* Find out if the model is dirty or not.
*/
isDirty(): boolean;
/**
* Serialize the difference between current model state and the original one.
*/
serializeDiff(): Partial<SerializedModel<Shape>>;
/**
* Set current properties values as original values.
*/
resetDiff(): void;
/**
* Get difference between original values and current ones, then reset it.
* Similar to call `serializeDiff()` then `resetDiff()`.
*/
save(): Partial<SerializedModel<Shape>>;
}
/**
* Define a Sharkitek model.
* @param shape Model shape definition.
* @param identifier Identifier property name.
*/
export function model<ModelType extends Model<Shape, IdentifierType<Shape, Identifier>>, Shape extends ModelShape, Identifier extends keyof Shape = any>(
shape: Shape,
identifier?: Identifier,
): ConstructorOf<ModelType>
{
// Get shape entries.
const shapeEntries = Object.entries(shape) as [keyof Shape, UnknownDefinition][];
return class GenericModel implements ModelDefinition<Shape, IdentifierType<Shape, Identifier>, ModelType>
{
constructor()
{
// Initialize properties to undefined.
Object.assign(this,
// Build empty properties model from shape entries.
Object.fromEntries(shapeEntries.map(([key]) => [key, undefined])) as PropertiesModel<Shape>
);
}
/**
* Calling a function for each defined property.
* @param callback - The function to call.
* @protected
*/
protected forEachModelProperty<ReturnType>(callback: (propertyName: keyof Shape, propertyDefinition: UnknownDefinition) => ReturnType): ReturnType
{
for (const [propertyName, propertyDefinition] of shapeEntries)
{ // For each property, checking that its type is defined and calling the callback with its type.
// 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;
}
}
/**
* The original properties values.
* @protected
*/
protected _originalProperties: Partial<PropertiesModel<Shape>> = {};
/**
* The original (serialized) object.
* @protected
*/
protected _originalObject: SerializedModel<Shape>|null = null;
getIdentifier(): IdentifierType<Shape, Identifier>
{
return (this as PropertiesModel<Shape>)?.[identifier];
}
serialize(): SerializedModel<Shape>
{
// Creating an empty (=> partial) serialized object.
const serializedObject: Partial<SerializedModel<Shape>> = {};
this.forEachModelProperty((propertyName, propertyDefinition) => {
// For each defined model property, adding it to the serialized object.
serializedObject[propertyName] = propertyDefinition.type.serialize((this as PropertiesModel<Shape>)?.[propertyName]);
});
return serializedObject as SerializedModel<Shape>; // Returning the serialized object.
}
deserialize(obj: SerializedModel<Shape>): ModelType
{
this.forEachModelProperty((propertyName, propertyDefinition) => {
// For each defined model property, assigning its deserialized value.
(this as PropertiesModel<Shape>)[propertyName] = propertyDefinition.type.deserialize(obj[propertyName]);
});
// Reset original property values.
this.resetDiff();
this._originalObject = obj; // The model is not a new one, but loaded from a deserialized one. Storing it.
return this as unknown as ModelType;
}
isNew(): boolean
{
return !this._originalObject;
}
isDirty(): boolean
{
return this.forEachModelProperty((propertyName, propertyDefinition) => (
// For each property, checking if it is different.
propertyDefinition.type.propertyHasChanged(this._originalProperties[propertyName], (this as PropertiesModel<Shape>)[propertyName])
// There is a difference, we should return false.
? true
// There is no difference, returning nothing.
: undefined
)) === true;
}
serializeDiff(): Partial<SerializedModel<Shape>>
{
// Creating an empty (=> partial) serialized object.
const serializedObject: Partial<SerializedModel<Shape>> = {};
this.forEachModelProperty((propertyName, propertyDefinition) => {
// For each defined model property, adding it to the serialized object if it has changed or if it is the identifier.
if (
identifier == propertyName ||
propertyDefinition.type.propertyHasChanged(this._originalProperties[propertyName], (this as PropertiesModel<Shape>)[propertyName])
) // Adding the current property to the serialized object if it is the identifier or its value has changed.
serializedObject[propertyName] = propertyDefinition.type.serializeDiff((this as PropertiesModel<Shape>)?.[propertyName]);
});
return serializedObject; // Returning the serialized object.
}
resetDiff(): void
{
this.forEachModelProperty((propertyName, propertyDefinition) => {
// For each property, set its original value to its current property value.
this._originalProperties[propertyName] = (this as PropertiesModel<Shape>)[propertyName];
propertyDefinition.type.resetDiff((this as PropertiesModel<Shape>)[propertyName]);
});
}
save(): Partial<SerializedModel<Shape>>
{
// Get the difference.
const diff = this.serializeDiff();
// Once the difference has been obtained, reset it.
this.resetDiff();
return diff; // Return the difference.
}
} as unknown as ConstructorOf<ModelType>;
}

10
src/Model/Properties.ts Normal file
View file

@ -0,0 +1,10 @@
export {define} from "./PropertyDefinition";
export {array} from "./Types/ArrayType";
export {bool, boolean} from "./Types/BoolType";
export {date} from "./Types/DateType";
export {decimal} from "./Types/DecimalType";
export {model} from "./Types/ModelType";
export {numeric} from "./Types/NumericType";
export {object} from "./Types/ObjectType";
export {string} from "./Types/StringType";

View file

@ -0,0 +1,26 @@
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>)
{}
}
/**
* 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);
}

View file

@ -0,0 +1,66 @@
import {Type} from "./Type";
import {define, Definition} from "../PropertyDefinition";
/**
* 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;
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;
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;
// 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));
}
}
/**
* New array property definition.
* @param valueDefinition Array values type definition.
*/
export function array<SerializedValueType, SharkitekValueType>(valueDefinition: Definition<SerializedValueType, SharkitekValueType>): Definition<SerializedValueType[], SharkitekValueType[]>
{
return define(new ArrayType(valueDefinition));
}

View file

@ -0,0 +1,42 @@
import {Type} from "./Type";
import {define, Definition} from "../PropertyDefinition";
/**
* Type of any boolean value.
*/
export class BoolType 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 bool(): Definition<boolean, boolean>
{
return define(new BoolType());
}
/**
* New boolean property definition.
* Alias of bool.
*/
export function boolean(): ReturnType<typeof bool>
{
return bool();
}

View file

@ -0,0 +1,32 @@
import {Type} from "./Type";
import {define, Definition} from "../PropertyDefinition";
/**
* 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;
return value?.toISOString();
}
}
/**
* New date property definition.
*/
export function date(): Definition<string, Date>
{
return define(new DateType());
}

View file

@ -0,0 +1,32 @@
import {Type} from "./Type";
import {define, Definition} from "../PropertyDefinition";
/**
* 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;
return value?.toString();
}
}
/**
* New decimal property definition.
*/
export function decimal(): Definition<string, number>
{
return define(new DecimalType());
}

View file

@ -0,0 +1,60 @@
import {Type} from "./Type";
import {define, Definition} from "../PropertyDefinition";
import {ConstructorOf, Model, ModelShape, SerializedModel} from "../Model";
/**
* Type of a Sharkitek model value.
*/
export class ModelType<Shape extends ModelShape> extends Type<SerializedModel<Shape>, Model<Shape>>
{
/**
* Initialize a new model type of a Sharkitek model property.
* @param modelConstructor Model constructor.
*/
constructor(protected modelConstructor: ConstructorOf<Model<Shape>>)
{
super();
}
serialize(value: Model<Shape>|null|undefined): SerializedModel<Shape>|null|undefined
{
if (value === undefined) return undefined;
if (value === null) return null;
// Serializing the given model.
return value?.serialize();
}
deserialize(value: SerializedModel<Shape>|null|undefined): Model<Shape>|null|undefined
{
if (value === undefined) return undefined;
if (value === null) return null;
// Deserializing the given object in the new model.
return (new this.modelConstructor()).deserialize(value) as Model<Shape>;
}
serializeDiff(value: Model<Shape>|null|undefined): Partial<SerializedModel<Shape>>|null|undefined
{
if (value === undefined) return undefined;
if (value === null) return null;
// Serializing the given model.
return value?.serializeDiff();
}
resetDiff(value: Model<Shape>|null|undefined): void
{
// Reset diff of the given model.
value?.resetDiff();
}
}
/**
* New model property definition.
* @param modelConstructor Model constructor.
*/
export function model<Shape extends ModelShape>(modelConstructor: ConstructorOf<Model<Shape>>): Definition<SerializedModel<Shape>, Model<Shape>>
{
return define(new ModelType(modelConstructor));
}

View file

@ -0,0 +1,26 @@
import {Type} from "./Type";
import {define, Definition} from "../PropertyDefinition";
/**
* Type of any numeric value.
*/
export class NumericType extends Type<number, number>
{
deserialize(value: number|null|undefined): number|null|undefined
{
return value;
}
serialize(value: number|null|undefined): number|null|undefined
{
return value;
}
}
/**
* New numeric property definition.
*/
export function numeric(): Definition<number, number>
{
return define(new NumericType());
}

View file

@ -0,0 +1,78 @@
import {Type} from "./Type";
import {define, Definition} from "../PropertyDefinition";
import {ModelShape, PropertiesModel, SerializedModel, UnknownDefinition} from "../Model";
/**
* Type of a custom object.
*/
export class ObjectType<Shape extends ModelShape> extends Type<SerializedModel<Shape>, PropertiesModel<Shape>>
{
/**
* Initialize a new object type of a Sharkitek model property.
* @param shape
*/
constructor(protected readonly shape: Shape)
{
super();
}
deserialize(value: SerializedModel<Shape>|null|undefined): PropertiesModel<Shape>|null|undefined
{
if (value === undefined) return undefined;
if (value === null) return null;
return Object.fromEntries(
// For each defined field, deserialize its value according to its type.
(Object.entries(this.shape) as [keyof Shape, UnknownDefinition][]).map(([fieldName, fieldDefinition]) => (
// Return an entry with the current field name and the deserialized value.
[fieldName, fieldDefinition.type.deserialize(value?.[fieldName])]
))
) as PropertiesModel<Shape>;
}
serialize(value: PropertiesModel<Shape>|null|undefined): SerializedModel<Shape>|null|undefined
{
if (value === undefined) return undefined;
if (value === null) return null;
return Object.fromEntries(
// For each defined field, serialize its value according to its type.
(Object.entries(this.shape) as [keyof Shape, UnknownDefinition][]).map(([fieldName, fieldDefinition]) => (
// Return an entry with the current field name and the serialized value.
[fieldName, fieldDefinition.type.serialize(value?.[fieldName])]
))
) as PropertiesModel<Shape>;
}
serializeDiff(value: PropertiesModel<Shape>|null|undefined): Partial<SerializedModel<Shape>>|null|undefined
{
if (value === undefined) return undefined;
if (value === null) return null;
return Object.fromEntries(
// For each defined field, serialize its diff value according to its type.
(Object.entries(this.shape) as [keyof Shape, UnknownDefinition][]).map(([fieldName, fieldDefinition]) => (
// Return an entry with the current field name and the serialized diff value.
[fieldName, fieldDefinition.type.serializeDiff(value?.[fieldName])]
))
) as PropertiesModel<Shape>;
}
resetDiff(value: PropertiesModel<Shape>|null|undefined)
{
// For each field, reset its diff.
(Object.entries(this.shape) as [keyof Shape, UnknownDefinition][]).forEach(([fieldName, fieldDefinition]) => {
// Reset diff of the current field.
fieldDefinition.type.resetDiff(value?.[fieldName]);
});
}
}
/**
* New object property definition.
* @param shape Shape of the object.
*/
export function object<Shape extends ModelShape>(shape: Shape): Definition<SerializedModel<Shape>, PropertiesModel<Shape>>
{
return define(new ObjectType(shape));
}

View file

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

55
src/Model/Types/Type.ts Normal file
View file

@ -0,0 +1,55 @@
/**
* 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(value: ModelType|null|undefined): 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: ModelType|null|undefined, currentValue: ModelType|null|undefined): 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|null|undefined, currentValue: SerializedType|null|undefined): boolean
{
return originalValue != currentValue;
}
}

14
src/Model/index.ts Normal file
View file

@ -0,0 +1,14 @@
import * as property from "./Properties";
export { property };
export * from "./Model";
export {Definition} from "./PropertyDefinition";
export {ArrayType} from "./Types/ArrayType";
export {BoolType} from "./Types/BoolType";
export {DateType} from "./Types/DateType";
export {DecimalType} from "./Types/DecimalType";
export {ModelType} from "./Types/ModelType";
export {NumericType} from "./Types/NumericType";
export {ObjectType} from "./Types/ObjectType";
export {StringType} from "./Types/StringType";

View file

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

View file

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

View file

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

View file

@ -1,16 +0,0 @@
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}` : ""}`,
);
}
}

4
src/index.ts Normal file
View file

@ -0,0 +1,4 @@
import * as s from "./Model";
export * from "./Model";
export { s };
export default s;

View file

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

View file

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

View file

@ -1,16 +0,0 @@
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";

View file

@ -1,667 +0,0 @@
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>;

View file

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

View file

@ -1,35 +0,0 @@
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);
}

View file

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

View file

@ -1,37 +0,0 @@
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();
}

View file

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

View file

@ -1,31 +0,0 @@
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());
}

View file

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

View file

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

View file

@ -1,35 +0,0 @@
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());
}

View file

@ -1,267 +0,0 @@
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));
}

View file

@ -1,28 +0,0 @@
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());
}

View file

@ -1,88 +0,0 @@
/**
* 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;
}
}

View file

@ -1,10 +0,0 @@
/**
* 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;

159
tests/Model.test.ts Normal file
View file

@ -0,0 +1,159 @@
import {s} from "../src";
/**
* Another test model.
*/
class Author extends s.model({
name: s.property.string(),
firstName: s.property.string(),
email: s.property.string(),
createdAt: s.property.date(),
active: s.property.bool(),
})
{
active: boolean = true;
constructor(name: string = "", firstName: string = "", email: string = "", createdAt: Date = new Date())
{
super();
this.name = name;
this.firstName = firstName;
this.email = email;
this.createdAt = createdAt;
}
}
/**
* A test model.
*/
class Article extends s.model({
id: s.property.numeric(),
title: s.property.string(),
authors: s.property.array(s.property.model(Author)),
text: s.property.string(),
evaluation: s.property.decimal(),
tags: s.property.array(
s.property.object({
name: s.property.string(),
})
),
}, "id")
{
id: number;
title: string;
authors: Author[] = [];
text: string;
evaluation: number;
tags: {
name: string;
}[];
}
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",
tags: [ {name: "test"}, {name: "foo"} ],
}).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",
tags: [ {name: "test"}, {name: "foo"} ],
});
});
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;
article.tags = [];
article.tags.push({name: "test"});
article.tags.push({name: "foo"});
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",
tags: [ {name: "test"}, {name: "foo"} ],
});
});
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()).toISOString(), active: true, },
{ name: "TEST", firstName: "Another", email: "another@test.test", createdAt: (new Date()).toISOString(), active: false, },
],
text: "this is a long test.",
evaluation: "25.23",
tags: [ {name: "test"}, {name: "foo"} ],
});
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()).toISOString(), active: true, },
{ name: "TEST", firstName: "Another", email: "another@test.test", createdAt: (new Date()).toISOString(), active: false, },
],
text: "this is a long test.",
evaluation: "25.23",
tags: [ {name: "test"}, {name: "foo"} ],
});
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" },
],
});
});

View file

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

View file

@ -1,24 +0,0 @@
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");
});
});

View file

@ -1,587 +0,0 @@
import {describe, expect, it} from "vitest";
import {circular, defineModel, s} from "../src/library";
/**
* Test class of an account.
*/
class Account {
id: number;
createdAt: Date;
name: string;
email: string;
active: boolean;
}
const AccountModel = s.defineModel({
Class: Account,
identifier: "id",
properties: {
id: s.property.numeric(),
createdAt: s.property.date(),
name: s.property.string(),
email: s.property.string(),
active: s.property.boolean(),
},
});
/**
* Test class of an article.
*/
class Article {
id: number;
title: string;
authors: Account[];
text: string;
evaluation: number;
tags: {name: string}[];
comments: ArticleComment[];
}
const ArticleModel = s.defineModel({
Class: Article,
identifier: "id",
properties: {
id: s.property.numeric(),
title: s.property.string(),
authors: s.property.array(s.property.model(() => AccountModel)),
text: s.property.string(),
evaluation: s.property.decimal(),
tags: s.property.array(s.property.object({name: s.property.string()})),
comments: s.property.array(s.property.model(() => ArticleCommentModel)),
},
});
/**
* Test class of a comment on an article.
*/
class ArticleComment {
id: number;
article?: Article;
author: Account;
message: string;
}
const ArticleCommentModel = s.defineModel({
Class: ArticleComment,
identifier: "id",
properties: {
id: s.property.numeric(),
article: s.property.model(circular<Article>(() => ArticleModel)),
author: s.property.model(() => AccountModel),
message: s.property.string(),
},
});
/**
* Get a test account instance.
*/
function getTestAccount(): Account {
const account = new Account();
account.id = 52;
account.createdAt = new Date();
account.name = "John Doe";
account.email = "john@doe.test";
account.active = true;
return account;
}
function getTestArticle(): Article {
const article = new Article();
article.id = 1;
article.title = "this is a test";
article.text = "this is a long test.";
article.evaluation = 25.23;
article.tags = [{name: "test"}, {name: "foo"}];
article.authors = [getTestAccount()];
article.comments = [];
return article;
}
describe("model", () => {
it("defines a new model, extending an existing one", () => {
class ExtendedAccount extends Account {
extendedProperty: string;
}
const ExtendedAccountModel = s.extend(AccountModel, {
Class: ExtendedAccount,
properties: {
extendedProperty: s.property.string(),
},
});
expect(ExtendedAccountModel.definition).toEqual({
Class: ExtendedAccount,
identifier: "id",
properties: {
id: s.property.numeric(),
createdAt: s.property.date(),
name: s.property.string(),
email: s.property.string(),
active: s.property.boolean(),
extendedProperty: s.property.string(),
},
});
});
it("initializes a new model", () => {
const article = getTestArticle();
const newModel = ArticleModel.model(article);
expect(newModel.instance).toBe(article);
});
it("gets a model state from its instance", () => {
const article = getTestArticle();
expect(ArticleModel.model(article).isNew()).toBeTruthy();
expect(ArticleModel.model(article).isDirty()).toBeFalsy();
});
it("gets a model identifier value", () => {
const article = getTestArticle();
expect(ArticleModel.model(article).getIdentifier()).toBe(1);
});
it("gets a model composite identifier value", () => {
class CompositeModel {
static model = s.defineModel({
Class: CompositeModel,
properties: {
firstId: s.property.numeric(),
secondId: s.property.numeric(),
label: s.property.string(),
},
identifier: ["firstId", "secondId"],
});
firstId: number;
secondId: number;
label: string;
}
expect(
CompositeModel.model
.model(
Object.assign(new CompositeModel(), {
firstId: 5,
secondId: 6,
label: "test",
}),
)
.getIdentifier(),
).toStrictEqual([5, 6]);
});
it("checks model dirtiness when altered, then reset diff", () => {
const article = getTestArticle();
expect(ArticleModel.model(article).isDirty()).toBeFalsy();
article.title = "new title";
expect(ArticleModel.model(article).isDirty()).toBeTruthy();
ArticleModel.model(article).resetDiff();
expect(ArticleModel.model(article).isDirty()).toBeFalsy();
});
it("deserializes a model from a serialized form", () => {
const expectedArticle = Object.assign(new Article(), {
id: 1,
title: "this is a test",
authors: [
Object.assign(new Account(), {
id: 52,
name: "John Doe",
email: "test@test.test",
createdAt: new Date("2022-08-07T08:47:01.000Z"),
active: true,
}),
Object.assign(new Account(), {
id: 4,
name: "Tester",
email: "another@test.test",
createdAt: new Date("2022-09-07T18:32:55.000Z"),
active: false,
}),
],
text: "this is a long test.",
evaluation: 8.52,
tags: [{name: "test"}, {name: "foo"}],
comments: [
Object.assign(new ArticleComment(), {
id: 542,
author: Object.assign(new Account(), {
id: 52,
name: "John Doe",
email: "test@test.test",
createdAt: new Date("2022-08-07T08:47:01.000Z"),
active: true,
}),
message: "comment content",
}),
],
});
const deserializedArticle = ArticleModel.parse({
id: 1,
title: "this is a test",
authors: [
{
id: 52,
name: "John Doe",
email: "test@test.test",
createdAt: "2022-08-07T08:47:01.000Z",
active: true,
},
{
id: 4,
name: "Tester",
email: "another@test.test",
createdAt: "2022-09-07T18:32:55.000Z",
active: false,
},
],
text: "this is a long test.",
evaluation: "8.52",
tags: [{name: "test"}, {name: "foo"}],
comments: [
{
id: 542,
author: {
id: 52,
name: "John Doe",
email: "test@test.test",
createdAt: "2022-08-07T08:47:01.000Z",
active: true,
},
message: "comment content",
},
],
});
const deserializedArticleProperties =
ArticleModel.model(deserializedArticle).getInstanceProperties();
delete deserializedArticleProperties.authors[0]._sharkitek;
delete deserializedArticleProperties.authors[1]._sharkitek;
delete deserializedArticleProperties.comments[0]._sharkitek;
delete (deserializedArticleProperties.comments[0].author as any)._sharkitek;
const expectedArticleProperties =
ArticleModel.model(expectedArticle).getInstanceProperties();
delete expectedArticleProperties.authors[0]._sharkitek;
delete expectedArticleProperties.authors[1]._sharkitek;
delete expectedArticleProperties.comments[0]._sharkitek;
delete (expectedArticleProperties.comments[0].author as any)._sharkitek;
expect(deserializedArticleProperties).toEqual(expectedArticleProperties);
});
it("serializes an initialized model", () => {
const article = getTestArticle();
expect(ArticleModel.model(article).serialize()).toEqual({
id: 1,
title: "this is a test",
text: "this is a long test.",
evaluation: "25.23",
tags: [{name: "test"}, {name: "foo"}],
authors: [
{
id: 52,
createdAt: article.authors[0].createdAt.toISOString(),
name: "John Doe",
email: "john@doe.test",
active: true,
},
],
comments: [],
});
});
it("deserializes, changes and patches", () => {
const deserializedArticle = ArticleModel.parse({
id: 1,
title: "this is a test",
authors: [
{
id: 52,
name: "John Doe",
email: "test@test.test",
createdAt: "2022-08-07T08:47:01.000Z",
active: true,
},
{
id: 4,
name: "Tester",
email: "another@test.test",
createdAt: "2022-09-07T18:32:55.000Z",
active: false,
},
],
text: "this is a long test.",
evaluation: "8.52",
tags: [{name: "test"}, {name: "foo"}],
comments: [
{
id: 542,
author: {
id: 52,
name: "John Doe",
email: "test@test.test",
createdAt: "2022-08-07T08:47:01.000Z",
active: true,
},
message: "comment content",
},
],
});
deserializedArticle.text = "A new text for a new life!";
expect(ArticleModel.model(deserializedArticle).patch()).toStrictEqual({
id: 1,
text: "A new text for a new life!",
});
deserializedArticle.evaluation = 5.24;
expect(ArticleModel.model(deserializedArticle).patch()).toStrictEqual({
id: 1,
evaluation: "5.24",
});
});
it("patches with modified submodels", () => {
const deserializedArticle = ArticleModel.parse({
id: 1,
title: "this is a test",
authors: [
{
id: 52,
name: "John Doe",
email: "test@test.test",
createdAt: "2022-08-07T08:47:01.000Z",
active: true,
},
{
id: 4,
name: "Tester",
email: "another@test.test",
createdAt: "2022-09-07T18:32:55.000Z",
active: false,
},
],
text: "this is a long test.",
evaluation: "8.52",
tags: [{name: "test"}, {name: "foo"}],
comments: [
{
id: 542,
author: {
id: 52,
name: "John Doe",
email: "test@test.test",
createdAt: "2022-08-07T08:47:01.000Z",
active: true,
},
message: "comment content",
},
],
});
deserializedArticle.authors[1].active = true;
expect(ArticleModel.model(deserializedArticle).patch()).toStrictEqual({
id: 1,
authors: [{id: 52}, {id: 4, active: true}],
});
deserializedArticle.comments[0].author.name = "Johnny";
expect(ArticleModel.model(deserializedArticle).patch()).toStrictEqual({
id: 1,
comments: [
{
id: 542,
author: {
id: 52,
name: "Johnny",
},
},
],
});
});
it("deserializes and patches with fields that are not properties", () => {
class TestModel {
static model = defineModel({
Class: TestModel,
properties: {
id: s.property.numeric(),
label: s.property.string(),
},
identifier: "id",
});
id: number;
label: string;
notAProperty: {hello: string} = {hello: "world"};
}
const deserializedModel = TestModel.model.parse({
id: 5,
label: "testing",
});
expect(deserializedModel.id).toBe(5);
expect(deserializedModel.label).toBe("testing");
expect(deserializedModel.notAProperty?.hello).toBe("world");
const clonedDeserializedModel = TestModel.model
.model(deserializedModel)
.clone();
deserializedModel.label = "new!";
expect(TestModel.model.model(deserializedModel).patch()).toStrictEqual({
id: 5,
label: "new!",
});
deserializedModel.notAProperty.hello = "monster";
expect(TestModel.model.model(deserializedModel).patch()).toStrictEqual({
id: 5,
});
expect(TestModel.model.model(deserializedModel).serialize()).toStrictEqual({
id: 5,
label: "new!",
});
expect(
TestModel.model.model(clonedDeserializedModel).serialize(),
).toStrictEqual({id: 5, label: "testing"});
expect(clonedDeserializedModel.notAProperty.hello).toEqual("world");
});
it("assigns properties, ignoring fields which are not properties", () => {
const deserializedArticle = ArticleModel.parse({
id: 1,
title: "this is a test",
authors: [
{
id: 52,
name: "John Doe",
email: "test@test.test",
createdAt: "2022-08-07T08:47:01.000Z",
active: true,
},
{
id: 4,
name: "Tester",
email: "another@test.test",
createdAt: "2022-09-07T18:32:55.000Z",
active: false,
},
],
text: "this is a long test.",
evaluation: "8.52",
tags: [{name: "test"}, {name: "foo"}],
comments: [
{
id: 542,
author: {
id: 52,
name: "John Doe",
email: "test@test.test",
createdAt: "2022-08-07T08:47:01.000Z",
active: true,
},
message: "comment content",
},
],
});
// Assign title and text, html is silently ignored.
ArticleModel.model(deserializedArticle).assign({
title: "something else",
text: "fully new text! yes!",
html: "<p>fully new text! yes!</p>",
});
expect((deserializedArticle as any)?.html).toBeUndefined();
expect(ArticleModel.model(deserializedArticle).patch()).toStrictEqual({
id: 1,
title: "something else",
text: "fully new text! yes!",
});
});
it("initializes a model from properties values", () => {
const testArticle = ArticleModel.from({
title: "this is a test",
authors: [
AccountModel.from({
name: "John Doe",
email: "test@test.test",
createdAt: new Date(),
active: true,
}),
],
text: "this is a long text",
evaluation: 8.52,
tags: [{name: "test"}, {name: "foo"}],
unknownField: true,
anotherOne: "test",
});
expect(testArticle.title).toBe("this is a test");
expect(testArticle.text).toBe("this is a long text");
expect(testArticle.evaluation).toBe(8.52);
expect(testArticle.authors).toHaveLength(1);
expect(testArticle.authors[0]?.name).toBe("John Doe");
expect((testArticle as any).unknownField).toBeUndefined();
expect((testArticle as any).anotherOne).toBeUndefined();
});
it("applies patches to an existing model", () => {
const testArticle = ArticleModel.from({
id: 1,
title: "this is a test",
authors: [
AccountModel.from({
id: 55,
name: "John Doe",
email: "test@test.test",
createdAt: new Date(),
active: true,
}),
],
text: "this is a long text",
evaluation: 8.52,
tags: [{name: "test"}, {name: "foo"}],
unknownField: true,
anotherOne: "test",
});
ArticleModel.model(testArticle).resetDiff();
// Test simple patch.
ArticleModel.model(testArticle).applyPatch({
title: "new title",
});
expect(testArticle.title).toBe("new title");
expect(ArticleModel.model(testArticle).serializeDiff()).toStrictEqual({
id: 1,
});
// Test originals update propagation.
ArticleModel.model(testArticle).applyPatch({
authors: [{email: "john@test.test"}],
});
expect(testArticle.authors[0].email).toBe("john@test.test");
expect(ArticleModel.model(testArticle).serializeDiff()).toStrictEqual({
id: 1,
});
// Test without originals update.
ArticleModel.model(testArticle).applyPatch(
{
authors: [{name: "Johnny"}],
},
false,
);
expect(testArticle.authors[0].name).toBe("Johnny");
expect(ArticleModel.model(testArticle).serializeDiff()).toStrictEqual({
id: 1,
authors: [{id: 55, name: "Johnny"}],
});
});
});

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -2,7 +2,7 @@
"ts-node": {
"compilerOptions": {
"module": "ESNext",
"types": ["node"]
"types": ["node"],
}
},
@ -11,7 +11,6 @@
"incremental": true,
"sourceMap": true,
"noImplicitAny": true,
"noImplicitThis": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"resolveJsonModule": true,
@ -22,6 +21,9 @@
"module": "ES6",
"target": "ES6",
"moduleResolution": "Bundler",
"lib": ["ESNext", "DOM"]
"lib": [
"ESNext",
"DOM"
],
}
}

View file

@ -1,19 +1,22 @@
import {defineConfig, UserConfig} from "vite";
import {ConfigEnv, defineConfig, UserConfig} from "vite";
import dts from "vite-plugin-dts";
// https://vitejs.dev/config/
export default defineConfig((): UserConfig => {
return {
export default defineConfig(({ mode }: ConfigEnv): UserConfig => {
return ({
build: {
outDir: "lib",
sourcemap: true,
minify: "esbuild",
lib: {
entry: "src/library.ts",
entry: "src/index.ts",
formats: ["es"],
fileName: "index",
},
rollupOptions: {
external: ["reflect-metadata"],
},
},
plugins: [
@ -22,6 +25,6 @@ export default defineConfig((): UserConfig => {
rollupTypes: true,
exclude: ["node_modules"],
}),
],
};
]
});
});

4254
yarn.lock

File diff suppressed because it is too large Load diff