Skip to content

Commit 6c009f8

Browse files
committed
FE-6300 Accept s3 URI as input for export and show s3 URI in output.
1 parent 3116623 commit 6c009f8

File tree

4 files changed

+192
-244
lines changed

4 files changed

+192
-244
lines changed

src/commands/export/create.mjs

+48-16
Original file line numberDiff line numberDiff line change
@@ -19,19 +19,24 @@ async function createS3Export(argv) {
1919
wait,
2020
maxWait,
2121
quiet,
22+
destination,
2223
} = argv;
2324
const logger = container.resolve("logger");
2425
const { createExport } = container.resolve("accountAPI");
25-
26-
let createdExport = await createExport({
27-
database,
28-
collections,
29-
destination: {
26+
let destinationInput = destination;
27+
if (!destinationInput) {
28+
destinationInput = {
3029
s3: {
3130
bucket,
3231
path,
3332
},
34-
},
33+
};
34+
}
35+
36+
let createdExport = await createExport({
37+
database,
38+
collections,
39+
destination: destinationInput,
3540
format,
3641
});
3742

@@ -44,7 +49,6 @@ async function createS3Export(argv) {
4449
},
4550
});
4651
}
47-
4852
if (json) {
4953
logger.stdout(colorize(createdExport, { color, format: Format.JSON }));
5054
} else {
@@ -54,39 +58,52 @@ async function createS3Export(argv) {
5458

5559
const sharedExamples = [
5660
[
57-
"$0 export create s3 --database us/my_db --bucket doc-example-bucket --path exports/my_db",
58-
"Export the 'us-std/my_db' database to the 'exports/my_db' path of the 'doc-example-bucket' S3 bucket. Outputs the export ID.",
61+
"$0 export create s3 --destination s3://doc-example-bucket/exports/my_db",
62+
"Export the 'us-std/my_db' database to the S3 URI 's3://doc-example-bucket/exports/my_db'.",
5963
],
6064
[
61-
"$0 export create s3 --database us/my_db --bucket doc-example-bucket --path my-prefix --json",
65+
"$0 export create s3 --bucket doc-example-bucket --path exports/my_db",
66+
"You can also specify the S3 location using --bucket and --path options rather than --destination.",
67+
],
68+
[
69+
"$0 export create s3 --destination s3://doc-example-bucket/my-prefix --json",
6270
"Output the full JSON of the export request.",
6371
],
6472
[
65-
"$0 export create s3 --database us/my_db --bucket doc-example-bucket --path my-prefix --collection my-collection",
73+
"$0 export create s3 --destination s3://doc-example-bucket/my-prefix --collection my-collection",
6674
"Export the 'my-collection' collection only.",
6775
],
6876
[
69-
"$0 export create s3 --database us/my_db --bucket doc-example-bucket --path my-prefix --format tagged",
77+
"$0 export create s3 --destination s3://doc-example-bucket/my-prefix --format tagged",
7078
"Encode the export's document data using the 'tagged' format.",
7179
],
7280
[
73-
"$0 export create s3 --database us/my_db --bucket doc-example-bucket --path my-prefix --wait --max-wait 180",
81+
"$0 export create s3 --destination s3://doc-example-bucket/my-prefix --wait --max-wait 180",
7482
"Wait for the export to complete or fail before exiting. Waits up to 180 minutes.",
7583
],
7684
];
7785

86+
const S3_URI_REGEX = /^s3:\/\/[^/]+\/.+$/;
87+
7888
function buildCreateS3ExportCommand(yargs) {
7989
return yargs
8090
.options({
91+
destination: {
92+
alias: ["uri", "destination-uri"],
93+
type: "string",
94+
required: false,
95+
description: "S3 URI in the format s3://bucket/path.",
96+
group: "API:",
97+
},
8198
bucket: {
8299
type: "string",
83-
required: true,
100+
required: false,
84101
description: "Name of the S3 bucket where the export will be stored.",
85102
group: "API:",
86103
},
87104
path: {
88105
type: "string",
89-
required: true,
106+
required: false,
90107
description:
91108
"Path prefix for the S3 bucket. Separate subfolders using a slash (`/`).",
92109
group: "API:",
@@ -108,7 +125,22 @@ function buildCreateS3ExportCommand(yargs) {
108125
"--database is required to create an export.",
109126
);
110127
}
111-
128+
if (argv.destination) {
129+
if (argv.bucket || argv.path) {
130+
throw new ValidationError(
131+
"Cannot specify --destination with --bucket or --path. Use either --destination or both --bucket and --path.",
132+
);
133+
}
134+
if (!S3_URI_REGEX.test(argv.destination)) {
135+
throw new ValidationError(
136+
"Invalid S3 URI format. Expected format: s3://bucket/path",
137+
);
138+
}
139+
} else if (!argv.bucket || !argv.path) {
140+
throw new ValidationError(
141+
"Either --destination or both --bucket and --path are required to create an export.",
142+
);
143+
}
112144
return true;
113145
})
114146
.example(sharedExamples);

src/lib/account-api.mjs

+1-1
Original file line numberDiff line numberDiff line change
@@ -450,7 +450,7 @@ async function createExport({
450450
});
451451

452452
const data = await responseHandler(response);
453-
return { ...data.response, destination_uri: getExportUri(data.response) }; // eslint-disable-line camelcase
453+
return { ...data.response, destination: data.response.destination.uri };
454454
}
455455

456456
/**

test/commands/export/create.mjs

+89-94
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ const createExportStub = (opts) => ({
2020
},
2121
created_at: "2025-01-02T22:59:51",
2222
updated_at: "2025-01-02T22:59:51",
23-
destination_uri: "",
2423
...opts,
2524
});
2625

@@ -34,113 +33,109 @@ describe("export create s3", () => {
3433
({ createExport } = container.resolve("accountAPI"));
3534
});
3635

37-
it("creates an export", async () => {
38-
const database = "us-std/example";
39-
const bucket = "test-bucket";
40-
const path = "/test/key";
41-
const stubbedResponse = createExportStub({
42-
database,
43-
destination: {
44-
s3: {
45-
path,
46-
bucket,
47-
},
48-
},
49-
format: "simple",
50-
});
51-
createExport.resolves(stubbedResponse);
52-
53-
await run(
54-
`export create s3 --database '${database}' --bucket '${bucket}' --path '${path}'`,
55-
container,
56-
);
57-
await stdout.waitForWritten();
36+
const scenarios = [
37+
{
38+
description: "using --destination",
39+
args: "--destination 's3://test-bucket/test/key'",
40+
expectedDestination: "s3://test-bucket/test/key",
41+
expectedDestArgs: "s3://test-bucket/test/key",
42+
},
43+
{
44+
description: "using --bucket and --path",
45+
args: "--bucket 'test-bucket' --path '/test/key'",
46+
expectedDestination: "s3://test-bucket/test/key",
47+
expectedDestArgs: { s3: { bucket: "test-bucket", path: "/test/key" } },
48+
},
49+
];
50+
51+
scenarios.forEach(
52+
({ description, args, expectedDestination, expectedDestArgs }) => {
53+
it(`creates an export ${description}`, async () => {
54+
const database = "us-std/example";
55+
const stubbedResponse = createExportStub({
56+
database,
57+
destination: expectedDestination,
58+
format: "simple",
59+
});
60+
createExport.resolves(stubbedResponse);
61+
62+
await run(
63+
`export create s3 --database '${database}' ${args}`,
64+
container,
65+
);
66+
await stdout.waitForWritten();
5867

59-
expect(stdout.getWritten()).to.equal(`id: test-export-id
68+
expect(stdout.getWritten()).to.equal(`id: test-export-id
6069
state: Pending
6170
database: us-std/example
6271
format: simple
63-
destination:
64-
s3:
65-
path: /test/key
66-
bucket: test-bucket
72+
destination: s3://test-bucket/test/key
6773
created_at: 2025-01-02T22:59:51
6874
updated_at: 2025-01-02T22:59:51
69-
destination_uri: ""
7075
`);
71-
expect(createExport).to.have.been.calledWith({
72-
database,
73-
collections: [],
74-
destination: {
75-
s3: {
76-
bucket,
77-
path,
78-
},
79-
},
80-
format: "simple",
81-
});
82-
});
76+
expect(createExport).to.have.been.calledWith({
77+
database,
78+
collections: [],
79+
destination: expectedDestArgs,
80+
format: "simple",
81+
});
82+
});
8383

84-
it("outputs the full response with --json", async () => {
85-
const database = "us-std/example";
86-
const bucket = "test-bucket";
87-
const path = "/test/key";
88-
const stubbedResponse = createExportStub({
89-
database,
90-
destination: {
91-
s3: {
92-
path,
93-
bucket,
94-
},
95-
},
96-
format: "simple",
97-
});
98-
createExport.resolves(stubbedResponse);
99-
100-
await run(
101-
`export create s3 --database '${database}' --bucket '${bucket}' --path '${path}' --json`,
102-
container,
103-
);
104-
await stdout.waitForWritten();
84+
it(`outputs the full response with --json ${description}`, async () => {
85+
const database = "us-std/example";
86+
const stubbedResponse = createExportStub({
87+
database,
88+
destination: expectedDestination,
89+
format: "simple",
90+
});
91+
createExport.resolves(stubbedResponse);
10592

106-
expect(stdout.getWritten()).to.equal(
107-
`${colorize(stubbedResponse, { format: Format.JSON })}\n`,
108-
);
109-
});
93+
await run(
94+
`export create s3 --database '${database}' ${args} --json`,
95+
container,
96+
);
97+
await stdout.waitForWritten();
11098

111-
it("passes the format to the account api", async () => {
112-
createExport.resolves(createExportStub({ format: "tagged" }));
113-
await run(
114-
`export create s3 --database 'us-std/example' --bucket 'test-bucket' --path 'test/key' --format 'tagged'`,
115-
container,
116-
);
117-
expect(createExport).to.have.been.calledWith(
118-
sinon.match({
119-
format: "tagged",
120-
}),
121-
);
122-
});
99+
expect(stdout.getWritten()).to.equal(
100+
`${colorize(stubbedResponse, { format: Format.JSON })}\n`,
101+
);
102+
});
123103

124-
it("should allow providing multiple collections", async () => {
125-
createExport.resolves(createExportStub({ collections: ["foo", "bar"] }));
126-
await run(
127-
`export create s3 --database 'us-std/example' --bucket 'test-bucket' --path 'test/key' --collection foo --collection bar`,
128-
container,
129-
);
130-
expect(createExport).to.have.been.calledWith(
131-
sinon.match({
132-
database: "us-std/example",
133-
collections: ["foo", "bar"],
134-
}),
135-
);
136-
});
104+
it(`passes the format to the account api ${description}`, async () => {
105+
createExport.resolves(createExportStub({ format: "tagged" }));
106+
await run(
107+
`export create s3 --database 'us-std/example' ${args} --format 'tagged'`,
108+
container,
109+
);
110+
expect(createExport).to.have.been.calledWith(
111+
sinon.match({
112+
format: "tagged",
113+
}),
114+
);
115+
});
116+
117+
it(`should allow providing multiple collections ${description}`, async () => {
118+
createExport.resolves(
119+
createExportStub({ collections: ["foo", "bar"] }),
120+
);
121+
await run(
122+
`export create s3 --database 'us-std/example' ${args} --collection foo --collection bar`,
123+
container,
124+
);
125+
expect(createExport).to.have.been.calledWith(
126+
sinon.match({
127+
database: "us-std/example",
128+
collections: ["foo", "bar"],
129+
}),
130+
);
131+
});
132+
},
133+
);
137134

138135
it("should output an error if --database is not provided", async () => {
136+
const destination = "s3://test-bucket/test/key";
139137
try {
140-
await run(
141-
"export create s3 --bucket test-bucket --path test/key",
142-
container,
143-
);
138+
await run(`export create s3 --destination '${destination}'`, container);
144139
} catch {}
145140

146141
await stderr.waitForWritten();

0 commit comments

Comments
 (0)