@distilled.cloud/core 0.30.3 → 1.0.0-rc.10

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.
Files changed (216) hide show
  1. package/LICENSE +201 -0
  2. package/lib/api.d.ts +165 -0
  3. package/lib/api.d.ts.map +1 -0
  4. package/lib/api.js +190 -0
  5. package/lib/api.js.map +1 -0
  6. package/lib/category.d.ts +4 -4
  7. package/lib/category.js +4 -4
  8. package/lib/codegen/boolean-string-enums.d.ts +36 -0
  9. package/lib/codegen/boolean-string-enums.d.ts.map +1 -0
  10. package/lib/codegen/boolean-string-enums.js +94 -0
  11. package/lib/codegen/boolean-string-enums.js.map +1 -0
  12. package/lib/codegen/boolean-string-enums.test.d.ts +2 -0
  13. package/lib/codegen/boolean-string-enums.test.d.ts.map +1 -0
  14. package/lib/codegen/boolean-string-enums.test.js +147 -0
  15. package/lib/codegen/boolean-string-enums.test.js.map +1 -0
  16. package/lib/codegen/cli.d.ts +79 -0
  17. package/lib/codegen/cli.d.ts.map +1 -0
  18. package/lib/codegen/cli.js +149 -0
  19. package/lib/codegen/cli.js.map +1 -0
  20. package/lib/codegen/emit.d.ts +125 -0
  21. package/lib/codegen/emit.d.ts.map +1 -0
  22. package/lib/codegen/emit.js +101 -0
  23. package/lib/codegen/emit.js.map +1 -0
  24. package/lib/codegen/format.d.ts +23 -0
  25. package/lib/codegen/format.d.ts.map +1 -0
  26. package/lib/codegen/format.js +28 -0
  27. package/lib/codegen/format.js.map +1 -0
  28. package/lib/codegen/generator.d.ts +334 -0
  29. package/lib/codegen/generator.d.ts.map +1 -0
  30. package/lib/codegen/generator.js +813 -0
  31. package/lib/codegen/generator.js.map +1 -0
  32. package/lib/codegen/graph.d.ts +36 -0
  33. package/lib/codegen/graph.d.ts.map +1 -0
  34. package/lib/codegen/graph.js +136 -0
  35. package/lib/codegen/graph.js.map +1 -0
  36. package/lib/codegen/graphql-client.d.ts +62 -0
  37. package/lib/codegen/graphql-client.d.ts.map +1 -0
  38. package/lib/codegen/graphql-client.js +294 -0
  39. package/lib/codegen/graphql-client.js.map +1 -0
  40. package/lib/codegen/graphql-client.test.d.ts +2 -0
  41. package/lib/codegen/graphql-client.test.d.ts.map +1 -0
  42. package/lib/codegen/graphql-client.test.js +311 -0
  43. package/lib/codegen/graphql-client.test.js.map +1 -0
  44. package/lib/codegen/graphql.d.ts +207 -0
  45. package/lib/codegen/graphql.d.ts.map +1 -0
  46. package/lib/codegen/graphql.js +799 -0
  47. package/lib/codegen/graphql.js.map +1 -0
  48. package/lib/codegen/members.d.ts +25 -0
  49. package/lib/codegen/members.d.ts.map +1 -0
  50. package/lib/codegen/members.js +55 -0
  51. package/lib/codegen/members.js.map +1 -0
  52. package/lib/codegen/naming.d.ts +29 -0
  53. package/lib/codegen/naming.d.ts.map +1 -0
  54. package/lib/codegen/naming.js +74 -0
  55. package/lib/codegen/naming.js.map +1 -0
  56. package/lib/codegen/openapi-cli.d.ts +52 -0
  57. package/lib/codegen/openapi-cli.d.ts.map +1 -0
  58. package/lib/codegen/openapi-cli.js +109 -0
  59. package/lib/codegen/openapi-cli.js.map +1 -0
  60. package/lib/codegen/openapi.d.ts +178 -0
  61. package/lib/codegen/openapi.d.ts.map +1 -0
  62. package/lib/codegen/openapi.js +1377 -0
  63. package/lib/codegen/openapi.js.map +1 -0
  64. package/lib/codegen/operations.d.ts +24 -0
  65. package/lib/codegen/operations.d.ts.map +1 -0
  66. package/lib/codegen/operations.js +56 -0
  67. package/lib/codegen/operations.js.map +1 -0
  68. package/lib/codegen/pagination.d.ts +39 -0
  69. package/lib/codegen/pagination.d.ts.map +1 -0
  70. package/lib/codegen/pagination.js +33 -0
  71. package/lib/codegen/pagination.js.map +1 -0
  72. package/lib/codegen/patches.d.ts +65 -0
  73. package/lib/codegen/patches.d.ts.map +1 -0
  74. package/lib/codegen/patches.js +236 -0
  75. package/lib/codegen/patches.js.map +1 -0
  76. package/lib/codegen/patches.test.d.ts +2 -0
  77. package/lib/codegen/patches.test.d.ts.map +1 -0
  78. package/lib/codegen/patches.test.js +105 -0
  79. package/lib/codegen/patches.test.js.map +1 -0
  80. package/lib/codegen/prelude.d.ts +15 -0
  81. package/lib/codegen/prelude.d.ts.map +1 -0
  82. package/lib/codegen/prelude.js +60 -0
  83. package/lib/codegen/prelude.js.map +1 -0
  84. package/lib/codegen/proto.d.ts +121 -0
  85. package/lib/codegen/proto.d.ts.map +1 -0
  86. package/lib/codegen/proto.js +962 -0
  87. package/lib/codegen/proto.js.map +1 -0
  88. package/lib/codegen/rewrite-operation-ids.d.ts +131 -0
  89. package/lib/codegen/rewrite-operation-ids.d.ts.map +1 -0
  90. package/lib/codegen/rewrite-operation-ids.js +1079 -0
  91. package/lib/codegen/rewrite-operation-ids.js.map +1 -0
  92. package/lib/codegen/rewrite-operation-ids.test.d.ts +2 -0
  93. package/lib/codegen/rewrite-operation-ids.test.d.ts.map +1 -0
  94. package/lib/codegen/rewrite-operation-ids.test.js +533 -0
  95. package/lib/codegen/rewrite-operation-ids.test.js.map +1 -0
  96. package/lib/codegen/spec-path.d.ts +16 -0
  97. package/lib/codegen/spec-path.d.ts.map +1 -0
  98. package/lib/codegen/spec-path.js +101 -0
  99. package/lib/codegen/spec-path.js.map +1 -0
  100. package/lib/error-category.d.ts +28 -0
  101. package/lib/error-category.d.ts.map +1 -0
  102. package/lib/error-category.js +46 -0
  103. package/lib/error-category.js.map +1 -0
  104. package/lib/errors.d.ts +1 -0
  105. package/lib/errors.d.ts.map +1 -1
  106. package/lib/errors.js +18 -13
  107. package/lib/errors.js.map +1 -1
  108. package/lib/graphql.d.ts +284 -0
  109. package/lib/graphql.d.ts.map +1 -0
  110. package/lib/graphql.fixture.d.ts +249 -0
  111. package/lib/graphql.fixture.d.ts.map +1 -0
  112. package/lib/graphql.fixture.js +240 -0
  113. package/lib/graphql.fixture.js.map +1 -0
  114. package/lib/graphql.js +718 -0
  115. package/lib/graphql.js.map +1 -0
  116. package/lib/graphql.test.d.ts +2 -0
  117. package/lib/graphql.test.d.ts.map +1 -0
  118. package/lib/graphql.test.js +780 -0
  119. package/lib/graphql.test.js.map +1 -0
  120. package/lib/graphql.types.d.ts +2 -0
  121. package/lib/graphql.types.d.ts.map +1 -0
  122. package/lib/graphql.types.js +45 -0
  123. package/lib/graphql.types.js.map +1 -0
  124. package/lib/json-patch.d.ts +30 -30
  125. package/lib/json-patch.d.ts.map +1 -1
  126. package/lib/json-patch.js +73 -107
  127. package/lib/json-patch.js.map +1 -1
  128. package/lib/pagination.d.ts +77 -51
  129. package/lib/pagination.d.ts.map +1 -1
  130. package/lib/pagination.js +162 -94
  131. package/lib/pagination.js.map +1 -1
  132. package/lib/protocol-http.d.ts +74 -0
  133. package/lib/protocol-http.d.ts.map +1 -0
  134. package/lib/protocol-http.js +590 -0
  135. package/lib/protocol-http.js.map +1 -0
  136. package/lib/protocol-http.test.d.ts +2 -0
  137. package/lib/protocol-http.test.d.ts.map +1 -0
  138. package/lib/protocol-http.test.js +88 -0
  139. package/lib/protocol-http.test.js.map +1 -0
  140. package/lib/protocol-rest.d.ts +134 -0
  141. package/lib/protocol-rest.d.ts.map +1 -0
  142. package/lib/protocol-rest.js +256 -0
  143. package/lib/protocol-rest.js.map +1 -0
  144. package/lib/retry.d.ts +8 -2
  145. package/lib/retry.d.ts.map +1 -1
  146. package/lib/retry.js +22 -16
  147. package/lib/retry.js.map +1 -1
  148. package/lib/schema.d.ts +8 -9
  149. package/lib/schema.d.ts.map +1 -1
  150. package/lib/schema.js +8 -9
  151. package/lib/schema.js.map +1 -1
  152. package/lib/trait.d.ts +174 -0
  153. package/lib/trait.d.ts.map +1 -0
  154. package/lib/trait.js +123 -0
  155. package/lib/trait.js.map +1 -0
  156. package/package.json +24 -78
  157. package/src/api.ts +460 -0
  158. package/src/category.ts +4 -4
  159. package/src/codegen/boolean-string-enums.test.ts +168 -0
  160. package/src/codegen/boolean-string-enums.ts +106 -0
  161. package/src/codegen/cli.ts +285 -0
  162. package/src/codegen/emit.ts +203 -0
  163. package/src/codegen/format.ts +47 -0
  164. package/src/codegen/generator.ts +1283 -0
  165. package/src/codegen/graph.ts +151 -0
  166. package/src/codegen/graphql-client.test.ts +386 -0
  167. package/src/codegen/graphql-client.ts +419 -0
  168. package/src/codegen/graphql.ts +1217 -0
  169. package/src/codegen/members.ts +71 -0
  170. package/src/codegen/naming.ts +86 -0
  171. package/src/codegen/openapi-cli.ts +182 -0
  172. package/src/codegen/openapi.ts +1689 -0
  173. package/src/codegen/operations.ts +76 -0
  174. package/src/codegen/pagination.ts +71 -0
  175. package/src/codegen/patches.test.ts +130 -0
  176. package/src/codegen/patches.ts +291 -0
  177. package/src/codegen/prelude.ts +70 -0
  178. package/src/codegen/proto.ts +1128 -0
  179. package/src/codegen/rewrite-operation-ids.test.ts +563 -0
  180. package/src/codegen/rewrite-operation-ids.ts +1206 -0
  181. package/src/codegen/spec-path.ts +115 -0
  182. package/src/error-category.ts +84 -0
  183. package/src/errors.ts +22 -25
  184. package/src/graphql.fixture.ts +371 -0
  185. package/src/graphql.test.ts +974 -0
  186. package/src/graphql.ts +1321 -0
  187. package/src/graphql.types.ts +185 -0
  188. package/src/json-patch.ts +95 -122
  189. package/src/pagination.ts +217 -146
  190. package/src/protocol-http.test.ts +107 -0
  191. package/src/protocol-http.ts +735 -0
  192. package/src/protocol-rest.ts +391 -0
  193. package/src/retry.ts +21 -22
  194. package/src/schema.ts +9 -10
  195. package/src/trait.ts +274 -0
  196. package/README.md +0 -30
  197. package/lib/client.d.ts +0 -167
  198. package/lib/client.d.ts.map +0 -1
  199. package/lib/client.js +0 -659
  200. package/lib/client.js.map +0 -1
  201. package/lib/schemas.d.ts +0 -60
  202. package/lib/schemas.d.ts.map +0 -1
  203. package/lib/schemas.js +0 -79
  204. package/lib/schemas.js.map +0 -1
  205. package/lib/sensitive.d.ts +0 -71
  206. package/lib/sensitive.d.ts.map +0 -1
  207. package/lib/sensitive.js +0 -96
  208. package/lib/sensitive.js.map +0 -1
  209. package/lib/traits.d.ts +0 -421
  210. package/lib/traits.d.ts.map +0 -1
  211. package/lib/traits.js +0 -737
  212. package/lib/traits.js.map +0 -1
  213. package/src/client.ts +0 -1177
  214. package/src/schemas.ts +0 -128
  215. package/src/sensitive.ts +0 -119
  216. package/src/traits.ts +0 -996
@@ -0,0 +1,185 @@
1
+ /** Compile-only contract checks. The coordinator runs these with noCheck=false. */
2
+ import type * as G from "./graphql.ts";
3
+ import type * as Stream from "effect/Stream";
4
+ import type {
5
+ FixtureSchema,
6
+ ProjectNotFound,
7
+ ServicesUnavailable,
8
+ Unauthorized,
9
+ PageInfoUnavailable,
10
+ } from "./graphql.fixture.ts";
11
+
12
+ type Assert<T extends true> = T;
13
+ type Assignable<A, B> = [A] extends [B] ? true : false;
14
+ type Equal<A, B> = Assignable<A, B> extends true ? Assignable<B, A> : false;
15
+
16
+ type IdentitySelection = {
17
+ project: { where: { id: "p1" }; select: { id: true } };
18
+ };
19
+ type DetailedSelection = {
20
+ project: {
21
+ where: { id: "p1" };
22
+ select: {
23
+ id: true;
24
+ services: { select: { edges: { node: { id: true } } } };
25
+ };
26
+ };
27
+ };
28
+ type AliasSelection = {
29
+ __alias: {
30
+ production: { project: { where: { id: "p1" }; select: { id: true } } };
31
+ };
32
+ };
33
+ type SearchSelection = {
34
+ search: {
35
+ where: { text: "prod" };
36
+ select: {
37
+ __typename: true;
38
+ __on: { Project: { name: true }; Service: { status: true } };
39
+ };
40
+ };
41
+ };
42
+
43
+ type Identity = G.Result<FixtureSchema, "Query!", IdentitySelection>;
44
+ type _Identity = Assert<Equal<Identity, { project: { id: string } | null }>>;
45
+ type _UnselectedFieldsAbsent = Assert<
46
+ Equal<keyof NonNullable<Identity["project"]>, "id">
47
+ >;
48
+
49
+ type Alias = G.Result<FixtureSchema, "Query!", AliasSelection>;
50
+ type _Alias = Assert<Equal<Alias, { production: { id: string } | null }>>;
51
+
52
+ type Search = G.Result<FixtureSchema, "Query!", SearchSelection>;
53
+ type _Union = Assert<
54
+ Equal<
55
+ Search["search"][number],
56
+ | { __typename: "Project"; name: string }
57
+ | { __typename: "Service"; status: "ACTIVE" | "ARCHIVED" }
58
+ >
59
+ >;
60
+
61
+ type IdentityErrors = G.Errors<FixtureSchema, "Query", IdentitySelection>;
62
+ type DetailedErrors = G.Errors<FixtureSchema, "Query", DetailedSelection>;
63
+ type _RootErrorIncluded = Assert<Assignable<ProjectNotFound, IdentityErrors>>;
64
+ type _GlobalErrorIncluded = Assert<Assignable<Unauthorized, IdentityErrors>>;
65
+ type _NestedErrorIncluded = Assert<
66
+ Assignable<ServicesUnavailable, DetailedErrors>
67
+ >;
68
+ type _UnselectedErrorExcluded = Assert<
69
+ Equal<Extract<IdentityErrors, { _tag: "ServicesUnavailable" }>, never>
70
+ >;
71
+
72
+ type ConditionalRoot = G.Result<
73
+ FixtureSchema,
74
+ "Query!",
75
+ {
76
+ ping: { $include: boolean; select: true };
77
+ }
78
+ >;
79
+ type _ConditionalRoot = Assert<Equal<ConditionalRoot, { ping?: boolean }>>;
80
+ type ConditionalNested = G.Result<
81
+ FixtureSchema,
82
+ "Query!",
83
+ {
84
+ project: {
85
+ where: { id: string };
86
+ select: {
87
+ id: true;
88
+ name: { $skip: boolean; select: true };
89
+ };
90
+ };
91
+ }
92
+ >;
93
+ type _ConditionalNested = Assert<
94
+ Equal<
95
+ ConditionalNested,
96
+ {
97
+ project: { id: string; name?: string } | null;
98
+ }
99
+ >
100
+ >;
101
+ type ConditionalAlias = G.Result<
102
+ FixtureSchema,
103
+ "Query!",
104
+ {
105
+ __alias: { healthy: { ping: { $include: boolean; select: true } } };
106
+ }
107
+ >;
108
+ type _ConditionalAlias = Assert<Equal<ConditionalAlias, { healthy?: boolean }>>;
109
+
110
+ type MergedInterface = G.Result<
111
+ FixtureSchema,
112
+ "Node!",
113
+ {
114
+ __typename: true;
115
+ parent: { id: true };
116
+ __on: { Project: { parent: { name: true } } };
117
+ }
118
+ >;
119
+ type _MergedInterface = Assert<
120
+ Equal<
121
+ Extract<MergedInterface, { __typename: "Project" }>["parent"],
122
+ { id: string; name: string } | null
123
+ >
124
+ >;
125
+
126
+ // These assignments exercise user-facing argument and nested selection validation.
127
+ const validSelection: G.Selection<FixtureSchema, "Query"> = {
128
+ project: {
129
+ where: { id: "p1" },
130
+ select: {
131
+ services: {
132
+ where: { first: 10 },
133
+ select: { edges: { node: { id: true } } },
134
+ },
135
+ },
136
+ },
137
+ };
138
+ const missingArgument: G.Selection<FixtureSchema, "Query"> = {
139
+ // @ts-expect-error Required GraphQL field arguments cannot be omitted.
140
+ project: { select: { id: true } },
141
+ };
142
+ const wrongArgument: G.Selection<FixtureSchema, "Query"> = {
143
+ // @ts-expect-error GraphQL argument types follow the schema.
144
+ project: { where: { id: 42 }, select: { id: true } },
145
+ };
146
+ const unknownField: G.Selection<FixtureSchema, "Query"> = {
147
+ // @ts-expect-error Unknown nested fields cannot be selected.
148
+ project: { where: { id: "p1" }, select: { invented: true } },
149
+ };
150
+ // @ts-expect-error Fields returning objects require a projection.
151
+ const noProjection: G.Selection<FixtureSchema, "Query"> = { project: true };
152
+
153
+ void [
154
+ validSelection,
155
+ missingArgument,
156
+ wrongArgument,
157
+ unknownField,
158
+ noProjection,
159
+ ];
160
+
161
+ // Named operations retain projection and pagination constraints during inference.
162
+ declare const inferredClient: ReturnType<
163
+ typeof G.makeClient<FixtureSchema, never>
164
+ >;
165
+
166
+ // @ts-expect-error An object-returning operation always requires a projection.
167
+ inferredClient.operation("query", "project")({ id: "p1" });
168
+ // @ts-expect-error Non-connection object queries do not expose pages.
169
+ inferredClient.operation("query", "project").pages;
170
+ // @ts-expect-error Scalar queries do not expose connection pagination.
171
+ inferredClient.operation("query", "ping").items;
172
+ // @ts-expect-error Mutations cannot be paginated.
173
+ inferredClient.operation("mutation", "editProject").pages;
174
+
175
+ const selectivePages = inferredClient
176
+ .operation("query", "services")
177
+ .pages({}, { edges: { node: { id: true } } });
178
+ type PageErrors = Stream.Error<typeof selectivePages>;
179
+ type PageIssues =
180
+ Extract<PageErrors, G.GraphQLFailure> extends G.GraphQLFailure<infer E>
181
+ ? E
182
+ : never;
183
+ type _HiddenPaginationErrorIncluded = Assert<
184
+ Assignable<PageInfoUnavailable, PageIssues>
185
+ >;
package/src/json-patch.ts CHANGED
@@ -1,20 +1,15 @@
1
1
  /**
2
2
  * JSON Patch (RFC 6902) Implementation
3
3
  *
4
- * Provides a unified spec patching system for all SDKs.
5
- * Patches are applied to OpenAPI/Discovery/Smithy specs before code generation
6
- * to add error types, fix nullable fields, mark sensitive data, etc.
4
+ * Provides a unified spec patching system for all SDKs. Patches apply in
5
+ * `convert` so `.generated-specs` is the patched Smithy model. generate does
6
+ * not patch. Prefer `operationNaming: "verbNoun"` for operationId renames —
7
+ * JSON pointers at `/paths/~1foo/operationId` go stale when upstream prefixes
8
+ * paths.
7
9
  *
8
- * @example
9
- * ```ts
10
- * import { applyAllPatches } from "@distilled.cloud/core/json-patch";
11
- *
12
- * const spec = JSON.parse(fs.readFileSync("openapi.json", "utf-8"));
13
- * const { applied, errors } = applyAllPatches(spec, "./patches");
14
- * ```
10
+ * Pure functions only — callers load patch files themselves (the generators
11
+ * use Effect's FileSystem) and hand the parsed operations to `applyPatch`.
15
12
  */
16
- import * as fs from "fs";
17
- import * as path from "path";
18
13
 
19
14
  // ============================================================================
20
15
  // Types
@@ -30,7 +25,7 @@ export interface JsonPatchOperation {
30
25
  export type JsonPatch = JsonPatchOperation[];
31
26
 
32
27
  export interface PatchFile {
33
- description: string;
28
+ description?: string;
34
29
  patches: JsonPatch;
35
30
  }
36
31
 
@@ -38,9 +33,7 @@ export interface PatchFile {
38
33
  // JSON Pointer (RFC 6901)
39
34
  // ============================================================================
40
35
 
41
- /**
42
- * Parse a JSON Pointer (RFC 6901) path into segments.
43
- */
36
+ /** Parse a JSON Pointer (RFC 6901) path into segments. */
44
37
  export function parseJsonPointer(pointer: string): string[] {
45
38
  if (pointer === "") return [];
46
39
  if (!pointer.startsWith("/")) {
@@ -52,31 +45,52 @@ export function parseJsonPointer(pointer: string): string[] {
52
45
  .map((segment) => segment.replace(/~1/g, "/").replace(/~0/g, "~"));
53
46
  }
54
47
 
48
+ /** Get a value at a JSON Pointer path. */
55
49
  /**
56
- * Get a value at a JSON Pointer path.
50
+ * The target location of a patch operation is absent from the document —
51
+ * spec drift (renamed/removed upstream), not a malformed patch.
57
52
  */
53
+ export class StaleTargetError extends Error {
54
+ override readonly name = "StaleTargetError";
55
+ }
56
+
58
57
  export function getValueAtPath(obj: unknown, pointer: string): unknown {
59
58
  const segments = parseJsonPointer(pointer);
60
59
  let current: unknown = obj;
61
60
 
62
61
  for (const segment of segments) {
63
- if (current === null || typeof current !== "object") {
64
- throw new Error(`Cannot traverse path ${pointer}: not an object`);
62
+ if (
63
+ current === null ||
64
+ current === undefined ||
65
+ typeof current !== "object"
66
+ ) {
67
+ throw new StaleTargetError(
68
+ `JSON pointer ${pointer} missing (at '${segment}'): not an object`,
69
+ );
65
70
  }
66
71
  if (Array.isArray(current)) {
67
72
  const index = segment === "-" ? current.length : parseInt(segment, 10);
73
+ if (index < 0 || index >= current.length) {
74
+ throw new StaleTargetError(
75
+ `JSON pointer ${pointer} missing index '${segment}'`,
76
+ );
77
+ }
68
78
  current = current[index];
69
79
  } else {
70
- current = (current as Record<string, unknown>)[segment];
80
+ const record = current as Record<string, unknown>;
81
+ if (!Object.prototype.hasOwnProperty.call(record, segment)) {
82
+ throw new StaleTargetError(
83
+ `JSON pointer ${pointer} missing key '${segment}'`,
84
+ );
85
+ }
86
+ current = record[segment];
71
87
  }
72
88
  }
73
89
 
74
90
  return current;
75
91
  }
76
92
 
77
- /**
78
- * Set a value at a JSON Pointer path.
79
- */
93
+ /** Set a value at a JSON Pointer path. */
80
94
  export function setValueAtPath(
81
95
  obj: unknown,
82
96
  pointer: string,
@@ -92,11 +106,12 @@ export function setValueAtPath(
92
106
  for (let i = 0; i < segments.length - 1; i++) {
93
107
  const segment = segments[i]!;
94
108
  if (current === null || typeof current !== "object") {
95
- throw new Error(`Cannot traverse path ${pointer}: not an object`);
109
+ throw new StaleTargetError(
110
+ `Cannot traverse path ${pointer}: not an object`,
111
+ );
96
112
  }
97
113
  if (Array.isArray(current)) {
98
- const index = parseInt(segment, 10);
99
- current = current[index];
114
+ current = current[parseInt(segment, 10)];
100
115
  } else {
101
116
  current = (current as Record<string, unknown>)[segment];
102
117
  }
@@ -104,27 +119,23 @@ export function setValueAtPath(
104
119
 
105
120
  const lastSegment = segments[segments.length - 1]!;
106
121
  if (current === null || typeof current !== "object") {
107
- throw new Error(
122
+ throw new StaleTargetError(
108
123
  `Cannot set value at path ${pointer}: parent is not an object`,
109
124
  );
110
125
  }
111
126
 
112
127
  if (Array.isArray(current)) {
113
- const index =
114
- lastSegment === "-" ? current.length : parseInt(lastSegment, 10);
115
128
  if (lastSegment === "-") {
116
129
  current.push(value);
117
130
  } else {
118
- current[index] = value;
131
+ current[parseInt(lastSegment, 10)] = value;
119
132
  }
120
133
  } else {
121
134
  (current as Record<string, unknown>)[lastSegment] = value;
122
135
  }
123
136
  }
124
137
 
125
- /**
126
- * Remove a value at a JSON Pointer path.
127
- */
138
+ /** Remove a value at a JSON Pointer path. */
128
139
  export function removeValueAtPath(obj: unknown, pointer: string): void {
129
140
  const segments = parseJsonPointer(pointer);
130
141
  if (segments.length === 0) {
@@ -136,7 +147,9 @@ export function removeValueAtPath(obj: unknown, pointer: string): void {
136
147
  for (let i = 0; i < segments.length - 1; i++) {
137
148
  const segment = segments[i]!;
138
149
  if (current === null || typeof current !== "object") {
139
- throw new Error(`Cannot traverse path ${pointer}: not an object`);
150
+ throw new StaleTargetError(
151
+ `Cannot traverse path ${pointer}: not an object`,
152
+ );
140
153
  }
141
154
  if (Array.isArray(current)) {
142
155
  current = current[parseInt(segment, 10)];
@@ -147,15 +160,28 @@ export function removeValueAtPath(obj: unknown, pointer: string): void {
147
160
 
148
161
  const lastSegment = segments[segments.length - 1]!;
149
162
  if (current === null || typeof current !== "object") {
150
- throw new Error(
163
+ throw new StaleTargetError(
151
164
  `Cannot remove at path ${pointer}: parent is not an object`,
152
165
  );
153
166
  }
154
167
 
168
+ // RFC 6902 §4.2: the target location MUST exist.
155
169
  if (Array.isArray(current)) {
156
- current.splice(parseInt(lastSegment, 10), 1);
170
+ const index = parseInt(lastSegment, 10);
171
+ if (Number.isNaN(index) || index < 0 || index >= current.length) {
172
+ throw new StaleTargetError(
173
+ `JSON pointer ${pointer} missing index '${lastSegment}'`,
174
+ );
175
+ }
176
+ current.splice(index, 1);
157
177
  } else {
158
- delete (current as Record<string, unknown>)[lastSegment];
178
+ const record = current as Record<string, unknown>;
179
+ if (!Object.prototype.hasOwnProperty.call(record, lastSegment)) {
180
+ throw new StaleTargetError(
181
+ `JSON pointer ${pointer} missing key '${lastSegment}'`,
182
+ );
183
+ }
184
+ delete record[lastSegment];
159
185
  }
160
186
  }
161
187
 
@@ -163,9 +189,7 @@ export function removeValueAtPath(obj: unknown, pointer: string): void {
163
189
  // Patch Operations
164
190
  // ============================================================================
165
191
 
166
- /**
167
- * Apply a single JSON Patch operation.
168
- */
192
+ /** Apply a single JSON Patch operation (mutates `obj` in place). */
169
193
  export function applyOperation(
170
194
  obj: unknown,
171
195
  operation: JsonPatchOperation,
@@ -177,14 +201,24 @@ export function applyOperation(
177
201
  case "remove":
178
202
  removeValueAtPath(obj, operation.path);
179
203
  break;
180
- case "replace":
181
- // For replace, the path must exist
182
- getValueAtPath(obj, operation.path); // throws if doesn't exist
204
+ case "replace": {
205
+ const existing = getValueAtPath(obj, operation.path);
206
+ if (existing === undefined) {
207
+ throw new StaleTargetError(
208
+ `JSON pointer ${operation.path} does not exist`,
209
+ );
210
+ }
183
211
  setValueAtPath(obj, operation.path, operation.value);
184
212
  break;
213
+ }
185
214
  case "move": {
186
215
  if (!operation.from) throw new Error("move operation requires 'from'");
187
216
  const moveValue = getValueAtPath(obj, operation.from);
217
+ if (moveValue === undefined) {
218
+ throw new StaleTargetError(
219
+ `Cannot move from path ${operation.from}: not an object`,
220
+ );
221
+ }
188
222
  removeValueAtPath(obj, operation.from);
189
223
  setValueAtPath(obj, operation.path, moveValue);
190
224
  break;
@@ -213,9 +247,7 @@ export function applyOperation(
213
247
  }
214
248
  }
215
249
 
216
- /**
217
- * Apply a JSON Patch to an object (mutates in place).
218
- */
250
+ /** Apply a JSON Patch to an object (mutates in place). */
219
251
  export function applyPatch(obj: unknown, patch: JsonPatch): void {
220
252
  for (const operation of patch) {
221
253
  applyOperation(obj, operation);
@@ -223,84 +255,25 @@ export function applyPatch(obj: unknown, patch: JsonPatch): void {
223
255
  }
224
256
 
225
257
  /**
226
- * Whether a per-operation failure is caused by the target location being
227
- * absent from the spec (i.e. vendor spec drift — the operation/schema the
228
- * patch targets was renamed or removed upstream) as opposed to a malformed
229
- * patch. Stale targets are skipped with a warning rather than aborting the
230
- * whole generate; a patch that only adds an error response to an operation
231
- * that no longer exists is harmless to drop.
258
+ * Whether a per-operation failure is a {@link StaleTargetError} (the
259
+ * operation/shape the patch targets was renamed or removed upstream) as
260
+ * opposed to a malformed patch. Accepts the thrown value or its message.
261
+ * Convert fails the run on these by default (`onStalePatch: "fail"`);
262
+ * `"warn"` restores skip-and-continue.
232
263
  */
233
- function isStaleTargetError(message: string): boolean {
264
+ export function isStaleTargetError(error: unknown): boolean {
265
+ if (error instanceof StaleTargetError) return true;
266
+ const message =
267
+ typeof error === "string"
268
+ ? error
269
+ : error instanceof Error
270
+ ? error.message
271
+ : "";
234
272
  return (
235
273
  message.includes("not an object") ||
236
- message.includes("parent is not an object")
274
+ message.includes("parent is not an object") ||
275
+ message.includes("missing key") ||
276
+ message.includes("missing index") ||
277
+ message.includes("does not exist")
237
278
  );
238
279
  }
239
-
240
- /**
241
- * Load and apply all patches from a directory.
242
- * Finds all *.patch.json files and applies them.
243
- *
244
- * Each operation within a patch file is applied independently. Operations
245
- * whose target path no longer exists in the spec (vendor spec drift) are
246
- * collected as `skipped` warnings and do not abort generation. Only genuine
247
- * failures (malformed JSON, invalid pointers, failed `test` ops) are reported
248
- * as `errors`.
249
- */
250
- export function applyAllPatches(
251
- spec: unknown,
252
- patchDir: string,
253
- ): { applied: string[]; skipped: string[]; errors: string[] } {
254
- const applied: string[] = [];
255
- const skipped: string[] = [];
256
- const errors: string[] = [];
257
-
258
- if (!fs.existsSync(patchDir)) {
259
- return { applied, skipped, errors };
260
- }
261
-
262
- // Find all .patch.json files
263
- const files = fs
264
- .readdirSync(patchDir)
265
- .filter((f) => f.endsWith(".patch.json"))
266
- .sort(); // Sort for deterministic application order
267
-
268
- for (const file of files) {
269
- const filePath = path.join(patchDir, file);
270
- let patchFile: PatchFile;
271
- try {
272
- const content = fs.readFileSync(filePath, "utf-8");
273
- patchFile = JSON.parse(content);
274
- } catch (error) {
275
- errors.push(
276
- `${file}: ${error instanceof Error ? error.message : String(error)}`,
277
- );
278
- continue;
279
- }
280
-
281
- let appliedAny = false;
282
- for (const operation of patchFile.patches) {
283
- try {
284
- applyOperation(spec, operation);
285
- appliedAny = true;
286
- } catch (error) {
287
- const message = error instanceof Error ? error.message : String(error);
288
- if (isStaleTargetError(message)) {
289
- skipped.push(
290
- `${file} [${operation.op} ${operation.path}]: ${message}`,
291
- );
292
- } else {
293
- errors.push(
294
- `${file} [${operation.op} ${operation.path}]: ${message}`,
295
- );
296
- }
297
- }
298
- }
299
-
300
- if (appliedAny) {
301
- applied.push(`${file}: ${patchFile.description}`);
302
- }
303
- }
304
-
305
- return { applied, skipped, errors };
306
- }