@haddowg/json-api-codegen 1.4.0 → 1.6.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/dist/cli.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { r as CodegenConfig } from "./index-Cnj374D6.mjs";
1
+ import { r as CodegenConfig } from "./index-CU-kUx70.mjs";
2
2
 
3
3
  //#region src/cli.d.ts
4
4
  declare const USAGE = "Usage: json-api-codegen --input <url|file> --output <file> [--server <name>] [--schemas <url|file>] [--check]";
package/dist/cli.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { n as generate, r as schemasOutputPath, t as check } from "./src-BE7OMLWG.mjs";
2
+ import { n as generate, r as schemasOutputPath, t as check } from "./src-C2INS1Wd.mjs";
3
3
  import { realpathSync } from "node:fs";
4
4
  import { fileURLToPath } from "node:url";
5
5
  //#region src/cli.ts
@@ -48,6 +48,7 @@ interface ParameterObject {
48
48
  'x-profile'?: string;
49
49
  }
50
50
  interface OperationObject {
51
+ operationId?: string;
51
52
  parameters?: readonly ParameterObject[];
52
53
  requestBody?: RequestBodyObject;
53
54
  responses?: Record<string, ResponseObject>;
@@ -76,12 +77,11 @@ interface OpenApiDocument {
76
77
  //#endregion
77
78
  //#region src/build-descriptor.d.ts
78
79
  declare class DescriptorBuilder {
79
- private readonly doc;
80
80
  private readonly schemas;
81
81
  private readonly paths;
82
82
  /** Lazily-built wire-type -> collection-level paginator kind (for per-relation divergence, D6a). */
83
83
  private typePaginators?;
84
- constructor(doc: OpenApiDocument);
84
+ constructor(source: OpenApiDocument);
85
85
  /**
86
86
  * The collection-level paginator kind for a WIRE type (`tracks`), memoised. Resolves each
87
87
  * resource schema's wire type (its `type.const`) to the paginator of its collection GET — the
@@ -89,13 +89,6 @@ declare class DescriptorBuilder {
89
89
  */
90
90
  private typePaginator;
91
91
  build(): ApiDescriptor;
92
- /**
93
- * Warn about relations a create or update body accepts but the resource never renders. A
94
- * write-only relation (generator contract 5) is left out of the response schema, and the
95
- * descriptor has no way to mark a relation as writable but unreadable, so it is missing from
96
- * the client entirely rather than typed onto reads that never carry it.
97
- */
98
- private warnWriteOnlyRelations;
99
92
  /**
100
93
  * The server-level Atomic Operations capability: the path whose `POST` requestBody
101
94
  * declares the atomic ext media type ({@link ATOMIC_EXT}). `null` when no such endpoint
@@ -111,7 +104,26 @@ declare class DescriptorBuilder {
111
104
  /** Follow a `$ref` to its identifier/resource schema and read `properties.type.const`. */
112
105
  private resolveConst;
113
106
  private attributes;
107
+ /**
108
+ * The relations of a type: every relation its resource renders, plus each write-only relation
109
+ * the create and update bodies accept. A write-only relation is one whose body property carries
110
+ * `writeOnly: true` (generator contract 8) or, in an older document, one the resource doesn't
111
+ * render. A pivot relation reads its response pivot off the rendered relationship and its
112
+ * request pivot off the write bodies.
113
+ */
114
114
  private relations;
115
+ /**
116
+ * The relationships the create and update bodies accept: each one's component `$ref` and
117
+ * whether the body marks it `writeOnly`.
118
+ */
119
+ private writeRelationships;
120
+ /**
121
+ * Each relation's related and relationship paths, keyed by relation name, from the
122
+ * operationIds the projector stamps (`fetchRelated.<type>.<relation>`, …). A relation's path
123
+ * segment is its URI field name, which may differ from its name, so the paths are never
124
+ * derived from the name when an operationId names them.
125
+ */
126
+ private relationEndpoints;
115
127
  /**
116
128
  * The per-relation mutation capability, derived from the HTTP methods the relationship
117
129
  * endpoint (`{collection}/{id}/relationships/{rel}`) advertises. For a to-many:
@@ -152,12 +164,19 @@ declare class DescriptorBuilder {
152
164
  /** Resolve a relationship component's `data` into a {@link RelationDescriptor}. */
153
165
  private relationFromComponent;
154
166
  /**
155
- * Extract the pivot field-shape from a pivot `allOf` — the entry carrying
156
- * `properties.meta.properties.pivot.properties` (a `belongsToMany` member's `meta.pivot`), as a
157
- * field-name → wire-format-hint map (D33). Returns `undefined` when the allOf carries no pivot
158
- * property block.
167
+ * The request pivot of a pivot relation: read off the component a write body (or a
168
+ * relationship mutation) refs, falling back to the rendered relationship's pivot minus its
169
+ * `readOnly` fields when no request shape is published (before generator contract 8).
170
+ */
171
+ private writePivot;
172
+ /**
173
+ * A {@link PivotDescriptor} from a member's `meta.pivot` schema: field hints, the required
174
+ * fields and the nullable ones. Generator contract 8 refs a `<Type><Rel>Pivot` (response) or
175
+ * `<Type><Rel>WritePivot` (request) component, each with its own `required`. An older document
176
+ * inlines one shared schema in the write context, so a read takes every field as rendered and a
177
+ * write drops the `readOnly` ones. `undefined` when the schema has no fields.
159
178
  */
160
- private pivotFieldsFromAllOf;
179
+ private pivotDescriptor;
161
180
  /** Pull the related type from the `$ref` entry of an `allOf` (the pivot shape). */
162
181
  private constFromAllOf;
163
182
  /** Resolve every `$ref` entry in a list to its wire type. */
@@ -241,7 +260,7 @@ declare function buildAtomic(doc: OpenApiDocument): AtomicDescriptor | null;
241
260
  /** Below this the document predates the structures this codegen reads. */
242
261
  declare const MINIMUM_CONTRACT = 1;
243
262
  /** Above this the document carries structures this codegen does not read. */
244
- declare const MAXIMUM_CONTRACT = 7;
263
+ declare const MAXIMUM_CONTRACT = 9;
245
264
  /**
246
265
  * Check the document's `info.x-generator.contract` against the range this codegen reads. A
247
266
  * document older than the range throws; a newer one returns a warning, because its new
@@ -262,7 +281,6 @@ interface VerbCollision {
262
281
  */
263
282
  declare function detectVerbCollisions(descriptor: ApiDescriptor): VerbCollision[];
264
283
  declare class Emitter {
265
- private readonly doc;
266
284
  private readonly descriptor;
267
285
  private readonly atomic;
268
286
  private readonly schemas;
@@ -287,7 +305,8 @@ declare class Emitter {
287
305
  private readonly actionAliasNames;
288
306
  /** Provenance of the source document, stamped into the generated header for drift tracking. */
289
307
  private readonly provenance;
290
- constructor(doc: OpenApiDocument, descriptor: ApiDescriptor, atomic: AtomicDescriptor | null);
308
+ private readonly doc;
309
+ constructor(source: OpenApiDocument, descriptor: ApiDescriptor, atomic: AtomicDescriptor | null);
291
310
  /** Map each declared wire type back to the `Base` of its resource schema. */
292
311
  private indexBases;
293
312
  emit(): string;
@@ -546,4 +565,4 @@ declare function generate(config: CodegenConfig): Promise<string>;
546
565
  declare function check(config: CodegenConfig): Promise<CheckResult>;
547
566
  //#endregion
548
567
  export { InfoObject as A, MAXIMUM_CONTRACT as C, buildAtomic as D, DescriptorBuilder as E, PathItemObject as F, RequestBodyObject as I, ResponseObject as L, OpenApiDocument as M, OperationObject as N, buildDescriptor as O, ParameterObject as P, SchemaObject as R, emit as S, checkContract as T, emitErrorCodes as _, generate as a, VerbCollision as b, deriveProvenance as c, emitSchemas as d, SchemaBundle as f, ErrorCatalogue as g, DocumentedErrorCode as h, check as i, MediaTypeObject as j, HttpMethod as k, hashJson as l, readSchemas as m, CheckResult as n, schemasOutputPath as o, readDocument as p, CodegenConfig as r, Provenance as s, ArtifactDrift as t, provenanceLines as u, readErrorCatalogue as v, MINIMUM_CONTRACT as w, detectVerbCollisions as x, Emitter as y, SchemaOrBool as z };
549
- //# sourceMappingURL=index-Cnj374D6.d.mts.map
568
+ //# sourceMappingURL=index-CU-kUx70.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index-CU-kUx70.d.mts","names":[],"sources":["../src/openapi.ts","../src/build-descriptor.ts","../src/contract.ts","../src/emit.ts","../src/error-codes.ts","../src/reader.ts","../src/emit-schemas.ts","../src/provenance.ts","../src/index.ts"],"mappings":"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"}
package/dist/index.d.mts CHANGED
@@ -1,2 +1,2 @@
1
- import { A as InfoObject, C as MAXIMUM_CONTRACT, D as buildAtomic, E as DescriptorBuilder, F as PathItemObject, I as RequestBodyObject, L as ResponseObject, M as OpenApiDocument, N as OperationObject, O as buildDescriptor, P as ParameterObject, R as SchemaObject, S as emit, T as checkContract, _ as emitErrorCodes, a as generate, b as VerbCollision, c as deriveProvenance, d as emitSchemas, f as SchemaBundle, g as ErrorCatalogue, h as DocumentedErrorCode, i as check, j as MediaTypeObject, k as HttpMethod, l as hashJson, m as readSchemas, n as CheckResult, o as schemasOutputPath, p as readDocument, r as CodegenConfig, s as Provenance, t as ArtifactDrift, u as provenanceLines, v as readErrorCatalogue, w as MINIMUM_CONTRACT, x as detectVerbCollisions, y as Emitter, z as SchemaOrBool } from "./index-Cnj374D6.mjs";
1
+ import { A as InfoObject, C as MAXIMUM_CONTRACT, D as buildAtomic, E as DescriptorBuilder, F as PathItemObject, I as RequestBodyObject, L as ResponseObject, M as OpenApiDocument, N as OperationObject, O as buildDescriptor, P as ParameterObject, R as SchemaObject, S as emit, T as checkContract, _ as emitErrorCodes, a as generate, b as VerbCollision, c as deriveProvenance, d as emitSchemas, f as SchemaBundle, g as ErrorCatalogue, h as DocumentedErrorCode, i as check, j as MediaTypeObject, k as HttpMethod, l as hashJson, m as readSchemas, n as CheckResult, o as schemasOutputPath, p as readDocument, r as CodegenConfig, s as Provenance, t as ArtifactDrift, u as provenanceLines, v as readErrorCatalogue, w as MINIMUM_CONTRACT, x as detectVerbCollisions, y as Emitter, z as SchemaOrBool } from "./index-CU-kUx70.mjs";
2
2
  export { ArtifactDrift, CheckResult, CodegenConfig, DescriptorBuilder, type DocumentedErrorCode, Emitter, type ErrorCatalogue, type HttpMethod, type InfoObject, MAXIMUM_CONTRACT, MINIMUM_CONTRACT, type MediaTypeObject, type OpenApiDocument, type OperationObject, type ParameterObject, type PathItemObject, type Provenance, type RequestBodyObject, type ResponseObject, type SchemaBundle, type SchemaObject, type SchemaOrBool, type VerbCollision, buildAtomic, buildDescriptor, check, checkContract, deriveProvenance, detectVerbCollisions, emit, emitErrorCodes, emitSchemas, generate, hashJson, provenanceLines, readDocument, readErrorCatalogue, readSchemas, schemasOutputPath };
package/dist/index.mjs CHANGED
@@ -1,2 +1,2 @@
1
- import { _ as checkContract, a as readSchemas, b as buildDescriptor, c as detectVerbCollisions, d as hashJson, f as provenanceLines, g as MINIMUM_CONTRACT, h as MAXIMUM_CONTRACT, i as readDocument, l as emit, m as readErrorCatalogue, n as generate, o as emitSchemas, p as emitErrorCodes, r as schemasOutputPath, s as Emitter, t as check, u as deriveProvenance, v as DescriptorBuilder, y as buildAtomic } from "./src-BE7OMLWG.mjs";
1
+ import { _ as checkContract, a as readSchemas, b as buildDescriptor, c as detectVerbCollisions, d as hashJson, f as provenanceLines, g as MINIMUM_CONTRACT, h as MAXIMUM_CONTRACT, i as readDocument, l as emit, m as readErrorCatalogue, n as generate, o as emitSchemas, p as emitErrorCodes, r as schemasOutputPath, s as Emitter, t as check, u as deriveProvenance, v as DescriptorBuilder, y as buildAtomic } from "./src-C2INS1Wd.mjs";
2
2
  export { DescriptorBuilder, Emitter, MAXIMUM_CONTRACT, MINIMUM_CONTRACT, buildAtomic, buildDescriptor, check, checkContract, deriveProvenance, detectVerbCollisions, emit, emitErrorCodes, emitSchemas, generate, hashJson, provenanceLines, readDocument, readErrorCatalogue, readSchemas, schemasOutputPath };
@@ -3,6 +3,64 @@ import { dirname } from "node:path";
3
3
  import { ATOMIC_EXT, CANONICAL_PAGE_KEYS, HANDLE_RESERVED } from "@haddowg/json-api-client";
4
4
  import { createHash } from "node:crypto";
5
5
  import { parse } from "yaml";
6
+ //#region src/unwrap-refs.ts
7
+ /**
8
+ * Keywords that annotate a schema without constraining it. A single-entry `allOf` wrapper is
9
+ * read as its `$ref` only when the wrapper carries nothing else, plus any `x-` extension.
10
+ */
11
+ const ANNOTATIONS = /* @__PURE__ */ new Set([
12
+ "title",
13
+ "description",
14
+ "example",
15
+ "examples",
16
+ "default",
17
+ "deprecated",
18
+ "readOnly",
19
+ "writeOnly",
20
+ "$comment"
21
+ ]);
22
+ const unwrapped = /* @__PURE__ */ new WeakMap();
23
+ /**
24
+ * The document with every `{ allOf: [{ $ref }], ...annotations }` node rewritten as
25
+ * `{ $ref, ...annotations }`. From generator contract 9 the projector wraps a `$ref` that
26
+ * carries an `example`, `examples` or `default` this way, because OAS 3.0 tooling ignores a
27
+ * `$ref`'s siblings. Reading the wrapper as its `$ref` keeps every lookup that follows a `$ref`
28
+ * working, and keeps the annotations for the TSDoc. The input is not modified.
29
+ */
30
+ function unwrapRefs(doc) {
31
+ let out = unwrapped.get(doc);
32
+ if (out === void 0) {
33
+ out = unwrapNode(doc);
34
+ unwrapped.set(doc, out);
35
+ unwrapped.set(out, out);
36
+ }
37
+ return out;
38
+ }
39
+ function unwrapNode(node) {
40
+ if (Array.isArray(node)) return node.map(unwrapNode);
41
+ if (typeof node !== "object" || node === null) return node;
42
+ const out = {};
43
+ for (const [key, value] of Object.entries(node)) out[key] = unwrapNode(value);
44
+ const ref = wrappedRef(out);
45
+ if (ref === void 0) return out;
46
+ const { allOf: _allOf, ...annotations } = out;
47
+ return {
48
+ $ref: ref,
49
+ ...annotations
50
+ };
51
+ }
52
+ /** The `$ref` a node wraps in a single-entry `allOf`, when that is all the node constrains. */
53
+ function wrappedRef(node) {
54
+ const allOf = node["allOf"];
55
+ if (!Array.isArray(allOf) || allOf.length !== 1 || "$ref" in node) return;
56
+ const entry = allOf[0];
57
+ if (typeof entry !== "object" || entry === null) return;
58
+ const keys = Object.keys(entry);
59
+ const ref = entry["$ref"];
60
+ if (keys.length !== 1 || typeof ref !== "string") return;
61
+ return Object.keys(node).every((key) => key === "allOf" || ANNOTATIONS.has(key) || key.startsWith("x-")) ? ref : void 0;
62
+ }
63
+ //#endregion
6
64
  //#region src/build-descriptor.ts
7
65
  const REF_PREFIX$2 = "#/components/schemas/";
8
66
  const JSON_API_MEDIA_TYPE$1 = "application/vnd.api+json";
@@ -19,19 +77,26 @@ function refName$1(ref) {
19
77
  function isSchema$2(value) {
20
78
  return typeof value === "object" && value !== null;
21
79
  }
80
+ /** The operationId prefixes of a relation's related read and its relationship endpoint. */
81
+ const RELATED_OPERATION = "fetchRelated";
82
+ const RELATIONSHIP_OPERATIONS = [
83
+ "fetchRelationship",
84
+ "updateRelationship",
85
+ "addRelationship",
86
+ "removeRelationship"
87
+ ];
22
88
  /** The set of `type` keys a JSON type node declares, with `"null"` removed. */
23
89
  function typeKeys(type) {
24
90
  if (type === void 0) return [];
25
91
  return (Array.isArray(type) ? type : [type]).filter((t) => t !== "null");
26
92
  }
27
93
  var DescriptorBuilder = class {
28
- doc;
29
94
  schemas;
30
95
  paths;
31
96
  /** Lazily-built wire-type -> collection-level paginator kind (for per-relation divergence, D6a). */
32
97
  typePaginators;
33
- constructor(doc) {
34
- this.doc = doc;
98
+ constructor(source) {
99
+ const doc = unwrapRefs(source);
35
100
  this.schemas = doc.components?.schemas ?? {};
36
101
  this.paths = doc.paths ?? {};
37
102
  }
@@ -60,7 +125,7 @@ var DescriptorBuilder = class {
60
125
  const actions = collection ? this.actions(collection) : {};
61
126
  const descriptor = {
62
127
  attributes: this.attributes(base),
63
- relations: this.relations(schema, collection),
128
+ relations: this.relations(wireType, base, schema, collection),
64
129
  paths: this.operationPaths(base),
65
130
  paginator: this.paginator(base),
66
131
  clientId: this.clientId(base)
@@ -75,27 +140,11 @@ var DescriptorBuilder = class {
75
140
  if (filterable !== void 0) descriptor.filterable = filterable;
76
141
  if (Object.keys(actions).length > 0) descriptor.actions = actions;
77
142
  if (Object.keys(schema.properties ?? {}).every((prop) => prop === "type") && Object.keys(descriptor.paths).length === 0) continue;
78
- this.warnWriteOnlyRelations(wireType, base, descriptor.relations);
79
143
  out[wireType] = descriptor;
80
144
  }
81
145
  return sortRecord(out);
82
146
  }
83
147
  /**
84
- * Warn about relations a create or update body accepts but the resource never renders. A
85
- * write-only relation (generator contract 5) is left out of the response schema, and the
86
- * descriptor has no way to mark a relation as writable but unreadable, so it is missing from
87
- * the client entirely rather than typed onto reads that never carry it.
88
- */
89
- warnWriteOnlyRelations(wireType, base, readable) {
90
- const writeOnly = /* @__PURE__ */ new Set();
91
- for (const request of [`${base}CreateRequest`, `${base}UpdateRequest`]) {
92
- const data = this.schemas[request]?.properties?.["data"];
93
- const relationships = isSchema$2(data) ? data.properties?.["relationships"] : void 0;
94
- for (const name of Object.keys(isSchema$2(relationships) ? relationships.properties ?? {} : {})) if (!(name in readable)) writeOnly.add(name);
95
- }
96
- for (const name of writeOnly) console.warn(`json-api codegen: relationship "${name}" on "${wireType}" is accepted on writes but never rendered (a write-only relation); the client has no slot for a relation it can't read, so it is left out.`);
97
- }
98
- /**
99
148
  * The server-level Atomic Operations capability: the path whose `POST` requestBody
100
149
  * declares the atomic ext media type ({@link ATOMIC_EXT}). `null` when no such endpoint
101
150
  * exists. Server-level, so it rides {@link buildAtomic}, not the per-type descriptor.
@@ -131,31 +180,106 @@ var DescriptorBuilder = class {
131
180
  for (const [name, prop] of Object.entries(props)) if (isSchema$2(prop)) out[name] = formatHint(prop);
132
181
  return sortRecord(out);
133
182
  }
134
- relations(resource, collection) {
135
- const relProps = isSchema$2(resource.properties?.["relationships"]) ? resource.properties["relationships"].properties : void 0;
136
- if (relProps === void 0) return {};
183
+ /**
184
+ * The relations of a type: every relation its resource renders, plus each write-only relation
185
+ * the create and update bodies accept. A write-only relation is one whose body property carries
186
+ * `writeOnly: true` (generator contract 8) or, in an older document, one the resource doesn't
187
+ * render. A pivot relation reads its response pivot off the rendered relationship and its
188
+ * request pivot off the write bodies.
189
+ */
190
+ relations(wireType, base, resource, collection) {
191
+ const relProps = isSchema$2(resource.properties?.["relationships"]) ? resource.properties["relationships"].properties ?? {} : {};
192
+ const writes = this.writeRelationships(base);
193
+ const endpoints = this.relationEndpoints(wireType);
137
194
  const out = {};
138
- for (const [name, prop] of Object.entries(relProps)) {
139
- if (!isSchema$2(prop) || typeof prop.$ref !== "string") continue;
140
- const component = this.schemas[refName$1(prop.$ref) ?? ""];
141
- const descriptor = component ? this.relationFromComponent(component) : void 0;
142
- if (descriptor === void 0) {
195
+ const add = (name, component, writeOnly) => {
196
+ const parsed = component ? this.relationFromComponent(component) : void 0;
197
+ if (parsed === void 0) {
143
198
  console.warn(`json-api codegen: relationship "${name}" has an unrecognized linkage shape; dropping it from the descriptor.`);
144
- continue;
199
+ return;
200
+ }
201
+ const descriptor = { ...parsed };
202
+ if (writeOnly) {
203
+ delete descriptor.readPivot;
204
+ descriptor.writeOnly = true;
145
205
  }
146
- out[name] = collection === void 0 ? descriptor : this.withRelationExposure(descriptor, collection, name);
206
+ if (descriptor.pivot === true) {
207
+ const writePivot = this.writePivot(writes.get(name)?.ref ?? endpoints.get(name)?.requestRef, component);
208
+ if (writePivot !== void 0) descriptor.writePivot = writePivot;
209
+ }
210
+ out[name] = collection === void 0 ? descriptor : this.withRelationExposure(descriptor, collection, name, endpoints.get(name));
211
+ };
212
+ for (const [name, prop] of Object.entries(relProps)) {
213
+ if (!isSchema$2(prop) || typeof prop.$ref !== "string" || writes.get(name)?.writeOnly) continue;
214
+ add(name, this.schemas[refName$1(prop.$ref) ?? ""], false);
147
215
  }
216
+ for (const [name, write] of writes) if (write.writeOnly || !(name in relProps)) add(name, this.schemas[refName$1(write.ref) ?? ""], true);
148
217
  return sortRecord(out);
149
218
  }
150
219
  /**
220
+ * The relationships the create and update bodies accept: each one's component `$ref` and
221
+ * whether the body marks it `writeOnly`.
222
+ */
223
+ writeRelationships(base) {
224
+ const out = /* @__PURE__ */ new Map();
225
+ for (const request of [`${base}CreateRequest`, `${base}UpdateRequest`]) {
226
+ const data = this.schemas[request]?.properties?.["data"];
227
+ const relationships = isSchema$2(data) ? data.properties?.["relationships"] : void 0;
228
+ const props = isSchema$2(relationships) ? relationships.properties ?? {} : {};
229
+ for (const [name, prop] of Object.entries(props)) {
230
+ if (!isSchema$2(prop) || typeof prop.$ref !== "string") continue;
231
+ const writeOnly = prop["writeOnly"] === true || out.get(name)?.writeOnly === true;
232
+ out.set(name, {
233
+ ref: out.get(name)?.ref ?? prop.$ref,
234
+ writeOnly
235
+ });
236
+ }
237
+ }
238
+ return out;
239
+ }
240
+ /**
241
+ * Each relation's related and relationship paths, keyed by relation name, from the
242
+ * operationIds the projector stamps (`fetchRelated.<type>.<relation>`, …). A relation's path
243
+ * segment is its URI field name, which may differ from its name, so the paths are never
244
+ * derived from the name when an operationId names them.
245
+ */
246
+ relationEndpoints(wireType) {
247
+ const out = /* @__PURE__ */ new Map();
248
+ const entry = (rel) => {
249
+ let found = out.get(rel);
250
+ if (found === void 0) {
251
+ found = {};
252
+ out.set(rel, found);
253
+ }
254
+ return found;
255
+ };
256
+ for (const [path, item] of Object.entries(this.paths)) for (const [method, op] of Object.entries(item)) {
257
+ const id = op?.operationId;
258
+ if (typeof id !== "string") continue;
259
+ const dot = id.indexOf(".");
260
+ const prefix = id.slice(0, dot);
261
+ const rest = id.slice(dot + 1);
262
+ if (dot < 0 || !rest.startsWith(`${wireType}.`)) continue;
263
+ const rel = rest.slice(wireType.length + 1);
264
+ if (prefix === RELATED_OPERATION) entry(rel).related = path;
265
+ else if (RELATIONSHIP_OPERATIONS.includes(prefix)) {
266
+ const found = entry(rel);
267
+ found.relationship = path;
268
+ const ref = method === "get" ? void 0 : this.requestBodyRef(op);
269
+ if (ref !== void 0) found.requestRef = ref;
270
+ }
271
+ }
272
+ return out;
273
+ }
274
+ /**
151
275
  * The per-relation mutation capability, derived from the HTTP methods the relationship
152
276
  * endpoint (`{collection}/{id}/relationships/{rel}`) advertises. For a to-many:
153
277
  * `add` <- POST, `remove` <- DELETE, `replace` <- PATCH. For a to-one: `set` <- PATCH.
154
278
  * Only `true` verbs are emitted (a missing verb is simply absent). Returns `undefined`
155
279
  * when the relation exposes no relationship endpoint at all.
156
280
  */
157
- relationMutations(collection, rel, cardinality) {
158
- const item = this.paths[`${collection}/{id}/relationships/${rel}`];
281
+ relationMutations(path, cardinality) {
282
+ const item = this.paths[path];
159
283
  if (item === void 0) return;
160
284
  const mutations = {};
161
285
  if (cardinality === "many") {
@@ -175,19 +299,23 @@ var DescriptorBuilder = class {
175
299
  * relationship endpoint exists, else `{}`) so the client gates a suppressed relation OFF rather
176
300
  * than failing open (offering `.get()`/`.related()`/mutations that 404).
177
301
  */
178
- withRelationExposure(descriptor, collection, name) {
302
+ withRelationExposure(descriptor, collection, name, endpoints) {
179
303
  const out = { ...descriptor };
180
- const relatedGet = this.operation(`${collection}/{id}/${name}`, "get");
304
+ const relatedPath = endpoints?.related ?? `${collection}/{id}/${name}`;
305
+ const relationshipPath = endpoints?.relationship ?? `${collection}/{id}/relationships/${name}`;
306
+ const segment = (endpoints?.related ?? endpoints?.relationship)?.split("/").pop();
307
+ if (segment !== void 0 && segment !== name) out.uriField = segment;
308
+ const relatedGet = this.operation(relatedPath, "get");
181
309
  if (relatedGet === void 0) out.related = false;
182
- const mutations = this.relationMutations(collection, name, descriptor.cardinality);
310
+ const mutations = this.relationMutations(relationshipPath, descriptor.cardinality);
183
311
  if (mutations === void 0) {
184
312
  out.relationship = false;
185
313
  out.mutations = {};
186
314
  } else out.mutations = mutations;
187
- const countable = withCountParam(relatedGet) ?? withCountParam(this.operation(`${collection}/{id}/relationships/${name}`, "get"));
315
+ const countable = withCountParam(relatedGet) ?? withCountParam(this.operation(relationshipPath, "get"));
188
316
  if (countable !== void 0) out.countable = countable;
189
317
  if (descriptor.cardinality === "many") {
190
- const relatedPaginator = this.paginatorOf(relatedGet) === "none" ? this.paginatorOf(this.operation(`${collection}/{id}/relationships/${name}`, "get")) : this.paginatorOf(relatedGet);
318
+ const relatedPaginator = this.paginatorOf(relatedGet) === "none" ? this.paginatorOf(this.operation(relationshipPath, "get")) : this.paginatorOf(relatedGet);
191
319
  const relatedType = descriptor.types[0];
192
320
  const typePaginator = relatedType !== void 0 ? this.typePaginator(relatedType) : "none";
193
321
  if (relatedPaginator !== "none" && JSON.stringify(relatedPaginator) !== JSON.stringify(typePaginator)) out.paginator = relatedPaginator;
@@ -311,8 +439,8 @@ var DescriptorBuilder = class {
311
439
  types: [type],
312
440
  pivot: true
313
441
  };
314
- const pivotFields = this.pivotFieldsFromAllOf(items.allOf);
315
- if (pivotFields !== void 0) relation.pivotFields = pivotFields;
442
+ const readPivot = this.pivotDescriptor(pivotNodeFromAllOf(items.allOf), "read");
443
+ if (readPivot !== void 0) relation.readPivot = readPivot;
316
444
  return relation;
317
445
  }
318
446
  if (Array.isArray(items.anyOf)) return {
@@ -325,7 +453,7 @@ var DescriptorBuilder = class {
325
453
  if (Array.isArray(data.anyOf)) {
326
454
  const types = [];
327
455
  let pivot = false;
328
- let pivotFields;
456
+ let readPivot;
329
457
  for (const entry of data.anyOf) if (typeof entry.$ref === "string") {
330
458
  const type = this.resolveConst(entry.$ref);
331
459
  if (type !== void 0) types.push(type);
@@ -334,7 +462,7 @@ var DescriptorBuilder = class {
334
462
  if (type !== void 0) {
335
463
  types.push(type);
336
464
  pivot = true;
337
- pivotFields = this.pivotFieldsFromAllOf(entry.allOf);
465
+ readPivot = this.pivotDescriptor(pivotNodeFromAllOf(entry.allOf), "read");
338
466
  }
339
467
  } else if (Array.isArray(entry.anyOf)) types.push(...this.constsFromRefs(entry.anyOf));
340
468
  if (types.length === 0) return;
@@ -343,26 +471,44 @@ var DescriptorBuilder = class {
343
471
  types,
344
472
  pivot
345
473
  };
346
- if (pivotFields !== void 0) relation.pivotFields = pivotFields;
474
+ if (readPivot !== void 0) relation.readPivot = readPivot;
347
475
  return relation;
348
476
  }
349
477
  }
350
478
  /**
351
- * Extract the pivot field-shape from a pivot `allOf` — the entry carrying
352
- * `properties.meta.properties.pivot.properties` (a `belongsToMany` member's `meta.pivot`), as a
353
- * field-name → wire-format-hint map (D33). Returns `undefined` when the allOf carries no pivot
354
- * property block.
479
+ * The request pivot of a pivot relation: read off the component a write body (or a
480
+ * relationship mutation) refs, falling back to the rendered relationship's pivot minus its
481
+ * `readOnly` fields when no request shape is published (before generator contract 8).
482
+ */
483
+ writePivot(ref, fallback) {
484
+ const component = ref === void 0 ? void 0 : this.schemas[refName$1(ref) ?? ""];
485
+ return this.pivotDescriptor(pivotNodeOf(component ?? fallback) ?? pivotNodeOf(fallback), "write");
486
+ }
487
+ /**
488
+ * A {@link PivotDescriptor} from a member's `meta.pivot` schema: field hints, the required
489
+ * fields and the nullable ones. Generator contract 8 refs a `<Type><Rel>Pivot` (response) or
490
+ * `<Type><Rel>WritePivot` (request) component, each with its own `required`. An older document
491
+ * inlines one shared schema in the write context, so a read takes every field as rendered and a
492
+ * write drops the `readOnly` ones. `undefined` when the schema has no fields.
355
493
  */
356
- pivotFieldsFromAllOf(allOf) {
357
- for (const entry of allOf) {
358
- const meta = entry.properties?.["meta"];
359
- const pivot = isSchema$2(meta) ? meta.properties?.["pivot"] : void 0;
360
- const props = isSchema$2(pivot) ? pivot.properties : void 0;
361
- if (props === void 0) continue;
362
- const out = {};
363
- for (const [name, schema] of Object.entries(props)) if (isSchema$2(schema)) out[name] = formatHint(schema);
364
- return Object.keys(out).length > 0 ? sortRecord(out) : void 0;
494
+ pivotDescriptor(node, side) {
495
+ if (node === void 0) return;
496
+ const shared = typeof node.$ref !== "string";
497
+ const schema = shared ? node : this.schemas[refName$1(node.$ref) ?? ""];
498
+ const fields = {};
499
+ const nullable = [];
500
+ for (const [name, prop] of Object.entries(schema?.properties ?? {})) {
501
+ if (!isSchema$2(prop) || prop[side === "read" ? "writeOnly" : "readOnly"] === true) continue;
502
+ fields[name] = formatHint(prop);
503
+ if (isNullable(prop)) nullable.push(name);
365
504
  }
505
+ const names = Object.keys(fields);
506
+ if (names.length === 0) return;
507
+ const required = shared && side === "read" ? names : names.filter((name) => schema?.required?.includes(name));
508
+ const out = { fields: sortRecord(fields) };
509
+ if (required.length < names.length) out.required = required.sort();
510
+ if (nullable.length > 0) out.nullable = nullable.sort();
511
+ return out;
366
512
  }
367
513
  /** Pull the related type from the `$ref` entry of an `allOf` (the pivot shape). */
368
514
  constFromAllOf(allOf) {
@@ -544,6 +690,30 @@ var DescriptorBuilder = class {
544
690
  return op?.responses?.["200"]?.content?.["application/vnd.api+json"]?.schema?.$ref;
545
691
  }
546
692
  };
693
+ /** The `meta.pivot` schema node of a pivot `allOf` entry list, if any. */
694
+ function pivotNodeFromAllOf(allOf) {
695
+ for (const entry of allOf) {
696
+ const meta = entry.properties?.["meta"];
697
+ const pivot = isSchema$2(meta) ? meta.properties?.["pivot"] : void 0;
698
+ if (isSchema$2(pivot)) return pivot;
699
+ }
700
+ }
701
+ /**
702
+ * The `meta.pivot` schema node of a relationship component or document: its to-many `items`
703
+ * `allOf`, or a to-one `anyOf` entry's `allOf`.
704
+ */
705
+ function pivotNodeOf(component) {
706
+ const data = component.properties?.["data"];
707
+ if (!isSchema$2(data)) return;
708
+ if (Array.isArray(data.items?.allOf)) return pivotNodeFromAllOf(data.items.allOf);
709
+ for (const entry of data.anyOf ?? []) if (Array.isArray(entry.allOf)) return pivotNodeFromAllOf(entry.allOf);
710
+ }
711
+ /** Whether a schema admits `null`: a `null` type, or a `null` branch in `anyOf`/`oneOf`. */
712
+ function isNullable(schema) {
713
+ const type = schema.type;
714
+ if (type === "null" || Array.isArray(type) && type.includes("null")) return true;
715
+ return [...schema.anyOf ?? [], ...schema.oneOf ?? []].some((branch) => branch.type === "null");
716
+ }
547
717
  /** A schema's wire format hint: explicit `format`, else the JSON type, else a ref-enum is `string`. */
548
718
  function formatHint(schema) {
549
719
  if (typeof schema.format === "string") return schema.format;
@@ -735,7 +905,7 @@ function stampedMenu(schema, stamp, warn) {
735
905
  /** Below this the document predates the structures this codegen reads. */
736
906
  const MINIMUM_CONTRACT = 1;
737
907
  /** Above this the document carries structures this codegen does not read. */
738
- const MAXIMUM_CONTRACT = 7;
908
+ const MAXIMUM_CONTRACT = 9;
739
909
  /**
740
910
  * Check the document's `info.x-generator.contract` against the range this codegen reads. A
741
911
  * document older than the range throws; a newer one returns a warning, because its new
@@ -745,8 +915,8 @@ const MAXIMUM_CONTRACT = 7;
745
915
  function checkContract(doc) {
746
916
  const contract = doc.info?.["x-generator"]?.contract;
747
917
  if (typeof contract !== "number") return;
748
- if (contract < 1) throw new Error(`the document declares contract ${contract}, which predates what this codegen reads (it supports 1 to 7). Use an older codegen release, or regenerate the document from a newer server.`);
749
- if (contract > 7) return `the document declares contract ${contract} and this codegen reads up to 7. It describes capabilities that were not generated; upgrade the codegen to pick them up.`;
918
+ if (contract < 1) throw new Error(`the document declares contract ${contract}, which predates what this codegen reads (it supports 1 to 9). Use an older codegen release, or regenerate the document from a newer server.`);
919
+ if (contract > 9) return `the document declares contract ${contract} and this codegen reads up to 9. It describes capabilities that were not generated; upgrade the codegen to pick them up.`;
750
920
  }
751
921
  //#endregion
752
922
  //#region src/error-codes.ts
@@ -1045,7 +1215,6 @@ function memberDoc(schema) {
1045
1215
  return description ? `${description}\n\n${tags.join("\n")}` : tags.join("\n");
1046
1216
  }
1047
1217
  var Emitter = class {
1048
- doc;
1049
1218
  descriptor;
1050
1219
  atomic;
1051
1220
  schemas;
@@ -1070,13 +1239,14 @@ var Emitter = class {
1070
1239
  actionAliasNames = /* @__PURE__ */ new Map();
1071
1240
  /** Provenance of the source document, stamped into the generated header for drift tracking. */
1072
1241
  provenance;
1073
- constructor(doc, descriptor, atomic) {
1074
- this.doc = doc;
1242
+ doc;
1243
+ constructor(source, descriptor, atomic) {
1075
1244
  this.descriptor = descriptor;
1076
1245
  this.atomic = atomic;
1077
- this.schemas = doc.components?.schemas ?? {};
1246
+ this.doc = unwrapRefs(source);
1247
+ this.schemas = this.doc.components?.schemas ?? {};
1078
1248
  this.baseByType = this.indexBases();
1079
- this.provenance = deriveProvenance(doc);
1249
+ this.provenance = deriveProvenance(source);
1080
1250
  }
1081
1251
  /** Map each declared wire type back to the `Base` of its resource schema. */
1082
1252
  indexBases() {
@@ -1662,4 +1832,4 @@ async function check(config) {
1662
1832
  //#endregion
1663
1833
  export { checkContract as _, readSchemas as a, buildDescriptor as b, detectVerbCollisions as c, hashJson as d, provenanceLines as f, MINIMUM_CONTRACT as g, MAXIMUM_CONTRACT as h, readDocument as i, emit as l, readErrorCatalogue as m, generate as n, emitSchemas as o, emitErrorCodes as p, schemasOutputPath as r, Emitter as s, check as t, deriveProvenance as u, DescriptorBuilder as v, buildAtomic as y };
1664
1834
 
1665
- //# sourceMappingURL=src-BE7OMLWG.mjs.map
1835
+ //# sourceMappingURL=src-C2INS1Wd.mjs.map