@beignet/core 0.0.50 → 0.0.52

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 (102) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +99 -32
  3. package/dist/application/index.d.ts +5 -5
  4. package/dist/application/index.d.ts.map +1 -1
  5. package/dist/application/index.js +8 -4
  6. package/dist/application/index.js.map +1 -1
  7. package/dist/events/index.d.ts +26 -2
  8. package/dist/events/index.d.ts.map +1 -1
  9. package/dist/events/index.js +84 -7
  10. package/dist/events/index.js.map +1 -1
  11. package/dist/events/payload-state.d.ts +16 -0
  12. package/dist/events/payload-state.d.ts.map +1 -0
  13. package/dist/events/payload-state.js +123 -0
  14. package/dist/events/payload-state.js.map +1 -0
  15. package/dist/events/transport.d.ts +25 -0
  16. package/dist/events/transport.d.ts.map +1 -0
  17. package/dist/events/transport.js +192 -0
  18. package/dist/events/transport.js.map +1 -0
  19. package/dist/locks/index.d.ts.map +1 -1
  20. package/dist/locks/index.js +0 -4
  21. package/dist/locks/index.js.map +1 -1
  22. package/dist/openapi/index.d.ts.map +1 -1
  23. package/dist/openapi/index.js +27 -4
  24. package/dist/openapi/index.js.map +1 -1
  25. package/dist/outbox/index.d.ts +163 -15
  26. package/dist/outbox/index.d.ts.map +1 -1
  27. package/dist/outbox/index.js +1009 -155
  28. package/dist/outbox/index.js.map +1 -1
  29. package/dist/payments/index.d.ts.map +1 -1
  30. package/dist/payments/index.js +0 -4
  31. package/dist/payments/index.js.map +1 -1
  32. package/dist/ports/events.d.ts +4 -1
  33. package/dist/ports/events.d.ts.map +1 -1
  34. package/dist/ports/index.d.ts +1 -1
  35. package/dist/ports/index.d.ts.map +1 -1
  36. package/dist/ports/index.js.map +1 -1
  37. package/dist/ports/storage.d.ts +7 -0
  38. package/dist/ports/storage.d.ts.map +1 -1
  39. package/dist/ports/storage.js +4 -0
  40. package/dist/ports/storage.js.map +1 -1
  41. package/dist/ports/testing.d.ts +16 -2
  42. package/dist/ports/testing.d.ts.map +1 -1
  43. package/dist/ports/testing.js +19 -4
  44. package/dist/ports/testing.js.map +1 -1
  45. package/dist/ports/unit-of-work.d.ts +9 -7
  46. package/dist/ports/unit-of-work.d.ts.map +1 -1
  47. package/dist/ports/unit-of-work.js +16 -7
  48. package/dist/ports/unit-of-work.js.map +1 -1
  49. package/dist/providers/index.d.ts +1 -1
  50. package/dist/providers/index.d.ts.map +1 -1
  51. package/dist/providers/index.js.map +1 -1
  52. package/dist/providers/provider.d.ts +0 -38
  53. package/dist/providers/provider.d.ts.map +1 -1
  54. package/dist/providers/provider.js.map +1 -1
  55. package/dist/search/index.d.ts.map +1 -1
  56. package/dist/search/index.js +2 -6
  57. package/dist/search/index.js.map +1 -1
  58. package/dist/server/hooks/cors.d.ts +5 -0
  59. package/dist/server/hooks/cors.d.ts.map +1 -1
  60. package/dist/server/hooks/cors.js +29 -1
  61. package/dist/server/hooks/cors.js.map +1 -1
  62. package/dist/server/instrumentation.d.ts +5 -5
  63. package/dist/server/instrumentation.d.ts.map +1 -1
  64. package/dist/server/instrumentation.js +3 -4
  65. package/dist/server/instrumentation.js.map +1 -1
  66. package/dist/server/response-finalization.d.ts.map +1 -1
  67. package/dist/server/response-finalization.js +25 -13
  68. package/dist/server/response-finalization.js.map +1 -1
  69. package/dist/server/server.d.ts +8 -5
  70. package/dist/server/server.d.ts.map +1 -1
  71. package/dist/server/server.js +35 -12
  72. package/dist/server/server.js.map +1 -1
  73. package/dist/testing/index.d.ts.map +1 -1
  74. package/dist/testing/index.js +12 -6
  75. package/dist/testing/index.js.map +1 -1
  76. package/dist/uploads/index.d.ts.map +1 -1
  77. package/dist/uploads/index.js +37 -16
  78. package/dist/uploads/index.js.map +1 -1
  79. package/package.json +2 -2
  80. package/skills/app-architecture/SKILL.md +16 -0
  81. package/src/application/index.ts +39 -7
  82. package/src/events/index.ts +137 -7
  83. package/src/events/payload-state.ts +223 -0
  84. package/src/events/transport.ts +242 -0
  85. package/src/locks/index.ts +0 -4
  86. package/src/openapi/index.ts +41 -3
  87. package/src/outbox/index.ts +1393 -178
  88. package/src/payments/index.ts +0 -4
  89. package/src/ports/events.ts +4 -1
  90. package/src/ports/index.ts +3 -0
  91. package/src/ports/storage.ts +10 -0
  92. package/src/ports/testing.ts +42 -4
  93. package/src/ports/unit-of-work.ts +33 -15
  94. package/src/providers/index.ts +0 -1
  95. package/src/providers/provider.ts +0 -40
  96. package/src/search/index.ts +2 -6
  97. package/src/server/hooks/cors.ts +42 -0
  98. package/src/server/instrumentation.ts +10 -8
  99. package/src/server/response-finalization.ts +33 -15
  100. package/src/server/server.ts +61 -27
  101. package/src/testing/index.ts +20 -9
  102. package/src/uploads/index.ts +34 -16
@@ -0,0 +1,223 @@
1
+ import type { EventPublishOptions } from "./index.js";
2
+ import type { EventTransportValue } from "./transport.js";
3
+
4
+ const PARSED_EVENT_PAYLOAD = Symbol.for("beignet.eventPayload.parsed");
5
+ const TRANSPORT_STABLE_EVENT_PAYLOAD = Symbol.for(
6
+ "beignet.eventPayload.transportStable",
7
+ );
8
+
9
+ type ParsedEventPublishOptions = EventPublishOptions & {
10
+ [PARSED_EVENT_PAYLOAD]?: object;
11
+ [TRANSPORT_STABLE_EVENT_PAYLOAD]?: object;
12
+ };
13
+
14
+ interface ParsedPayloadState {
15
+ readonly eventName: string;
16
+ readonly payloadSchema: object;
17
+ readonly validate: unknown;
18
+ readonly payload: unknown;
19
+ }
20
+
21
+ type PayloadSnapshot =
22
+ | { readonly kind: "primitive"; readonly value: unknown }
23
+ | {
24
+ readonly kind: "array";
25
+ readonly value: object;
26
+ readonly prototype: object | null;
27
+ readonly items: readonly PayloadSnapshot[];
28
+ }
29
+ | {
30
+ readonly kind: "object";
31
+ readonly value: object;
32
+ readonly prototype: object | null;
33
+ readonly entries: readonly (readonly [string, PayloadSnapshot])[];
34
+ };
35
+
36
+ interface TransportStablePayloadState {
37
+ readonly eventName: string;
38
+ readonly payloadSchema: object;
39
+ readonly validate: unknown;
40
+ readonly payload: PayloadSnapshot;
41
+ readonly fingerprint: string;
42
+ }
43
+
44
+ interface EventPayloadIdentity {
45
+ readonly name: string;
46
+ readonly payload: {
47
+ readonly "~standard": {
48
+ readonly validate: unknown;
49
+ };
50
+ };
51
+ }
52
+
53
+ const transportStablePayloadStates = new WeakMap<
54
+ object,
55
+ TransportStablePayloadState
56
+ >();
57
+ const parsedPayloadStates = new WeakMap<object, ParsedPayloadState>();
58
+
59
+ function transportFingerprint(value: EventTransportValue): string {
60
+ return JSON.stringify(value);
61
+ }
62
+
63
+ function capturePayloadSnapshot(value: unknown): PayloadSnapshot {
64
+ if (value === null || typeof value !== "object") {
65
+ return { kind: "primitive", value };
66
+ }
67
+ if (Array.isArray(value)) {
68
+ const items: PayloadSnapshot[] = [];
69
+ for (let index = 0; index < value.length; index += 1) {
70
+ const descriptor = Object.getOwnPropertyDescriptor(value, String(index));
71
+ if (!descriptor || !("value" in descriptor)) {
72
+ throw new TypeError(
73
+ "Transport-stable array state must contain data properties.",
74
+ );
75
+ }
76
+ items.push(capturePayloadSnapshot(descriptor.value));
77
+ }
78
+ return {
79
+ kind: "array",
80
+ value,
81
+ prototype: Object.getPrototypeOf(value),
82
+ items,
83
+ };
84
+ }
85
+ return {
86
+ kind: "object",
87
+ value,
88
+ prototype: Object.getPrototypeOf(value),
89
+ entries: Reflect.ownKeys(value).map((key) => {
90
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
91
+ if (typeof key !== "string" || !descriptor || !("value" in descriptor)) {
92
+ throw new TypeError(
93
+ "Transport-stable payload state must contain data properties.",
94
+ );
95
+ }
96
+ return [key, capturePayloadSnapshot(descriptor.value)] as const;
97
+ }),
98
+ };
99
+ }
100
+
101
+ function matchesPayloadSnapshot(
102
+ snapshot: PayloadSnapshot,
103
+ value: unknown,
104
+ ): boolean {
105
+ if (snapshot.kind === "primitive") return Object.is(snapshot.value, value);
106
+ if (snapshot.value !== value || value === null || typeof value !== "object") {
107
+ return false;
108
+ }
109
+ if (Object.getPrototypeOf(value) !== snapshot.prototype) return false;
110
+ if (snapshot.kind === "array") {
111
+ return (
112
+ Array.isArray(value) &&
113
+ value.length === snapshot.items.length &&
114
+ snapshot.items.every((item, index) => {
115
+ const descriptor = Object.getOwnPropertyDescriptor(
116
+ value,
117
+ String(index),
118
+ );
119
+ return (
120
+ descriptor !== undefined &&
121
+ "value" in descriptor &&
122
+ matchesPayloadSnapshot(item, descriptor.value)
123
+ );
124
+ })
125
+ );
126
+ }
127
+ if (Array.isArray(value)) return false;
128
+ const keys = Reflect.ownKeys(value);
129
+ return (
130
+ keys.length === snapshot.entries.length &&
131
+ snapshot.entries.every(([key, item], index) => {
132
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
133
+ return (
134
+ keys[index] === key &&
135
+ descriptor !== undefined &&
136
+ "value" in descriptor &&
137
+ matchesPayloadSnapshot(item, descriptor.value)
138
+ );
139
+ })
140
+ );
141
+ }
142
+
143
+ export function markEventPayloadParsed(
144
+ event: EventPayloadIdentity,
145
+ payload: unknown,
146
+ options?: EventPublishOptions,
147
+ ): EventPublishOptions {
148
+ const token = {};
149
+ parsedPayloadStates.set(token, {
150
+ eventName: event.name,
151
+ payloadSchema: event.payload,
152
+ validate: event.payload["~standard"].validate,
153
+ payload,
154
+ });
155
+ return {
156
+ ...options,
157
+ [PARSED_EVENT_PAYLOAD]: token,
158
+ } as ParsedEventPublishOptions;
159
+ }
160
+
161
+ export function isEventPayloadParsed(
162
+ event: EventPayloadIdentity,
163
+ payload: unknown,
164
+ options?: EventPublishOptions,
165
+ ): boolean {
166
+ const token = (options as ParsedEventPublishOptions | undefined)?.[
167
+ PARSED_EVENT_PAYLOAD
168
+ ];
169
+ const state = token ? parsedPayloadStates.get(token) : undefined;
170
+ return (
171
+ state?.eventName === event.name &&
172
+ state.payloadSchema === event.payload &&
173
+ state.validate === event.payload["~standard"].validate &&
174
+ Object.is(state.payload, payload)
175
+ );
176
+ }
177
+
178
+ export function markEventPayloadTransportStable(
179
+ event: EventPayloadIdentity,
180
+ payload: unknown,
181
+ transportValue: EventTransportValue,
182
+ options?: EventPublishOptions,
183
+ ): EventPublishOptions {
184
+ const parsedToken = {};
185
+ parsedPayloadStates.set(parsedToken, {
186
+ eventName: event.name,
187
+ payloadSchema: event.payload,
188
+ validate: event.payload["~standard"].validate,
189
+ payload,
190
+ });
191
+ const stableToken = {};
192
+ transportStablePayloadStates.set(stableToken, {
193
+ eventName: event.name,
194
+ payloadSchema: event.payload,
195
+ validate: event.payload["~standard"].validate,
196
+ payload: capturePayloadSnapshot(payload),
197
+ fingerprint: transportFingerprint(transportValue),
198
+ });
199
+ return {
200
+ ...options,
201
+ [PARSED_EVENT_PAYLOAD]: parsedToken,
202
+ [TRANSPORT_STABLE_EVENT_PAYLOAD]: stableToken,
203
+ } as ParsedEventPublishOptions;
204
+ }
205
+
206
+ export function isEventPayloadTransportStable(
207
+ event: EventPayloadIdentity,
208
+ payload: unknown,
209
+ transportValue: EventTransportValue,
210
+ options?: EventPublishOptions,
211
+ ): boolean {
212
+ const token = (options as ParsedEventPublishOptions | undefined)?.[
213
+ TRANSPORT_STABLE_EVENT_PAYLOAD
214
+ ];
215
+ const state = token ? transportStablePayloadStates.get(token) : undefined;
216
+ return (
217
+ state?.eventName === event.name &&
218
+ state.payloadSchema === event.payload &&
219
+ state.validate === event.payload["~standard"].validate &&
220
+ matchesPayloadSnapshot(state.payload, payload) &&
221
+ state.fingerprint === transportFingerprint(transportValue)
222
+ );
223
+ }
@@ -0,0 +1,242 @@
1
+ /** Canonical JSON value that can cross an event transport boundary. */
2
+ export type EventTransportValue =
3
+ | null
4
+ | string
5
+ | number
6
+ | boolean
7
+ | readonly EventTransportValue[]
8
+ | { readonly [key: string]: EventTransportValue };
9
+
10
+ /** Why an event payload cannot safely cross a serialized transport. */
11
+ export type EventTransportErrorReason = "not-json-safe" | "not-stable";
12
+
13
+ /** Error thrown when an event payload cannot survive canonical JSON transport. */
14
+ export class EventTransportError extends Error {
15
+ /** Stable event name. */
16
+ readonly eventName: string;
17
+ /** Whether the value is not JSON-safe or changes under repeated parsing. */
18
+ readonly reason: EventTransportErrorReason;
19
+ /** Payload path associated with a JSON-safety failure. */
20
+ readonly path?: string;
21
+
22
+ constructor(args: {
23
+ eventName: string;
24
+ reason: EventTransportErrorReason;
25
+ message: string;
26
+ path?: string;
27
+ cause?: unknown;
28
+ }) {
29
+ super(`Event "${args.eventName}" payload ${args.message}`, {
30
+ cause: args.cause,
31
+ });
32
+ this.name = "EventTransportError";
33
+ this.eventName = args.eventName;
34
+ this.reason = args.reason;
35
+ this.path = args.path;
36
+ }
37
+ }
38
+
39
+ function childTransportPath(path: string, key: string): string {
40
+ return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(key)
41
+ ? `${path}.${key}`
42
+ : `${path}[${JSON.stringify(key)}]`;
43
+ }
44
+
45
+ function throwJsonSafetyError(args: {
46
+ eventName: string;
47
+ path: string;
48
+ message: string;
49
+ }): never {
50
+ throw new EventTransportError({
51
+ eventName: args.eventName,
52
+ reason: "not-json-safe",
53
+ path: args.path,
54
+ message: `is not transport-safe at ${args.path}: ${args.message}`,
55
+ });
56
+ }
57
+
58
+ export function toEventTransportValue(
59
+ eventName: string,
60
+ value: unknown,
61
+ path = "payload",
62
+ seen: WeakSet<object> = new WeakSet(),
63
+ ): EventTransportValue {
64
+ if (value === null) return null;
65
+
66
+ if (typeof value === "string" || typeof value === "boolean") return value;
67
+ if (typeof value === "number") {
68
+ if (!Number.isFinite(value)) {
69
+ return throwJsonSafetyError({
70
+ eventName,
71
+ path,
72
+ message: "numbers must be finite.",
73
+ });
74
+ }
75
+ return Object.is(value, -0) ? 0 : value;
76
+ }
77
+
78
+ if (typeof value !== "object") {
79
+ return throwJsonSafetyError({
80
+ eventName,
81
+ path,
82
+ message: `received ${typeof value}; use null, strings, finite numbers, booleans, arrays, or plain objects.`,
83
+ });
84
+ }
85
+
86
+ try {
87
+ if (value instanceof Date) {
88
+ return throwJsonSafetyError({
89
+ eventName,
90
+ path,
91
+ message: "Date values are not supported; use an ISO string or number.",
92
+ });
93
+ }
94
+
95
+ if (seen.has(value)) {
96
+ return throwJsonSafetyError({
97
+ eventName,
98
+ path,
99
+ message: "circular references are not supported.",
100
+ });
101
+ }
102
+
103
+ seen.add(value);
104
+ if (Array.isArray(value)) {
105
+ const output: EventTransportValue[] = [];
106
+ const ownKeys = Reflect.ownKeys(value);
107
+ const hasUnsupportedKey = ownKeys.some((key) => {
108
+ if (key === "length") return false;
109
+ if (typeof key !== "string") return true;
110
+ const index = Number(key);
111
+ return (
112
+ !Number.isInteger(index) ||
113
+ index < 0 ||
114
+ index >= value.length ||
115
+ String(index) !== key
116
+ );
117
+ });
118
+ if (hasUnsupportedKey) {
119
+ return throwJsonSafetyError({
120
+ eventName,
121
+ path,
122
+ message: "arrays cannot contain symbol or custom properties.",
123
+ });
124
+ }
125
+ if (ownKeys.length !== value.length + 1) {
126
+ return throwJsonSafetyError({
127
+ eventName,
128
+ path,
129
+ message: "sparse arrays are not supported.",
130
+ });
131
+ }
132
+ for (let index = 0; index < value.length; index += 1) {
133
+ const key = String(index);
134
+ const itemPath = `${path}[${index}]`;
135
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
136
+ if (!descriptor) {
137
+ return throwJsonSafetyError({
138
+ eventName,
139
+ path: itemPath,
140
+ message: "sparse arrays are not supported.",
141
+ });
142
+ }
143
+ if (!("value" in descriptor) || !descriptor.enumerable) {
144
+ return throwJsonSafetyError({
145
+ eventName,
146
+ path: itemPath,
147
+ message: "array items must be enumerable data properties.",
148
+ });
149
+ }
150
+ output.push(
151
+ toEventTransportValue(eventName, descriptor.value, itemPath, seen),
152
+ );
153
+ }
154
+ return output;
155
+ }
156
+
157
+ const prototype = Object.getPrototypeOf(value);
158
+ if (prototype !== Object.prototype && prototype !== null) {
159
+ return throwJsonSafetyError({
160
+ eventName,
161
+ path,
162
+ message: "objects must be plain objects.",
163
+ });
164
+ }
165
+
166
+ const output = {} as Record<string, EventTransportValue>;
167
+ for (const key of Reflect.ownKeys(value)) {
168
+ if (typeof key !== "string") {
169
+ return throwJsonSafetyError({
170
+ eventName,
171
+ path,
172
+ message: "symbol properties are not supported.",
173
+ });
174
+ }
175
+ const propertyPath = childTransportPath(path, key);
176
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
177
+ if (!descriptor || !("value" in descriptor) || !descriptor.enumerable) {
178
+ return throwJsonSafetyError({
179
+ eventName,
180
+ path: propertyPath,
181
+ message: "properties must be enumerable data properties.",
182
+ });
183
+ }
184
+ Object.defineProperty(output, key, {
185
+ configurable: true,
186
+ enumerable: true,
187
+ writable: true,
188
+ value: toEventTransportValue(
189
+ eventName,
190
+ descriptor.value,
191
+ propertyPath,
192
+ seen,
193
+ ),
194
+ });
195
+ }
196
+ return output;
197
+ } catch (error) {
198
+ if (error instanceof EventTransportError) throw error;
199
+ throw new EventTransportError({
200
+ eventName,
201
+ reason: "not-json-safe",
202
+ path,
203
+ message: `could not be inspected as transport data at ${path}.`,
204
+ cause: error,
205
+ });
206
+ } finally {
207
+ seen.delete(value);
208
+ }
209
+ }
210
+
211
+ export function eventTransportValuesEqual(
212
+ left: EventTransportValue,
213
+ right: EventTransportValue,
214
+ ): boolean {
215
+ if (left === right) return true;
216
+ if (left === null || right === null || typeof left !== typeof right) {
217
+ return false;
218
+ }
219
+ if (Array.isArray(left) || Array.isArray(right)) {
220
+ if (!Array.isArray(left) || !Array.isArray(right)) return false;
221
+ return (
222
+ left.length === right.length &&
223
+ left.every((value, index) =>
224
+ eventTransportValuesEqual(value, right[index]),
225
+ )
226
+ );
227
+ }
228
+ if (typeof left !== "object" || typeof right !== "object") return false;
229
+
230
+ const leftRecord = left as Readonly<Record<string, EventTransportValue>>;
231
+ const rightRecord = right as Readonly<Record<string, EventTransportValue>>;
232
+ const leftKeys = Object.keys(leftRecord).sort();
233
+ const rightKeys = Object.keys(rightRecord).sort();
234
+ return (
235
+ leftKeys.length === rightKeys.length &&
236
+ leftKeys.every(
237
+ (key, index) =>
238
+ key === rightKeys[index] &&
239
+ eventTransportValuesEqual(leftRecord[key], rightRecord[key]),
240
+ )
241
+ );
242
+ }
@@ -285,10 +285,6 @@ export function createMemoryLocksProvider(
285
285
 
286
286
  return createProvider({
287
287
  name,
288
- metadata: {
289
- ports: ["locks"],
290
- watchers: ["locks"],
291
- },
292
288
  setup({ ports }) {
293
289
  const instrumentation = createProviderInstrumentation(ports, {
294
290
  providerName: name,
@@ -436,20 +436,58 @@ export function contractsToOpenAPI(
436
436
  ],
437
437
  };
438
438
  const operationIds = new Map<string, string>();
439
+ const operations = new Map<
440
+ string,
441
+ { readonly contractName: string; readonly route: string }
442
+ >();
443
+ const pathShapes = new Map<
444
+ string,
445
+ {
446
+ readonly contractName: string;
447
+ readonly pathKey: string;
448
+ readonly route: string;
449
+ }
450
+ >();
439
451
 
440
452
  for (const contract of contracts) {
441
453
  const config = resolveContract(contract);
442
454
  assertValidContractLifecycle(config);
455
+ const pathTemplate = parsePathTemplate(config.path);
456
+ const pathKey = pathTemplate.openApiPath;
457
+ const route = `${config.method.toUpperCase()} ${config.path}`;
458
+ const operationRoute = `${config.method.toUpperCase()} ${pathKey}`;
459
+ const conflictingOperation = operations.get(operationRoute);
460
+ if (conflictingOperation) {
461
+ throw new Error(
462
+ `Duplicate OpenAPI operation: ${operationRoute} is produced by both contract "${conflictingOperation.contractName}" (${conflictingOperation.route}) and contract "${config.name}" (${route}). Each method + normalized path combination must be unique within an OpenAPI document.`,
463
+ );
464
+ }
465
+ const conflictingPathShape = pathShapes.get(pathTemplate.shapeKey);
466
+ if (conflictingPathShape && conflictingPathShape.pathKey !== pathKey) {
467
+ throw new Error(
468
+ `Ambiguous OpenAPI path: ${pathKey} from contract "${config.name}" (${route}) conflicts with ${conflictingPathShape.pathKey} from contract "${conflictingPathShape.contractName}" (${conflictingPathShape.route}). Templated paths with the same hierarchy must use the same parameter names within an OpenAPI document.`,
469
+ );
470
+ }
443
471
  const operationId = getContractOperationId(config);
444
- const route = `${config.method} ${config.path}`;
445
472
  const conflictingRoute = operationIds.get(operationId);
446
473
  if (conflictingRoute) {
447
474
  throw new Error(
448
475
  `Duplicate OpenAPI operationId: "${operationId}" is used by both ${conflictingRoute} and ${route}. Operation IDs must be unique within an OpenAPI document.`,
449
476
  );
450
477
  }
478
+ operations.set(operationRoute, {
479
+ contractName: config.name,
480
+ route,
481
+ });
482
+ if (!conflictingPathShape) {
483
+ pathShapes.set(pathTemplate.shapeKey, {
484
+ contractName: config.name,
485
+ pathKey,
486
+ route,
487
+ });
488
+ }
451
489
  operationIds.set(operationId, route);
452
- addContractToPaths(config, paths, state);
490
+ addContractToPaths(config, pathKey, paths, state);
453
491
  }
454
492
 
455
493
  const openapi: OpenAPIObject = {
@@ -481,10 +519,10 @@ export function contractsToOpenAPI(
481
519
  */
482
520
  function addContractToPaths(
483
521
  contract: AnyContract,
522
+ pathKey: string,
484
523
  paths: PathsObject,
485
524
  state: GeneratorState,
486
525
  ): void {
487
- const pathKey = parsePathTemplate(contract.path).openApiPath;
488
526
  if (!paths[pathKey]) {
489
527
  paths[pathKey] = {};
490
528
  }