yayson 3.0.0 → 4.1.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 (57) hide show
  1. package/README.md +586 -67
  2. package/build/legacy.cjs +331 -0
  3. package/build/legacy.d.cts +113 -0
  4. package/build/legacy.d.cts.map +1 -0
  5. package/build/legacy.d.mts +113 -0
  6. package/build/legacy.d.mts.map +1 -0
  7. package/build/legacy.mjs +332 -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-Do2flKZX.d.mts +129 -0
  13. package/build/types-Do2flKZX.d.mts.map +1 -0
  14. package/build/types-NiKV-lj-.d.cts +129 -0
  15. package/build/types-NiKV-lj-.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-3UYKq2H5.d.cts +81 -0
  24. package/build/yayson-3UYKq2H5.d.cts.map +1 -0
  25. package/build/yayson-Ce5uGpgU.d.mts +81 -0
  26. package/build/yayson-Ce5uGpgU.d.mts.map +1 -0
  27. package/build/yayson-CwZg5FNt.mjs +452 -0
  28. package/build/yayson-CwZg5FNt.mjs.map +1 -0
  29. package/build/yayson-l2JKseMH.cjs +468 -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 +72 -28
  35. package/skill/yayson/SKILL.md +233 -0
  36. package/skill/yayson/references/api.md +266 -0
  37. package/.eslintrc.json +0 -28
  38. package/.github/workflows/node.js.yml +0 -30
  39. package/.mocharc.js +0 -15
  40. package/.nvmrc +0 -1
  41. package/RELEASE.md +0 -3
  42. package/babel.config.json +0 -4
  43. package/legacy.js +0 -2
  44. package/prettier.config.js +0 -5
  45. package/src/legacy.js +0 -14
  46. package/src/yayson/adapter.js +0 -18
  47. package/src/yayson/adapters/index.js +0 -1
  48. package/src/yayson/adapters/sequelize.js +0 -11
  49. package/src/yayson/legacy-presenter.js +0 -121
  50. package/src/yayson/legacy-store.js +0 -156
  51. package/src/yayson/presenter.js +0 -198
  52. package/src/yayson/store.js +0 -194
  53. package/src/yayson.js +0 -23
  54. package/tags +0 -59
  55. package/webpack.browser.js +0 -27
  56. package/webpack.common.js +0 -39
  57. package/webpack.dist.js +0 -21
package/README.md CHANGED
@@ -2,19 +2,17 @@
2
2
 
3
3
  A library for serializing and reading [JSON API](http://jsonapi.org) data in JavaScript.
4
4
 
5
- From version 3 we now support native JavaScript classes. YAYSON has zero dependencies and works in the browser and in node 14 and up.
5
+ - Zero dependencies
6
+ - Full TypeScript support with type inference from schema registries
7
+ - Optional runtime validation with [Zod](https://github.com/colinhacks/zod) or any compatible library
8
+ - Dual ESM/CJS package — works in browsers and Node.js 20+
6
9
 
7
10
  [![NPM](https://nodei.co/npm/yayson.png?downloads=true)](https://nodei.co/npm/yayson/)
8
11
 
9
-
10
12
  ## Installing
11
13
 
12
- Install yayson by running:
13
-
14
14
  ```
15
-
16
-
17
- $ npm i yayson
15
+ npm install yayson
18
16
  ```
19
17
 
20
18
  ## Presenting data
@@ -22,6 +20,11 @@ $ npm i yayson
22
20
  A basic `Presenter` can look like this:
23
21
 
24
22
  ```javascript
23
+ // ESM
24
+ import yayson from 'yayson'
25
+ const { Presenter } = yayson()
26
+
27
+ // CommonJS
25
28
  const yayson = require('yayson')
26
29
  const { Presenter } = yayson()
27
30
 
@@ -31,29 +34,26 @@ class BikePresenter extends Presenter {
31
34
 
32
35
  const bike = {
33
36
  id: 5,
34
- name: 'Monark'
35
- };
37
+ name: 'Monark',
38
+ }
36
39
 
37
- BikePresenter.render(bike);
40
+ BikePresenter.render(bike)
38
41
  ```
39
42
 
40
-
41
43
  This would produce:
42
44
 
43
45
  ```javascript
44
46
 
45
-
46
-
47
47
  {
48
48
  data: {
49
- id: 5,
49
+ id: '5',
50
50
  type: 'bikes',
51
51
  attributes: {
52
- id: 5,
53
52
  name: 'Monark'
54
53
  }
55
54
  }
56
55
  }
56
+
57
57
  ```
58
58
 
59
59
  It also works with arrays, so if you send an array to render, "data" will
@@ -61,10 +61,8 @@ be an array.
61
61
 
62
62
  A bit more advanced example:
63
63
 
64
-
65
64
  ```javascript
66
-
67
- const yayson = require('yayson')
65
+ import yayson from 'yayson'
68
66
  const { Presenter } = yayson()
69
67
 
70
68
  class WheelPresenter extends Presenter {
@@ -81,125 +79,646 @@ class BikePresenter extends Presenter {
81
79
  return { wheels: WheelPresenter }
82
80
  }
83
81
  }
82
+ ```
83
+
84
+ ### Filtering attributes with fields
85
+
86
+ Use the static `fields` property to limit which attributes are included in the output:
87
+
88
+ ```javascript
89
+ class UserPresenter extends Presenter {
90
+ static type = 'users'
91
+ static fields = ['name', 'email', 'createdAt']
92
+ }
93
+
94
+ const user = {
95
+ id: 1,
96
+ name: 'John',
97
+ email: 'john@example.com',
98
+ password: 'secret',
99
+ createdAt: '2024-01-01',
100
+ }
101
+
102
+ UserPresenter.render(user)
103
+ ```
84
104
 
105
+ This would produce:
85
106
 
107
+ ```javascript
108
+ {
109
+ data: {
110
+ id: '1',
111
+ type: 'users',
112
+ attributes: {
113
+ name: 'John',
114
+ email: 'john@example.com',
115
+ createdAt: '2024-01-01'
116
+ }
117
+ }
118
+ }
86
119
  ```
87
120
 
121
+ 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.
122
+
88
123
  ### Sequelize support
89
124
 
90
125
  By default it is set up to handle standard JS objects. You can also make
91
126
  it handle Sequelize.js models like this:
92
127
 
93
128
  ```javascript
94
-
95
- const yayson = require('yayson')
96
- {Presenter} = yayson({adapter: 'sequelize'})
97
-
129
+ import yayson from 'yayson'
130
+ const { Presenter } = yayson({ adapter: 'sequelize' })
98
131
  ```
99
132
 
100
133
  You can also define your own adapter globally:
101
134
 
102
135
  ```javascript
136
+ import yayson from 'yayson'
137
+ const { Presenter } = yayson({
138
+ adapter: {
139
+ id: function (model) {
140
+ return 'omg' + model.id
141
+ },
142
+ get: function (model, key) {
143
+ return model[key]
144
+ },
145
+ },
146
+ })
147
+ ```
103
148
 
149
+ Take a look at the SequelizeAdapter if you want to extend YAYSON to your ORM. Pull requests are welcome. :)
104
150
 
105
- const yayson = require('yayson')
106
- {Presenter} = yayson(adapter: {
107
- id: function(model){ return 'omg' + model.id},
108
- get: function(model, key){ return model[key] }
109
- })
151
+ ### render() options
152
+
153
+ The second argument to `render()` accepts an options object with `meta` and `links`:
110
154
 
155
+ ```javascript
156
+ ItemsPresenter.render(items, {
157
+ meta: { total: 100, page: 1 },
158
+ links: {
159
+ self: 'http://example.com/items?page=1',
160
+ next: 'http://example.com/items?page=2',
161
+ },
162
+ })
111
163
  ```
112
164
 
165
+ This would produce:
113
166
 
114
- Take a look at the SequelizeAdapter if you want to extend YAYSON to your ORM. Pull requests are welcome. :)
167
+ ```javascript
168
+ {
169
+ meta: {
170
+ total: 100,
171
+ page: 1
172
+ },
173
+ links: {
174
+ self: 'http://example.com/items?page=1',
175
+ next: 'http://example.com/items?page=2'
176
+ },
177
+ data: [...]
178
+ }
179
+ ```
180
+
181
+ Both `meta` and `links` are optional and add top-level properties to the JSON API document.
115
182
 
116
- ### Metadata
183
+ ### Creating payloads with payload()
117
184
 
118
- You can add metadata to the top level object.
185
+ Use `payload()` to serialize a single resource for create or update API requests. Unlike `render()`, it omits `included` resources — only the primary resource with relationship linkage is produced:
119
186
 
120
- ``` javascript
187
+ ```javascript
188
+ class WheelPresenter extends Presenter {
189
+ static type = 'wheels'
190
+ }
121
191
 
192
+ class BikePresenter extends Presenter {
193
+ static type = 'bikes'
194
+ relationships() {
195
+ return { wheels: WheelPresenter }
196
+ }
197
+ }
122
198
 
123
- ItemsPresenter.render(items, {meta: count: 10})
199
+ // Create (no id)
200
+ BikePresenter.payload({ name: 'Monark', wheels: [{ id: 1 }, { id: 2 }] })
124
201
  ```
125
202
 
126
203
  This would produce:
127
204
 
128
205
  ```javascript
129
-
130
-
131
206
  {
132
- meta: {
133
- count: 10
134
- }
135
207
  data: {
136
- id: 5,
137
- type: 'items',
208
+ type: 'bikes',
138
209
  attributes: {
139
- id: 5,
140
- name: 'First'
210
+ name: 'Monark'
211
+ },
212
+ relationships: {
213
+ wheels: {
214
+ data: [
215
+ { type: 'wheels', id: '1' },
216
+ { type: 'wheels', id: '2' }
217
+ ]
218
+ }
141
219
  }
142
220
  }
143
221
  }
144
222
  ```
145
223
 
224
+ For updates, include an `id` and it will appear in the output. `payload()` also accepts `meta` and `links` options like `render()`. It throws on arrays and null — use `render()` for collections.
225
+
146
226
  ## Parsing data
147
227
 
148
- You can use a `Store` can like this:
228
+ You can use a `Store` like this:
149
229
 
150
230
  ```javascript
151
- const {Store} = require('yayson')();
152
- const store = new Store();
231
+ import yayson from 'yayson'
232
+ const { Store } = yayson()
233
+ const store = new Store()
234
+
235
+ const data = await adapter.get({ path: '/events/' + id })
236
+ const event = store.sync(data) // single resource -> model directly
237
+ ```
238
+
239
+ The `sync()` method returns a single model for single resources and an array for collections. Use `syncAll()` if you always want an array.
153
240
 
154
- const data = await adapter.get({path: '/events/' + id});
155
- const event = store.sync(data);
241
+ ```javascript
242
+ const allSynced = store.syncAll(data) // always returns array
156
243
  ```
157
244
 
158
- This will give you the parsed event with all its relationships.
245
+ ### Filtering by type with retrieveAll
246
+
247
+ Use `retrieveAll()` to sync data and return only models of a specific type:
248
+
249
+ ```javascript
250
+ const data = await adapter.get({ path: '/events/' })
159
251
 
160
- Its also possible to find in the synched data:
252
+ // Sync and return only events (filters out included resources)
253
+ const events = store.retrieveAll('events', data)
254
+ ```
161
255
 
256
+ This is useful when the response contains multiple types but you only need the primary resources:
162
257
 
163
258
  ```javascript
164
- const event = this.store.find('events', id)
259
+ // Response contains events and their related images
260
+ const response = {
261
+ data: [
262
+ { type: 'events', id: '1', attributes: { name: 'Conference' } },
263
+ { type: 'events', id: '2', attributes: { name: 'Meetup' } },
264
+ ],
265
+ included: [{ type: 'images', id: '10', attributes: { url: 'http://example.com/img.jpg' } }],
266
+ }
165
267
 
166
- const images = this.store.findAll('images')
268
+ // Get only the events, not the included images
269
+ const events = store.retrieveAll('events', response)
270
+ // events.length === 2
167
271
  ```
168
272
 
273
+ ### Building models from create payloads
169
274
 
170
- ## Use in the browser
275
+ Use `build()` to create a model from a JSON:API document without storing it. This is useful for create payloads where the `id` may not yet exist:
171
276
 
172
- 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.
277
+ ```javascript
278
+ const model = store.build({
279
+ data: {
280
+ type: 'events',
281
+ attributes: { name: 'New Event' },
282
+ },
283
+ })
284
+
285
+ model.name // 'New Event'
286
+ model.id // undefined
287
+ ```
288
+
289
+ If the store already has synced data, `build()` resolves relationships against it. Unresolved references become stubs with just `id` and type:
290
+
291
+ ```javascript
292
+ store.syncAll({
293
+ data: [{ type: 'images', id: '5', attributes: { url: 'http://example.com/img.jpg' } }],
294
+ })
295
+
296
+ const model = store.build({
297
+ data: {
298
+ type: 'events',
299
+ attributes: { name: 'New Event' },
300
+ relationships: {
301
+ image: { data: { type: 'images', id: '5' } },
302
+ sponsor: { data: { type: 'sponsors', id: '99' } },
303
+ },
304
+ },
305
+ })
306
+
307
+ model.image.url // 'http://example.com/img.jpg' (resolved from store)
308
+ model.sponsor.id // '99' (stub — not in store)
309
+ ```
310
+
311
+ A static version is also available when you don't need a store instance:
312
+
313
+ ```javascript
314
+ const model = Store.build({
315
+ data: {
316
+ type: 'events',
317
+ attributes: { name: 'New Event' },
318
+ },
319
+ })
320
+ ```
321
+
322
+ ### Finding synced data
323
+
324
+ You can also find in previously synced data:
325
+
326
+ ```javascript
327
+ const event = store.find('events', id)
328
+
329
+ const images = store.findAll('images')
330
+ ```
331
+
332
+ ### Schema Validation and Type Inference
333
+
334
+ YAYSON supports optional schema validation using [Zod](https://github.com/colinhacks/zod) or any compatible schema library. This enables:
335
+
336
+ - **Runtime validation**: Ensure your data matches expected schemas
337
+ - **TypeScript type inference**: Get full type safety without manual type definitions
338
+ - **Strict or safe modes**: Throw errors or collect validation issues
339
+
340
+ #### Basic Usage with Zod
341
+
342
+ ```typescript
343
+ import { z } from 'zod'
344
+ import yayson from 'yayson'
345
+
346
+ const { Store } = yayson()
347
+
348
+ const eventSchema = z
349
+ .object({
350
+ id: z.string(),
351
+ name: z.string(),
352
+ date: z.string(),
353
+ })
354
+ .passthrough()
355
+
356
+ const schemas = {
357
+ events: eventSchema,
358
+ } as const
359
+
360
+ const store = new Store({ schemas, strict: true })
361
+
362
+ store.sync({
363
+ data: {
364
+ type: 'events',
365
+ id: '1',
366
+ attributes: { name: 'TypeScript Meetup', date: '2025-01-15' },
367
+ },
368
+ })
369
+
370
+ // TypeScript infers the correct type automatically
371
+ const event = store.find('events', '1')
372
+ // event.name, event.date are fully typed!
373
+ ```
374
+
375
+ #### Validation Modes
376
+
377
+ **Strict mode** (throws errors on validation failure):
378
+
379
+ ```typescript
380
+ const store = new Store({ schemas, strict: true })
381
+ ```
382
+
383
+ **Safe mode** (collects errors without throwing):
384
+
385
+ ```typescript
386
+ const store = new Store({ schemas, strict: false })
387
+
388
+ store.sync(invalidData)
389
+
390
+ if (store.validationErrors.length > 0) {
391
+ console.warn('Validation issues:', store.validationErrors)
392
+ }
393
+ ```
394
+
395
+ Schemas must be Zod-like objects with `parse()` and `safeParse()` methods. Any library that provides this interface will work.
396
+
397
+ ### Utilities
398
+
399
+ YAYSON stores metadata on models using Symbol keys. The `yayson/utils` entry point provides helpers for reading them:
400
+
401
+ ```javascript
402
+ import { getType, getLinks, getMeta, getRelationshipLinks, getRelationshipMeta } from 'yayson/utils'
403
+
404
+ const events = store.syncAll(data)
405
+ const event = events[0]
173
406
 
174
- If you just want to try it out, copy the file `dist/yayson.js` to your project. Then simply include it:
175
- ```html
407
+ getType(event) // 'events'
408
+ getLinks(event) // { self: 'http://...' }
409
+ getMeta(event) // { createdBy: 'admin' }
410
+ getRelationshipLinks(event.image) // { self: 'http://.../relationships/image' }
411
+ getRelationshipMeta(event.image) // { permission: 'read' }
412
+ ```
413
+
414
+ The raw symbols (`TYPE`, `LINKS`, `META`, `REL_LINKS`, `REL_META`) are also exported from `yayson/utils` if you prefer direct access.
415
+
416
+ 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:
417
+
418
+ ```javascript
419
+ import { META } from 'yayson/utils'
176
420
 
177
- <script src="./lib/yayson.js"></script>
421
+ const result = store.syncAll(data)
422
+ const meta = result[META] // { total: 100, page: 1 }
423
+ const filtered = result.filter((e) => e.name === 'Demo')
424
+ // filtered[META] is undefined — use the extracted `meta` instead
178
425
  ```
179
- Then you can `var yayson = window.yayson()` use the `yayson.Presenter` and `yayson.Store` as usual.
180
426
 
181
- ### Browser support
427
+ ## Claude Code skill
428
+
429
+ 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/`:
182
430
 
183
- #### Tested
184
- - Chrome
185
- - Firefox
186
- - Safari
187
- - Safari iOS
431
+ ```bash
432
+ # Project-level (available to everyone working on that project)
433
+ cp -r node_modules/yayson/skill/yayson .claude/skills/
434
+
435
+ # Personal (available across all your projects)
436
+ cp -r node_modules/yayson/skill/yayson ~/.claude/skills/
437
+ ```
188
438
 
189
- #### Untested, but should work
190
- - IE 11
191
- - Android
439
+ ## Use in the browser
192
440
 
441
+ 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.
193
442
 
194
443
  ## Legacy support
195
444
 
196
- 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. Its used similar to the standard presenters:
445
+ 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.
446
+
447
+ ### Basic Usage
197
448
 
198
449
  ```javascript
450
+ // ESM
451
+ import yayson from 'yayson/legacy'
452
+ const { Presenter, Store } = yayson()
199
453
 
454
+ // CommonJS
200
455
  const yayson = require('yayson/legacy')
201
- const { Presenter } = yayson()
456
+ const { Presenter, Store } = yayson()
457
+ ```
458
+
459
+ ```javascript
460
+ const store = new Store({
461
+ types: {
462
+ events: 'event',
463
+ images: 'image',
464
+ },
465
+ })
466
+
467
+ const event = store.sync({
468
+ event: { id: '1', name: 'Demo Event' },
469
+ })
470
+ // single resource -> returns model directly
471
+
472
+ const allSynced = store.syncAll({
473
+ event: { id: '1', name: 'Demo Event' },
474
+ images: [{ id: '2', url: 'http://example.com/image.jpg' }],
475
+ })
476
+ // syncAll always returns array
477
+ ```
478
+
479
+ ```javascript
480
+ const event = store.find('event', '1')
481
+ ```
482
+
483
+ #### Creating payloads with payload()
484
+
485
+ The legacy presenter also supports `payload()` for create/update requests. It produces the legacy envelope format without `links` or sideloaded collections:
486
+
487
+ ```javascript
488
+ class TirePresenter extends Presenter {
489
+ static type = 'tire'
490
+ }
491
+
492
+ class CarPresenter extends Presenter {
493
+ static type = 'car'
494
+ relationships() {
495
+ return { tires: TirePresenter }
496
+ }
497
+ }
498
+
499
+ CarPresenter.payload({ name: 'Volvo', tires: [{ id: 1 }, { id: 2 }] })
500
+ // { car: { name: 'Volvo', tires: [1, 2] } }
501
+ ```
502
+
503
+ #### Filtering by type with retrieveAll
504
+
505
+ Use `retrieveAll()` to sync data and return only models of a specific type:
506
+
507
+ ```javascript
508
+ const events = store.retrieveAll('event', {
509
+ event: [
510
+ { id: '1', name: 'Event 1' },
511
+ { id: '2', name: 'Event 2' },
512
+ ],
513
+ image: [{ id: '3', url: 'http://example.com/image.jpg' }],
514
+ })
515
+ // events.length === 2 (only events, not images)
516
+ ```
517
+
518
+ Options can be passed when creating the store:
519
+
520
+ ```javascript
521
+ const store = new Store({
522
+ types: { events: 'event' },
523
+ })
524
+ ```
525
+
526
+ #### Building models from create payloads
527
+
528
+ The legacy store also supports `build()` for creating models without storing them:
529
+
530
+ ```javascript
531
+ const model = store.build({
532
+ event: { name: 'New Event' },
533
+ })
534
+
535
+ model.name // 'New Event'
536
+ model.id // undefined
537
+ ```
538
+
539
+ Relationships are resolved against existing store data when available:
540
+
541
+ ```javascript
542
+ store.syncAll({
543
+ links: { 'event.images': { type: 'images' } },
544
+ images: [{ id: '2', name: 'Header' }],
545
+ })
546
+
547
+ const model = store.build({
548
+ event: { name: 'New Event', images: [2] },
549
+ })
550
+
551
+ model.images[0].name // 'Header' (resolved from store)
552
+ ```
553
+
554
+ A static version is also available:
555
+
556
+ ```javascript
557
+ const model = Store.build({ event: { name: 'New Event' } })
558
+ ```
559
+
560
+ ### Schema Validation for Legacy Store
561
+
562
+ The legacy store also supports schema validation and type inference, maintaining full backward compatibility:
563
+
564
+ ```typescript
565
+ import yayson from 'yayson/legacy'
566
+ import { z } from 'zod'
567
+
568
+ const { Store } = yayson()
569
+
570
+ const eventSchema = z.object({
571
+ id: z.string(),
572
+ name: z.string(),
573
+ date: z.string(),
574
+ })
575
+
576
+ const store = new Store({
577
+ schemas: { event: eventSchema },
578
+ strict: true,
579
+ })
580
+
581
+ store.sync({ event: { id: '1', name: 'Event', date: '2025-01-15' } })
582
+
583
+ // TypeScript infers the correct type
584
+ const event = store.find('event', '1')
585
+ // event.name, event.date are fully typed!
586
+ ```
587
+
588
+ #### Validation with Type Mapping
589
+
590
+ You can combine type mapping with schemas:
591
+
592
+ ```typescript
593
+ const store = new Store({
594
+ types: {
595
+ events: 'event', // Map plural to singular
596
+ },
597
+ schemas: {
598
+ event: eventSchema, // Schema uses normalized type
599
+ },
600
+ strict: false, // Safe mode
601
+ })
602
+
603
+ store.sync({ events: [{ id: '1', name: 'Event' }] })
604
+
605
+ if (store.validationErrors.length > 0) {
606
+ console.warn('Validation issues:', store.validationErrors)
607
+ }
608
+ ```
609
+
610
+ #### Validation with Relations
611
+
612
+ Schemas validate the complete model after relations are resolved:
613
+
614
+ ```typescript
615
+ const eventSchema = z.object({
616
+ id: z.string(),
617
+ name: z.string(),
618
+ images: z.array(
619
+ z.object({
620
+ id: z.string(),
621
+ url: z.string(),
622
+ }),
623
+ ),
624
+ })
202
625
 
626
+ const imageSchema = z.object({
627
+ id: z.string(),
628
+ url: z.string(),
629
+ })
630
+
631
+ const store = new Store({
632
+ schemas: { event: eventSchema, image: imageSchema },
633
+ strict: true,
634
+ })
635
+
636
+ store.sync({
637
+ links: {
638
+ 'event.images': { type: 'image' },
639
+ },
640
+ event: { id: '1', name: 'Event', images: ['2'] },
641
+ image: [{ id: '2', url: 'http://example.com/image.jpg' }],
642
+ })
643
+
644
+ const event = store.find('event', '1')
645
+ // Relations are resolved before validation
646
+ // event.images is an array of validated image objects
647
+ ```
648
+
649
+ **Note**: Validation happens eagerly during `sync()` when schemas are configured. This allows you to check `store.validationErrors` immediately after syncing.
650
+
651
+ ## Upgrading from 4.0
652
+
653
+ ### Unresolved relationships resolve to stubs
654
+
655
+ References missing from `included` now resolve to a stub `{ id, [TYPE]: type }` instead of `null`. References with `id: null` are dropped (arrays filter them out; to-one becomes `null`).
656
+
657
+ Tighten schemas that typed relationships as nullable, and detect "not sideloaded" by absence of attributes rather than `=== null`.
658
+
659
+ ## Upgrading from 3.x
660
+
661
+ Version 4.x is a full TypeScript rewrite with several breaking changes.
662
+
663
+ ### Node.js version
664
+
665
+ Node.js 20+ is now required (was 14+).
666
+
667
+ ### Metadata uses Symbols instead of plain properties
668
+
669
+ 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`.
670
+
671
+ In 4.x, these use Symbol keys to avoid collisions with your data. Use the helpers from `yayson/utils`:
672
+
673
+ ```javascript
674
+ // 3.x
675
+ model.type
676
+ model.links
677
+ model.meta
678
+ relatedModel._links
679
+ relatedModel._meta
680
+
681
+ // 4.x
682
+ import { getType, getLinks, getMeta, getRelationshipLinks, getRelationshipMeta } from 'yayson/utils'
683
+ getType(model)
684
+ getLinks(model)
685
+ getMeta(model)
686
+ getRelationshipLinks(relatedModel)
687
+ getRelationshipMeta(relatedModel)
688
+ ```
689
+
690
+ ### `sync()` restores 3.x behavior
691
+
692
+ `sync()` now matches 3.x behavior: single resources return a model directly, collections return an array. If you always want an array, use `syncAll()`.
693
+
694
+ ```javascript
695
+ // Single resource — returns model directly
696
+ const event = store.sync({ data: { type: 'events', id: '1', attributes: { name: 'Demo' } } })
697
+ event.name // 'Demo'
698
+
699
+ // Collection — returns array
700
+ const events = store.sync({ data: [{ type: 'events', id: '1', attributes: { name: 'Demo' } }] })
701
+ events.length // 1
702
+
703
+ // Always returns array
704
+ const events = store.syncAll(data)
705
+
706
+ // retrieve/retrieveAll also available
707
+ const event = store.retrieve('events', data)
708
+ const events = store.retrieveAll('events', data)
203
709
  ```
204
710
 
711
+ ### Document-level meta uses Symbols
712
+
713
+ In 3.x, document-level metadata was stored as `result.meta`. In 4.x, it uses a Symbol key:
205
714
 
715
+ ```javascript
716
+ // 3.x
717
+ const result = store.sync(data)
718
+ result.meta // { total: 100 }
719
+
720
+ // 4.x
721
+ import { META } from 'yayson/utils'
722
+ const result = store.sync(data)
723
+ result[META] // { total: 100 }
724
+ ```