Skip to content

Commit 0cd8b9d

Browse files
authored
Create an InternalExpression type which allows precanned access fields to env.FIREBASE_CONFIG (#1231)
These correspond to magic params defined in the CLI which can be accessed during config resolution but are not saved to .env
1 parent 4193029 commit 0cd8b9d

File tree

4 files changed

+117
-5
lines changed

4 files changed

+117
-5
lines changed

spec/fixtures/sources/commonjs-params/index.js

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,18 +21,20 @@ params.defineInt("ANOTHER_INT", {
2121

2222
params.defineSecret("SUPER_SECRET_FLAG");
2323

24+
// N.B: invocation of the precanned internal params should not affect the manifest
25+
2426
exports.v1http = functions.https.onRequest((req, resp) => {
25-
resp.status(200).send("PASS");
27+
resp.status(200).send(params.projectID);
2628
});
2729

2830
exports.v1callable = functions.https.onCall(() => {
29-
return "PASS";
31+
return params.databaseURL;
3032
});
3133

3234
exports.v2http = functionsv2.https.onRequest((req, resp) => {
33-
resp.status(200).send("PASS");
35+
resp.status(200).send(params.gcloudProject);
3436
});
3537

3638
exports.v2callable = functionsv2.https.onCall(() => {
37-
return "PASS";
39+
return params.databaseURL;
3840
});

spec/params/params.spec.ts

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,32 @@ describe("Params value extraction", () => {
5555
expect(falseParam.value()).to.be.false;
5656
});
5757

58+
it("extracts the special case internal params from env.FIREBASE_CONFIG", () => {
59+
process.env.FIREBASE_CONFIG = JSON.stringify({
60+
projectId: "foo",
61+
storageBucket: "foo.appspot.com",
62+
databaseURL: "https://foo.firebaseio.com",
63+
});
64+
expect(params.databaseURL.value()).to.equal("https://foo.firebaseio.com");
65+
expect(params.gcloudProject.value()).to.equal("foo");
66+
expect(params.projectID.value()).to.equal("foo");
67+
expect(params.storageBucket.value()).to.equal("foo.appspot.com");
68+
69+
process.env.FIREBASE_CONFIG = JSON.stringify({ projectId: "foo" });
70+
expect(params.databaseURL.value()).to.equal("");
71+
expect(params.gcloudProject.value()).to.equal("foo");
72+
expect(params.projectID.value()).to.equal("foo");
73+
expect(params.storageBucket.value()).to.equal("");
74+
75+
process.env.FIREBASE_CONFIG = JSON.stringify({});
76+
expect(params.databaseURL.value()).to.equal("");
77+
expect(params.gcloudProject.value()).to.equal("");
78+
expect(params.projectID.value()).to.equal("");
79+
expect(params.storageBucket.value()).to.equal("");
80+
81+
delete process.env.FIREBASE_CONFIG;
82+
});
83+
5884
it("falls back on the javascript zero values in case of type mismatch", () => {
5985
const stringToInt = params.defineInt("A_STRING");
6086
expect(stringToInt.value()).to.equal(0);
@@ -89,7 +115,6 @@ describe("Params value extraction", () => {
89115
expect(int.equals(-11).value()).to.be.true;
90116
expect(int.equals(diffInt).value()).to.be.false;
91117
expect(int.equals(22).value()).to.be.false;
92-
93118
expect(int.notEquals(diffInt).value()).to.be.true;
94119
expect(int.notEquals(22).value()).to.be.true;
95120
expect(int.greaterThan(diffInt).value()).to.be.false;
@@ -155,6 +180,35 @@ describe("Params value extraction", () => {
155180
});
156181

157182
describe("Params as CEL", () => {
183+
it("internal expressions behave like strings", () => {
184+
const str = params.defineString("A_STRING");
185+
186+
expect(params.projectID.toCEL()).to.equal(`{{ params.PROJECT_ID }}`);
187+
expect(params.projectID.equals("foo").toCEL()).to.equal(`{{ params.PROJECT_ID == "foo" }}`);
188+
expect(params.projectID.equals(str).toCEL()).to.equal(
189+
`{{ params.PROJECT_ID == params.A_STRING }}`
190+
);
191+
expect(params.gcloudProject.toCEL()).to.equal(`{{ params.GCLOUD_PROJECT }}`);
192+
expect(params.gcloudProject.equals("foo").toCEL()).to.equal(
193+
`{{ params.GCLOUD_PROJECT == "foo" }}`
194+
);
195+
expect(params.gcloudProject.equals(str).toCEL()).to.equal(
196+
`{{ params.GCLOUD_PROJECT == params.A_STRING }}`
197+
);
198+
expect(params.databaseURL.toCEL()).to.equal(`{{ params.DATABASE_URL }}`);
199+
expect(params.databaseURL.equals("foo").toCEL()).to.equal(`{{ params.DATABASE_URL == "foo" }}`);
200+
expect(params.databaseURL.equals(str).toCEL()).to.equal(
201+
`{{ params.DATABASE_URL == params.A_STRING }}`
202+
);
203+
expect(params.storageBucket.toCEL()).to.equal(`{{ params.STORAGE_BUCKET }}`);
204+
expect(params.storageBucket.equals("foo").toCEL()).to.equal(
205+
`{{ params.STORAGE_BUCKET == "foo" }}`
206+
);
207+
expect(params.storageBucket.equals(str).toCEL()).to.equal(
208+
`{{ params.STORAGE_BUCKET == params.A_STRING }}`
209+
);
210+
});
211+
158212
it("identity expressions", () => {
159213
expect(params.defineString("FOO").toCEL()).to.equal("{{ params.FOO }}");
160214
expect(params.defineInt("FOO").toCEL()).to.equal("{{ params.FOO }}");

src/params/index.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import {
3535
ParamOptions,
3636
SecretParam,
3737
StringParam,
38+
InternalExpression,
3839
} from "./types";
3940

4041
export { ParamOptions, Expression };
@@ -64,6 +65,40 @@ export function clearParams() {
6465
declaredParams.splice(0, declaredParams.length);
6566
}
6667

68+
/**
69+
* A builtin param that resolves to the default RTDB database URL associated
70+
* with the project, without prompting the deployer. Empty string if none exists.
71+
*/
72+
export const databaseURL: Param<string> = new InternalExpression(
73+
"DATABASE_URL",
74+
(env: NodeJS.ProcessEnv) => JSON.parse(env.FIREBASE_CONFIG)?.databaseURL || ""
75+
);
76+
/**
77+
* A builtin param that resolves to the Cloud project ID associated with
78+
* the project, without prompting the deployer.
79+
*/
80+
export const projectID: Param<string> = new InternalExpression(
81+
"PROJECT_ID",
82+
(env: NodeJS.ProcessEnv) => JSON.parse(env.FIREBASE_CONFIG)?.projectId || ""
83+
);
84+
/**
85+
* A builtin param that resolves to the Cloud project ID, without prompting
86+
* the deployer.
87+
*/
88+
export const gcloudProject: Param<string> = new InternalExpression(
89+
"GCLOUD_PROJECT",
90+
(env: NodeJS.ProcessEnv) => JSON.parse(env.FIREBASE_CONFIG)?.projectId || ""
91+
);
92+
/**
93+
* A builtin param that resolves to the Cloud storage bucket associated
94+
* with the function, without prompting the deployer. Empty string if not
95+
* defined.
96+
*/
97+
export const storageBucket: Param<string> = new InternalExpression(
98+
"STORAGE_BUCKET",
99+
(env: NodeJS.ProcessEnv) => JSON.parse(env.FIREBASE_CONFIG)?.storageBucket || ""
100+
);
101+
67102
/**
68103
* Declares a secret param, that will persist values only in Cloud Secret Manager.
69104
* Secrets are stored interally as bytestrings. Use ParamOptions.`as` to provide type

src/params/types.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,27 @@ export class StringParam extends Param<string> {
291291
}
292292
}
293293

294+
/**
295+
* A CEL expression which represents an internal Firebase variable. This class
296+
* cannot be instantiated by developers, but we provide several canned instances
297+
* of it to make available params that will never have to be defined at
298+
* deployment time, and can always be read from process.env.
299+
* @internal
300+
*/
301+
export class InternalExpression extends Param<string> {
302+
constructor(name: string, private readonly getter: (env: NodeJS.ProcessEnv) => string) {
303+
super(name);
304+
}
305+
306+
value(): string {
307+
return this.getter(process.env) || "";
308+
}
309+
310+
toSpec(): WireParamSpec<string> {
311+
throw new Error("An InternalExpression should never be marshalled for wire transmission.");
312+
}
313+
}
314+
294315
export class IntParam extends Param<number> {
295316
static type: ParamValueType = "int";
296317

0 commit comments

Comments
 (0)