Skip to content

Commit de2e28b

Browse files
committed
Support importing at data path
1 parent 077cc97 commit de2e28b

File tree

4 files changed

+85
-15
lines changed

4 files changed

+85
-15
lines changed

npm-shrinkwrap.json

Lines changed: 19 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/commands/database-import.ts

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ export const command = new Command("database:import <path> [infile]")
2626
"suppress any Cloud functions triggered by this operation, default to true",
2727
true
2828
)
29+
.option(
30+
"--only <dataPath>",
31+
"import only data at this path in the JSON file (if omitted, use root)"
32+
)
2933
.before(requirePermissions, ["firebasedatabase.instances.update"])
3034
.before(requireDatabaseInstance)
3135
.before(populateInstanceDetails)
@@ -35,6 +39,10 @@ export const command = new Command("database:import <path> [infile]")
3539
throw new FirebaseError("Path must begin with /");
3640
}
3741

42+
if (options.only && !options.only.startsWith("/")) {
43+
throw new FirebaseError("Data path must begin with /");
44+
}
45+
3846
if (!infile) {
3947
throw new FirebaseError("No file supplied");
4048
}
@@ -60,9 +68,12 @@ export const command = new Command("database:import <path> [infile]")
6068
}
6169

6270
const inStream = fs.createReadStream(infile);
63-
const importer = new DatabaseImporter(dbUrl, inStream);
71+
const dataPath = options.only || "/";
72+
const importer = new DatabaseImporter(dbUrl, inStream, dataPath);
73+
74+
let responses;
6475
try {
65-
await importer.execute();
76+
responses = await importer.execute();
6677
} catch (err: any) {
6778
if (err instanceof FirebaseError) {
6879
throw err;
@@ -71,7 +82,12 @@ export const command = new Command("database:import <path> [infile]")
7182
throw new FirebaseError(`Unexpected error while importing data: ${err}`, { exit: 2 });
7283
}
7384

74-
utils.logSuccess("Data persisted successfully");
85+
if (responses.length) {
86+
utils.logSuccess("Data persisted successfully");
87+
} else {
88+
utils.logWarning("No data was persisted. Check the data path supplied.");
89+
}
90+
7591
logger.info();
7692
logger.info(
7793
clc.bold("View data at:"),

src/database/import.ts

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import * as clc from "colorette";
22
import * as stream from "stream";
33
import pLimit from "p-limit";
4+
45
import { URL } from "url";
56
import { Client } from "../apiv2";
67
import { FirebaseError } from "../error";
@@ -26,14 +27,17 @@ type ChunkedData = {
2627
* The data is parsed and chunked into subtrees of ~1 MB, to be subsequently written in parallel.
2728
*/
2829
export default class DatabaseImporter {
30+
private jsonPath: string;
2931
private client: Client;
3032
private limit = pLimit(CONCURRENCY_LIMIT);
3133

3234
constructor(
3335
private dbUrl: URL,
3436
private inStream: NodeJS.ReadableStream,
37+
dataPath: string,
3538
private chunkSize = MAX_CHUNK_SIZE
3639
) {
40+
this.jsonPath = this.computeJsonPath(dataPath);
3741
this.client = new Client({ urlPrefix: dbUrl.origin, auth: true });
3842
}
3943

@@ -66,7 +70,7 @@ export default class DatabaseImporter {
6670
const { dbUrl } = this;
6771
const chunkData = this.chunkData.bind(this);
6872
const writeChunk = this.writeChunk.bind(this);
69-
const getJoinedPath = this.getJoinedPath.bind(this);
73+
const getJoinedPath = this.joinPath.bind(this);
7074

7175
const readChunks = new stream.Transform({ objectMode: true });
7276
readChunks._transform = function (chunk: { key: string; value: any }, _, done) {
@@ -85,9 +89,9 @@ export default class DatabaseImporter {
8589
};
8690

8791
return new Promise((resolve, reject) => {
88-
const results: any[] = [];
92+
const responses: any[] = [];
8993
inStream
90-
.pipe(JSONStream.parse("$*"))
94+
.pipe(JSONStream.parse(this.jsonPath))
9195
.on("error", (err: any) =>
9296
reject(
9397
new FirebaseError("Invalid data; couldn't parse JSON object, array, or value.", {
@@ -98,9 +102,9 @@ export default class DatabaseImporter {
98102
)
99103
.pipe(readChunks)
100104
.pipe(writeChunks)
101-
.on("data", (res: any) => results.push(res))
105+
.on("data", (res: any) => responses.push(res))
102106
.on("error", reject)
103-
.once("end", () => resolve(results));
107+
.once("end", () => resolve(responses));
104108
});
105109
}
106110

@@ -129,7 +133,7 @@ export default class DatabaseImporter {
129133
for (const key of Object.keys(json)) {
130134
size += key.length + 3; // "":
131135

132-
const child = { json: json[key], pathname: this.getJoinedPath(pathname, key) };
136+
const child = { json: json[key], pathname: this.joinPath(pathname, key) };
133137
const childChunks = this.chunkData(child);
134138
size += childChunks.size;
135139
if (childChunks.chunks) {
@@ -148,7 +152,15 @@ export default class DatabaseImporter {
148152
}
149153
}
150154

151-
private getJoinedPath(root: string, key: string): string {
155+
private computeJsonPath(dataPath: string): string {
156+
if (dataPath === "/") {
157+
return "$*";
158+
} else {
159+
return `${dataPath.split("/").slice(1).join(".")}.$*`;
160+
}
161+
}
162+
163+
private joinPath(root: string, key: string): string {
152164
return [root, key].join("/").replace("//", "/");
153165
}
154166
}

src/test/database/import.spec.ts

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { FirebaseError } from "../../error";
77

88
const dbUrl = new URL("https://test-db.firebaseio.com/foo");
99

10-
describe("DatabaseImporter", () => {
10+
describe.only("DatabaseImporter", () => {
1111
const DATA = { a: 100, b: [true, "bar", { f: { g: 0, h: 1 }, i: "baz" }] };
1212
let DATA_STREAM: NodeJS.ReadableStream;
1313

@@ -19,7 +19,11 @@ describe("DatabaseImporter", () => {
1919
nock("https://test-db.firebaseio.com").get("/foo.json?shallow=true").reply(200);
2020

2121
const INVALID_JSON = '{"a": {"b"}}';
22-
const importer = new DatabaseImporter(dbUrl, utils.stringToStream(INVALID_JSON)!);
22+
const importer = new DatabaseImporter(
23+
dbUrl,
24+
utils.stringToStream(INVALID_JSON)!,
25+
/* importPath= */ "/"
26+
);
2327
await expect(importer.execute()).to.be.rejectedWith(
2428
FirebaseError,
2529
"Invalid data; couldn't parse JSON object, array, or value."
@@ -33,7 +37,7 @@ describe("DatabaseImporter", () => {
3337
.put("/foo/b.json", JSON.stringify([true, "bar", { f: { g: 0, h: 1 }, i: "baz" }]))
3438
.reply(200);
3539

36-
const importer = new DatabaseImporter(dbUrl, DATA_STREAM);
40+
const importer = new DatabaseImporter(dbUrl, DATA_STREAM, /* importPath= */ "/");
3741
const responses = await importer.execute();
3842
expect(responses).to.have.length(2);
3943
expect(nock.isDone()).to.be.true;
@@ -49,15 +53,34 @@ describe("DatabaseImporter", () => {
4953
.reply(200);
5054
nock("https://test-db.firebaseio.com").put("/foo/b/2/i.json", '"baz"').reply(200);
5155

52-
const importer = new DatabaseImporter(dbUrl, DATA_STREAM, /* chunkSize= */ 20);
56+
const importer = new DatabaseImporter(
57+
dbUrl,
58+
DATA_STREAM,
59+
/* importPath= */ "/",
60+
/* chunkSize= */ 20
61+
);
5362
const responses = await importer.execute();
5463
expect(responses).to.have.length(5);
5564
expect(nock.isDone()).to.be.true;
5665
});
5766

67+
it("imports from data path", async () => {
68+
nock("https://test-db.firebaseio.com").get("/foo.json?shallow=true").reply(200);
69+
nock("https://test-db.firebaseio.com").put("/foo/0.json", "true").reply(200);
70+
nock("https://test-db.firebaseio.com").put("/foo/1.json", '"bar"').reply(200);
71+
nock("https://test-db.firebaseio.com")
72+
.put("/foo/2.json", JSON.stringify({ f: { g: 0, h: 1 }, i: "baz" }))
73+
.reply(200);
74+
75+
const importer = new DatabaseImporter(dbUrl, DATA_STREAM, /* importPath= */ "/b");
76+
const responses = await importer.execute();
77+
expect(responses).to.have.length(3);
78+
expect(nock.isDone()).to.be.true;
79+
});
80+
5881
it("throws FirebaseError when data location is nonempty", async () => {
5982
nock("https://test-db.firebaseio.com").get("/foo.json?shallow=true").reply(200, { a: "foo" });
60-
const importer = new DatabaseImporter(dbUrl, DATA_STREAM);
83+
const importer = new DatabaseImporter(dbUrl, DATA_STREAM, /* importPath= */ "/");
6184
await expect(importer.execute()).to.be.rejectedWith(
6285
FirebaseError,
6386
/Importing is only allowed for an empty location./

0 commit comments

Comments
 (0)