@distilled.cloud/core 1.0.0-rc.1 → 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 (148) hide show
  1. package/LICENSE +201 -0
  2. package/lib/api.d.ts.map +1 -1
  3. package/lib/api.js +16 -4
  4. package/lib/api.js.map +1 -1
  5. package/lib/category.d.ts +4 -4
  6. package/lib/category.js +4 -4
  7. package/lib/codegen/boolean-string-enums.d.ts +36 -0
  8. package/lib/codegen/boolean-string-enums.d.ts.map +1 -0
  9. package/lib/codegen/boolean-string-enums.js +94 -0
  10. package/lib/codegen/boolean-string-enums.js.map +1 -0
  11. package/lib/codegen/boolean-string-enums.test.d.ts +2 -0
  12. package/lib/codegen/boolean-string-enums.test.d.ts.map +1 -0
  13. package/lib/codegen/boolean-string-enums.test.js +147 -0
  14. package/lib/codegen/boolean-string-enums.test.js.map +1 -0
  15. package/lib/codegen/cli.d.ts +56 -6
  16. package/lib/codegen/cli.d.ts.map +1 -1
  17. package/lib/codegen/cli.js +59 -75
  18. package/lib/codegen/cli.js.map +1 -1
  19. package/lib/codegen/emit.d.ts +3 -7
  20. package/lib/codegen/emit.d.ts.map +1 -1
  21. package/lib/codegen/emit.js +6 -10
  22. package/lib/codegen/emit.js.map +1 -1
  23. package/lib/codegen/generator.d.ts.map +1 -1
  24. package/lib/codegen/generator.js +142 -20
  25. package/lib/codegen/generator.js.map +1 -1
  26. package/lib/codegen/graphql-client.d.ts +62 -0
  27. package/lib/codegen/graphql-client.d.ts.map +1 -0
  28. package/lib/codegen/graphql-client.js +294 -0
  29. package/lib/codegen/graphql-client.js.map +1 -0
  30. package/lib/codegen/graphql-client.test.d.ts +2 -0
  31. package/lib/codegen/graphql-client.test.d.ts.map +1 -0
  32. package/lib/codegen/graphql-client.test.js +311 -0
  33. package/lib/codegen/graphql-client.test.js.map +1 -0
  34. package/lib/codegen/graphql.d.ts +207 -0
  35. package/lib/codegen/graphql.d.ts.map +1 -0
  36. package/lib/codegen/graphql.js +799 -0
  37. package/lib/codegen/graphql.js.map +1 -0
  38. package/lib/codegen/openapi-cli.d.ts +17 -3
  39. package/lib/codegen/openapi-cli.d.ts.map +1 -1
  40. package/lib/codegen/openapi-cli.js +50 -48
  41. package/lib/codegen/openapi-cli.js.map +1 -1
  42. package/lib/codegen/openapi.d.ts +69 -6
  43. package/lib/codegen/openapi.d.ts.map +1 -1
  44. package/lib/codegen/openapi.js +173 -16
  45. package/lib/codegen/openapi.js.map +1 -1
  46. package/lib/codegen/patches.d.ts +65 -0
  47. package/lib/codegen/patches.d.ts.map +1 -0
  48. package/lib/codegen/patches.js +236 -0
  49. package/lib/codegen/patches.js.map +1 -0
  50. package/lib/codegen/patches.test.d.ts +2 -0
  51. package/lib/codegen/patches.test.d.ts.map +1 -0
  52. package/lib/codegen/patches.test.js +105 -0
  53. package/lib/codegen/patches.test.js.map +1 -0
  54. package/lib/codegen/proto.d.ts +121 -0
  55. package/lib/codegen/proto.d.ts.map +1 -0
  56. package/lib/codegen/proto.js +962 -0
  57. package/lib/codegen/proto.js.map +1 -0
  58. package/lib/codegen/rewrite-operation-ids.d.ts +131 -0
  59. package/lib/codegen/rewrite-operation-ids.d.ts.map +1 -0
  60. package/lib/codegen/rewrite-operation-ids.js +1079 -0
  61. package/lib/codegen/rewrite-operation-ids.js.map +1 -0
  62. package/lib/codegen/rewrite-operation-ids.test.d.ts +2 -0
  63. package/lib/codegen/rewrite-operation-ids.test.d.ts.map +1 -0
  64. package/lib/codegen/rewrite-operation-ids.test.js +533 -0
  65. package/lib/codegen/rewrite-operation-ids.test.js.map +1 -0
  66. package/lib/codegen/spec-path.d.ts +16 -0
  67. package/lib/codegen/spec-path.d.ts.map +1 -0
  68. package/lib/codegen/spec-path.js +101 -0
  69. package/lib/codegen/spec-path.js.map +1 -0
  70. package/lib/errors.d.ts.map +1 -1
  71. package/lib/errors.js +17 -13
  72. package/lib/errors.js.map +1 -1
  73. package/lib/graphql.d.ts +284 -0
  74. package/lib/graphql.d.ts.map +1 -0
  75. package/lib/graphql.fixture.d.ts +249 -0
  76. package/lib/graphql.fixture.d.ts.map +1 -0
  77. package/lib/graphql.fixture.js +240 -0
  78. package/lib/graphql.fixture.js.map +1 -0
  79. package/lib/graphql.js +718 -0
  80. package/lib/graphql.js.map +1 -0
  81. package/lib/graphql.test.d.ts +2 -0
  82. package/lib/graphql.test.d.ts.map +1 -0
  83. package/lib/graphql.test.js +780 -0
  84. package/lib/graphql.test.js.map +1 -0
  85. package/lib/graphql.types.d.ts +2 -0
  86. package/lib/graphql.types.d.ts.map +1 -0
  87. package/lib/graphql.types.js +45 -0
  88. package/lib/graphql.types.js.map +1 -0
  89. package/lib/json-patch.d.ts +18 -11
  90. package/lib/json-patch.d.ts.map +1 -1
  91. package/lib/json-patch.js +63 -25
  92. package/lib/json-patch.js.map +1 -1
  93. package/lib/pagination.d.ts +43 -3
  94. package/lib/pagination.d.ts.map +1 -1
  95. package/lib/pagination.js +91 -5
  96. package/lib/pagination.js.map +1 -1
  97. package/lib/protocol-http.d.ts.map +1 -1
  98. package/lib/protocol-http.js +62 -26
  99. package/lib/protocol-http.js.map +1 -1
  100. package/lib/protocol-http.test.d.ts +2 -0
  101. package/lib/protocol-http.test.d.ts.map +1 -0
  102. package/lib/protocol-http.test.js +88 -0
  103. package/lib/protocol-http.test.js.map +1 -0
  104. package/lib/protocol-rest.d.ts +12 -2
  105. package/lib/protocol-rest.d.ts.map +1 -1
  106. package/lib/protocol-rest.js +17 -3
  107. package/lib/protocol-rest.js.map +1 -1
  108. package/lib/retry.js +1 -1
  109. package/lib/schema.d.ts +1 -1
  110. package/lib/schema.d.ts.map +1 -1
  111. package/lib/schema.js +1 -1
  112. package/lib/schema.js.map +1 -1
  113. package/lib/trait.d.ts +27 -3
  114. package/lib/trait.d.ts.map +1 -1
  115. package/lib/trait.js +17 -1
  116. package/lib/trait.js.map +1 -1
  117. package/package.json +13 -10
  118. package/src/api.ts +18 -4
  119. package/src/category.ts +4 -4
  120. package/src/codegen/boolean-string-enums.test.ts +168 -0
  121. package/src/codegen/boolean-string-enums.ts +106 -0
  122. package/src/codegen/cli.ts +127 -110
  123. package/src/codegen/emit.ts +6 -10
  124. package/src/codegen/generator.ts +151 -21
  125. package/src/codegen/graphql-client.test.ts +386 -0
  126. package/src/codegen/graphql-client.ts +419 -0
  127. package/src/codegen/graphql.ts +1217 -0
  128. package/src/codegen/openapi-cli.ts +75 -59
  129. package/src/codegen/openapi.ts +255 -16
  130. package/src/codegen/patches.test.ts +130 -0
  131. package/src/codegen/patches.ts +291 -0
  132. package/src/codegen/proto.ts +1128 -0
  133. package/src/codegen/rewrite-operation-ids.test.ts +563 -0
  134. package/src/codegen/rewrite-operation-ids.ts +1206 -0
  135. package/src/codegen/spec-path.ts +115 -0
  136. package/src/errors.ts +20 -25
  137. package/src/graphql.fixture.ts +371 -0
  138. package/src/graphql.test.ts +974 -0
  139. package/src/graphql.ts +1321 -0
  140. package/src/graphql.types.ts +185 -0
  141. package/src/json-patch.ts +82 -25
  142. package/src/pagination.ts +134 -7
  143. package/src/protocol-http.test.ts +107 -0
  144. package/src/protocol-http.ts +67 -31
  145. package/src/protocol-rest.ts +28 -4
  146. package/src/retry.ts +1 -1
  147. package/src/schema.ts +2 -2
  148. package/src/trait.ts +39 -3
@@ -22,10 +22,12 @@ import {
22
22
  keyDictionarySymbol,
23
23
  labelSymbol,
24
24
  querySymbol,
25
+ stringEncodedSymbol,
25
26
  unionCasesSymbol,
26
27
  type ErrorMatcher,
27
28
  type HttpTrait,
28
29
  type KeyDictionaryEntries,
30
+ type UnionDiscriminator,
29
31
  } from "./trait.ts";
30
32
 
31
33
  //#region AST helpers (survive S.optional / Suspend / transforms)
@@ -167,13 +169,19 @@ export const mapKeys = (
167
169
 
168
170
  // Discriminated union whose cases the API returns merged (every case's
169
171
  // keys present, `null` for the inactive ones). Map wire names, then keep
170
- // only the active case's keys. A case does NOT need every key present —
171
- // arms carry optional members the wire may omit — so the active case is
172
- // the one that best EXPLAINS the value: most present, non-null keys
173
- // covered, minus present keys the case cannot explain (ties break by
174
- // coverage, then declaration order).
172
+ // only the active case's keys. The case is read off the union's
173
+ // discriminator when it has one — cases that share a key set (every
174
+ // resource kind of `resource_tagging`, every page-rule action) are
175
+ // indistinguishable by keys alone. Without one, a case does NOT need every
176
+ // key present — arms carry optional members the wire may omit — so the
177
+ // active case is the one that best EXPLAINS the value: most present,
178
+ // non-null keys covered, minus present keys the case cannot explain (ties
179
+ // break by coverage, then declaration order).
175
180
  const unionCases = getAnn(ast, unionCasesSymbol) as
176
- | ReadonlyArray<ReadonlyArray<string>>
181
+ | {
182
+ readonly cases: ReadonlyArray<ReadonlyArray<string>>;
183
+ readonly discriminator?: UnionDiscriminator;
184
+ }
177
185
  | undefined;
178
186
  if (unionCases && direction === "decode" && !Array.isArray(value)) {
179
187
  const obj = (
@@ -182,25 +190,32 @@ export const mapKeys = (
182
190
  const present = Object.keys(obj).filter(
183
191
  (k) => obj[k] !== undefined && obj[k] !== null,
184
192
  );
193
+ const disc = unionCases.discriminator;
194
+ const tag = disc ? obj[disc.key] : undefined;
195
+ const tagged = typeof tag === "string" ? disc!.values.indexOf(tag) : -1;
185
196
  let best:
186
197
  | { keys: ReadonlyArray<string>; score: number; matched: number }
187
198
  | undefined;
188
- for (const keys of unionCases) {
189
- const set = new Set(keys);
190
- let matched = 0;
191
- let excess = 0;
192
- for (const k of present) {
193
- if (set.has(k)) matched++;
194
- else excess++;
195
- }
196
- const score = matched - excess;
197
- if (
198
- matched > 0 &&
199
- (!best ||
200
- score > best.score ||
201
- (score === best.score && matched > best.matched))
202
- ) {
203
- best = { keys, score, matched };
199
+ if (tagged >= 0) {
200
+ best = { keys: unionCases.cases[tagged]!, score: 0, matched: 0 };
201
+ } else {
202
+ for (const keys of unionCases.cases) {
203
+ const set = new Set(keys);
204
+ let matched = 0;
205
+ let excess = 0;
206
+ for (const k of present) {
207
+ if (set.has(k)) matched++;
208
+ else excess++;
209
+ }
210
+ const score = matched - excess;
211
+ if (
212
+ matched > 0 &&
213
+ (!best ||
214
+ score > best.score ||
215
+ (score === best.score && matched > best.matched))
216
+ ) {
217
+ best = { keys, score, matched };
218
+ }
204
219
  }
205
220
  }
206
221
  if (best) {
@@ -319,6 +334,18 @@ const BODYLESS = new Set(["GET", "HEAD"]);
319
334
  * filter matching nothing — the call "succeeds" with zero results and the
320
335
  * bug is invisible to the caller.
321
336
  */
337
+ /**
338
+ * Value form for a `StringEncoded()` member: the string spelling of the
339
+ * value, element-wise for lists. `null` stays `null` — an API that models a
340
+ * flag as `"true" | "false"` still means "unset" by null, not `"null"`.
341
+ */
342
+ const stringEncode = (value: unknown): unknown =>
343
+ value === null
344
+ ? null
345
+ : Array.isArray(value)
346
+ ? value.map(stringEncode)
347
+ : String(value);
348
+
322
349
  const appendQuery = (
323
350
  query: URLSearchParams,
324
351
  name: string,
@@ -482,12 +509,13 @@ export const buildRequest = ({
482
509
  } else if (hasPropAnn(prop, httpBodySymbol)) {
483
510
  rawBody = mapKeys(prop.type, value, "encode", rootDict);
484
511
  } else {
485
- body[nameOf(prop, bodySymbol)] = mapKeys(
486
- prop.type,
487
- value,
488
- "encode",
489
- rootDict,
490
- );
512
+ // T.StringEncoded(): the API takes this member's value only as its
513
+ // string spelling (`true` → `"true"`), while the TS surface keeps the
514
+ // natural type. Explicit here so a JSON body carries the string too,
515
+ // rather than relying on the multipart encoder's own `String()`.
516
+ body[nameOf(prop, bodySymbol)] = hasPropAnn(prop, stringEncodedSymbol)
517
+ ? stringEncode(value)
518
+ : mapKeys(prop.type, value, "encode", rootDict);
491
519
  }
492
520
  }
493
521
 
@@ -616,9 +644,17 @@ export const buildRequest = ({
616
644
  request = request.pipe(HttpClientRequest.bodyJsonUnsafe(rawBody));
617
645
  }
618
646
  } else if (
619
- !BODYLESS.has(http.method) &&
620
- (Object.keys(body).length > 0 ||
621
- (hasBodyMembers && http.method !== "DELETE"))
647
+ // A GET/HEAD sends a body only when body members actually carry values.
648
+ // Some APIs really do document GET-with-body: MongoDB Atlas's
649
+ // `…/lineItems:search` declares a `requestBody` on `get`, and AWS EFS's
650
+ // DescribeAccountPreferences takes `{ MaxResults, NextToken }` on a GET.
651
+ // Suppressing it outright dropped those members silently — the request
652
+ // succeeded, unfiltered, with nothing on the wire to show why.
653
+ //
654
+ // The `{}`-when-empty rule below stays limited to methods that expect a
655
+ // body, so a bodyless method with nothing set still sends nothing.
656
+ Object.keys(body).length > 0 ||
657
+ (hasBodyMembers && !BODYLESS.has(http.method) && http.method !== "DELETE")
622
658
  ) {
623
659
  // Send `{}` rather than no body when the schema declares body members —
624
660
  // some endpoints reject a missing JSON body outright.
@@ -30,9 +30,10 @@ import type * as AST from "effect/SchemaAST";
30
30
  import type * as HttpClientRequest from "effect/unstable/http/HttpClientRequest";
31
31
  import type * as HttpClientResponse from "effect/unstable/http/HttpClientResponse";
32
32
  import * as API from "./api.ts";
33
- import { makeAnnotation } from "./trait.ts";
33
+ import { httpSymbol, makeAnnotation, type HttpTrait } from "./trait.ts";
34
34
  import {
35
35
  buildRequest,
36
+ getAnn,
36
37
  getPropAnn,
37
38
  getProps,
38
39
  isOpaqueValue,
@@ -185,8 +186,18 @@ export interface RestProtocolOptions<C> {
185
186
  * `<Sdk>OpContext` annotations.
186
187
  */
187
188
  readonly credentials: Effect.Effect<C, any, any>;
188
- /** API base URL from the resolved credentials. */
189
- readonly baseUrl: (credentials: C) => string;
189
+ /**
190
+ * API base URL from the resolved credentials. Receives the operation's
191
+ * route so APIs with more than one endpoint (e.g. S2's account vs
192
+ * per-basin hosts, chosen by path) can route per request; single-endpoint
193
+ * providers ignore the argument. A THROWN error (e.g. a required scope
194
+ * missing from the credentials) is caught and surfaced on the calling
195
+ * effect's error channel rather than dying as a defect.
196
+ */
197
+ readonly baseUrl: (
198
+ credentials: C,
199
+ target: { readonly uri: string; readonly method: string },
200
+ ) => string;
190
201
  /** Auth (and any fixed) headers from the resolved credentials. */
191
202
  readonly headers: (credentials: C) => Record<string, string>;
192
203
  /**
@@ -257,10 +268,23 @@ export const makeRestProtocol = <C>(
257
268
  }) =>
258
269
  Effect.gen(function* () {
259
270
  const creds = yield* options.credentials as Effect.Effect<C>;
271
+ const http = getAnn(inputAst, httpSymbol) as HttpTrait | undefined;
272
+ let baseUrl: string;
273
+ try {
274
+ baseUrl = options.baseUrl(creds, {
275
+ uri: http?.uri ?? "",
276
+ method: http?.method ?? "",
277
+ });
278
+ } catch (e) {
279
+ // A baseUrl that refuses the route (missing scope in the
280
+ // credentials) fails the call with that error, typed for callers by
281
+ // the operation's declared error channel.
282
+ return yield* Effect.fail(e);
283
+ }
260
284
  return buildRequest({
261
285
  input: unwrapRedactedDeep(input),
262
286
  inputAst,
263
- baseUrl: options.baseUrl(creds),
287
+ baseUrl,
264
288
  headers: options.headers(creds),
265
289
  mapMemberHeader: options.mapMemberHeader,
266
290
  unknownKeyToWire: options.unknownKeyToWire,
package/src/retry.ts CHANGED
@@ -121,7 +121,7 @@ export const ServerRetryHintCapMs = Context.Service<number>(
121
121
  export const serverRetryHintCapLayer = (capMs: number) =>
122
122
  Layer.succeed(ServerRetryHintCapMs, capMs);
123
123
 
124
- const serverRetryHintCapMsConfig: Config.Config<number> = Config.string(
124
+ const serverRetryHintCapMsConfig: Config.Config<number> = Config.String(
125
125
  ENV_SERVER_RETRY_HINT_CAP_MS,
126
126
  ).pipe(
127
127
  Config.map((raw) => {
package/src/schema.ts CHANGED
@@ -23,7 +23,7 @@ export * from "effect/Schema";
23
23
 
24
24
  import * as S from "effect/Schema";
25
25
 
26
- type AnyFn = (...args: any[]) => any;
26
+ export type AnyFn = (...args: any[]) => any;
27
27
 
28
28
  // Construction surface — collapse to `any` so generics are never instantiated.
29
29
  export const optional: AnyFn = S.optional as AnyFn;
@@ -48,6 +48,6 @@ export const Null: any = S.Null;
48
48
  export const Void: any = S.Void;
49
49
  export const Any: any = S.Any;
50
50
 
51
- // NOTE: `Schema` / `Codec` (types) and `TaggedErrorClass` are intentionally NOT
51
+ // NOTE: `Schema` / `Codec` (types) and `TaggedError` are intentionally NOT
52
52
  // overridden — they flow through `export *` with real types so cast targets and
53
53
  // typed error classes stay precise.
package/src/trait.ts CHANGED
@@ -2,7 +2,7 @@ export const annotationMetaSymbol = Symbol.for(
2
2
  "@distilled.cloud/core/annotation-meta",
3
3
  );
4
4
 
5
- type Annotatable = {
5
+ export type Annotatable = {
6
6
  annotate(annotations: any): Annotatable;
7
7
  };
8
8
 
@@ -147,6 +147,20 @@ export const httpBodySymbol = Symbol.for("@distilled.cloud/core/http-body");
147
147
  */
148
148
  export const HttpBody = () => makeAnnotation(httpBodySymbol, true);
149
149
 
150
+ export const stringEncodedSymbol = Symbol.for(
151
+ "@distilled.cloud/core/http/string-encoded",
152
+ );
153
+ /**
154
+ * Marks a member the API only accepts as the STRING spelling of its value —
155
+ * `true` travels as `"true"`, `3` as `"3"` — while the TS surface keeps the
156
+ * natural type (e.g. Cloudflare's multipart `validation_enabled`, which the
157
+ * docs model as the enum `"true" | "false"`). The protocol stringifies body
158
+ * members on encode, element-wise for lists; `null` is left alone. Query and
159
+ * header bindings already serialize their values as strings, so the trait is
160
+ * documentation there.
161
+ */
162
+ export const StringEncoded = () => makeAnnotation(stringEncodedSymbol, true);
163
+
150
164
  export const formDataFileSymbol = Symbol.for(
151
165
  "@distilled.cloud/core/form-data-file",
152
166
  );
@@ -184,15 +198,37 @@ export const KeyDictionary = (dict: KeyDictionaryEntries) =>
184
198
  makeAnnotation(keyDictionarySymbol, dict);
185
199
 
186
200
  export const unionCasesSymbol = Symbol.for("@distilled.cloud/core/union-cases");
201
+
202
+ /**
203
+ * The literal member every case of a union fixes to its own value — what the
204
+ * docs call the resource `type` or the action `id`. `values[i]` is the value
205
+ * case `i` carries.
206
+ */
207
+ export interface UnionDiscriminator {
208
+ readonly key: string;
209
+ readonly values: ReadonlyArray<string>;
210
+ }
211
+
187
212
  /**
188
213
  * Marks an opaque schema standing in for a discriminated union of object
189
214
  * cases, carrying each case's TS-facing key set. For APIs that return every
190
215
  * case's keys with `null` for the inactive ones, the protocol uses these key
191
216
  * sets to pick the active case and drop the others, so consumers' `"key" in
192
217
  * value` discrimination works.
218
+ *
219
+ * Key sets alone cannot tell apart cases that share them — the 41 resource
220
+ * kinds of `resource_tagging`, the 34 page-rule actions — so when the union
221
+ * has a discriminator the protocol reads the case off it and only falls back
222
+ * to key-set scoring for a value whose tag is missing or unknown.
193
223
  */
194
- export const UnionCases = (cases: ReadonlyArray<ReadonlyArray<string>>) =>
195
- makeAnnotation(unionCasesSymbol, cases);
224
+ export const UnionCases = (
225
+ cases: ReadonlyArray<ReadonlyArray<string>>,
226
+ discriminator?: UnionDiscriminator,
227
+ ) =>
228
+ makeAnnotation(
229
+ unionCasesSymbol,
230
+ discriminator ? { cases, discriminator } : { cases },
231
+ );
196
232
  //#endregion
197
233
 
198
234
  //#region Error matcher traits