Skip to content

Commit 3d1bd1a

Browse files
committed
Add unit tests
1 parent 731a6cb commit 3d1bd1a

File tree

4 files changed

+57
-29
lines changed

4 files changed

+57
-29
lines changed

src/commands/database-set.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import * as clc from "colorette";
22
import * as fs from "fs";
33

4-
import { Client } from "../apiv2";
54
import { Command } from "../command";
65
import { Emulators } from "../emulator/types";
76
import { FirebaseError } from "../error";
@@ -14,7 +13,7 @@ import { URL } from "url";
1413
import { logger } from "../logger";
1514
import { requireDatabaseInstance } from "../requireDatabaseInstance";
1615
import * as utils from "../utils";
17-
import { DatabaseChunkUploader } from "../databaseChunkUploader";
16+
import DatabaseImporter from "../database/import";
1817

1918
export const command = new Command("database:set <path> [infile]")
2019
.description("store JSON data at the specified path via STDIN, arg, or file")
@@ -61,9 +60,9 @@ export const command = new Command("database:set <path> [infile]")
6160
utils.explainStdin();
6261
}
6362

64-
const uploader = new DatabaseChunkUploader(dbUrl, inputString);
63+
const importer = new DatabaseImporter(dbUrl, inputString);
6564
try {
66-
await uploader.upload(/* overwrite= */ true);
65+
await importer.execute(/* overwrite= */ true);
6766
} catch (err: any) {
6867
logger.debug(err);
6968
throw new FirebaseError(`Unexpected error while setting data: ${err}`, { exit: 2 });

src/commands/database-update.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import { URL } from "url";
22
import * as clc from "colorette";
33
import * as fs from "fs";
44

5-
import { Client } from "../apiv2";
65
import { Command } from "../command";
76
import { Emulators } from "../emulator/types";
87
import { FirebaseError } from "../error";
@@ -14,7 +13,7 @@ import { requirePermissions } from "../requirePermissions";
1413
import { logger } from "../logger";
1514
import { requireDatabaseInstance } from "../requireDatabaseInstance";
1615
import * as utils from "../utils";
17-
import { DatabaseChunkUploader } from "../databaseChunkUploader";
16+
import DatabaseImporter from "../database/import";
1817

1918
export const command = new Command("database:update <path> [infile]")
2019
.description("update some of the keys for the defined path in your Firebase")
@@ -61,9 +60,9 @@ export const command = new Command("database:update <path> [infile]")
6160
utils.explainStdin();
6261
}
6362

64-
const uploader = new DatabaseChunkUploader(dbUrl, inputString);
63+
const importer = new DatabaseImporter(dbUrl, inputString);
6564
try {
66-
await uploader.upload(/* overwrite= */ false);
65+
await importer.execute(/* overwrite= */ false);
6766
} catch (err: any) {
6867
throw new FirebaseError("Unexpected error while setting data");
6968
}

src/databaseChunkUploader.ts renamed to src/database/import.ts

Lines changed: 26 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
import { URL } from "url";
2-
import { Client } from "./apiv2";
3-
import * as utils from "./utils";
2+
import { Client } from "../apiv2";
43

54
const MAX_CHUNK_SIZE = 1024 * 1024;
65

76
type Data = {
8-
json: any;
7+
json: { [key: string]: any } | string | number | boolean;
98
pathname: string;
109
};
1110

@@ -14,19 +13,28 @@ type ChunkedData = {
1413
size: number;
1514
};
1615

17-
export class DatabaseChunkUploader {
16+
/**
17+
* Imports JSON data to a given RTDB instance.
18+
*
19+
* The data is parsed and chunked into subtrees of ~1 MB, to be subsequently written in parallel.
20+
*/
21+
export default class DatabaseImporter {
22+
chunks: Data[];
1823
private client: Client;
19-
private chunks: Data[];
20-
21-
constructor(private dbUrl: URL, file: string) {
22-
this.client = new Client({ urlPrefix: dbUrl.origin, auth: true });
2324

25+
constructor(private dbUrl: URL, file: string, private chunkSize = MAX_CHUNK_SIZE) {
2426
const data = { json: JSON.parse(file), pathname: dbUrl.pathname };
2527
const chunkedData = this.chunkData(data);
2628
this.chunks = chunkedData.chunks || [data];
29+
this.client = new Client({ urlPrefix: dbUrl.origin, auth: true });
2730
}
2831

29-
public async upload(overwrite: boolean): Promise<any> {
32+
/**
33+
* Writes the chunked data to RTDB.
34+
*
35+
* @param overwrite Whether to overwrite the existing data at the given location.
36+
*/
37+
async execute(overwrite: boolean): Promise<any> {
3038
return Promise.all(
3139
this.chunks.map((chunk: Data) =>
3240
this.client.request({
@@ -40,16 +48,20 @@ export class DatabaseChunkUploader {
4048
}
4149

4250
private chunkData({ json, pathname }: Data): ChunkedData {
43-
if (isObject(json)) {
51+
if (typeof json === "string" || typeof json === "number" || typeof json === "boolean") {
52+
// Leaf node, cannot be chunked
53+
return { chunks: null, size: JSON.stringify(json).length };
54+
} else {
4455
// Children node
4556
let size = 2; // {}
46-
let chunks = [];
57+
58+
const chunks = [];
4759
let hasChunkedChild = false;
4860

49-
for (const key in json) {
61+
for (const key of Object.keys(json)) {
5062
size += key.length + 3; // "[key]":
5163

52-
const child = { json: json[key], pathname: pathname + "/" + key };
64+
const child = { json: json[key], pathname: [pathname, key].join("/").replace("//", "/") };
5365
const childChunks = this.chunkData(child);
5466
size += childChunks.size;
5567
if (childChunks.chunks) {
@@ -60,18 +72,11 @@ export class DatabaseChunkUploader {
6072
}
6173
}
6274

63-
if (hasChunkedChild || size >= MAX_CHUNK_SIZE) {
75+
if (hasChunkedChild || size >= this.chunkSize) {
6476
return { chunks, size };
6577
} else {
6678
return { chunks: null, size };
6779
}
68-
} else {
69-
// Leaf node, cannot be chunked
70-
return { chunks: null, size: JSON.stringify(json).length };
7180
}
7281
}
7382
}
74-
75-
function isObject(blob: any): boolean {
76-
return blob !== null && typeof blob === "object";
77-
}

src/test/database/import.spec.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { expect } from "chai";
2+
3+
import DatabaseImporter from "../../database/import";
4+
5+
const dbUrl = new URL("https://test-db.firebaseio.com/foo");
6+
7+
describe("DatabaseImporter", () => {
8+
const DATA = { a: 100, b: { c: true, d: { e: "bar", f: { g: 0, h: 1 } } } };
9+
10+
it("parses data as single chunk", () => {
11+
const importer = new DatabaseImporter(dbUrl, JSON.stringify(DATA));
12+
expect(importer.chunks.length).to.equal(1);
13+
expect(importer.chunks[0].json).to.deep.equal(DATA);
14+
expect(importer.chunks[0].pathname).to.equal("/foo");
15+
});
16+
17+
it("parses data as multiple chunks", () => {
18+
const importer = new DatabaseImporter(dbUrl, JSON.stringify(DATA), /* chunkSize= */ 20);
19+
expect(importer.chunks.length).to.equal(4);
20+
expect(importer.chunks).to.deep.include({ json: 100, pathname: "/foo/a" });
21+
expect(importer.chunks).to.deep.include({ json: true, pathname: "/foo/b/c" });
22+
expect(importer.chunks).to.deep.include({ json: "bar", pathname: "/foo/b/d/e" });
23+
expect(importer.chunks).to.deep.include({ json: { g: 0, h: 1 }, pathname: "/foo/b/d/f" });
24+
});
25+
});

0 commit comments

Comments
 (0)