yayson 2.1.0 → 4.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.
Files changed (51) hide show
  1. package/README.md +475 -104
  2. package/build/legacy.cjs +278 -0
  3. package/build/legacy.d.cts +105 -0
  4. package/build/legacy.d.cts.map +1 -0
  5. package/build/legacy.d.mts +105 -0
  6. package/build/legacy.d.mts.map +1 -0
  7. package/build/legacy.mjs +279 -0
  8. package/build/legacy.mjs.map +1 -0
  9. package/build/symbols-DSjKJ8vh.mjs +26 -0
  10. package/build/symbols-DSjKJ8vh.mjs.map +1 -0
  11. package/build/symbols-nFs99aEX.cjs +61 -0
  12. package/build/types-BsfPiPEw.d.mts +122 -0
  13. package/build/types-BsfPiPEw.d.mts.map +1 -0
  14. package/build/types-DbMIe8E2.d.cts +122 -0
  15. package/build/types-DbMIe8E2.d.cts.map +1 -0
  16. package/build/utils.cjs +31 -0
  17. package/build/utils.d.cts +11 -0
  18. package/build/utils.d.cts.map +1 -0
  19. package/build/utils.d.mts +11 -0
  20. package/build/utils.d.mts.map +1 -0
  21. package/build/utils.mjs +22 -0
  22. package/build/utils.mjs.map +1 -0
  23. package/build/yayson-BJv2Z0Z6.mjs +391 -0
  24. package/build/yayson-BJv2Z0Z6.mjs.map +1 -0
  25. package/build/yayson-BKEyEcGk.d.cts +69 -0
  26. package/build/yayson-BKEyEcGk.d.cts.map +1 -0
  27. package/build/yayson-CRbukIqr.d.mts +69 -0
  28. package/build/yayson-CRbukIqr.d.mts.map +1 -0
  29. package/build/yayson-CUfrxK9d.cjs +407 -0
  30. package/build/yayson.cjs +3 -0
  31. package/build/yayson.d.cts +3 -0
  32. package/build/yayson.d.mts +3 -0
  33. package/build/yayson.mjs +3 -0
  34. package/package.json +74 -33
  35. package/skill/yayson/SKILL.md +233 -0
  36. package/skill/yayson/references/api.md +266 -0
  37. package/.circleci/config.yml +0 -35
  38. package/.github/workflows/node.js.yml +0 -30
  39. package/.mocharc.js +0 -15
  40. package/RELEASE.md +0 -3
  41. package/bower.json +0 -31
  42. package/coffeelint.json +0 -120
  43. package/gulpfile.coffee +0 -43
  44. package/lib/yayson/adapter.js +0 -22
  45. package/lib/yayson/adapters/index.js +0 -3
  46. package/lib/yayson/adapters/sequelize.js +0 -14
  47. package/lib/yayson/presenter.js +0 -209
  48. package/lib/yayson/store.js +0 -169
  49. package/lib/yayson/utils.js +0 -47
  50. package/lib/yayson.js +0 -53
  51. package/tags +0 -59
package/README.md CHANGED
@@ -1,67 +1,135 @@
1
1
  # YAYSON
2
2
 
3
- A library for serializing and reading [JSON API](http://jsonapi.org) data in JavaScript. As of 2.0.0 YAYSON aims to support JSON API version 1.
3
+ A library for serializing and reading [JSON API](http://jsonapi.org) data in JavaScript.
4
4
 
5
- [![NPM](https://nodei.co/npm/yayson.png?downloads=true)](https://nodei.co/npm/yayson/)
5
+ YAYSON supports both ESM and CommonJS, has zero dependencies, and works in the browser and in Node.js 20+.
6
6
 
7
+ [![NPM](https://nodei.co/npm/yayson.png?downloads=true)](https://nodei.co/npm/yayson/)
7
8
 
8
9
  ## Installing
9
10
 
10
- Install yayson by running:
11
-
12
11
  ```
13
- $ npm install yayson --save
12
+ npm install yayson
14
13
  ```
15
14
 
16
- ## Presenting data
15
+ ## Upgrading from 3.x
16
+
17
+ Version 4.x is a full TypeScript rewrite with several new features and breaking changes.
18
+
19
+ ### New features
17
20
 
18
- A basic `Presenter` can look like this in Coffeescript:
21
+ - **TypeScript support** Full type definitions included, with type inference from schema registries
22
+ - **Schema validation** — Optional runtime validation using [Zod](https://github.com/colinhacks/zod) or any compatible library with `parse()`/`safeParse()` methods
23
+ - **Dual ESM/CJS package** — Native ES module support alongside CommonJS
24
+ - **`yayson/utils` entry point** — Helper functions (`getType`, `getLinks`, `getMeta`, `getRelationshipLinks`, `getRelationshipMeta`) for reading model metadata
25
+ - **`retrieveAll(type, data)`** — Sync data and return only models of a specific type
26
+ - **`syncAll()`** — Always returns an array (useful when you always want array behavior)
27
+ - **`fields` property** — Limit which attributes a Presenter includes in its output
19
28
 
20
- ```coffee
21
- {Presenter} = require('yayson')(adapter: 'default')
29
+ ### Breaking changes
22
30
 
23
- class ItemsPresenter extends Presenter
24
- type: 'items'
31
+ #### Node.js version
25
32
 
26
- item =
27
- id: 5
28
- name: 'First'
33
+ Node.js 20+ is now required (was 14+).
29
34
 
30
- ItemsPresenter.render(item)
35
+ #### Metadata uses Symbols instead of plain properties
36
+
37
+ In 3.x, resource type, links, and meta were stored as plain properties on models (`model.type`, `model.links`, `model.meta`). Relationship metadata used `model._links` and `model._meta`.
38
+
39
+ In 4.x, these use Symbol keys to avoid collisions with your data. Use the helpers from `yayson/utils`:
40
+
41
+ ```javascript
42
+ // 3.x
43
+ model.type
44
+ model.links
45
+ model.meta
46
+ relatedModel._links
47
+ relatedModel._meta
48
+
49
+ // 4.x
50
+ import { getType, getLinks, getMeta, getRelationshipLinks, getRelationshipMeta } from 'yayson/utils'
51
+ getType(model)
52
+ getLinks(model)
53
+ getMeta(model)
54
+ getRelationshipLinks(relatedModel)
55
+ getRelationshipMeta(relatedModel)
31
56
  ```
32
57
 
33
- Or in plain JavaScript:
58
+ #### `sync()` restores 3.x behavior
59
+
60
+ `sync()` now matches 3.x behavior: single resources return a model directly, collections return an array. If you always want an array, use `syncAll()`.
34
61
 
35
62
  ```javascript
36
- const Presenter = require('yayson')({
37
- adapter: 'default'
38
- }).Presenter;
63
+ // Single resource — returns model directly
64
+ const event = store.sync({ data: { type: 'events', id: '1', attributes: { name: 'Demo' } } })
65
+ event.name // 'Demo'
39
66
 
40
- class ItemsPresenter extends Presenter {};
41
- ItemsPresenter.prototype.type = 'items';
67
+ // Collection returns array
68
+ const events = store.sync({ data: [{ type: 'events', id: '1', attributes: { name: 'Demo' } }] })
69
+ events.length // 1
42
70
 
43
- var item = {
44
- id: 5,
45
- name: 'First'
46
- };
71
+ // Always returns array
72
+ const events = store.syncAll(data)
47
73
 
48
- ItemsPresenter.render(item);
74
+ // retrieve/retrieveAll also available
75
+ const event = store.retrieve('events', data)
76
+ const events = store.retrieveAll('events', data)
49
77
  ```
50
78
 
79
+ #### Document-level meta uses Symbols
80
+
81
+ In 3.x, document-level metadata was stored as `result.meta`. In 4.x, it uses a Symbol key:
82
+
83
+ ```javascript
84
+ // 3.x
85
+ const result = store.sync(data)
86
+ result.meta // { total: 100 }
87
+
88
+ // 4.x
89
+ import { META } from 'yayson/utils'
90
+ const result = store.sync(data)
91
+ result[META] // { total: 100 }
92
+ ```
93
+
94
+ ## Presenting data
95
+
96
+ A basic `Presenter` can look like this:
97
+
98
+ ```javascript
99
+ // ESM
100
+ import yayson from 'yayson'
101
+ const { Presenter } = yayson()
102
+
103
+ // CommonJS
104
+ const yayson = require('yayson')
105
+ const { Presenter } = yayson()
106
+
107
+ class BikePresenter extends Presenter {
108
+ static type = 'bikes'
109
+ }
110
+
111
+ const bike = {
112
+ id: 5,
113
+ name: 'Monark',
114
+ }
115
+
116
+ BikePresenter.render(bike)
117
+ ```
51
118
 
52
119
  This would produce:
53
120
 
54
121
  ```javascript
122
+
55
123
  {
56
124
  data: {
57
- id: 5,
58
- type: 'items',
125
+ id: '5',
126
+ type: 'bikes',
59
127
  attributes: {
60
- id: 5,
61
- name: 'First'
128
+ name: 'Monark'
62
129
  }
63
130
  }
64
131
  }
132
+
65
133
  ```
66
134
 
67
135
  It also works with arrays, so if you send an array to render, "data" will
@@ -69,138 +137,441 @@ be an array.
69
137
 
70
138
  A bit more advanced example:
71
139
 
72
- ```coffee
73
- {Presenter} = require('yayson')(adapter: 'default')
74
-
75
- class ItemsPresenter extends Presenter
76
- type: 'items'
140
+ ```javascript
141
+ import yayson from 'yayson'
142
+ const { Presenter } = yayson()
77
143
 
78
- attributes: ->
79
- attrs = super
80
- attrs.start = moment.utc(attrs.start).toDate()
81
- attrs
144
+ class WheelPresenter extends Presenter {
145
+ static type = 'wheels'
82
146
 
83
- relationships: ->
84
- event: EventsPresenter
147
+ relationships() {
148
+ return { bike: BikePresenter }
149
+ }
150
+ }
85
151
 
86
- ItemsPresenter.render(item)
152
+ class BikePresenter extends Presenter {
153
+ static type = 'bikes'
154
+ relationships() {
155
+ return { wheels: WheelPresenter }
156
+ }
157
+ }
87
158
  ```
88
159
 
89
- In JavaScript this would be done as:
160
+ ### Filtering attributes with fields
90
161
 
91
- ```javascript
162
+ Use the static `fields` property to limit which attributes are included in the output:
92
163
 
93
- var Presenter = require('yayson')().Presenter;
164
+ ```javascript
165
+ class UserPresenter extends Presenter {
166
+ static type = 'users'
167
+ static fields = ['name', 'email', 'createdAt']
168
+ }
94
169
 
95
- class ItemsPresenter extends Presenter {};
96
- ItemsPresenter.prototype.type = 'items'
170
+ const user = {
171
+ id: 1,
172
+ name: 'John',
173
+ email: 'john@example.com',
174
+ password: 'secret',
175
+ createdAt: '2024-01-01',
176
+ }
97
177
 
98
- ItemsPresenter.prototype.attributes = function() {
99
- var attrs = Presenter.prototype.attributes.apply(this, arguments);
178
+ UserPresenter.render(user)
179
+ ```
100
180
 
101
- attrs.start = moment.utc(attrs.start).toDate();
102
- return attrs;
103
- }
181
+ This would produce:
104
182
 
105
- ItemsPresenter.prototype.relationships = function() {
106
- return {
107
- event: EventsPresenter
183
+ ```javascript
184
+ {
185
+ data: {
186
+ id: '1',
187
+ type: 'users',
188
+ attributes: {
189
+ name: 'John',
190
+ email: 'john@example.com',
191
+ createdAt: '2024-01-01'
192
+ }
108
193
  }
109
194
  }
110
-
111
- ItemsPresenter.render(item)
112
195
  ```
113
196
 
197
+ The `password` field is excluded because it's not in the `fields` array. This is useful for hiding sensitive data or reducing payload size without overriding the `attributes()` method.
198
+
114
199
  ### Sequelize support
115
200
 
116
201
  By default it is set up to handle standard JS objects. You can also make
117
202
  it handle Sequelize.js models like this:
118
203
 
119
204
  ```javascript
120
- {Presenter} = require('yayson')({adapter: 'sequelize'})
121
-
205
+ import yayson from 'yayson'
206
+ const { Presenter } = yayson({ adapter: 'sequelize' })
122
207
  ```
123
208
 
124
209
  You can also define your own adapter globally:
125
210
 
126
211
  ```javascript
127
- {Presenter} = require('yayson')(adapter: {
128
- id: function(model){ return 'omg' + model.id},
129
- get: function(model, key){ return model[key] }
212
+ import yayson from 'yayson'
213
+ const { Presenter } = yayson({
214
+ adapter: {
215
+ id: function (model) {
216
+ return 'omg' + model.id
217
+ },
218
+ get: function (model, key) {
219
+ return model[key]
220
+ },
221
+ },
130
222
  })
223
+ ```
131
224
 
225
+ Take a look at the SequelizeAdapter if you want to extend YAYSON to your ORM. Pull requests are welcome. :)
226
+
227
+ ### render() options
228
+
229
+ The second argument to `render()` accepts an options object with `meta` and `links`:
230
+
231
+ ```javascript
232
+ ItemsPresenter.render(items, {
233
+ meta: { total: 100, page: 1 },
234
+ links: {
235
+ self: 'http://example.com/items?page=1',
236
+ next: 'http://example.com/items?page=2',
237
+ },
238
+ })
132
239
  ```
133
240
 
134
- Or at Presenter level:
241
+ This would produce:
135
242
 
136
243
  ```javascript
137
- ItemPresenter.adapter = {
138
- id: function(model){ return 'omg' + model.id},
139
- get: function(model, key){ return model[key] }
244
+ {
245
+ meta: {
246
+ total: 100,
247
+ page: 1
248
+ },
249
+ links: {
250
+ self: 'http://example.com/items?page=1',
251
+ next: 'http://example.com/items?page=2'
252
+ },
253
+ data: [...]
140
254
  }
141
255
  ```
142
256
 
143
- Take a look at the SequelizeAdapter if you want to extend YAYSON to your ORM. Pull requests are welcome. :)
257
+ Both `meta` and `links` are optional and add top-level properties to the JSON API document.
144
258
 
145
- ### Metadata
259
+ ## Parsing data
146
260
 
147
- You can add metadata to the top level object.
261
+ You can use a `Store` like this:
148
262
 
149
- ``` coffee
150
- ItemsPresenter.render(items, meta: count: 10)
263
+ ```javascript
264
+ import yayson from 'yayson'
265
+ const { Store } = yayson()
266
+ const store = new Store()
267
+
268
+ const data = await adapter.get({ path: '/events/' + id })
269
+ const event = store.sync(data) // single resource → model directly
151
270
  ```
152
271
 
153
- This would produce:
272
+ The `sync()` method returns a single model for single resources and an array for collections. Use `syncAll()` if you always want an array.
154
273
 
155
274
  ```javascript
156
- {
157
- meta: {
158
- count: 10
159
- }
275
+ const allSynced = store.syncAll(data) // always returns array
276
+ ```
277
+
278
+ ### Filtering by type with retrieveAll
279
+
280
+ Use `retrieveAll()` to sync data and return only models of a specific type:
281
+
282
+ ```javascript
283
+ const data = await adapter.get({ path: '/events/' })
284
+
285
+ // Sync and return only events (filters out included resources)
286
+ const events = store.retrieveAll('events', data)
287
+ ```
288
+
289
+ This is useful when the response contains multiple types but you only need the primary resources:
290
+
291
+ ```javascript
292
+ // Response contains events and their related images
293
+ const response = {
294
+ data: [
295
+ { type: 'events', id: '1', attributes: { name: 'Conference' } },
296
+ { type: 'events', id: '2', attributes: { name: 'Meetup' } },
297
+ ],
298
+ included: [{ type: 'images', id: '10', attributes: { url: 'http://example.com/img.jpg' } }],
299
+ }
300
+
301
+ // Get only the events, not the included images
302
+ const events = store.retrieveAll('events', response)
303
+ // events.length === 2
304
+ ```
305
+
306
+ ### Finding synced data
307
+
308
+ You can also find in previously synced data:
309
+
310
+ ```javascript
311
+ const event = store.find('events', id)
312
+
313
+ const images = store.findAll('images')
314
+ ```
315
+
316
+ ### Schema Validation and Type Inference
317
+
318
+ YAYSON supports optional schema validation using [Zod](https://github.com/colinhacks/zod) or any compatible schema library. This enables:
319
+
320
+ - **Runtime validation**: Ensure your data matches expected schemas
321
+ - **TypeScript type inference**: Get full type safety without manual type definitions
322
+ - **Strict or safe modes**: Throw errors or collect validation issues
323
+
324
+ #### Basic Usage with Zod
325
+
326
+ ```typescript
327
+ import { z } from 'zod'
328
+ import yayson from 'yayson'
329
+
330
+ const { Store } = yayson()
331
+
332
+ const eventSchema = z
333
+ .object({
334
+ id: z.string(),
335
+ name: z.string(),
336
+ date: z.string(),
337
+ })
338
+ .passthrough()
339
+
340
+ const schemas = {
341
+ events: eventSchema,
342
+ } as const
343
+
344
+ const store = new Store({ schemas, strict: true })
345
+
346
+ store.sync({
160
347
  data: {
161
- id: 5,
162
- type: 'items',
163
- attributes: {
164
- id: 5,
165
- name: 'First'
166
- }
167
- }
348
+ type: 'events',
349
+ id: '1',
350
+ attributes: { name: 'TypeScript Meetup', date: '2025-01-15' },
351
+ },
352
+ })
353
+
354
+ // TypeScript infers the correct type automatically
355
+ const event = store.find('events', '1')
356
+ // event.name, event.date are fully typed!
357
+ ```
358
+
359
+ #### Validation Modes
360
+
361
+ **Strict mode** (throws errors on validation failure):
362
+
363
+ ```typescript
364
+ const store = new Store({ schemas, strict: true })
365
+ ```
366
+
367
+ **Safe mode** (collects errors without throwing):
368
+
369
+ ```typescript
370
+ const store = new Store({ schemas, strict: false })
371
+
372
+ store.sync(invalidData)
373
+
374
+ if (store.validationErrors.length > 0) {
375
+ console.warn('Validation issues:', store.validationErrors)
168
376
  }
169
377
  ```
170
378
 
171
- ## Parsing data
379
+ Schemas must be Zod-like objects with `parse()` and `safeParse()` methods. Any library that provides this interface will work.
172
380
 
173
- You can use a `Store` can like this:
381
+ ### Utilities
174
382
 
175
- ```coffee
176
- {Store} = require('yayson')()
177
- store = new Store()
383
+ YAYSON stores metadata on models using Symbol keys. The `yayson/utils` entry point provides helpers for reading them:
178
384
 
179
- adapter.get(path: '/events/' + id).then (data) ->
180
- event = store.sync(data)
385
+ ```javascript
386
+ import { getType, getLinks, getMeta, getRelationshipLinks, getRelationshipMeta } from 'yayson/utils'
387
+
388
+ const events = store.syncAll(data)
389
+ const event = events[0]
390
+
391
+ getType(event) // 'events'
392
+ getLinks(event) // { self: 'http://...' }
393
+ getMeta(event) // { createdBy: 'admin' }
394
+ getRelationshipLinks(event.image) // { self: 'http://.../relationships/image' }
395
+ getRelationshipMeta(event.image) // { permission: 'read' }
181
396
  ```
182
397
 
183
- This will give you the parsed event with all its relationships.
398
+ The raw symbols (`TYPE`, `LINKS`, `META`, `REL_LINKS`, `REL_META`) are also exported from `yayson/utils` if you prefer direct access.
399
+
400
+ Note that `syncAll()` and `retrieveAll()` return arrays with a `META` symbol property for document-level metadata. Array methods like `.filter()` and `.map()` return plain arrays without this property, so extract it before transforming:
401
+
402
+ ```javascript
403
+ import { META } from 'yayson/utils'
404
+
405
+ const result = store.syncAll(data)
406
+ const meta = result[META] // { total: 100, page: 1 }
407
+ const filtered = result.filter((e) => e.name === 'Demo')
408
+ // filtered[META] is undefined — use the extracted `meta` instead
409
+ ```
410
+
411
+ ## Claude Code skill
412
+
413
+ YAYSON includes a [Claude Code skill](https://docs.anthropic.com/en/docs/claude-code/skills) that helps you write Presenters and set up Stores. To install it, copy the `skill/yayson` directory to your project's `.claude/skills/` or your personal `~/.claude/skills/`:
414
+
415
+ ```bash
416
+ # Project-level (available to everyone working on that project)
417
+ cp -r node_modules/yayson/skill/yayson .claude/skills/
184
418
 
419
+ # Personal (available across all your projects)
420
+ cp -r node_modules/yayson/skill/yayson ~/.claude/skills/
421
+ ```
185
422
 
186
423
  ## Use in the browser
187
424
 
188
425
  Recommended way is to use it via [webpack](https://github.com/webpack/webpack) or similar build system wich lets you just require the package as usual.
189
426
 
190
- If you just want to try it out, copy the file `dist/yayson.js` to your project. Then simply include it:
191
- ```html
192
- <script src="./lib/yayson.js"></script>
427
+ ## Legacy support
428
+
429
+ Earlier versions of JSON API worked a bit different from 1.0. Therefore YAYSON provides legacy presenters and stores in order to have interoperability between the versions.
430
+
431
+ ### Basic Usage
432
+
433
+ ```javascript
434
+ // ESM
435
+ import yayson from 'yayson/legacy'
436
+ const { Presenter, Store } = yayson()
437
+
438
+ // CommonJS
439
+ const yayson = require('yayson/legacy')
440
+ const { Presenter, Store } = yayson()
193
441
  ```
194
- Then you can `var yayson = window.yayson()` use the `yayson.Presenter` and `yayson.Store` as usual.
195
442
 
196
- ### Browser support
443
+ ```javascript
444
+ const store = new Store({
445
+ types: {
446
+ events: 'event',
447
+ images: 'image',
448
+ },
449
+ })
197
450
 
198
- #### Tested
199
- - Chrome
200
- - Firefox
201
- - Safari
202
- - Safari iOS
451
+ const event = store.sync({
452
+ event: { id: '1', name: 'Demo Event' },
453
+ })
454
+ // single resource → returns model directly
455
+
456
+ const allSynced = store.syncAll({
457
+ event: { id: '1', name: 'Demo Event' },
458
+ images: [{ id: '2', url: 'http://example.com/image.jpg' }],
459
+ })
460
+ // syncAll always returns array
461
+
462
+ const event = store.find('event', '1')
463
+ ```
464
+
465
+ #### Filtering by type with retrieveAll
466
+
467
+ Use `retrieveAll()` to sync data and return only models of a specific type:
468
+
469
+ ```javascript
470
+ const events = store.retrieveAll('event', {
471
+ event: [
472
+ { id: '1', name: 'Event 1' },
473
+ { id: '2', name: 'Event 2' },
474
+ ],
475
+ image: [{ id: '3', url: 'http://example.com/image.jpg' }],
476
+ })
477
+ // events.length === 2 (only events, not images)
478
+ ```
479
+
480
+ Options can be passed when creating the store:
481
+
482
+ ```javascript
483
+ const store = new Store({
484
+ types: { events: 'event' },
485
+ })
486
+ ```
487
+
488
+ ### Schema Validation for Legacy Store
489
+
490
+ The legacy store also supports schema validation and type inference, maintaining full backward compatibility:
491
+
492
+ ```typescript
493
+ import yayson from 'yayson/legacy'
494
+ import { z } from 'zod'
495
+
496
+ const { Store } = yayson()
497
+
498
+ const eventSchema = z.object({
499
+ id: z.string(),
500
+ name: z.string(),
501
+ date: z.string(),
502
+ })
503
+
504
+ const store = new Store({
505
+ schemas: { event: eventSchema },
506
+ strict: true,
507
+ })
508
+
509
+ store.sync({ event: { id: '1', name: 'Event', date: '2025-01-15' } })
510
+
511
+ // TypeScript infers the correct type
512
+ const event = store.find('event', '1')
513
+ // event.name, event.date are fully typed!
514
+ ```
515
+
516
+ #### Validation with Type Mapping
517
+
518
+ You can combine type mapping with schemas:
519
+
520
+ ```typescript
521
+ const store = new Store({
522
+ types: {
523
+ events: 'event', // Map plural to singular
524
+ },
525
+ schemas: {
526
+ event: eventSchema, // Schema uses normalized type
527
+ },
528
+ strict: false, // Safe mode
529
+ })
530
+
531
+ store.sync({ events: [{ id: '1', name: 'Event' }] })
532
+
533
+ if (store.validationErrors.length > 0) {
534
+ console.warn('Validation issues:', store.validationErrors)
535
+ }
536
+ ```
537
+
538
+ #### Validation with Relations
539
+
540
+ Schemas validate the complete model after relations are resolved:
541
+
542
+ ```typescript
543
+ const eventSchema = z.object({
544
+ id: z.string(),
545
+ name: z.string(),
546
+ images: z.array(
547
+ z.object({
548
+ id: z.string(),
549
+ url: z.string(),
550
+ }),
551
+ ),
552
+ })
553
+
554
+ const imageSchema = z.object({
555
+ id: z.string(),
556
+ url: z.string(),
557
+ })
558
+
559
+ const store = new Store({
560
+ schemas: { event: eventSchema, image: imageSchema },
561
+ strict: true,
562
+ })
563
+
564
+ store.sync({
565
+ links: {
566
+ 'event.images': { type: 'image' },
567
+ },
568
+ event: { id: '1', name: 'Event', images: ['2'] },
569
+ image: [{ id: '2', url: 'http://example.com/image.jpg' }],
570
+ })
571
+
572
+ const event = store.find('event', '1')
573
+ // Relations are resolved before validation
574
+ // event.images is an array of validated image objects
575
+ ```
203
576
 
204
- #### Untested, but should work
205
- - IE 9+
206
- - Android
577
+ **Note**: Validation happens eagerly during `sync()` when schemas are configured. This allows you to check `store.validationErrors` immediately after syncing.