functional-models 2.1.14 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -4,392 +4,575 @@
4
4
  ![Feature Tests](https://github.com/monolithst/functional-models/actions/workflows/feature.yml/badge.svg?branch=master)
5
5
  [![Coverage Status](https://coveralls.io/repos/github/monolithst/functional-models/badge.svg?branch=master)](https://coveralls.io/github/monolithst/functional-models?branch=master)
6
6
 
7
- Love functional javascript/typescript but still like composing objects/models? This is the library for you.
7
+ <br>
8
+ <img src="./docs/images/chocolate.png" alt="drawing" width="200"/>
9
+ </br>
8
10
 
9
- This library empowers the creation of pure JavaScript function based models that can be used on a client, a web frontend, and/or a backend all the same time. Use this library to create models that can be reused everywhere.
11
+ ## Making System Building Fun
10
12
 
11
- This library is fully supportive of both Typescript and Javascript. In fact, the typescript empowers some really sweet dynamic type checking, and autocomplete! It handles validation as well.
13
+ Does this sound like you?
12
14
 
13
- Functional Models was born out of the enjoyment and power of working with Django models.
15
+ > I want to code once, use it everywhere, and auto-generate my entire system
16
+
17
+ If so this is the framework for you.
18
+
19
+ Functional Models empowers the creation of pure TypeScript/JavaScript function based models that can be used on a client, a web frontend, and/or a backend all the same time. Use this library to create models that can be reused <b>EVERYWHERE</b>.
20
+
21
+ Write validation code, metadata, property descriptions, and more! Functional Models is fully supportive of both TypeScript and JavaScript. In fact, the typescript empowers some really sweet dynamic type checking, and autocomplete!
22
+
23
+ This framework was born out of the enjoyment and power of working with Django models, but, restricting their "god-like abilities" which can cause developers to make a spaghetti system that is nearly impossible to optimize or improve without starting from scratch.
24
+
25
+ <b>Functional models is ooey gooey framework for building and using awesome models EVERYWHERE.</b>
14
26
 
15
27
  # Primary Features
16
28
 
17
- - Define models that have properties, methods, and methods on the model itself.
18
- - Create instances of models
19
- - Validate model instances
20
- - ORM ready via the functional-models-orm package, with DynamoDb, Mongo, in-memory datastores supported.
21
- - Many common properties out of the box.
22
- - Supports foreign keys, 1 to 1 as well as 1 to many (via an Array).
23
- - Supports custom primary key name. (id is used by default)
29
+ - Define models that have robust properties and are scoped to a namespace or app
30
+ - Robust typing system for TypeScript goodness.
31
+ - Same modeling code can be used on front end and backends.
32
+ - Validate model data
33
+ - ORM ready via the [functional-models-orm](https://github.com/monolithst/functional-models-orm) package. Available Datastores: DynamoDb, Mongo, in-memory, elastic/opensearch, Sqlite, Postgres, Mysql
34
+ - Most common properties out of the box.
35
+ - Supports "foreign keys", 1 to 1 as well as 1 to many (via an Array).
36
+ - Models support custom primary key name. (id is used by default)
24
37
  - Supports different model namings, (plural, singular, display), and the ability to customize them.
38
+ - Add Api Information that can be used for auto-generating frontend and backend code as well as documentation.
39
+
40
+ # Table Of Contents
41
+
42
+ - [Version 3.0 Updates](#the-big-30-updates)
43
+ - [Simple JavasScript Example](#simple-javascript-example-usage)
44
+ - [Simple TypeScript Example](#simple-typescript-example-usage)
45
+ - [Validation](#validation)
46
+ - [Properties](#properties)
47
+ - [List of Properties](#list-of-properties-out-of-the-box)
48
+
49
+ ## The Big 3.0 Updates
50
+
51
+ Version 3 is a major update that changes most of the primary interfaces from Version 2. This version should be simpler to extend (see the companion library [Functional Models Orm](https://github.com/monolithst/functional-models-orm)) making models much easier to reuse across front and back ends. Here is a non-exhaustive list.
52
+
53
+ - Model/ModelInstance/ModelFactory types have been reworked, so that they are simpler and much easier to extend
54
+ - Some "automagical" stuff has been removed, because experience has shown them to be more of a hassle than they were worth.
55
+ - Interfaces for ModelType/ModelInstance/ModelDefinitions have been reworked
56
+ - API Endpoint information can be added to a ModelDefinition
57
+ - Additional Value Types added for better differentiation downstream. (Date/Datetime, Text/BigText, etc)
58
+ - Removes dependency on date-fns to lighten the install, and prevent duplicate dependencies.
59
+ - Memoized computations to reduce expensive recalculations.
60
+ - Promise types allowed in model type. (Perfect for asynchronous loaded properties)
25
61
 
26
62
  ## Simple JavaScript Example Usage
27
63
 
28
- const {
29
- BaseModel: Model,
30
- DateProperty,
31
- NumberProperty,
32
- TextProperty,
33
- } = require('functional-models')
34
-
35
- // Create your model. Standard is to use a plural name.
36
- const Trucks = Model('Trucks', {
37
- properties: {
38
- // id: UniqueId(), # this property is provided for free!
39
- make: TextProperty({ maxLength: 20, minLength: 3, required: true}),
40
- model: TextProperty({ maxLength: 20, minLength: 3, required: true}),
41
- color: TextProperty({
42
- maxLength: 10,
43
- minLength: 3,
44
- choices: ['red', 'green', 'blue', 'black', 'white'],
45
- }),
46
- year: NumberProperty({ maxValue: 2500, minValue: 1900}),
47
- lastModified: DateProperty({ autoNow: true}),
48
- }
49
- })
50
-
51
- // Create an instance of the model.
52
- const myTruck = Trucks.create({ make: 'Ford', model: 'F-150', color: 'white', year: 2013})
53
-
54
-
55
- // Get the properties of the model instance.
56
- console.log(await myTruck.get.id()) // a random uuid NOTE: this is a promise by default
57
- console.log(myTruck.get.make()) // 'Ford'
58
- console.log(myTruck.get.model()) // 'F-150'
59
- console.log(myTruck.get.color()) // 'white'
60
- console.log(myTruck.get.year()) // 2013
61
-
62
- // Get a raw javascript object representation of the model.
63
- const obj = await myTruck.toObj()
64
- console.log(obj)
65
- /*
66
- {
67
- "id": "a-random-uuid",
68
- "make": "Ford",
69
- "model": "F-150",
70
- "color": "white",
71
- "year": 2013
72
- }
73
- */
74
-
75
- // Create a copy of the model from the raw javascript object.
76
- const sameTruck = Truck.create(obj)
77
- console.log(await myTruck.get.id()) // same as above.
78
- console.log(myTruck.get.make()) // 'Ford'
79
- console.log(myTruck.get.model()) // 'F-150'
80
- console.log(myTruck.get.color()) // 'white'
81
- console.log(myTruck.get.year()) // 2013
82
-
83
- // Validate the model. An empty object, means no errors.
84
- const errors = await sameTruck.validate()
85
- console.log(errors) // {}
86
-
87
- const newTruck = Truck({ make: 'Ford', model: 'F-150', color: 'white', year: 20130})
88
- const errors2 = await newTruck.validate()
89
- console.log(errors2)
90
-
91
- // Key is the property's name, and an array of validation errors for that property.
92
- // {"year": ['Value is too long']}
64
+ ```javascript
65
+ const {
66
+ Model,
67
+ DatetimeProperty,
68
+ NumberProperty,
69
+ TextProperty,
70
+ PrimaryKeyUuidProperty,
71
+ } = require('functional-models')
72
+
73
+ // Create your model. Our recommended standard is to use a plural uppercase name for your variable. (You are creating a Model factory)
74
+ const Trucks = Model({
75
+ pluralName: 'Trucks',
76
+ namespace: '@my-package/cars',
77
+ properties: {
78
+ id: PrimaryKeyUuidProperty(),
79
+ make: TextProperty({ maxLength: 20, minLength: 3, required: true }),
80
+ model: TextProperty({ maxLength: 20, minLength: 3, required: true }),
81
+ color: TextProperty({
82
+ maxLength: 10,
83
+ minLength: 3,
84
+ choices: ['red', 'green', 'blue', 'black', 'white'],
85
+ }),
86
+ year: NumberProperty({ maxValue: 2500, minValue: 1900 }),
87
+ lastModified: DatetimeProperty({ autoNow: true }),
88
+ },
89
+ })
90
+
91
+ // Create an instance of the model. In this case, you don't need 'id', because it gets created automatically with UniquePropertyId()
92
+ const myTruck = Trucks.create({
93
+ make: 'Ford',
94
+ model: 'F-150',
95
+ color: 'white',
96
+ year: 2013,
97
+ })
98
+
99
+ // Get the properties of the model instance.
100
+ console.log(myTruck.get.id()) // a auto generated uuid
101
+ console.log(myTruck.get.make()) // 'Ford'
102
+ console.log(myTruck.get.model()) // 'F-150'
103
+ console.log(myTruck.get.color()) // 'white'
104
+ console.log(myTruck.get.year()) // 2013
105
+
106
+ // Get a raw javascript object representation of the model.
107
+ const obj = await myTruck.toObj()
108
+ console.log(obj)
109
+ /*
110
+ {
111
+ "id": "3561e6c5-422d-46c7-954f-f7261b11d3d4",
112
+ "make": "Ford",
113
+ "model": "F-150",
114
+ "color": "white",
115
+ "year": 2013
116
+ }
117
+ */
118
+
119
+ // Create a copy of the model from the raw javascript object.
120
+ const sameTruck = Truck.create(obj)
121
+ console.log(myTruck.get.id()) // same as above.
122
+ console.log(myTruck.get.make()) // 'Ford'
123
+ console.log(myTruck.get.model()) // 'F-150'
124
+ console.log(myTruck.get.color()) // 'white'
125
+ console.log(myTruck.get.year()) // 2013
126
+
127
+ // Validate the model. Undefined, means no errors.
128
+ const errors = await sameTruck.validate()
129
+ console.log(errors) // undefined
130
+
131
+ const newTruck = Truck({
132
+ make: 'Ford',
133
+ model: 'F-150',
134
+ color: 'white',
135
+ year: 20130,
136
+ })
137
+ const errors2 = await newTruck.validate()
138
+ console.log(errors2)
139
+
140
+ // Key is the property's name, and an array of validation errors for that property.
141
+ // {"year": ['Value is too long']}
142
+ ```
93
143
 
94
144
  ## Simple TypeScript Example Usage
95
145
 
96
- While functional-mode3ls works very well and easy without TypeScript, using typescript empowers
146
+ While functional-models works very well and easy without TypeScript, using typescript empowers
97
147
  modern code completion engines to show the properties/methods on models and model instances.
98
- Libraries built ontop of functional-models is encouraged to use TypeScript, while applications,
148
+ Libraries built on top of functional-models is encouraged to use TypeScript, while applications,
99
149
  may or may not be as useful, given the overhead of typing. NOTE: Behind the covers functional-models
100
150
  typing, is extremely strict, and verbose, which can make it somewhat difficult to work with, but
101
- it provides the backbone of expressive and clear typing.
102
-
103
- import {
104
- BaseModel as Model,
105
- DateProperty,
106
- NumberProperty,
107
- TextProperty,
108
- } from 'functional-models'
109
-
110
- // Create your model's type
111
- type TruckType = {
112
- /*
113
- * id: string # this property is provided for free. No need to put. If using a custom
114
- * primary key, the property needs to be explicit.
115
- */
116
- make: string,
117
- model: string,
118
- color: string,
119
- year?: number, // NOTE: Clearly indicates a property is optional.
120
- lastModified: Date,
121
- }
122
-
123
- // Create your model. Standard is to use a plural name.
124
- const Trucks = Model<TruckType>('Trucks', {
125
- properties: {
126
- // id: UniqueId(), # this property is provided for free!
127
- make: TextProperty({ maxLength: 20, minLength: 3, required: true}),
128
- model: TextProperty({ maxLength: 20, minLength: 3, required: true}),
129
- color: TextProperty({
130
- maxLength: 10,
131
- minLength: 3,
132
- choices: ['red', 'green', 'blue', 'black', 'white'],
133
- }),
134
- year: NumberProperty({ maxValue: 2500, minValue: 1900}),
135
- lastModified: DateProperty({ autoNow: true}),
136
- }
137
- })
138
-
139
- // Create an instance of the model.
140
- const myTruck = Trucks.create({ make: 'Ford', model: 'F-150', color: 'white', year: 2013})
141
-
142
- // Will cause a typescript error because "make" is a string, not a number.
143
- const myTruck2 = Trucks.create({ make: 5, model: 'F-150', color: 'white', year: 2013})
144
-
145
- // Will NOT cause a typescript error because year is optional.
146
- const myTruck = Trucks.create({ make: 'Ford', model: 'F-150', color: 'white' })
147
-
148
-
149
- // Will cause a typescript error because model is not implemented, even though its required.
150
- const Trucks2 = Model<TruckType>('Trucks2', {
151
- properties: {
152
- make: TextProperty({ maxLength: 20, minLength: 3, required: true}),
153
- color: TextProperty({
154
- maxLength: 10,
155
- minLength: 3,
156
- choices: ['red', 'green', 'blue', 'black', 'white'],
157
- }),
158
- year: NumberProperty({ maxValue: 2500, minValue: 1900}),
159
- lastModified: DateProperty({ autoNow: true}),
160
- }
161
- })
151
+ it provides the backbone of expressive and clear typing that "just works" for nearly all situations.
162
152
 
163
- ## Validation
153
+ ```typescript
154
+ import {
155
+ Model,
156
+ DatetimeProperty,
157
+ NumberProperty,
158
+ TextProperty,
159
+ PrimaryKeyUuidProperty,
160
+ } from 'functional-models'
161
+
162
+ // Create an object type. NOTE: Singular Uppercase
163
+ type VehicleMake = {
164
+ id: string // Our primary key
165
+ name: string // A simple text name of the maker
166
+ }
164
167
 
165
- Validation is baked into the functional-models framwork. Both individual properties as well as an entire model instance can be covered by validators. The following are the interfaces for a validator. Validation overall is a combination of property validator components as well as model validator components. These components combine together to create a complete validation picture of a model.
168
+ // Create your main type that has reference to another type.
169
+ type Vehicle = {
170
+ id: string // Our primary key
171
+ make: ModelReference<VehicleMake> // A reference to another model.
172
+ model: string // A simple text field
173
+ color: Promise<string> // A property that requires asynchronous to create.
174
+ year?: number // An optional number property. We enforce this is an integer in the Model Definition
175
+ lastModified?: DateValueType // A Date|string
176
+ history?: string // A complex text data type.
177
+ }
166
178
 
167
- Here is an example of a model instance failing validation.
179
+ // Create a model for the VehicleMake type. NOTE: Plural and Uppercase.
180
+ const VehicleMakes = Model<VehicleMake>({
181
+ pluralName: 'VehicleMakes',
182
+ namespace: '@my-package/cars',
183
+ properties: {
184
+ id: PrimaryKeyUuidProperty(),
185
+ name: TextProperty({ required: true }),
186
+ },
187
+ })
168
188
 
169
- // Call .validate() on the model, and await its result.
170
- const errors = await myModelInstance.validate()
189
+ // Create a model for the Vehicle type
190
+ const Vehicles = Model<Vehicle>({
191
+ pluralName: 'Vehicles',
192
+ namespace: '@my-package/cars',
193
+ properties: {
194
+ id: PrimaryKeyUuidProperty(),
195
+ model: TextProperty({
196
+ maxLength: 20,
197
+ minLength: 3,
198
+ required: true,
199
+ }),
200
+ color: TextProperty({
201
+ maxLength: 10,
202
+ minLength: 3,
203
+ choices: ['red', 'green', 'blue', 'black', 'white'],
204
+ }),
205
+ year: IntegerProperty({
206
+ maxValue: 2500,
207
+ minValue: 1900,
208
+ }),
209
+ make: ModelReferenceProperty<VehicleMake>(VehicleMakes, { required: true }),
210
+ history: BigTextProperty({ required: false }),
211
+ lastModified: DatetimeProperty({ autoNow: true }),
212
+ },
213
+ })
171
214
 
172
- console.log(errors)
215
+ // Create an instance of our Make. NOTE: The 'id' is to tell the create factory to ignore the id field as it is auto-generated.
216
+ const ford = VehicleMakes.create<'id'>({
217
+ name: 'Ford',
218
+ })
219
+
220
+ // GOOD: Create an instance of the model.
221
+ const myTruck = Vehicles.create<'id'>({
222
+ make: ford,
223
+ model: 'F-150',
224
+ color: 'white',
225
+ year: 2013,
226
+ })
227
+
228
+ // GOOD: You can also use the id to set the make
229
+ const myTruck2 = Vehicles.create<'id'>({
230
+ make: ford.get.id(),
231
+ model: 'F-150',
232
+ color: 'white',
233
+ year: 2013,
234
+ })
173
235
 
174
- /*
175
- * {
176
- * "overall": [
177
- * "This is a custom model validator that failed for the entire model",
178
- * "Here is a second model failing message",
179
- * ],
180
- * "aDateProperty": [
181
- * "A value is required",
182
- * "Value is not a date",
183
- * ],
184
- * "anArrayProperty": [
185
- * "BadChoice is not a valid choice",
186
- * ],
187
- * // the 'otherProperty' did not fail, and therefore is not shown here.
188
- * }
189
- */
236
+ // GOOD: Will NOT cause a typescript error because year is optional.
237
+ const myTruck4 = Vehicles.create<'id'>({
238
+ make: ford,
239
+ model: 'F-150',
240
+ color: 'white',
241
+ })
242
+
243
+ // ERROR: Will cause a typescript error because year must be a string.
244
+ const myTruck3 = Vehicles.create<'id'>({
245
+ make: ford,
246
+ model: 'F-150',
247
+ color: 'white',
248
+ year: '2013', // must be a string
249
+ })
250
+ ```
251
+
252
+ ## Validation
253
+
254
+ Validation is baked into the functional-models framework. Both individual properties and an entire model instance can be covered by validators. The following are the interfaces for a validator. Validation overall is a combination of property validator components as well as model validator components. These components combine to create a complete validation picture of a model.
255
+
256
+ When calling validate, you either get undefined (for passing), or you get an object that shows you the errors at both the model and the individual property level.
257
+
258
+ Here is an example of validating different model instances:
259
+
260
+ ```javascript
261
+ // Call .validate() on the instance, and await its result.
262
+ const errors = await myModelInstance.validate()
263
+
264
+ console.log(errors)
265
+
266
+ /*
267
+ * {
268
+ * "overall": [
269
+ * "This is a custom model validator that failed for the entire model",
270
+ * "Here is a second model failing message",
271
+ * ],
272
+ * "aDateProperty": [
273
+ * "A value is required",
274
+ * "Value is not a date",
275
+ * ],
276
+ * "anArrayProperty": [
277
+ * "BadChoice is not a valid choice",
278
+ * ],
279
+ * // the 'otherProperty' did not fail, and therefore is not shown here.
280
+ * }
281
+ */
282
+
283
+ // Here is one that passes
284
+ const errors2 = await anotherInstance.validate()
285
+
286
+ console.log(errors)
287
+ /*
288
+ undefined
289
+ */
290
+ ```
190
291
 
191
292
  ### Property validators
192
293
 
193
- A property validator validates the value of a property. The inputs are the value, a model instance, the JavaScript object representation of the model, and optional configurations that are passed into the validator. The return can either be a string error or undefined if there are no errors. This function can be asynchronous, such as doing database lookups.
194
-
195
- /**
196
- * An example validator function that only allows the value of 5.
197
- * @constructor
198
- * @param {string} value - The value to be tested.
199
- * @param {string} instance - A model instance, which can be used for cross referencing.
200
- * @param {string} instanceData - The JavaScript object representation of the model.
201
- * @param {string} configurations - An optional configuration object passed in as part of validating.
202
- * @return {string|undefined} - If error, returns a string, otherwise returns undefined.
203
- */
204
- const valueIsFiveValidator = (
205
- value, // any kind of value.
206
- instance, // A ModelInstance,
207
- instanceData: JavaScript object representation,
208
- configurations: {}
209
- ) => {
210
- return value === 5
211
- ? undefined
212
- : 'Value is not 5'
213
- }
214
-
215
- /**
216
- * An example async validator function that checks a database using an object passed into the configurations.
217
- * @constructor
218
- * @param {string} value - The value to be tested.
219
- * @param {string} instance - A model instance, which can be used for cross referencing.
220
- * @param {string} instanceData - The JavaScript object representation of the model.
221
- * @param {string} configurations - An optional configuration object passed in as part of validating.
222
- * @return {Promise<string|undefined>} - Returns a promise, If error, returns a string, otherwise returns undefined.
223
- */
224
- const checkDatabaseError = async (
225
- value, // any kind of value.
226
- instance, // A ModelInstance,
227
- instanceData: JavaScript object representation,
228
- configurations: {}
229
- ) => {
230
- const result = await configurations.someDatabaseObj.check(value)
231
- if (result) {
232
- return 'Some sort of database error'
233
- }
234
- return undefined
235
- }
294
+ A property validator validates the value of a property. The inputs are the value, a model instance, the JavaScript object representation of the model, and optional configurations that are passed into the validator. The return can either be a string error or undefined if there are no errors. This function can be asynchronous, such as doing database lookups. An implementation in `functional-models-orm` does a "unique together" database query to make sure that only one entry has the value of two or more properties.
295
+
296
+ ```javascript
297
+ /**
298
+ * An example validator function that only allows the value of 5.
299
+ * @constructor
300
+ * @param {string} value - The value to be tested.
301
+ * @param {string} instance - A model instance, which can be used for cross referencing.
302
+ * @param {string} instanceData - The JavaScript object representation of the model.
303
+ * @param {string} context - An optional context object passed in as part of validating.
304
+ * @return {string|undefined} - If error, returns a string, otherwise returns undefined.
305
+ */
306
+ const valueIsFiveValidator = (
307
+ value, // any kind of value.
308
+ instance, // A ModelInstance,
309
+ instanceData, // JavaScript object representation,
310
+ context = {}
311
+ ) => {
312
+ return value === 5 ? undefined : 'Value is not 5'
313
+ }
314
+
315
+ // A simpler more realistic implementation
316
+ const valueIsFiveValidator2 = value => {
317
+ return value === 5 ? undefined : 'Value is not 5'
318
+ }
319
+
320
+ /**
321
+ * An example async validator function that checks a database using an object passed into the configurations.
322
+ * @constructor
323
+ * @param {string} value - The value to be tested.
324
+ * @param {string} instance - A model instance, which can be used for cross referencing.
325
+ * @param {string} context - The JavaScript object representation of the model.
326
+ * @param {string} configurations - An optional context object passed in as part of validating.
327
+ * @return {Promise<string|undefined>} - Returns a promise, If error, returns a string, otherwise returns undefined.
328
+ */
329
+ const checkDatabaseError = async (
330
+ value, // any kind of value.
331
+ instance, // A ModelInstance,
332
+ instanceData, // JavaScript object representation,
333
+ context = {}
334
+ ) => {
335
+ const result = await context.someDatabaseObj.check(value)
336
+ if (result) {
337
+ return 'Some sort of database error'
338
+ }
339
+ return undefined
340
+ }
341
+ ```
236
342
 
237
343
  ### Model Validators
238
344
 
239
345
  Model validators allows one to check values across a model, ensuring that multiple values work together. The inputs are the model instance, the JavaScript object representation, and optional configurations. The return can either be a string error or undefined if there are no errors. This function can be asynchronous, such as doing database lookups.
240
346
 
241
- /**
242
- * An example model validator that checks to see if two properties have the same value.
243
- * @constructor
244
- * @param {string} instance - A model instance, used for cross referencing.
245
- * @param {string} instanceData - The JavaScript object representation of the model.
246
- * @param {string} configurations - An optional configuration object passed in as part of validating.
247
- * @return {string|undefined} - If error, returns a string, otherwise returns undefined.
248
- */
249
- const checkForDuplicateValues = (
250
- instance, // A ModelInstance,
251
- instanceData: JavaScript object representation,
252
- configurations: {}
253
- ) => {
254
- if(instanceData.firstProperty === instanceData.secondProperty) {
255
- return 'Both properties must have different values'
256
- }
257
- return undefined
258
- }
259
-
260
- ## Properties and Custom Properties
347
+ ```javascript
348
+ /**
349
+ * An example model validator that checks to see if two properties have the same value.
350
+ * @constructor
351
+ * @param {string} instance - A model instance, used for cross referencing.
352
+ * @param {string} instanceData - The JavaScript object representation of the model.
353
+ * @param {string} context - An optional context object passed in as part of validating.
354
+ * @return {string|undefined} - If error, returns a string, otherwise returns undefined.
355
+ */
356
+ const checkForDuplicateValues = (
357
+ instance, // A ModelInstance,
358
+ instanceData, // JavaScript object representation,
359
+ context = {}
360
+ ) => {
361
+ if (instanceData.firstProperty === instanceData.secondProperty) {
362
+ return 'Both properties must have different values'
363
+ }
364
+ return undefined
365
+ }
366
+ ```
367
+
368
+ ## Properties
261
369
 
262
370
  There are numerous properties that are supported out of the box that cover most data modeling needs. It is also very easy to create custom properties that encapsulate unique choices
263
371
  validation requirements, etc.
264
372
 
265
- ### List of Properties Out-Of-The-Box
373
+ ## List of Properties Out-Of-The-Box
266
374
 
267
- #### UniqueId
375
+ ### Dates
268
376
 
269
- A UUID Property.
377
+ #### DateProperty
270
378
 
271
- #### NaturalIdProperty
379
+ A property for handling dates. (Without time)
272
380
 
273
- An id that is composed of other properties on an object. It is "natural" in the sense that it is
274
- not an arbitrary id, but rather a mixture of properties that make up a unique instance. This is often
275
- useful for when creating a "key" property.
381
+ [Documentation](https://monolithst.github.io/functional-models/functions/index.properties.DateProperty.html)
276
382
 
277
- NOTE: This property is never automatically updated if the properties changed. It is recommended that
278
- any model that has a NaturalIdProperty should be deleted and then re-created rather than "updated" if
279
- any property changes that make up the key composition.
383
+ #### DatetimeProperty
280
384
 
281
- #### DateProperty
385
+ A property for handling dates with times.
282
386
 
283
- A property for dates. Includes the ability to "autoNow".
387
+ [Documentation](https://monolithst.github.io/functional-models/functions/index.properties.DatetimeProperty.html)
388
+
389
+ ### Arrays
284
390
 
285
391
  #### ArrayProperty
286
392
 
287
- An array property which can be used to limit types within it.
393
+ A property that can handle multiple values. If you want it to only have a single type, look below at the [SingleTypeArrayProperty](#singletypearrayproperty).
288
394
 
289
- #### IntegerProperty
395
+ [Documentation](https://monolithst.github.io/functional-models/functions/index.properties.ArrayProperty.html)
396
+
397
+ #### SingleTypeArrayProperty
398
+
399
+ A property that can handle multiple values of the same type. This is enforced via validation and by typing.
400
+
401
+ [Documentation](https://monolithst.github.io/functional-models/functions/index.properties.SingleTypeArrayProperty.html)
402
+
403
+ ### Objects
404
+
405
+ #### ObjectProperty
290
406
 
291
- A property for integers.
407
+ A property that can handle "JSON compliant" objects. Simple objects.
408
+
409
+ [Documentation](https://monolithst.github.io/functional-models/functions/index.properties.ObjectProperty.html)
410
+
411
+ ### Text
292
412
 
293
413
  #### TextProperty
294
414
 
295
- A text or string property.
415
+ A property for simple text values. If you want to hold large values look at [BigTextProperty](#bigtextproperty).
296
416
 
297
- #### ConstantValueProperty
417
+ [Documentation](https://monolithst.github.io/functional-models/functions/index.properties.TextProperty.html)
418
+
419
+ #### BigTextProperty
420
+
421
+ A property for holding large text values.
422
+
423
+ [Documentation](https://monolithst.github.io/functional-models/functions/index.properties.BigTextProperty.html)
424
+
425
+ #### EmailProperty
298
426
 
299
- A property that contains a single, unchanging, static value.
427
+ A property that holds Emails.
428
+
429
+ [Documentation](https://monolithst.github.io/functional-models/functions/index.properties.EmailProperty.html)
430
+
431
+ ### Numbers
432
+
433
+ #### IntegerProperty
434
+
435
+ A property that holds integer values. (No floating point).
436
+
437
+ [Documentation](https://monolithst.github.io/functional-models/functions/index.properties.IntegerProperty.html)
438
+
439
+ #### YearProperty
440
+
441
+ An integer property that holds year values.
442
+
443
+ [Documentation](https://monolithst.github.io/functional-models/functions/index.properties.YearProperty.html)
300
444
 
301
445
  #### NumberProperty
302
446
 
303
- A property for float/number types.
447
+ A property that holds floating point numbers.
304
448
 
305
- #### ObjectProperty
449
+ [Documentation](https://monolithst.github.io/functional-models/functions/index.properties.NumberProperty.html)
306
450
 
307
- A property that has a JavaScript object. (Not a foreign key references)
451
+ ### Misc
308
452
 
309
- #### EmailProperty
453
+ #### ConstantValueProperty
310
454
 
311
- An email property.
455
+ A property that has a single value that is hardcoded and can never be changed. Good for encoding values like the model name in the data.
456
+
457
+ [Documentation](https://monolithst.github.io/functional-models/functions/index.properties.ConstantValueProperty.html)
312
458
 
313
459
  #### BooleanProperty
314
460
 
315
- A true or false value property.
461
+ A property that can hold a true or a false.
462
+
463
+ [Documentation](https://monolithst.github.io/functional-models/functions/index.properties.BooleanProperty.html)
464
+
465
+ ### Keys / Primary / Foreign
466
+
467
+ #### PrimaryKeyUuidProperty
468
+
469
+ A property that holds a uuid as a primary key. It is automatically created if not provided.
470
+
471
+ [Documentation](https://monolithst.github.io/functional-models/functions/index.properties.PrimaryKeyUuidProperty.html)
316
472
 
317
473
  #### ModelReferenceProperty
318
474
 
319
- A property that references another property. (Think Foreign Key)
475
+ A property that holds a reference to another model instance. (In database-speak a foreign key). When code requests the value for this property, it is fetched and returns an object. However, when `.toObj()` is called on the model, this reference turns into a id. (number or string)
476
+
477
+ [Documentation](https://monolithst.github.io/functional-models/functions/index.properties.ModelReferenceProperty.html)
320
478
 
321
479
  #### AdvancedModelReferenceProperty
322
480
 
323
- A fuller more advanced property for referencing other properties. Useful for typescripting.
481
+ The underlying implementation for {@link ModelReferenceProperty} that allows Model and ModelInstance expansions. This should only be used if there are certain expanded features that a referenced model needs to have.
482
+
483
+ [Documentation](https://monolithst.github.io/functional-models/functions/index.properties.AdvancedModelReferenceProperty.html)
484
+
485
+ ### Calculated At RunTime
324
486
 
325
487
  #### DenormalizedProperty
326
488
 
327
- A value that is calculated and save if it doesn't exist, using other values for a model instance. This property adds a `isDenormalized:true` to the property's config, as well as a `calculate()` function to the property itself.
489
+ A property that provides a denormalized value. This is the underlying property for other (simpler) denormalized values, and allows you to build your own customized denormalization.
490
+
491
+ All denormalized properties are calculated once and then never again unless requested.
492
+
493
+ [Documentation](https://monolithst.github.io/functional-models/functions/index.properties.DenormalizedProperty.html)
494
+
495
+ #### DenormalizedTextProperty
496
+
497
+ A text property that is denormalized.
498
+
499
+ [Documentation](https://monolithst.github.io/functional-models/functions/index.properties.DenormalizedTextProperty.html)
500
+
501
+ #### DenormalizedNumberProperty
502
+
503
+ A number property that is denormalized and calculated when it doesn't exist.
504
+
505
+ [Documentation](https://monolithst.github.io/functional-models/functions/index.properties.DenormalizedNumberProperty.html)
506
+
507
+ #### DenormalizedIntegerProperty
328
508
 
329
- NOTE: If the value is provided as part of the instance, it is not re-calculated. If you want to re-calculate it, you must use either the property's method `calculate()` method to get the value and replace the existing OR pass in undefined for the property.
509
+ An integer property that is denormalized and calculated when it doesn't exist.
330
510
 
331
- <strong>Incremental data creation such as GUI forms:</strong>
511
+ [Documentation](https://monolithst.github.io/functional-models/functions/index.properties.DenormalizedIntegerProperty.html)
332
512
 
333
- If you are incrementally creating and validating model data, such as a GUI form, you should make your denormalization callback understand that there may be properties that are required, but are not present (yet). What this means, is if you need a particular property's value to be part of the denormalization value, but it isn't there, you should check for the value, and if not there, return undefined. This will allow it to be recalculated later.
513
+ #### NaturalIdProperty
514
+
515
+ A property that represents an id that is composed of other properties on an object. It is "natural" in the sense that it is not an arbitrary id, but rather a mixture of properties that make up a unique instance. This is often useful as a Primary Key where the data is uniquely represented by the combination of multiple values.
334
516
 
335
- <strong>A Strong Word of Caution</strong>
517
+ This can be useful to optimize a situation where you have a "unique together" requirement for model in a database, where there can only be one model that has the same of multiple properties.
336
518
 
337
- Generally, we would recommend not using this as a primary key in a database. However, if you want to use a DenormalizedProperty as primary key in a database and you want to make changes to an instance, you need to delete the previous entry and then recreate it for every update. A dynamic primary key is not tracked between changes.
519
+ NOTE: This property is never automatically updated if the properties changed. It is recommended that
520
+ any model that has a NaturalIdProperty should be deleted and then re-created rather than "updated" if
521
+ any property changes that make up the key composition.
338
522
 
339
- <strong>Example</strong>
523
+ For example if you are making a model for a `Species` and `Genera`, where we want to dynamically create the latinName for Species which a combination of the Genera's latin name with a latin name ending in this format: `GeneraLatinName speciesLatinName`
340
524
 
341
525
  ```typescript
342
- import { DenormalizedProperty } from 'functional-models/src/properties'
343
- import { TypedJsonObj } from 'functional-models/interfaces'
344
-
345
- // Your base data
346
- type Greeting = {
347
- name: 'Dolly',
348
- greeting: 'Hello',
349
- displayName?: string
526
+ type Genus = {
527
+ latinName: string // our key. Only one Genus can have a latinName.
528
+ commonName: string
350
529
  }
351
530
 
352
- // Create your model
353
- const Greetings = Model<Greeting>('Greetings', {
531
+ const Genera = Model<Genus>({
532
+ pluralName: 'Genera',
533
+ singularName: 'Genus',
534
+ primaryKeyName: 'latinName',
354
535
  properties: {
355
- name: TextProperty(),
356
- greeting: TextProperty(),
357
- displayName: DenormalizedProperty<string>("TextProperty", (modelData: Greeting) => {
358
- return `${modelData.greeting} ${modelData.name}`
536
+ latinName: TextProperty({ required: true }),
537
+ commonName: TextProperty({ required: true }),
538
+ },
539
+ })
540
+
541
+ type SpeciesType = {
542
+ latinName: Promise<string>
543
+ genusLatinName: string // We are using a string here, but a ModelReference would probably be better.
544
+ speciesName: string // We are going to combine this with the genusLatinName
545
+ commonName: string
546
+ }
547
+
548
+ const Species = Model<SpeciesType>({
549
+ pluralName: 'Species',
550
+ singularName: 'Species',
551
+ primaryKeyName: 'latinName',
552
+ properties: {
553
+ // We want to combine genusLatinName with speciesName with a space between.
554
+ latinName: NaturalIdProperty({
555
+ propertyKeys: ['genusLatinName', 'speciesName'],
556
+ joiner: ' ',
359
557
  }),
360
- }
558
+ genusLatinName: TextProperty({ required: true }),
559
+ speciesName: TextProperty({ required: true }),
560
+ commonName: TextProperty({ required: true }),
561
+ },
361
562
  })
362
563
 
363
- // Create Your Instance
364
- const instance = Model<Greeting>.create({
365
- name: 'Dolly',
366
- greeting: 'Hello',
564
+ const apples = Genus.create({ latinName: 'Malus', commonName: 'Apples' })
565
+ const domesticApples = Species.create<'latinName'>({
566
+ commonName: 'Apples',
567
+ genusLatinName: apples.latinName,
568
+ speciesName: 'domestica',
367
569
  })
368
570
 
369
- // Let's look at the displayName property
370
- const value = await instance.get.displayName()
371
- console.info(value) // Hello Dolly
571
+ const id = await domesticApples.get.latinName()
572
+ console.info(id)
573
+ // Malus domestica
574
+ ```
372
575
 
373
- // Here is the object as a whole
374
- const data = await instance.toObj()
375
- console.info(data) // { name: 'Dolly', greeting: 'Hello', displayName: 'Hello Dolly' }
576
+ In this situation, the latinName for species is not passed in, but calculated from the two other properties. This becomes the primary key for this object, which is unique.
376
577
 
377
- // DON"T TRY TO CHANGE THE MODEL THIS WAY. It doesn't work.
378
- const newData = {
379
- ...data,
380
- name: 'Fred',
381
- }
382
- const instanceBad = Model<Greeting>.create(newData)
383
- const badValue = await instance.get.displayName()
384
- console.info(badValue) // Hello Dolly - this does not change!!!
385
-
386
- // "A better way"
387
- const newDataGood = {
388
- ...data,
389
- name: 'Fred',
390
- displayName: undefined
391
- }
392
- const instanceGood = Model<Greeting>.create(newDataGood)
393
- const goodValue = await instance.get.displayName()
394
- console.info(goodValue) // Hello Fred - Expected
395
- ```
578
+ [Documentation](https://monolithst.github.io/functional-models/functions/index.properties.NaturalIdProperty.html)