@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.
Files changed (42) hide show
  1. package/AUTHORING.md +24 -0
  2. package/CHANGELOG.md +11 -0
  3. package/README.md +23 -2
  4. package/SUBMISSION.md +2 -1
  5. package/bin/apifuse-check.ts +60 -6
  6. package/bin/apifuse-dev.ts +48 -5
  7. package/bin/apifuse-perf.ts +106 -26
  8. package/bin/apifuse-record.ts +142 -52
  9. package/bin/apifuse-submit-check.ts +1489 -3
  10. package/package.json +107 -92
  11. package/src/ceremonies/index.ts +8 -2
  12. package/src/choice-token.ts +1 -0
  13. package/src/cli/commands.ts +10 -8
  14. package/src/cli/create.ts +49 -1
  15. package/src/cli/templates/provider/.dockerignore.tpl +22 -0
  16. package/src/cli/templates/provider/.gitignore.tpl +22 -0
  17. package/src/cli/templates/provider/README.md.tpl +18 -0
  18. package/src/cli/templates/provider/operations/ping.ts.tpl +3 -2
  19. package/src/cli/templates/provider/schemas/ping.ts.tpl +8 -0
  20. package/src/config/loader.ts +19 -1
  21. package/src/contract-json.ts +75 -0
  22. package/src/contract-serialization.ts +89 -0
  23. package/src/contract-types.ts +52 -0
  24. package/src/contract.ts +215 -0
  25. package/src/define.ts +40 -5
  26. package/src/errors.ts +15 -0
  27. package/src/i18n/catalog.ts +156 -0
  28. package/src/index.ts +22 -1
  29. package/src/lint.ts +265 -46
  30. package/src/provider.ts +45 -2
  31. package/src/runtime/browser.ts +685 -30
  32. package/src/runtime/cache.ts +35 -89
  33. package/src/runtime/choice.ts +760 -0
  34. package/src/runtime/executor.ts +19 -2
  35. package/src/runtime/redis.ts +116 -0
  36. package/src/runtime/state.ts +487 -0
  37. package/src/runtime/stealth.ts +8 -1
  38. package/src/runtime/trace.ts +1 -1
  39. package/src/server/serve.ts +361 -46
  40. package/src/server/types.ts +2 -0
  41. package/src/testing/run.ts +16 -3
  42. package/src/types.ts +225 -18
@@ -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
- export async function main() {
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
- console.log(`[apifuse record] Calling ${operationName} on ${provider.id}...`);
40
+ Calls a real upstream-backed operation through ctx.http or ctx.stealth and writes __fixtures__/raw.json.
47
41
 
48
- const result = await executeOperation(
49
- provider,
50
- operationName,
51
- capture.ctx,
52
- parsedParams,
53
- );
54
- const captured = capture.getCapturedRaw();
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
- if (captured === undefined) {
57
- throw new Error(
58
- `No upstream response was captured for ${provider.id}.${operationName}.`,
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
- const rawPayload = args.sanitize ? sanitizeFixture(captured) : captured;
63
- const fixturePath = resolve(location.rootDir, "__fixtures__", "raw.json");
64
- const nextPayload = await prepareFixturePayload(
65
- fixturePath,
66
- rawPayload,
67
- args.append,
68
- );
67
+ console.log(
68
+ `[apifuse record] Calling ${operationName} on ${provider.id}...`,
69
+ );
69
70
 
70
- await mkdir(dirname(fixturePath), { recursive: true });
71
- await writeFile(fixturePath, `${JSON.stringify(nextPayload, null, 2)}\n`);
71
+ const result = await executeOperation(
72
+ provider,
73
+ operationName,
74
+ capture.ctx,
75
+ parsedParams,
76
+ );
77
+ const captured = capture.getCapturedRaw();
72
78
 
73
- console.log(
74
- `[apifuse record] Captured response (${formatBytes(
75
- Buffer.byteLength(JSON.stringify(rawPayload)),
76
- )})`,
77
- );
78
- console.log(
79
- `[apifuse record] Saved to ${relative(process.cwd(), fixturePath)}`,
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
- void result;
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
- get: (key) => process.env[key],
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 {