manyfest 1.0.43 → 1.0.44

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.
@@ -0,0 +1,186 @@
1
+ # Schema Manipulation
2
+
3
+ Manyfest provides tools for remapping, merging and auto-generating schemas. These are useful when adapting a schema to different object shapes, combining schemas from multiple sources, or bootstrapping a schema from sample data.
4
+
5
+ ## Address Remapping
6
+
7
+ ### resolveAddressMappings
8
+
9
+ Remap descriptors from one set of addresses to another. This permanently mutates the descriptor object.
10
+
11
+ ```javascript
12
+ const descriptors = {
13
+ 'Address.Of.a': { Hash: 'a', DataType: 'Number' },
14
+ 'Address.Of.b': { Hash: 'b', DataType: 'String' }
15
+ };
16
+
17
+ const mapping = {
18
+ 'a': 'New.Address.Of.a',
19
+ 'b': 'New.Address.Of.b'
20
+ };
21
+
22
+ manifest.schemaManipulations.resolveAddressMappings(descriptors, mapping);
23
+ // descriptors is now:
24
+ // {
25
+ // 'New.Address.Of.a': { Hash: 'a', DataType: 'Number' },
26
+ // 'New.Address.Of.b': { Hash: 'b', DataType: 'String' }
27
+ // }
28
+ ```
29
+
30
+ The mapping keys can be either addresses or hashes. If a key matches a descriptor's address directly, that descriptor is moved. If a key matches a descriptor's hash, the descriptor at that hash's address is moved.
31
+
32
+ If a mapping key matches neither an existing address nor a hash, a new descriptor is created with the key as its hash.
33
+
34
+ ### safeResolveAddressMappings
35
+
36
+ The same operation, but returns a new object instead of mutating the original:
37
+
38
+ ```javascript
39
+ const original = {
40
+ 'old.path': { Hash: 'value', DataType: 'String' }
41
+ };
42
+
43
+ const remapped = manifest.schemaManipulations.safeResolveAddressMappings(original, {
44
+ 'value': 'new.path'
45
+ });
46
+
47
+ // original is unchanged
48
+ // remapped is: { 'new.path': { Hash: 'value', DataType: 'String' } }
49
+ ```
50
+
51
+ ## Merging Schemas
52
+
53
+ ### mergeAddressMappings
54
+
55
+ Combine two sets of descriptors. The destination (first argument) takes precedence when both contain the same address:
56
+
57
+ ```javascript
58
+ const base = {
59
+ 'Name': { DataType: 'String' },
60
+ 'Email': { DataType: 'String' }
61
+ };
62
+
63
+ const extra = {
64
+ 'Email': { DataType: 'String', Required: true },
65
+ 'Phone': { DataType: 'String' }
66
+ };
67
+
68
+ const merged = manifest.schemaManipulations.mergeAddressMappings(base, extra);
69
+ // {
70
+ // 'Name': { DataType: 'String' },
71
+ // 'Email': { DataType: 'String' }, <-- base wins (no Required)
72
+ // 'Phone': { DataType: 'String' } <-- added from extra
73
+ // }
74
+ ```
75
+
76
+ Returns a new object. Neither input is mutated.
77
+
78
+ ## Auto-Generating Schemas
79
+
80
+ ### generateAddressses
81
+
82
+ Generate a schema from a sample object. This recursively walks the object and produces a descriptor for every reachable address:
83
+
84
+ ```javascript
85
+ const sample = {
86
+ name: 'Alice',
87
+ age: 30,
88
+ address: {
89
+ city: 'Portland',
90
+ zip: '97201'
91
+ },
92
+ tags: ['admin', 'user']
93
+ };
94
+
95
+ const generated = manifest.objectAddressGeneration.generateAddressses(sample);
96
+ // {
97
+ // 'name': { Address: 'name', Hash: 'name', Name: 'name', DataType: 'String', Default: 'Alice', InSchema: false },
98
+ // 'age': { Address: 'age', Hash: 'age', Name: 'age', DataType: 'Number', Default: 30, InSchema: false },
99
+ // 'address': { Address: 'address', Hash: 'address', Name: 'address', DataType: 'Object', InSchema: false },
100
+ // 'address.city':{ Address: 'address.city', Hash: 'address.city', Name: 'address.city', DataType: 'String', Default: 'Portland', InSchema: false },
101
+ // 'address.zip': { Address: 'address.zip', Hash: 'address.zip', Name: 'address.zip', DataType: 'String', Default: '97201', InSchema: false },
102
+ // 'tags': { Address: 'tags', Hash: 'tags', Name: 'tags', DataType: 'Array', InSchema: false },
103
+ // 'tags[0]': { Address: 'tags[0]', Hash: 'tags[0]', Name: 'tags[0]', DataType: 'String', Default: 'admin', InSchema: false },
104
+ // 'tags[1]': { Address: 'tags[1]', Hash: 'tags[1]', Name: 'tags[1]', DataType: 'String', Default: 'user', InSchema: false }
105
+ // }
106
+ ```
107
+
108
+ Every generated descriptor has `InSchema: false`, which acts as a flag for tooling to prompt a developer to opt-in before including it in a final schema.
109
+
110
+ Data types are inferred from JavaScript types:
111
+ - `string` becomes `String`
112
+ - `number` and `bigint` become `Number`
113
+ - Arrays become `Array` (with each element also generated)
114
+ - Objects become `Object` (with each property also generated)
115
+ - `null` and `undefined` become `Any`
116
+ - Functions and Symbols are skipped
117
+
118
+ ## Use Cases
119
+
120
+ ### Adapting a Schema to a Different API Shape
121
+
122
+ ```javascript
123
+ const baseSchema = {
124
+ 'user.name': { Hash: 'UserName', DataType: 'String' },
125
+ 'user.email': { Hash: 'UserEmail', DataType: 'String' },
126
+ 'user.role': { Hash: 'UserRole', DataType: 'String' }
127
+ };
128
+
129
+ // API v2 returns data in a flat shape
130
+ const v2Mapping = {
131
+ 'UserName': 'display_name',
132
+ 'UserEmail': 'email_address',
133
+ 'UserRole': 'access_level'
134
+ };
135
+
136
+ const v2Schema = manifest.schemaManipulations.safeResolveAddressMappings(baseSchema, v2Mapping);
137
+ // v2Schema addresses are now: display_name, email_address, access_level
138
+ // Hashes remain: UserName, UserEmail, UserRole
139
+ ```
140
+
141
+ ### Bootstrapping a Schema from Sample Data
142
+
143
+ ```javascript
144
+ // Take a sample API response
145
+ const sampleResponse = await fetch('/api/users/1').then(r => r.json());
146
+
147
+ // Generate all possible addresses
148
+ const generated = manifest.objectAddressGeneration.generateAddressses(sampleResponse);
149
+
150
+ // Pick the ones you need and build a proper schema
151
+ const schema = new libManyfest({
152
+ Scope: 'User',
153
+ Descriptors: {
154
+ 'data.id': { ...generated['data.id'], InSchema: true, Required: true },
155
+ 'data.name': { ...generated['data.name'], InSchema: true, Name: 'Display Name' },
156
+ 'data.email': { ...generated['data.email'], InSchema: true, Name: 'Email' }
157
+ }
158
+ });
159
+ ```
160
+
161
+ ### Combining Base and Extension Schemas
162
+
163
+ ```javascript
164
+ const coreFields = {
165
+ 'ID': { DataType: 'Integer', Required: true },
166
+ 'Name': { DataType: 'String', Required: true },
167
+ 'Created': { DataType: 'DateTime' }
168
+ };
169
+
170
+ const auditFields = {
171
+ 'CreatedBy': { DataType: 'String' },
172
+ 'ModifiedBy': { DataType: 'String' },
173
+ 'Modified': { DataType: 'DateTime' }
174
+ };
175
+
176
+ const fullSchema = manifest.schemaManipulations.mergeAddressMappings(coreFields, auditFields);
177
+ // Contains all six fields
178
+ ```
179
+
180
+ ## Notes
181
+
182
+ - `resolveAddressMappings` mutates the input object; use `safeResolveAddressMappings` when you need to preserve the original
183
+ - `mergeAddressMappings` gives precedence to the first (destination) argument on address conflicts
184
+ - `generateAddressses` produces every reachable address including individual array elements -- the output can be large for complex objects
185
+ - Generated schemas use `InSchema: false` as a signal that the descriptor was auto-generated and has not been reviewed
186
+ - Symbols and functions in source objects are silently skipped during generation
package/docs/schema.md ADDED
@@ -0,0 +1,319 @@
1
+ # Schema Definition
2
+
3
+ A manyfest schema describes the structure, types and metadata for the elements of an object. Schemas power validation, default population and hash-based lookups.
4
+
5
+ ## Access
6
+
7
+ ```javascript
8
+ const libManyfest = require('manyfest');
9
+
10
+ // From a definition object
11
+ const manifest = new libManyfest({
12
+ Scope: 'Animal',
13
+ Descriptors: {
14
+ 'IDAnimal': { Name: 'Database ID', DataType: 'Integer', Default: 0 },
15
+ 'Name': { Description: 'The species name.' }
16
+ }
17
+ });
18
+
19
+ // Or build one programmatically
20
+ const manifest = new libManyfest();
21
+ manifest.scope = 'Animal';
22
+ manifest.addDescriptor('IDAnimal', { Name: 'Database ID', DataType: 'Integer', Default: 0 });
23
+ manifest.addDescriptor('Name', { Description: 'The species name.' });
24
+ ```
25
+
26
+ ## Scope
27
+
28
+ The scope is a label that identifies what kind of data this manifest describes. It is used in log messages and has no functional effect on address resolution.
29
+
30
+ ```javascript
31
+ const manifest = new libManyfest({
32
+ Scope: 'Invoice',
33
+ Descriptors: { /* ... */ }
34
+ });
35
+
36
+ manifest.scope; // 'Invoice'
37
+ ```
38
+
39
+ If no scope is provided, it defaults to `'DEFAULT'`.
40
+
41
+ ## Descriptors
42
+
43
+ Each key in the `Descriptors` object is an address. The value is a descriptor object with metadata about that element.
44
+
45
+ ```javascript
46
+ {
47
+ Scope: 'User',
48
+ Descriptors: {
49
+ 'Email': {
50
+ Hash: 'email',
51
+ Name: 'Email Address',
52
+ NameShort: 'Email',
53
+ Description: 'The primary contact email for this user.',
54
+ DataType: 'String',
55
+ Required: true,
56
+ Default: ''
57
+ }
58
+ }
59
+ }
60
+ ```
61
+
62
+ ### Descriptor Properties
63
+
64
+ | Property | Type | Description |
65
+ |----------|------|-------------|
66
+ | **Hash** | `string` | Short identifier for hash-based lookups. Defaults to the address if not set. |
67
+ | **Name** | `string` | Human-readable name for the element. |
68
+ | **NameShort** | `string` | Abbreviated name for compact displays (tables, charts, logs). |
69
+ | **Description** | `string` | Longer description of what this element represents. |
70
+ | **DataType** | `string` | The expected data type. Used for validation and default values. |
71
+ | **Required** | `boolean` | When `true`, validation reports an error if this element is missing. |
72
+ | **Default** | `any` | The value returned when the element is not present in an object. |
73
+ | **Address** | `string` | Auto-populated with the descriptor's key if not explicitly set. |
74
+
75
+ All properties are optional. A descriptor can be as minimal as an empty object:
76
+
77
+ ```javascript
78
+ {
79
+ Descriptors: {
80
+ 'Name': {} // Valid -- address is 'Name', hash defaults to 'Name'
81
+ }
82
+ }
83
+ ```
84
+
85
+ ## Data Types
86
+
87
+ | DataType | JavaScript Type | Default Value | Description |
88
+ |----------|----------------|---------------|-------------|
89
+ | `String` | `string` | `""` | A text value |
90
+ | `Integer` | `number` | `0` | A whole number (no decimal point) |
91
+ | `Float` | `number` | `0.0` | A floating point number |
92
+ | `Number` | `number` | `0` | Any numeric value |
93
+ | `PreciseNumber` | `string` | `"0.0"` | Arbitrary precision number stored as a string |
94
+ | `Boolean` | `boolean` | `false` | `true` or `false` |
95
+ | `Binary` | `number` | `0` | A boolean represented as `1` or `0` |
96
+ | `YesNo` | `string` | - | A boolean represented as `"Y"` or `"N"` |
97
+ | `DateTime` | varies | `0` | A value parsable by JavaScript's `Date` constructor |
98
+ | `Key` | varies | - | A two-part key with ID and GUID (see below) |
99
+ | `Array` | `array` | `[]` | A JavaScript array |
100
+ | `Object` | `object` | `{}` | A JavaScript object |
101
+ | `Null` | `null` | `null` | An explicit null value |
102
+
103
+ ### The Key Data Type
104
+
105
+ Keys represent a paired identifier: an integer ID and a string GUID. This is useful for database records that have both a local numeric identifier and a globally unique string identifier.
106
+
107
+ ```javascript
108
+ {
109
+ Descriptors: {
110
+ 'IDBook': {
111
+ Hash: 'IDBook',
112
+ Name: 'Book Identifier',
113
+ DataType: 'Key',
114
+ KeyRepresentation: 'ID',
115
+ GUIDAddress: 'GUIDBook',
116
+ IDAddress: 'IDBook'
117
+ },
118
+ 'GUIDBook': {
119
+ Hash: 'GUIDBook',
120
+ Name: 'Book GUID',
121
+ DataType: 'Key',
122
+ KeyRepresentation: 'GUID',
123
+ GUIDAddress: 'GUIDBook',
124
+ IDAddress: 'IDBook'
125
+ }
126
+ }
127
+ }
128
+ ```
129
+
130
+ The `KeyRepresentation` indicates which half of the pair this descriptor points to. The `GUIDAddress` and `IDAddress` properties link the two together.
131
+
132
+ ## Adding Descriptors
133
+
134
+ ### addDescriptor
135
+
136
+ Add a single descriptor to an existing manifest:
137
+
138
+ ```javascript
139
+ manifest.addDescriptor('Profile.Phone', {
140
+ Name: 'Phone Number',
141
+ DataType: 'String',
142
+ Required: false
143
+ });
144
+ ```
145
+
146
+ If a descriptor already exists at that address, it is replaced. The `Address` property is auto-set if not provided. The `Hash` property defaults to the address if not provided.
147
+
148
+ Returns `true` on success, `false` if the descriptor is not a valid object.
149
+
150
+ ## Retrieving Descriptors
151
+
152
+ ### getDescriptor
153
+
154
+ Get a descriptor by its address:
155
+
156
+ ```javascript
157
+ const desc = manifest.getDescriptor('Profile.Phone');
158
+ // { Name: 'Phone Number', DataType: 'String', Required: false, Address: 'Profile.Phone', Hash: 'Profile.Phone' }
159
+ ```
160
+
161
+ ### getDescriptorByHash
162
+
163
+ Get a descriptor by its hash:
164
+
165
+ ```javascript
166
+ const manifest = new libManyfest({
167
+ Scope: 'Config',
168
+ Descriptors: {
169
+ 'Database.Connection.Host': { Hash: 'DBHost', DataType: 'String' }
170
+ }
171
+ });
172
+
173
+ manifest.getDescriptorByHash('DBHost');
174
+ // { Hash: 'DBHost', DataType: 'String', Address: 'Database.Connection.Host' }
175
+ ```
176
+
177
+ ### eachDescriptor
178
+
179
+ Iterate over all descriptors:
180
+
181
+ ```javascript
182
+ manifest.eachDescriptor((pDescriptor) => {
183
+ console.log(`${pDescriptor.Address}: ${pDescriptor.Name || '(unnamed)'}`);
184
+ });
185
+ ```
186
+
187
+ ## Serialization
188
+
189
+ ### serialize
190
+
191
+ Convert the manifest to a JSON string for storage or transmission:
192
+
193
+ ```javascript
194
+ const json = manifest.serialize();
195
+ // '{"Scope":"User","Descriptors":{...},"HashTranslations":{...}}'
196
+ ```
197
+
198
+ ### deserialize
199
+
200
+ Load a manifest from a JSON string:
201
+
202
+ ```javascript
203
+ const manifest = new libManyfest();
204
+ manifest.deserialize(json);
205
+ ```
206
+
207
+ ### getManifest
208
+
209
+ Get the manifest state as a plain object:
210
+
211
+ ```javascript
212
+ const state = manifest.getManifest();
213
+ // { Scope: 'User', Descriptors: {...}, HashTranslations: {...} }
214
+ ```
215
+
216
+ ### clone
217
+
218
+ Create a deep copy of the manifest:
219
+
220
+ ```javascript
221
+ const copy = manifest.clone();
222
+ // Independent copy -- changes to one don't affect the other
223
+ ```
224
+
225
+ ## Default Values
226
+
227
+ When a descriptor defines a `Default`, that value is returned by `getValueAtAddress` and `getValueByHash` if the element is missing from the object.
228
+
229
+ When no explicit `Default` is set but a `DataType` is defined, the type's built-in default is used (see the table above).
230
+
231
+ ### Overriding Type Defaults
232
+
233
+ The built-in defaults for each type can be overridden at construction time:
234
+
235
+ ```javascript
236
+ const manifest = new libManyfest({
237
+ Scope: 'Custom',
238
+ defaultValues: {
239
+ String: '(empty)',
240
+ Integer: -1,
241
+ Boolean: true
242
+ },
243
+ Descriptors: {
244
+ 'Name': { DataType: 'String' },
245
+ 'Count': { DataType: 'Integer' }
246
+ }
247
+ });
248
+
249
+ manifest.getValueAtAddress({}, 'Name'); // '(empty)'
250
+ manifest.getValueAtAddress({}, 'Count'); // -1
251
+ ```
252
+
253
+ ## Strict Mode
254
+
255
+ When `strict` is set to `true`, every described element that is missing from an object is treated as an error during validation, regardless of the `Required` flag:
256
+
257
+ ```javascript
258
+ const manifest = new libManyfest({
259
+ Scope: 'Config',
260
+ strict: true,
261
+ Descriptors: {
262
+ 'Host': { DataType: 'String' },
263
+ 'Port': { DataType: 'Integer' }
264
+ }
265
+ });
266
+
267
+ manifest.validate({});
268
+ // Both Host and Port are reported as errors
269
+ ```
270
+
271
+ Without strict mode, only elements with `Required: true` generate errors when missing.
272
+
273
+ ## Use Cases
274
+
275
+ ### API Response Schema
276
+
277
+ ```javascript
278
+ const apiSchema = new libManyfest({
279
+ Scope: 'WeatherAPI',
280
+ Descriptors: {
281
+ 'data.temperature': { Hash: 'Temp', Name: 'Temperature', DataType: 'Float' },
282
+ 'data.humidity': { Hash: 'Humidity', Name: 'Humidity', DataType: 'Float' },
283
+ 'data.wind.speed': { Hash: 'WindSpeed', Name: 'Wind Speed', DataType: 'Float' },
284
+ 'metadata.station': { Hash: 'Station', Name: 'Station ID', DataType: 'String' }
285
+ }
286
+ });
287
+
288
+ // Different API, same hashes
289
+ const response = fetchWeatherData();
290
+ const temp = apiSchema.getValueByHash(response, 'Temp');
291
+ ```
292
+
293
+ ### Form Field Definitions
294
+
295
+ ```javascript
296
+ const formSchema = new libManyfest({
297
+ Scope: 'ContactForm',
298
+ Descriptors: {
299
+ 'FirstName': { Name: 'First Name', DataType: 'String', Required: true },
300
+ 'LastName': { Name: 'Last Name', DataType: 'String', Required: true },
301
+ 'Email': { Name: 'Email Address', DataType: 'String', Required: true },
302
+ 'Phone': { Name: 'Phone Number', DataType: 'String' },
303
+ 'Message': { Name: 'Message', DataType: 'String', Default: '' }
304
+ }
305
+ });
306
+
307
+ // Use descriptors to generate form labels
308
+ formSchema.eachDescriptor((desc) => {
309
+ console.log(`<label>${desc.Name}${desc.Required ? ' *' : ''}</label>`);
310
+ });
311
+ ```
312
+
313
+ ## Notes
314
+
315
+ - The address (the key in `Descriptors`) is the canonical identifier for an element
316
+ - Hashes provide an alias; if no hash is set, the address is used as the hash
317
+ - Multiple descriptors can share the same hash, but the last one wins
318
+ - Descriptors are stored by address, so two descriptors at the same address will overwrite
319
+ - `reset()` clears all descriptors, hashes and the scope