@beignet/core 0.0.48 → 0.0.49

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 (89) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +64 -14
  3. package/dist/client/client.d.ts +0 -2
  4. package/dist/client/client.d.ts.map +1 -1
  5. package/dist/client/client.js +28 -25
  6. package/dist/client/client.js.map +1 -1
  7. package/dist/contracts/contract-builder.d.ts +7 -2
  8. package/dist/contracts/contract-builder.d.ts.map +1 -1
  9. package/dist/contracts/contract-builder.js +20 -2
  10. package/dist/contracts/contract-builder.js.map +1 -1
  11. package/dist/contracts/contract-group.d.ts.map +1 -1
  12. package/dist/contracts/contract-group.js +1 -0
  13. package/dist/contracts/contract-group.js.map +1 -1
  14. package/dist/contracts/contract-like.d.ts +2 -0
  15. package/dist/contracts/contract-like.d.ts.map +1 -1
  16. package/dist/contracts/contract-like.js +27 -1
  17. package/dist/contracts/contract-like.js.map +1 -1
  18. package/dist/contracts/index.d.ts +4 -0
  19. package/dist/contracts/index.d.ts.map +1 -1
  20. package/dist/contracts/index.js +4 -0
  21. package/dist/contracts/index.js.map +1 -1
  22. package/dist/contracts/query-transport.d.ts +126 -0
  23. package/dist/contracts/query-transport.d.ts.map +1 -0
  24. package/dist/contracts/query-transport.js +406 -0
  25. package/dist/contracts/query-transport.js.map +1 -0
  26. package/dist/contracts/schema-shape.d.ts +11 -0
  27. package/dist/contracts/schema-shape.d.ts.map +1 -1
  28. package/dist/contracts/schema-shape.js +13 -0
  29. package/dist/contracts/schema-shape.js.map +1 -1
  30. package/dist/contracts/types.d.ts +5 -0
  31. package/dist/contracts/types.d.ts.map +1 -1
  32. package/dist/contracts/types.js.map +1 -1
  33. package/dist/openapi/index.d.ts +8 -0
  34. package/dist/openapi/index.d.ts.map +1 -1
  35. package/dist/openapi/index.js +79 -5
  36. package/dist/openapi/index.js.map +1 -1
  37. package/dist/outbox/index.d.ts +8 -5
  38. package/dist/outbox/index.d.ts.map +1 -1
  39. package/dist/outbox/index.js +17 -3
  40. package/dist/outbox/index.js.map +1 -1
  41. package/dist/ports/cache.d.ts +9 -1
  42. package/dist/ports/cache.d.ts.map +1 -1
  43. package/dist/ports/cache.js +20 -5
  44. package/dist/ports/cache.js.map +1 -1
  45. package/dist/ports/index.d.ts +1 -1
  46. package/dist/ports/index.d.ts.map +1 -1
  47. package/dist/ports/index.js +1 -1
  48. package/dist/ports/index.js.map +1 -1
  49. package/dist/query-codec.d.ts +24 -0
  50. package/dist/query-codec.d.ts.map +1 -1
  51. package/dist/query-codec.js +207 -72
  52. package/dist/query-codec.js.map +1 -1
  53. package/dist/server/hooks/rate-limit.d.ts +16 -8
  54. package/dist/server/hooks/rate-limit.d.ts.map +1 -1
  55. package/dist/server/hooks/rate-limit.js +31 -17
  56. package/dist/server/hooks/rate-limit.js.map +1 -1
  57. package/dist/server/request-executor.d.ts.map +1 -1
  58. package/dist/server/request-executor.js +9 -9
  59. package/dist/server/request-executor.js.map +1 -1
  60. package/dist/server/request-preparation.d.ts.map +1 -1
  61. package/dist/server/request-preparation.js +11 -9
  62. package/dist/server/request-preparation.js.map +1 -1
  63. package/dist/server/server.d.ts.map +1 -1
  64. package/dist/server/server.js +2 -0
  65. package/dist/server/server.js.map +1 -1
  66. package/dist/server/use-case-route.d.ts +8 -5
  67. package/dist/server/use-case-route.d.ts.map +1 -1
  68. package/dist/server/use-case-route.js +44 -17
  69. package/dist/server/use-case-route.js.map +1 -1
  70. package/package.json +3 -3
  71. package/skills/app-architecture/SKILL.md +27 -1
  72. package/src/client/client.ts +29 -28
  73. package/src/contracts/contract-builder.ts +32 -2
  74. package/src/contracts/contract-group.ts +1 -0
  75. package/src/contracts/contract-like.ts +40 -1
  76. package/src/contracts/index.ts +23 -0
  77. package/src/contracts/query-transport.ts +697 -0
  78. package/src/contracts/schema-shape.ts +24 -0
  79. package/src/contracts/types.ts +5 -0
  80. package/src/openapi/index.ts +126 -2
  81. package/src/outbox/index.ts +26 -5
  82. package/src/ports/cache.ts +29 -7
  83. package/src/ports/index.ts +1 -1
  84. package/src/server/hooks/rate-limit.ts +50 -24
  85. package/src/server/request-executor.ts +17 -9
  86. package/src/server/request-preparation.ts +22 -9
  87. package/src/server/server.ts +2 -0
  88. package/src/server/use-case-route.ts +62 -23
  89. package/src/query-codec.ts +0 -130
@@ -192,8 +192,10 @@ type UseCaseRouteShape<
192
192
  /**
193
193
  * Map parsed request parts to the use case input.
194
194
  *
195
- * Defaults to `defaultBinderInput`, which merges query, body, and path
196
- * objects (path wins collisions) and never merges headers.
195
+ * A sole declared path, query, or body schema is passed through unchanged
196
+ * when no additional path, query, or object body values are present.
197
+ * Otherwise `defaultBinderInput` merges query, body, and path objects (path
198
+ * wins collisions) and never merges headers.
197
199
  */
198
200
  input?: (parts: UseCaseRouteInputParts<C>) => UseCaseRouteInput<UC>;
199
201
  handle?: never;
@@ -330,9 +332,10 @@ function isPlainObject(value: unknown): value is Record<string, unknown> {
330
332
  * Merges parsed query, body, and path objects into one input object. Path
331
333
  * keys win all collisions, then body keys, then query keys. Headers are never
332
334
  * merged: parsed headers include every raw request header, so merging them
333
- * would poison the use case input. Non-object bodies (text, arrays, scalars)
334
- * are excluded. Routes that need headers or non-object bodies declare an
335
- * explicit `input` mapper.
335
+ * would poison the use case input. The route binder passes a sole declared
336
+ * input schema through unchanged when no other object input contains values;
337
+ * this merge handles every other default mapping. Routes that combine a
338
+ * non-object body with another source declare an explicit `input` mapper.
336
339
  */
337
340
  export function defaultBinderInput(parts: {
338
341
  path: unknown;
@@ -356,16 +359,42 @@ export function isUseCaseRouteDef(route: {
356
359
  return route.useCase !== undefined && route.useCase !== null;
357
360
  }
358
361
 
359
- function computeTrustedInput(
362
+ function computeSingleInput(
360
363
  contract: HttpContractConfig,
361
364
  def: RuntimeUseCaseRouteDef,
362
- ): boolean {
363
- if (def.input) return false;
365
+ ): { source: "path" | "query" | "body"; schema: unknown } | undefined {
366
+ if (def.input) return undefined;
367
+
368
+ const sources = [
369
+ { source: "path", schema: contract.pathParams },
370
+ {
371
+ source: "query",
372
+ schema: contract.query,
373
+ },
374
+ {
375
+ source: "body",
376
+ schema: contract.body,
377
+ },
378
+ ] as const;
379
+ const present = sources.filter(
380
+ (candidate) => candidate.schema !== null && candidate.schema !== undefined,
381
+ );
382
+ const single = present[0];
383
+ return present.length === 1 && single
384
+ ? { source: single.source, schema: single.schema }
385
+ : undefined;
386
+ }
364
387
 
365
- const sources = [contract.pathParams, contract.query, contract.body].filter(
366
- (schema) => schema !== null && schema !== undefined,
388
+ function canPassSingleInput(
389
+ parts: { path: unknown; query: unknown; body: unknown },
390
+ source: "path" | "query" | "body",
391
+ ): boolean {
392
+ return (["path", "query", "body"] as const).every(
393
+ (candidate) =>
394
+ candidate === source ||
395
+ !isPlainObject(parts[candidate]) ||
396
+ Object.keys(parts[candidate]).length === 0,
367
397
  );
368
- return sources.length === 1 && sources[0] === def.useCase.inputSchema;
369
398
  }
370
399
 
371
400
  function computeResponseExemption(
@@ -405,11 +434,11 @@ export function createUseCaseRouteHandler<Ctx, C extends HttpContractConfig>(
405
434
  );
406
435
  }
407
436
 
408
- const mapInput = def.input ?? defaultBinderInput;
409
- const trustedRun = computeTrustedInput(contract, def)
410
- ? def.useCase[USE_CASE_TRUSTED_RUN_KEY]
411
- : undefined;
412
- const run = trustedRun ?? def.useCase.run;
437
+ const singleInput = computeSingleInput(contract, def);
438
+ const trustedRun =
439
+ singleInput?.schema === def.useCase.inputSchema
440
+ ? def.useCase[USE_CASE_TRUSTED_RUN_KEY]
441
+ : undefined;
413
442
 
414
443
  const handler: Handler<Ctx, C> = async ({
415
444
  ctx,
@@ -417,13 +446,23 @@ export function createUseCaseRouteHandler<Ctx, C extends HttpContractConfig>(
417
446
  query,
418
447
  headers,
419
448
  body,
420
- }) => ({
421
- status,
422
- body: await run.call(def.useCase, {
423
- ctx,
424
- input: mapInput({ path, query, headers, body }),
425
- }),
426
- });
449
+ }) => {
450
+ const parts = { path, query, headers, body };
451
+ const passSingle =
452
+ singleInput !== undefined &&
453
+ canPassSingleInput(parts, singleInput.source);
454
+ const input = def.input
455
+ ? def.input(parts)
456
+ : passSingle
457
+ ? parts[singleInput.source]
458
+ : defaultBinderInput(parts);
459
+ const run = passSingle && trustedRun ? trustedRun : def.useCase.run;
460
+
461
+ return {
462
+ status,
463
+ body: await run.call(def.useCase, { ctx, input }),
464
+ };
465
+ };
427
466
 
428
467
  return {
429
468
  handler,
@@ -1,130 +0,0 @@
1
- const QUERY_VALUE_PREFIX = "~beignet-query:";
2
-
3
- type EncodedQueryValue =
4
- | { type: "null" }
5
- | { type: "string"; value: string }
6
- | { type: "number"; value: number }
7
- | { type: "boolean"; value: boolean }
8
- | { type: "date"; value: string }
9
- | { type: "array"; value: EncodedQueryValue[] }
10
- | { type: "object"; value: Record<string, EncodedQueryValue> };
11
-
12
- function encodeStructuredValue(
13
- value: unknown,
14
- ancestors: Set<object>,
15
- ): EncodedQueryValue {
16
- if (value === null) return { type: "null" };
17
- if (typeof value === "string") return { type: "string", value };
18
- if (typeof value === "number" && Number.isFinite(value)) {
19
- return { type: "number", value };
20
- }
21
- if (typeof value === "boolean") return { type: "boolean", value };
22
- if (value instanceof Date && Number.isFinite(value.getTime())) {
23
- return { type: "date", value: value.toISOString() };
24
- }
25
- if (typeof value !== "object" || value === null) {
26
- throw new TypeError("Query value is not serializable.");
27
- }
28
- if (ancestors.has(value)) {
29
- throw new TypeError("Query value contains a circular reference.");
30
- }
31
-
32
- ancestors.add(value);
33
- try {
34
- if (Array.isArray(value)) {
35
- return {
36
- type: "array",
37
- value: value.map((entry) => encodeStructuredValue(entry, ancestors)),
38
- };
39
- }
40
-
41
- const prototype = Object.getPrototypeOf(value);
42
- if (prototype !== Object.prototype && prototype !== null) {
43
- throw new TypeError("Query value contains a non-plain object.");
44
- }
45
- return {
46
- type: "object",
47
- value: Object.fromEntries(
48
- Object.entries(value)
49
- .filter(([, entry]) => entry !== undefined)
50
- .map(([key, entry]) => [
51
- key,
52
- encodeStructuredValue(entry, ancestors),
53
- ]),
54
- ),
55
- };
56
- } finally {
57
- ancestors.delete(value);
58
- }
59
- }
60
-
61
- function decodeStructuredValue(value: unknown): unknown {
62
- if (typeof value !== "object" || value === null || !("type" in value)) {
63
- throw new TypeError("Invalid encoded query value.");
64
- }
65
- const encoded = value as { type: unknown; value?: unknown };
66
-
67
- switch (encoded.type) {
68
- case "null":
69
- return null;
70
- case "string":
71
- if (typeof encoded.value === "string") return encoded.value;
72
- break;
73
- case "number":
74
- if (typeof encoded.value === "number" && Number.isFinite(encoded.value)) {
75
- return encoded.value;
76
- }
77
- break;
78
- case "boolean":
79
- if (typeof encoded.value === "boolean") return encoded.value;
80
- break;
81
- case "date": {
82
- if (typeof encoded.value !== "string") break;
83
- const date = new Date(encoded.value);
84
- if (Number.isFinite(date.getTime())) return date;
85
- break;
86
- }
87
- case "array":
88
- if (Array.isArray(encoded.value)) {
89
- return encoded.value.map(decodeStructuredValue);
90
- }
91
- break;
92
- case "object":
93
- if (
94
- typeof encoded.value === "object" &&
95
- encoded.value !== null &&
96
- !Array.isArray(encoded.value)
97
- ) {
98
- return Object.fromEntries(
99
- Object.entries(encoded.value).map(([key, entry]) => [
100
- key,
101
- decodeStructuredValue(entry),
102
- ]),
103
- );
104
- }
105
- break;
106
- }
107
-
108
- throw new TypeError("Invalid encoded query value.");
109
- }
110
-
111
- export function encodeQueryValue(value: unknown): string {
112
- if (typeof value === "string" && !value.startsWith(QUERY_VALUE_PREFIX)) {
113
- return value;
114
- }
115
- return `${QUERY_VALUE_PREFIX}${JSON.stringify(
116
- encodeStructuredValue(value, new Set()),
117
- )}`;
118
- }
119
-
120
- export function decodeQueryValue(value: string): unknown {
121
- if (!value.startsWith(QUERY_VALUE_PREFIX)) return value;
122
-
123
- try {
124
- return decodeStructuredValue(
125
- JSON.parse(value.slice(QUERY_VALUE_PREFIX.length)) as unknown,
126
- );
127
- } catch {
128
- return value;
129
- }
130
- }