Skip to content

Commit 4956dcc

Browse files
committed
Address PR feedback
1 parent 90991b9 commit 4956dcc

File tree

5 files changed

+59
-81
lines changed

5 files changed

+59
-81
lines changed

npm-shrinkwrap.json

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

package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,6 @@
120120
"google-auth-library": "^7.11.0",
121121
"inquirer": "^8.2.0",
122122
"js-yaml": "^3.13.1",
123-
"JSONStream": "^1.3.5",
124123
"jsonwebtoken": "^9.0.0",
125124
"leven": "^3.1.0",
126125
"libsodium-wrappers": "^0.7.10",

src/commands/database-import.ts

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,27 @@ import DatabaseImporter from "../database/import";
77
import { Emulators } from "../emulator/types";
88
import { FirebaseError } from "../error";
99
import { logger } from "../logger";
10+
import { needProjectId } from "../projectUtils";
11+
import { Options } from "../options";
1012
import { printNoticeIfEmulated } from "../emulator/commandUtils";
1113
import { promptOnce } from "../prompt";
12-
import { populateInstanceDetails } from "../management/database";
14+
import { DatabaseInstance, populateInstanceDetails } from "../management/database";
1315
import { realtimeOriginOrEmulatorOrCustomUrl } from "../database/api";
1416
import { requireDatabaseInstance } from "../requireDatabaseInstance";
1517
import { requirePermissions } from "../requirePermissions";
1618

19+
interface DatabaseImportOptions extends Options {
20+
instance: string;
21+
instanceDetails: DatabaseInstance;
22+
disableTriggers?: boolean;
23+
filter?: string;
24+
}
25+
1726
export const command = new Command("database:import <path> [infile]")
18-
.description("non-atomically import JSON file to the specified path")
19-
.option("-f, --force", "pass this option to bypass confirmation prompt")
27+
.description(
28+
"non-atomically import the contents of a JSON file to the specified path in Realtime Database"
29+
)
30+
.withForce()
2031
.option(
2132
"--instance <instance>",
2233
"use the database <instance>.firebaseio.com (if omitted, use default database instance)"
@@ -27,26 +38,23 @@ export const command = new Command("database:import <path> [infile]")
2738
true
2839
)
2940
.option(
30-
"--only <dataPath>",
41+
"--filter <dataPath>",
3142
"import only data at this path in the JSON file (if omitted, import entire file)"
3243
)
3344
.before(requirePermissions, ["firebasedatabase.instances.update"])
3445
.before(requireDatabaseInstance)
3546
.before(populateInstanceDetails)
3647
.before(printNoticeIfEmulated, Emulators.DATABASE)
37-
.action(async (path: string, infile, options) => {
48+
.action(async (path: string, infile: string | undefined, options: DatabaseImportOptions) => {
3849
if (!path.startsWith("/")) {
3950
throw new FirebaseError("Path must begin with /");
4051
}
4152

42-
if (options.only && !options.only.startsWith("/")) {
43-
throw new FirebaseError("Data path must begin with /");
44-
}
45-
4653
if (!infile) {
4754
throw new FirebaseError("No file supplied");
4855
}
4956

57+
const projectId = needProjectId(options);
5058
const origin = realtimeOriginOrEmulatorOrCustomUrl(options.instanceDetails.databaseUrl);
5159
const dbPath = utils.getDatabaseUrl(origin, options.instance, path);
5260
const dbUrl = new URL(dbPath);
@@ -68,7 +76,7 @@ export const command = new Command("database:import <path> [infile]")
6876
}
6977

7078
const inStream = fs.createReadStream(infile);
71-
const dataPath = options.only || "/";
79+
const dataPath = options.filter || "";
7280
const importer = new DatabaseImporter(dbUrl, inStream, dataPath);
7381

7482
let responses;
@@ -91,6 +99,6 @@ export const command = new Command("database:import <path> [infile]")
9199
logger.info();
92100
logger.info(
93101
clc.bold("View data at:"),
94-
utils.getDatabaseViewDataUrl(origin, options.project, options.instance, path)
102+
utils.getDatabaseViewDataUrl(origin, projectId, options.instance, path)
95103
);
96104
});

src/database/import.ts

Lines changed: 28 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
1+
import * as Chain from "stream-chain";
12
import * as clc from "colorette";
3+
import * as Filter from "stream-json/filters/Filter";
24
import * as stream from "stream";
5+
import * as StreamObject from "stream-json/streamers/StreamObject";
6+
37
import pLimit from "p-limit";
48

59
import { URL } from "url";
6-
import { Client } from "../apiv2";
10+
import { Client, ClientResponse } from "../apiv2";
711
import { FirebaseError } from "../error";
812

9-
const JSONStream = require("JSONStream");
10-
1113
const MAX_CHUNK_SIZE = 1024 * 1024;
1214
const CONCURRENCY_LIMIT = 5;
1315

@@ -27,17 +29,15 @@ type ChunkedData = {
2729
* The data is parsed and chunked into subtrees of ~1 MB, to be subsequently written in parallel.
2830
*/
2931
export default class DatabaseImporter {
30-
private jsonPath: string;
3132
private client: Client;
3233
private limit = pLimit(CONCURRENCY_LIMIT);
3334

3435
constructor(
3536
private dbUrl: URL,
36-
private inStream: NodeJS.ReadableStream,
37-
dataPath: string,
37+
private inStream: stream.Readable,
38+
private dataPath: string,
3839
private chunkSize = MAX_CHUNK_SIZE
3940
) {
40-
this.jsonPath = this.computeJsonPath(dataPath);
4141
this.client = new Client({ urlPrefix: dbUrl.origin, auth: true });
4242
}
4343

@@ -46,7 +46,7 @@ export default class DatabaseImporter {
4646
*/
4747
async execute(): Promise<any> {
4848
await this.checkLocationIsEmpty();
49-
return this.readAndWriteChunks(this.inStream);
49+
return this.readAndWriteChunks();
5050
}
5151

5252
private async checkLocationIsEmpty(): Promise<void> {
@@ -66,7 +66,7 @@ export default class DatabaseImporter {
6666
}
6767
}
6868

69-
private readAndWriteChunks(inStream: NodeJS.ReadableStream): Promise<any> {
69+
private readAndWriteChunks(): Promise<any> {
7070
const { dbUrl } = this;
7171
const chunkData = this.chunkData.bind(this);
7272
const writeChunk = this.writeChunk.bind(this);
@@ -90,14 +90,24 @@ export default class DatabaseImporter {
9090

9191
return new Promise((resolve, reject) => {
9292
const responses: any[] = [];
93-
inStream
94-
.pipe(JSONStream.parse(this.jsonPath))
93+
const pipeline = new Chain([
94+
this.inStream,
95+
Filter.withParser({
96+
filter: this.computeFilterString(this.dataPath) || (() => true),
97+
pathSeparator: "/",
98+
}),
99+
StreamObject.streamObject(),
100+
]);
101+
pipeline
95102
.on("error", (err: any) =>
96103
reject(
97-
new FirebaseError("Invalid data; couldn't parse JSON object, array, or value.", {
98-
original: err,
99-
exit: 2,
100-
})
104+
new FirebaseError(
105+
`Invalid data; couldn't parse JSON object, array, or value. ${err.message}`,
106+
{
107+
original: err,
108+
exit: 2,
109+
}
110+
)
101111
)
102112
)
103113
.pipe(readChunks)
@@ -108,7 +118,7 @@ export default class DatabaseImporter {
108118
});
109119
}
110120

111-
private writeChunk(chunk: Data): Promise<any> {
121+
private writeChunk(chunk: Data): Promise<ClientResponse<any>> {
112122
return this.limit(() =>
113123
this.client.request({
114124
method: "PUT",
@@ -152,12 +162,8 @@ export default class DatabaseImporter {
152162
}
153163
}
154164

155-
private computeJsonPath(dataPath: string): string {
156-
if (dataPath === "/") {
157-
return "$*";
158-
} else {
159-
return `${dataPath.split("/").slice(1).join(".")}.$*`;
160-
}
165+
private computeFilterString(dataPath: string): string {
166+
return dataPath.split("/").filter(Boolean).join("/");
161167
}
162168

163169
private joinPath(root: string, key: string): string {

src/test/database/import.spec.ts

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import * as nock from "nock";
2+
import * as stream from "stream";
23
import * as utils from "../../utils";
34
import { expect } from "chai";
45

@@ -9,21 +10,21 @@ const dbUrl = new URL("https://test-db.firebaseio.com/foo");
910

1011
describe("DatabaseImporter", () => {
1112
const DATA = { a: 100, b: [true, "bar", { f: { g: 0, h: 1 }, i: "baz" }] };
12-
let DATA_STREAM: NodeJS.ReadableStream;
13+
let DATA_STREAM: stream.Readable;
1314

1415
beforeEach(() => {
1516
DATA_STREAM = utils.stringToStream(JSON.stringify(DATA))!;
1617
});
1718

1819
it("throws FirebaseError when JSON is invalid", async () => {
1920
nock("https://test-db.firebaseio.com").get("/foo.json?shallow=true").reply(200);
20-
2121
const INVALID_JSON = '{"a": {"b"}}';
2222
const importer = new DatabaseImporter(
2323
dbUrl,
2424
utils.stringToStream(INVALID_JSON)!,
2525
/* importPath= */ "/"
2626
);
27+
2728
await expect(importer.execute()).to.be.rejectedWith(
2829
FirebaseError,
2930
"Invalid data; couldn't parse JSON object, array, or value."
@@ -36,9 +37,10 @@ describe("DatabaseImporter", () => {
3637
nock("https://test-db.firebaseio.com")
3738
.put("/foo/b.json", JSON.stringify([true, "bar", { f: { g: 0, h: 1 }, i: "baz" }]))
3839
.reply(200);
39-
4040
const importer = new DatabaseImporter(dbUrl, DATA_STREAM, /* importPath= */ "/");
41+
4142
const responses = await importer.execute();
43+
4244
expect(responses).to.have.length(2);
4345
expect(nock.isDone()).to.be.true;
4446
});
@@ -52,35 +54,36 @@ describe("DatabaseImporter", () => {
5254
.put("/foo/b/2/f.json", JSON.stringify({ g: 0, h: 1 }))
5355
.reply(200);
5456
nock("https://test-db.firebaseio.com").put("/foo/b/2/i.json", '"baz"').reply(200);
55-
5657
const importer = new DatabaseImporter(
5758
dbUrl,
5859
DATA_STREAM,
5960
/* importPath= */ "/",
6061
/* chunkSize= */ 20
6162
);
63+
6264
const responses = await importer.execute();
65+
6366
expect(responses).to.have.length(5);
6467
expect(nock.isDone()).to.be.true;
6568
});
6669

6770
it("imports from data path", async () => {
6871
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);
7172
nock("https://test-db.firebaseio.com")
72-
.put("/foo/2.json", JSON.stringify({ f: { g: 0, h: 1 }, i: "baz" }))
73+
.put("/foo/b.json", JSON.stringify([true, "bar", { f: { g: 0, h: 1 }, i: "baz" }]))
7374
.reply(200);
74-
7575
const importer = new DatabaseImporter(dbUrl, DATA_STREAM, /* importPath= */ "/b");
76+
7677
const responses = await importer.execute();
77-
expect(responses).to.have.length(3);
78+
79+
expect(responses).to.have.length(1);
7880
expect(nock.isDone()).to.be.true;
7981
});
8082

8183
it("throws FirebaseError when data location is nonempty", async () => {
8284
nock("https://test-db.firebaseio.com").get("/foo.json?shallow=true").reply(200, { a: "foo" });
8385
const importer = new DatabaseImporter(dbUrl, DATA_STREAM, /* importPath= */ "/");
86+
8487
await expect(importer.execute()).to.be.rejectedWith(
8588
FirebaseError,
8689
/Importing is only allowed for an empty location./

0 commit comments

Comments
 (0)