@zeroxsolutions/jsonapi 0.13.1 → 0.13.3

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.3 (2026-10-18)
2
+
3
+ ### 🚀 Features
4
+
5
+ - **jsonapi:** turn a guarded query into a finder's options ([9f5f895](https://github.com/zeroxsolutions/zeroxsolutions/commit/9f5f895))
6
+
7
+ ### ❤️ Thank You
8
+
9
+ - Claude Opus 5.5
10
+ - Lương Văn Tú
11
+
12
+ ## 0.13.2 (2026-10-06)
13
+
14
+ No code changes. It depends on @zeroxsolutions/response, so its version follows response 0.14.4.
15
+
1
16
  ## 0.13.1 (2026-09-30)
2
17
 
3
18
  ### 🚀 Features
@@ -13,11 +28,11 @@
13
28
 
14
29
  ### 🩹 Fixes
15
30
 
16
- - ⚠️ **jsonapi:** negotiate by the four json:api server rules alone ([67f4529](https://github.com/zeroxsolutions/zeroxsolutions/commit/67f4529))
31
+ - ⚠️ **jsonapi:** negotiate by the four json:api server rules alone ([67f4529](https://github.com/zeroxsolutions/zeroxsolutions/commit/67f4529))
17
32
 
18
- ### ⚠️ Breaking Changes
33
+ ### ⚠️ Breaking Changes
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
  negotiateJsonApi returns null for a Content-Type that
22
37
  is not the JSON:API media type (or absent), and for an Accept that holds
23
38
  no JSON:API instance. A route refuses a body it cannot read through its
@@ -37,12 +52,12 @@ This was a version bump only for @zeroxsolutions/jsonapi to align it with other
37
52
  ### 🩹 Fixes
38
53
 
39
54
  - **jsonapi:** read every content-type parameter, q included ([07f81b9](https://github.com/zeroxsolutions/zeroxsolutions/commit/07f81b9))
40
- - ⚠️ **jsonapi:** negotiate ext, q, wildcards and content-type per spec ([2e2ffad](https://github.com/zeroxsolutions/zeroxsolutions/commit/2e2ffad))
55
+ - ⚠️ **jsonapi:** negotiate ext, q, wildcards and content-type per spec ([2e2ffad](https://github.com/zeroxsolutions/zeroxsolutions/commit/2e2ffad))
41
56
  - **jsonapi:** read q as a weight when parsing a media type ([5403c39](https://github.com/zeroxsolutions/zeroxsolutions/commit/5403c39))
42
57
 
43
- ### ⚠️ Breaking Changes
58
+ ### ⚠️ Breaking Changes
44
59
 
45
- - **jsonapi:** negotiate ext, q, wildcards and content-type per spec ([2e2ffad](https://github.com/zeroxsolutions/zeroxsolutions/commit/2e2ffad))
60
+ - **jsonapi:** negotiate ext, q, wildcards and content-type per spec ([2e2ffad](https://github.com/zeroxsolutions/zeroxsolutions/commit/2e2ffad))
46
61
  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.
47
62
 
48
63
  ### ❤️ Thank You
@@ -66,11 +81,11 @@ This was a version bump only for @zeroxsolutions/jsonapi to align it with other
66
81
 
67
82
  ### 💅 Refactors
68
83
 
69
- - ⚠️ **jsonapi:** list the names each entry publishes ([3746762](https://github.com/zeroxsolutions/zeroxsolutions/commit/3746762))
84
+ - ⚠️ **jsonapi:** list the names each entry publishes ([3746762](https://github.com/zeroxsolutions/zeroxsolutions/commit/3746762))
70
85
 
71
- ### ⚠️ Breaking Changes
86
+ ### ⚠️ Breaking Changes
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
  the root no longer exports `parseMediaType`,
75
90
  `isJsonApiWithDisallowedParams` or `ParsedMediaType`; `negotiateJsonApi`
76
91
  is the negotiation decision. `./client` publishes the same names as
@@ -103,11 +118,11 @@ Version bump only, to take the repackaged `@zeroxsolutions/response`.
103
118
 
104
119
  ### 🚀 Features
105
120
 
106
- - ⚠️ **server,jsonapi:** one surface, one error-code vocabulary ([32b4aed](https://github.com/zeroxsolutions/zeroxsolutions/commit/32b4aed))
121
+ - ⚠️ **server,jsonapi:** one surface, one error-code vocabulary ([32b4aed](https://github.com/zeroxsolutions/zeroxsolutions/commit/32b4aed))
107
122
 
108
- ### ⚠️ Breaking Changes
123
+ ### ⚠️ Breaking Changes
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
  createErrorHandler, createValidationHook and
112
127
  jsonApiContentNegotiation take a required `namespace`; negotiateJsonApi takes it
113
128
  as a third argument. Codes these mint are now `<namespace>.<name>`.
@@ -120,11 +135,11 @@ Version bump only, to take the repackaged `@zeroxsolutions/response`.
120
135
 
121
136
  ### 💅 Refactors
122
137
 
123
- - ⚠️ **query,response,server:** one wire spelling, flat exports ([5c57b3a](https://github.com/zeroxsolutions/zeroxsolutions/commit/5c57b3a))
138
+ - ⚠️ **query,response,server:** one wire spelling, flat exports ([5c57b3a](https://github.com/zeroxsolutions/zeroxsolutions/commit/5c57b3a))
124
139
 
125
- ### ⚠️ Breaking Changes
140
+ ### ⚠️ Breaking Changes
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
  `@zeroxsolutions/response/http`, `@zeroxsolutions/query/http`
129
144
  and `@zeroxsolutions/server/hono/http` are gone, and the surviving `/jsonapi`
130
145
  and `/hono` subpaths move to the package root. Every import drops its subpath.
@@ -141,11 +156,11 @@ This was a version bump only for @zeroxsolutions/jsonapi to align it with other
141
156
 
142
157
  ### 💅 Refactors
143
158
 
144
- - ⚠️ **jsonapi:** widen negotiation, move its middleware out ([d28eb2b](https://github.com/zeroxsolutions/zeroxsolutions/commit/d28eb2b))
159
+ - ⚠️ **jsonapi:** widen negotiation, move its middleware out ([d28eb2b](https://github.com/zeroxsolutions/zeroxsolutions/commit/d28eb2b))
145
160
 
146
- ### ⚠️ Breaking Changes
161
+ ### ⚠️ Breaking Changes
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
  jsonApiContentNegotiation is no longer exported here; it is
150
165
  @zeroxsolutions/server. negotiateJsonApi answers 415 for any
151
166
  Content-Type that is not the JSON:API media type, and 406 for an Accept naming
@@ -193,12 +208,12 @@ This was a version bump only for @zeroxsolutions/jsonapi to align it with other
193
208
 
194
209
  ### 🩹 Fixes
195
210
 
196
- - ⚠️ **jsonapi:** shape the collection model on the document, not the other spelling ([4ae8d26](https://github.com/zeroxsolutions/zeroxsolutions/commit/4ae8d26))
211
+ - ⚠️ **jsonapi:** shape the collection model on the document, not the other spelling ([4ae8d26](https://github.com/zeroxsolutions/zeroxsolutions/commit/4ae8d26))
197
212
  - **jsonapi:** add the reader source the previous commit left untracked ([7399114](https://github.com/zeroxsolutions/zeroxsolutions/commit/7399114))
198
213
 
199
- ### ⚠️ Breaking Changes
214
+ ### ⚠️ Breaking Changes
200
215
 
201
- - **jsonapi:** shape the collection model on the document, not the other spelling ([4ae8d26](https://github.com/zeroxsolutions/zeroxsolutions/commit/4ae8d26))
216
+ - **jsonapi:** shape the collection model on the document, not the other spelling ([4ae8d26](https://github.com/zeroxsolutions/zeroxsolutions/commit/4ae8d26))
202
217
  `Collection.items` is `Collection.data`, and `Collection.total`
203
218
  is gone - read `meta` for it.
204
219
 
@@ -302,12 +317,12 @@ This was a version bump only for @zeroxsolutions/jsonapi to align it with other
302
317
 
303
318
  ### 🚀 Features
304
319
 
305
- - ⚠️ **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))
320
+ - ⚠️ **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))
306
321
  - **jsonapi:** declare a list endpoint's query parameters from its permit-list ([bb3c3c8](https://github.com/zeroxsolutions/zeroxsolutions/commit/bb3c3c8))
307
322
 
308
- ### ⚠️ Breaking Changes
323
+ ### ⚠️ Breaking Changes
309
324
 
310
- - **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))
325
+ - **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))
311
326
  `jsonApiQuerySchema` now returns a piped schema whose output is
312
327
  `GuardedJsonApiQuery`, not the raw query map. Unreleased, so no consumer is affected.
313
328
  The two type-level checks are pinned by `@ts-expect-error` in a spec, watched failing
@@ -335,11 +350,11 @@ This was a version bump only for @zeroxsolutions/jsonapi to align it with other
335
350
 
336
351
  ### 🚀 Features
337
352
 
338
- - ⚠️ **jsonapi:** shape the document by the query, and close the escape hatch ([79da0f2](https://github.com/zeroxsolutions/zeroxsolutions/commit/79da0f2))
353
+ - ⚠️ **jsonapi:** shape the document by the query, and close the escape hatch ([79da0f2](https://github.com/zeroxsolutions/zeroxsolutions/commit/79da0f2))
339
354
 
340
- ### ⚠️ Breaking Changes
355
+ ### ⚠️ Breaking Changes
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
  `customFamilies` removed from `JsonApiQuerySpec`, its
344
359
  argument removed from `unprocessableQueryParams`, and `isCustomFamilyBase` no
345
360
  longer exported. No declaration can accept a parameter outside the families
@@ -354,13 +369,13 @@ This was a version bump only for @zeroxsolutions/jsonapi to align it with other
354
369
 
355
370
  ### 🚀 Features
356
371
 
357
- - ⚠️ **jsonapi:** bind the list query to a permit-list and parse the filter tree ([2c212b0](https://github.com/zeroxsolutions/zeroxsolutions/commit/2c212b0))
372
+ - ⚠️ **jsonapi:** bind the list query to a permit-list and parse the filter tree ([2c212b0](https://github.com/zeroxsolutions/zeroxsolutions/commit/2c212b0))
358
373
  - **jsonapi:** screen query parameters against the standard's grammar ([e7bd26c](https://github.com/zeroxsolutions/zeroxsolutions/commit/e7bd26c))
359
374
  - **jsonapi:** refuse a query the endpoint did not permit ([1dd1df6](https://github.com/zeroxsolutions/zeroxsolutions/commit/1dd1df6))
360
375
 
361
- ### ⚠️ Breaking Changes
376
+ ### ⚠️ Breaking Changes
362
377
 
363
- - **jsonapi:** bind the list query to a permit-list and parse the filter tree ([2c212b0](https://github.com/zeroxsolutions/zeroxsolutions/commit/2c212b0))
378
+ - **jsonapi:** bind the list query to a permit-list and parse the filter tree ([2c212b0](https://github.com/zeroxsolutions/zeroxsolutions/commit/2c212b0))
364
379
  `parseJsonApiQuery` and `parseFilter` are removed;
365
380
  `filterable` entries are `{ operators, type }` rather than a bare operator
366
381
  list; `filter` is a bracketed family, not a JSON-encoded array; queries
@@ -416,11 +431,11 @@ This was a version bump only for @zeroxsolutions/jsonapi to align it with other
416
431
 
417
432
  ### 🚀 Features
418
433
 
419
- - ⚠️ add JSON:API v1.1 toolkit and routing lib, dissolve common ([53545a6](https://github.com/zeroxsolutions/zeroxsolutions/commit/53545a6))
434
+ - ⚠️ add JSON:API v1.1 toolkit and routing lib, dissolve common ([53545a6](https://github.com/zeroxsolutions/zeroxsolutions/commit/53545a6))
420
435
 
421
- ### ⚠️ Breaking Changes
436
+ ### ⚠️ Breaking Changes
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
  @zeroxsolutions/common is removed. Its error/response
425
440
  schemas move to @zeroxsolutions/jsonapi; its path/is-match helpers move to
426
441
  @zeroxsolutions/routing.
@@ -428,4 +443,4 @@ This was a version bump only for @zeroxsolutions/jsonapi to align it with other
428
443
  ### ❤️ Thank You
429
444
 
430
445
  - Claude Opus 4.8 (1M context)
431
- - Lương Văn Tú
446
+ - 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.1",
3
+ "version": "0.13.3",
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.3"
33
+ "@zeroxsolutions/query": "0.7.4",
34
+ "@zeroxsolutions/response": "0.14.4"
34
35
  }
35
36
  }