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.
- package/README.md +586 -67
- package/build/legacy.cjs +331 -0
- package/build/legacy.d.cts +113 -0
- package/build/legacy.d.cts.map +1 -0
- package/build/legacy.d.mts +113 -0
- package/build/legacy.d.mts.map +1 -0
- package/build/legacy.mjs +332 -0
- package/build/legacy.mjs.map +1 -0
- package/build/symbols-DSjKJ8vh.mjs +26 -0
- package/build/symbols-DSjKJ8vh.mjs.map +1 -0
- package/build/symbols-nFs99aEX.cjs +61 -0
- package/build/types-Do2flKZX.d.mts +129 -0
- package/build/types-Do2flKZX.d.mts.map +1 -0
- package/build/types-NiKV-lj-.d.cts +129 -0
- package/build/types-NiKV-lj-.d.cts.map +1 -0
- package/build/utils.cjs +31 -0
- package/build/utils.d.cts +11 -0
- package/build/utils.d.cts.map +1 -0
- package/build/utils.d.mts +11 -0
- package/build/utils.d.mts.map +1 -0
- package/build/utils.mjs +22 -0
- package/build/utils.mjs.map +1 -0
- package/build/yayson-3UYKq2H5.d.cts +81 -0
- package/build/yayson-3UYKq2H5.d.cts.map +1 -0
- package/build/yayson-Ce5uGpgU.d.mts +81 -0
- package/build/yayson-Ce5uGpgU.d.mts.map +1 -0
- package/build/yayson-CwZg5FNt.mjs +452 -0
- package/build/yayson-CwZg5FNt.mjs.map +1 -0
- package/build/yayson-l2JKseMH.cjs +468 -0
- package/build/yayson.cjs +3 -0
- package/build/yayson.d.cts +3 -0
- package/build/yayson.d.mts +3 -0
- package/build/yayson.mjs +3 -0
- package/package.json +72 -28
- package/skill/yayson/SKILL.md +233 -0
- package/skill/yayson/references/api.md +266 -0
- package/.eslintrc.json +0 -28
- package/.github/workflows/node.js.yml +0 -30
- package/.mocharc.js +0 -15
- package/.nvmrc +0 -1
- package/RELEASE.md +0 -3
- package/babel.config.json +0 -4
- package/legacy.js +0 -2
- package/prettier.config.js +0 -5
- package/src/legacy.js +0 -14
- package/src/yayson/adapter.js +0 -18
- package/src/yayson/adapters/index.js +0 -1
- package/src/yayson/adapters/sequelize.js +0 -11
- package/src/yayson/legacy-presenter.js +0 -121
- package/src/yayson/legacy-store.js +0 -156
- package/src/yayson/presenter.js +0 -198
- package/src/yayson/store.js +0 -194
- package/src/yayson.js +0 -23
- package/tags +0 -59
- package/webpack.browser.js +0 -27
- package/webpack.common.js +0 -39
- 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
|
-
|
|
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
|
[](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
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
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
|
-
###
|
|
183
|
+
### Creating payloads with payload()
|
|
117
184
|
|
|
118
|
-
|
|
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
|
-
```
|
|
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
|
-
|
|
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
|
-
|
|
137
|
-
type: 'items',
|
|
208
|
+
type: 'bikes',
|
|
138
209
|
attributes: {
|
|
139
|
-
|
|
140
|
-
|
|
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`
|
|
228
|
+
You can use a `Store` like this:
|
|
149
229
|
|
|
150
230
|
```javascript
|
|
151
|
-
|
|
152
|
-
|
|
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
|
-
|
|
155
|
-
|
|
241
|
+
```javascript
|
|
242
|
+
const allSynced = store.syncAll(data) // always returns array
|
|
156
243
|
```
|
|
157
244
|
|
|
158
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
175
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
184
|
-
-
|
|
185
|
-
-
|
|
186
|
-
|
|
187
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
+
```
|