diff --git a/spec/fixtures/sources/commonjs-params/index.js b/spec/fixtures/sources/commonjs-params/index.js index 55e34d387..5f982fcaa 100644 --- a/spec/fixtures/sources/commonjs-params/index.js +++ b/spec/fixtures/sources/commonjs-params/index.js @@ -21,18 +21,20 @@ params.defineInt("ANOTHER_INT", { params.defineSecret("SUPER_SECRET_FLAG"); +// N.B: invocation of the precanned internal params should not affect the manifest + exports.v1http = functions.https.onRequest((req, resp) => { - resp.status(200).send("PASS"); + resp.status(200).send(params.projectID); }); exports.v1callable = functions.https.onCall(() => { - return "PASS"; + return params.databaseURL; }); exports.v2http = functionsv2.https.onRequest((req, resp) => { - resp.status(200).send("PASS"); + resp.status(200).send(params.gcloudProject); }); exports.v2callable = functionsv2.https.onCall(() => { - return "PASS"; + return params.databaseURL; }); diff --git a/spec/params/params.spec.ts b/spec/params/params.spec.ts index 5774fd336..f7993b1ed 100644 --- a/spec/params/params.spec.ts +++ b/spec/params/params.spec.ts @@ -55,6 +55,32 @@ describe("Params value extraction", () => { expect(falseParam.value()).to.be.false; }); + it("extracts the special case internal params from env.FIREBASE_CONFIG", () => { + process.env.FIREBASE_CONFIG = JSON.stringify({ + projectId: "foo", + storageBucket: "foo.appspot.com", + databaseURL: "https://foo.firebaseio.com", + }); + expect(params.databaseURL.value()).to.equal("https://foo.firebaseio.com"); + expect(params.gcloudProject.value()).to.equal("foo"); + expect(params.projectID.value()).to.equal("foo"); + expect(params.storageBucket.value()).to.equal("foo.appspot.com"); + + process.env.FIREBASE_CONFIG = JSON.stringify({ projectId: "foo" }); + expect(params.databaseURL.value()).to.equal(""); + expect(params.gcloudProject.value()).to.equal("foo"); + expect(params.projectID.value()).to.equal("foo"); + expect(params.storageBucket.value()).to.equal(""); + + process.env.FIREBASE_CONFIG = JSON.stringify({}); + expect(params.databaseURL.value()).to.equal(""); + expect(params.gcloudProject.value()).to.equal(""); + expect(params.projectID.value()).to.equal(""); + expect(params.storageBucket.value()).to.equal(""); + + delete process.env.FIREBASE_CONFIG; + }); + it("falls back on the javascript zero values in case of type mismatch", () => { const stringToInt = params.defineInt("A_STRING"); expect(stringToInt.value()).to.equal(0); @@ -89,7 +115,6 @@ describe("Params value extraction", () => { expect(int.equals(-11).value()).to.be.true; expect(int.equals(diffInt).value()).to.be.false; expect(int.equals(22).value()).to.be.false; - expect(int.notEquals(diffInt).value()).to.be.true; expect(int.notEquals(22).value()).to.be.true; expect(int.greaterThan(diffInt).value()).to.be.false; @@ -155,6 +180,35 @@ describe("Params value extraction", () => { }); describe("Params as CEL", () => { + it("internal expressions behave like strings", () => { + const str = params.defineString("A_STRING"); + + expect(params.projectID.toCEL()).to.equal(`{{ params.PROJECT_ID }}`); + expect(params.projectID.equals("foo").toCEL()).to.equal(`{{ params.PROJECT_ID == "foo" }}`); + expect(params.projectID.equals(str).toCEL()).to.equal( + `{{ params.PROJECT_ID == params.A_STRING }}` + ); + expect(params.gcloudProject.toCEL()).to.equal(`{{ params.GCLOUD_PROJECT }}`); + expect(params.gcloudProject.equals("foo").toCEL()).to.equal( + `{{ params.GCLOUD_PROJECT == "foo" }}` + ); + expect(params.gcloudProject.equals(str).toCEL()).to.equal( + `{{ params.GCLOUD_PROJECT == params.A_STRING }}` + ); + expect(params.databaseURL.toCEL()).to.equal(`{{ params.DATABASE_URL }}`); + expect(params.databaseURL.equals("foo").toCEL()).to.equal(`{{ params.DATABASE_URL == "foo" }}`); + expect(params.databaseURL.equals(str).toCEL()).to.equal( + `{{ params.DATABASE_URL == params.A_STRING }}` + ); + expect(params.storageBucket.toCEL()).to.equal(`{{ params.STORAGE_BUCKET }}`); + expect(params.storageBucket.equals("foo").toCEL()).to.equal( + `{{ params.STORAGE_BUCKET == "foo" }}` + ); + expect(params.storageBucket.equals(str).toCEL()).to.equal( + `{{ params.STORAGE_BUCKET == params.A_STRING }}` + ); + }); + it("identity expressions", () => { expect(params.defineString("FOO").toCEL()).to.equal("{{ params.FOO }}"); expect(params.defineInt("FOO").toCEL()).to.equal("{{ params.FOO }}"); diff --git a/src/params/index.ts b/src/params/index.ts index 0910733e4..f2291bdcd 100644 --- a/src/params/index.ts +++ b/src/params/index.ts @@ -35,6 +35,7 @@ import { ParamOptions, SecretParam, StringParam, + InternalExpression, } from "./types"; export { ParamOptions, Expression }; @@ -64,6 +65,40 @@ export function clearParams() { declaredParams.splice(0, declaredParams.length); } +/** + * A builtin param that resolves to the default RTDB database URL associated + * with the project, without prompting the deployer. Empty string if none exists. + */ +export const databaseURL: Param = new InternalExpression( + "DATABASE_URL", + (env: NodeJS.ProcessEnv) => JSON.parse(env.FIREBASE_CONFIG)?.databaseURL || "" +); +/** + * A builtin param that resolves to the Cloud project ID associated with + * the project, without prompting the deployer. + */ +export const projectID: Param = new InternalExpression( + "PROJECT_ID", + (env: NodeJS.ProcessEnv) => JSON.parse(env.FIREBASE_CONFIG)?.projectId || "" +); +/** + * A builtin param that resolves to the Cloud project ID, without prompting + * the deployer. + */ +export const gcloudProject: Param = new InternalExpression( + "GCLOUD_PROJECT", + (env: NodeJS.ProcessEnv) => JSON.parse(env.FIREBASE_CONFIG)?.projectId || "" +); +/** + * A builtin param that resolves to the Cloud storage bucket associated + * with the function, without prompting the deployer. Empty string if not + * defined. + */ +export const storageBucket: Param = new InternalExpression( + "STORAGE_BUCKET", + (env: NodeJS.ProcessEnv) => JSON.parse(env.FIREBASE_CONFIG)?.storageBucket || "" +); + /** * Declares a secret param, that will persist values only in Cloud Secret Manager. * Secrets are stored interally as bytestrings. Use ParamOptions.`as` to provide type diff --git a/src/params/types.ts b/src/params/types.ts index 53d390808..35ff8919a 100644 --- a/src/params/types.ts +++ b/src/params/types.ts @@ -291,6 +291,27 @@ export class StringParam extends Param { } } +/** + * A CEL expression which represents an internal Firebase variable. This class + * cannot be instantiated by developers, but we provide several canned instances + * of it to make available params that will never have to be defined at + * deployment time, and can always be read from process.env. + * @internal + */ +export class InternalExpression extends Param { + constructor(name: string, private readonly getter: (env: NodeJS.ProcessEnv) => string) { + super(name); + } + + value(): string { + return this.getter(process.env) || ""; + } + + toSpec(): WireParamSpec { + throw new Error("An InternalExpression should never be marshalled for wire transmission."); + } +} + export class IntParam extends Param { static type: ParamValueType = "int";