diff --git a/CHANGELOG.md b/CHANGELOG.md index e69de29bb..6b6b1028a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -0,0 +1 @@ +- Future Extensions support (#1590) diff --git a/scripts/bin-test/extsdks/local/index.d.ts b/scripts/bin-test/extsdks/local/index.d.ts new file mode 100644 index 000000000..620621e2e --- /dev/null +++ b/scripts/bin-test/extsdks/local/index.d.ts @@ -0,0 +1,46 @@ +/** + * TaskQueue/LifecycleEvent/RuntimeStatus Tester SDK for backfill@0.0.2 + * + * When filing bugs or feature requests please specify: + * "Extensions SDK v1.0.0 for Local extension. + * https://github.com/firebase/firebase-tools/issues/new/choose + * + * GENERATED FILE. DO NOT EDIT. + */ +export type DoBackfillParam = "True" | "False"; +export type LocationParam = + | "us-central1" + | "us-east1" + | "us-east4" + | "europe-west1" + | "europe-west2" + | "europe-west3" + | "asia-east2" + | "asia-northeast1"; +/** + * Parameters for backfill@0.0.2 extension + */ +export interface BackfillParams { + /** + * Do a backfill + */ + DO_BACKFILL: DoBackfillParam; + /** + * Cloud Functions location + */ + LOCATION: LocationParam; +} +export declare function backfill(instanceId: string, params: BackfillParams): Backfill; +/** + * TaskQueue/LifecycleEvent/RuntimeStatus Tester + * A tester for the TaskQueue/LCE/RuntimeStatus project + */ +export declare class Backfill { + private instanceId; + private params; + readonly FIREBASE_EXTENSION_LOCAL_PATH = + "./functions/generated/extensions/local/backfill/0.0.2/src"; + constructor(instanceId: string, params: BackfillParams); + getInstanceId(): string; + getParams(): BackfillParams; +} diff --git a/scripts/bin-test/extsdks/local/index.js b/scripts/bin-test/extsdks/local/index.js new file mode 100644 index 000000000..f1f9cce55 --- /dev/null +++ b/scripts/bin-test/extsdks/local/index.js @@ -0,0 +1,30 @@ +"use strict"; +/** + * TaskQueue/LifecycleEvent/RuntimeStatus Tester SDK for extensions-try-backfill3@0.0.2 + * + * When filing bugs or feature requests please specify: + * "Extensions SDK v1.0.0 for Local extension. + * https://github.com/firebase/firebase-tools/issues/new/choose + * + * GENERATED FILE. DO NOT EDIT. + */ +Object.defineProperty(exports, "__esModule", { value: true }); +exports.backfill = exports.backfill = void 0; +function backfill(instanceId, params) { + return new Backfill(instanceId, params); +} +exports.backfill = backfill; +/** + * TaskQueue/LifecycleEvent/RuntimeStatus Tester + * A tester for the TaskQueue/LCE/RuntimeStatus project + */ +class Backfill { + constructor(instanceId, params) { + this.instanceId = instanceId; + this.params = params; + this.FIREBASE_EXTENSION_LOCAL_PATH = "./functions/generated/extensions/local/backfill/0.0.2/src"; + } + getInstanceId() { return this.instanceId; } + getParams() { return this.params; } +} +exports.Backfill = Backfill; diff --git a/scripts/bin-test/extsdks/local/package.json b/scripts/bin-test/extsdks/local/package.json new file mode 100644 index 000000000..700806b3e --- /dev/null +++ b/scripts/bin-test/extsdks/local/package.json @@ -0,0 +1,4 @@ +{ + "name": "@firebase-extensions/local-backfill-sdk", + "main": "./index.js" + } \ No newline at end of file diff --git a/scripts/bin-test/extsdks/translate/index.d.ts b/scripts/bin-test/extsdks/translate/index.d.ts new file mode 100644 index 000000000..ba706712c --- /dev/null +++ b/scripts/bin-test/extsdks/translate/index.d.ts @@ -0,0 +1,169 @@ +/** + * Translate Text in Firestore SDK for firestore-translate-text@0.1.18 + * + * When filing bugs or feature requests please specify: + * "Extensions SDK v1.0.0 for firestore-translate-text@0.1.18" + * https://github.com/firebase/firebase-tools/issues/new/choose + * + * GENERATED FILE. DO NOT EDIT. + */ +import { CloudEvent } from "../../../../v2"; +import { EventarcTriggerOptions } from "../../../../v2/eventarc"; +export type EventCallback = (event: CloudEvent) => unknown | Promise; +export type SimpleEventarcTriggerOptions = Omit< + EventarcTriggerOptions, + "eventType" | "channel" | "region" +>; +export type EventArcRegionType = "us-central1" | "us-west1" | "europe-west4" | "asia-northeast1"; +export type SystemFunctionVpcConnectorEgressSettingsParam = + | "VPC_CONNECTOR_EGRESS_SETTINGS_UNSPECIFIED" + | "PRIVATE_RANGES_ONLY" + | "ALL_TRAFFIC"; +export type SystemFunctionIngressSettingsParam = + | "ALLOW_ALL" + | "ALLOW_INTERNAL_ONLY" + | "ALLOW_INTERNAL_AND_GCLB"; +export type SystemFunctionLocationParam = + | "us-central1" + | "us-east1" + | "us-east4" + | "us-west1" + | "us-west2" + | "us-west3" + | "us-west4" + | "europe-central2" + | "europe-west1" + | "europe-west2" + | "europe-west3" + | "europe-west6" + | "asia-east1" + | "asia-east2" + | "asia-northeast1" + | "asia-northeast2" + | "asia-northeast3" + | "asia-south1" + | "asia-southeast1" + | "asia-southeast2" + | "northamerica-northeast1" + | "southamerica-east1" + | "australia-southeast1"; +export type SystemFunctionMemoryParam = "128" | "256" | "512" | "1024" | "2048" | "4096" | "8192"; +/** + * Parameters for firestore-translate-text@0.1.18 extension + */ +export interface FirestoreTranslateTextParams { + /** + * Target languages for translations, as a comma-separated list + */ + LANGUAGES: string; + /** + * Collection path + */ + COLLECTION_PATH: string; + /** + * Input field name + */ + INPUT_FIELD_NAME: string; + /** + * Translations output field name + */ + OUTPUT_FIELD_NAME: string; + /** + * Languages field name + */ + LANGUAGES_FIELD_NAME?: string; + /** + * Event Arc Region + */ + _EVENT_ARC_REGION?: EventArcRegionType; + /** + * Function timeout seconds + */ + _FUNCTION_TIMEOUT_SECONDS?: string; + /** + * VPC Connector + */ + _FUNCTION_VPC_CONNECTOR?: string; + /** + * VPC Connector Egress settings + */ + _FUNCTION_VPC_CONNECTOR_EGRESS_SETTINGS?: SystemFunctionVpcConnectorEgressSettingsParam; + /** + * Minimum function instances + */ + _FUNCTION_MIN_INSTANCES?: string; + /** + * Maximum function instances + */ + _FUNCTION_MAX_INSTANCES?: string; + /** + * Function ingress settings + */ + _FUNCTION_INGRESS_SETTINGS?: SystemFunctionIngressSettingsParam; + /** + * Function labels + */ + _FUNCTION_LABELS?: string; + /** + * KMS key name + */ + _FUNCTION_KMS_KEY_NAME?: string; + /** + * Docker repository + */ + _FUNCTION_DOCKER_REPOSITORY?: string; + /** + * Cloud Functions location + */ + _FUNCTION_LOCATION: SystemFunctionLocationParam; + /** + * Function memory + */ + _FUNCTION_MEMORY?: SystemFunctionMemoryParam; +} +export declare function firestoreTranslateText( + instanceId: string, + params: FirestoreTranslateTextParams +): FirestoreTranslateText; +/** + * Translate Text in Firestore + * Translates strings written to a Cloud Firestore collection into multiple languages (uses Cloud Translation API). + */ +export declare class FirestoreTranslateText { + private instanceId; + private params; + events: string[]; + readonly FIREBASE_EXTENSION_REFERENCE = "firebase/firestore-translate-text@0.1.18"; + readonly EXTENSION_VERSION = "0.1.18"; + constructor(instanceId: string, params: FirestoreTranslateTextParams); + getInstanceId(): string; + getParams(): FirestoreTranslateTextParams; + /** + * Occurs when a trigger has been called within the Extension, and will include data such as the context of the trigger request. + */ + onStart( + callback: EventCallback, + options?: SimpleEventarcTriggerOptions + ): import("firebase-functions/v2").CloudFunction>; + /** + * Occurs when image resizing completes successfully. The event will contain further details about specific formats and sizes. + */ + onSuccess( + callback: EventCallback, + options?: SimpleEventarcTriggerOptions + ): import("firebase-functions/v2").CloudFunction>; + /** + * Occurs when an issue has been experienced in the Extension. This will include any error data that has been included within the Error Exception. + */ + onError( + callback: EventCallback, + options?: SimpleEventarcTriggerOptions + ): import("firebase-functions/v2").CloudFunction>; + /** + * Occurs when the function is settled. Provides no customized data other than the context. + */ + onCompletion( + callback: EventCallback, + options?: SimpleEventarcTriggerOptions + ): import("firebase-functions/v2").CloudFunction>; +} diff --git a/scripts/bin-test/extsdks/translate/index.js b/scripts/bin-test/extsdks/translate/index.js new file mode 100644 index 000000000..6721d13d6 --- /dev/null +++ b/scripts/bin-test/extsdks/translate/index.js @@ -0,0 +1,61 @@ +"use strict"; +/** + * Translate Text in Firestore SDK for firestore-translate-text@0.1.18 + * + * When filing bugs or feature requests please specify: + * "Extensions SDK v1.0.0 for firestore-translate-text@0.1.18" + * https://github.com/firebase/firebase-tools/issues/new/choose + * + * GENERATED FILE. DO NOT EDIT. + */ +Object.defineProperty(exports, "__esModule", { value: true }); +exports.FirestoreTranslateText = exports.firestoreTranslateText = void 0; +const eventarc_1 = require("firebase-functions/v2/eventarc"); +function firestoreTranslateText(instanceId, params) { + return new FirestoreTranslateText(instanceId, params); +} +exports.firestoreTranslateText = firestoreTranslateText; +/** + * Translate Text in Firestore + * Translates strings written to a Cloud Firestore collection into multiple languages (uses Cloud Translation API). + */ +class FirestoreTranslateText { + constructor(instanceId, params) { + this.instanceId = instanceId; + this.params = params; + this.events = []; + this.FIREBASE_EXTENSION_REFERENCE = "firebase/firestore-translate-text@0.1.18"; + this.EXTENSION_VERSION = "0.1.18"; + } + getInstanceId() { return this.instanceId; } + getParams() { return this.params; } + /** + * Occurs when a trigger has been called within the Extension, and will include data such as the context of the trigger request. + */ + onStart(callback, options) { + this.events.push("firebase.extensions.firestore-translate-text.v1.onStart"); + return (0, eventarc_1.onCustomEventPublished)(Object.assign(Object.assign({}, options), { "eventType": "firebase.extensions.firestore-translate-text.v1.onStart", "channel": `projects/locations/${this.params._EVENT_ARC_REGION}/channels/firebase`, "region": `${this.params._EVENT_ARC_REGION}` }), callback); + } + /** + * Occurs when image resizing completes successfully. The event will contain further details about specific formats and sizes. + */ + onSuccess(callback, options) { + this.events.push("firebase.extensions.firestore-translate-text.v1.onSuccess"); + return (0, eventarc_1.onCustomEventPublished)(Object.assign(Object.assign({}, options), { "eventType": "firebase.extensions.firestore-translate-text.v1.onSuccess", "channel": `projects/locations/${this.params._EVENT_ARC_REGION}/channels/firebase`, "region": `${this.params._EVENT_ARC_REGION}` }), callback); + } + /** + * Occurs when an issue has been experienced in the Extension. This will include any error data that has been included within the Error Exception. + */ + onError(callback, options) { + this.events.push("firebase.extensions.firestore-translate-text.v1.onError"); + return (0, eventarc_1.onCustomEventPublished)(Object.assign(Object.assign({}, options), { "eventType": "firebase.extensions.firestore-translate-text.v1.onError", "channel": `projects/locations/${this.params._EVENT_ARC_REGION}/channels/firebase`, "region": `${this.params._EVENT_ARC_REGION}` }), callback); + } + /** + * Occurs when the function is settled. Provides no customized data other than the context. + */ + onCompletion(callback, options) { + this.events.push("firebase.extensions.firestore-translate-text.v1.onCompletion"); + return (0, eventarc_1.onCustomEventPublished)(Object.assign(Object.assign({}, options), { "eventType": "firebase.extensions.firestore-translate-text.v1.onCompletion", "channel": `projects/locations/${this.params._EVENT_ARC_REGION}/channels/firebase`, "region": `${this.params._EVENT_ARC_REGION}` }), callback); + } +} +exports.FirestoreTranslateText = FirestoreTranslateText; diff --git a/scripts/bin-test/extsdks/translate/package.json b/scripts/bin-test/extsdks/translate/package.json new file mode 100644 index 000000000..964287a7e --- /dev/null +++ b/scripts/bin-test/extsdks/translate/package.json @@ -0,0 +1,4 @@ +{ + "name": "@firebase-extensions/firebase-firestore-translate-text-sdk", + "main": "./index.js" +} \ No newline at end of file diff --git a/scripts/bin-test/run.sh b/scripts/bin-test/run.sh index aa10b4286..8998ad6f1 100755 --- a/scripts/bin-test/run.sh +++ b/scripts/bin-test/run.sh @@ -5,10 +5,17 @@ set -ex # Immediately exit on failure npm run build npm link -# Link local SDK to all test sources. +# Link the extensions SDKs for the testing environment. +(cd scripts/bin-test/extsdks/local && npm link) +(cd scripts/bin-test/extsdks/translate && npm link) +(cd scripts/bin-test/extsdks/translate && npm link firebase-functions) + +# Link SDKs to all test sources. for f in scripts/bin-test/sources/*; do if [ -d "$f" ]; then (cd "$f" && npm link firebase-functions) + (cd "$f" && npm link @firebase-extensions/firebase-firestore-translate-text-sdk) + (cd "$f" && npm link @firebase-extensions/local-backfill-sdk) fi done diff --git a/scripts/bin-test/sources/commonjs-grouped/index.js b/scripts/bin-test/sources/commonjs-grouped/index.js index b00828de0..022374a65 100644 --- a/scripts/bin-test/sources/commonjs-grouped/index.js +++ b/scripts/bin-test/sources/commonjs-grouped/index.js @@ -1,5 +1,8 @@ const functions = require("firebase-functions"); const functionsv2 = require("firebase-functions/v2"); +const firestoreTranslateText = require("@firebase-extensions/firebase-firestore-translate-text-sdk").firestoreTranslateText; +const backfill = require("@firebase-extensions/local-backfill-sdk").backfill; + exports.v1http = functions.https.onRequest((req, resp) => { resp.status(200).send("PASS"); @@ -17,4 +20,27 @@ exports.v2callable = functionsv2.https.onCall(() => { return "PASS"; }); +// A Firebase extension by ref +const extRef1 = firestoreTranslateText("extRef1", { + "COLLECTION_PATH": "collection1", + "INPUT_FIELD_NAME": "input1", + "LANGUAGES": "de,es", + "OUTPUT_FIELD_NAME": "translated", + "_EVENT_ARC_REGION": "us-central1", + "_FUNCTION_LOCATION": "us-central1", +}); +exports.extRef1 = extRef1; + +// A Firebase function defined by extension event +const ttOnStart = extRef1.onStart((event) => { + console.log("onStart got event: " + JSON.stringify(event, null, 2)); +}); +exports.ttOnStart = ttOnStart; + +// A Firebase extension by localPath +exports.extLocal2 = backfill("extLocal2", { + DO_BACKFILL: "False", + LOCATION: "us-central1", +}); + exports.g1 = require("./g1"); diff --git a/scripts/bin-test/sources/commonjs-main/functions.js b/scripts/bin-test/sources/commonjs-main/functions.js index 9122f9223..031e109d7 100644 --- a/scripts/bin-test/sources/commonjs-main/functions.js +++ b/scripts/bin-test/sources/commonjs-main/functions.js @@ -1,5 +1,7 @@ const functions = require("firebase-functions"); const functionsv2 = require("firebase-functions/v2"); +const firestoreTranslateText = require("@firebase-extensions/firebase-firestore-translate-text-sdk").firestoreTranslateText; +const backfill = require("@firebase-extensions/local-backfill-sdk").backfill; exports.v1http = functions.https.onRequest((req, resp) => { resp.status(200).send("PASS"); @@ -16,3 +18,26 @@ exports.v2http = functionsv2.https.onRequest((req, resp) => { exports.v2callable = functionsv2.https.onCall(() => { return "PASS"; }); + +// A Firebase extension by ref +const extRef1 = firestoreTranslateText("extRef1", { + "COLLECTION_PATH": "collection1", + "INPUT_FIELD_NAME": "input1", + "LANGUAGES": "de,es", + "OUTPUT_FIELD_NAME": "translated", + "_EVENT_ARC_REGION": "us-central1", + "_FUNCTION_LOCATION": "us-central1", +}); +exports.extRef1 = extRef1; + +// A Firebase function defined by extension event +const ttOnStart = extRef1.onStart((event) => { + console.log("onStart got event: " + JSON.stringify(event, null, 2)); +}); +exports.ttOnStart = ttOnStart; + +// A Firebase extension by localPath +exports.extLocal2 = backfill("extLocal2", { + DO_BACKFILL: "False", + LOCATION: "us-central1", +}); diff --git a/scripts/bin-test/sources/commonjs-preserve/index.js b/scripts/bin-test/sources/commonjs-preserve/index.js index 7c953dec8..768fb6c4e 100644 --- a/scripts/bin-test/sources/commonjs-preserve/index.js +++ b/scripts/bin-test/sources/commonjs-preserve/index.js @@ -1,5 +1,7 @@ const functions = require("firebase-functions"); const functionsv2 = require("firebase-functions/v2"); +const firestoreTranslateText = require("@firebase-extensions/firebase-firestore-translate-text-sdk").firestoreTranslateText; +const backfill = require("@firebase-extensions/local-backfill-sdk").backfill; exports.v1http = functions.https.onRequest((req, resp) => { resp.status(200).send("PASS"); @@ -16,3 +18,26 @@ functionsv2.setGlobalOptions({ preserveExternalChanges: true }); exports.v2http = functionsv2.https.onRequest((req, resp) => { resp.status(200).send("PASS"); }); + +// A Firebase extension by ref +const extRef1 = firestoreTranslateText("extRef1", { + "COLLECTION_PATH": "collection1", + "INPUT_FIELD_NAME": "input1", + "LANGUAGES": "de,es", + "OUTPUT_FIELD_NAME": "translated", + "_EVENT_ARC_REGION": "us-central1", + "_FUNCTION_LOCATION": "us-central1", +}); +exports.extRef1 = extRef1; + +// A Firebase function defined by extension event +const ttOnStart = extRef1.onStart((event) => { + console.log("onStart got event: " + JSON.stringify(event, null, 2)); +}); +exports.ttOnStart = ttOnStart; + +// A Firebase extension by localPath +exports.extLocal2 = backfill("extLocal2", { + DO_BACKFILL: "False", + LOCATION: "us-central1", +}); \ No newline at end of file diff --git a/scripts/bin-test/sources/commonjs/index.js b/scripts/bin-test/sources/commonjs/index.js index 9122f9223..031e109d7 100644 --- a/scripts/bin-test/sources/commonjs/index.js +++ b/scripts/bin-test/sources/commonjs/index.js @@ -1,5 +1,7 @@ const functions = require("firebase-functions"); const functionsv2 = require("firebase-functions/v2"); +const firestoreTranslateText = require("@firebase-extensions/firebase-firestore-translate-text-sdk").firestoreTranslateText; +const backfill = require("@firebase-extensions/local-backfill-sdk").backfill; exports.v1http = functions.https.onRequest((req, resp) => { resp.status(200).send("PASS"); @@ -16,3 +18,26 @@ exports.v2http = functionsv2.https.onRequest((req, resp) => { exports.v2callable = functionsv2.https.onCall(() => { return "PASS"; }); + +// A Firebase extension by ref +const extRef1 = firestoreTranslateText("extRef1", { + "COLLECTION_PATH": "collection1", + "INPUT_FIELD_NAME": "input1", + "LANGUAGES": "de,es", + "OUTPUT_FIELD_NAME": "translated", + "_EVENT_ARC_REGION": "us-central1", + "_FUNCTION_LOCATION": "us-central1", +}); +exports.extRef1 = extRef1; + +// A Firebase function defined by extension event +const ttOnStart = extRef1.onStart((event) => { + console.log("onStart got event: " + JSON.stringify(event, null, 2)); +}); +exports.ttOnStart = ttOnStart; + +// A Firebase extension by localPath +exports.extLocal2 = backfill("extLocal2", { + DO_BACKFILL: "False", + LOCATION: "us-central1", +}); diff --git a/scripts/bin-test/sources/esm-ext/index.mjs b/scripts/bin-test/sources/esm-ext/index.mjs index 91e974d93..5413b5fa2 100644 --- a/scripts/bin-test/sources/esm-ext/index.mjs +++ b/scripts/bin-test/sources/esm-ext/index.mjs @@ -1,5 +1,7 @@ import * as functions from "firebase-functions"; import * as functionsv2 from "firebase-functions/v2"; +import { firestoreTranslateText } from "@firebase-extensions/firebase-firestore-translate-text-sdk"; +import { backfill } from "@firebase-extensions/local-backfill-sdk"; export const v1http = functions.https.onRequest((req, resp) => { resp.status(200).send("PASS"); @@ -16,3 +18,24 @@ export const v2http = functionsv2.https.onRequest((req, resp) => { export const v2callable = functionsv2.https.onCall(() => { return "PASS"; }); + +// A Firebase extension by ref +export const extRef1 = firestoreTranslateText("extRef1", { + "COLLECTION_PATH": "collection1", + "INPUT_FIELD_NAME": "input1", + "LANGUAGES": "de,es", + "OUTPUT_FIELD_NAME": "translated", + "_EVENT_ARC_REGION": "us-central1", + "_FUNCTION_LOCATION": "us-central1", +}); + +// A Firebase function defined by extension event +export const ttOnStart = extRef1.onStart((event) => { + console.log("onStart got event: " + JSON.stringify(event, null, 2)); +}); + +// A Firebase extension by localPath +export const extLocal2 = backfill("extLocal2", { + DO_BACKFILL: "False", + LOCATION: "us-central1", +}); \ No newline at end of file diff --git a/scripts/bin-test/sources/esm-main/functions.js b/scripts/bin-test/sources/esm-main/functions.js index b09186731..3515c2124 100644 --- a/scripts/bin-test/sources/esm-main/functions.js +++ b/scripts/bin-test/sources/esm-main/functions.js @@ -1,5 +1,7 @@ import * as functions from "firebase-functions"; import * as functionsv2 from "firebase-functions/v2"; +import { firestoreTranslateText } from "@firebase-extensions/firebase-firestore-translate-text-sdk"; +import { backfill } from "@firebase-extensions/local-backfill-sdk"; export const v1http = functions.https.onRequest((req, resp) => { resp.status(200).send("PASS"); @@ -16,3 +18,24 @@ export const v2http = functionsv2.https.onRequest((req, resp) => { export const v2callable = functionsv2.https.onCall(() => { return "PASS"; }); + +// A Firebase extension by ref +export const extRef1 = firestoreTranslateText("extRef1", { + "COLLECTION_PATH": "collection1", + "INPUT_FIELD_NAME": "input1", + "LANGUAGES": "de,es", + "OUTPUT_FIELD_NAME": "translated", + "_EVENT_ARC_REGION": "us-central1", + "_FUNCTION_LOCATION": "us-central1", +}); + +// A Firebase function defined by extension event +export const ttOnStart = extRef1.onStart((event) => { + console.log("onStart got event: " + JSON.stringify(event, null, 2)); +}); + +// A Firebase extension by localPath +export const extLocal2 = backfill("extLocal2", { + DO_BACKFILL: "False", + LOCATION: "us-central1", +}); \ No newline at end of file diff --git a/scripts/bin-test/sources/esm/index.js b/scripts/bin-test/sources/esm/index.js index b09186731..3515c2124 100644 --- a/scripts/bin-test/sources/esm/index.js +++ b/scripts/bin-test/sources/esm/index.js @@ -1,5 +1,7 @@ import * as functions from "firebase-functions"; import * as functionsv2 from "firebase-functions/v2"; +import { firestoreTranslateText } from "@firebase-extensions/firebase-firestore-translate-text-sdk"; +import { backfill } from "@firebase-extensions/local-backfill-sdk"; export const v1http = functions.https.onRequest((req, resp) => { resp.status(200).send("PASS"); @@ -16,3 +18,24 @@ export const v2http = functionsv2.https.onRequest((req, resp) => { export const v2callable = functionsv2.https.onCall(() => { return "PASS"; }); + +// A Firebase extension by ref +export const extRef1 = firestoreTranslateText("extRef1", { + "COLLECTION_PATH": "collection1", + "INPUT_FIELD_NAME": "input1", + "LANGUAGES": "de,es", + "OUTPUT_FIELD_NAME": "translated", + "_EVENT_ARC_REGION": "us-central1", + "_FUNCTION_LOCATION": "us-central1", +}); + +// A Firebase function defined by extension event +export const ttOnStart = extRef1.onStart((event) => { + console.log("onStart got event: " + JSON.stringify(event, null, 2)); +}); + +// A Firebase extension by localPath +export const extLocal2 = backfill("extLocal2", { + DO_BACKFILL: "False", + LOCATION: "us-central1", +}); \ No newline at end of file diff --git a/scripts/bin-test/test.ts b/scripts/bin-test/test.ts index efc5a5127..85b2eb249 100644 --- a/scripts/bin-test/test.ts +++ b/scripts/bin-test/test.ts @@ -26,6 +26,29 @@ const DEFAULT_V1_OPTIONS = { ...DEFAULT_OPTIONS }; const DEFAULT_V2_OPTIONS = { ...DEFAULT_OPTIONS, concurrency: null }; +const BASE_EXTENSIONS = { + extRef1: { + params: { + COLLECTION_PATH: "collection1", + INPUT_FIELD_NAME: "input1", + LANGUAGES: "de,es", + OUTPUT_FIELD_NAME: "translated", + _EVENT_ARC_REGION: "us-central1", + "firebaseextensions.v1beta.function/location": "us-central1", + }, + ref: "firebase/firestore-translate-text@0.1.18", + events: ["firebase.extensions.firestore-translate-text.v1.onStart"], + }, + extLocal2: { + params: { + DO_BACKFILL: "False", + LOCATION: "us-central1", + }, + localPath: "./functions/generated/extensions/local/backfill/0.0.2/src", + events: [], + }, +}; + const BASE_STACK = { endpoints: { v1http: { @@ -55,9 +78,28 @@ const BASE_STACK = { labels: {}, callableTrigger: {}, }, + ttOnStart: { + ...DEFAULT_V2_OPTIONS, + platform: "gcfv2", + entryPoint: "ttOnStart", + labels: {}, + region: ["us-central1"], + eventTrigger: { + eventType: "firebase.extensions.firestore-translate-text.v1.onStart", + eventFilters: {}, + retry: false, + channel: "projects/locations/us-central1/channels/firebase", + }, + }, }, - requiredAPIs: [], + requiredAPIs: [ + { + api: "eventarcpublishing.googleapis.com", + reason: "Needed for custom event functions", + }, + ], specVersion: "v1alpha1", + extensions: BASE_EXTENSIONS, }; interface Testcase { @@ -108,10 +150,15 @@ async function startBin( FUNCTIONS_CONTROL_API: "true", }, }); - if (!proc) { throw new Error("Failed to start firebase functions"); } + proc.stdout?.on("data", (chunk: Buffer) => { + console.log(chunk.toString("utf8")); + }); + proc.stderr?.on("data", (chunk: Buffer) => { + console.log(chunk.toString("utf8")); + }); await retryUntil(async () => { try { @@ -246,9 +293,27 @@ describe("functions.yaml", function () { labels: {}, httpsTrigger: {}, }, + ttOnStart: { + platform: "gcfv2", + entryPoint: "ttOnStart", + labels: {}, + region: ["us-central1"], + eventTrigger: { + eventType: "firebase.extensions.firestore-translate-text.v1.onStart", + eventFilters: {}, + retry: false, + channel: "projects/locations/us-central1/channels/firebase", + }, + }, }, - requiredAPIs: [], + requiredAPIs: [ + { + api: "eventarcpublishing.googleapis.com", + reason: "Needed for custom event functions", + }, + ], specVersion: "v1alpha1", + extensions: BASE_EXTENSIONS, }, }, ]; diff --git a/spec/fixtures/extsdk/local/index.d.ts b/spec/fixtures/extsdk/local/index.d.ts new file mode 100644 index 000000000..ed3a39c74 --- /dev/null +++ b/spec/fixtures/extsdk/local/index.d.ts @@ -0,0 +1,37 @@ +/** + * TaskQueue/LifecycleEvent/RuntimeStatus Tester SDK for backfill@0.0.2 + * + * When filing bugs or feature requests please specify: + * "Extensions SDK v1.0.0 for Local extension. + * https://github.com/firebase/firebase-tools/issues/new/choose + * + * GENERATED FILE. DO NOT EDIT. + */ +export type DoBackfillParam = "True" | "False"; +export type LocationParam = "us-central1" | "us-east1" | "us-east4" | "europe-west1" | "europe-west2" | "europe-west3" | "asia-east2" | "asia-northeast1"; +/** + * Parameters for backfill@0.0.2 extension + */ +export interface BackfillParams { + /** + * Do a backfill + */ + DO_BACKFILL: DoBackfillParam; + /** + * Cloud Functions location + */ + LOCATION: LocationParam; +} +export declare function backfill(instanceId: string, params: BackfillParams): Backfill; +/** + * TaskQueue/LifecycleEvent/RuntimeStatus Tester + * A tester for the TaskQueue/LCE/RuntimeStatus project + */ +export declare class Backfill { + private instanceId; + private params; + readonly FIREBASE_EXTENSION_LOCAL_PATH = "./functions/generated/extensions/local/backfill/0.0.2/src"; + constructor(instanceId: string, params: BackfillParams); + getInstanceId(): string; + getParams(): BackfillParams; +} \ No newline at end of file diff --git a/spec/fixtures/extsdk/local/index.js b/spec/fixtures/extsdk/local/index.js new file mode 100644 index 000000000..f1f9cce55 --- /dev/null +++ b/spec/fixtures/extsdk/local/index.js @@ -0,0 +1,30 @@ +"use strict"; +/** + * TaskQueue/LifecycleEvent/RuntimeStatus Tester SDK for extensions-try-backfill3@0.0.2 + * + * When filing bugs or feature requests please specify: + * "Extensions SDK v1.0.0 for Local extension. + * https://github.com/firebase/firebase-tools/issues/new/choose + * + * GENERATED FILE. DO NOT EDIT. + */ +Object.defineProperty(exports, "__esModule", { value: true }); +exports.backfill = exports.backfill = void 0; +function backfill(instanceId, params) { + return new Backfill(instanceId, params); +} +exports.backfill = backfill; +/** + * TaskQueue/LifecycleEvent/RuntimeStatus Tester + * A tester for the TaskQueue/LCE/RuntimeStatus project + */ +class Backfill { + constructor(instanceId, params) { + this.instanceId = instanceId; + this.params = params; + this.FIREBASE_EXTENSION_LOCAL_PATH = "./functions/generated/extensions/local/backfill/0.0.2/src"; + } + getInstanceId() { return this.instanceId; } + getParams() { return this.params; } +} +exports.Backfill = Backfill; diff --git a/spec/fixtures/extsdk/local/package.json b/spec/fixtures/extsdk/local/package.json new file mode 100644 index 000000000..700806b3e --- /dev/null +++ b/spec/fixtures/extsdk/local/package.json @@ -0,0 +1,4 @@ +{ + "name": "@firebase-extensions/local-backfill-sdk", + "main": "./index.js" + } \ No newline at end of file diff --git a/spec/fixtures/extsdk/translate/index.d.ts b/spec/fixtures/extsdk/translate/index.d.ts new file mode 100644 index 000000000..8e5cee195 --- /dev/null +++ b/spec/fixtures/extsdk/translate/index.d.ts @@ -0,0 +1,122 @@ +/** + * Translate Text in Firestore SDK for firestore-translate-text@0.1.18 + * + * When filing bugs or feature requests please specify: + * "Extensions SDK v1.0.0 for firestore-translate-text@0.1.18" + * https://github.com/firebase/firebase-tools/issues/new/choose + * + * GENERATED FILE. DO NOT EDIT. + */ +import { CloudEvent } from "../../../../v2"; +import { EventarcTriggerOptions } from "../../../../v2/eventarc"; +export type EventCallback = (event: CloudEvent) => unknown | Promise; +export type SimpleEventarcTriggerOptions = Omit; +export type EventArcRegionType = "us-central1" | "us-west1" | "europe-west4" | "asia-northeast1"; +export type SystemFunctionVpcConnectorEgressSettingsParam = "VPC_CONNECTOR_EGRESS_SETTINGS_UNSPECIFIED" | "PRIVATE_RANGES_ONLY" | "ALL_TRAFFIC"; +export type SystemFunctionIngressSettingsParam = "ALLOW_ALL" | "ALLOW_INTERNAL_ONLY" | "ALLOW_INTERNAL_AND_GCLB"; +export type SystemFunctionLocationParam = "us-central1" | "us-east1" | "us-east4" | "us-west1" | "us-west2" | "us-west3" | "us-west4" | "europe-central2" | "europe-west1" | "europe-west2" | "europe-west3" | "europe-west6" | "asia-east1" | "asia-east2" | "asia-northeast1" | "asia-northeast2" | "asia-northeast3" | "asia-south1" | "asia-southeast1" | "asia-southeast2" | "northamerica-northeast1" | "southamerica-east1" | "australia-southeast1"; +export type SystemFunctionMemoryParam = "128" | "256" | "512" | "1024" | "2048" | "4096" | "8192"; +/** + * Parameters for firestore-translate-text@0.1.18 extension + */ +export interface FirestoreTranslateTextParams { + /** + * Target languages for translations, as a comma-separated list + */ + LANGUAGES: string; + /** + * Collection path + */ + COLLECTION_PATH: string; + /** + * Input field name + */ + INPUT_FIELD_NAME: string; + /** + * Translations output field name + */ + OUTPUT_FIELD_NAME: string; + /** + * Languages field name + */ + LANGUAGES_FIELD_NAME?: string; + /** + * Event Arc Region + */ + _EVENT_ARC_REGION?: EventArcRegionType; + /** + * Function timeout seconds + */ + _FUNCTION_TIMEOUT_SECONDS?: string; + /** + * VPC Connector + */ + _FUNCTION_VPC_CONNECTOR?: string; + /** + * VPC Connector Egress settings + */ + _FUNCTION_VPC_CONNECTOR_EGRESS_SETTINGS?: SystemFunctionVpcConnectorEgressSettingsParam; + /** + * Minimum function instances + */ + _FUNCTION_MIN_INSTANCES?: string; + /** + * Maximum function instances + */ + _FUNCTION_MAX_INSTANCES?: string; + /** + * Function ingress settings + */ + _FUNCTION_INGRESS_SETTINGS?: SystemFunctionIngressSettingsParam; + /** + * Function labels + */ + _FUNCTION_LABELS?: string; + /** + * KMS key name + */ + _FUNCTION_KMS_KEY_NAME?: string; + /** + * Docker repository + */ + _FUNCTION_DOCKER_REPOSITORY?: string; + /** + * Cloud Functions location + */ + _FUNCTION_LOCATION: SystemFunctionLocationParam; + /** + * Function memory + */ + _FUNCTION_MEMORY?: SystemFunctionMemoryParam; +} +export declare function firestoreTranslateText(instanceId: string, params: FirestoreTranslateTextParams): FirestoreTranslateText; +/** + * Translate Text in Firestore + * Translates strings written to a Cloud Firestore collection into multiple languages (uses Cloud Translation API). + */ +export declare class FirestoreTranslateText { + private instanceId; + private params; + events: string[]; + readonly FIREBASE_EXTENSION_REFERENCE = "firebase/firestore-translate-text@0.1.18"; + readonly EXTENSION_VERSION = "0.1.18"; + constructor(instanceId: string, params: FirestoreTranslateTextParams); + getInstanceId(): string; + getParams(): FirestoreTranslateTextParams; + /** + * Occurs when a trigger has been called within the Extension, and will include data such as the context of the trigger request. + */ + onStart(callback: EventCallback, options?: SimpleEventarcTriggerOptions): import("firebase-functions/v2").CloudFunction>; + /** + * Occurs when image resizing completes successfully. The event will contain further details about specific formats and sizes. + */ + onSuccess(callback: EventCallback, options?: SimpleEventarcTriggerOptions): import("firebase-functions/v2").CloudFunction>; + /** + * Occurs when an issue has been experienced in the Extension. This will include any error data that has been included within the Error Exception. + */ + onError(callback: EventCallback, options?: SimpleEventarcTriggerOptions): import("firebase-functions/v2").CloudFunction>; + /** + * Occurs when the function is settled. Provides no customized data other than the context. + */ + onCompletion(callback: EventCallback, options?: SimpleEventarcTriggerOptions): import("firebase-functions/v2").CloudFunction>; +} \ No newline at end of file diff --git a/spec/fixtures/extsdk/translate/index.js b/spec/fixtures/extsdk/translate/index.js new file mode 100644 index 000000000..e263f582f --- /dev/null +++ b/spec/fixtures/extsdk/translate/index.js @@ -0,0 +1,61 @@ +"use strict"; +/** + * Translate Text in Firestore SDK for firestore-translate-text@0.1.18 + * + * When filing bugs or feature requests please specify: + * "Extensions SDK v1.0.0 for firestore-translate-text@0.1.18" + * https://github.com/firebase/firebase-tools/issues/new/choose + * + * GENERATED FILE. DO NOT EDIT. + */ +Object.defineProperty(exports, "__esModule", { value: true }); +exports.FirestoreTranslateText = exports.firestoreTranslateText = void 0; +const eventarc_1 = require("../../../../src/v2/providers/eventarc"); +function firestoreTranslateText(instanceId, params) { + return new FirestoreTranslateText(instanceId, params); +} +exports.firestoreTranslateText = firestoreTranslateText; +/** + * Translate Text in Firestore + * Translates strings written to a Cloud Firestore collection into multiple languages (uses Cloud Translation API). + */ +class FirestoreTranslateText { + constructor(instanceId, params) { + this.instanceId = instanceId; + this.params = params; + this.events = []; + this.FIREBASE_EXTENSION_REFERENCE = "firebase/firestore-translate-text@0.1.18"; + this.EXTENSION_VERSION = "0.1.18"; + } + getInstanceId() { return this.instanceId; } + getParams() { return this.params; } + /** + * Occurs when a trigger has been called within the Extension, and will include data such as the context of the trigger request. + */ + onStart(callback, options) { + this.events.push("firebase.extensions.firestore-translate-text.v1.onStart"); + return (0, eventarc_1.onCustomEventPublished)(Object.assign(Object.assign({}, options), { "eventType": "firebase.extensions.firestore-translate-text.v1.onStart", "channel": `projects/locations/${this.params._EVENT_ARC_REGION}/channels/firebase`, "region": `${this.params._EVENT_ARC_REGION}` }), callback); + } + /** + * Occurs when image resizing completes successfully. The event will contain further details about specific formats and sizes. + */ + onSuccess(callback, options) { + this.events.push("firebase.extensions.firestore-translate-text.v1.onSuccess"); + return (0, eventarc_1.onCustomEventPublished)(Object.assign(Object.assign({}, options), { "eventType": "firebase.extensions.firestore-translate-text.v1.onSuccess", "channel": `projects/locations/${this.params._EVENT_ARC_REGION}/channels/firebase`, "region": `${this.params._EVENT_ARC_REGION}` }), callback); + } + /** + * Occurs when an issue has been experienced in the Extension. This will include any error data that has been included within the Error Exception. + */ + onError(callback, options) { + this.events.push("firebase.extensions.firestore-translate-text.v1.onError"); + return (0, eventarc_1.onCustomEventPublished)(Object.assign(Object.assign({}, options), { "eventType": "firebase.extensions.firestore-translate-text.v1.onError", "channel": `projects/locations/${this.params._EVENT_ARC_REGION}/channels/firebase`, "region": `${this.params._EVENT_ARC_REGION}` }), callback); + } + /** + * Occurs when the function is settled. Provides no customized data other than the context. + */ + onCompletion(callback, options) { + this.events.push("firebase.extensions.firestore-translate-text.v1.onCompletion"); + return (0, eventarc_1.onCustomEventPublished)(Object.assign(Object.assign({}, options), { "eventType": "firebase.extensions.firestore-translate-text.v1.onCompletion", "channel": `projects/locations/${this.params._EVENT_ARC_REGION}/channels/firebase`, "region": `${this.params._EVENT_ARC_REGION}` }), callback); + } +} +exports.FirestoreTranslateText = FirestoreTranslateText; diff --git a/spec/fixtures/extsdk/translate/package.json b/spec/fixtures/extsdk/translate/package.json new file mode 100644 index 000000000..964287a7e --- /dev/null +++ b/spec/fixtures/extsdk/translate/package.json @@ -0,0 +1,4 @@ +{ + "name": "@firebase-extensions/firebase-firestore-translate-text-sdk", + "main": "./index.js" +} \ No newline at end of file diff --git a/spec/fixtures/sources/commonjs-grouped/index.js b/spec/fixtures/sources/commonjs-grouped/index.js index 385f8ea79..06c976a71 100644 --- a/spec/fixtures/sources/commonjs-grouped/index.js +++ b/spec/fixtures/sources/commonjs-grouped/index.js @@ -1,5 +1,8 @@ const functions = require("../../../../src/v1"); const functionsv2 = require("../../../../src/v2"); +const firestoreTranslateText = require("../../extsdk/translate").firestoreTranslateText; +const backfill = require("../../extsdk/local").backfill; + exports.v1http = functions.https.onRequest((req, resp) => { resp.status(200).send("PASS"); @@ -17,4 +20,27 @@ exports.v2callable = functionsv2.https.onCall(() => { return "PASS"; }); +// A Firebase extension by ref +const extRef1 = firestoreTranslateText("extRef1", { + "COLLECTION_PATH": "collection1", + "INPUT_FIELD_NAME": "input1", + "LANGUAGES": "de,es", + "OUTPUT_FIELD_NAME": "translated", + "_EVENT_ARC_REGION": "us-central1", + "_FUNCTION_LOCATION": "us-central1", +}); +exports.extRef1 = extRef1; + +// A Firebase function defined by extension event +const ttOnStart = extRef1.onStart((event) => { + console.log("onStart got event: " + JSON.stringify(event, null, 2)); +}); +exports.ttOnStart = ttOnStart; + +// A Firebase extension by localPath +exports.extLocal2 = backfill("extLocal2", { + DO_BACKFILL: "False", + LOCATION: "us-central1", +}); + exports.g1 = require("./g1"); diff --git a/spec/fixtures/sources/commonjs-main/functions.js b/spec/fixtures/sources/commonjs-main/functions.js index 31b0ae2fa..b33e0cb71 100644 --- a/spec/fixtures/sources/commonjs-main/functions.js +++ b/spec/fixtures/sources/commonjs-main/functions.js @@ -1,5 +1,7 @@ const functions = require("../../../../src/v1"); const functionsv2 = require("../../../../src/v2"); +const firestoreTranslateText = require("../../extsdk/translate").firestoreTranslateText; +const backfill = require("../../extsdk/local").backfill; exports.v1http = functions.https.onRequest((req, resp) => { resp.status(200).send("PASS"); @@ -16,3 +18,26 @@ exports.v2http = functionsv2.https.onRequest((req, resp) => { exports.v2callable = functionsv2.https.onCall(() => { return "PASS"; }); + +// A Firebase extension by ref +const extRef1 = firestoreTranslateText("extRef1", { + "COLLECTION_PATH": "collection1", + "INPUT_FIELD_NAME": "input1", + "LANGUAGES": "de,es", + "OUTPUT_FIELD_NAME": "translated", + "_EVENT_ARC_REGION": "us-central1", + "_FUNCTION_LOCATION": "us-central1", +}); +exports.extRef1 = extRef1; + +// A Firebase function defined by extension event +const ttOnStart = extRef1.onStart((event) => { + console.log("onStart got event: " + JSON.stringify(event, null, 2)); +}); +exports.ttOnStart = ttOnStart; + +// A Firebase extension by localPath +exports.extLocal2 = backfill("extLocal2", { + DO_BACKFILL: "False", + LOCATION: "us-central1", +}); \ No newline at end of file diff --git a/spec/fixtures/sources/commonjs-params/index.js b/spec/fixtures/sources/commonjs-params/index.js index 447990730..76d07559c 100644 --- a/spec/fixtures/sources/commonjs-params/index.js +++ b/spec/fixtures/sources/commonjs-params/index.js @@ -1,5 +1,7 @@ const functions = require("../../../../src/v1/index"); const functionsv2 = require("../../../../src/v2/index"); +const firestoreTranslateText = require("../../extsdk/translate").firestoreTranslateText; +const backfill = require("../../extsdk/local").backfill; const params = require("../../../../src/params"); params.defineString("BORING"); @@ -40,3 +42,26 @@ exports.v2http = functionsv2.https.onRequest((req, resp) => { exports.v2callable = functionsv2.https.onCall(() => { return params.databaseURL; }); + +// A Firebase extension by ref +const extRef1 = firestoreTranslateText("extRef1", { + "COLLECTION_PATH": "collection1", + "INPUT_FIELD_NAME": "input1", + "LANGUAGES": "de,es", + "OUTPUT_FIELD_NAME": "translated", + "_EVENT_ARC_REGION": "us-central1", + "_FUNCTION_LOCATION": "us-central1", +}); +exports.extRef1 = extRef1; + +// A Firebase function defined by extension event +const ttOnStart = extRef1.onStart((event) => { + console.log("onStart got event: " + JSON.stringify(event, null, 2)); +}); +exports.ttOnStart = ttOnStart; + +// A Firebase extension by localPath +exports.extLocal2 = backfill("extLocal2", { + DO_BACKFILL: "False", + LOCATION: "us-central1", +}); diff --git a/spec/fixtures/sources/commonjs/index.js b/spec/fixtures/sources/commonjs/index.js index 31b0ae2fa..b33e0cb71 100644 --- a/spec/fixtures/sources/commonjs/index.js +++ b/spec/fixtures/sources/commonjs/index.js @@ -1,5 +1,7 @@ const functions = require("../../../../src/v1"); const functionsv2 = require("../../../../src/v2"); +const firestoreTranslateText = require("../../extsdk/translate").firestoreTranslateText; +const backfill = require("../../extsdk/local").backfill; exports.v1http = functions.https.onRequest((req, resp) => { resp.status(200).send("PASS"); @@ -16,3 +18,26 @@ exports.v2http = functionsv2.https.onRequest((req, resp) => { exports.v2callable = functionsv2.https.onCall(() => { return "PASS"; }); + +// A Firebase extension by ref +const extRef1 = firestoreTranslateText("extRef1", { + "COLLECTION_PATH": "collection1", + "INPUT_FIELD_NAME": "input1", + "LANGUAGES": "de,es", + "OUTPUT_FIELD_NAME": "translated", + "_EVENT_ARC_REGION": "us-central1", + "_FUNCTION_LOCATION": "us-central1", +}); +exports.extRef1 = extRef1; + +// A Firebase function defined by extension event +const ttOnStart = extRef1.onStart((event) => { + console.log("onStart got event: " + JSON.stringify(event, null, 2)); +}); +exports.ttOnStart = ttOnStart; + +// A Firebase extension by localPath +exports.extLocal2 = backfill("extLocal2", { + DO_BACKFILL: "False", + LOCATION: "us-central1", +}); \ No newline at end of file diff --git a/spec/runtime/loader.spec.ts b/spec/runtime/loader.spec.ts index f405b3cb9..e67140c1b 100644 --- a/spec/runtime/loader.spec.ts +++ b/spec/runtime/loader.spec.ts @@ -3,7 +3,12 @@ import * as path from "path"; import * as functions from "../../src/v1"; import * as loader from "../../src/runtime/loader"; -import { ManifestEndpoint, ManifestRequiredAPI, ManifestStack } from "../../src/runtime/manifest"; +import { + ManifestEndpoint, + ManifestExtension, + ManifestRequiredAPI, + ManifestStack, +} from "../../src/runtime/manifest"; import { clearParams } from "../../src/params"; import { MINIMAL_V1_ENDPOINT, MINIMAL_V2_ENDPOINT } from "../fixtures"; import { MINIMAL_SCHEDULE_TRIGGER, MINIMIAL_TASK_QUEUE_TRIGGER } from "../v1/providers/fixtures"; @@ -31,8 +36,9 @@ describe("extractStack", () => { const endpoints: Record = {}; const requiredAPIs: ManifestRequiredAPI[] = []; + const extensions: Record = {}; - loader.extractStack(module, endpoints, requiredAPIs); + loader.extractStack(module, endpoints, requiredAPIs, extensions); expect(endpoints).to.be.deep.equal({ http: { @@ -57,8 +63,9 @@ describe("extractStack", () => { const endpoints: Record = {}; const requiredAPIs: ManifestRequiredAPI[] = []; + const extensions: Record = {}; - loader.extractStack(module, endpoints, requiredAPIs); + loader.extractStack(module, endpoints, requiredAPIs, extensions); expect(endpoints).to.be.deep.equal({ taskq: { @@ -87,8 +94,9 @@ describe("extractStack", () => { const endpoints: Record = {}; const requiredAPIs: ManifestRequiredAPI[] = []; + const extensions: Record = {}; - loader.extractStack(module, endpoints, requiredAPIs); + loader.extractStack(module, endpoints, requiredAPIs, extensions); expect(endpoints).to.be.deep.equal({ fn1: { @@ -125,8 +133,9 @@ describe("extractStack", () => { const endpoints: Record = {}; const requiredAPIs: ManifestRequiredAPI[] = []; + const extensions: Record = {}; - loader.extractStack(module, endpoints, requiredAPIs); + loader.extractStack(module, endpoints, requiredAPIs, extensions); expect(endpoints).to.be.deep.equal({ fn: { @@ -152,8 +161,9 @@ describe("extractStack", () => { const endpoints: Record = {}; const requiredAPIs: ManifestRequiredAPI[] = []; + const extensions: Record = {}; - loader.extractStack(module, endpoints, requiredAPIs); + loader.extractStack(module, endpoints, requiredAPIs, extensions); expect(endpoints).to.be.deep.equal({ scheduled: { @@ -259,8 +269,48 @@ describe("loadStack", () => { labels: {}, callableTrigger: {}, }, + ttOnStart: { + ...MINIMAL_V2_ENDPOINT, + entryPoint: "ttOnStart", + eventTrigger: { + channel: "projects/locations/us-central1/channels/firebase", + eventFilters: {}, + eventType: "firebase.extensions.firestore-translate-text.v1.onStart", + retry: false, + }, + labels: {}, + platform: "gcfv2", + region: ["us-central1"], + }, + }, + requiredAPIs: [ + { + api: "eventarcpublishing.googleapis.com", + reason: "Needed for custom event functions", + }, + ], + extensions: { + extRef1: { + params: { + COLLECTION_PATH: "collection1", + INPUT_FIELD_NAME: "input1", + LANGUAGES: "de,es", + OUTPUT_FIELD_NAME: "translated", + "firebaseextensions.v1beta.function/location": "us-central1", + _EVENT_ARC_REGION: "us-central1", + }, + ref: "firebase/firestore-translate-text@0.1.18", + events: ["firebase.extensions.firestore-translate-text.v1.onStart"], + }, + extLocal2: { + params: { + DO_BACKFILL: "False", + LOCATION: "us-central1", + }, + localPath: "./functions/generated/extensions/local/backfill/0.0.2/src", + events: [], + }, }, - requiredAPIs: [], specVersion: "v1alpha1", }; @@ -387,6 +437,8 @@ describe("loadStack", () => { { name: "INT_PARAM", type: "int" }, { name: "BOOLEAN_PARAM", type: "boolean" }, ], + requiredAPIs: [], + extensions: {}, endpoints: { v1http: { ...MINIMAL_V1_ENDPOINT, diff --git a/spec/runtime/manifest.spec.ts b/spec/runtime/manifest.spec.ts index eb553643b..7534ba2ee 100644 --- a/spec/runtime/manifest.spec.ts +++ b/spec/runtime/manifest.spec.ts @@ -27,6 +27,7 @@ describe("stackToWire", () => { requiredAPIs: [], params: [regExpParam.toSpec()], specVersion: "v1alpha1", + extensions: {}, }; const expected = { endpoints: {}, @@ -43,6 +44,7 @@ describe("stackToWire", () => { }, ], specVersion: "v1alpha1", + extensions: {}, }; expect(stackToWire(stack)).to.deep.equal(expected); }); @@ -60,6 +62,13 @@ describe("stackToWire", () => { }, requiredAPIs: [], specVersion: "v1alpha1", + extensions: { + ext1: { + params: {}, + localPath: "localPath", + events: [], + }, + }, }; const expected = { endpoints: { @@ -73,6 +82,13 @@ describe("stackToWire", () => { }, requiredAPIs: [], specVersion: "v1alpha1", + extensions: { + ext1: { + params: {}, + localPath: "localPath", + events: [], + }, + }, }; expect(stackToWire(stack)).to.deep.equal(expected); }); @@ -97,6 +113,13 @@ describe("stackToWire", () => { }, requiredAPIs: [], specVersion: "v1alpha1", + extensions: { + ext1: { + params: {}, + localPath: "localPath", + events: [], + }, + }, }; const expected = { endpoints: { @@ -117,6 +140,13 @@ describe("stackToWire", () => { }, requiredAPIs: [], specVersion: "v1alpha1", + extensions: { + ext1: { + params: {}, + localPath: "localPath", + events: [], + }, + }, }; expect(stackToWire(stack)).to.deep.equal(expected); }); @@ -149,6 +179,13 @@ describe("stackToWire", () => { }, requiredAPIs: [], specVersion: "v1alpha1", + extensions: { + ext1: { + params: {}, + localPath: "localPath", + events: [], + }, + }, }; const expected = { endpoints: { @@ -172,6 +209,13 @@ describe("stackToWire", () => { }, requiredAPIs: [], specVersion: "v1alpha1", + extensions: { + ext1: { + params: {}, + localPath: "localPath", + events: [], + }, + }, }; expect(stackToWire(stack)).to.deep.equal(expected); }); diff --git a/src/runtime/loader.ts b/src/runtime/loader.ts index d1f9f2f32..953444e35 100644 --- a/src/runtime/loader.ts +++ b/src/runtime/loader.ts @@ -22,7 +22,12 @@ import * as path from "path"; import * as url from "url"; -import { ManifestEndpoint, ManifestRequiredAPI, ManifestStack } from "./manifest"; +import { + ManifestEndpoint, + ManifestExtension, + ManifestRequiredAPI, + ManifestStack, +} from "./manifest"; import * as params from "../params"; @@ -59,6 +64,7 @@ export function extractStack( module, endpoints: Record, requiredAPIs: ManifestRequiredAPI[], + extensions: Record, prefix = "" ) { for (const [name, valAsUnknown] of Object.entries(module)) { @@ -73,12 +79,96 @@ export function extractStack( if (val.__requiredAPIs && Array.isArray(val.__requiredAPIs)) { requiredAPIs.push(...val.__requiredAPIs); } - } else if (typeof val === "object" && val !== null) { - extractStack(val, endpoints, requiredAPIs, prefix + name + "-"); + } else if (isFirebaseRefExtension(val)) { + extensions[val.instanceId] = { + params: convertExtensionParams(val.params), + ref: val.FIREBASE_EXTENSION_REFERENCE, + events: val.events || [], + }; + } else if (isFirebaseLocalExtension(val)) { + extensions[val.instanceId] = { + params: convertExtensionParams(val.params), + localPath: val.FIREBASE_EXTENSION_LOCAL_PATH, + events: val.events || [], + }; + } else if (isObject(val)) { + extractStack(val, endpoints, requiredAPIs, extensions, prefix + name + "-"); + } + } +} + +function toTitleCase(txt: string): string { + return txt.charAt(0).toUpperCase() + txt.substring(1).toLowerCase(); +} + +function snakeToCamelCase(txt: string): string { + let ret = txt.toLowerCase(); + ret = ret.replace(/_/g, " "); + ret = ret.replace(/\w\S*/g, toTitleCase); + ret = ret.charAt(0).toLowerCase() + ret.substring(1); + return ret; +} + +function convertExtensionParams(params: object): Record { + const systemPrefixes: Record = { + FUNCTION: "firebaseextensions.v1beta.function", + V2FUNCTION: "firebaseextensions.v1beta.v2function", + }; + const converted: Record = {}; + for (const [rawKey, paramVal] of Object.entries(params)) { + let key = rawKey; + if (rawKey.startsWith("_") && rawKey !== "_EVENT_ARC_REGION") { + const prefix = rawKey.substring(1).split("_")[0]; + const suffix = rawKey.substring(2 + prefix.length); // 2 for underscores + key = `${systemPrefixes[prefix]}/${snakeToCamelCase(suffix)}`; + } + if (Array.isArray(paramVal)) { + converted[key] = paramVal.join(","); + } else { + converted[key] = paramVal as string; } } + return converted; +} + +function isObject(value: unknown): value is Record { + return typeof value === "object" && value !== null; +} + +interface FirebaseLocalExtension { + FIREBASE_EXTENSION_LOCAL_PATH: string; + instanceId: string; + params: Record; + events?: string[]; } +const isFirebaseLocalExtension = (val: unknown): val is FirebaseLocalExtension => { + return ( + isObject(val) && + typeof val.FIREBASE_EXTENSION_LOCAL_PATH === "string" && + typeof val.instanceId === "string" && + isObject(val.params) && + (!val.events || Array.isArray(val.events)) + ); +}; + +interface FirebaseRefExtension { + FIREBASE_EXTENSION_REFERENCE: string; + instanceId: string; + params: Record; + events?: string[]; +} + +const isFirebaseRefExtension = (val: unknown): val is FirebaseRefExtension => { + return ( + isObject(val) && + typeof val.FIREBASE_EXTENSION_REFERENCE === "string" && + typeof val.instanceId === "string" && + isObject(val.params) && + (!val.events || Array.isArray(val.events)) + ); +}; + /* @internal */ export function mergeRequiredAPIs(requiredAPIs: ManifestRequiredAPI[]): ManifestRequiredAPI[] { const apiToReasons: Record> = {}; @@ -99,14 +189,16 @@ export function mergeRequiredAPIs(requiredAPIs: ManifestRequiredAPI[]): Manifest export async function loadStack(functionsDir: string): Promise { const endpoints: Record = {}; const requiredAPIs: ManifestRequiredAPI[] = []; + const extensions: Record = {}; const mod = await loadModule(functionsDir); - extractStack(mod, endpoints, requiredAPIs); + extractStack(mod, endpoints, requiredAPIs, extensions); const stack: ManifestStack = { endpoints, specVersion: "v1alpha1", requiredAPIs: mergeRequiredAPIs(requiredAPIs), + extensions, }; if (params.declaredParams.length > 0) { stack.params = params.declaredParams.map((p) => p.toSpec()); diff --git a/src/runtime/manifest.ts b/src/runtime/manifest.ts index 6ff5fe0f9..47b948825 100644 --- a/src/runtime/manifest.ts +++ b/src/runtime/manifest.ts @@ -22,10 +22,21 @@ import { RESET_VALUE, ResettableKeys, ResetValue } from "../common/options"; import { Expression } from "../params"; -import { WireParamSpec } from "../params/types"; +import { WireParamSpec, SecretParam } from "../params/types"; /** - * An definition of a function as appears in the Manifest. + * A definition of an extension as appears in the Manifest. + * Exactly one of ref or localPath must be present. + */ +export interface ManifestExtension { + params: Record; + ref?: string; + localPath?: string; + events: string[]; +} + +/** + * A definition of a function as appears in the Manifest. * * @alpha */ @@ -113,7 +124,8 @@ export interface ManifestRequiredAPI { } /** - * An definition of a function deployment as appears in the Manifest. + * A definition of a function/extension deployment as appears in the Manifest. + * * @alpha */ export interface ManifestStack { @@ -121,6 +133,7 @@ export interface ManifestStack { params?: WireParamSpec[]; requiredAPIs: ManifestRequiredAPI[]; endpoints: Record; + extensions?: Record; } /**