@haddowg/json-api-codegen 1.9.0 → 1.10.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-k_oATv7Q.mjs";
1
+ import { r as CodegenConfig } from "./index-DqeWPRAD.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>] [--examples] [--check]";
package/dist/cli.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { i as generateFiles, t as check } from "./src-py1RBWZr.mjs";
2
+ import { i as generateFiles, t as check } from "./src-C3noZURK.mjs";
3
3
  import { realpathSync } from "node:fs";
4
4
  import { fileURLToPath } from "node:url";
5
5
  //#region src/cli.ts
@@ -56,6 +56,12 @@ interface OperationObject {
56
56
  parameters?: readonly ParameterObject[];
57
57
  requestBody?: RequestBodyObject;
58
58
  responses?: Record<string, ResponseObject>;
59
+ /** What a custom action declares about its effects (generator contract 15). */
60
+ 'x-safety'?: {
61
+ readOnly?: unknown;
62
+ destructive?: unknown;
63
+ idempotent?: unknown;
64
+ };
59
65
  }
60
66
  type HttpMethod = 'get' | 'post' | 'patch' | 'put' | 'delete';
61
67
  type PathItemObject = Partial<Record<HttpMethod, OperationObject>>;
@@ -152,9 +158,12 @@ declare class DescriptorBuilder {
152
158
  * `{collection}/{id}/-actions/{name}` paths. The action name is the last segment; scope
153
159
  * is `resource` when the path carries `/{id}/`, else `collection`. `input` is `document`
154
160
  * (a JSON:API requestBody), `none` (no requestBody) or `raw` (a non-JSON:API body);
155
- * `output` is `document` (a 2xx returns a JSON:API document) or `none` (only `204`).
161
+ * `output` is `document` (a 2xx returns a JSON:API document) or `none` (only `204`). An action
162
+ * answering to several methods gives one entry per method (see {@link pickOperations}).
156
163
  */
157
164
  private actions;
165
+ /** The descriptor for one operation of a custom action. */
166
+ private action;
158
167
  /**
159
168
  * The non-JSON:API media types an action answers with under its 2xx responses, each once, in
160
169
  * the document's order, with how the client reads its body.
@@ -295,7 +304,7 @@ declare function buildExamples(doc: OpenApiDocument, descriptor: ApiDescriptor):
295
304
  /** Below this the document predates the structures this codegen reads. */
296
305
  declare const MINIMUM_CONTRACT = 1;
297
306
  /** Above this the document carries structures this codegen does not read. */
298
- declare const MAXIMUM_CONTRACT = 14;
307
+ declare const MAXIMUM_CONTRACT = 15;
299
308
  /**
300
309
  * Check the document's `info.x-generator.contract` against the range this codegen reads. A
301
310
  * document older than the range throws; a newer one returns a warning, because its new
@@ -683,4 +692,4 @@ declare function generateFiles(config: CodegenConfig): Promise<string[]>;
683
692
  declare function check(config: CodegenConfig): Promise<CheckResult>;
684
693
  //#endregion
685
694
  export { operationExamples as A, PathItemObject as B, VerbCollision as C, MINIMUM_CONTRACT as D, MAXIMUM_CONTRACT as E, InfoObject as F, ResponseObject as H, MediaTypeObject as I, OpenApiDocument as L, buildAtomic as M, buildDescriptor as N, checkContract as O, HttpMethod as P, OperationObject as R, Emitter as S, emit as T, SchemaObject as U, RequestBodyObject as V, SchemaOrBool as W, emitExamples as _, examplesOutputPath as a, emitErrorCodes as b, schemasOutputPath as c, hashJson as d, provenanceLines as f, readSchemas as g, readDocument as h, check as i, DescriptorBuilder as j, buildExamples as k, Provenance as l, SchemaBundle as m, CheckResult as n, generate as o, emitSchemas as p, CodegenConfig as r, generateFiles as s, ArtifactDrift as t, deriveProvenance as u, DocumentedErrorCode as v, detectVerbCollisions as w, readErrorCatalogue as x, ErrorCatalogue as y, ParameterObject as z };
686
- //# sourceMappingURL=index-k_oATv7Q.d.mts.map
695
+ //# sourceMappingURL=index-DqeWPRAD.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index-DqeWPRAD.d.mts","names":[],"sources":["../src/openapi.ts","../src/build-descriptor.ts","../src/build-examples.ts","../src/contract.ts","../src/emit.ts","../src/error-codes.ts","../src/emit-examples.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 operationExamples, B as PathItemObject, C as VerbCollision, D as MINIMUM_CONTRACT, E as MAXIMUM_CONTRACT, F as InfoObject, H as ResponseObject, I as MediaTypeObject, L as OpenApiDocument, M as buildAtomic, N as buildDescriptor, O as checkContract, P as HttpMethod, R as OperationObject, S as Emitter, T as emit, U as SchemaObject, V as RequestBodyObject, W as SchemaOrBool, _ as emitExamples, a as examplesOutputPath, b as emitErrorCodes, c as schemasOutputPath, d as hashJson, f as provenanceLines, g as readSchemas, h as readDocument, i as check, j as DescriptorBuilder, k as buildExamples, l as Provenance, m as SchemaBundle, n as CheckResult, o as generate, p as emitSchemas, r as CodegenConfig, s as generateFiles, t as ArtifactDrift, u as deriveProvenance, v as DocumentedErrorCode, w as detectVerbCollisions, x as readErrorCatalogue, y as ErrorCatalogue, z as ParameterObject } from "./index-k_oATv7Q.mjs";
1
+ import { A as operationExamples, B as PathItemObject, C as VerbCollision, D as MINIMUM_CONTRACT, E as MAXIMUM_CONTRACT, F as InfoObject, H as ResponseObject, I as MediaTypeObject, L as OpenApiDocument, M as buildAtomic, N as buildDescriptor, O as checkContract, P as HttpMethod, R as OperationObject, S as Emitter, T as emit, U as SchemaObject, V as RequestBodyObject, W as SchemaOrBool, _ as emitExamples, a as examplesOutputPath, b as emitErrorCodes, c as schemasOutputPath, d as hashJson, f as provenanceLines, g as readSchemas, h as readDocument, i as check, j as DescriptorBuilder, k as buildExamples, l as Provenance, m as SchemaBundle, n as CheckResult, o as generate, p as emitSchemas, r as CodegenConfig, s as generateFiles, t as ArtifactDrift, u as deriveProvenance, v as DocumentedErrorCode, w as detectVerbCollisions, x as readErrorCatalogue, y as ErrorCatalogue, z as ParameterObject } from "./index-DqeWPRAD.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, buildExamples, check, checkContract, deriveProvenance, detectVerbCollisions, emit, emitErrorCodes, emitExamples, emitSchemas, examplesOutputPath, generate, generateFiles, hashJson, operationExamples, provenanceLines, readDocument, readErrorCatalogue, readSchemas, schemasOutputPath };
package/dist/index.mjs CHANGED
@@ -1,2 +1,2 @@
1
- import { C as DescriptorBuilder, S as operationExamples, T as buildDescriptor, _ as readErrorCatalogue, a as schemasOutputPath, b as checkContract, c as emitSchemas, d as detectVerbCollisions, f as emit, g as emitErrorCodes, h as provenanceLines, i as generateFiles, l as emitExamples, m as hashJson, n as examplesOutputPath, o as readDocument, p as deriveProvenance, r as generate, s as readSchemas, t as check, u as Emitter, v as MAXIMUM_CONTRACT, w as buildAtomic, x as buildExamples, y as MINIMUM_CONTRACT } from "./src-py1RBWZr.mjs";
1
+ import { C as DescriptorBuilder, S as operationExamples, T as buildDescriptor, _ as readErrorCatalogue, a as schemasOutputPath, b as checkContract, c as emitSchemas, d as detectVerbCollisions, f as emit, g as emitErrorCodes, h as provenanceLines, i as generateFiles, l as emitExamples, m as hashJson, n as examplesOutputPath, o as readDocument, p as deriveProvenance, r as generate, s as readSchemas, t as check, u as Emitter, v as MAXIMUM_CONTRACT, w as buildAtomic, x as buildExamples, y as MINIMUM_CONTRACT } from "./src-C3noZURK.mjs";
2
2
  export { DescriptorBuilder, Emitter, MAXIMUM_CONTRACT, MINIMUM_CONTRACT, buildAtomic, buildDescriptor, buildExamples, check, checkContract, deriveProvenance, detectVerbCollisions, emit, emitErrorCodes, emitExamples, emitSchemas, examplesOutputPath, generate, generateFiles, hashJson, operationExamples, provenanceLines, readDocument, readErrorCatalogue, readSchemas, schemasOutputPath };
@@ -327,7 +327,8 @@ var DescriptorBuilder = class {
327
327
  * `{collection}/{id}/-actions/{name}` paths. The action name is the last segment; scope
328
328
  * is `resource` when the path carries `/{id}/`, else `collection`. `input` is `document`
329
329
  * (a JSON:API requestBody), `none` (no requestBody) or `raw` (a non-JSON:API body);
330
- * `output` is `document` (a 2xx returns a JSON:API document) or `none` (only `204`).
330
+ * `output` is `document` (a 2xx returns a JSON:API document) or `none` (only `204`). An action
331
+ * answering to several methods gives one entry per method (see {@link pickOperations}).
331
332
  */
332
333
  actions(collection) {
333
334
  const out = {};
@@ -344,47 +345,66 @@ var DescriptorBuilder = class {
344
345
  name = path.slice(collectionPrefix.length);
345
346
  } else continue;
346
347
  if (name.length === 0 || name.includes("/")) continue;
347
- const picked = pickOperation(item);
348
- if (picked === void 0) {
348
+ const picked = pickOperations(item);
349
+ if (picked.length === 0) {
349
350
  console.warn(`json-api codegen: custom action path "${path}" advertises no usable operation; skipping.`);
350
351
  continue;
351
352
  }
352
- const { method, op } = picked;
353
- const input = actionInput(op);
354
- const output = actionOutput(op);
355
- const action = {
356
- scope,
357
- path,
358
- input,
359
- output
360
- };
361
- if (method !== "post") action.method = method.toUpperCase();
362
- if (input === "document") {
363
- const inputType = this.documentPrimary(actionRequestRef(op))?.type;
364
- if (inputType !== void 0) action.inputType = inputType;
365
- }
366
- if (output === "document") {
367
- const primary = this.documentPrimary(this.firstOkJsonApiRef(op));
368
- if (primary !== void 0) {
369
- action.outputType = primary.type;
370
- action.outputCardinality = primary.cardinality;
353
+ for (const [index, { method, op }] of picked.entries()) {
354
+ const key = index === 0 ? name : methodActionName(name, method);
355
+ if (key in out) {
356
+ console.warn(`json-api codegen: the ${method.toUpperCase()} operation of custom action path "${path}" would be named "${key}", which another action already uses; skipping.`);
357
+ continue;
371
358
  }
359
+ out[key] = this.action(scope, path, method, op);
372
360
  }
373
- if (input === "raw") {
374
- const contentType = rawContentType(op);
375
- if (contentType !== void 0) action.contentType = contentType;
376
- }
377
- const rawOutput = this.rawOutput(op);
378
- if (rawOutput.length > 0) action.rawOutput = rawOutput;
379
- const requestContent = Object.entries(op.requestBody?.content ?? {});
380
- if (requestContent.length > 1) action.inputMedia = requestContent.map(([mediaType, media]) => ({
381
- mediaType,
382
- kind: this.inputMediaKind(mediaType, media.schema)
383
- }));
384
- out[name] = action;
385
361
  }
386
362
  return sortRecord(out);
387
363
  }
364
+ /** The descriptor for one operation of a custom action. */
365
+ action(scope, path, method, op) {
366
+ const input = actionInput(op);
367
+ const output = actionOutput(op);
368
+ const action = {
369
+ scope,
370
+ path,
371
+ input,
372
+ output
373
+ };
374
+ if (method !== "post") action.method = method.toUpperCase();
375
+ if (input === "document") {
376
+ const inputType = this.documentPrimary(actionRequestRef(op))?.type;
377
+ if (inputType !== void 0) action.inputType = inputType;
378
+ }
379
+ if (output === "document") {
380
+ const primary = this.documentPrimary(this.firstOkJsonApiRef(op));
381
+ if (primary !== void 0) {
382
+ action.outputType = primary.type;
383
+ action.outputCardinality = primary.cardinality;
384
+ }
385
+ }
386
+ if (input === "raw") {
387
+ const contentType = rawContentType(op);
388
+ if (contentType !== void 0) action.contentType = contentType;
389
+ }
390
+ const rawOutput = this.rawOutput(op);
391
+ if (rawOutput.length > 0) action.rawOutput = rawOutput;
392
+ const requestContent = Object.entries(op.requestBody?.content ?? {});
393
+ if (requestContent.length > 1) action.inputMedia = requestContent.map(([mediaType, media]) => ({
394
+ mediaType,
395
+ kind: this.inputMediaKind(mediaType, media.schema)
396
+ }));
397
+ const safety = op["x-safety"];
398
+ for (const flag of [
399
+ "readOnly",
400
+ "destructive",
401
+ "idempotent"
402
+ ]) {
403
+ const value = safety?.[flag];
404
+ if (typeof value === "boolean") action[flag] = value;
405
+ }
406
+ return action;
407
+ }
388
408
  /**
389
409
  * The non-JSON:API media types an action answers with under its 2xx responses, each once, in
390
410
  * the document's order, with how the client reads its body.
@@ -837,18 +857,39 @@ const ACTION_METHODS = [
837
857
  "delete"
838
858
  ];
839
859
  /**
840
- * The single operation an action is invoked over: POST when advertised (the common default), else
841
- * the first advertised method. `undefined` when the path item carries no operation at all.
860
+ * The operations an action is invoked over, the preferred one first: POST when advertised (the
861
+ * common default), else the first advertised method. The other methods follow only when their
862
+ * `operationId` differs from the preferred one's, as contract 15 makes it for an action answering
863
+ * to several methods. An older document gave every method one id, and its client exposed only the
864
+ * preferred one. Empty when the path item carries no operation at all.
842
865
  */
843
- function pickOperation(item) {
866
+ function pickOperations(item) {
844
867
  const available = ACTION_METHODS.filter((method) => item[method] !== void 0);
845
- const method = available.includes("post") ? "post" : available[0];
846
- if (method === void 0) return;
847
- const op = item[method];
848
- return op === void 0 ? void 0 : {
849
- method,
850
- op
851
- };
868
+ const preferred = available.includes("post") ? "post" : available[0];
869
+ if (preferred === void 0) return [];
870
+ const first = item[preferred];
871
+ const out = [{
872
+ method: preferred,
873
+ op: first
874
+ }];
875
+ for (const method of available) {
876
+ const op = item[method];
877
+ if (method !== preferred && op.operationId !== first.operationId) out.push({
878
+ method,
879
+ op
880
+ });
881
+ }
882
+ return out;
883
+ }
884
+ /**
885
+ * The name a multi-method action's other methods go under: the action's name and the method,
886
+ * joined the way the name joins its words. So `export` gives `exportGet` and `bulk-import` gives
887
+ * `bulk-import-get`. Core refuses action ids that differ only in case or punctuation, so the
888
+ * name can't clash with another action's.
889
+ */
890
+ function methodActionName(name, method) {
891
+ const separator = /[-_.]/.exec(name)?.[0];
892
+ return separator === void 0 ? `${name}${method[0].toUpperCase()}${method.slice(1)}` : `${name}${separator}${method}`;
852
893
  }
853
894
  /** The body shape a custom action accepts: a JSON:API document, none, or a raw payload. */
854
895
  function actionInput(op) {
@@ -1044,7 +1085,7 @@ function buildExamples(doc, descriptor) {
1044
1085
  /** Below this the document predates the structures this codegen reads. */
1045
1086
  const MINIMUM_CONTRACT = 1;
1046
1087
  /** Above this the document carries structures this codegen does not read. */
1047
- const MAXIMUM_CONTRACT = 14;
1088
+ const MAXIMUM_CONTRACT = 15;
1048
1089
  /**
1049
1090
  * Check the document's `info.x-generator.contract` against the range this codegen reads. A
1050
1091
  * document older than the range throws; a newer one returns a warning, because its new
@@ -1054,8 +1095,8 @@ const MAXIMUM_CONTRACT = 14;
1054
1095
  function checkContract(doc) {
1055
1096
  const contract = doc.info?.["x-generator"]?.contract;
1056
1097
  if (typeof contract !== "number") return;
1057
- if (contract < 1) throw new Error(`the document declares contract ${contract}, which predates what this codegen reads (it supports 1 to 14). Use an older codegen release, or regenerate the document from a newer server.`);
1058
- if (contract > 14) return `the document declares contract ${contract} and this codegen reads up to 14. It describes capabilities that were not generated; upgrade the codegen to pick them up.`;
1098
+ if (contract < 1) throw new Error(`the document declares contract ${contract}, which predates what this codegen reads (it supports 1 to 15). Use an older codegen release, or regenerate the document from a newer server.`);
1099
+ if (contract > 15) return `the document declares contract ${contract} and this codegen reads up to 15. It describes capabilities that were not generated; upgrade the codegen to pick them up.`;
1059
1100
  }
1060
1101
  //#endregion
1061
1102
  //#region src/declared-links.ts
@@ -1521,6 +1562,19 @@ function actionDoc(name, op) {
1521
1562
  if (description && !generated.test(description)) parts.push(description);
1522
1563
  return parts.length === 0 ? void 0 : parts.join("\n\n");
1523
1564
  }
1565
+ /**
1566
+ * What an action declares about its effects (generator contract 15), as one sentence, or
1567
+ * `undefined` when it declares nothing.
1568
+ */
1569
+ function safetyDoc(action) {
1570
+ const words = [
1571
+ [action.readOnly, "read-only"],
1572
+ [action.destructive, "destructive"],
1573
+ [action.idempotent, "idempotent"]
1574
+ ].filter(([value]) => value !== void 0).map(([value, word]) => value === true ? word : `not ${word}`);
1575
+ if (words.length === 0) return;
1576
+ return `The server declares this action ${words.length === 1 ? words[0] : `${words.slice(0, -1).join(", ")} and ${words.at(-1)}`}.`;
1577
+ }
1524
1578
  /** Whether a media type carries JSON: JSON:API, `application/json` or a `+json` suffix. */
1525
1579
  function isJsonMedia(mediaType) {
1526
1580
  const essence = mediaType.split(";")[0].trim().toLowerCase();
@@ -1630,7 +1684,11 @@ var Emitter = class {
1630
1684
  const single = action.contentType;
1631
1685
  if (single !== void 0 && (single.startsWith("multipart/") || single === "application/x-www-form-urlencoded")) this.takenNames.add(`${stem}${pascalCase(name)}${pascalCase(single)}Input`);
1632
1686
  const op = actionOperation(this.doc, action);
1633
- const parts = [actionDoc(name, op), exampleDoc(op)].filter((part) => part !== void 0);
1687
+ const parts = [
1688
+ actionDoc(action.path.slice(action.path.lastIndexOf("/") + 1), op),
1689
+ safetyDoc(action),
1690
+ exampleDoc(op)
1691
+ ].filter((part) => part !== void 0);
1634
1692
  const doc = parts.length === 0 ? void 0 : parts.join("\n\n");
1635
1693
  if (doc !== void 0) this.literalDocs.set(`${type}/actions/${name}`, doc);
1636
1694
  }
@@ -2460,4 +2518,4 @@ async function check(config) {
2460
2518
  //#endregion
2461
2519
  export { DescriptorBuilder as C, operationExamples as S, buildDescriptor as T, readErrorCatalogue as _, schemasOutputPath as a, checkContract as b, emitSchemas as c, detectVerbCollisions as d, emit as f, emitErrorCodes as g, provenanceLines as h, generateFiles as i, emitExamples as l, hashJson as m, examplesOutputPath as n, readDocument as o, deriveProvenance as p, generate as r, readSchemas as s, check as t, Emitter as u, MAXIMUM_CONTRACT as v, buildAtomic as w, buildExamples as x, MINIMUM_CONTRACT as y };
2462
2520
 
2463
- //# sourceMappingURL=src-py1RBWZr.mjs.map
2521
+ //# sourceMappingURL=src-C3noZURK.mjs.map