@crvouga/mockingbird-service-rxvortex 1.0.0 → 1.1.1
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/CHANGELOG.md +13 -4
- package/README.md +15 -5
- package/dist/{chunk-DNELVPK4.js → chunk-5KK6W2PE.js} +229 -51
- package/dist/chunk-5KK6W2PE.js.map +7 -0
- package/dist/{chunk-ZAM35JQO.js → chunk-XAWKEXKJ.js} +40 -4
- package/dist/chunk-XAWKEXKJ.js.map +7 -0
- package/dist/cli.js +2 -2
- package/dist/index.d.ts +160 -103
- package/dist/index.js +5 -1
- package/dist/server.d.ts +272 -193
- package/dist/server.js +4 -2
- package/package.json +2 -2
- package/dist/chunk-DNELVPK4.js.map +0 -7
- package/dist/chunk-ZAM35JQO.js.map +0 -7
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
|
-
createRuntime
|
|
3
|
-
|
|
2
|
+
createRuntime,
|
|
3
|
+
parseCatalog
|
|
4
|
+
} from "./chunk-5KK6W2PE.js";
|
|
5
|
+
|
|
6
|
+
// src/server.ts
|
|
7
|
+
import { readFileSync } from "node:fs";
|
|
4
8
|
|
|
5
9
|
// ../../adapters/node/dist/cli.js
|
|
6
10
|
import { readFile } from "node:fs/promises";
|
|
@@ -208,7 +212,9 @@ var formatLog = (format) => {
|
|
|
208
212
|
const ns = entry.namespace === "default" ? "" : ` [${entry.namespace}]`;
|
|
209
213
|
const fault = entry.faultId ? ` fault=${entry.faultId}` : "";
|
|
210
214
|
const adopted = entry.adopted ? " adopted" : "";
|
|
211
|
-
|
|
215
|
+
const received = entry.request ? ` received=${entry.request.bodyBytes === null ? "?" : `${entry.request.bodyBytes}B`} ct=${entry.request.contentType ?? "-"}${entry.request.transferEncoding ? ` te=${entry.request.transferEncoding}` : ""}` : "";
|
|
216
|
+
const issues = entry.issues?.length ? ` issues=${entry.issues.slice(0, 3).map((issue) => `${issue.path || "$"}: ${issue.message}`).join("; ")}${entry.issues.length > 3 ? ` (+${entry.issues.length - 3})` : ""}` : "";
|
|
217
|
+
console.log(`${entry.service} ${entry.method} ${entry.path} ${entry.status} ${op} ${entry.durationMs}ms${ns}${fault}${adopted}${received}${issues}`);
|
|
212
218
|
};
|
|
213
219
|
};
|
|
214
220
|
var asString = (value) => typeof value === "string" ? value : void 0;
|
|
@@ -318,6 +324,19 @@ var createServer2 = async (options = {}) => {
|
|
|
318
324
|
};
|
|
319
325
|
};
|
|
320
326
|
var text = (value) => typeof value === "string" ? value : void 0;
|
|
327
|
+
var loadCatalogFile = (path) => {
|
|
328
|
+
let raw;
|
|
329
|
+
try {
|
|
330
|
+
raw = readFileSync(path, "utf8");
|
|
331
|
+
} catch {
|
|
332
|
+
throw new Error(`rxvortex catalog not found: ${path}`);
|
|
333
|
+
}
|
|
334
|
+
try {
|
|
335
|
+
return parseCatalog(JSON.parse(raw));
|
|
336
|
+
} catch (error) {
|
|
337
|
+
throw new Error(`rxvortex catalog ${path}: ${error.message}`);
|
|
338
|
+
}
|
|
339
|
+
};
|
|
321
340
|
var serveTarget = {
|
|
322
341
|
name: "rxvortex",
|
|
323
342
|
defaultPort: DEFAULT_PORT,
|
|
@@ -337,6 +356,16 @@ var serveTarget = {
|
|
|
337
356
|
value: "<token>",
|
|
338
357
|
description: "A static bearer token to accept (the app's RXVORTEX_API_TOKEN)"
|
|
339
358
|
},
|
|
359
|
+
catalog: {
|
|
360
|
+
type: "string",
|
|
361
|
+
value: "<file.json>",
|
|
362
|
+
description: "Preset catalog every namespace starts with and resets to: a JSON array, or {data: [...]} as GET /api/v1/preset-catalog-items returns"
|
|
363
|
+
},
|
|
364
|
+
"unknown-presets": {
|
|
365
|
+
type: "string",
|
|
366
|
+
value: "<reject|accept>",
|
|
367
|
+
description: "An unknown preset_catalog_id: 422 like the sandbox (reject, default) or add it (accept)"
|
|
368
|
+
},
|
|
340
369
|
"auto-advance": {
|
|
341
370
|
type: "string",
|
|
342
371
|
value: "<ms:Status,Status,\u2026>",
|
|
@@ -348,13 +377,19 @@ var serveTarget = {
|
|
|
348
377
|
const secret = text(values["webhook-secret"]);
|
|
349
378
|
const token = text(values["api-token"]);
|
|
350
379
|
const auto = text(values["auto-advance"]);
|
|
380
|
+
const catalogPath = text(values.catalog);
|
|
381
|
+
const unknownPresets = text(values["unknown-presets"]);
|
|
382
|
+
if (unknownPresets !== void 0 && unknownPresets !== "reject" && unknownPresets !== "accept")
|
|
383
|
+
throw new Error('--unknown-presets must be "reject" or "accept"');
|
|
351
384
|
const plan = auto ? /^(\d+):(.+)$/.exec(auto) : null;
|
|
352
385
|
if (auto && !plan)
|
|
353
386
|
throw new Error('--auto-advance must look like "2000:Fill,Shipping,Delivered"');
|
|
354
387
|
return createRuntime({
|
|
355
388
|
tickMs: 100,
|
|
356
389
|
...url ? { webhooks: { url, ...secret ? { secret } : {} } } : {},
|
|
390
|
+
...catalogPath ? { catalog: loadCatalogFile(catalogPath) } : {},
|
|
357
391
|
settings: {
|
|
392
|
+
...unknownPresets ? { unknownPresets } : {},
|
|
358
393
|
...token ? { staticTokens: [token] } : {},
|
|
359
394
|
...plan ? {
|
|
360
395
|
autoAdvance: {
|
|
@@ -379,6 +414,7 @@ export {
|
|
|
379
414
|
serveCommand,
|
|
380
415
|
DEFAULT_PORT,
|
|
381
416
|
createServer2 as createServer,
|
|
417
|
+
loadCatalogFile,
|
|
382
418
|
serveTarget
|
|
383
419
|
};
|
|
384
|
-
//# sourceMappingURL=chunk-
|
|
420
|
+
//# sourceMappingURL=chunk-XAWKEXKJ.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/server.ts", "../../../adapters/node/src/cli.ts", "../../../adapters/node/src/serve.ts", "../../../adapters/node/src/listen.ts"],
|
|
4
|
+
"sourcesContent": ["/// <reference types=\"node\" />\nimport { readFileSync } from \"node:fs\"\nimport { type Listening, listen, type ServeTarget } from \"@crvouga/mockingbird-adapter-node\"\nimport { type CatalogItem, parseCatalog } from \"./catalog.js\"\nimport { createRuntime, type RxVortexRuntime, type RxVortexRuntimeOptions } from \"./runtime.js\"\n\n/** Port `mockingbird-rxvortex serve` listens on when none is given. */\nexport const DEFAULT_PORT = 8791\n\nexport type RxVortexServerOptions = RxVortexRuntimeOptions & {\n /** Default `0`: the OS picks a free port. */\n port?: number\n /** Default `127.0.0.1`. */\n host?: string\n}\n\nexport type RxVortexServer = Listening & { runtime: RxVortexRuntime }\n\n/** Serve the RxVortex mock over `node:http`, with auto-advance ticking every 100 ms. */\nexport const createServer = async (\n options: RxVortexServerOptions = {},\n): Promise<RxVortexServer> => {\n const { port, host, ...rest } = options\n const runtime = createRuntime({ tickMs: 100, ...rest })\n const listening = await listen(runtime, {\n port: port ?? 0,\n ...(host !== undefined ? { host } : {}),\n })\n return {\n ...listening,\n runtime,\n close: async () => {\n runtime.stop()\n await listening.close()\n },\n }\n}\n\nconst text = (value: string | boolean | undefined) =>\n typeof value === \"string\" ? value : undefined\n\n/**\n * Read a catalog file: a JSON array of rows, or the `{data: [...]}` body\n * `GET /api/v1/preset-catalog-items` answers with, so a recorded response loads as-is.\n */\nexport const loadCatalogFile = (path: string): CatalogItem[] => {\n let raw: string\n try {\n raw = readFileSync(path, \"utf8\")\n } catch {\n throw new Error(`rxvortex catalog not found: ${path}`)\n }\n try {\n return parseCatalog(JSON.parse(raw))\n } catch (error) {\n throw new Error(`rxvortex catalog ${path}: ${(error as Error).message}`)\n }\n}\n\n/** How `serve` (and `serve --config`) builds the RxVortex mock from flags. */\nexport const serveTarget: ServeTarget = {\n name: \"rxvortex\",\n defaultPort: DEFAULT_PORT,\n options: {\n \"webhook-url\": {\n type: \"string\",\n value: \"<url>\",\n description:\n \"Deliver status webhooks here (e.g. http://127.0.0.1:3000/prescriptions/webhooks/rxvortex)\",\n },\n \"webhook-secret\": {\n type: \"string\",\n value: \"<secret>\",\n description: \"Sent as x-rxvortex-webhook-secret (the app's RXVORTEX_WEBHOOK_SECRET)\",\n },\n \"api-token\": {\n type: \"string\",\n value: \"<token>\",\n description: \"A static bearer token to accept (the app's RXVORTEX_API_TOKEN)\",\n },\n catalog: {\n type: \"string\",\n value: \"<file.json>\",\n description:\n \"Preset catalog every namespace starts with and resets to: a JSON array, or {data: [...]} as GET /api/v1/preset-catalog-items returns\",\n },\n \"unknown-presets\": {\n type: \"string\",\n value: \"<reject|accept>\",\n description:\n \"An unknown preset_catalog_id: 422 like the sandbox (reject, default) or add it (accept)\",\n },\n \"auto-advance\": {\n type: \"string\",\n value: \"<ms:Status,Status,\u2026>\",\n description: 'Walk every new order along a path, e.g. \"2000:Fill,Shipping,Delivered\"',\n },\n },\n create: (values, common) => {\n const url = text(values[\"webhook-url\"])\n const secret = text(values[\"webhook-secret\"])\n const token = text(values[\"api-token\"])\n const auto = text(values[\"auto-advance\"])\n const catalogPath = text(values.catalog)\n const unknownPresets = text(values[\"unknown-presets\"])\n if (unknownPresets !== undefined && unknownPresets !== \"reject\" && unknownPresets !== \"accept\")\n throw new Error('--unknown-presets must be \"reject\" or \"accept\"')\n const plan = auto ? /^(\\d+):(.+)$/.exec(auto) : null\n if (auto && !plan)\n throw new Error('--auto-advance must look like \"2000:Fill,Shipping,Delivered\"')\n return createRuntime({\n tickMs: 100,\n ...(url ? { webhooks: { url, ...(secret ? { secret } : {}) } } : {}),\n ...(catalogPath ? { catalog: loadCatalogFile(catalogPath) } : {}),\n settings: {\n ...(unknownPresets ? { unknownPresets } : {}),\n ...(token ? { staticTokens: [token] } : {}),\n ...(plan\n ? {\n autoAdvance: {\n afterMs: Number(plan[1]),\n path: (plan[2] as string).split(\",\").map((s) => s.trim()),\n },\n }\n : {}),\n },\n ...(common.adminKey !== undefined ? { adminKey: common.adminKey } : {}),\n ...(common.seed !== undefined ? { seed: common.seed } : {}),\n ...(common.onLog ? { onLog: common.onLog } : {}),\n })\n },\n banner: () => [\n \"auth: POST /api/v1/generate-access-token {client_id, client_secret}, then Authorization: Bearer <token>\",\n \"namespaces: x-mockingbird-namespace, /ns/<name>/\u2026, or PUT /__admin/credentials {<client_id>: <ns>}\",\n ],\n}\n", "import { readFile } from \"node:fs/promises\"\nimport { type ParseArgsConfig, parseArgs } from \"node:util\"\nimport type { RequestLog, ServiceInstance, ServiceRuntime } from \"@crvouga/mockingbird-service\"\nimport { type Listening, listen } from \"./listen.js\"\n\nexport type CliOption = {\n type: \"string\" | \"boolean\"\n description: string\n /** Shown in help; the value placeholder, e.g. `<port>`. */\n value?: string\n default?: string | boolean\n}\n\nexport type CliValues = Record<string, string | boolean | undefined>\n\nexport type CliCommand = {\n summary: string\n usage?: string\n options?: Record<string, CliOption>\n /** Resolves to an exit code; a server command resolves only when it stops. */\n run(values: CliValues, positionals: string[]): Promise<number>\n}\n\nexport type CliSpec = {\n bin: string\n description: string\n commands: Record<string, CliCommand>\n}\n\nconst optionHelp = (options: Record<string, CliOption>): string[] =>\n Object.entries(options).map(([name, option]) => {\n const flag = `--${name}${option.type === \"string\" ? ` ${option.value ?? \"<value>\"}` : \"\"}`\n const fallback = option.default !== undefined ? ` (default: ${String(option.default)})` : \"\"\n return ` ${flag.padEnd(30)} ${option.description}${fallback}`\n })\n\nconst help = (spec: CliSpec): string =>\n [\n `${spec.bin} \u2014 ${spec.description}`,\n \"\",\n \"Usage:\",\n ` ${spec.bin} <command> [options]`,\n \"\",\n \"Commands:\",\n ...Object.entries(spec.commands).map(([name, c]) => ` ${name.padEnd(30)} ${c.summary}`),\n \"\",\n `Run \\`${spec.bin} <command> --help\\` for a command's options.`,\n ].join(\"\\n\")\n\nconst commandHelp = (spec: CliSpec, name: string, command: CliCommand): string =>\n [\n `${spec.bin} ${name} \u2014 ${command.summary}`,\n \"\",\n \"Usage:\",\n ` ${command.usage ?? `${spec.bin} ${name} [options]`}`,\n ...(command.options ? [\"\", \"Options:\", ...optionHelp(command.options)] : []),\n ].join(\"\\n\")\n\n/** Parse `argv` against `spec` and run the chosen command. Resolves to an exit code. */\nexport const runCli = async (spec: CliSpec, argv: string[]): Promise<number> => {\n // Two-word commands (`corpus pull`) win over one-word ones.\n const pair = argv.length >= 2 ? `${argv[0]} ${argv[1]}` : undefined\n const words = pair !== undefined && spec.commands[pair] ? 2 : 1\n const name = words === 2 ? pair : argv[0]\n const rest = argv.slice(words)\n if (name === undefined || name === \"--help\" || name === \"-h\" || name === \"help\") {\n console.log(help(spec))\n return 0\n }\n const command = spec.commands[name]\n if (!command) {\n console.error(`${spec.bin}: unknown command ${JSON.stringify(name)}\\n\\n${help(spec)}`)\n return 2\n }\n if (rest.includes(\"--help\") || rest.includes(\"-h\")) {\n console.log(commandHelp(spec, name, command))\n return 0\n }\n let parsed: { values: CliValues; positionals: string[] }\n try {\n parsed = parseArgs({\n args: rest,\n allowPositionals: true,\n strict: true,\n options: Object.fromEntries(\n Object.entries(command.options ?? {}).map(([key, option]) => [\n key,\n {\n type: option.type,\n ...(option.default !== undefined ? { default: option.default } : {}),\n },\n ]),\n ) as ParseArgsConfig[\"options\"],\n }) as { values: CliValues; positionals: string[] }\n } catch (error) {\n console.error(\n `${spec.bin} ${name}: ${error instanceof Error ? error.message : String(error)}\\n\\n${commandHelp(spec, name, command)}`,\n )\n return 2\n }\n return command.run(parsed.values, parsed.positionals)\n}\n\n// \u2500\u2500 serve \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport type LogFormat = \"pretty\" | \"json\" | \"off\"\n\nexport type CommonServeOptions = {\n adminKey: string | undefined\n seed: string | undefined\n onLog: ((entry: RequestLog) => void) | undefined\n}\n\n/**\n * What a service contributes to `serve`: how to build its runtime from CLI flags,\n * and what to say at startup. Every service's `./server` entry exports one as\n * `serveTarget`, which is also how `serve --config` finds services by name.\n */\nexport type ServeTarget = {\n name: string\n defaultPort: number\n /** Serve flags beyond the common ones. */\n options?: Record<string, CliOption>\n create(\n values: CliValues,\n common: CommonServeOptions,\n ): Promise<ServiceRuntime<ServiceInstance>> | ServiceRuntime<ServiceInstance>\n /** Startup lines after the listen address, e.g. the loaded corpus version. */\n banner?(runtime: ServiceRuntime<ServiceInstance>): string[]\n}\n\nconst COMMON_SERVE_OPTIONS: Record<string, CliOption> = {\n port: { type: \"string\", value: \"<port>\", description: \"Port to listen on\" },\n host: { type: \"string\", value: \"<host>\", description: \"Interface to bind\", default: \"127.0.0.1\" },\n \"admin-key\": {\n type: \"string\",\n value: \"<key>\",\n description: \"Require x-mockingbird-admin-key on /__admin/* (env MOCKINGBIRD_ADMIN_KEY)\",\n },\n seed: { type: \"string\", value: \"<seed>\", description: \"Seed for every random choice\" },\n log: {\n type: \"string\",\n value: \"<pretty|json|off>\",\n description: \"Request log format\",\n default: \"pretty\",\n },\n \"log-requests\": {\n type: \"boolean\",\n description:\n \"One JSON line per request: namespace, operationId, status, ids touched (never bodies). Same as --log json\",\n },\n config: {\n type: \"string\",\n value: \"<file>\",\n description: \"Serve every service in a mockingbird.json config instead\",\n },\n}\n\nconst formatLog = (format: LogFormat) => {\n if (format === \"off\") return undefined\n if (format === \"json\") return (entry: RequestLog) => console.log(JSON.stringify(entry))\n return (entry: RequestLog) => {\n const op = entry.operationId ?? (entry.unmatched ? \"UNMATCHED\" : \"-\")\n const ns = entry.namespace === \"default\" ? \"\" : ` [${entry.namespace}]`\n const fault = entry.faultId ? ` fault=${entry.faultId}` : \"\"\n const adopted = entry.adopted ? \" adopted\" : \"\"\n // A rejection says what arrived (never the body) and why the service refused it.\n const received = entry.request\n ? ` received=${entry.request.bodyBytes === null ? \"?\" : `${entry.request.bodyBytes}B`} ct=${entry.request.contentType ?? \"-\"}${\n entry.request.transferEncoding ? ` te=${entry.request.transferEncoding}` : \"\"\n }`\n : \"\"\n const issues = entry.issues?.length\n ? ` issues=${entry.issues\n .slice(0, 3)\n .map((issue) => `${issue.path || \"$\"}: ${issue.message}`)\n .join(\"; \")}${entry.issues.length > 3 ? ` (+${entry.issues.length - 3})` : \"\"}`\n : \"\"\n console.log(\n `${entry.service} ${entry.method} ${entry.path} ${entry.status} ${op} ${entry.durationMs}ms${ns}${fault}${adopted}${received}${issues}`,\n )\n }\n}\n\nconst asString = (value: string | boolean | undefined): string | undefined =>\n typeof value === \"string\" ? value : undefined\n\n/** A service entry in `mockingbird.json`. */\nexport type ConfigService = {\n port?: number\n host?: string\n adminKey?: string\n seed?: string\n /** Service-specific serve flags, by long name: `{ \"webhook-url\": \"\u2026\" }`. */\n options?: Record<string, string | boolean>\n}\n\nexport type MockingbirdConfig = {\n /** Keyed by service name: `junction` loads `@crvouga/mockingbird-service-junction`. */\n services: Record<string, ConfigService>\n log?: LogFormat\n}\n\nconst loadTarget = async (name: string, own: ServeTarget): Promise<ServeTarget> => {\n if (name === own.name) return own\n const specifier = `@crvouga/mockingbird-service-${name}/server`\n try {\n const mod = (await import(specifier)) as { serveTarget?: ServeTarget }\n if (!mod.serveTarget) throw new Error(`${specifier} exports no serveTarget`)\n return mod.serveTarget\n } catch (error) {\n const reason = error instanceof Error ? error.message : String(error)\n throw new Error(\n `cannot load service \"${name}\": ${reason}. Install @crvouga/mockingbird-service-${name}.`,\n )\n }\n}\n\nconst start = async (\n target: ServeTarget,\n values: CliValues,\n config: { port: number; host: string; adminKey?: string; seed?: string; log: LogFormat },\n): Promise<Listening> => {\n const runtime = await target.create(values, {\n adminKey: config.adminKey,\n seed: config.seed,\n onLog: formatLog(config.log),\n })\n const listening = await listen(runtime, { port: config.port, host: config.host })\n console.log(`${target.name} mock listening on ${listening.url}`)\n console.log(`${target.name} health: GET ${listening.url}/health`)\n console.log(\n `${target.name} admin: ${listening.url}/__admin (${config.adminKey ? \"x-mockingbird-admin-key required\" : \"open \u2014 pass --admin-key to lock\"})`,\n )\n for (const line of target.banner?.(runtime) ?? []) console.log(`${target.name} ${line}`)\n return listening\n}\n\nconst untilSignal = async (servers: Listening[]): Promise<number> =>\n new Promise((resolve) => {\n const stop = async () => {\n await Promise.allSettled(servers.map((s) => s.close()))\n resolve(0)\n }\n process.once(\"SIGINT\", stop)\n process.once(\"SIGTERM\", stop)\n })\n\n/** The standard `serve` command for a service, including multi-service `--config`. */\nexport const serveCommand = (target: ServeTarget): CliCommand => ({\n summary: `Serve the ${target.name} mock over HTTP`,\n options: { ...COMMON_SERVE_OPTIONS, ...target.options },\n async run(values) {\n const log = (\n values[\"log-requests\"] === true ? \"json\" : (asString(values.log) ?? \"pretty\")\n ) as LogFormat\n if (![\"pretty\", \"json\", \"off\"].includes(log)) {\n console.error(`--log must be pretty, json or off (got ${log})`)\n return 2\n }\n const configPath = asString(values.config)\n if (configPath !== undefined) {\n const config = JSON.parse(await readFile(configPath, \"utf8\")) as MockingbirdConfig\n const servers: Listening[] = []\n try {\n for (const [name, entry] of Object.entries(config.services ?? {})) {\n const each = await loadTarget(name, target)\n servers.push(\n await start(each, entry.options ?? {}, {\n port: entry.port ?? each.defaultPort,\n host: entry.host ?? \"127.0.0.1\",\n ...(entry.adminKey !== undefined ? { adminKey: entry.adminKey } : {}),\n ...(entry.seed !== undefined ? { seed: entry.seed } : {}),\n log: config.log ?? log,\n }),\n )\n }\n } catch (error) {\n await Promise.allSettled(servers.map((s) => s.close()))\n console.error(error instanceof Error ? error.message : String(error))\n return 1\n }\n return untilSignal(servers)\n }\n const port = asString(values.port)\n const adminKey = asString(values[\"admin-key\"]) ?? process.env.MOCKINGBIRD_ADMIN_KEY\n const seed = asString(values.seed)\n let listening: Listening\n try {\n listening = await start(target, values, {\n port: port === undefined ? target.defaultPort : Number.parseInt(port, 10),\n host: asString(values.host) ?? \"127.0.0.1\",\n ...(adminKey !== undefined ? { adminKey } : {}),\n ...(seed !== undefined ? { seed } : {}),\n log,\n })\n } catch (error) {\n console.error(error instanceof Error ? error.message : String(error))\n return 1\n }\n return untilSignal([listening])\n },\n})\n", "import { createServer } from \"node:http\"\nimport type { FetchAPI } from \"@crvouga/mockingbird-core\"\n\n/** Options for {@link serve}. */\nexport type NodeServeOptions = {\n port?: number\n host?: string\n}\n\n/**\n * Serve any Mockingbird {@link FetchAPI} over `node:http`.\n * Port defaults to `0`, so the OS assigns an ephemeral port (read from `server.address()`).\n */\nexport const serve = async (api: FetchAPI, options: NodeServeOptions = {}) => {\n const server = createServer(async (req, res) => {\n const chunks: Buffer[] = []\n for await (const chunk of req) {\n chunks.push(Buffer.from(chunk))\n }\n const body = Buffer.concat(chunks)\n const address = server.address()\n const port = typeof address === \"object\" && address !== null ? address.port : undefined\n const base = `http://${req.headers.host ?? `localhost:${port ?? 80}`}`\n const raw = req.url ?? \"/\"\n const url = new URL(raw.replace(/^\\/+/, \"/\"), base)\n const method = req.method ?? \"GET\"\n const init: RequestInit = { method, headers: req.headers as Record<string, string> }\n if (method !== \"GET\" && method !== \"HEAD\" && body.length > 0) {\n init.body = body\n }\n const aborted = new AbortController()\n res.once(\"close\", () => {\n if (!res.writableFinished) aborted.abort()\n })\n init.signal = aborted.signal\n const request = new Request(url, init)\n let response: Response\n try {\n response = await api.fetch(request)\n } catch (error) {\n // A `drop` fault: destroy the socket so the client sees the connection die.\n if ((error as { code?: string }).code === \"MOCKINGBIRD_DROP\") {\n req.socket.destroy()\n return\n }\n res.writeHead(500, { \"content-type\": \"application/json\" })\n res.end(\n JSON.stringify({\n error: {\n type: \"mockingbird_internal\",\n message: error instanceof Error ? error.message : String(error),\n },\n }),\n )\n return\n }\n // Headers#entries() joins repeated headers; Set-Cookie must stay one header per cookie.\n const headers: Record<string, string | string[]> = Object.fromEntries(response.headers)\n const cookies = response.headers.getSetCookie()\n if (cookies.length > 0) headers[\"set-cookie\"] = cookies\n if (!response.body) {\n res.writeHead(response.status, headers)\n res.end()\n return\n }\n // Stream the body chunk by chunk: event streams and long-polls must not be buffered.\n res.writeHead(response.status, headers)\n res.flushHeaders()\n const reader = response.body.getReader()\n try {\n for (;;) {\n const { done, value } = await reader.read()\n if (done) break\n if (!res.write(value)) await new Promise<void>((resolve) => res.once(\"drain\", resolve))\n }\n res.end()\n } catch {\n res.destroy()\n } finally {\n reader.releaseLock()\n }\n })\n await new Promise<void>((resolve, reject) => {\n server.once(\"error\", reject)\n server.listen(options.port ?? 0, options.host, resolve)\n })\n return server\n}\n", "import type { Server } from \"node:http\"\nimport type { FetchAPI } from \"@crvouga/mockingbird-core\"\nimport { serve } from \"./serve.js\"\n\nexport type ListenOptions = {\n /** Default `0`: the OS picks a free port. */\n port?: number\n /** Default `127.0.0.1`: a mock should not be reachable off the machine by accident. */\n host?: string\n}\n\n/** A running server, with the address it actually bound. */\nexport type Listening = {\n url: string\n port: number\n host: string\n server: Server\n close(): Promise<void>\n}\n\nexport const listen = async (api: FetchAPI, options: ListenOptions = {}): Promise<Listening> => {\n const host = options.host ?? \"127.0.0.1\"\n const server = await serve(api, { port: options.port ?? 0, host })\n const address = server.address()\n const port = typeof address === \"object\" && address !== null ? address.port : (options.port ?? 0)\n const shown = host.includes(\":\") ? `[${host}]` : host\n return {\n url: `http://${shown}:${port}`,\n port,\n host,\n server,\n close: () =>\n new Promise<void>((resolve, reject) => {\n // Stop accepting first, then drop keep-alive sockets so close() can finish.\n server.close((error) => {\n if (error && (error as { code?: string }).code !== \"ERR_SERVER_NOT_RUNNING\") reject(error)\n else resolve()\n })\n server.closeAllConnections?.()\n }),\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["createServer"]
|
|
7
|
+
}
|
package/dist/cli.js
CHANGED
package/dist/index.d.ts
CHANGED
|
@@ -228,94 +228,6 @@ type FaultRegistry = {
|
|
|
228
228
|
take(candidate: FaultCandidate): Promise<FaultHit[]>;
|
|
229
229
|
};
|
|
230
230
|
|
|
231
|
-
/** One handled request, as the structured log sees it. */
|
|
232
|
-
type RequestLog = {
|
|
233
|
-
service: string;
|
|
234
|
-
namespace: string;
|
|
235
|
-
operationId: string | undefined;
|
|
236
|
-
method: string;
|
|
237
|
-
path: string;
|
|
238
|
-
status: number;
|
|
239
|
-
durationMs: number;
|
|
240
|
-
/** True when the path matched no operation in the contract. */
|
|
241
|
-
unmatched: boolean;
|
|
242
|
-
/** Set when a fault rule produced the response. */
|
|
243
|
-
faultId?: string;
|
|
244
|
-
/** Resource ids the handler touched (`userId`, `orderId`, …), when the service reports them. */
|
|
245
|
-
ids?: Record<string, string>;
|
|
246
|
-
/** Set when the service created a resource the request referred to but that did not exist. */
|
|
247
|
-
adopted?: boolean;
|
|
248
|
-
};
|
|
249
|
-
type MetricsReport = {
|
|
250
|
-
requests: number;
|
|
251
|
-
/** Counts keyed `<operationId> <status>`. */
|
|
252
|
-
byOperation: Record<string, number>;
|
|
253
|
-
/**
|
|
254
|
-
* Paths that matched no operation, most frequent first.
|
|
255
|
-
*
|
|
256
|
-
* This is the early-warning signal: a consumer calling something the mock does
|
|
257
|
-
* not implement shows up here as a count, before it fails a suite as a 404.
|
|
258
|
-
*/
|
|
259
|
-
unmatched: {
|
|
260
|
-
method: string;
|
|
261
|
-
path: string;
|
|
262
|
-
count: number;
|
|
263
|
-
}[];
|
|
264
|
-
faults: number;
|
|
265
|
-
totalDurationMs: number;
|
|
266
|
-
};
|
|
267
|
-
type Metrics = {
|
|
268
|
-
record(entry: RequestLog): void;
|
|
269
|
-
report(): MetricsReport;
|
|
270
|
-
reset(): void;
|
|
271
|
-
};
|
|
272
|
-
|
|
273
|
-
/** One journal entry: a request log stamped with when (on the mock clock) it was handled. */
|
|
274
|
-
type JournalEntry = RequestLog & {
|
|
275
|
-
at: string;
|
|
276
|
-
};
|
|
277
|
-
type JournalQuery = {
|
|
278
|
-
/** Only this namespace. Omit for every namespace, oldest first across all of them. */
|
|
279
|
-
namespace?: string;
|
|
280
|
-
operationId?: string;
|
|
281
|
-
status?: number;
|
|
282
|
-
/** Only entries at or after this instant (epoch ms). */
|
|
283
|
-
since?: number;
|
|
284
|
-
/** At most this many, the most recent kept. */
|
|
285
|
-
limit?: number;
|
|
286
|
-
};
|
|
287
|
-
type Journal = {
|
|
288
|
-
readonly size: number;
|
|
289
|
-
record(entry: JournalEntry): void;
|
|
290
|
-
list(query?: JournalQuery): JournalEntry[];
|
|
291
|
-
/** Forget one namespace's entries, or every namespace's. */
|
|
292
|
-
clear(namespace?: string): void;
|
|
293
|
-
};
|
|
294
|
-
|
|
295
|
-
/** Credential → namespace mapping behind `PUT /__admin/credentials`. */
|
|
296
|
-
type CredentialRegistry = {
|
|
297
|
-
set(credential: string, namespace: string): void;
|
|
298
|
-
get(credential: string): string | undefined;
|
|
299
|
-
remove(credential: string): boolean;
|
|
300
|
-
clear(): void;
|
|
301
|
-
entries(): {
|
|
302
|
-
credential: string;
|
|
303
|
-
namespace: string;
|
|
304
|
-
}[];
|
|
305
|
-
};
|
|
306
|
-
|
|
307
|
-
/**
|
|
308
|
-
* Sequential id source persisted in SQLite. Ids are deterministic for a given
|
|
309
|
-
* sequence history (`cus_` + 14 opaque chars), so reproductions stay stable.
|
|
310
|
-
*/
|
|
311
|
-
declare class IdSequence {
|
|
312
|
-
private readonly sqlite;
|
|
313
|
-
private readonly namespace;
|
|
314
|
-
private readonly salt;
|
|
315
|
-
constructor(sqlite: SqliteClient$1, namespace: string, salt?: string);
|
|
316
|
-
next(prefix: string, length?: number): string;
|
|
317
|
-
}
|
|
318
|
-
|
|
319
231
|
/** A stable identifier for a point in a {@link Timeline}. */
|
|
320
232
|
type CheckpointId = string;
|
|
321
233
|
/** An immutable node in a timeline's checkpoint DAG. */
|
|
@@ -406,6 +318,135 @@ interface FetchAPI {
|
|
|
406
318
|
fetch(request: Request): Promise<Response>;
|
|
407
319
|
}
|
|
408
320
|
|
|
321
|
+
/** Options every provider constructor accepts. */
|
|
322
|
+
type APIOptions = {
|
|
323
|
+
/** Sync SQLite client. Defaults to `@crvouga/mockingbird-service-sqlite`. */
|
|
324
|
+
sqlite?: SqliteClient$1;
|
|
325
|
+
/** Clock used for `created`-style fields. Default `Date.now`. */
|
|
326
|
+
now?: () => number;
|
|
327
|
+
/**
|
|
328
|
+
* Storage namespace for this instance's records. Instances sharing one SQLite
|
|
329
|
+
* client stay isolated when their namespaces differ. Defaults to the service name.
|
|
330
|
+
*/
|
|
331
|
+
namespace?: string;
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* One problem with a request body, at a dotted path (`patient.address.city`, `items.0.sku`).
|
|
336
|
+
* `kind` is `media_type` when the body is present but its media type is not one the operation
|
|
337
|
+
* accepts (a `415` on most vendors), `syntax` when it could not be decoded, `required` when
|
|
338
|
+
* the operation needs a body and none arrived, and `schema` for a contract violation.
|
|
339
|
+
*/
|
|
340
|
+
type BodyIssue = {
|
|
341
|
+
path: string;
|
|
342
|
+
message: string;
|
|
343
|
+
kind?: "media_type" | "syntax" | "required" | "schema";
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* What the mock received from a request it rejected, so a 4xx can be attributed after the
|
|
348
|
+
* fact (an empty body, a missing `content-type`, a schema violation) without logging the body.
|
|
349
|
+
*/
|
|
350
|
+
type RejectedRequest = {
|
|
351
|
+
/** The raw `content-type` header, or `null` when the request sent none. */
|
|
352
|
+
contentType: string | null;
|
|
353
|
+
/** Bytes of body received from `content-length` (`0` for none), or `null` when the client sent no length (a streamed / chunked body). */
|
|
354
|
+
bodyBytes: number | null;
|
|
355
|
+
/** The raw `transfer-encoding` header (`chunked`), or `null`. */
|
|
356
|
+
transferEncoding: string | null;
|
|
357
|
+
};
|
|
358
|
+
/** One handled request, as the structured log sees it. */
|
|
359
|
+
type RequestLog = {
|
|
360
|
+
service: string;
|
|
361
|
+
namespace: string;
|
|
362
|
+
operationId: string | undefined;
|
|
363
|
+
method: string;
|
|
364
|
+
path: string;
|
|
365
|
+
status: number;
|
|
366
|
+
durationMs: number;
|
|
367
|
+
/** True when the path matched no operation in the contract. */
|
|
368
|
+
unmatched: boolean;
|
|
369
|
+
/** Set when a fault rule produced the response. */
|
|
370
|
+
faultId?: string;
|
|
371
|
+
/** Resource ids the handler touched (`userId`, `orderId`, …), when the service reports them. */
|
|
372
|
+
ids?: Record<string, string>;
|
|
373
|
+
/** Set when the service created a resource the request referred to but that did not exist. */
|
|
374
|
+
adopted?: boolean;
|
|
375
|
+
/** Set on a rejection (status ≥ 400) the mock produced itself, never on a scripted fault. */
|
|
376
|
+
request?: RejectedRequest;
|
|
377
|
+
/** The validation issues behind a rejection, when the service found any. */
|
|
378
|
+
issues?: BodyIssue[];
|
|
379
|
+
};
|
|
380
|
+
type MetricsReport = {
|
|
381
|
+
requests: number;
|
|
382
|
+
/** Counts keyed `<operationId> <status>`. */
|
|
383
|
+
byOperation: Record<string, number>;
|
|
384
|
+
/**
|
|
385
|
+
* Paths that matched no operation, most frequent first.
|
|
386
|
+
*
|
|
387
|
+
* This is the early-warning signal: a consumer calling something the mock does
|
|
388
|
+
* not implement shows up here as a count, before it fails a suite as a 404.
|
|
389
|
+
*/
|
|
390
|
+
unmatched: {
|
|
391
|
+
method: string;
|
|
392
|
+
path: string;
|
|
393
|
+
count: number;
|
|
394
|
+
}[];
|
|
395
|
+
faults: number;
|
|
396
|
+
totalDurationMs: number;
|
|
397
|
+
};
|
|
398
|
+
type Metrics = {
|
|
399
|
+
record(entry: RequestLog): void;
|
|
400
|
+
report(): MetricsReport;
|
|
401
|
+
reset(): void;
|
|
402
|
+
};
|
|
403
|
+
|
|
404
|
+
/** One journal entry: a request log stamped with when (on the mock clock) it was handled. */
|
|
405
|
+
type JournalEntry = RequestLog & {
|
|
406
|
+
at: string;
|
|
407
|
+
};
|
|
408
|
+
type JournalQuery = {
|
|
409
|
+
/** Only this namespace. Omit for every namespace, oldest first across all of them. */
|
|
410
|
+
namespace?: string;
|
|
411
|
+
operationId?: string;
|
|
412
|
+
status?: number;
|
|
413
|
+
/** Only entries at or after this instant (epoch ms). */
|
|
414
|
+
since?: number;
|
|
415
|
+
/** At most this many, the most recent kept. */
|
|
416
|
+
limit?: number;
|
|
417
|
+
};
|
|
418
|
+
type Journal = {
|
|
419
|
+
readonly size: number;
|
|
420
|
+
record(entry: JournalEntry): void;
|
|
421
|
+
list(query?: JournalQuery): JournalEntry[];
|
|
422
|
+
/** Forget one namespace's entries, or every namespace's. */
|
|
423
|
+
clear(namespace?: string): void;
|
|
424
|
+
};
|
|
425
|
+
|
|
426
|
+
/** Credential → namespace mapping behind `PUT /__admin/credentials`. */
|
|
427
|
+
type CredentialRegistry = {
|
|
428
|
+
set(credential: string, namespace: string): void;
|
|
429
|
+
get(credential: string): string | undefined;
|
|
430
|
+
remove(credential: string): boolean;
|
|
431
|
+
clear(): void;
|
|
432
|
+
entries(): {
|
|
433
|
+
credential: string;
|
|
434
|
+
namespace: string;
|
|
435
|
+
}[];
|
|
436
|
+
};
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Sequential id source persisted in SQLite. Ids are deterministic for a given
|
|
440
|
+
* sequence history (`cus_` + 14 opaque chars), so reproductions stay stable.
|
|
441
|
+
*/
|
|
442
|
+
declare class IdSequence {
|
|
443
|
+
private readonly sqlite;
|
|
444
|
+
private readonly namespace;
|
|
445
|
+
private readonly salt;
|
|
446
|
+
constructor(sqlite: SqliteClient$1, namespace: string, salt?: string);
|
|
447
|
+
next(prefix: string, length?: number): string;
|
|
448
|
+
}
|
|
449
|
+
|
|
409
450
|
/**
|
|
410
451
|
* A point-in-time copy of everything a service namespace holds.
|
|
411
452
|
*
|
|
@@ -556,19 +597,6 @@ type ServiceRuntime<T extends ServiceInstance> = FetchAPI & {
|
|
|
556
597
|
timeline(namespace?: string): Timeline<ServiceTimelineState>;
|
|
557
598
|
};
|
|
558
599
|
|
|
559
|
-
/** Options every provider constructor accepts. */
|
|
560
|
-
type APIOptions = {
|
|
561
|
-
/** Sync SQLite client. Defaults to `@crvouga/mockingbird-service-sqlite`. */
|
|
562
|
-
sqlite?: SqliteClient$1;
|
|
563
|
-
/** Clock used for `created`-style fields. Default `Date.now`. */
|
|
564
|
-
now?: () => number;
|
|
565
|
-
/**
|
|
566
|
-
* Storage namespace for this instance's records. Instances sharing one SQLite
|
|
567
|
-
* client stay isolated when their namespaces differ. Defaults to the service name.
|
|
568
|
-
*/
|
|
569
|
-
namespace?: string;
|
|
570
|
-
};
|
|
571
|
-
|
|
572
600
|
/** Bind values accepted by Mockingbird's SQLite port (matches sqlite-mem / better-sqlite3). */
|
|
573
601
|
type SqliteValue = null | number | bigint | string | Uint8Array | boolean;
|
|
574
602
|
/** Mutation counters returned by {@link SqliteStatement.run}. */
|
|
@@ -619,6 +647,17 @@ declare const CUSTOM_CREAM_ANCHOR_PRESET_ID = "e404ad76-0f82-4b04-8f25-841650e2e
|
|
|
619
647
|
* it with the real rows); the custom-cream anchor preset must always resolve.
|
|
620
648
|
*/
|
|
621
649
|
declare const DEFAULT_CATALOG: readonly CatalogItem[];
|
|
650
|
+
/**
|
|
651
|
+
* One catalog row from loose input: `catalog_id` and `medication_name` are required; the
|
|
652
|
+
* other fields default to `null`, `states` to `[]` and `status` to `"active"`.
|
|
653
|
+
*/
|
|
654
|
+
declare const parseCatalogItem: (value: unknown) => CatalogItem;
|
|
655
|
+
/**
|
|
656
|
+
* Catalog rows from a JSON value: a bare array, or the `{data: [...]}` envelope
|
|
657
|
+
* `GET /api/v1/preset-catalog-items` answers with (so a recorded response loads as-is).
|
|
658
|
+
* Throws on the first malformed row.
|
|
659
|
+
*/
|
|
660
|
+
declare const parseCatalog: (value: unknown) => CatalogItem[];
|
|
622
661
|
|
|
623
662
|
/**
|
|
624
663
|
* One order as the vendor tracks it. Patient and prescriber details are validated but never
|
|
@@ -662,6 +701,12 @@ type Settings = {
|
|
|
662
701
|
client_secret: string;
|
|
663
702
|
}[];
|
|
664
703
|
autoAdvance: AutoAdvance | null;
|
|
704
|
+
/**
|
|
705
|
+
* What a submit does with a well-formed `preset_catalog_id` the catalog does not hold:
|
|
706
|
+
* `"reject"` answers 422 as the sandbox does; `"accept"` adds it as an active row named
|
|
707
|
+
* after the request's `medication_name`. Inactive rows are refused either way.
|
|
708
|
+
*/
|
|
709
|
+
unknownPresets: "reject" | "accept";
|
|
665
710
|
};
|
|
666
711
|
declare class RxVortexState {
|
|
667
712
|
private readonly seed;
|
|
@@ -670,11 +715,14 @@ declare class RxVortexState {
|
|
|
670
715
|
readonly settings: Collection<Settings>;
|
|
671
716
|
readonly ids: IdSequence;
|
|
672
717
|
constructor(sqlite: SqliteClient, namespace: string, seed: {
|
|
673
|
-
catalog: readonly CatalogItem[];
|
|
718
|
+
catalog: readonly CatalogItem[] | undefined;
|
|
674
719
|
settings: Partial<Settings>;
|
|
675
720
|
});
|
|
676
721
|
/** Re-apply the catalog and settings after a reset. */
|
|
677
722
|
ensureSeeded(): void;
|
|
723
|
+
/** Load rows: `replace` drops every other row first, `merge` upserts by `catalog_id`. */
|
|
724
|
+
putCatalog(items: readonly CatalogItem[], mode: "replace" | "merge"): number;
|
|
725
|
+
catalogRows(): CatalogItem[];
|
|
678
726
|
current(): Settings;
|
|
679
727
|
update(patch: Partial<Settings>): Settings;
|
|
680
728
|
/** By vendor tracking id first, then by our sender order id (the recovery lookup). */
|
|
@@ -864,6 +912,10 @@ type RxVortexRuntimeOptions = {
|
|
|
864
912
|
seed?: number | string;
|
|
865
913
|
adminKey?: string;
|
|
866
914
|
onLog?: (entry: RequestLog) => void;
|
|
915
|
+
/**
|
|
916
|
+
* Rows every namespace starts with, and returns to on `POST /__admin/reset` (`serve
|
|
917
|
+
* --catalog <file>`). Default: `DEFAULT_CATALOG`.
|
|
918
|
+
*/
|
|
867
919
|
catalog?: readonly CatalogItem[];
|
|
868
920
|
settings?: Partial<Settings>;
|
|
869
921
|
/** Where status webhooks go (`POST /prescriptions/webhooks/rxvortex`), sent with the secret header. */
|
|
@@ -950,6 +1002,11 @@ declare class RxVortexAPI implements FetchAPI$1 {
|
|
|
950
1002
|
private iso;
|
|
951
1003
|
private generateToken;
|
|
952
1004
|
private createOrder;
|
|
1005
|
+
/**
|
|
1006
|
+
* Under `unknownPresets: "accept"`, a well-formed preset id the catalog lacks becomes an
|
|
1007
|
+
* active row described by the request, so later submits and catalog reads see it.
|
|
1008
|
+
*/
|
|
1009
|
+
private adoptPreset;
|
|
953
1010
|
private getOrder;
|
|
954
1011
|
private cancelOrder;
|
|
955
1012
|
/** Move an order to a vendor status, set the three fields coherently, emit the webhook. */
|
|
@@ -962,5 +1019,5 @@ declare class RxVortexAPI implements FetchAPI$1 {
|
|
|
962
1019
|
orders(): OrderRecord[];
|
|
963
1020
|
}
|
|
964
1021
|
|
|
965
|
-
export { CUSTOM_CREAM_ANCHOR_PRESET_ID, DEFAULT_CATALOG, RXVORTEX_NAMESPACE, RXVORTEX_PRESETS, RxVortexAPI, createRuntime, document, operationIds, supportedOperationIds, tokenCredential };
|
|
1022
|
+
export { CUSTOM_CREAM_ANCHOR_PRESET_ID, DEFAULT_CATALOG, RXVORTEX_NAMESPACE, RXVORTEX_PRESETS, RxVortexAPI, createRuntime, document, operationIds, parseCatalog, parseCatalogItem, supportedOperationIds, tokenCredential };
|
|
966
1023
|
export type { AutoAdvance, CatalogItem, FetchAPI$1 as FetchAPI, OperationId, OrderRecord, RxVortexAPIOptions, RxVortexRuntime, RxVortexRuntimeOptions, RxVortexWebhook, Settings, SqliteClient, SupportedOperationId, TransitionInput };
|
package/dist/index.js
CHANGED
|
@@ -7,9 +7,11 @@ import {
|
|
|
7
7
|
createRuntime,
|
|
8
8
|
document,
|
|
9
9
|
operationIds,
|
|
10
|
+
parseCatalog,
|
|
11
|
+
parseCatalogItem,
|
|
10
12
|
supportedOperationIds,
|
|
11
13
|
tokenCredential
|
|
12
|
-
} from "./chunk-
|
|
14
|
+
} from "./chunk-5KK6W2PE.js";
|
|
13
15
|
export {
|
|
14
16
|
CUSTOM_CREAM_ANCHOR_PRESET_ID,
|
|
15
17
|
DEFAULT_CATALOG,
|
|
@@ -19,6 +21,8 @@ export {
|
|
|
19
21
|
createRuntime,
|
|
20
22
|
document,
|
|
21
23
|
operationIds,
|
|
24
|
+
parseCatalog,
|
|
25
|
+
parseCatalogItem,
|
|
22
26
|
supportedOperationIds,
|
|
23
27
|
tokenCredential
|
|
24
28
|
};
|