@zeroxsolutions/jsonapi 0.13.2 → 0.13.4

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/CHANGELOG.md CHANGED
@@ -1,3 +1,18 @@
1
+ ## 0.13.4 (2026-10-19)
2
+
3
+ This was a version bump only for @zeroxsolutions/jsonapi to align it with other projects, there were no code changes.
4
+
5
+ ## 0.13.3 (2026-10-18)
6
+
7
+ ### 🚀 Features
8
+
9
+ - **jsonapi:** turn a guarded query into a finder's options ([9f5f895](https://github.com/zeroxsolutions/zeroxsolutions/commit/9f5f895))
10
+
11
+ ### ❤️ Thank You
12
+
13
+ - Claude Opus 5.5
14
+ - Lương Văn Tú
15
+
1
16
  ## 0.13.2 (2026-10-06)
2
17
 
3
18
  No code changes. It depends on @zeroxsolutions/response, so its version follows response 0.14.4.
@@ -17,11 +32,11 @@ No code changes. It depends on @zeroxsolutions/response, so its version follows
17
32
 
18
33
  ### 🩹 Fixes
19
34
 
20
- - ⚠️ **jsonapi:** negotiate by the four json:api server rules alone ([67f4529](https://github.com/zeroxsolutions/zeroxsolutions/commit/67f4529))
35
+ - ⚠️ **jsonapi:** negotiate by the four json:api server rules alone ([67f4529](https://github.com/zeroxsolutions/zeroxsolutions/commit/67f4529))
21
36
 
22
- ### ⚠️ Breaking Changes
37
+ ### ⚠️ Breaking Changes
23
38
 
24
- - **jsonapi:** negotiate by the four json:api server rules alone ([67f4529](https://github.com/zeroxsolutions/zeroxsolutions/commit/67f4529))
39
+ - **jsonapi:** negotiate by the four json:api server rules alone ([67f4529](https://github.com/zeroxsolutions/zeroxsolutions/commit/67f4529))
25
40
  negotiateJsonApi returns null for a Content-Type that
26
41
  is not the JSON:API media type (or absent), and for an Accept that holds
27
42
  no JSON:API instance. A route refuses a body it cannot read through its
@@ -41,12 +56,12 @@ This was a version bump only for @zeroxsolutions/jsonapi to align it with other
41
56
  ### 🩹 Fixes
42
57
 
43
58
  - **jsonapi:** read every content-type parameter, q included ([07f81b9](https://github.com/zeroxsolutions/zeroxsolutions/commit/07f81b9))
44
- - ⚠️ **jsonapi:** negotiate ext, q, wildcards and content-type per spec ([2e2ffad](https://github.com/zeroxsolutions/zeroxsolutions/commit/2e2ffad))
59
+ - ⚠️ **jsonapi:** negotiate ext, q, wildcards and content-type per spec ([2e2ffad](https://github.com/zeroxsolutions/zeroxsolutions/commit/2e2ffad))
45
60
  - **jsonapi:** read q as a weight when parsing a media type ([5403c39](https://github.com/zeroxsolutions/zeroxsolutions/commit/5403c39))
46
61
 
47
- ### ⚠️ Breaking Changes
62
+ ### ⚠️ Breaking Changes
48
63
 
49
- - **jsonapi:** negotiate ext, q, wildcards and content-type per spec ([2e2ffad](https://github.com/zeroxsolutions/zeroxsolutions/commit/2e2ffad))
64
+ - **jsonapi:** negotiate ext, q, wildcards and content-type per spec ([2e2ffad](https://github.com/zeroxsolutions/zeroxsolutions/commit/2e2ffad))
50
65
  a Content-Type wildcard or a missing Content-Type is 415; an unsupported ext is 415 or 406; a wildcard no longer hides modified JSON:API Accept members; q=0 refuses a member.
51
66
 
52
67
  ### ❤️ Thank You
@@ -70,11 +85,11 @@ This was a version bump only for @zeroxsolutions/jsonapi to align it with other
70
85
 
71
86
  ### 💅 Refactors
72
87
 
73
- - ⚠️ **jsonapi:** list the names each entry publishes ([3746762](https://github.com/zeroxsolutions/zeroxsolutions/commit/3746762))
88
+ - ⚠️ **jsonapi:** list the names each entry publishes ([3746762](https://github.com/zeroxsolutions/zeroxsolutions/commit/3746762))
74
89
 
75
- ### ⚠️ Breaking Changes
90
+ ### ⚠️ Breaking Changes
76
91
 
77
- - **jsonapi:** list the names each entry publishes ([3746762](https://github.com/zeroxsolutions/zeroxsolutions/commit/3746762))
92
+ - **jsonapi:** list the names each entry publishes ([3746762](https://github.com/zeroxsolutions/zeroxsolutions/commit/3746762))
78
93
  the root no longer exports `parseMediaType`,
79
94
  `isJsonApiWithDisallowedParams` or `ParsedMediaType`; `negotiateJsonApi`
80
95
  is the negotiation decision. `./client` publishes the same names as
@@ -107,11 +122,11 @@ Version bump only, to take the repackaged `@zeroxsolutions/response`.
107
122
 
108
123
  ### 🚀 Features
109
124
 
110
- - ⚠️ **server,jsonapi:** one surface, one error-code vocabulary ([32b4aed](https://github.com/zeroxsolutions/zeroxsolutions/commit/32b4aed))
125
+ - ⚠️ **server,jsonapi:** one surface, one error-code vocabulary ([32b4aed](https://github.com/zeroxsolutions/zeroxsolutions/commit/32b4aed))
111
126
 
112
- ### ⚠️ Breaking Changes
127
+ ### ⚠️ Breaking Changes
113
128
 
114
- - **server,jsonapi:** one surface, one error-code vocabulary ([32b4aed](https://github.com/zeroxsolutions/zeroxsolutions/commit/32b4aed))
129
+ - **server,jsonapi:** one surface, one error-code vocabulary ([32b4aed](https://github.com/zeroxsolutions/zeroxsolutions/commit/32b4aed))
115
130
  createErrorHandler, createValidationHook and
116
131
  jsonApiContentNegotiation take a required `namespace`; negotiateJsonApi takes it
117
132
  as a third argument. Codes these mint are now `<namespace>.<name>`.
@@ -124,11 +139,11 @@ Version bump only, to take the repackaged `@zeroxsolutions/response`.
124
139
 
125
140
  ### 💅 Refactors
126
141
 
127
- - ⚠️ **query,response,server:** one wire spelling, flat exports ([5c57b3a](https://github.com/zeroxsolutions/zeroxsolutions/commit/5c57b3a))
142
+ - ⚠️ **query,response,server:** one wire spelling, flat exports ([5c57b3a](https://github.com/zeroxsolutions/zeroxsolutions/commit/5c57b3a))
128
143
 
129
- ### ⚠️ Breaking Changes
144
+ ### ⚠️ Breaking Changes
130
145
 
131
- - **query,response,server:** one wire spelling, flat exports ([5c57b3a](https://github.com/zeroxsolutions/zeroxsolutions/commit/5c57b3a))
146
+ - **query,response,server:** one wire spelling, flat exports ([5c57b3a](https://github.com/zeroxsolutions/zeroxsolutions/commit/5c57b3a))
132
147
  `@zeroxsolutions/response/http`, `@zeroxsolutions/query/http`
133
148
  and `@zeroxsolutions/server/hono/http` are gone, and the surviving `/jsonapi`
134
149
  and `/hono` subpaths move to the package root. Every import drops its subpath.
@@ -145,11 +160,11 @@ This was a version bump only for @zeroxsolutions/jsonapi to align it with other
145
160
 
146
161
  ### 💅 Refactors
147
162
 
148
- - ⚠️ **jsonapi:** widen negotiation, move its middleware out ([d28eb2b](https://github.com/zeroxsolutions/zeroxsolutions/commit/d28eb2b))
163
+ - ⚠️ **jsonapi:** widen negotiation, move its middleware out ([d28eb2b](https://github.com/zeroxsolutions/zeroxsolutions/commit/d28eb2b))
149
164
 
150
- ### ⚠️ Breaking Changes
165
+ ### ⚠️ Breaking Changes
151
166
 
152
- - **jsonapi:** widen negotiation, move its middleware out ([d28eb2b](https://github.com/zeroxsolutions/zeroxsolutions/commit/d28eb2b))
167
+ - **jsonapi:** widen negotiation, move its middleware out ([d28eb2b](https://github.com/zeroxsolutions/zeroxsolutions/commit/d28eb2b))
153
168
  jsonApiContentNegotiation is no longer exported here; it is
154
169
  @zeroxsolutions/server. negotiateJsonApi answers 415 for any
155
170
  Content-Type that is not the JSON:API media type, and 406 for an Accept naming
@@ -197,12 +212,12 @@ This was a version bump only for @zeroxsolutions/jsonapi to align it with other
197
212
 
198
213
  ### 🩹 Fixes
199
214
 
200
- - ⚠️ **jsonapi:** shape the collection model on the document, not the other spelling ([4ae8d26](https://github.com/zeroxsolutions/zeroxsolutions/commit/4ae8d26))
215
+ - ⚠️ **jsonapi:** shape the collection model on the document, not the other spelling ([4ae8d26](https://github.com/zeroxsolutions/zeroxsolutions/commit/4ae8d26))
201
216
  - **jsonapi:** add the reader source the previous commit left untracked ([7399114](https://github.com/zeroxsolutions/zeroxsolutions/commit/7399114))
202
217
 
203
- ### ⚠️ Breaking Changes
218
+ ### ⚠️ Breaking Changes
204
219
 
205
- - **jsonapi:** shape the collection model on the document, not the other spelling ([4ae8d26](https://github.com/zeroxsolutions/zeroxsolutions/commit/4ae8d26))
220
+ - **jsonapi:** shape the collection model on the document, not the other spelling ([4ae8d26](https://github.com/zeroxsolutions/zeroxsolutions/commit/4ae8d26))
206
221
  `Collection.items` is `Collection.data`, and `Collection.total`
207
222
  is gone - read `meta` for it.
208
223
 
@@ -306,12 +321,12 @@ This was a version bump only for @zeroxsolutions/jsonapi to align it with other
306
321
 
307
322
  ### 🚀 Features
308
323
 
309
- - ⚠️ **jsonapi:** bind the permit-list to a row and an operator vocabulary, and pipe the schema through it ([5f824d0](https://github.com/zeroxsolutions/zeroxsolutions/commit/5f824d0))
324
+ - ⚠️ **jsonapi:** bind the permit-list to a row and an operator vocabulary, and pipe the schema through it ([5f824d0](https://github.com/zeroxsolutions/zeroxsolutions/commit/5f824d0))
310
325
  - **jsonapi:** declare a list endpoint's query parameters from its permit-list ([bb3c3c8](https://github.com/zeroxsolutions/zeroxsolutions/commit/bb3c3c8))
311
326
 
312
- ### ⚠️ Breaking Changes
327
+ ### ⚠️ Breaking Changes
313
328
 
314
- - **jsonapi:** bind the permit-list to a row and an operator vocabulary, and pipe the schema through it ([5f824d0](https://github.com/zeroxsolutions/zeroxsolutions/commit/5f824d0))
329
+ - **jsonapi:** bind the permit-list to a row and an operator vocabulary, and pipe the schema through it ([5f824d0](https://github.com/zeroxsolutions/zeroxsolutions/commit/5f824d0))
315
330
  `jsonApiQuerySchema` now returns a piped schema whose output is
316
331
  `GuardedJsonApiQuery`, not the raw query map. Unreleased, so no consumer is affected.
317
332
  The two type-level checks are pinned by `@ts-expect-error` in a spec, watched failing
@@ -339,11 +354,11 @@ This was a version bump only for @zeroxsolutions/jsonapi to align it with other
339
354
 
340
355
  ### 🚀 Features
341
356
 
342
- - ⚠️ **jsonapi:** shape the document by the query, and close the escape hatch ([79da0f2](https://github.com/zeroxsolutions/zeroxsolutions/commit/79da0f2))
357
+ - ⚠️ **jsonapi:** shape the document by the query, and close the escape hatch ([79da0f2](https://github.com/zeroxsolutions/zeroxsolutions/commit/79da0f2))
343
358
 
344
- ### ⚠️ Breaking Changes
359
+ ### ⚠️ Breaking Changes
345
360
 
346
- - **jsonapi:** shape the document by the query, and close the escape hatch ([79da0f2](https://github.com/zeroxsolutions/zeroxsolutions/commit/79da0f2))
361
+ - **jsonapi:** shape the document by the query, and close the escape hatch ([79da0f2](https://github.com/zeroxsolutions/zeroxsolutions/commit/79da0f2))
347
362
  `customFamilies` removed from `JsonApiQuerySpec`, its
348
363
  argument removed from `unprocessableQueryParams`, and `isCustomFamilyBase` no
349
364
  longer exported. No declaration can accept a parameter outside the families
@@ -358,13 +373,13 @@ This was a version bump only for @zeroxsolutions/jsonapi to align it with other
358
373
 
359
374
  ### 🚀 Features
360
375
 
361
- - ⚠️ **jsonapi:** bind the list query to a permit-list and parse the filter tree ([2c212b0](https://github.com/zeroxsolutions/zeroxsolutions/commit/2c212b0))
376
+ - ⚠️ **jsonapi:** bind the list query to a permit-list and parse the filter tree ([2c212b0](https://github.com/zeroxsolutions/zeroxsolutions/commit/2c212b0))
362
377
  - **jsonapi:** screen query parameters against the standard's grammar ([e7bd26c](https://github.com/zeroxsolutions/zeroxsolutions/commit/e7bd26c))
363
378
  - **jsonapi:** refuse a query the endpoint did not permit ([1dd1df6](https://github.com/zeroxsolutions/zeroxsolutions/commit/1dd1df6))
364
379
 
365
- ### ⚠️ Breaking Changes
380
+ ### ⚠️ Breaking Changes
366
381
 
367
- - **jsonapi:** bind the list query to a permit-list and parse the filter tree ([2c212b0](https://github.com/zeroxsolutions/zeroxsolutions/commit/2c212b0))
382
+ - **jsonapi:** bind the list query to a permit-list and parse the filter tree ([2c212b0](https://github.com/zeroxsolutions/zeroxsolutions/commit/2c212b0))
368
383
  `parseJsonApiQuery` and `parseFilter` are removed;
369
384
  `filterable` entries are `{ operators, type }` rather than a bare operator
370
385
  list; `filter` is a bracketed family, not a JSON-encoded array; queries
@@ -420,11 +435,11 @@ This was a version bump only for @zeroxsolutions/jsonapi to align it with other
420
435
 
421
436
  ### 🚀 Features
422
437
 
423
- - ⚠️ add JSON:API v1.1 toolkit and routing lib, dissolve common ([53545a6](https://github.com/zeroxsolutions/zeroxsolutions/commit/53545a6))
438
+ - ⚠️ add JSON:API v1.1 toolkit and routing lib, dissolve common ([53545a6](https://github.com/zeroxsolutions/zeroxsolutions/commit/53545a6))
424
439
 
425
- ### ⚠️ Breaking Changes
440
+ ### ⚠️ Breaking Changes
426
441
 
427
- - add JSON:API v1.1 toolkit and routing lib, dissolve common ([53545a6](https://github.com/zeroxsolutions/zeroxsolutions/commit/53545a6))
442
+ - add JSON:API v1.1 toolkit and routing lib, dissolve common ([53545a6](https://github.com/zeroxsolutions/zeroxsolutions/commit/53545a6))
428
443
  @zeroxsolutions/common is removed. Its error/response
429
444
  schemas move to @zeroxsolutions/jsonapi; its path/is-match helpers move to
430
445
  @zeroxsolutions/routing.
@@ -432,4 +447,4 @@ This was a version bump only for @zeroxsolutions/jsonapi to align it with other
432
447
  ### ❤️ Thank You
433
448
 
434
449
  - Claude Opus 4.8 (1M context)
435
- - Lương Văn Tú
450
+ - Lương Văn Tú
package/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # @zeroxsolutions/jsonapi
2
2
 
3
3
  The [JSON:API v1.1](https://jsonapi.org/) halves that are **not** a response body: content negotiation
4
- on the request, and document deserialization on the client.
4
+ on the request, the finder options a guarded query reads as, and document deserialization on the client.
5
5
 
6
6
  Everything a server puts _in_ a response - the document schemas, the serializers, the `errors[]`
7
7
  document - is `@zeroxsolutions/response`, and the central `onError` that renders it is
@@ -18,16 +18,16 @@ asks for, what the server answers, and what the caller may address.
18
18
  pnpm add @zeroxsolutions/jsonapi @zeroxsolutions/response
19
19
  ```
20
20
 
21
- No peer dep, and no framework: every export here is a plain function over strings and documents. The
21
+ No peer dep, and no framework: every export here is a plain function over strings, queries and documents. The
22
22
  Hono middleware that mounts the negotiation decision is `@zeroxsolutions/server`, which is
23
23
  what keeps `hono` out of the package an SPA also loads.
24
24
 
25
25
  ## Entry points
26
26
 
27
- | Import | Use in | What it holds |
28
- | -------------------------------- | ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
29
- | `@zeroxsolutions/jsonapi` | services, gateway | `negotiateJsonApi` - the whole decision, from `Content-Type`, `Accept` and the `ext` URIs this surface supports (`supportedExtensions`), to the `JsonApiError` to reject with or `null`: 415 where `Content-Type` is the JSON:API media type carrying a parameter beyond `ext`/`profile` or an `ext` outside `supportedExtensions`; 406 where `Accept` holds JSON:API instances and every one carries such a parameter or `ext`; nothing else |
30
- | `@zeroxsolutions/jsonapi/client` | SPA, external consumers | `deserializeDocument` (jsona), `readResource` / `readCollection` over it - the same flattening, typed off the route's own document - `readRelated` for what an `include` resolved, which no document type can name; `toRequestDocument`, the JSON:API request document a write sends, each relationship wrapped in its relationship object; and `ApiError`, the non-2xx a caller throws once a response has come back |
27
+ | Import | Use in | What it holds |
28
+ | -------------------------------- | ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
29
+ | `@zeroxsolutions/jsonapi` | services, gateway | `negotiateJsonApi` - the whole decision, from `Content-Type`, `Accept` and the `ext` URIs this surface supports (`supportedExtensions`), to the `JsonApiError` to reject with or `null`: 415 where `Content-Type` is the JSON:API media type carrying a parameter beyond `ext`/`profile` or an `ext` outside `supportedExtensions`; 406 where `Accept` holds JSON:API instances and every one carries such a parameter or `ext`; nothing else. `findOptionsOf` - a guarded query to the options `@zeroxsolutions/db`'s finders take |
30
+ | `@zeroxsolutions/jsonapi/client` | SPA, external consumers | `deserializeDocument` (jsona), `readResource` / `readCollection` over it - the same flattening, typed off the route's own document - `readRelated` for what an `include` resolved, which no document type can name; `toRequestDocument`, the JSON:API request document a write sends, each relationship wrapped in its relationship object; and `ApiError`, the non-2xx a caller throws once a response has come back |
31
31
 
32
32
  A transport mounts the decision rather than calling it: `jsonApiContentNegotiation` is the Hono adapter
33
33
  over it, and it lives with the other framework wiring. `negotiateJsonApi` itself takes the surface's
@@ -74,3 +74,30 @@ The decision is the four server rules of JSON:API v1.1 and nothing more:
74
74
  names `Content-Type`, or one fewer usable member in `Accept`, which is a 406 where none is left.
75
75
  `supportedExtensions` is `negotiateJsonApi`'s fourth parameter: the extension URIs this surface answers
76
76
  `ext` requests for, empty by default, which refuses every `ext`.
77
+
78
+ ## A guarded query as a finder's options
79
+
80
+ `findOptionsOf(query, permits)` turns the query `@zeroxsolutions/query`'s guard answered into the options a base
81
+ repository's finder takes, so no route writes a converter of its own. It is called from the controller, and the
82
+ finder hands the options to the base unchanged:
83
+
84
+ ```ts
85
+ import { findOptionsOf } from '@zeroxsolutions/jsonapi';
86
+
87
+ const options = findOptionsOf(c.req.valid('query'), placesQueryPermits); // { read, filter, orderBy, limit, offset }
88
+ const [rows, total] = await placesRepository.findAllAndCountByTenantId(tenantId, options);
89
+ ```
90
+
91
+ A read-one query answers `{ read }` alone. The permits name the `type` the read starts from and the `relationships`
92
+ `relationsOf` answers in `@zeroxsolutions/db/utils`; it throws on permits naming no `type`. The `read` is built so:
93
+
94
+ | The guarded query holds | The read loads |
95
+ | -------------------------------------------- | ------------------------------------------------------------------------------ |
96
+ | a column in the type's fieldset | that column |
97
+ | a relationship in the fieldset, not included | the related row's primary key, which is the linkage its relationship publishes |
98
+ | a facet (`facets`) in the fieldset | that relation under its own type's fieldset, exactly |
99
+ | an `include` path | every relation on it, each under its type's fieldset and its primary key |
100
+ | a computed name given a `ComputedRead` | the columns and relations it names, whatever the fieldset asked |
101
+
102
+ The filter, the order and the page window pass through unchanged. The package depends on `@zeroxsolutions/query`
103
+ alone: the read names are strings, and `@zeroxsolutions/db` refuses one its table does not hold with `UnknownReadName`.
package/dist/index.d.ts CHANGED
@@ -1,2 +1,3 @@
1
1
  export { negotiateJsonApi } from './lib/negotiation/index.js';
2
+ export { findOptionsOf, type CollectionFindOptions, type ReadOf, type ResourceFindOptions, } from './lib/find-options/index.js';
2
3
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,4BAA4B,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,4BAA4B,CAAC;AAC9D,OAAO,EACL,aAAa,EACb,KAAK,qBAAqB,EAC1B,KAAK,MAAM,EACX,KAAK,mBAAmB,GACzB,MAAM,6BAA6B,CAAC"}
package/dist/index.js CHANGED
@@ -1 +1,2 @@
1
1
  export { negotiateJsonApi } from './lib/negotiation/index.js';
2
+ export { findOptionsOf, } from './lib/find-options/index.js';
@@ -0,0 +1,41 @@
1
+ import type { FilterTree, PermittedCollectionQuery, PermittedResourceQuery, ResourceQueryPermits, SortField } from '@zeroxsolutions/query';
2
+ /**
3
+ * What one level of a read loads, in the shape a base finder of `@zeroxsolutions/db` takes: columns absent load every
4
+ * column, and a relation named under `relations` loads under its own read.
5
+ */
6
+ export interface ReadOf {
7
+ columns?: string[];
8
+ relations?: Record<string, ReadOf>;
9
+ }
10
+ /** The finder options a guarded read-one query asks for. */
11
+ export interface ResourceFindOptions {
12
+ read: ReadOf;
13
+ }
14
+ /** The finder options a guarded collection query asks for: the read, and its filter, order and page window. */
15
+ export interface CollectionFindOptions<Leaf> extends ResourceFindOptions {
16
+ filter: FilterTree<Leaf>[];
17
+ orderBy: SortField[];
18
+ offset: number;
19
+ limit: number;
20
+ }
21
+ /**
22
+ * The finder options a guarded query asks for, built off the permits that guarded it: one function for every entity,
23
+ * so no context declares a converter of its own.
24
+ *
25
+ * | The guarded query holds | The options carry |
26
+ * | --- | --- |
27
+ * | `fields[<type>]` naming columns | those columns in `read` |
28
+ * | a relationship in a fieldset, not included | that relation in `read` with its primary key alone, the linkage it publishes |
29
+ * | an `include` path | every relation on it in `read`, each under its own type's fieldset and its primary key |
30
+ * | a facet in a fieldset | that relation in `read` with exactly the columns its own type's fieldset names |
31
+ * | `filter`, `sort`, `offset`, `limit` | `filter`, `orderBy`, `offset` and `limit`, unchanged |
32
+ *
33
+ * Each computed attribute the permits map to a read loads that read on every query, whatever fieldset was sent.
34
+ *
35
+ * @param permits - the permits the query was guarded under; its `type` names the root type and its `relationships`
36
+ * (from `relationsOf` in `@zeroxsolutions/db/utils`) what each relationship reaches
37
+ * @throws Error when the permits name no `type`, or an include path a relationship of theirs does not reach
38
+ */
39
+ export declare function findOptionsOf<Leaf, P extends ResourceQueryPermits>(query: PermittedCollectionQuery<Leaf, P>, permits: P): CollectionFindOptions<Leaf>;
40
+ export declare function findOptionsOf<P extends ResourceQueryPermits>(query: PermittedResourceQuery<P>, permits: P): ResourceFindOptions;
41
+ //# sourceMappingURL=find-options-of.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"find-options-of.d.ts","sourceRoot":"","sources":["../../../src/lib/find-options/find-options-of.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAEV,UAAU,EACV,wBAAwB,EACxB,sBAAsB,EAEtB,oBAAoB,EACpB,SAAS,EACV,MAAM,uBAAuB,CAAC;AAE/B;;;GAGG;AACH,MAAM,WAAW,MAAM;IACrB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACpC;AAED,4DAA4D;AAC5D,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,MAAM,CAAC;CACd;AAED,+GAA+G;AAC/G,MAAM,WAAW,qBAAqB,CAAC,IAAI,CAAE,SAAQ,mBAAmB;IACtE,MAAM,EAAE,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC;IAC3B,OAAO,EAAE,SAAS,EAAE,CAAC;IACrB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;CACf;AA4FD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,CAAC,SAAS,oBAAoB,EAChE,KAAK,EAAE,wBAAwB,CAAC,IAAI,EAAE,CAAC,CAAC,EACxC,OAAO,EAAE,CAAC,GACT,qBAAqB,CAAC,IAAI,CAAC,CAAC;AAC/B,wBAAgB,aAAa,CAAC,CAAC,SAAS,oBAAoB,EAC1D,KAAK,EAAE,sBAAsB,CAAC,CAAC,CAAC,EAChC,OAAO,EAAE,CAAC,GACT,mBAAmB,CAAC"}
@@ -0,0 +1,86 @@
1
+ function includeTreeOf(paths) {
2
+ const root = {};
3
+ for (const path of paths ?? []) {
4
+ let level = root;
5
+ for (const segment of path.split('.'))
6
+ level = level[segment] ??= {};
7
+ }
8
+ return root;
9
+ }
10
+ /** `a` and `b` joined, each name once, in the order they were first named. */
11
+ const union = (a, b) => [...new Set([...a, ...b])];
12
+ /**
13
+ * Adds what a computed attribute is computed from to a level. Columns join where both name some; a level that names
14
+ * none already loads every column, and a computed read naming none adds nothing to a level that names some.
15
+ */
16
+ function mergeComputed(level, computed) {
17
+ if (computed.columns !== undefined && level.columns !== undefined)
18
+ level.columns = union(level.columns, computed.columns);
19
+ for (const [name, nested] of Object.entries(computed.relations ?? {})) {
20
+ level.relations ??= {};
21
+ const existing = level.relations[name];
22
+ if (existing) {
23
+ mergeComputed(existing, nested);
24
+ continue;
25
+ }
26
+ const added = nested.columns === undefined ? {} : { columns: [...nested.columns] };
27
+ level.relations[name] = added;
28
+ mergeComputed(added, { relations: nested.relations });
29
+ }
30
+ }
31
+ /**
32
+ * One level of the read: its fieldset's columns, the linkage of each relationship it names, each facet under its own
33
+ * table's fieldset, each included relation under its own type's fieldset, and what its computed attributes need.
34
+ */
35
+ function levelOf(walk, type, relations, include, path) {
36
+ const level = {};
37
+ const fieldset = walk.fields[type];
38
+ const facets = new Set(walk.permits.facets?.[type] ?? []);
39
+ if (fieldset !== undefined) {
40
+ level.columns = fieldset.filter((name) => !Object.hasOwn(relations, name));
41
+ for (const name of fieldset) {
42
+ const target = Object.hasOwn(relations, name) ? relations[name] : undefined;
43
+ if (!target)
44
+ continue;
45
+ level.relations ??= {};
46
+ const facetColumns = walk.fields[target.type];
47
+ level.relations[name] = facets.has(name)
48
+ ? facetColumns === undefined
49
+ ? {}
50
+ : { columns: [...facetColumns] }
51
+ : { columns: [...target.primaryKey] };
52
+ }
53
+ }
54
+ for (const [name, below] of Object.entries(include)) {
55
+ const target = Object.hasOwn(relations, name) ? relations[name] : undefined;
56
+ if (!target)
57
+ throw new Error(`"${path}${name}" names no relationship the permits' relationships declare.`);
58
+ const included = levelOf(walk, target.type, target.relations, below, `${path}${name}.`);
59
+ // An included resource publishes its id, which its type's fieldset never names.
60
+ if (included.columns !== undefined)
61
+ included.columns = union(target.primaryKey, included.columns);
62
+ level.relations ??= {};
63
+ level.relations[name] = included;
64
+ }
65
+ const computed = walk.permits.computed?.[type];
66
+ if (computed !== undefined && !Array.isArray(computed)) {
67
+ for (const read of Object.values(computed))
68
+ mergeComputed(level, read);
69
+ }
70
+ return level;
71
+ }
72
+ export function findOptionsOf(query, permits) {
73
+ if (permits.type === undefined)
74
+ throw new Error('The permits name no `type` for the read to start from.');
75
+ const walk = { fields: query.fields, permits };
76
+ const read = levelOf(walk, permits.type, permits.relationships ?? {}, includeTreeOf(query.include), '');
77
+ if (query.limit === undefined)
78
+ return { read };
79
+ return {
80
+ read,
81
+ filter: query.filter ?? [],
82
+ orderBy: query.sort ?? [],
83
+ offset: query.offset ?? 0,
84
+ limit: query.limit,
85
+ };
86
+ }
@@ -0,0 +1,2 @@
1
+ export * from './find-options-of.js';
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/lib/find-options/index.ts"],"names":[],"mappings":"AAAA,cAAc,sBAAsB,CAAC"}
@@ -0,0 +1 @@
1
+ export * from './find-options-of.js';
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@zeroxsolutions/jsonapi",
3
- "version": "0.13.2",
3
+ "version": "0.13.4",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "sideEffects": false,
7
- "description": "The JSON:API halves that are not the response body: v1.1 content negotiation (media type, 415/406/400) and the client-side document deserializer. The response document, its serializers and its error document live in @zeroxsolutions/response.",
7
+ "description": "The JSON:API halves that are not the response body: v1.1 content negotiation (media type, 415/406/400), findOptionsOf from a guarded query to a finder's options, and the client-side document deserializer. The response document, its serializers and its error document live in @zeroxsolutions/response.",
8
8
  "main": "./dist/index.js",
9
9
  "module": "./dist/index.js",
10
10
  "types": "./dist/index.d.ts",
@@ -30,6 +30,7 @@
30
30
  "dependencies": {
31
31
  "jsona": "^1.12.0",
32
32
  "tslib": "^2.3.0",
33
- "@zeroxsolutions/response": "0.14.4"
33
+ "@zeroxsolutions/response": "0.14.5",
34
+ "@zeroxsolutions/query": "0.7.4"
34
35
  }
35
36
  }