@superdurable/connectors-sdk 0.0.0-stage → 0.1.0

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.
@@ -0,0 +1,350 @@
1
+ import {
2
+ validateMutationDefinition,
3
+ validateQueryDefinition
4
+ } from "../chunk-2NBM7P4T.js";
5
+ import {
6
+ DefectBranchID,
7
+ FailureSchema,
8
+ UncertainBranchID,
9
+ newMutationBranch,
10
+ newMutationRetry,
11
+ newMutationUncertain,
12
+ newQueryBranch,
13
+ newQueryRetry,
14
+ zeroReceipt
15
+ } from "../chunk-SOXYPIMO.js";
16
+
17
+ // src/connectormock/index.ts
18
+ import { readFile } from "fs/promises";
19
+ import { isDeepStrictEqual } from "util";
20
+ import { z } from "zod";
21
+
22
+ // src/connectormock/scripted.ts
23
+ var Script = class {
24
+ rules = [];
25
+ queue = [];
26
+ defaults = [];
27
+ defaultCount = 0;
28
+ nextScripted(input) {
29
+ for (const rule of this.rules) if (rule.matches(input)) return rule.response;
30
+ return this.queue.shift();
31
+ }
32
+ nextDefault() {
33
+ if (!this.defaults.length) return void 0;
34
+ return this.defaults[Math.min(this.defaultCount++, this.defaults.length - 1)];
35
+ }
36
+ };
37
+ var FlowScript = class {
38
+ #owner;
39
+ #script;
40
+ /** Capture one Flow prefix and its owning operation. */
41
+ constructor(owner, script) {
42
+ this.#owner = owner;
43
+ this.#script = script;
44
+ }
45
+ /** Queue one case per matching call. */
46
+ respond(...cases) {
47
+ this.#owner.addResponses(this.#script, cases);
48
+ return this;
49
+ }
50
+ /** Use a persistent input predicate before queued cases. */
51
+ when(matches, response) {
52
+ this.#owner.addRule(this.#script, matches, response);
53
+ return this;
54
+ }
55
+ /** Replace fallback cases; the last case repeats. */
56
+ default(...cases) {
57
+ this.#owner.setDefaults(this.#script, cases);
58
+ return this;
59
+ }
60
+ };
61
+ var ScriptedOperation = class {
62
+ #definition;
63
+ #isMutation;
64
+ #options;
65
+ #unscoped = new Script();
66
+ #flowScripts = /* @__PURE__ */ new Map();
67
+ #terminalByKey = /* @__PURE__ */ new Map();
68
+ #calls = [];
69
+ #failures = [];
70
+ #isStrict = false;
71
+ /** Validate the real operation definition and capture test checks. */
72
+ constructor(definition, isMutation, options = {}) {
73
+ if (isMutation) validateMutationDefinition(definition);
74
+ else validateQueryDefinition(definition);
75
+ if (options.answerCheck && !definition.branches.some((branch) => branch.id === options.answerCheck?.branch)) throw new TypeError("mock answer check branch is not declared");
76
+ this.#definition = definition;
77
+ this.#isMutation = isMutation;
78
+ this.#options = options;
79
+ }
80
+ /** Queue cases on the unscoped script. */
81
+ respond(...cases) {
82
+ this.addResponses(this.#unscoped, cases);
83
+ return this;
84
+ }
85
+ /** Add a persistent input predicate on the unscoped script. */
86
+ when(matches, response) {
87
+ this.addRule(this.#unscoped, matches, response);
88
+ return this;
89
+ }
90
+ /** Replace unscoped fallback cases; the last repeats. */
91
+ default(...cases) {
92
+ this.setDefaults(this.#unscoped, cases);
93
+ return this;
94
+ }
95
+ /** Require a Respond or When case for every call, ignoring all defaults. */
96
+ strict() {
97
+ this.#isStrict = true;
98
+ return this;
99
+ }
100
+ /** Select a script by exact Flow ID or prefix; longest prefixes win. */
101
+ forFlow(flowIdOrPrefix) {
102
+ if (!flowIdOrPrefix.trim()) throw new TypeError("mock ForFlow requires a Flow ID or prefix");
103
+ const existing = this.#flowScripts.get(flowIdOrPrefix);
104
+ if (existing) return existing.publicScript;
105
+ const script = new Script();
106
+ const publicScript = new FlowScript(this, script);
107
+ this.#flowScripts.set(flowIdOrPrefix, { script, publicScript });
108
+ return publicScript;
109
+ }
110
+ /** Copy answered calls in answer order, including retries and replays. */
111
+ calls() {
112
+ return [...this.#calls];
113
+ }
114
+ /** Copy scripting errors; callers can assert after a real Flow completes. */
115
+ failures() {
116
+ return [...this.#failures];
117
+ }
118
+ /** Throw after integration verification if a script was invalid or unscripted. */
119
+ assertNoFailures() {
120
+ if (this.#failures.length) throw new Error(this.#failures.join("\n"));
121
+ }
122
+ /** Add validated ordered responses to a script. */
123
+ addResponses(script, cases) {
124
+ for (const response of cases) this.#reportCaseProblem(response);
125
+ script.queue.push(...cases);
126
+ }
127
+ /** Add a validated persistent predicate to a script. */
128
+ addRule(script, matches, response) {
129
+ if (!matches) throw new TypeError("mock When requires an input predicate");
130
+ this.#reportCaseProblem(response);
131
+ script.rules.push({ matches, response });
132
+ }
133
+ /** Replace a script's fallback cases. */
134
+ setDefaults(script, cases) {
135
+ for (const response of cases) this.#reportCaseProblem(response);
136
+ script.defaults = [...cases];
137
+ script.defaultCount = 0;
138
+ }
139
+ /** Answer one real SDK invocation and record its safe outcome. */
140
+ answer(call, input, idempotencyKey) {
141
+ const flowId = call.context.flowId;
142
+ let resolved;
143
+ const requestFailure = this.#options.requestCheck?.(input);
144
+ if (requestFailure) {
145
+ resolved = { response: { kind: "branch", branch: DefectBranchID, value: void 0, failure: this.#completeFailure(requestFailure) }, branch: DefectBranchID };
146
+ } else {
147
+ const remembered = idempotencyKey ? this.#terminalByKey.get(idempotencyKey) : void 0;
148
+ if (remembered) resolved = { ...remembered, isReplay: true };
149
+ else {
150
+ resolved = this.#selectCase(flowId, input);
151
+ const answerCheck = this.#options.answerCheck;
152
+ if (answerCheck && resolved.response.kind === "branch" && resolved.branch === answerCheck.branch) {
153
+ try {
154
+ answerCheck.check(input, resolved.response.value);
155
+ } catch {
156
+ this.#reportFailure("connector mock answer does not match request");
157
+ resolved = this.#defectCase("connector mock answer does not match request");
158
+ }
159
+ }
160
+ if (idempotencyKey && resolved.response.kind !== "retry") this.#terminalByKey.set(idempotencyKey, resolved);
161
+ }
162
+ }
163
+ const response = resolved.response;
164
+ const record = {
165
+ connectorId: this.#definition.operation.connectorId,
166
+ operationId: this.#definition.operation.operationId,
167
+ connectionName: call.connection.name,
168
+ flowId,
169
+ callId: call.id,
170
+ input,
171
+ ...call.idempotencyKey ? { idempotencyKey: call.idempotencyKey } : {},
172
+ ...resolved.branch ? { branch: resolved.branch } : {},
173
+ ...response.kind === "retry" ? { retry: true } : {},
174
+ ...resolved.isReplay ? { replay: true } : {},
175
+ ...resolved.isUnscripted ? { unscripted: true } : {}
176
+ };
177
+ this.#calls.push(record);
178
+ return resolved;
179
+ }
180
+ #selectCase(flowId, input) {
181
+ const scripts = [...this.#flowScripts.entries()].filter(([prefix]) => flowId.startsWith(prefix)).sort(([left], [right]) => right.length - left.length).map(([, entry]) => entry.script);
182
+ scripts.push(this.#unscoped);
183
+ for (const script of scripts) {
184
+ const response = script.nextScripted(input);
185
+ if (response) return this.#resolveCase(response);
186
+ }
187
+ if (!this.#isStrict) for (const script of scripts) {
188
+ const response = script.nextDefault();
189
+ if (response) return this.#resolveCase(response);
190
+ }
191
+ this.#reportFailure(`unscripted call for Flow ${flowId}`);
192
+ return { ...this.#defectCase("unscripted connector mock call"), isUnscripted: true };
193
+ }
194
+ #resolveCase(response) {
195
+ const problem = this.#caseProblem(response);
196
+ if (problem) return this.#defectCase(problem);
197
+ if (response.kind === "retry") return { response: { ...response, failure: this.#completeFailure(response.failure) } };
198
+ if (response.kind === "uncertain") return { response: { ...response, failure: this.#completeFailure(response.failure) }, branch: UncertainBranchID };
199
+ return { response: response.failure ? { ...response, failure: this.#completeFailure(response.failure) } : response, branch: response.branch };
200
+ }
201
+ #defectCase(message) {
202
+ return { response: { kind: "branch", branch: DefectBranchID, value: void 0, failure: { kind: "LOCAL_DEFECT", provider: this.#definition.operation.connectorId, operation: this.#definition.operation.operationId, message } }, branch: DefectBranchID };
203
+ }
204
+ #reportCaseProblem(response) {
205
+ const problem = this.#caseProblem(response);
206
+ if (problem) this.#reportFailure(problem);
207
+ }
208
+ #caseProblem(response) {
209
+ if (response.kind === "branch" && !this.#definition.branches.some((branch) => branch.id === response.branch)) return `branch ${response.branch} is not declared by the operation`;
210
+ if (response.kind === "uncertain" && (!this.#isMutation || !this.#definition.branches.some((branch) => branch.id === UncertainBranchID))) return "an uncertain case requires a mutation uncertain branch";
211
+ if (response.kind === "retry" && (!Number.isSafeInteger(response.retryAfterMs) || response.retryAfterMs < 0)) return "the retry delay is invalid";
212
+ if ("failure" in response && response.failure && !FailureSchema.safeParse(this.#completeFailure(response.failure)).success) return "the case failure is invalid";
213
+ return void 0;
214
+ }
215
+ #completeFailure(failure) {
216
+ return { ...failure, provider: failure.provider || this.#definition.operation.connectorId, operation: failure.operation || this.#definition.operation.operationId };
217
+ }
218
+ #reportFailure(message) {
219
+ const safeMessage = `connector mock ${this.#definition.operation.connectorId}.${this.#definition.operation.operationId}: ${message}`;
220
+ this.#failures.push(safeMessage);
221
+ this.#options.onFailure?.(safeMessage);
222
+ }
223
+ };
224
+ var MockQuery = class extends ScriptedOperation {
225
+ #definition;
226
+ /** Construct from the real query definition. */
227
+ constructor(definition, options) {
228
+ super(definition, false, options);
229
+ this.#definition = definition;
230
+ }
231
+ /** Return the real query definition. */
232
+ definition() {
233
+ return this.#definition;
234
+ }
235
+ /** Answer one query call from the script. */
236
+ invoke(call, input) {
237
+ const response = this.answer(call, input, "").response;
238
+ if (response.kind === "retry") return newQueryRetry(response.failure, response.retryAfterMs);
239
+ if (response.kind === "uncertain") return newQueryBranch(DefectBranchID, void 0, zeroReceipt(), { kind: "LOCAL_DEFECT", provider: call.operation.connectorId, operation: call.operation.operationId, message: "query mock cannot return uncertainty" });
240
+ return newQueryBranch(response.branch, response.value, zeroReceipt(), response.failure);
241
+ }
242
+ };
243
+ var MockMutation = class extends ScriptedOperation {
244
+ #definition;
245
+ /** Construct from the real mutation definition. */
246
+ constructor(definition, options) {
247
+ super(definition, true, options);
248
+ this.#definition = definition;
249
+ }
250
+ /** Return the real mutation definition. */
251
+ definition() {
252
+ return this.#definition;
253
+ }
254
+ /** Use the stable SDK call ID as the provider deduplication key. */
255
+ idempotencyKey(callId) {
256
+ return callId;
257
+ }
258
+ /** Answer one mutation call and remember terminal outcomes by key. */
259
+ invoke(call, input) {
260
+ const response = this.answer(call, input, call.idempotencyKey ?? "").response;
261
+ if (response.kind === "retry") return newMutationRetry(response.failure, response.retryAfterMs);
262
+ if (response.kind === "uncertain") return newMutationUncertain(response.value, response.failure, zeroReceipt());
263
+ return newMutationBranch(response.branch, response.value, zeroReceipt(), response.failure);
264
+ }
265
+ };
266
+ function newQuery(definition, options) {
267
+ return new MockQuery(definition, options);
268
+ }
269
+ function newMutation(definition, options) {
270
+ return new MockMutation(definition, options);
271
+ }
272
+
273
+ // src/connectormock/index.ts
274
+ var ReportDirectoryEnvironment = "DEX_CONNECTOR_MOCK_REPORT_DIR";
275
+ var ReportFileName = "connector-calls.json";
276
+ function branchCase(branch, value, failure) {
277
+ if (!branch) throw new TypeError("mock branch is required");
278
+ return failure ? { kind: "branch", branch, value, failure: FailureSchema.parse(failure) } : { kind: "branch", branch, value };
279
+ }
280
+ function retryCase(failure, retryAfterMs = 0) {
281
+ if (!Number.isSafeInteger(retryAfterMs) || retryAfterMs < 0) throw new RangeError("mock retry delay is invalid");
282
+ return { kind: "retry", failure: FailureSchema.parse(failure), retryAfterMs };
283
+ }
284
+ function uncertainCase(value, failure) {
285
+ return { kind: "uncertain", branch: UncertainBranchID, value, failure: FailureSchema.parse(failure) };
286
+ }
287
+ var RecordedCallSchema = z.strictObject({
288
+ connectorId: z.string().min(1),
289
+ operationId: z.string().min(1),
290
+ connectionName: z.string().min(1),
291
+ flowId: z.string(),
292
+ callId: z.uuid(),
293
+ branch: z.string().optional(),
294
+ retry: z.boolean().optional(),
295
+ replay: z.boolean().optional(),
296
+ unscripted: z.boolean().optional()
297
+ });
298
+ var ReportRowSchema = z.strictObject({
299
+ connection: z.string(),
300
+ operation: z.string(),
301
+ branch: z.string().optional(),
302
+ connectorId: z.string(),
303
+ flowId: z.string().optional(),
304
+ callId: z.string().optional(),
305
+ retry: z.boolean().optional(),
306
+ replay: z.boolean().optional(),
307
+ unscripted: z.boolean().optional()
308
+ });
309
+ var CallReportSchema = z.strictObject({ calls: z.array(ReportRowSchema) });
310
+ function decodeOutput(encoded, outputSchema) {
311
+ let document;
312
+ try {
313
+ document = JSON.parse(encoded);
314
+ } catch {
315
+ throw new TypeError("mock output fixture is not valid JSON");
316
+ }
317
+ const parsed = outputSchema.safeParse(document);
318
+ if (!parsed.success || !isDeepStrictEqual(parsed.data, document)) throw new TypeError("mock output fixture violates the output schema");
319
+ return parsed.data;
320
+ }
321
+ async function loadOutputFixture(path, outputSchema) {
322
+ return decodeOutput(await readFile(path, "utf8"), outputSchema);
323
+ }
324
+ function decodeCallReport(encoded) {
325
+ let document;
326
+ try {
327
+ document = JSON.parse(encoded);
328
+ } catch {
329
+ throw new TypeError("connector call report is not valid JSON");
330
+ }
331
+ return CallReportSchema.parse(document);
332
+ }
333
+ export {
334
+ CallReportSchema,
335
+ FlowScript,
336
+ MockMutation,
337
+ MockQuery,
338
+ RecordedCallSchema,
339
+ ReportDirectoryEnvironment,
340
+ ReportFileName,
341
+ ReportRowSchema,
342
+ branchCase,
343
+ decodeCallReport,
344
+ decodeOutput,
345
+ loadOutputFixture,
346
+ newMutation,
347
+ newQuery,
348
+ retryCase,
349
+ uncertainCase
350
+ };