Skip to content

Commit 38ec997

Browse files
authored
update database:update to use apiv2 (with streaming request support) (#2803)
* update database:update to use new apiv2 module. add streaming body support * update apiv2 to accept a body parameter * better stream handling * pass json stright through * add one more undefined check
1 parent dde1dbe commit 38ec997

File tree

4 files changed

+121
-112
lines changed

4 files changed

+121
-112
lines changed

src/apiv2.ts

Lines changed: 28 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ type HttpMethod = "GET" | "PUT" | "POST" | "DELETE" | "PATCH";
1313
interface RequestOptions<T> extends VerbOptions<T> {
1414
method: HttpMethod;
1515
path: string;
16-
json?: T;
16+
body?: T | string | NodeJS.ReadableStream;
1717
responseType?: "json" | "stream";
1818
signal?: AbortSignal;
1919
}
@@ -94,7 +94,7 @@ export class Client {
9494
const reqOptions: ClientRequestOptions<ReqT> = Object.assign(options, {
9595
method: "POST",
9696
path,
97-
json,
97+
body: json,
9898
});
9999
return this.request<ReqT, ResT>(reqOptions);
100100
}
@@ -107,7 +107,7 @@ export class Client {
107107
const reqOptions: ClientRequestOptions<ReqT> = Object.assign(options, {
108108
method: "PATCH",
109109
path,
110-
json,
110+
body: json,
111111
});
112112
return this.request<ReqT, ResT>(reqOptions);
113113
}
@@ -120,7 +120,7 @@ export class Client {
120120
const reqOptions: ClientRequestOptions<ReqT> = Object.assign(options, {
121121
method: "PUT",
122122
path,
123-
json,
123+
body: json,
124124
});
125125
return this.request<ReqT, ResT>(reqOptions);
126126
}
@@ -248,8 +248,10 @@ export class Client {
248248
signal: options.signal,
249249
};
250250

251-
if (options.json !== undefined) {
252-
fetchOptions.body = JSON.stringify(options.json);
251+
if (typeof options.body === "string" || isStream(options.body)) {
252+
fetchOptions.body = options.body;
253+
} else if (options.body !== undefined) {
254+
fetchOptions.body = JSON.stringify(options.body);
253255
}
254256

255257
this.logRequest(options);
@@ -296,10 +298,10 @@ export class Client {
296298
}
297299
const logURL = this.requestURL(options);
298300
logger.debug(`>>> [apiv2][query] ${options.method} ${logURL} ${queryParamsLog}`);
299-
if (options.json) {
301+
if (options.body !== undefined) {
300302
let logBody = "[omitted]";
301303
if (!options.skipLog?.body) {
302-
logBody = JSON.stringify(options.json);
304+
logBody = bodyToString(options.body);
303305
}
304306
logger.debug(`>>> [apiv2][body] ${options.method} ${logURL} ${logBody}`);
305307
}
@@ -310,17 +312,25 @@ export class Client {
310312
logger.debug(`<<< [apiv2][status] ${options.method} ${logURL} ${res.status}`);
311313
let logBody = "[omitted]";
312314
if (!options.skipLog?.resBody) {
313-
if (body instanceof Readable) {
314-
// Don't attempt to read any stream type, in case the caller needs it.
315-
logBody = "[stream]";
316-
} else {
317-
try {
318-
logBody = JSON.stringify(body);
319-
} catch (_) {
320-
logBody = `${body}`;
321-
}
322-
}
315+
logBody = bodyToString(body);
323316
}
324317
logger.debug(`<<< [apiv2][body] ${options.method} ${logURL} ${logBody}`);
325318
}
326319
}
320+
321+
function bodyToString(body: unknown): string {
322+
if (isStream(body)) {
323+
// Don't attempt to read any stream type, in case the caller needs it.
324+
return "[stream]";
325+
} else {
326+
try {
327+
return JSON.stringify(body);
328+
} catch (_) {
329+
return `${body}`;
330+
}
331+
}
332+
}
333+
334+
function isStream(o: unknown): o is NodeJS.ReadableStream {
335+
return o instanceof Readable;
336+
}

src/commands/database-update.js

Lines changed: 0 additions & 92 deletions
This file was deleted.

src/commands/database-update.ts

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import * as clc from "cli-color";
2+
import * as fs from "fs";
3+
4+
import { Client } from "../apiv2";
5+
import { Command } from "../command";
6+
import { Emulators } from "../emulator/types";
7+
import { FirebaseError } from "../error";
8+
import { populateInstanceDetails } from "../management/database";
9+
import { printNoticeIfEmulated } from "../emulator/commandUtils";
10+
import { promptOnce } from "../prompt";
11+
import { realtimeOriginOrEmulatorOrCustomUrl } from "../database/api";
12+
import { requirePermissions } from "../requirePermissions";
13+
import * as logger from "../logger";
14+
import * as requireInstance from "../requireInstance";
15+
import * as utils from "../utils";
16+
17+
export default new Command("database:update <path> [infile]")
18+
.description("update some of the keys for the defined path in your Firebase")
19+
.option("-d, --data <data>", "specify escaped JSON directly")
20+
.option("-y, --confirm", "pass this option to bypass confirmation prompt")
21+
.option(
22+
"--instance <instance>",
23+
"use the database <instance>.firebaseio.com (if omitted, use default database instance)"
24+
)
25+
.before(requirePermissions, ["firebasedatabase.instances.update"])
26+
.before(requireInstance)
27+
.before(populateInstanceDetails)
28+
.before(printNoticeIfEmulated, Emulators.DATABASE)
29+
.action(async (path: string, infile: string | undefined, options) => {
30+
if (!path.startsWith("/")) {
31+
throw new FirebaseError("Path must begin with /");
32+
}
33+
const origin = realtimeOriginOrEmulatorOrCustomUrl(options);
34+
const url = utils.getDatabaseUrl(origin, options.instance, path);
35+
if (!options.confirm) {
36+
const confirmed = await promptOnce({
37+
type: "confirm",
38+
name: "confirm",
39+
default: false,
40+
message: `You are about to modify data at ${clc.cyan(url)}. Are you sure?`,
41+
});
42+
if (!confirmed) {
43+
throw new FirebaseError("Command aborted.");
44+
}
45+
}
46+
47+
const inStream =
48+
utils.stringToStream(options.data) ||
49+
(infile && fs.createReadStream(infile)) ||
50+
process.stdin;
51+
const jsonUrl = new URL(utils.getDatabaseUrl(origin, options.instance, path + ".json"));
52+
53+
if (!infile && !options.data) {
54+
utils.explainStdin();
55+
}
56+
57+
const c = new Client({ urlPrefix: jsonUrl.origin, auth: true });
58+
try {
59+
await c.request({
60+
method: "PATCH",
61+
path: jsonUrl.pathname,
62+
body: inStream,
63+
});
64+
} catch (err) {
65+
throw new FirebaseError("Unexpected error while setting data");
66+
}
67+
68+
utils.logSuccess("Data updated successfully");
69+
logger.info();
70+
logger.info(
71+
clc.bold("View data at:"),
72+
utils.getDatabaseViewDataUrl(origin, options.project, options.instance, path)
73+
);
74+
});

src/test/apiv2.spec.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,14 @@ import * as nock from "nock";
33

44
import { Client } from "../apiv2";
55
import { FirebaseError } from "../error";
6-
import { streamToString } from "../utils";
6+
import { streamToString, stringToStream } from "../utils";
77

88
describe("apiv2", () => {
99
beforeEach(() => {
1010
// The api module has package variables that we don't want sticking around.
1111
delete require.cache[require.resolve("../apiv2")];
12+
13+
nock.cleanAll();
1214
});
1315

1416
after(() => {
@@ -207,7 +209,22 @@ describe("apiv2", () => {
207209
const r = await c.request({
208210
method: "POST",
209211
path: "/path/to/foo",
210-
json: POST_DATA,
212+
body: POST_DATA,
213+
});
214+
expect(r.body).to.deep.equal({ success: true });
215+
expect(nock.isDone()).to.be.true;
216+
});
217+
218+
it("should make a basic POST request with a stream", async () => {
219+
nock("https://example.com")
220+
.post("/path/to/foo", "hello world")
221+
.reply(200, { success: true });
222+
223+
const c = new Client({ urlPrefix: "https://example.com" });
224+
const r = await c.request({
225+
method: "POST",
226+
path: "/path/to/foo",
227+
body: stringToStream("hello world"),
211228
});
212229
expect(r.body).to.deep.equal({ success: true });
213230
expect(nock.isDone()).to.be.true;

0 commit comments

Comments
 (0)