@apifuse/provider-sdk 2.1.0-beta.4 → 2.1.0-beta.6
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/AUTHORING.md +24 -0
- package/CHANGELOG.md +11 -0
- package/README.md +23 -2
- package/SUBMISSION.md +2 -1
- package/bin/apifuse-check.ts +60 -6
- package/bin/apifuse-dev.ts +48 -5
- package/bin/apifuse-perf.ts +106 -26
- package/bin/apifuse-record.ts +142 -52
- package/bin/apifuse-submit-check.ts +1489 -3
- package/package.json +107 -92
- package/src/ceremonies/index.ts +8 -2
- package/src/choice-token.ts +1 -0
- package/src/cli/commands.ts +10 -8
- package/src/cli/create.ts +49 -1
- package/src/cli/templates/provider/.dockerignore.tpl +22 -0
- package/src/cli/templates/provider/.gitignore.tpl +22 -0
- package/src/cli/templates/provider/README.md.tpl +18 -0
- package/src/cli/templates/provider/operations/ping.ts.tpl +3 -2
- package/src/cli/templates/provider/schemas/ping.ts.tpl +8 -0
- package/src/config/loader.ts +19 -1
- package/src/contract-json.ts +75 -0
- package/src/contract-serialization.ts +89 -0
- package/src/contract-types.ts +52 -0
- package/src/contract.ts +215 -0
- package/src/define.ts +40 -5
- package/src/errors.ts +15 -0
- package/src/i18n/catalog.ts +156 -0
- package/src/index.ts +22 -1
- package/src/lint.ts +265 -46
- package/src/provider.ts +45 -2
- package/src/runtime/browser.ts +685 -30
- package/src/runtime/cache.ts +35 -89
- package/src/runtime/choice.ts +760 -0
- package/src/runtime/executor.ts +19 -2
- package/src/runtime/redis.ts +116 -0
- package/src/runtime/state.ts +487 -0
- package/src/runtime/stealth.ts +8 -1
- package/src/runtime/trace.ts +1 -1
- package/src/server/serve.ts +361 -46
- package/src/server/types.ts +2 -0
- package/src/testing/run.ts +16 -3
- package/src/types.ts +225 -18
package/bin/apifuse-record.ts
CHANGED
|
@@ -9,14 +9,19 @@ import { pathToFileURL } from "node:url";
|
|
|
9
9
|
import {
|
|
10
10
|
createBypassProviderCache,
|
|
11
11
|
createHttpClient,
|
|
12
|
+
createProviderChoiceContext,
|
|
12
13
|
createStealthClient,
|
|
13
14
|
createSttClientFromEnv,
|
|
14
15
|
executeOperation,
|
|
15
16
|
type HttpClient,
|
|
16
17
|
type ProviderContext,
|
|
17
18
|
type ProviderDefinition,
|
|
19
|
+
ProviderError,
|
|
18
20
|
type StealthClient,
|
|
21
|
+
TransportError,
|
|
22
|
+
ValidationError,
|
|
19
23
|
} from "../src";
|
|
24
|
+
import { createMemoryProviderRuntimeState } from "../src/runtime/state";
|
|
20
25
|
|
|
21
26
|
type CliArgs = {
|
|
22
27
|
append: boolean;
|
|
@@ -30,56 +35,77 @@ type ProviderRuntime = ProviderDefinition;
|
|
|
30
35
|
|
|
31
36
|
type MutableRecord = Record<string, unknown>;
|
|
32
37
|
|
|
33
|
-
|
|
34
|
-
const args = parseArgs(normalizeArgs(process.argv.slice(2)));
|
|
35
|
-
const location = resolveProviderLocation(args.providerPath);
|
|
36
|
-
const provider = await loadProvider(location.rootDir);
|
|
37
|
-
const operationName = resolveOperationName(provider, args.operation);
|
|
38
|
-
const operation = provider.operations[operationName];
|
|
39
|
-
const parsedParams = parseParams(operation, args.params);
|
|
40
|
-
|
|
41
|
-
const capture = createCaptureContext(
|
|
42
|
-
provider,
|
|
43
|
-
resolveOperationBaseUrl(provider, operationName),
|
|
44
|
-
);
|
|
38
|
+
const HELP_TEXT = `Usage: apifuse record [path] --operation <operation> --params '<json>'
|
|
45
39
|
|
|
46
|
-
|
|
40
|
+
Calls a real upstream-backed operation through ctx.http or ctx.stealth and writes __fixtures__/raw.json.
|
|
47
41
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
42
|
+
Options:
|
|
43
|
+
--operation, -o <name> operation to call
|
|
44
|
+
--params, -p <json> JSON input passed to the operation (default: {})
|
|
45
|
+
--append append to an existing array fixture
|
|
46
|
+
--sanitize redact common token/header fields (default)
|
|
47
|
+
--no-sanitize write the captured upstream payload as-is
|
|
48
|
+
--help, -h show this help
|
|
55
49
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
50
|
+
Example:
|
|
51
|
+
apifuse record providers/korea-air-quality --operation realtime --params '{"stationName":"jongno"}'`;
|
|
52
|
+
|
|
53
|
+
export async function main() {
|
|
54
|
+
try {
|
|
55
|
+
const args = parseArgs(normalizeArgs(process.argv.slice(2)));
|
|
56
|
+
const location = resolveProviderLocation(args.providerPath);
|
|
57
|
+
const provider = await loadProvider(location.rootDir);
|
|
58
|
+
const operationName = resolveOperationName(provider, args.operation);
|
|
59
|
+
const operation = provider.operations[operationName];
|
|
60
|
+
const parsedParams = parseParams(operation, args.params);
|
|
61
|
+
|
|
62
|
+
const capture = createCaptureContext(
|
|
63
|
+
provider,
|
|
64
|
+
resolveOperationBaseUrl(provider, operationName),
|
|
59
65
|
);
|
|
60
|
-
}
|
|
61
66
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
fixturePath,
|
|
66
|
-
rawPayload,
|
|
67
|
-
args.append,
|
|
68
|
-
);
|
|
67
|
+
console.log(
|
|
68
|
+
`[apifuse record] Calling ${operationName} on ${provider.id}...`,
|
|
69
|
+
);
|
|
69
70
|
|
|
70
|
-
|
|
71
|
-
|
|
71
|
+
const result = await executeOperation(
|
|
72
|
+
provider,
|
|
73
|
+
operationName,
|
|
74
|
+
capture.ctx,
|
|
75
|
+
parsedParams,
|
|
76
|
+
);
|
|
77
|
+
const captured = capture.getCapturedRaw();
|
|
72
78
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
79
|
+
if (captured === undefined) {
|
|
80
|
+
throw new Error(
|
|
81
|
+
`No upstream response was captured for ${provider.id}.${operationName}.`,
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const rawPayload = args.sanitize ? sanitizeFixture(captured) : captured;
|
|
86
|
+
const fixturePath = resolve(location.rootDir, "__fixtures__", "raw.json");
|
|
87
|
+
const nextPayload = await prepareFixturePayload(
|
|
88
|
+
fixturePath,
|
|
89
|
+
rawPayload,
|
|
90
|
+
args.append,
|
|
91
|
+
);
|
|
81
92
|
|
|
82
|
-
|
|
93
|
+
await mkdir(dirname(fixturePath), { recursive: true });
|
|
94
|
+
await writeFile(fixturePath, `${JSON.stringify(nextPayload, null, 2)}\n`);
|
|
95
|
+
|
|
96
|
+
console.log(
|
|
97
|
+
`[apifuse record] Captured response (${formatBytes(
|
|
98
|
+
Buffer.byteLength(JSON.stringify(rawPayload)),
|
|
99
|
+
)})`,
|
|
100
|
+
);
|
|
101
|
+
console.log(
|
|
102
|
+
`[apifuse record] Saved to ${relative(process.cwd(), fixturePath)}`,
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
void result;
|
|
106
|
+
} catch (error) {
|
|
107
|
+
handleCliError(error);
|
|
108
|
+
}
|
|
83
109
|
}
|
|
84
110
|
|
|
85
111
|
function normalizeArgs(argv: string[]): string[] {
|
|
@@ -107,6 +133,11 @@ function parseArgs(argv: string[]): CliArgs {
|
|
|
107
133
|
continue;
|
|
108
134
|
}
|
|
109
135
|
|
|
136
|
+
if (arg === "--help" || arg === "-h") {
|
|
137
|
+
console.log(HELP_TEXT);
|
|
138
|
+
process.exit(0);
|
|
139
|
+
}
|
|
140
|
+
|
|
110
141
|
if (arg.startsWith("--operation=")) {
|
|
111
142
|
operation = arg.slice("--operation=".length);
|
|
112
143
|
continue;
|
|
@@ -158,6 +189,43 @@ function parseArgs(argv: string[]): CliArgs {
|
|
|
158
189
|
return { append, providerPath, operation, params, sanitize };
|
|
159
190
|
}
|
|
160
191
|
|
|
192
|
+
function handleCliError(error: unknown): never {
|
|
193
|
+
const message = formatCliError(error);
|
|
194
|
+
console.error(`[apifuse record] ${message}`);
|
|
195
|
+
process.exit(1);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function formatCliError(error: unknown): string {
|
|
199
|
+
if (error instanceof TransportError) {
|
|
200
|
+
return [
|
|
201
|
+
error.message,
|
|
202
|
+
error.upstreamStatus ? `status=${error.upstreamStatus}` : undefined,
|
|
203
|
+
error.options?.retryable !== undefined
|
|
204
|
+
? `retryable=${String(error.options.retryable)}`
|
|
205
|
+
: undefined,
|
|
206
|
+
error.fix ? `fix=${error.fix}` : undefined,
|
|
207
|
+
]
|
|
208
|
+
.filter(Boolean)
|
|
209
|
+
.join(" ");
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (error instanceof ProviderError || error instanceof ValidationError) {
|
|
213
|
+
return [
|
|
214
|
+
error.message,
|
|
215
|
+
error.code ? `code=${error.code}` : undefined,
|
|
216
|
+
error.fix,
|
|
217
|
+
]
|
|
218
|
+
.filter(Boolean)
|
|
219
|
+
.join(" ");
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (error instanceof Error) {
|
|
223
|
+
return error.message;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return String(error);
|
|
227
|
+
}
|
|
228
|
+
|
|
161
229
|
function resolveProviderLocation(inputPath?: string) {
|
|
162
230
|
const originalInput = inputPath ?? process.cwd();
|
|
163
231
|
const resolvedInput = resolve(process.cwd(), originalInput);
|
|
@@ -285,25 +353,40 @@ function createCaptureContext(provider: ProviderRuntime, baseUrl: string) {
|
|
|
285
353
|
},
|
|
286
354
|
);
|
|
287
355
|
|
|
356
|
+
const env = {
|
|
357
|
+
get: (key: string) => process.env[key],
|
|
358
|
+
};
|
|
359
|
+
const credential = {
|
|
360
|
+
mode: "none" as const,
|
|
361
|
+
get: () => undefined,
|
|
362
|
+
getAll: () => ({}),
|
|
363
|
+
getAccessToken: () => undefined,
|
|
364
|
+
getScopes: () => [],
|
|
365
|
+
};
|
|
366
|
+
const state = createMemoryProviderRuntimeState();
|
|
288
367
|
const ctx: ProviderContext = {
|
|
289
|
-
env
|
|
290
|
-
|
|
291
|
-
},
|
|
292
|
-
credential: {
|
|
293
|
-
mode: "none",
|
|
294
|
-
get: () => undefined,
|
|
295
|
-
getAll: () => ({}),
|
|
296
|
-
getAccessToken: () => undefined,
|
|
297
|
-
getScopes: () => [],
|
|
298
|
-
},
|
|
368
|
+
env,
|
|
369
|
+
credential,
|
|
370
|
+
request: { headers: {} },
|
|
299
371
|
http,
|
|
300
372
|
cache: createBypassProviderCache({ providerId: provider.id }),
|
|
373
|
+
state,
|
|
301
374
|
stealth,
|
|
302
375
|
browser: {
|
|
303
376
|
engine: "playwright-stealth",
|
|
377
|
+
close: async () => {},
|
|
304
378
|
newPage: async () => {
|
|
305
379
|
throw new Error("Browser client is not available in apifuse record.");
|
|
306
380
|
},
|
|
381
|
+
rawPage: async () => {
|
|
382
|
+
throw new Error("Browser client is not available in apifuse record.");
|
|
383
|
+
},
|
|
384
|
+
withIsolatedContext: async () => {
|
|
385
|
+
throw new Error("Browser client is not available in apifuse record.");
|
|
386
|
+
},
|
|
387
|
+
solveChallenge: async () => {
|
|
388
|
+
throw new Error("Browser client is not available in apifuse record.");
|
|
389
|
+
},
|
|
307
390
|
},
|
|
308
391
|
trace: {
|
|
309
392
|
span: async (_name, fn) => fn(),
|
|
@@ -314,6 +397,13 @@ function createCaptureContext(provider: ProviderRuntime, baseUrl: string) {
|
|
|
314
397
|
},
|
|
315
398
|
},
|
|
316
399
|
stt: createSttClientFromEnv(provider.stt),
|
|
400
|
+
choice: createProviderChoiceContext({
|
|
401
|
+
providerId: provider.id,
|
|
402
|
+
env,
|
|
403
|
+
request: { headers: {} },
|
|
404
|
+
credential,
|
|
405
|
+
state,
|
|
406
|
+
}),
|
|
317
407
|
};
|
|
318
408
|
|
|
319
409
|
return {
|