Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/rremigius/mozel
Easy-to-use model, nestable, strongly typed (typescript and runtime), with change watching and JSON import/export.
https://github.com/rremigius/mozel
Last synced: 3 months ago
JSON representation
Easy-to-use model, nestable, strongly typed (typescript and runtime), with change watching and JSON import/export.
- Host: GitHub
- URL: https://github.com/rremigius/mozel
- Owner: rremigius
- Created: 2021-03-02T10:58:08.000Z (over 3 years ago)
- Default Branch: main
- Last Pushed: 2024-07-08T12:30:46.000Z (4 months ago)
- Last Synced: 2024-07-12T01:49:54.967Z (4 months ago)
- Language: TypeScript
- Size: 665 KB
- Stars: 3
- Watchers: 2
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
Awesome Lists containing this project
README
Mozel
===A Mozel is a strongly-typed model, which ensures that its properties are of the correct type,
both at runtime and compile-time (Typescript). It is easy to define a Mozel, and it brings a number of useful features.Mozel can be used both in Typescript and plain Javascript.
## Features
- Nested models
- Strongly-typed properties and collections (both compile-time and runtime)
- Simple Typescript declarations
- Deep change watching
- Import/export from/to plain objects - allows easy transmission between systems through JSON.
- Deep string templating (e.g. {"name": "{username}"})## Getting Started
### Definition
A Mozel definition is simple and can be done both in Typescript and plain Javascript:
Typescript:
```typescript
import Mozel, {required} from "mozel";class Person extends Mozel {
@property(String, {required, default: 'John Doe'}) // runtime typing
name!:string; // compile-time (Typescript) typing
@collection(String) // runtime typing
nicknames!:Collection; // compile-time (Typescript) typing
}
```Note that, if you set `{required}` on a `@property`, you can safely assume the value will never be undefined.
In Typescript, you can therefore use `!` with the property. Without `{required}`, you should use `?`.
Collections are always instantiated and therefore will never be undefined.Javascript:
```javascript
import Mozel, {required} from "mozel";class Person extends Mozel {}
Person.property('name', String, {required});
Person.collection('nicknames', String);
```To set a default, use `{default: ...}` in the property options, instead of `myProperty:string = 'myDefault'`.
### Instantiation
A Mozel can be instantiated with data using the static `create` method. The instantiation is the same for Javascript and
Typescript, but in Typescript, the plain instantiation object will be strongly typed according to the Mozel's properties.```typescript
let person = Person.create({
name: 'James',
nicknames: ['Johnny', 'Jack']
});console.log(person.name); // James
console.log(person.nicknames.toArray()); // ['Johnny', 'Jack']
```Only valid values will be accepted:
```typescript
person.name = 123;
console.log(person.name); // still 'James'
```### Type definitions
In Typescript, each property has runtime type definition, as well as a Typescript type definition. These should always
match for the mozel to be considered type-safe.**Examples**:
| Runtime definition | Typescript definition | Description |
|:------------------------------------------------|:---------------------------|:------------------------------------------------------------------------------|
| `@property(String)` | `foo?:string` | Optional string |
| `@property(Number)` | `foo?:number` | Optional number |
| `@property(Alphanumeric)` | `foo?:alphanumeric` | Optional string or number |
| `@property(MyMozel)` | `foo?:MyMozel` | Optional instance of MyMozel |
| `@collection(String)` | `foo!:Collection` | Collection of strings* |
| `@collection(MyMozel)` | `foo!:Collection` | Collection of MyMozels* |
| `@property(String, {required})` | `foo!:string` | Required string, defaults to emtpy string |
| `@property(MyMozel, {required})` | `foo!:MyMozel` | Required instsance of MyMozel, generates a default MyMozel if empty. |
| `@property(String, {required, default: "foo"})` | `foo!:string` | Required string, defaults to `"foo"` |
| `@property(String, {required, default: ()=>bar` | `foo!:string` | Required string, defaults to the return value of the given function if empty. |\* Collections are always defined at initialization, even if empty. It is therefore safe to place the `!` in the Typescript definition.
### Nested Mozels
Properties can be either primitive, or other Mozels.
Nested Mozels can be instantiated entirely by providing nested data to the `create` method, or partially by providing
existing Mozels for the nested data.```typescript
// Definitionsclass Dog extends Mozel {
@property(String, {required})
name!:string;
}
class Person extends Mozel {
@property(String, {required})
name!:string;@property(Dog)
dog?:Dog;
}// Instances
let bobby = Dog.create({
name: 'Bobby'
});// Lisa has an existing dog
let lisa = Person.create({
name: 'Lisa',
dog: bobby
})// James has a new dog
let james = Person.create({
name: 'James',
dog: { // will create a new Dog called Baxter
name: 'Baxter'
}
});console.log(lisa.dog instanceof Dog); //true
console.log(lisa.dog.name); // Bobby
console.log(james.dog instanceof Dog); // true
console.log(james.dog.name); // Baxter
```### Collections
Collections can contain primitives (string/number/boolean) or Mozels. The definition determines which type all items in
the collection should be. A Collection on a Mozel will always be instantiated with the Mozel, so it cannot be `undefined`.```typescript
// Definitionsclass Dog extends Mozel {
@property(String, {required})
name!:string;
}
class Person extends Mozel {
@property(String, {required})
name!:string;
@collection(Dog)
dogs!:Collection;
}// Instances
let james = Person.create({
name: 'James',
dogs: [{name: 'Baxter'}, {name: 'Bobby'}]
});console.log(james.dogs.get(0) instanceof Dog); // true
console.log(james.dogs.map(dog => dog.name)); // ['Baxter', 'Bobby']
```### Transferral
A Mozel can only have one parent (although multiple Mozels can reference it). If it is transferred from one parent
to another, the original parent's property is automatically set to undefined.```typescript
let baxter = Dog.create({name: 'Baxter'});
let james = Person.create({
name: 'James',
dog: baxter
})
let lisa = Person.create({name: 'Lisa'});// James has the dog
console.log(james.dog.name); // baxter
console.log(lisa.dog); // undefined// Transfer to Lisa
lisa.dog = baxter;// Lisa has the dog; James no longer has a dog
console.log(james.dog); // undefined
console.log(lisa.dog.name); // baxter
```If the same Mozel needs to be accessed from multiple other Mozels, only one Mozel can be the parent and the others
can have references to it:```typescript
class Dog extends Mozel {
@property(String, {required})
name!:string;
}
class Person extends Mozel {
@property(Dog)
dog?:Dog
@property(Dog)
favourite?:Dog
}let baxter = Dog.create({name: 'Baxter'});
let james = Person.create({
dog: baxter
});
let lisa = Person.create();// James has the dog
console.log(james.dog.name); // Baxter// Lisa's favourite dog is Baxter
lisa.favourite = baxter;// James still has the dog, but Lisa also has a reference
console.log(james.dog.name); // Baxter
console.log(lisa.dogSits.name); // Baxter
```To initialize a reference together with the same data that creates the referenced instance, use `_id`:
```typescript
let james = Person.create({
name: 'James',
dog: {
_id: 'baxter',
name: 'Baxter'
},
favourite: { _id: 'baxter' }
})
```References will be resolved directly after all Mozels have been created.
### Import/export
The import/export feature makes it easy to transmit a Mozel as plain object data or JSON to another system, and have it
reconstructed into a Mozel on the other side.```typescript
// Definitions
class Dog extends Mozel {
@property(String, {required})
name!:string;
}
class Person extends Mozel {
@property(String, {required})
name!:string;
@collection(Dog)
dogs!:Collection;
}// Instances
let person = Person.create({
name: 'James',
dogs: [{name: 'Bobby'}, {name: 'Baxter'}]
});
let exported = person.$export();
let imported = Person.create(exported);console.log(person.name, imported.name); // both 'James'
console.log(person.dogs.get(1).name); // both 'Baxter'
```### Change watching
Throughout the hierarchy of the Mozel, you can watch for changes. Watchers can be defined at any level, but if watchers
need to persist even if some part of the hierarchy is replaced, they should be defined above the changing level in the
hierarchy.```typescript
// Definitions
class Toy extends Mozel {
@property(String, {required, default: 'new'})
state!:string;
}
class Dog extends Mozel {
@property(Toy)
toy?:Toy;
}
class Person extends Mozel {
@property(Dog)
dog?:Dog
}// Instances
let james = Person.create({
dog: {
toy: {}
}
});// Watchers
james.$watch('dog.toy.state', (change) => { /*...*/ }); // watcher A
james.$watch('dog.toy', (change) => { /*...*/ }); // watcher B
james.$watch('dog', (change) => { /*...*/ }); // watcher C
james.$watch('dog', (change) => { /*...*/ }, {deep: true}); // watcher Djames.dog = Dog.create(); // potentially triggers watchers A, B, C and D
james.dog.toy = Toy.create(); // potentially triggers watchers A, B and D
james.dog.toy.state = 'old'; // potentially triggers watchers A and D
```Note: watchers only get triggered if the new value is different than the old value.
If a new dog has a toy with the same state, the `dog.toy.state` watcher will not be triggered.##### Using `schema`
Using `schema` in a watcher can provide Typescript type checking:
```typescript
james.$watch(schema(Person).dog.toy, change => {
// schema provides type for handler; no need for type casting in handler
})
```#### Wildcard watchers
Wildcards (`*`) can be used in the watcher's path to match any property. This can also be used to watch for changes
in any of a Collection's items:```typescript
class Toy extends Mozel {
@property(String, {required})
name!:string;
}
class Dog extends Mozel {
@collection(Toy)
toys!:Collection;
}
let dog = Dog.create({
toys: [{name: 'ball'}, {name: 'stick'}]
});
dog.$watch('toys.*.name', ({changePath}) => {
// do something if the name of any toy changes
// changePath will provide the path to changed name (* replaced with the index of the toy in the Collection)
});
```#### Watcher properties
A watcher can be configured with the following properties:
- `path`: (string, required): The path from the current Mozel to watch.
- `handler`: (function, required): The handler to call when a value changes. Takes one argument, which is an object with the following properties:
- `newValue`: (any): The value after the change.
- `oldValue`: (any): The value before the change.
- `valuePath`: (string): The path at the level the watcher is watching.
- `changePath`: (string): The path at the deepest level where the actual change occurred.
- `deep`: (boolean) If set to `true`, will respond to changes deeper than the given path. Will make a deep clone to provide the old value.
- `immediate`: (boolean) If set to `true`, will call the handler immediately with the current value.
- `validator`: (boolean) If set to `true`, the handler will be called to validate each new input. If the handler does not return `true`, the property change will not be applied.### Non-strict mode
Mozels can be set to non-strict mode, in which they will accept invalid property values and will report errors where
the values are invalid. Note that, in that case, the mozel will *not* be considered type-safe, even though Typescript
cannot report the type errors. Runtime type checking should always be performed if non-strict mozel properties are used.```typescript
let james = Person.create({
name: 'James',
dog: { name: 'Bobby' }
});
james.$strict = false;
james.dog.name = 123;console.log(james.dog.$errors.name); // Error: Dog.name expects string.
console.log(james.$errorsDeep()['dog.name']); // Error: Dog.name expects string.
```## Advanced
### ModelFactory
The ModelFactory enhances plain-data import, allowing to 1) instantiate different subtypes of Mozels, 2) make references
between sub-Mozels and 3) inject Mozel dependencies.#### Mozel subtype instantiation
```typescript
// Definitionsclass Person extends Mozel {
@collection(Dog)
dogs!:Collection;
}
class Dog extends Mozel {}
class Pug extends Dog {}
class StBernard extends Dog {}// Instances
let factory = new MozelFactory();
factory.register([Dog, Pug, StBernard]);
let james = factory.create(Person, {
dogs: [{_type: 'Pug'}, {_type: 'StBernard'}]
});console.log(james.dogs.get(0) instanceof Pug); // true
console.log(james.dogs.get(1) instanceof StBernard); // true
```#### References between sub-Mozels
All mozels have a built-in `gid` property. This property allows the MozelFactory to uniquely identify Mozels, and
make references rather than nested Mozels.```typescript
// Definitionsclass Person extends Mozel {
@collection(Person, {reference})
likes!:Collection;
}// Instances
let data = [
{gid: 'james', likes: [{gid: 'peter'}, {gid: 'frank'}]},
{gid: 'lisa', likes: [{gid: 'james'}, {gid: 'frank'}]},
{gid: 'jessica', likes: [{gid: 'jessica'}, {gid: 'james'}, {gid: 'frank'}]},
{gid: 'frank', likes: [{gid: 'lisa'}]}
]let factory = new MozelFactory();
factory.register(Person);
let people = factory.createSet(data);console.log(people[0].likes.get(1) === people[3]); // true (both frank)
console.log(people[0].likes.get(1) === people[1].likes.get(1)); // true (both frank)```
### Mozel dependency injection
```typescript
let rome = new MozelFactory();
let egypt = new MozelFactory();// Definitions
class Roman extends Mozel {
static get type() {
return 'Person';
}
}
rome.register(Roman);class Egyptian extends Mozel {
static get type() {
return 'Person'
}
}
egypt.register(Egyptian);// Instances
let data = {_type: 'Person'};
let roman = rome.create(data);
let egyptian = egypt.create(data);console.log(roman instanceof Roman); // true
console.log(egyptian instanceof Egyptian); // true```
#### Templating
Mozel properties that are strings can contain simple template placeholders, denoted by curly brackets. These placeholders
can be filled in with `mozel.$renderTemplates(data)`, where `data` is an object containing the relevant keys. Example:```typescript
class Person extends Mozel {
@property(String, {required})
name!:string;@property(Person)
child?:Person
}let james = Person.create({
name: 'James {lastName}',
child: {
name: 'Fred {lastName}'
}
})james.$renderTemplates({lastName: 'Smith'})
console.log(james.name); // James Smith
console.log(james.child.name); // Fred Smith
```### Logging
Mozel has fine-grained logging controls, based on the [Log Control](https://www.npmjs.com/package/log-control) library.
For example, it is possible to change the log levels for Mozel, or use a custom driver rather than `console`:```typescript
Mozel.log.setLevel(LogLevel.OFF);
Mozel.log.setDriver({
trace(...args:any[]){ /* ... */ },
debug(...args:any[]){ /* ... */ },
log(...args:any[]){ /* ... */ },
info(...args:any[]){ /* ... */ },
warn(...args:any[]){ /* ... */ },
error(...args:any[]){ /* ... */ }
});
```