@nyrra/prismantix-cli 0.0.1 → 0.0.2-next.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.
Files changed (56) hide show
  1. package/README.md +20 -28
  2. package/bin/dev.js +1 -3
  3. package/bin/run.js +1 -3
  4. package/dist/api.d.ts +201 -173
  5. package/dist/api.d.ts.map +1 -1
  6. package/dist/api.js +237 -110
  7. package/dist/command-support.d.ts +13 -0
  8. package/dist/command-support.d.ts.map +1 -0
  9. package/dist/command-support.js +40 -0
  10. package/dist/commands/object/details.d.ts +13 -0
  11. package/dist/commands/object/details.d.ts.map +1 -0
  12. package/dist/commands/object/details.js +28 -0
  13. package/dist/commands/object/list.d.ts +13 -0
  14. package/dist/commands/object/list.d.ts.map +1 -0
  15. package/dist/commands/object/list.js +27 -0
  16. package/dist/commands/ontology/list.d.ts +10 -0
  17. package/dist/commands/ontology/list.d.ts.map +1 -0
  18. package/dist/commands/ontology/list.js +25 -0
  19. package/dist/commands/records/cancel.d.ts +13 -0
  20. package/dist/commands/records/cancel.d.ts.map +1 -0
  21. package/dist/commands/records/cancel.js +21 -0
  22. package/dist/commands/records/generate.d.ts +15 -0
  23. package/dist/commands/records/generate.d.ts.map +1 -0
  24. package/dist/commands/records/generate.js +56 -0
  25. package/dist/commands/{download.d.ts → records/pull.d.ts} +5 -11
  26. package/dist/commands/records/pull.d.ts.map +1 -0
  27. package/dist/commands/records/pull.js +59 -0
  28. package/dist/commands/records/status.d.ts +13 -0
  29. package/dist/commands/records/status.d.ts.map +1 -0
  30. package/dist/commands/records/status.js +22 -0
  31. package/dist/constants.d.ts +2 -1
  32. package/dist/constants.d.ts.map +1 -1
  33. package/dist/constants.js +2 -1
  34. package/dist/errors.d.ts.map +1 -1
  35. package/dist/errors.js +39 -0
  36. package/dist/generation.d.ts +13 -0
  37. package/dist/generation.d.ts.map +1 -0
  38. package/dist/generation.js +77 -0
  39. package/dist/io.d.ts.map +1 -1
  40. package/dist/io.js +1 -1
  41. package/dist/runtime.d.ts +3 -0
  42. package/dist/runtime.d.ts.map +1 -0
  43. package/dist/runtime.js +10 -0
  44. package/dist/workflow.d.ts +3 -14
  45. package/dist/workflow.d.ts.map +1 -1
  46. package/dist/workflow.js +85 -61
  47. package/oclif.manifest.json +266 -40
  48. package/package.json +74 -74
  49. package/dist/commands/download.d.ts.map +0 -1
  50. package/dist/commands/download.js +0 -136
  51. package/dist/commands/guided.d.ts +0 -7
  52. package/dist/commands/guided.d.ts.map +0 -1
  53. package/dist/commands/guided.js +0 -15
  54. package/dist/ui/guided-flow.d.ts +0 -20
  55. package/dist/ui/guided-flow.d.ts.map +0 -1
  56. package/dist/ui/guided-flow.js +0 -156
@@ -0,0 +1,56 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { Command, Flags } from "@oclif/core";
3
+ import { Effect } from "effect";
4
+ import { apiFlags, failCommand, printResult, requireValue, runApiEffect, validatePositiveInteger, } from "../../command-support.js";
5
+ import { DEFAULT_GENERATION_TIMEOUT_MS, DEFAULT_POLL_INTERVAL_MS } from "../../constants.js";
6
+ import { createRecordGeneration, waitForRecordGeneration } from "../../generation.js";
7
+ export default class RecordsGenerate extends Command {
8
+ static description = "Append a complete ontology-sized default record set for a client.";
9
+ static flags = {
10
+ ...apiFlags,
11
+ client: Flags.string({ description: "Client ID.", required: true }),
12
+ ontology: Flags.string({ description: "Registered ontology ID.", required: true }),
13
+ "poll-interval": Flags.integer({
14
+ default: DEFAULT_POLL_INTERVAL_MS,
15
+ description: "Polling interval in milliseconds.",
16
+ }),
17
+ timeout: Flags.integer({
18
+ default: DEFAULT_GENERATION_TIMEOUT_MS,
19
+ description: "Polling timeout in milliseconds (default: 30 minutes).",
20
+ }),
21
+ wait: Flags.boolean({
22
+ allowNo: true,
23
+ default: true,
24
+ description: "Wait for the operation to reach a terminal state.",
25
+ }),
26
+ };
27
+ async run() {
28
+ const { flags } = await this.parse(RecordsGenerate);
29
+ try {
30
+ validatePositiveInteger(flags["poll-interval"], "--poll-interval");
31
+ validatePositiveInteger(flags.timeout, "--timeout");
32
+ const clientId = requireValue(flags.client, "--client");
33
+ const ontologyId = requireValue(flags.ontology, "--ontology");
34
+ const idempotencyKey = randomUUID();
35
+ const printOperationId = (operationId) => this.logToStderr(operationId);
36
+ const result = await runApiEffect(Effect.gen(function* () {
37
+ const created = yield* createRecordGeneration(clientId, ontologyId, idempotencyKey);
38
+ yield* Effect.sync(() => printOperationId(created.operationId));
39
+ if (!flags.wait)
40
+ return created;
41
+ return yield* waitForRecordGeneration(created, {
42
+ pollIntervalMs: flags["poll-interval"],
43
+ timeoutMs: flags.timeout,
44
+ });
45
+ }), flags["api-base"]);
46
+ printResult(this, result, flags.json, () => {
47
+ this.log(`Record generation ${result.operationId}: ${result.status}`);
48
+ this.log(`Client: ${result.clientId}`);
49
+ this.log(`Ontology: ${result.ontologyId}`);
50
+ });
51
+ }
52
+ catch (error) {
53
+ failCommand(this, error, flags.json);
54
+ }
55
+ }
56
+ }
@@ -1,22 +1,16 @@
1
1
  import { Command } from "@oclif/core";
2
- export default class Download extends Command {
2
+ export default class RecordsPull extends Command {
3
3
  static description: string;
4
- static examples: string[];
5
4
  static flags: {
6
- "api-base": import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
7
- client: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
8
- "export-id": import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
9
- json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
5
+ client: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
10
6
  output: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
11
7
  "poll-interval": import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
12
8
  table: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
13
- tables: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
14
9
  timeout: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
15
- yes: import("@oclif/core/interfaces").BooleanFlag<boolean>;
10
+ "api-base": import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
11
+ json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
16
12
  };
17
13
  run(): Promise<void>;
18
- private fail;
19
- private printProgress;
20
14
  private printSummary;
21
15
  }
22
- //# sourceMappingURL=download.d.ts.map
16
+ //# sourceMappingURL=pull.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pull.d.ts","sourceRoot":"","sources":["../../../src/commands/records/pull.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAS,MAAM,aAAa,CAAC;AAkB7C,MAAM,CAAC,OAAO,OAAO,WAAY,SAAQ,OAAO;IAC/C,OAAgB,WAAW,SAA6D;IACxF,OAAgB,KAAK;;;;;;;;MAgBnB;IAEW,GAAG,IAAI,OAAO,CAAC,IAAI,CAAC;IAyBjC,OAAO,CAAC,YAAY;CAMpB"}
@@ -0,0 +1,59 @@
1
+ import { Command, Flags } from "@oclif/core";
2
+ import { Match } from "effect";
3
+ import { makePrismantixApiClientLayer } from "../../api.js";
4
+ import { apiFlags, failCommand, printResult, requireValue, validatePositiveInteger, } from "../../command-support.js";
5
+ import { DEFAULT_POLL_INTERVAL_MS, DEFAULT_PULL_TIMEOUT_MS } from "../../constants.js";
6
+ import { runCliEffect } from "../../runtime.js";
7
+ import { runDownloadWorkflowEffect, } from "../../workflow.js";
8
+ export default class RecordsPull extends Command {
9
+ static description = "Pull a client CSV-bundle export into a local directory.";
10
+ static flags = {
11
+ ...apiFlags,
12
+ client: Flags.string({ description: "Client ID to export.", required: true }),
13
+ output: Flags.string({ description: "Output directory." }),
14
+ "poll-interval": Flags.integer({
15
+ default: DEFAULT_POLL_INTERVAL_MS,
16
+ description: "Polling interval in milliseconds.",
17
+ }),
18
+ table: Flags.string({
19
+ description: "Table to pull; repeat for multiple tables.",
20
+ multiple: true,
21
+ }),
22
+ timeout: Flags.integer({
23
+ default: DEFAULT_PULL_TIMEOUT_MS,
24
+ description: "Polling timeout in milliseconds.",
25
+ }),
26
+ };
27
+ async run() {
28
+ const { flags } = await this.parse(RecordsPull);
29
+ try {
30
+ validatePositiveInteger(flags["poll-interval"], "--poll-interval");
31
+ validatePositiveInteger(flags.timeout, "--timeout");
32
+ const clientId = requireValue(flags.client, "--client");
33
+ const result = await runCliEffect(runDownloadWorkflowEffect({
34
+ clientId,
35
+ outputDir: flags.output,
36
+ pollIntervalMs: flags["poll-interval"],
37
+ tables: flags.table,
38
+ timeoutMs: flags.timeout,
39
+ }, { onEvent: (event) => this.logToStderr(progressMessage(event)) }), makePrismantixApiClientLayer({ apiBaseUrl: flags["api-base"] }));
40
+ printResult(this, result, flags.json, () => this.printSummary(result));
41
+ }
42
+ catch (error) {
43
+ failCommand(this, error, flags.json);
44
+ }
45
+ }
46
+ printSummary(result) {
47
+ this.log(`Pulled PrismantiX export ${result.exportId}`);
48
+ this.log(`Output: ${result.outputDir}`);
49
+ this.log(`Manifest: ${result.manifestPath}`);
50
+ this.log(`Files: ${result.files.length}`);
51
+ }
52
+ }
53
+ const progressMessage = Match.type().pipe(Match.discriminatorsExhaustive("type")({
54
+ created: (event) => `Queued export ${event.exportId} for ${event.clientId}.`,
55
+ "download-complete": (event) => `Pulled ${event.file.tableName}.`,
56
+ "download-start": (event) => `Pulling ${event.file.tableName}...`,
57
+ manifest: (event) => `Manifest ready: ${event.manifest.files.length} file(s).`,
58
+ poll: (event) => `Export ${event.status.exportId}: ${event.status.status}.`,
59
+ }));
@@ -0,0 +1,13 @@
1
+ import { Command } from "@oclif/core";
2
+ export default class RecordsStatus extends Command {
3
+ static args: {
4
+ operationId: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
5
+ };
6
+ static description: string;
7
+ static flags: {
8
+ "api-base": import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
9
+ json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
10
+ };
11
+ run(): Promise<void>;
12
+ }
13
+ //# sourceMappingURL=status.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"status.d.ts","sourceRoot":"","sources":["../../../src/commands/records/status.ts"],"names":[],"mappings":"AAAA,OAAO,EAAQ,OAAO,EAAE,MAAM,aAAa,CAAC;AAU5C,MAAM,CAAC,OAAO,OAAO,aAAc,SAAQ,OAAO;IACjD,OAAgB,IAAI;;MAAoD;IACxE,OAAgB,WAAW,SAAyC;IACpE,OAAgB,KAAK;;;MAAY;IAEpB,GAAG,IAAI,OAAO,CAAC,IAAI,CAAC;CAajC"}
@@ -0,0 +1,22 @@
1
+ import { Args, Command } from "@oclif/core";
2
+ import { apiFlags, failCommand, printResult, requireValue, runApiEffect, } from "../../command-support.js";
3
+ import { getRecordGeneration } from "../../generation.js";
4
+ export default class RecordsStatus extends Command {
5
+ static args = { operationId: Args.string({ required: true }) };
6
+ static description = "Show a record-generation operation.";
7
+ static flags = apiFlags;
8
+ async run() {
9
+ const { args, flags } = await this.parse(RecordsStatus);
10
+ try {
11
+ const operationId = requireValue(args.operationId, "operation-id");
12
+ const result = await runApiEffect(getRecordGeneration(operationId), flags["api-base"]);
13
+ printResult(this, result, flags.json, () => {
14
+ this.log(`Record generation ${result.operationId}: ${result.status}`);
15
+ this.log(`Persisted: ${result.totals.persistedCount}/${result.totals.requestedCount}`);
16
+ });
17
+ }
18
+ catch (error) {
19
+ failCommand(this, error, flags.json);
20
+ }
21
+ }
22
+ }
@@ -1,5 +1,6 @@
1
1
  export declare const DEFAULT_API_BASE_URL = "https://api.prismantix.com";
2
2
  export declare const DEFAULT_OUTPUT_ROOT = "prismantix-exports";
3
3
  export declare const DEFAULT_POLL_INTERVAL_MS = 1000;
4
- export declare const DEFAULT_TIMEOUT_MS = 120000;
4
+ export declare const DEFAULT_PULL_TIMEOUT_MS = 120000;
5
+ export declare const DEFAULT_GENERATION_TIMEOUT_MS: number;
5
6
  //# sourceMappingURL=constants.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,oBAAoB,+BAA+B,CAAC;AACjE,eAAO,MAAM,mBAAmB,uBAAuB,CAAC;AACxD,eAAO,MAAM,wBAAwB,OAAO,CAAC;AAC7C,eAAO,MAAM,kBAAkB,SAAU,CAAC"}
1
+ {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,oBAAoB,+BAA+B,CAAC;AACjE,eAAO,MAAM,mBAAmB,uBAAuB,CAAC;AACxD,eAAO,MAAM,wBAAwB,OAAO,CAAC;AAC7C,eAAO,MAAM,uBAAuB,SAAU,CAAC;AAC/C,eAAO,MAAM,6BAA6B,QAAiB,CAAC"}
package/dist/constants.js CHANGED
@@ -1,4 +1,5 @@
1
1
  export const DEFAULT_API_BASE_URL = "https://api.prismantix.com";
2
2
  export const DEFAULT_OUTPUT_ROOT = "prismantix-exports";
3
3
  export const DEFAULT_POLL_INTERVAL_MS = 1000;
4
- export const DEFAULT_TIMEOUT_MS = 120_000;
4
+ export const DEFAULT_PULL_TIMEOUT_MS = 120_000;
5
+ export const DEFAULT_GENERATION_TIMEOUT_MS = 30 * 60 * 1000;
@@ -1 +1 @@
1
- {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,qBAAa,YAAa,SAAQ,KAAK;IACtC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;gBAEd,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,SAAI;CAMvD;AAED,wBAAgB,cAAc,CAAC,KAAK,EAAE,OAAO,gBAU5C"}
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAQA,qBAAa,YAAa,SAAQ,KAAK;IACtC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;gBAEd,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,SAAI;CAMvD;AAED,wBAAgB,cAAc,CAAC,KAAK,EAAE,OAAO,gBA8B5C"}
package/dist/errors.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { Predicate } from "effect";
1
2
  export class CliUserError extends Error {
2
3
  code;
3
4
  exitCode;
@@ -12,8 +13,46 @@ export function toCliUserError(error) {
12
13
  if (error instanceof CliUserError) {
13
14
  return error;
14
15
  }
16
+ if (isCodedError(error)) {
17
+ return new CliUserError(error.code, error.message, error.exitCode);
18
+ }
19
+ if (hasTag(error, "ApiConfigurationError")) {
20
+ return new CliUserError("missing_api_key", error.message);
21
+ }
22
+ if (hasTag(error, "ApiHttpError")) {
23
+ return new CliUserError(codeForStatus(error.status), error.message);
24
+ }
25
+ if (hasTag(error, "ApiDecodeError")) {
26
+ return new CliUserError("unexpected_error", error.message);
27
+ }
28
+ if (hasTag(error, "ApiTransportError")) {
29
+ return new CliUserError("unexpected_error", error.message);
30
+ }
15
31
  if (error instanceof Error) {
16
32
  return new CliUserError("unexpected_error", error.message);
17
33
  }
18
34
  return new CliUserError("unexpected_error", String(error));
19
35
  }
36
+ function hasTag(error, tag) {
37
+ return Predicate.hasProperty(error, "_tag") && error._tag === tag;
38
+ }
39
+ function codeForStatus(status) {
40
+ if (status === 401)
41
+ return "unauthorized";
42
+ if (status === 403)
43
+ return "forbidden";
44
+ if (status === 404)
45
+ return "not_found";
46
+ if (status === 409)
47
+ return "conflict";
48
+ if (status === 429)
49
+ return "rate_limited";
50
+ return "api_error";
51
+ }
52
+ function isCodedError(error) {
53
+ return (error instanceof Error &&
54
+ Predicate.hasProperty(error, "code") &&
55
+ typeof error.code === "string" &&
56
+ Predicate.hasProperty(error, "exitCode") &&
57
+ typeof error.exitCode === "number");
58
+ }
@@ -0,0 +1,13 @@
1
+ import { Effect } from "effect";
2
+ import { type RecordGenerationCreated, type RecordGenerationStatus, PrismantixApiClient } from "./api.js";
3
+ export interface WaitForGenerationOptions {
4
+ readonly now?: () => number;
5
+ readonly pollIntervalMs: number;
6
+ readonly sleep?: (milliseconds: number) => Promise<void>;
7
+ readonly timeoutMs: number;
8
+ }
9
+ export declare function createRecordGeneration(clientId: string, ontologyId: string, idempotencyKey: string): Effect.Effect<RecordGenerationCreated, import("./api.js").ApiConfigurationError | import("./api.js").ApiHttpError | import("./api.js").ApiTransportError | import("./api.js").ApiDecodeError, PrismantixApiClient>;
10
+ export declare function getRecordGeneration(operationId: string): Effect.Effect<RecordGenerationStatus, import("./api.js").ApiConfigurationError | import("./api.js").ApiHttpError | import("./api.js").ApiTransportError | import("./api.js").ApiDecodeError, PrismantixApiClient>;
11
+ export declare function cancelRecordGeneration(operationId: string): Effect.Effect<RecordGenerationStatus, import("./api.js").ApiConfigurationError | import("./api.js").ApiHttpError | import("./api.js").ApiTransportError | import("./api.js").ApiDecodeError, PrismantixApiClient>;
12
+ export declare function waitForRecordGeneration(created: RecordGenerationCreated, options: WaitForGenerationOptions): Effect.Effect<RecordGenerationStatus, unknown, PrismantixApiClient>;
13
+ //# sourceMappingURL=generation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"generation.d.ts","sourceRoot":"","sources":["../src/generation.ts"],"names":[],"mappings":"AAAA,OAAO,EAAuB,MAAM,EAAgB,MAAM,QAAQ,CAAC;AACnE,OAAO,EACN,KAAK,uBAAuB,EAC5B,KAAK,sBAAsB,EAC3B,mBAAmB,EACnB,MAAM,UAAU,CAAC;AAKlB,MAAM,WAAW,wBAAwB;IACxC,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IAC5B,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC,YAAY,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACzD,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC3B;AAED,wBAAgB,sBAAsB,CACrC,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,EAClB,cAAc,EAAE,MAAM,sNAMtB;AAED,wBAAgB,mBAAmB,CAAC,WAAW,EAAE,MAAM,qNAKtD;AAED,wBAAgB,sBAAsB,CAAC,WAAW,EAAE,MAAM,qNAKzD;AAED,wBAAgB,uBAAuB,CACtC,OAAO,EAAE,uBAAuB,EAChC,OAAO,EAAE,wBAAwB,uEASjC"}
@@ -0,0 +1,77 @@
1
+ import { Cause, Effect, Exit, Option } from "effect";
2
+ import { PrismantixApiClient, } from "./api.js";
3
+ import { CliUserError } from "./errors.js";
4
+ export function createRecordGeneration(clientId, ontologyId, idempotencyKey) {
5
+ return Effect.gen(function* () {
6
+ const api = yield* PrismantixApiClient;
7
+ return yield* api.createRecordGeneration(clientId, ontologyId, idempotencyKey);
8
+ });
9
+ }
10
+ export function getRecordGeneration(operationId) {
11
+ return Effect.gen(function* () {
12
+ const api = yield* PrismantixApiClient;
13
+ return yield* api.getRecordGeneration(operationId);
14
+ });
15
+ }
16
+ export function cancelRecordGeneration(operationId) {
17
+ return Effect.gen(function* () {
18
+ const api = yield* PrismantixApiClient;
19
+ return yield* api.cancelRecordGeneration(operationId);
20
+ });
21
+ }
22
+ export function waitForRecordGeneration(created, options) {
23
+ return Effect.gen(function* () {
24
+ const api = yield* PrismantixApiClient;
25
+ return yield* Effect.tryPromise({
26
+ try: () => waitForTerminalStatus(api, created.operationId, options),
27
+ catch: (error) => error,
28
+ });
29
+ });
30
+ }
31
+ async function waitForTerminalStatus(api, operationId, options) {
32
+ const now = resolveNow(options.now);
33
+ const sleep = resolveSleep(options.sleep);
34
+ const startedAt = now();
35
+ for (;;) {
36
+ const status = await runClientEffect(api.getRecordGeneration(operationId));
37
+ if (isTerminal(status)) {
38
+ return requireCompletedGeneration(status);
39
+ }
40
+ throwIfTimedOut(operationId, now() - startedAt, options.timeoutMs);
41
+ await sleep(options.pollIntervalMs);
42
+ }
43
+ }
44
+ function resolveNow(now) {
45
+ return now ?? (() => Date.now());
46
+ }
47
+ function resolveSleep(sleep) {
48
+ return sleep ?? ((milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds)));
49
+ }
50
+ function isTerminal(status) {
51
+ return ["canceled", "completed", "failed"].includes(status.status);
52
+ }
53
+ function requireCompletedGeneration(status) {
54
+ if (status.status === "completed")
55
+ return status;
56
+ if (status.status === "canceled") {
57
+ throw new CliUserError("record_generation_canceled", `Record generation ${status.operationId} was canceled.`);
58
+ }
59
+ const failureDetails = status.failure
60
+ ? `${status.failure.message} (${status.failure.code})`
61
+ : "The API did not provide failure details.";
62
+ throw new CliUserError("record_generation_failed", `Record generation ${status.operationId} failed: ${failureDetails}`);
63
+ }
64
+ function throwIfTimedOut(operationId, elapsedMs, timeoutMs) {
65
+ if (elapsedMs < timeoutMs)
66
+ return;
67
+ throw new CliUserError("timeout", `Timed out waiting for record generation ${operationId}.`);
68
+ }
69
+ async function runClientEffect(effect) {
70
+ const exit = await Effect.runPromiseExit(effect);
71
+ if (Exit.isSuccess(exit))
72
+ return exit.value;
73
+ const failure = Cause.findErrorOption(exit.cause);
74
+ if (Option.isSome(failure))
75
+ throw failure.value;
76
+ throw Cause.squash(exit.cause);
77
+ }
package/dist/io.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"io.d.ts","sourceRoot":"","sources":["../src/io.ts"],"names":[],"mappings":"AAKA,wBAAsB,uBAAuB,CAC5C,QAAQ,EAAE,QAAQ,EAClB,IAAI,EAAE,MAAM,mBAuBZ"}
1
+ {"version":3,"file":"io.d.ts","sourceRoot":"","sources":["../src/io.ts"],"names":[],"mappings":"AAKA,wBAAsB,uBAAuB,CAAC,QAAQ,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,mBAkB7E"}
package/dist/io.js CHANGED
@@ -15,6 +15,6 @@ export async function writeResponseBodyToFile(response, path) {
15
15
  callback(null, chunk);
16
16
  },
17
17
  });
18
- await pipeline(Readable.fromWeb(response.body), byteCounter, createWriteStream(path));
18
+ await pipeline(Readable.from(response.body), byteCounter, createWriteStream(path));
19
19
  return sizeBytes;
20
20
  }
@@ -0,0 +1,3 @@
1
+ import { Effect, type Layer } from "effect";
2
+ export declare const runCliEffect: <A, E, R>(effect: Effect.Effect<A, E, R>, layer: Layer.Layer<R, unknown, never>) => Promise<A>;
3
+ //# sourceMappingURL=runtime.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../src/runtime.ts"],"names":[],"mappings":"AAAA,OAAO,EAAS,MAAM,EAAQ,KAAK,KAAK,EAAU,MAAM,QAAQ,CAAC;AAEjE,eAAO,MAAM,YAAY,GAAU,CAAC,EAAE,CAAC,EAAE,CAAC,EACzC,QAAQ,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,EAC9B,OAAO,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,OAAO,EAAE,KAAK,CAAC,eAQrC,CAAC"}
@@ -0,0 +1,10 @@
1
+ import { Cause, Effect, Exit, Option } from "effect";
2
+ export const runCliEffect = async (effect, layer) => {
3
+ const exit = await Effect.runPromiseExit(effect.pipe(Effect.provide(layer)));
4
+ if (Exit.isSuccess(exit))
5
+ return exit.value;
6
+ const failure = Cause.findErrorOption(exit.cause);
7
+ if (Option.isSome(failure))
8
+ throw failure.value;
9
+ throw Cause.squash(exit.cause);
10
+ };
@@ -1,8 +1,7 @@
1
1
  import { mkdir, writeFile } from "node:fs/promises";
2
- import { type ExportManifest, type ExportManifestFile, type ExportStatus } from "./api.js";
2
+ import { Effect } from "effect";
3
+ import { type ExportManifest, type ExportManifestFile, type ExportStatus, PrismantixApiClient } from "./api.js";
3
4
  export interface DownloadWorkflowOptions {
4
- readonly apiBaseUrl?: string;
5
- readonly apiKey?: string;
6
5
  readonly clientId?: string;
7
6
  readonly exportId?: string;
8
7
  readonly outputDir?: string;
@@ -12,7 +11,6 @@ export interface DownloadWorkflowOptions {
12
11
  }
13
12
  export interface DownloadWorkflowRuntime {
14
13
  readonly cwd?: string;
15
- readonly fetchImpl?: typeof fetch;
16
14
  readonly mkdir?: typeof mkdir;
17
15
  readonly now?: () => number;
18
16
  readonly onEvent?: (event: WorkflowEvent) => void;
@@ -51,14 +49,5 @@ export interface DownloadResult {
51
49
  readonly manifestPath: string;
52
50
  readonly outputDir: string;
53
51
  }
54
- export interface ManifestSummary {
55
- readonly fileCount: number;
56
- readonly tables: readonly {
57
- readonly rowCount?: number;
58
- readonly tableName: string;
59
- }[];
60
- readonly totalRows: number;
61
- }
62
- export declare function runDownloadWorkflow(options: DownloadWorkflowOptions, runtime?: DownloadWorkflowRuntime): Promise<DownloadResult>;
63
- export declare function summarizeManifest(manifest: ExportManifest): ManifestSummary;
52
+ export declare function runDownloadWorkflowEffect(options: DownloadWorkflowOptions, runtime?: DownloadWorkflowRuntime): Effect.Effect<DownloadResult, unknown, PrismantixApiClient>;
64
53
  //# sourceMappingURL=workflow.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"workflow.d.ts","sourceRoot":"","sources":["../src/workflow.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAEpD,OAAO,EACN,KAAK,cAAc,EACnB,KAAK,kBAAkB,EACvB,KAAK,YAAY,EAEjB,MAAM,UAAU,CAAC;AAUlB,MAAM,WAAW,uBAAuB;IACvC,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,MAAM,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACpC,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED,MAAM,WAAW,uBAAuB;IACvC,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;IAClC,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,KAAK,CAAC;IAC9B,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IAC5B,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,CAAC;IAClD,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/C,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,SAAS,CAAC;IACtC,QAAQ,CAAC,iBAAiB,CAAC,EAAE,CAC5B,QAAQ,EAAE,QAAQ,EAClB,IAAI,EAAE,MAAM,KACR,OAAO,CAAC,MAAM,CAAC,CAAC;CACrB;AAED,MAAM,MAAM,aAAa,GACtB;IACA,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;CACxB,GACD;IACA,QAAQ,CAAC,MAAM,EAAE,YAAY,CAAC;IAC9B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACrB,GACD;IACA,QAAQ,CAAC,QAAQ,EAAE,cAAc,CAAC;IAClC,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC;CACzB,GACD;IACA,QAAQ,CAAC,IAAI,EAAE,kBAAkB,CAAC;IAClC,QAAQ,CAAC,IAAI,EAAE,gBAAgB,CAAC;CAC/B,GACD;IACA,QAAQ,CAAC,IAAI,EAAE,cAAc,CAAC;IAC9B,QAAQ,CAAC,IAAI,EAAE,mBAAmB,CAAC;CAClC,CAAC;AAEL,MAAM,WAAW,cAAc;IAC9B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,WAAW,cAAc;IAC9B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,KAAK,EAAE,SAAS,cAAc,EAAE,CAAC;IAC1C,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,WAAW,eAAe;IAC/B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,MAAM,EAAE,SAAS;QACzB,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;QAC3B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;KAC3B,EAAE,CAAC;IACJ,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC3B;AAED,wBAAsB,mBAAmB,CACxC,OAAO,EAAE,uBAAuB,EAChC,OAAO,GAAE,uBAA4B,GACnC,OAAO,CAAC,cAAc,CAAC,CAyGzB;AAED,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,cAAc,GAAG,eAAe,CAS3E"}
1
+ {"version":3,"file":"workflow.d.ts","sourceRoot":"","sources":["../src/workflow.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAEpD,OAAO,EAAuB,MAAM,EAAgB,MAAM,QAAQ,CAAC;AACnE,OAAO,EACN,KAAK,cAAc,EACnB,KAAK,kBAAkB,EACvB,KAAK,YAAY,EACjB,mBAAmB,EACnB,MAAM,UAAU,CAAC;AASlB,MAAM,WAAW,uBAAuB;IACvC,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,MAAM,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACpC,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED,MAAM,WAAW,uBAAuB;IACvC,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,KAAK,CAAC;IAC9B,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IAC5B,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,CAAC;IAClD,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/C,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,SAAS,CAAC;IACtC,QAAQ,CAAC,iBAAiB,CAAC,EAAE,CAAC,QAAQ,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;CACnF;AAED,MAAM,MAAM,aAAa,GACtB;IACA,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;CACxB,GACD;IACA,QAAQ,CAAC,MAAM,EAAE,YAAY,CAAC;IAC9B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACrB,GACD;IACA,QAAQ,CAAC,QAAQ,EAAE,cAAc,CAAC;IAClC,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC;CACzB,GACD;IACA,QAAQ,CAAC,IAAI,EAAE,kBAAkB,CAAC;IAClC,QAAQ,CAAC,IAAI,EAAE,gBAAgB,CAAC;CAC/B,GACD;IACA,QAAQ,CAAC,IAAI,EAAE,cAAc,CAAC;IAC9B,QAAQ,CAAC,IAAI,EAAE,mBAAmB,CAAC;CAClC,CAAC;AAEL,MAAM,WAAW,cAAc;IAC9B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,WAAW,cAAc;IAC9B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,KAAK,EAAE,SAAS,cAAc,EAAE,CAAC;IAC1C,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC3B;AAED,wBAAgB,yBAAyB,CACxC,OAAO,EAAE,uBAAuB,EAChC,OAAO,GAAE,uBAA4B,+DASrC"}
package/dist/workflow.js CHANGED
@@ -1,37 +1,32 @@
1
1
  import { mkdir, writeFile } from "node:fs/promises";
2
2
  import { basename, join, resolve } from "node:path";
3
+ import { Cause, Effect, Exit, Option } from "effect";
3
4
  import { PrismantixApiClient, } from "./api.js";
4
- import { DEFAULT_API_BASE_URL, DEFAULT_OUTPUT_ROOT, DEFAULT_POLL_INTERVAL_MS, DEFAULT_TIMEOUT_MS, } from "./constants.js";
5
+ import { DEFAULT_OUTPUT_ROOT, DEFAULT_POLL_INTERVAL_MS, DEFAULT_PULL_TIMEOUT_MS, } from "./constants.js";
5
6
  import { CliUserError } from "./errors.js";
6
7
  import { writeResponseBodyToFile } from "./io.js";
7
- export async function runDownloadWorkflow(options, runtime = {}) {
8
- const apiKey = options.apiKey?.trim();
9
- if (!apiKey) {
10
- throw new CliUserError("missing_api_key", "Set PRISMANTIX_API_KEY before running the CLI.");
11
- }
12
- const sleep = runtime.sleep ??
13
- ((ms) => new Promise((resolveSleep) => setTimeout(resolveSleep, ms)));
14
- const now = runtime.now ?? (() => Date.now());
15
- const mkdirImpl = runtime.mkdir ?? mkdir;
16
- const writeFileImpl = runtime.writeFile ?? writeFile;
17
- const writeResponseBody = runtime.writeResponseBody ?? writeResponseBodyToFile;
18
- const emit = runtime.onEvent ?? (() => undefined);
19
- const api = new PrismantixApiClient({
20
- apiBaseUrl: options.apiBaseUrl ?? DEFAULT_API_BASE_URL,
21
- apiKey,
22
- fetchImpl: runtime.fetchImpl,
8
+ export function runDownloadWorkflowEffect(options, runtime = {}) {
9
+ return Effect.gen(function* () {
10
+ const api = yield* PrismantixApiClient;
11
+ return yield* Effect.tryPromise({
12
+ try: () => runDownloadWorkflowPromise(api, options, runtime),
13
+ catch: (error) => error,
14
+ });
23
15
  });
24
- let clientId = normalizeClientId(options.clientId);
25
- let exportId = options.exportId?.trim();
26
- if (!exportId) {
27
- if (!clientId) {
28
- throw new CliUserError("missing_client", "Pass --client when creating a new export.");
29
- }
30
- const created = await api.createExport(clientId);
31
- clientId = created.clientId;
32
- exportId = created.exportId;
33
- emit({ clientId, exportId, type: "created" });
34
- }
16
+ }
17
+ const resolveWorkflowRuntime = (runtime) => ({
18
+ emit: runtime.onEvent ?? (() => undefined),
19
+ mkdirImpl: runtime.mkdir ?? mkdir,
20
+ now: runtime.now ?? (() => Date.now()),
21
+ sleep: runtime.sleep ?? ((ms) => new Promise((resolveSleep) => setTimeout(resolveSleep, ms))),
22
+ writeFileImpl: runtime.writeFile ?? writeFile,
23
+ writeResponseBody: runtime.writeResponseBody ?? writeResponseBodyToFile,
24
+ });
25
+ async function runDownloadWorkflowPromise(api, options, runtime = {}) {
26
+ const { emit, mkdirImpl, now, sleep, writeFileImpl, writeResponseBody } = resolveWorkflowRuntime(runtime);
27
+ const resolvedExport = await resolveExport(api, options, emit);
28
+ let clientId = resolvedExport.clientId;
29
+ const exportId = resolvedExport.exportId;
35
30
  const status = await waitForCompletedExport({
36
31
  api,
37
32
  exportId,
@@ -39,10 +34,10 @@ export async function runDownloadWorkflow(options, runtime = {}) {
39
34
  onPoll: (polledStatus) => emit({ status: polledStatus, type: "poll" }),
40
35
  pollIntervalMs: options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS,
41
36
  sleep,
42
- timeoutMs: options.timeoutMs ?? DEFAULT_TIMEOUT_MS,
37
+ timeoutMs: options.timeoutMs ?? DEFAULT_PULL_TIMEOUT_MS,
43
38
  });
44
39
  clientId = status.clientId;
45
- const manifest = await api.getManifest(exportId);
40
+ const manifest = await runClientEffect(api.getManifest(exportId));
46
41
  emit({ manifest, type: "manifest" });
47
42
  const outputDir = resolveOutputDir({
48
43
  clientId,
@@ -55,26 +50,15 @@ export async function runDownloadWorkflow(options, runtime = {}) {
55
50
  await writeFileImpl(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, {
56
51
  encoding: "utf8",
57
52
  });
58
- const selectedFiles = selectManifestFiles(manifest.files, options.tables ?? []);
59
- if (selectedFiles.length === 0) {
60
- throw new CliUserError("no_matching_files", `No export files matched: ${(options.tables ?? []).join(", ")}`);
61
- }
62
- const files = [];
63
- for (const file of selectedFiles) {
64
- emit({ file, type: "download-start" });
65
- const destinationPath = join(outputDir, basename(file.fileName));
66
- const response = await api.getFile(exportId, file.tableName);
67
- const writtenSizeBytes = await writeResponseBody(response, destinationPath);
68
- const downloadedFile = {
69
- path: destinationPath,
70
- rowCount: file.rowCount,
71
- sha256: file.sha256,
72
- sizeBytes: file.sizeBytes ?? writtenSizeBytes,
73
- tableName: file.tableName,
74
- };
75
- files.push(downloadedFile);
76
- emit({ file: downloadedFile, type: "download-complete" });
77
- }
53
+ const selectedFiles = requireSelectedFiles(manifest.files, options.tables ?? []);
54
+ const files = await downloadFiles({
55
+ api,
56
+ emit,
57
+ exportId,
58
+ files: selectedFiles,
59
+ outputDir,
60
+ writeResponseBody,
61
+ });
78
62
  return {
79
63
  clientId,
80
64
  exportId,
@@ -83,16 +67,6 @@ export async function runDownloadWorkflow(options, runtime = {}) {
83
67
  outputDir,
84
68
  };
85
69
  }
86
- export function summarizeManifest(manifest) {
87
- return {
88
- fileCount: manifest.files.length,
89
- tables: manifest.files.map((file) => ({
90
- rowCount: file.rowCount,
91
- tableName: file.tableName,
92
- })),
93
- totalRows: manifest.totalRows,
94
- };
95
- }
96
70
  function normalizeClientId(value) {
97
71
  const normalized = value?.trim();
98
72
  return normalized ? normalized.toUpperCase() : undefined;
@@ -100,7 +74,7 @@ function normalizeClientId(value) {
100
74
  async function waitForCompletedExport(args) {
101
75
  const startedAt = args.now();
102
76
  for (;;) {
103
- const status = await args.api.getExportStatus(args.exportId);
77
+ const status = await runClientEffect(args.api.getExportStatus(args.exportId));
104
78
  args.onPoll(status);
105
79
  if (status.status === "completed") {
106
80
  return status;
@@ -114,6 +88,50 @@ async function waitForCompletedExport(args) {
114
88
  await args.sleep(args.pollIntervalMs);
115
89
  }
116
90
  }
91
+ async function runClientEffect(effect) {
92
+ const exit = await Effect.runPromiseExit(effect);
93
+ if (Exit.isSuccess(exit))
94
+ return exit.value;
95
+ const failure = Cause.findErrorOption(exit.cause);
96
+ if (Option.isSome(failure))
97
+ throw failure.value;
98
+ throw Cause.squash(exit.cause);
99
+ }
100
+ async function resolveExport(api, options, emit) {
101
+ const clientId = normalizeClientId(options.clientId);
102
+ const exportId = options.exportId?.trim();
103
+ if (exportId)
104
+ return { clientId, exportId };
105
+ if (!clientId) {
106
+ throw new CliUserError("missing_client", "Pass --client when creating a new export.");
107
+ }
108
+ const created = await runClientEffect(api.createExport(clientId));
109
+ emit({
110
+ clientId: created.clientId,
111
+ exportId: created.exportId,
112
+ type: "created",
113
+ });
114
+ return { clientId: created.clientId, exportId: created.exportId };
115
+ }
116
+ async function downloadFiles(args) {
117
+ const downloadedFiles = [];
118
+ for (const file of args.files) {
119
+ args.emit({ file, type: "download-start" });
120
+ const destinationPath = join(args.outputDir, basename(file.fileName));
121
+ const response = await runClientEffect(args.api.getFile(args.exportId, file.tableName));
122
+ const writtenSizeBytes = await args.writeResponseBody(response, destinationPath);
123
+ const downloadedFile = {
124
+ path: destinationPath,
125
+ rowCount: file.rowCount,
126
+ sha256: file.sha256,
127
+ sizeBytes: file.sizeBytes ?? writtenSizeBytes,
128
+ tableName: file.tableName,
129
+ };
130
+ downloadedFiles.push(downloadedFile);
131
+ args.emit({ file: downloadedFile, type: "download-complete" });
132
+ }
133
+ return downloadedFiles;
134
+ }
117
135
  function resolveOutputDir(args) {
118
136
  return resolve(args.cwd, args.outputDir ?? join(DEFAULT_OUTPUT_ROOT, args.clientId, args.exportId));
119
137
  }
@@ -128,3 +146,9 @@ function selectManifestFiles(files, tables) {
128
146
  const allowedTables = new Set(normalizedTables);
129
147
  return files.filter((file) => allowedTables.has(file.tableName));
130
148
  }
149
+ function requireSelectedFiles(files, tables) {
150
+ const selectedFiles = selectManifestFiles(files, tables);
151
+ if (selectedFiles.length > 0)
152
+ return selectedFiles;
153
+ throw new CliUserError("no_matching_files", `No export files matched: ${tables.join(", ")}`);
154
+ }