firebase-tools 13.29.3 → 13.31.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/lib/apphosting/secrets/dialogs.js +2 -2
  2. package/lib/commands/appdistribution-groups-list.js +1 -1
  3. package/lib/commands/appdistribution-testers-list.js +4 -4
  4. package/lib/commands/apphosting-backends-list.js +1 -1
  5. package/lib/commands/apphosting-secrets-describe.js +1 -1
  6. package/lib/commands/apps-android-sha-list.js +1 -1
  7. package/lib/commands/apps-create.js +1 -104
  8. package/lib/commands/apps-init.js +94 -0
  9. package/lib/commands/apps-list.js +1 -1
  10. package/lib/commands/apps-sdkconfig.js +3 -3
  11. package/lib/commands/database-instances-list.js +1 -1
  12. package/lib/commands/dataconnect-services-list.js +1 -1
  13. package/lib/commands/emulators-start.js +1 -1
  14. package/lib/commands/experiments-list.js +1 -1
  15. package/lib/commands/ext-dev-list.js +1 -1
  16. package/lib/commands/ext-dev-usage.js +1 -1
  17. package/lib/commands/functions-list.js +1 -1
  18. package/lib/commands/hosting-channel-list.js +1 -1
  19. package/lib/commands/hosting-sites-get.js +1 -1
  20. package/lib/commands/hosting-sites-list.js +1 -1
  21. package/lib/commands/index.js +1 -0
  22. package/lib/commands/projects-list.js +1 -1
  23. package/lib/commands/remoteconfig-get.js +1 -1
  24. package/lib/commands/remoteconfig-versions-list.js +1 -1
  25. package/lib/crashlytics/buildToolsJarHelper.js +1 -1
  26. package/lib/dataconnect/build.js +6 -0
  27. package/lib/dataconnect/client.js +1 -1
  28. package/lib/dataconnect/dataplaneClient.js +1 -1
  29. package/lib/dataconnect/fileUtils.js +25 -2
  30. package/lib/dataconnect/graphqlError.js +5 -3
  31. package/lib/emulator/commandUtils.js +1 -1
  32. package/lib/emulator/dataconnectEmulator.js +4 -1
  33. package/lib/emulator/dataconnectToolkitController.js +5 -0
  34. package/lib/emulator/downloadableEmulators.js +9 -9
  35. package/lib/emulator/extensionsEmulator.js +2 -2
  36. package/lib/experiments.js +6 -0
  37. package/lib/extensions/change-log.js +1 -25
  38. package/lib/extensions/listExtensions.js +1 -1
  39. package/lib/extensions/provisioningHelper.js +1 -1
  40. package/lib/firestore/pretty-print.js +1 -1
  41. package/lib/frameworks/angular/index.js +11 -6
  42. package/lib/frameworks/utils.js +3 -1
  43. package/lib/functions/secrets.js +1 -1
  44. package/lib/gcp/cloudfunctionsv2.js +3 -0
  45. package/lib/gcp/cloudsql/interactive.js +1 -1
  46. package/lib/init/features/dataconnect/sdk.js +28 -3
  47. package/lib/init/features/genkit/index.js +17 -9
  48. package/lib/management/apps.js +314 -7
  49. package/lib/profileReport.js +1 -1
  50. package/lib/prompt.js +10 -2
  51. package/package.json +14 -5
  52. package/templates/genkit/firebase.0.9.0.template +1 -1
  53. package/templates/genkit/firebase.1.0.0.template +66 -0
  54. package/templates/init/dataconnect/mutations.gql +9 -26
  55. package/templates/init/dataconnect/queries.gql +9 -14
  56. package/templates/init/dataconnect/schema.gql +28 -21
  57. package/templates/setup/web.js +0 -5
@@ -1,16 +1,248 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.deleteAppAndroidSha = exports.createAppAndroidSha = exports.listAppAndroidSha = exports.getAppConfig = exports.getAppConfigFile = exports.listFirebaseApps = exports.createWebApp = exports.createAndroidApp = exports.createIosApp = exports.getAppPlatform = exports.ShaCertificateType = exports.AppPlatform = exports.APP_LIST_PAGE_SIZE = void 0;
3
+ exports.findIntelligentPathForAndroid = exports.findIntelligentPathForIOS = exports.deleteAppAndroidSha = exports.createAppAndroidSha = exports.listAppAndroidSha = exports.getAppConfig = exports.writeConfigToFile = exports.getAppConfigFile = exports.listFirebaseApps = exports.createWebApp = exports.createAndroidApp = exports.createIosApp = exports.getAppPlatform = exports.ShaCertificateType = exports.AppPlatform = exports.getSdkConfig = exports.checkForApps = exports.getSdkOutputPath = exports.sdkInit = exports.getPlatform = exports.APP_LIST_PAGE_SIZE = void 0;
4
+ const fs = require("fs-extra");
5
+ const ora = require("ora");
6
+ const path = require("path");
4
7
  const apiv2_1 = require("../apiv2");
5
8
  const api_1 = require("../api");
6
9
  const error_1 = require("../error");
7
10
  const logger_1 = require("../logger");
8
11
  const operation_poller_1 = require("../operation-poller");
9
- const templates_1 = require("../templates");
12
+ const types_1 = require("../dataconnect/types");
13
+ const projectUtils_1 = require("../projectUtils");
14
+ const prompt_1 = require("../prompt");
15
+ const projects_1 = require("./projects");
16
+ const fileUtils_1 = require("../dataconnect/fileUtils");
17
+ const utils_1 = require("../utils");
10
18
  const TIMEOUT_MILLIS = 30000;
11
19
  exports.APP_LIST_PAGE_SIZE = 100;
12
20
  const CREATE_APP_API_REQUEST_TIMEOUT_MILLIS = 15000;
13
- const WEB_CONFIG_FILE_NAME = "google-config.js";
21
+ const DISPLAY_NAME_QUESTION = {
22
+ type: "input",
23
+ name: "displayName",
24
+ default: "",
25
+ message: "What would you like to call your app?",
26
+ };
27
+ async function getPlatform(appDir, config) {
28
+ let targetPlatform = await (0, fileUtils_1.getPlatformFromFolder)(appDir);
29
+ if (targetPlatform === types_1.Platform.NONE) {
30
+ appDir = await (0, prompt_1.promptForDirectory)({
31
+ config,
32
+ relativeTo: appDir,
33
+ message: "We couldn't determine what kind of app you're using. Where is your app directory?",
34
+ });
35
+ targetPlatform = await (0, fileUtils_1.getPlatformFromFolder)(appDir);
36
+ }
37
+ if (targetPlatform === types_1.Platform.NONE || targetPlatform === types_1.Platform.MULTIPLE) {
38
+ if (targetPlatform === types_1.Platform.NONE) {
39
+ (0, utils_1.logBullet)(`Couldn't automatically detect app your in directory ${appDir}.`);
40
+ }
41
+ else {
42
+ (0, utils_1.logSuccess)(`Detected multiple app platforms in directory ${appDir}`);
43
+ }
44
+ const platforms = [
45
+ { name: "iOS (Swift)", value: types_1.Platform.IOS },
46
+ { name: "Web (JavaScript)", value: types_1.Platform.WEB },
47
+ { name: "Android (Kotlin)", value: types_1.Platform.ANDROID },
48
+ ];
49
+ targetPlatform = await (0, prompt_1.promptOnce)({
50
+ message: "Which platform do you want to set up an SDK for? Note: We currently do not support automatically setting up C++ or Unity projects.",
51
+ type: "list",
52
+ choices: platforms,
53
+ });
54
+ }
55
+ else if (targetPlatform === types_1.Platform.FLUTTER) {
56
+ (0, utils_1.logWarning)(`Detected ${targetPlatform} app in directory ${appDir}`);
57
+ throw new error_1.FirebaseError(`Flutter is not supported by apps:configure.
58
+ Please follow the link below to set up firebase for your Flutter app:
59
+ https://firebase.google.com/docs/flutter/setup
60
+ `);
61
+ }
62
+ else {
63
+ (0, utils_1.logSuccess)(`Detected ${targetPlatform} app in directory ${appDir}`);
64
+ }
65
+ return targetPlatform === types_1.Platform.MULTIPLE
66
+ ? AppPlatform.PLATFORM_UNSPECIFIED
67
+ : targetPlatform;
68
+ }
69
+ exports.getPlatform = getPlatform;
70
+ async function initiateIosAppCreation(options) {
71
+ if (!options.nonInteractive) {
72
+ await (0, prompt_1.prompt)(options, [
73
+ DISPLAY_NAME_QUESTION,
74
+ {
75
+ type: "input",
76
+ default: "",
77
+ name: "bundleId",
78
+ message: "Please specify your iOS app bundle ID:",
79
+ },
80
+ {
81
+ type: "input",
82
+ default: "",
83
+ name: "appStoreId",
84
+ message: "Please specify your iOS app App Store ID:",
85
+ },
86
+ ]);
87
+ }
88
+ if (!options.bundleId) {
89
+ throw new error_1.FirebaseError("Bundle ID for iOS app cannot be empty");
90
+ }
91
+ const spinner = ora("Creating your iOS app").start();
92
+ try {
93
+ const appData = await createIosApp(options.project, {
94
+ displayName: options.displayName,
95
+ bundleId: options.bundleId,
96
+ appStoreId: options.appStoreId,
97
+ });
98
+ spinner.succeed();
99
+ return appData;
100
+ }
101
+ catch (err) {
102
+ spinner.fail();
103
+ throw err;
104
+ }
105
+ }
106
+ async function initiateAndroidAppCreation(options) {
107
+ if (!options.nonInteractive) {
108
+ await (0, prompt_1.prompt)(options, [
109
+ DISPLAY_NAME_QUESTION,
110
+ {
111
+ type: "input",
112
+ default: "",
113
+ name: "packageName",
114
+ message: "Please specify your Android app package name:",
115
+ },
116
+ ]);
117
+ }
118
+ if (!options.packageName) {
119
+ throw new error_1.FirebaseError("Package name for Android app cannot be empty");
120
+ }
121
+ const spinner = ora("Creating your Android app").start();
122
+ try {
123
+ const appData = await createAndroidApp(options.project, {
124
+ displayName: options.displayName,
125
+ packageName: options.packageName,
126
+ });
127
+ spinner.succeed();
128
+ return appData;
129
+ }
130
+ catch (err) {
131
+ spinner.fail();
132
+ throw err;
133
+ }
134
+ }
135
+ async function initiateWebAppCreation(options) {
136
+ if (!options.nonInteractive) {
137
+ await (0, prompt_1.prompt)(options, [DISPLAY_NAME_QUESTION]);
138
+ }
139
+ if (!options.displayName) {
140
+ throw new error_1.FirebaseError("Display name for Web app cannot be empty");
141
+ }
142
+ const spinner = ora("Creating your Web app").start();
143
+ try {
144
+ const appData = await createWebApp(options.project, { displayName: options.displayName });
145
+ spinner.succeed();
146
+ return appData;
147
+ }
148
+ catch (err) {
149
+ spinner.fail();
150
+ throw err;
151
+ }
152
+ }
153
+ async function sdkInit(appPlatform, options) {
154
+ let appData;
155
+ switch (appPlatform) {
156
+ case AppPlatform.IOS:
157
+ appData = await initiateIosAppCreation(options);
158
+ break;
159
+ case AppPlatform.ANDROID:
160
+ appData = await initiateAndroidAppCreation(options);
161
+ break;
162
+ case AppPlatform.WEB:
163
+ appData = await initiateWebAppCreation(options);
164
+ break;
165
+ default:
166
+ throw new error_1.FirebaseError("Unexpected error. This should not happen");
167
+ }
168
+ return appData;
169
+ }
170
+ exports.sdkInit = sdkInit;
171
+ async function getSdkOutputPath(appDir, platform, config) {
172
+ switch (platform) {
173
+ case AppPlatform.ANDROID:
174
+ const androidPath = await findIntelligentPathForAndroid(appDir, config);
175
+ return path.join(androidPath, "google-services.json");
176
+ case AppPlatform.WEB:
177
+ return path.join(appDir, "firebase-js-config.json");
178
+ case AppPlatform.IOS:
179
+ const iosPath = await findIntelligentPathForIOS(appDir, config);
180
+ return path.join(iosPath, "GoogleService-Info.plist");
181
+ }
182
+ throw new error_1.FirebaseError("Platform " + platform.toString() + " is not supported yet.");
183
+ }
184
+ exports.getSdkOutputPath = getSdkOutputPath;
185
+ function checkForApps(apps, appPlatform) {
186
+ if (!apps.length) {
187
+ throw new error_1.FirebaseError(`There are no ${appPlatform === AppPlatform.ANY ? "" : appPlatform + " "}apps ` +
188
+ "associated with this Firebase project");
189
+ }
190
+ }
191
+ exports.checkForApps = checkForApps;
192
+ async function selectAppInteractively(apps, appPlatform) {
193
+ checkForApps(apps, appPlatform);
194
+ const choices = apps.map((app) => {
195
+ return {
196
+ name: `${app.displayName || app.bundleId || app.packageName}` +
197
+ ` - ${app.appId} (${app.platform})`,
198
+ value: app,
199
+ };
200
+ });
201
+ return await (0, prompt_1.promptOnce)({
202
+ type: "list",
203
+ message: `Select the ${appPlatform === AppPlatform.ANY ? "" : appPlatform + " "}` +
204
+ "app to get the configuration data:",
205
+ choices,
206
+ });
207
+ }
208
+ async function getSdkConfig(options, appPlatform, appId) {
209
+ if (!appId) {
210
+ let projectId = (0, projectUtils_1.needProjectId)(options);
211
+ if (options.nonInteractive && !projectId) {
212
+ throw new error_1.FirebaseError("Must supply app and project ids in non-interactive mode.");
213
+ }
214
+ else if (!projectId) {
215
+ const result = await (0, projects_1.getOrPromptProject)(options);
216
+ projectId = result.projectId;
217
+ }
218
+ const apps = await listFirebaseApps(projectId, appPlatform);
219
+ checkForApps(apps, appPlatform);
220
+ if (apps.length === 1) {
221
+ appId = apps[0].appId;
222
+ appPlatform = apps[0].platform;
223
+ }
224
+ else if (options.nonInteractive) {
225
+ throw new error_1.FirebaseError(`Project ${projectId} has multiple apps, must specify an app id.`);
226
+ }
227
+ else {
228
+ const appMetadata = await selectAppInteractively(apps, appPlatform);
229
+ appId = appMetadata.appId;
230
+ appPlatform = appMetadata.platform;
231
+ }
232
+ }
233
+ let configData;
234
+ const spinner = ora(`Downloading configuration data for your Firebase ${appPlatform} app`).start();
235
+ try {
236
+ configData = await getAppConfig(appId, appPlatform);
237
+ }
238
+ catch (err) {
239
+ spinner.fail();
240
+ throw err;
241
+ }
242
+ spinner.succeed();
243
+ return configData;
244
+ }
245
+ exports.getSdkConfig = getSdkConfig;
14
246
  var AppPlatform;
15
247
  (function (AppPlatform) {
16
248
  AppPlatform["PLATFORM_UNSPECIFIED"] = "PLATFORM_UNSPECIFIED";
@@ -182,13 +414,12 @@ function getAppConfigResourceString(appId, platform) {
182
414
  }
183
415
  function parseConfigFromResponse(responseBody, platform) {
184
416
  if (platform === AppPlatform.WEB) {
185
- const JS_TEMPLATE = (0, templates_1.readTemplateSync)("setup/web.js");
186
417
  return {
187
- fileName: WEB_CONFIG_FILE_NAME,
188
- fileContents: JS_TEMPLATE.replace("{/*--CONFIG--*/}", JSON.stringify(responseBody, null, 2)),
418
+ fileName: "firebase-js-config.json",
419
+ fileContents: JSON.stringify(responseBody, null, 2),
189
420
  };
190
421
  }
191
- else if (platform === AppPlatform.ANDROID || platform === AppPlatform.IOS) {
422
+ else if ("configFilename" in responseBody) {
192
423
  return {
193
424
  fileName: responseBody.configFilename,
194
425
  fileContents: Buffer.from(responseBody.configFileContents, "base64").toString("utf8"),
@@ -200,6 +431,24 @@ function getAppConfigFile(config, platform) {
200
431
  return parseConfigFromResponse(config, platform);
201
432
  }
202
433
  exports.getAppConfigFile = getAppConfigFile;
434
+ async function writeConfigToFile(filename, nonInteractive, fileContents) {
435
+ if (fs.existsSync(filename)) {
436
+ if (nonInteractive) {
437
+ throw new error_1.FirebaseError(`${filename} already exists`);
438
+ }
439
+ const overwrite = await (0, prompt_1.promptOnce)({
440
+ type: "confirm",
441
+ default: false,
442
+ message: `${filename} already exists. Do you want to overwrite?`,
443
+ });
444
+ if (!overwrite) {
445
+ return false;
446
+ }
447
+ }
448
+ await fs.writeFile(filename, fileContents);
449
+ return true;
450
+ }
451
+ exports.writeConfigToFile = writeConfigToFile;
203
452
  async function getAppConfig(appId, platform) {
204
453
  try {
205
454
  const response = await apiClient.request({
@@ -278,3 +527,61 @@ async function deleteAppAndroidSha(projectId, appId, shaId) {
278
527
  }
279
528
  }
280
529
  exports.deleteAppAndroidSha = deleteAppAndroidSha;
530
+ async function findIntelligentPathForIOS(appDir, options) {
531
+ const currentFiles = await fs.readdir(appDir, { withFileTypes: true });
532
+ for (let i = 0; i < currentFiles.length; i++) {
533
+ const dirent = currentFiles[i];
534
+ const xcodeStr = ".xcodeproj";
535
+ const file = dirent.name;
536
+ if (file.endsWith(xcodeStr)) {
537
+ return path.join(appDir, file.substring(0, file.length - xcodeStr.length));
538
+ }
539
+ else if (file === "Info.plist" ||
540
+ file === "Assets.xcassets" ||
541
+ (dirent.isDirectory() && file === "Preview Content")) {
542
+ return appDir;
543
+ }
544
+ }
545
+ let outputPath = null;
546
+ if (!options.nonInteractive) {
547
+ outputPath = await (0, prompt_1.promptForDirectory)({
548
+ config: options.config,
549
+ message: `We weren't able to automatically determine the output directory. Where would you like to output your config file?`,
550
+ relativeTo: appDir,
551
+ });
552
+ }
553
+ if (!outputPath) {
554
+ throw new Error("We weren't able to automatically determine the output directory.");
555
+ }
556
+ return outputPath;
557
+ }
558
+ exports.findIntelligentPathForIOS = findIntelligentPathForIOS;
559
+ async function findIntelligentPathForAndroid(appDir, options) {
560
+ const paths = appDir.split("/");
561
+ if (paths[0] === "app") {
562
+ return appDir;
563
+ }
564
+ else {
565
+ const currentFiles = await fs.readdir(appDir, { withFileTypes: true });
566
+ const dirs = [];
567
+ for (const fileOrDir of currentFiles) {
568
+ if (fileOrDir.isDirectory()) {
569
+ if (fileOrDir.name === "src") {
570
+ return appDir;
571
+ }
572
+ }
573
+ }
574
+ let module = path.join(appDir, "app");
575
+ if (dirs.length === 1) {
576
+ return module;
577
+ }
578
+ if (!options.nonInteractive) {
579
+ module = await (0, prompt_1.promptForDirectory)({
580
+ config: options.config,
581
+ message: `We weren't able to automatically determine the output directory. Where would you like to output your config file?`,
582
+ });
583
+ }
584
+ return module;
585
+ }
586
+ }
587
+ exports.findIntelligentPathForAndroid = findIntelligentPathForAndroid;
@@ -2,7 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.ProfileReport = exports.extractReadableIndex = exports.formatBytes = exports.formatNumber = exports.pathString = exports.extractJSON = void 0;
4
4
  const clc = require("colorette");
5
- const Table = require("cli-table");
5
+ const Table = require("cli-table3");
6
6
  const fs = require("fs");
7
7
  const _ = require("lodash");
8
8
  const readline = require("readline");
package/lib/prompt.js CHANGED
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.promptForDirectory = exports.confirm = exports.promptOnce = exports.prompt = void 0;
4
4
  const inquirer = require("inquirer");
5
+ const path = require("path");
5
6
  const fsutils_1 = require("./fsutils");
6
7
  const error_1 = require("./error");
7
8
  const logger_1 = require("./logger");
@@ -54,9 +55,16 @@ exports.confirm = confirm;
54
55
  async function promptForDirectory(args) {
55
56
  let dir = "";
56
57
  while (!dir) {
57
- const target = args.config.path(await promptOnce({
58
+ const promptPath = await promptOnce({
58
59
  message: args.message,
59
- }));
60
+ });
61
+ let target;
62
+ if (args.relativeTo) {
63
+ target = path.resolve(args.relativeTo, promptPath);
64
+ }
65
+ else {
66
+ target = args.config.path(promptPath);
67
+ }
60
68
  if ((0, fsutils_1.fileExistsSync)(target)) {
61
69
  logger_1.logger.error(`Expected a directory, but ${target} is a file. Please provide a path to a directory.`);
62
70
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "firebase-tools",
3
- "version": "13.29.3",
3
+ "version": "13.31.0",
4
4
  "description": "Command-Line Interface for Firebase",
5
5
  "main": "./lib/index.js",
6
6
  "bin": {
@@ -29,7 +29,7 @@
29
29
  ],
30
30
  "preferGlobal": true,
31
31
  "engines": {
32
- "node": ">=18.0.0 || >=20.0.0"
32
+ "node": ">=18.0.0 || >=20.0.0 || >=22.0.0"
33
33
  },
34
34
  "author": "Firebase (https://firebase.google.com/)",
35
35
  "license": "MIT",
@@ -71,7 +71,7 @@
71
71
  "body-parser": "^1.19.0",
72
72
  "chokidar": "^3.6.0",
73
73
  "cjson": "^0.3.1",
74
- "cli-table": "0.3.11",
74
+ "cli-table3": "0.6.5",
75
75
  "colorette": "^2.0.19",
76
76
  "commander": "^5.1.0",
77
77
  "configstore": "^5.0.1",
@@ -84,7 +84,7 @@
84
84
  "exegesis-express": "^4.0.0",
85
85
  "express": "^4.16.4",
86
86
  "filesize": "^6.1.0",
87
- "form-data": "^4.0.0",
87
+ "form-data": "^4.0.1",
88
88
  "fs-extra": "^10.1.0",
89
89
  "fuzzy": "^0.1.3",
90
90
  "gaxios": "^6.7.0",
@@ -116,7 +116,7 @@
116
116
  "sql-formatter": "^15.3.0",
117
117
  "stream-chain": "^2.2.4",
118
118
  "stream-json": "^1.7.3",
119
- "superstatic": "^9.1.0",
119
+ "superstatic": "^9.2.0",
120
120
  "tar": "^6.1.11",
121
121
  "tcp-port-used": "^1.0.2",
122
122
  "tmp": "^0.2.3",
@@ -128,5 +128,14 @@
128
128
  "winston-transport": "^4.4.0",
129
129
  "ws": "^7.5.10",
130
130
  "yaml": "^2.4.1"
131
+ },
132
+ "overrides": {
133
+ "@angular-devkit/core": {
134
+ "ajv-formats": "3.0.1",
135
+ "ajv": "^8.17.1"
136
+ },
137
+ "node-fetch": {
138
+ "whatwg-url": "^14.0.0"
139
+ }
131
140
  }
132
141
  }
@@ -19,7 +19,7 @@ export const menuSuggestionFlow = onFlow(
19
19
  ai,
20
20
  {
21
21
  name: "menuSuggestionFlow",
22
- inputSchema: z.string(),
22
+ inputSchema: z.string().describe("A restaurant theme").default("seafood"),
23
23
  outputSchema: z.string(),
24
24
  authPolicy: firebaseAuth((user) => {
25
25
  // By default, the firebaseAuth policy requires that all requests have an
@@ -0,0 +1,66 @@
1
+ // Import the Genkit core libraries and plugins.
2
+ import {genkit, z} from "genkit";
3
+ $GENKIT_CONFIG_IMPORTS
4
+ $GENKIT_MODEL_IMPORT
5
+
6
+ // Cloud Functions for Firebase supports Genkit natively. The onCallGenkit function creates a callable
7
+ // function from a Genkit action. It automatically implements streaming if your flow does.
8
+ // The https library also has other utility methods such as hasClaim, which verifies that
9
+ // a caller's token has a specific claim (optionally matching a specific value)
10
+ import { onCallGenkit, hasClaim } from "firebase-functions/https";
11
+
12
+ // Genkit models generally depend on an API key. APIs should be stored in Cloud Secret Manager so that
13
+ // access to these sensitive values can be controlled. defineSecret does this for you automatically.
14
+ // If you are using Google generative AI you can get an API key at https://aistudio.google.com/app/apikey
15
+ import { defineSecret } from "firebase-functions/params";
16
+ const apiKey = defineSecret("GOOGLE_GENAI_API_KEY");
17
+
18
+ const ai = genkit({
19
+ plugins: [
20
+ $GENKIT_CONFIG_PLUGINS
21
+ ],
22
+ });
23
+
24
+ // Define a simple flow that prompts an LLM to generate menu suggestions.
25
+ const menuSuggestionFlow = ai.defineFlow({
26
+ name: "menuSuggestionFlow",
27
+ inputSchema: z.string().describe("A restaurant theme").default("seafood"),
28
+ outputSchema: z.string(),
29
+ streamSchema: z.string(),
30
+ }, async (subject, { sendChunk }) => {
31
+ // Construct a request and send it to the model API.
32
+ const prompt =
33
+ `Suggest an item for the menu of a ${subject} themed restaurant`;
34
+ const { response, stream } = ai.generateStream({
35
+ model: $GENKIT_MODEL,
36
+ prompt: prompt,
37
+ config: {
38
+ temperature: 1,
39
+ },
40
+ });
41
+
42
+ for await (const chunk of stream) {
43
+ sendChunk(chunk.text);
44
+ }
45
+
46
+ // Handle the response from the model API. In this sample, we just
47
+ // convert it to a string, but more complicated flows might coerce the
48
+ // response into structured output or chain the response into another
49
+ // LLM call, etc.
50
+ return (await response).text;
51
+ }
52
+ );
53
+
54
+ export const menuSuggestion = onCallGenkit({
55
+ // Uncomment to enable AppCheck. This can reduce costs by ensuring only your Verified
56
+ // app users can use your API. Read more at https://firebase.google.com/docs/app-check/cloud-functions
57
+ // enforceAppCheck: true,
58
+
59
+ // authPolicy can be any callback that accepts an AuthData (a uid and tokens dictionary) and the
60
+ // request data. The isSignedIn() and hasClaim() helpers can be used to simplify. The following
61
+ // will require the user to have the email_verified claim, for example.
62
+ // authPolicy: hasClaim("email_verified"),
63
+
64
+ // Grant access to the API key to this function:
65
+ secrets: [apiKey],
66
+ }, menuSuggestionFlow);
@@ -1,36 +1,20 @@
1
1
  # # Example mutations for a simple movie app
2
2
 
3
3
  # # Create a movie based on user input
4
- # mutation CreateMovie(
5
- # $title: String!
6
- # $genre: String!
7
- # $imageUrl: String!
8
- # ) @auth(level: USER_EMAIL_VERIFIED) {
9
- # movie_insert(
10
- # data: {
11
- # title: $title
12
- # genre: $genre
13
- # imageUrl: $imageUrl
14
- # }
15
- # )
4
+ # mutation CreateMovie($title: String!, $genre: String!, $imageUrl: String!)
5
+ # @auth(level: USER_EMAIL_VERIFIED) {
6
+ # movie_insert(data: { title: $title, genre: $genre, imageUrl: $imageUrl })
16
7
  # }
17
8
 
18
9
  # # Upsert (update or insert) a user's username based on their auth.uid
19
10
  # mutation UpsertUser($username: String!) @auth(level: USER) {
20
- # user_upsert(
21
- # data: {
22
- # id_expr: "auth.uid"
23
- # username: $username
24
- # }
25
- # )
11
+ # # The "auth.uid" server value ensures that users can only register their own user.
12
+ # user_upsert(data: { id_expr: "auth.uid", username: $username })
26
13
  # }
27
14
 
28
15
  # # Add a review for a movie
29
- # mutation AddReview(
30
- # $movieId: UUID!
31
- # $rating: Int!
32
- # $reviewText: String!
33
- # ) @auth(level: USER) {
16
+ # mutation AddReview($movieId: UUID!, $rating: Int!, $reviewText: String!)
17
+ # @auth(level: USER) {
34
18
  # review_upsert(
35
19
  # data: {
36
20
  # userId_expr: "auth.uid"
@@ -43,8 +27,7 @@
43
27
  # }
44
28
 
45
29
  # # Logged in user can delete their review for a movie
46
- # mutation DeleteReview(
47
- # $movieId: UUID!
48
- # ) @auth(level: USER) {
30
+ # mutation DeleteReview($movieId: UUID!) @auth(level: USER) {
31
+ # # The "auth.uid" server value ensures that users can only delete their own reviews.
49
32
  # review_delete(key: { userId_expr: "auth.uid", movieId: $movieId })
50
33
  # }
@@ -13,19 +13,21 @@
13
13
 
14
14
  # # List all users, only admins should be able to list all users, so we use NO_ACCESS
15
15
  # query ListUsers @auth(level: NO_ACCESS) {
16
- # users { id, username }
16
+ # users {
17
+ # id
18
+ # username
19
+ # }
17
20
  # }
18
21
 
19
- # # Logged in user can list all their reviews and movie titles associated with the review
20
- # # Since the query requires the uid of the current authenticated user, the auth level is set to USER
22
+ # # Logged in users can list all their reviews and movie titles associated with the review
23
+ # # Since the query uses the uid of the current authenticated user, we set auth level to USER
21
24
  # query ListUserReviews @auth(level: USER) {
22
- # user(key: {id_expr: "auth.uid"}) {
25
+ # user(key: { id_expr: "auth.uid" }) {
23
26
  # id
24
27
  # username
25
28
  # # <field>_on_<foreign_key_field> makes it easy to grab info from another table
26
29
  # # Here, we use it to grab all the reviews written by the user.
27
30
  # reviews: reviews_on_user {
28
- # id
29
31
  # rating
30
32
  # reviewDate
31
33
  # reviewText
@@ -50,7 +52,6 @@
50
52
  # description
51
53
  # }
52
54
  # reviews: reviews_on_movie {
53
- # id
54
55
  # reviewText
55
56
  # reviewDate
56
57
  # rating
@@ -63,16 +64,10 @@
63
64
  # }
64
65
 
65
66
  # # Search for movies, actors, and reviews
66
- # query SearchMovie(
67
- # $titleInput: String
68
- # $genre: String
69
- # ) @auth(level: PUBLIC) {
67
+ # query SearchMovie($titleInput: String, $genre: String) @auth(level: PUBLIC) {
70
68
  # movies(
71
69
  # where: {
72
- # _and: [
73
- # { genre: { eq: $genre } }
74
- # { title: { contains: $titleInput } }
75
- # ]
70
+ # _and: [{ genre: { eq: $genre } }, { title: { contains: $titleInput } }]
76
71
  # }
77
72
  # ) {
78
73
  # id