@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.
- package/LICENSE +201 -0
- package/lib/api.d.ts +165 -0
- package/lib/api.d.ts.map +1 -0
- package/lib/api.js +190 -0
- package/lib/api.js.map +1 -0
- package/lib/category.d.ts +4 -4
- package/lib/category.js +4 -4
- package/lib/codegen/boolean-string-enums.d.ts +36 -0
- package/lib/codegen/boolean-string-enums.d.ts.map +1 -0
- package/lib/codegen/boolean-string-enums.js +94 -0
- package/lib/codegen/boolean-string-enums.js.map +1 -0
- package/lib/codegen/boolean-string-enums.test.d.ts +2 -0
- package/lib/codegen/boolean-string-enums.test.d.ts.map +1 -0
- package/lib/codegen/boolean-string-enums.test.js +147 -0
- package/lib/codegen/boolean-string-enums.test.js.map +1 -0
- package/lib/codegen/cli.d.ts +79 -0
- package/lib/codegen/cli.d.ts.map +1 -0
- package/lib/codegen/cli.js +149 -0
- package/lib/codegen/cli.js.map +1 -0
- package/lib/codegen/emit.d.ts +125 -0
- package/lib/codegen/emit.d.ts.map +1 -0
- package/lib/codegen/emit.js +101 -0
- package/lib/codegen/emit.js.map +1 -0
- package/lib/codegen/format.d.ts +23 -0
- package/lib/codegen/format.d.ts.map +1 -0
- package/lib/codegen/format.js +28 -0
- package/lib/codegen/format.js.map +1 -0
- package/lib/codegen/generator.d.ts +334 -0
- package/lib/codegen/generator.d.ts.map +1 -0
- package/lib/codegen/generator.js +813 -0
- package/lib/codegen/generator.js.map +1 -0
- package/lib/codegen/graph.d.ts +36 -0
- package/lib/codegen/graph.d.ts.map +1 -0
- package/lib/codegen/graph.js +136 -0
- package/lib/codegen/graph.js.map +1 -0
- package/lib/codegen/graphql-client.d.ts +62 -0
- package/lib/codegen/graphql-client.d.ts.map +1 -0
- package/lib/codegen/graphql-client.js +294 -0
- package/lib/codegen/graphql-client.js.map +1 -0
- package/lib/codegen/graphql-client.test.d.ts +2 -0
- package/lib/codegen/graphql-client.test.d.ts.map +1 -0
- package/lib/codegen/graphql-client.test.js +311 -0
- package/lib/codegen/graphql-client.test.js.map +1 -0
- package/lib/codegen/graphql.d.ts +207 -0
- package/lib/codegen/graphql.d.ts.map +1 -0
- package/lib/codegen/graphql.js +799 -0
- package/lib/codegen/graphql.js.map +1 -0
- package/lib/codegen/members.d.ts +25 -0
- package/lib/codegen/members.d.ts.map +1 -0
- package/lib/codegen/members.js +55 -0
- package/lib/codegen/members.js.map +1 -0
- package/lib/codegen/naming.d.ts +29 -0
- package/lib/codegen/naming.d.ts.map +1 -0
- package/lib/codegen/naming.js +74 -0
- package/lib/codegen/naming.js.map +1 -0
- package/lib/codegen/openapi-cli.d.ts +52 -0
- package/lib/codegen/openapi-cli.d.ts.map +1 -0
- package/lib/codegen/openapi-cli.js +109 -0
- package/lib/codegen/openapi-cli.js.map +1 -0
- package/lib/codegen/openapi.d.ts +178 -0
- package/lib/codegen/openapi.d.ts.map +1 -0
- package/lib/codegen/openapi.js +1377 -0
- package/lib/codegen/openapi.js.map +1 -0
- package/lib/codegen/operations.d.ts +24 -0
- package/lib/codegen/operations.d.ts.map +1 -0
- package/lib/codegen/operations.js +56 -0
- package/lib/codegen/operations.js.map +1 -0
- package/lib/codegen/pagination.d.ts +39 -0
- package/lib/codegen/pagination.d.ts.map +1 -0
- package/lib/codegen/pagination.js +33 -0
- package/lib/codegen/pagination.js.map +1 -0
- package/lib/codegen/patches.d.ts +65 -0
- package/lib/codegen/patches.d.ts.map +1 -0
- package/lib/codegen/patches.js +236 -0
- package/lib/codegen/patches.js.map +1 -0
- package/lib/codegen/patches.test.d.ts +2 -0
- package/lib/codegen/patches.test.d.ts.map +1 -0
- package/lib/codegen/patches.test.js +105 -0
- package/lib/codegen/patches.test.js.map +1 -0
- package/lib/codegen/prelude.d.ts +15 -0
- package/lib/codegen/prelude.d.ts.map +1 -0
- package/lib/codegen/prelude.js +60 -0
- package/lib/codegen/prelude.js.map +1 -0
- package/lib/codegen/proto.d.ts +121 -0
- package/lib/codegen/proto.d.ts.map +1 -0
- package/lib/codegen/proto.js +962 -0
- package/lib/codegen/proto.js.map +1 -0
- package/lib/codegen/rewrite-operation-ids.d.ts +131 -0
- package/lib/codegen/rewrite-operation-ids.d.ts.map +1 -0
- package/lib/codegen/rewrite-operation-ids.js +1079 -0
- package/lib/codegen/rewrite-operation-ids.js.map +1 -0
- package/lib/codegen/rewrite-operation-ids.test.d.ts +2 -0
- package/lib/codegen/rewrite-operation-ids.test.d.ts.map +1 -0
- package/lib/codegen/rewrite-operation-ids.test.js +533 -0
- package/lib/codegen/rewrite-operation-ids.test.js.map +1 -0
- package/lib/codegen/spec-path.d.ts +16 -0
- package/lib/codegen/spec-path.d.ts.map +1 -0
- package/lib/codegen/spec-path.js +101 -0
- package/lib/codegen/spec-path.js.map +1 -0
- package/lib/error-category.d.ts +28 -0
- package/lib/error-category.d.ts.map +1 -0
- package/lib/error-category.js +46 -0
- package/lib/error-category.js.map +1 -0
- package/lib/errors.d.ts +1 -0
- package/lib/errors.d.ts.map +1 -1
- package/lib/errors.js +18 -13
- package/lib/errors.js.map +1 -1
- package/lib/graphql.d.ts +284 -0
- package/lib/graphql.d.ts.map +1 -0
- package/lib/graphql.fixture.d.ts +249 -0
- package/lib/graphql.fixture.d.ts.map +1 -0
- package/lib/graphql.fixture.js +240 -0
- package/lib/graphql.fixture.js.map +1 -0
- package/lib/graphql.js +718 -0
- package/lib/graphql.js.map +1 -0
- package/lib/graphql.test.d.ts +2 -0
- package/lib/graphql.test.d.ts.map +1 -0
- package/lib/graphql.test.js +780 -0
- package/lib/graphql.test.js.map +1 -0
- package/lib/graphql.types.d.ts +2 -0
- package/lib/graphql.types.d.ts.map +1 -0
- package/lib/graphql.types.js +45 -0
- package/lib/graphql.types.js.map +1 -0
- package/lib/json-patch.d.ts +30 -30
- package/lib/json-patch.d.ts.map +1 -1
- package/lib/json-patch.js +73 -107
- package/lib/json-patch.js.map +1 -1
- package/lib/pagination.d.ts +77 -51
- package/lib/pagination.d.ts.map +1 -1
- package/lib/pagination.js +162 -94
- package/lib/pagination.js.map +1 -1
- package/lib/protocol-http.d.ts +74 -0
- package/lib/protocol-http.d.ts.map +1 -0
- package/lib/protocol-http.js +590 -0
- package/lib/protocol-http.js.map +1 -0
- package/lib/protocol-http.test.d.ts +2 -0
- package/lib/protocol-http.test.d.ts.map +1 -0
- package/lib/protocol-http.test.js +88 -0
- package/lib/protocol-http.test.js.map +1 -0
- package/lib/protocol-rest.d.ts +134 -0
- package/lib/protocol-rest.d.ts.map +1 -0
- package/lib/protocol-rest.js +256 -0
- package/lib/protocol-rest.js.map +1 -0
- package/lib/retry.d.ts +8 -2
- package/lib/retry.d.ts.map +1 -1
- package/lib/retry.js +22 -16
- package/lib/retry.js.map +1 -1
- package/lib/schema.d.ts +8 -9
- package/lib/schema.d.ts.map +1 -1
- package/lib/schema.js +8 -9
- package/lib/schema.js.map +1 -1
- package/lib/trait.d.ts +174 -0
- package/lib/trait.d.ts.map +1 -0
- package/lib/trait.js +123 -0
- package/lib/trait.js.map +1 -0
- package/package.json +24 -78
- package/src/api.ts +460 -0
- package/src/category.ts +4 -4
- package/src/codegen/boolean-string-enums.test.ts +168 -0
- package/src/codegen/boolean-string-enums.ts +106 -0
- package/src/codegen/cli.ts +285 -0
- package/src/codegen/emit.ts +203 -0
- package/src/codegen/format.ts +47 -0
- package/src/codegen/generator.ts +1283 -0
- package/src/codegen/graph.ts +151 -0
- package/src/codegen/graphql-client.test.ts +386 -0
- package/src/codegen/graphql-client.ts +419 -0
- package/src/codegen/graphql.ts +1217 -0
- package/src/codegen/members.ts +71 -0
- package/src/codegen/naming.ts +86 -0
- package/src/codegen/openapi-cli.ts +182 -0
- package/src/codegen/openapi.ts +1689 -0
- package/src/codegen/operations.ts +76 -0
- package/src/codegen/pagination.ts +71 -0
- package/src/codegen/patches.test.ts +130 -0
- package/src/codegen/patches.ts +291 -0
- package/src/codegen/prelude.ts +70 -0
- package/src/codegen/proto.ts +1128 -0
- package/src/codegen/rewrite-operation-ids.test.ts +563 -0
- package/src/codegen/rewrite-operation-ids.ts +1206 -0
- package/src/codegen/spec-path.ts +115 -0
- package/src/error-category.ts +84 -0
- package/src/errors.ts +22 -25
- package/src/graphql.fixture.ts +371 -0
- package/src/graphql.test.ts +974 -0
- package/src/graphql.ts +1321 -0
- package/src/graphql.types.ts +185 -0
- package/src/json-patch.ts +95 -122
- package/src/pagination.ts +217 -146
- package/src/protocol-http.test.ts +107 -0
- package/src/protocol-http.ts +735 -0
- package/src/protocol-rest.ts +391 -0
- package/src/retry.ts +21 -22
- package/src/schema.ts +9 -10
- package/src/trait.ts +274 -0
- package/README.md +0 -30
- package/lib/client.d.ts +0 -167
- package/lib/client.d.ts.map +0 -1
- package/lib/client.js +0 -659
- package/lib/client.js.map +0 -1
- package/lib/schemas.d.ts +0 -60
- package/lib/schemas.d.ts.map +0 -1
- package/lib/schemas.js +0 -79
- package/lib/schemas.js.map +0 -1
- package/lib/sensitive.d.ts +0 -71
- package/lib/sensitive.d.ts.map +0 -1
- package/lib/sensitive.js +0 -96
- package/lib/sensitive.js.map +0 -1
- package/lib/traits.d.ts +0 -421
- package/lib/traits.d.ts.map +0 -1
- package/lib/traits.js +0 -737
- package/lib/traits.js.map +0 -1
- package/src/client.ts +0 -1177
- package/src/schemas.ts +0 -128
- package/src/sensitive.ts +0 -119
- 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
|
-
*
|
|
6
|
-
*
|
|
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
|
-
*
|
|
9
|
-
*
|
|
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
|
|
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
|
-
*
|
|
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 (
|
|
64
|
-
|
|
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
|
-
|
|
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
|
|
109
|
+
throw new StaleTargetError(
|
|
110
|
+
`Cannot traverse path ${pointer}: not an object`,
|
|
111
|
+
);
|
|
96
112
|
}
|
|
97
113
|
if (Array.isArray(current)) {
|
|
98
|
-
|
|
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
|
|
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[
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
182
|
-
|
|
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
|
|
227
|
-
*
|
|
228
|
-
*
|
|
229
|
-
*
|
|
230
|
-
*
|
|
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(
|
|
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
|
-
}
|