firebase-tools 13.8.0 → 13.8.1

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.
@@ -1,88 +1,67 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.webApps = void 0;
4
- const fuzzy = require("fuzzy");
5
- const inquirer = require("inquirer");
6
4
  const apps_1 = require("../management/apps");
7
- const prompt_1 = require("../prompt");
8
5
  const error_1 = require("../error");
6
+ const utils_1 = require("../utils");
9
7
  const CREATE_NEW_FIREBASE_WEB_APP = "CREATE_NEW_WEB_APP";
10
8
  const CONTINUE_WITHOUT_SELECTING_FIREBASE_WEB_APP = "CONTINUE_WITHOUT_SELECTING_FIREBASE_WEB_APP";
11
9
  exports.webApps = {
12
10
  CREATE_NEW_FIREBASE_WEB_APP,
13
11
  CONTINUE_WITHOUT_SELECTING_FIREBASE_WEB_APP,
14
12
  getOrCreateWebApp,
15
- promptFirebaseWebApp,
13
+ generateWebAppName,
16
14
  };
17
15
  async function getOrCreateWebApp(projectId, firebaseWebAppName, backendId) {
18
16
  const webAppsInProject = await (0, apps_1.listFirebaseApps)(projectId, apps_1.AppPlatform.WEB);
19
- if (webAppsInProject.length === 0) {
20
- const { displayName, appId } = await createFirebaseWebApp(projectId, {
21
- displayName: backendId,
22
- });
23
- return { name: displayName, id: appId };
24
- }
25
- const existingUserProjectWebApps = new Map(webAppsInProject.map((obj) => {
26
- var _a;
27
- return [
28
- (_a = obj.displayName) !== null && _a !== void 0 ? _a : obj.appId,
29
- obj.appId,
30
- ];
31
- }));
17
+ const existingUserProjectWebApps = firebaseAppsToMap(webAppsInProject);
32
18
  if (firebaseWebAppName) {
33
19
  if (existingUserProjectWebApps.get(firebaseWebAppName) === undefined) {
34
20
  throw new error_1.FirebaseError(`The web app '${firebaseWebAppName}' does not exist in project ${projectId}`);
35
21
  }
36
- return { name: firebaseWebAppName, id: existingUserProjectWebApps.get(firebaseWebAppName) };
37
- }
38
- return await exports.webApps.promptFirebaseWebApp(projectId, backendId, existingUserProjectWebApps);
39
- }
40
- async function promptFirebaseWebApp(projectId, backendId, existingUserProjectWebApps) {
41
- const existingWebAppKeys = Array.from(existingUserProjectWebApps.keys());
42
- const firebaseWebAppName = await (0, prompt_1.promptOnce)({
43
- type: "autocomplete",
44
- name: "app",
45
- message: "Which of the following Firebase web apps would you like to associate your backend with?",
46
- source: (_, input = "") => {
47
- return new Promise((resolve) => resolve([
48
- new inquirer.Separator(),
49
- {
50
- name: "Create a new Firebase web app.",
51
- value: CREATE_NEW_FIREBASE_WEB_APP,
52
- },
53
- {
54
- name: "Continue without a Firebase web app.",
55
- value: CONTINUE_WITHOUT_SELECTING_FIREBASE_WEB_APP,
56
- },
57
- new inquirer.Separator(),
58
- ...fuzzy.filter(input, existingWebAppKeys).map((result) => {
59
- return result.original;
60
- }),
61
- ]));
62
- },
63
- });
64
- if (firebaseWebAppName === CREATE_NEW_FIREBASE_WEB_APP) {
65
- const newFirebaseWebApp = await createFirebaseWebApp(projectId, { displayName: backendId });
66
- return { name: newFirebaseWebApp.displayName, id: newFirebaseWebApp.appId };
67
- }
68
- else if (firebaseWebAppName === CONTINUE_WITHOUT_SELECTING_FIREBASE_WEB_APP) {
69
- return;
22
+ return {
23
+ name: firebaseWebAppName,
24
+ id: existingUserProjectWebApps.get(firebaseWebAppName),
25
+ };
70
26
  }
71
- return { name: firebaseWebAppName, id: existingUserProjectWebApps.get(firebaseWebAppName) };
72
- }
73
- async function createFirebaseWebApp(projectId, options) {
27
+ const webAppName = await generateWebAppName(projectId, backendId);
74
28
  try {
75
- return await (0, apps_1.createWebApp)(projectId, options);
29
+ const app = await (0, apps_1.createWebApp)(projectId, { displayName: webAppName });
30
+ return { name: app.displayName, id: app.appId };
76
31
  }
77
32
  catch (e) {
78
33
  if (isQuotaError(e)) {
79
- throw new error_1.FirebaseError("Unable to create a new web app, the project has reached the quota for Firebase apps. Navigate to your Firebase console to manage or delete a Firebase app to continue. ", { original: e instanceof Error ? e : undefined });
34
+ (0, utils_1.logWarning)("Unable to create a new web app, the project has reached the quota for Firebase apps. Navigate to your Firebase console to manage or delete a Firebase app to continue. ");
35
+ return;
80
36
  }
81
37
  throw new error_1.FirebaseError("Unable to create a Firebase web app", {
82
38
  original: e instanceof Error ? e : undefined,
83
39
  });
84
40
  }
85
41
  }
42
+ async function generateWebAppName(projectId, backendId) {
43
+ const webAppsInProject = await (0, apps_1.listFirebaseApps)(projectId, apps_1.AppPlatform.WEB);
44
+ const appsMap = firebaseAppsToMap(webAppsInProject);
45
+ if (!appsMap.get(backendId)) {
46
+ return backendId;
47
+ }
48
+ let uniqueId = 1;
49
+ let webAppName = `${backendId}_${uniqueId}`;
50
+ while (appsMap.get(webAppName)) {
51
+ uniqueId += 1;
52
+ webAppName = `${backendId}_${uniqueId}`;
53
+ }
54
+ return webAppName;
55
+ }
56
+ function firebaseAppsToMap(apps) {
57
+ return new Map(apps.map((obj) => {
58
+ var _a;
59
+ return [
60
+ (_a = obj.displayName) !== null && _a !== void 0 ? _a : obj.appId,
61
+ obj.appId,
62
+ ];
63
+ }));
64
+ }
86
65
  function isQuotaError(error) {
87
66
  var _a, _b, _c, _d, _e;
88
67
  const original = error.original;
@@ -1,7 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.getBackendForAmbiguousLocation = exports.promptLocation = exports.deleteBackendAndPoll = exports.orchestrateRollout = exports.setDefaultTrafficPolicy = exports.createBackend = exports.ensureAppHostingComputeServiceAccount = exports.doSetup = void 0;
4
- const repo = require("./repo");
5
4
  const poller = require("../operation-poller");
6
5
  const apphosting = require("../gcp/apphosting");
7
6
  const githubConnections = require("./githubConnections");
@@ -17,6 +16,7 @@ const ensureApiEnabled_1 = require("../ensureApiEnabled");
17
16
  const deploymentTool = require("../deploymentTool");
18
17
  const app_1 = require("./app");
19
18
  const ora = require("ora");
19
+ const node_fetch_1 = require("node-fetch");
20
20
  const DEFAULT_COMPUTE_SERVICE_ACCOUNT_NAME = "firebase-app-hosting-compute";
21
21
  const apphostingPollerOptions = {
22
22
  apiOrigin: (0, api_1.apphostingOrigin)(),
@@ -24,7 +24,30 @@ const apphostingPollerOptions = {
24
24
  masterTimeout: 25 * 60 * 1000,
25
25
  maxBackoff: 10000,
26
26
  };
27
- async function doSetup(projectId, webAppName, location, serviceAccount, withCloudBuildRepos) {
27
+ async function tlsReady(url) {
28
+ var _a;
29
+ try {
30
+ await (0, node_fetch_1.default)(url);
31
+ return true;
32
+ }
33
+ catch (err) {
34
+ const maybeNodeError = err;
35
+ if (/HANDSHAKE_FAILURE/.test((_a = maybeNodeError === null || maybeNodeError === void 0 ? void 0 : maybeNodeError.cause) === null || _a === void 0 ? void 0 : _a.code)) {
36
+ return false;
37
+ }
38
+ return true;
39
+ }
40
+ }
41
+ async function awaitTlsReady(url) {
42
+ let ready;
43
+ do {
44
+ ready = await tlsReady(url);
45
+ if (!ready) {
46
+ await (0, utils_1.sleep)(1000);
47
+ }
48
+ } while (!ready);
49
+ }
50
+ async function doSetup(projectId, webAppName, location, serviceAccount) {
28
51
  await Promise.all([
29
52
  (0, ensureApiEnabled_1.ensure)(projectId, (0, api_1.developerConnectOrigin)(), "apphosting", true),
30
53
  (0, ensureApiEnabled_1.ensure)(projectId, (0, api_1.cloudbuildOrigin)(), "apphosting", true),
@@ -52,14 +75,12 @@ async function doSetup(projectId, webAppName, location, serviceAccount, withClou
52
75
  });
53
76
  const webApp = await app_1.webApps.getOrCreateWebApp(projectId, webAppName, backendId);
54
77
  if (webApp) {
55
- (0, utils_1.logSuccess)(`Firebase web app set to ${webApp.name}.\n`);
78
+ (0, utils_1.logSuccess)(`Created a new Firebase web app named "${webApp.name}"`);
56
79
  }
57
80
  else {
58
81
  (0, utils_1.logWarning)(`Firebase web app not set`);
59
82
  }
60
- const gitRepositoryConnection = withCloudBuildRepos
61
- ? await repo.linkGitHubRepository(projectId, location)
62
- : await githubConnections.linkGitHubRepository(projectId, location);
83
+ const gitRepositoryConnection = await githubConnections.linkGitHubRepository(projectId, location);
63
84
  const rootDir = await (0, prompt_1.promptOnce)({
64
85
  name: "rootDir",
65
86
  type: "input",
@@ -86,8 +107,9 @@ async function doSetup(projectId, webAppName, location, serviceAccount, withClou
86
107
  (0, utils_1.logSuccess)(`Your backend will be deployed at:\n\thttps://${backend.uri}`);
87
108
  return;
88
109
  }
110
+ const url = `https://${backend.uri}`;
89
111
  (0, utils_1.logBullet)(`You may also track this rollout at:\n\t${(0, api_1.consoleOrigin)()}/project/${projectId}/apphosting`);
90
- const createRolloutSpinner = ora("Starting a new rollout... This make take a few minutes. It's safe to exit now.").start();
112
+ const createRolloutSpinner = ora("Starting a new rollout; this may take a few minutes. It's safe to exit now.").start();
91
113
  await orchestrateRollout(projectId, location, backendId, {
92
114
  source: {
93
115
  codebase: {
@@ -95,7 +117,13 @@ async function doSetup(projectId, webAppName, location, serviceAccount, withClou
95
117
  },
96
118
  },
97
119
  });
98
- createRolloutSpinner.succeed(`Your backend is now deployed at:\n\thttps://${backend.uri}`);
120
+ createRolloutSpinner.succeed("Rollout complete");
121
+ if (!(await tlsReady(url))) {
122
+ const tlsSpinner = ora("Finalizing your backend's TLS certificate; this may take a few minutes.").start();
123
+ await awaitTlsReady(url);
124
+ tlsSpinner.succeed("TLS certificate ready");
125
+ }
126
+ (0, utils_1.logSuccess)(`Your backend is now deployed at:\n\thttps://${backend.uri}`);
99
127
  }
100
128
  exports.doSetup = doSetup;
101
129
  async function ensureAppHostingComputeServiceAccount(projectId, serviceAccount) {
@@ -205,7 +233,7 @@ async function orchestrateRollout(projectId, location, backendId, buildInput) {
205
233
  if (tries >= 5) {
206
234
  throw err;
207
235
  }
208
- await new Promise((resolve) => setTimeout(resolve, 1000));
236
+ await (0, utils_1.sleep)(1000);
209
237
  }
210
238
  else {
211
239
  throw err;
@@ -6,19 +6,20 @@ const projectUtils_1 = require("../projectUtils");
6
6
  const requireInteractive_1 = require("../requireInteractive");
7
7
  const apphosting_1 = require("../apphosting");
8
8
  const apphosting_2 = require("../gcp/apphosting");
9
+ const firedata_1 = require("../gcp/firedata");
10
+ const requireTosAcceptance_1 = require("../requireTosAcceptance");
9
11
  exports.command = new command_1.Command("apphosting:backends:create")
10
12
  .description("create a Firebase App Hosting backend")
11
13
  .option("-a, --app <webApp>", "specify an existing Firebase web app to associate your App Hosting backend with")
12
14
  .option("-l, --location <location>", "specify the location of the backend", "")
13
15
  .option("-s, --service-account <serviceAccount>", "specify the service account used to run the server", "")
14
- .option("-w, --with-cloud-build-repos", "use Cloud Build Repositories flow instead of the Developer Connect flow")
15
16
  .before(apphosting_2.ensureApiEnabled)
16
17
  .before(requireInteractive_1.default)
18
+ .before((0, requireTosAcceptance_1.requireTosAcceptance)(firedata_1.APPHOSTING_TOS_ID))
17
19
  .action(async (options) => {
18
20
  const projectId = (0, projectUtils_1.needProjectId)(options);
19
21
  const webApp = options.app;
20
22
  const location = options.location;
21
23
  const serviceAccount = options.serviceAccount;
22
- const withCloudBuildRepos = options.withCloudBuildRepos;
23
- await (0, apphosting_1.doSetup)(projectId, webApp, location, serviceAccount, withCloudBuildRepos);
24
+ await (0, apphosting_1.doSetup)(projectId, webApp, location, serviceAccount);
24
25
  });
@@ -25,11 +25,12 @@ exports.command = new command_1.Command("dataconnect:sdk:generate")
25
25
  configDir,
26
26
  auto_download: true,
27
27
  rc: options.rc,
28
+ locationId: service.location,
28
29
  };
29
30
  const dataconnectEmulator = new dataconnectEmulator_1.DataConnectEmulator(args);
30
31
  for (const conn of serviceInfo.connectorInfo) {
31
32
  const output = await dataconnectEmulator.generate(conn.connectorYaml.connectorId);
32
- console.log(output);
33
+ logger_1.logger.info(output);
33
34
  logger_1.logger.info(`Generated SDKs for ${conn.connectorYaml.connectorId}`);
34
35
  }
35
36
  }
@@ -30,7 +30,12 @@ exports.command = new command_1.Command("dataconnect:sql:migrate [serviceId]")
30
30
  if (!instanceId) {
31
31
  throw new error_1.FirebaseError("dataconnect.yaml is missing field schema.datasource.postgresql.cloudsql.instanceId");
32
32
  }
33
- const diffs = await (0, schemaMigration_1.migrateSchema)(options, serviceInfo.schema, true);
33
+ const diffs = await (0, schemaMigration_1.migrateSchema)({
34
+ options,
35
+ schema: serviceInfo.schema,
36
+ allowNonInteractiveMigration: true,
37
+ validateOnly: true,
38
+ });
34
39
  if (diffs.length) {
35
40
  logger_1.logger.info(`Schema sucessfully migrated! Run 'firebase deploy' to deploy your new schema to your Data Connect service.`);
36
41
  }
@@ -1,8 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.checkInstanceConfig = exports.installRequiredExtensions = exports.REQUIRED_EXTENSIONS_COMMANDS = exports.provisionCloudSql = void 0;
3
+ exports.checkInstanceConfig = exports.provisionCloudSql = void 0;
4
4
  const cloudSqlAdminClient = require("../gcp/cloudsql/cloudsqladmin");
5
- const connect_1 = require("../gcp/cloudsql/connect");
6
5
  const utils = require("../utils");
7
6
  const checkIam_1 = require("./checkIam");
8
7
  const GOOGLE_ML_INTEGRATION_ROLE = "roles/aiplatform.user";
@@ -55,22 +54,6 @@ async function provisionCloudSql(args) {
55
54
  return connectionName;
56
55
  }
57
56
  exports.provisionCloudSql = provisionCloudSql;
58
- exports.REQUIRED_EXTENSIONS_COMMANDS = [
59
- `CREATE SCHEMA IF NOT EXISTS "public"`,
60
- `CREATE EXTENSION IF NOT EXISTS "uuid-ossp" with SCHEMA public`,
61
- `CREATE EXTENSION IF NOT EXISTS "vector" with SCHEMA public`,
62
- `CREATE EXTENSION IF NOT EXISTS "google_ml_integration" with SCHEMA public CASCADE`,
63
- ];
64
- async function installRequiredExtensions(projectId, instanceId, databaseId, username) {
65
- await (0, connect_1.execute)(exports.REQUIRED_EXTENSIONS_COMMANDS, {
66
- projectId,
67
- instanceId,
68
- databaseId,
69
- username,
70
- silent: true,
71
- });
72
- }
73
- exports.installRequiredExtensions = installRequiredExtensions;
74
57
  function checkInstanceConfig(instance, requireGoogleMlIntegration) {
75
58
  var _a, _b, _c, _d;
76
59
  const settings = instance.settings;
@@ -3,12 +3,12 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.migrateSchema = exports.diffSchema = void 0;
4
4
  const clc = require("colorette");
5
5
  const sql_formatter_1 = require("sql-formatter");
6
+ const types_1 = require("./types");
6
7
  const client_1 = require("./client");
7
8
  const connect_1 = require("../gcp/cloudsql/connect");
8
9
  const prompt_1 = require("../prompt");
9
10
  const logger_1 = require("../logger");
10
11
  const error_1 = require("../error");
11
- const provisionCloudSql_1 = require("./provisionCloudSql");
12
12
  const projectUtils_1 = require("../projectUtils");
13
13
  const utils_1 = require("../utils");
14
14
  const IMCOMPATIBLE_SCHEMA_ERROR_TYPESTRING = "type.googleapis.com/google.firebase.dataconnect.v1main.IncompatibleSqlSchemaError";
@@ -20,6 +20,8 @@ async function diffSchema(schema) {
20
20
  throw new error_1.FirebaseError(`tried to diff schema but ${instanceName} was undefined`);
21
21
  }
22
22
  try {
23
+ const serviceName = schema.name.replace(`/schemas/${types_1.SCHEMA_ID}`, "");
24
+ await ensureServiceIsConnectedToCloudSql(serviceName);
23
25
  await (0, client_1.upsertSchema)(schema, true);
24
26
  }
25
27
  catch (err) {
@@ -34,9 +36,9 @@ async function diffSchema(schema) {
34
36
  return [];
35
37
  }
36
38
  exports.diffSchema = diffSchema;
37
- async function migrateSchema(options, schema, allowNonInteractiveMigration) {
39
+ async function migrateSchema(args) {
38
40
  var _a, _b;
39
- const projectId = (0, projectUtils_1.needProjectId)(options);
41
+ const { schema, validateOnly } = args;
40
42
  const databaseId = (_a = schema.primaryDatasource.postgresql) === null || _a === void 0 ? void 0 : _a.database;
41
43
  if (!databaseId) {
42
44
  throw new error_1.FirebaseError("Schema is missing primaryDatasource.postgresql?.database, cannot migrate");
@@ -45,46 +47,57 @@ async function migrateSchema(options, schema, allowNonInteractiveMigration) {
45
47
  if (!instanceId) {
46
48
  throw new error_1.FirebaseError(`tried to migrate schema but ${instanceId} was undefined`);
47
49
  }
48
- const iamUser = await (0, connect_1.setupIAMUser)(instanceId, databaseId, options);
49
50
  try {
50
- await (0, client_1.upsertSchema)(schema, true);
51
+ const serviceName = schema.name.replace(`/schemas/${types_1.SCHEMA_ID}`, "");
52
+ await ensureServiceIsConnectedToCloudSql(serviceName);
53
+ await (0, client_1.upsertSchema)(schema, validateOnly);
54
+ logger_1.logger.debug(`Database schema was up to date for ${instanceId}:${databaseId}`);
55
+ return [];
51
56
  }
52
57
  catch (err) {
53
58
  const incompatible = getIncompatibleSchemaError(err);
54
- if (incompatible) {
55
- const choice = await promptForSchemaMigration(options, databaseId, incompatible, allowNonInteractiveMigration);
56
- const commandsToExecute = incompatible.diffs
57
- .filter((d) => {
58
- switch (choice) {
59
- case "all":
60
- return true;
61
- case "safe":
62
- return !d.destructive;
63
- case "none":
64
- return false;
65
- }
66
- })
67
- .map((d) => d.sql);
68
- if (commandsToExecute.length) {
69
- await (0, connect_1.execute)([
70
- ...provisionCloudSql_1.REQUIRED_EXTENSIONS_COMMANDS,
71
- ...commandsToExecute,
72
- `GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA "public" TO PUBLIC`,
73
- ], {
74
- projectId,
75
- instanceId,
76
- databaseId,
77
- username: iamUser,
78
- });
79
- return incompatible.diffs;
80
- }
59
+ if (!incompatible) {
60
+ throw err;
81
61
  }
82
- throw err;
62
+ const diffs = await handleIncompatibleSchemaError(Object.assign(Object.assign({}, args), { incompatibleSchemaError: incompatible, instanceId,
63
+ databaseId }));
64
+ await (0, client_1.upsertSchema)(schema, validateOnly);
65
+ return diffs;
83
66
  }
84
- logger_1.logger.debug(`Schema was up to date for ${instanceId}:${databaseId}`);
85
- return [];
86
67
  }
87
68
  exports.migrateSchema = migrateSchema;
69
+ async function handleIncompatibleSchemaError(args) {
70
+ const { incompatibleSchemaError, options, instanceId, databaseId, allowNonInteractiveMigration } = args;
71
+ const projectId = (0, projectUtils_1.needProjectId)(options);
72
+ const iamUser = await (0, connect_1.setupIAMUser)(instanceId, databaseId, options);
73
+ const choice = await promptForSchemaMigration(options, databaseId, incompatibleSchemaError, allowNonInteractiveMigration);
74
+ const commandsToExecute = incompatibleSchemaError.diffs
75
+ .filter((d) => {
76
+ switch (choice) {
77
+ case "all":
78
+ return true;
79
+ case "safe":
80
+ return !d.destructive;
81
+ case "none":
82
+ return false;
83
+ }
84
+ })
85
+ .map((d) => d.sql);
86
+ if (commandsToExecute.length) {
87
+ await (0, connect_1.execute)([
88
+ `SET ROLE "${(0, connect_1.firebaseowner)(databaseId)}"`,
89
+ ...commandsToExecute,
90
+ `GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA "public" TO PUBLIC`,
91
+ ], {
92
+ projectId,
93
+ instanceId,
94
+ databaseId,
95
+ username: iamUser,
96
+ });
97
+ return incompatibleSchemaError.diffs;
98
+ }
99
+ return [];
100
+ }
88
101
  async function promptForSchemaMigration(options, databaseName, err, allowNonInteractiveMigration) {
89
102
  displaySchemaChanges(err);
90
103
  if (!options.nonInteractive) {
@@ -119,6 +132,24 @@ async function promptForSchemaMigration(options, databaseName, err, allowNonInte
119
132
  return "none";
120
133
  }
121
134
  }
135
+ async function ensureServiceIsConnectedToCloudSql(serviceName) {
136
+ let currentSchema;
137
+ try {
138
+ currentSchema = await (0, client_1.getSchema)(serviceName);
139
+ }
140
+ catch (err) {
141
+ if (err.status === 404) {
142
+ return;
143
+ }
144
+ throw err;
145
+ }
146
+ if (!currentSchema.primaryDatasource.postgresql ||
147
+ currentSchema.primaryDatasource.postgresql.schemaValidation === "STRICT") {
148
+ return;
149
+ }
150
+ currentSchema.primaryDatasource.postgresql.schemaValidation = "STRICT";
151
+ await (0, client_1.upsertSchema)(currentSchema, false);
152
+ }
122
153
  function displaySchemaChanges(error) {
123
154
  const message = "Your new schema is incompatible with the schema of your CloudSQL database. " +
124
155
  "The following SQL statements will migrate your database schema to match your new Data Connect schema.\n" +
@@ -130,7 +161,10 @@ function toString(diff) {
130
161
  }
131
162
  function getIncompatibleSchemaError(err) {
132
163
  var _a;
133
- const original = (_a = err.context) === null || _a === void 0 ? void 0 : _a.body.error;
164
+ const original = ((_a = err.context) === null || _a === void 0 ? void 0 : _a.body.error) || err.orignal;
165
+ if (!original) {
166
+ throw err;
167
+ }
134
168
  const details = original.details;
135
169
  const incompatibles = details.filter((d) => d["@type"] === IMCOMPATIBLE_SCHEMA_ERROR_TYPESTRING);
136
170
  return incompatibles[0];
@@ -3,7 +3,6 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const utils = require("../../utils");
4
4
  const client_1 = require("../../dataconnect/client");
5
5
  const prompts_1 = require("../../dataconnect/prompts");
6
- const error_1 = require("../../error");
7
6
  const schemaMigration_1 = require("../../dataconnect/schemaMigration");
8
7
  async function default_1(context, options) {
9
8
  const serviceInfos = context.dataconnect.serviceInfos;
@@ -17,15 +16,14 @@ async function default_1(context, options) {
17
16
  })
18
17
  .map((s) => s.schema);
19
18
  if (wantSchemas.length) {
20
- utils.logLabeledBullet("dataconnect", "Checking if database schemas match Data Connect schemas...");
19
+ utils.logLabeledBullet("dataconnect", "Releasing Data Connect schemas...");
21
20
  for (const s of wantSchemas) {
22
- await (0, schemaMigration_1.migrateSchema)(options, s, false);
23
- }
24
- utils.logLabeledBullet("dataconnect", "Releasing schemas...");
25
- const schemaPromises = await Promise.allSettled(wantSchemas.map((s) => (0, client_1.upsertSchema)(s)));
26
- const failedSchemas = schemaPromises.filter((p) => p.status === "rejected");
27
- if (failedSchemas.length) {
28
- throw new error_1.FirebaseError(`Errors while updating your schemas:\n ${failedSchemas.map((f) => f.reason).join("\n")}`);
21
+ await (0, schemaMigration_1.migrateSchema)({
22
+ options,
23
+ schema: s,
24
+ allowNonInteractiveMigration: false,
25
+ validateOnly: false,
26
+ });
29
27
  }
30
28
  utils.logLabeledBullet("dataconnect", "Schemas released.");
31
29
  }
@@ -16,6 +16,7 @@ const deploymentTool = require("../../../deploymentTool");
16
16
  const gcf = require("../../../gcp/cloudfunctions");
17
17
  const gcfV2 = require("../../../gcp/cloudfunctionsv2");
18
18
  const eventarc = require("../../../gcp/eventarc");
19
+ const experiments = require("../../../experiments");
19
20
  const helper = require("../functionsDeployHelper");
20
21
  const poller = require("../../../operation-poller");
21
22
  const pubsub = require("../../../gcp/pubsub");
@@ -276,7 +277,9 @@ class Fabricator {
276
277
  while (!resultFunction) {
277
278
  resultFunction = await this.functionExecutor
278
279
  .run(async () => {
279
- apiFunction.buildConfig.sourceToken = await scraper.getToken();
280
+ if (experiments.isEnabled("functionsv2deployoptimizations")) {
281
+ apiFunction.buildConfig.sourceToken = await scraper.getToken();
282
+ }
280
283
  const op = await gcfV2.createFunction(apiFunction);
281
284
  return await poller.pollOperation(Object.assign(Object.assign({}, gcfV2PollerOptions), { pollerName: `create-${endpoint.codebase}-${endpoint.region}-${endpoint.id}`, operationResourceName: op.name, onPoll: scraper.poller }));
282
285
  })
@@ -384,7 +387,9 @@ class Fabricator {
384
387
  }
385
388
  const resultFunction = await this.functionExecutor
386
389
  .run(async () => {
387
- apiFunction.buildConfig.sourceToken = await scraper.getToken();
390
+ if (experiments.isEnabled("functionsv2deployoptimizations")) {
391
+ apiFunction.buildConfig.sourceToken = await scraper.getToken();
392
+ }
388
393
  const op = await gcfV2.updateFunction(apiFunction);
389
394
  return await poller.pollOperation(Object.assign(Object.assign({}, gcfV2PollerOptions), { pollerName: `update-${endpoint.codebase}-${endpoint.region}-${endpoint.id}`, operationResourceName: op.name, onPoll: scraper.poller }));
390
395
  }, { retryCodes: [...executor_1.DEFAULT_RETRY_CODES, CLOUD_RUN_RESOURCE_EXHAUSTED_CODE] })
@@ -537,6 +537,7 @@ async function startAll(options, showUI = true, runningTestScript = false) {
537
537
  projectId,
538
538
  auto_download: true,
539
539
  configDir,
540
+ locationId: config[0].location,
540
541
  rc: options.rc,
541
542
  });
542
543
  await startEmulator(dataConnectEmulator);
@@ -26,7 +26,7 @@ class DataConnectEmulator {
26
26
  this.logger.logLabeled("WARN", "Data Connect", "Operations that use vector_embed will make calls to production Vertex AI");
27
27
  }
28
28
  }
29
- return (0, downloadableEmulators_1.start)(types_1.Emulators.DATACONNECT, Object.assign(Object.assign({}, this.args), { http_port: port, grpc_port: port + 1, config_dir: this.args.configDir, local_connection_string: this.getLocalConectionString(), project_id: this.args.projectId }));
29
+ return (0, downloadableEmulators_1.start)(types_1.Emulators.DATACONNECT, Object.assign(Object.assign({}, this.args), { http_port: port, grpc_port: port + 1, config_dir: this.args.configDir, local_connection_string: this.getLocalConectionString(), project_id: this.args.projectId, service_location: this.args.locationId }));
30
30
  }
31
31
  connect() {
32
32
  return Promise.resolve();
@@ -42,6 +42,7 @@ class DataConnectEmulator {
42
42
  host,
43
43
  port,
44
44
  pid: (0, downloadableEmulators_1.getPID)(types_1.Emulators.DATACONNECT),
45
+ timeout: 10000,
45
46
  };
46
47
  }
47
48
  getName() {
@@ -51,6 +52,7 @@ class DataConnectEmulator {
51
52
  const commandInfo = await (0, downloadableEmulators_1.downloadIfNecessary)(types_1.Emulators.DATACONNECT);
52
53
  const cmd = [
53
54
  "generate",
55
+ `--service_location=${this.args.locationId}`,
54
56
  `--config_dir=${this.args.configDir}`,
55
57
  `--connector_id=${connectorId}`,
56
58
  ];
@@ -46,14 +46,14 @@ const EMULATOR_UPDATE_DETAILS = {
46
46
  },
47
47
  dataconnect: process.platform === "darwin"
48
48
  ? {
49
- version: "1.1.12",
50
- expectedSize: 25542352,
51
- expectedChecksum: "b523fe9d5506cc07480d1a571c72de9a",
49
+ version: "1.1.15",
50
+ expectedSize: 25600896,
51
+ expectedChecksum: "36dcf9be7273b9ba6052faf0b3c0347f",
52
52
  }
53
53
  : {
54
- version: "1.1.12",
55
- expectedSize: 23004624,
56
- expectedChecksum: "e69f79755263b7cd77ec7cd15ef0d825",
54
+ version: "1.1.15",
55
+ expectedSize: 23036688,
56
+ expectedChecksum: "e42203947bf984993f295976ee3ba2be",
57
57
  },
58
58
  };
59
59
  exports.DownloadDetails = {
@@ -131,9 +131,8 @@ async function checkListenable(arg1, port) {
131
131
  });
132
132
  }
133
133
  exports.checkListenable = checkListenable;
134
- async function waitForPortUsed(port, host) {
134
+ async function waitForPortUsed(port, host, timeout = 60000) {
135
135
  const interval = 200;
136
- const timeout = 5000;
137
136
  try {
138
137
  await tcpport.waitUntilUsedOnHost(port, host, interval, timeout);
139
138
  }
@@ -19,7 +19,7 @@ class EmulatorRegistry {
19
19
  await instance.start();
20
20
  if (instance.getName() !== types_1.Emulators.EXTENSIONS) {
21
21
  const info = instance.getInfo();
22
- await portUtils.waitForPortUsed(info.port, (0, utils_1.connectableHostname)(info.host));
22
+ await portUtils.waitForPortUsed(info.port, (0, utils_1.connectableHostname)(info.host), info.timeout);
23
23
  }
24
24
  }
25
25
  static async stop(name) {
@@ -20,11 +20,13 @@ exports.ALL_EXPERIMENTS = experiments({
20
20
  rtdbmanagement: {
21
21
  shortDescription: "Use new endpoint to administer realtime database instances",
22
22
  },
23
- pythonfunctions: {
24
- shortDescription: "Python support for Cloud Functions for Firebase",
25
- fullDescription: "Adds the ability to initializea and deploy Cloud " +
26
- "Functions for Firebase in Python. While this feature is experimental " +
27
- "breaking API changes are allowed in MINOR API revisions",
23
+ functionsv2deployoptimizations: {
24
+ shortDescription: "Optimize deployments of v2 firebase functions",
25
+ fullDescription: "Reuse build images across funtions to increase performance and reliaibility " +
26
+ "of deploys. This has been made an experiment due to backend bugs that are " +
27
+ "temporarily causing failures in some regions with this optimization enabled",
28
+ public: true,
29
+ default: false,
28
30
  },
29
31
  deletegcfartifacts: {
30
32
  shortDescription: `Add the ${(0, colorette_1.bold)("functions:deletegcfartifacts")} command to purge docker build images`,
@@ -400,7 +400,9 @@ async function ɵcodegenFunctionsDirectory(sourceDir, destDir, target, context)
400
400
  (0, utils_2.getProductionDistDirFiles)(sourceDir, distDir),
401
401
  (0, fs_extra_1.mkdirp)((0, path_1.join)(destDir, distDir)),
402
402
  ]);
403
- await Promise.all(productionDistDirfiles.map((file) => (0, fs_extra_1.copy)(file, file.replace(sourceDir, destDir), { recursive: true })));
403
+ await Promise.all(productionDistDirfiles.map((file) => (0, fs_extra_1.copy)((0, path_1.join)(sourceDir, distDir, file), (0, path_1.join)(destDir, distDir, file), {
404
+ recursive: true,
405
+ })));
404
406
  return { packageJson, frameworksEntry: "next.js", dotEnv };
405
407
  }
406
408
  exports.ɵcodegenFunctionsDirectory = ɵcodegenFunctionsDirectory;
@@ -230,7 +230,7 @@ async function getProductionDistDirFiles(sourceDir, distDir) {
230
230
  ignore: [(0, path_1.join)("cache", "webpack", "*-development", "**"), (0, path_1.join)("cache", "eslint", "**")],
231
231
  cwd: (0, path_1.join)(sourceDir, distDir),
232
232
  nodir: true,
233
- absolute: true,
233
+ absolute: false,
234
234
  realpath: utils_2.IS_WINDOWS,
235
235
  }, (err, matches) => {
236
236
  if (err)
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.setupIAMUser = exports.execute = void 0;
3
+ exports.firebaseowner = exports.setupIAMUser = exports.execute = void 0;
4
4
  const pg = require("pg");
5
5
  const cloud_sql_connector_1 = require("@google-cloud/cloud-sql-connector");
6
6
  const requireAuth_1 = require("../../requireAuth");
@@ -116,6 +116,7 @@ exports.setupIAMUser = setupIAMUser;
116
116
  function firebaseowner(databaseId) {
117
117
  return `firebaseowner_${databaseId}_public`;
118
118
  }
119
+ exports.firebaseowner = firebaseowner;
119
120
  function toDatabaseUser(account) {
120
121
  let mode = "CLOUD_IAM_USER";
121
122
  let user = account;
@@ -0,0 +1,26 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.isProductTosAccepted = exports.getAcceptanceStatus = exports.getTosStatus = exports.APP_CHECK_TOS_ID = exports.APPHOSTING_TOS_ID = void 0;
4
+ const apiv2_1 = require("../apiv2");
5
+ const api_1 = require("../api");
6
+ const error_1 = require("../error");
7
+ const client = new apiv2_1.Client({ urlPrefix: (0, api_1.firedataOrigin)(), auth: true, apiVersion: "v1" });
8
+ exports.APPHOSTING_TOS_ID = "APP_HOSTING_TOS";
9
+ exports.APP_CHECK_TOS_ID = "APP_CHECK";
10
+ async function getTosStatus() {
11
+ const res = await client.get("accessmanagement/tos:getStatus");
12
+ return res.body;
13
+ }
14
+ exports.getTosStatus = getTosStatus;
15
+ function getAcceptanceStatus(response, tosId) {
16
+ const perServiceStatus = response.perServiceStatus.find((tosStatus) => tosStatus.tosId === tosId);
17
+ if (perServiceStatus === undefined) {
18
+ throw new error_1.FirebaseError(`Missing terms of service status for product: ${tosId}`);
19
+ }
20
+ return perServiceStatus.serviceStatus.status;
21
+ }
22
+ exports.getAcceptanceStatus = getAcceptanceStatus;
23
+ function isProductTosAccepted(response, tosId) {
24
+ return getAcceptanceStatus(response, tosId) === "ACCEPTED";
25
+ }
26
+ exports.isProductTosAccepted = isProductTosAccepted;
@@ -43,6 +43,7 @@ async function interactiveCreateHostingSite(siteId, appId, options) {
43
43
  if (options.nonInteractive) {
44
44
  throw err;
45
45
  }
46
+ id = "";
46
47
  suggestion = getSuggestionFromError(err);
47
48
  }
48
49
  }
@@ -71,5 +72,8 @@ function getSuggestionFromError(err) {
71
72
  return match[1];
72
73
  }
73
74
  }
75
+ else {
76
+ (0, utils_1.logWarning)(err.message);
77
+ }
74
78
  return;
75
79
  }
@@ -5,6 +5,7 @@ const path_1 = require("path");
5
5
  const prompt_1 = require("../../../prompt");
6
6
  const fs_1 = require("fs");
7
7
  const provisionCloudSql_1 = require("../../../dataconnect/provisionCloudSql");
8
+ const cloudsql = require("../../../gcp/cloudsql/cloudsqladmin");
8
9
  const ensureApis_1 = require("../../../dataconnect/ensureApis");
9
10
  const client_1 = require("../../../dataconnect/client");
10
11
  const TEMPLATE_ROOT = (0, path_1.resolve)(__dirname, "../../../../templates/init/dataconnect/");
@@ -49,26 +50,60 @@ async function doSetup(setup, config) {
49
50
  type: "input",
50
51
  default: "my-connector",
51
52
  });
52
- const dir = config.get("dataconnect.source") ||
53
- (await (0, prompt_1.promptOnce)({
54
- message: "What directory should be used for DataConnect config and schema?",
55
- type: "input",
56
- default: "dataconnect",
57
- }));
53
+ const dir = config.get("dataconnect.source") || "dataconnect";
58
54
  if (!config.has("dataconnect")) {
59
55
  config.set("dataconnect.source", dir);
60
56
  config.set("dataconnect.location", locationId);
61
57
  }
62
- const cloudSqlInstanceId = await (0, prompt_1.promptOnce)({
63
- message: `What CloudSQL instance would you like to use? Please enter the ID of an existing instance in ${locationId}`,
64
- type: "input",
65
- default: `dataconnect-test`,
66
- });
67
- const cloudSqlDatabase = await (0, prompt_1.promptOnce)({
68
- message: `Which database would you like to use from ${cloudSqlInstanceId}?`,
69
- type: "input",
70
- default: `dataconnect-test`,
71
- });
58
+ let cloudSqlInstanceId = "";
59
+ let newInstance = false;
60
+ if (setup.projectId) {
61
+ const instances = await cloudsql.listInstances(setup.projectId);
62
+ const instancesInLocation = instances.filter((i) => i.region === locationId);
63
+ const choices = instancesInLocation.map((i) => {
64
+ return { name: i.name, value: i.name };
65
+ });
66
+ choices.push({ name: "Create a new instance", value: "" });
67
+ if (instancesInLocation.length) {
68
+ cloudSqlInstanceId = await (0, prompt_1.promptOnce)({
69
+ message: `Which CloudSSQL in ${locationId} would you like to use?`,
70
+ type: "list",
71
+ choices,
72
+ });
73
+ }
74
+ }
75
+ if (cloudSqlInstanceId === "") {
76
+ newInstance = true;
77
+ cloudSqlInstanceId = await (0, prompt_1.promptOnce)({
78
+ message: `What ID would you like to use for your new CloudSQL instance?`,
79
+ type: "input",
80
+ default: `dataconnect-test`,
81
+ });
82
+ }
83
+ let cloudSqlDatabase = "";
84
+ let newDB = false;
85
+ if (!newInstance && setup.projectId) {
86
+ const dbs = await cloudsql.listDatabases(setup.projectId, cloudSqlInstanceId);
87
+ const choices = dbs.map((d) => {
88
+ return { name: d.name, value: d.name };
89
+ });
90
+ choices.push({ name: "Create a new database", value: "" });
91
+ if (dbs.length) {
92
+ cloudSqlDatabase = await (0, prompt_1.promptOnce)({
93
+ message: `Which database in ${cloudSqlInstanceId} would you like to use?`,
94
+ type: "list",
95
+ choices,
96
+ });
97
+ }
98
+ }
99
+ if (cloudSqlDatabase === "") {
100
+ newDB = true;
101
+ cloudSqlDatabase = await (0, prompt_1.promptOnce)({
102
+ message: `What ID would you like to use for your new database in ${cloudSqlInstanceId}?`,
103
+ type: "input",
104
+ default: `dataconnect`,
105
+ });
106
+ }
72
107
  const defaultConnectionString = (_c = (_b = (_a = setup.rcfile.dataconnectEmulatorConfig) === null || _a === void 0 ? void 0 : _a.postgres) === null || _b === void 0 ? void 0 : _b.localConnectionString) !== null && _c !== void 0 ? _c : "postgresql://localhost:5432?sslmode=disable";
73
108
  const localConnectionString = await (0, prompt_1.promptOnce)({
74
109
  type: "input",
@@ -95,6 +130,7 @@ async function doSetup(setup, config) {
95
130
  await config.askWriteProjectFile((0, path_1.join)(dir, "connector", "queries.gql"), QUERIES_TEMPLATE);
96
131
  await config.askWriteProjectFile((0, path_1.join)(dir, "connector", "mutations.gql"), MUTATIONS_TEMPLATE);
97
132
  if (setup.projectId &&
133
+ (newInstance || newDB) &&
98
134
  (await (0, prompt_1.confirm)({
99
135
  message: "Would you like to provision your CloudSQL instance and database now? This will take a few minutes.",
100
136
  default: true,
@@ -21,7 +21,7 @@ class OperationPoller {
21
21
  if (error) {
22
22
  throw error instanceof error_1.FirebaseError
23
23
  ? error
24
- : new error_1.FirebaseError(error.message, { status: error.code });
24
+ : new error_1.FirebaseError(error.message, { status: error.code, original: error });
25
25
  }
26
26
  return response;
27
27
  }
@@ -0,0 +1,21 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.requireTosAcceptance = void 0;
4
+ const error_1 = require("./error");
5
+ const firedata_1 = require("./gcp/firedata");
6
+ const api_1 = require("./api");
7
+ const consoleLandingPage = new Map([
8
+ [firedata_1.APPHOSTING_TOS_ID, `${(0, api_1.consoleOrigin)()}/project/_/apphosting`],
9
+ ]);
10
+ function requireTosAcceptance(tosId) {
11
+ return () => requireTos(tosId);
12
+ }
13
+ exports.requireTosAcceptance = requireTosAcceptance;
14
+ async function requireTos(tosId) {
15
+ const res = await (0, firedata_1.getTosStatus)();
16
+ if ((0, firedata_1.isProductTosAccepted)(res, tosId)) {
17
+ return;
18
+ }
19
+ const console = consoleLandingPage.get(tosId) || (0, api_1.consoleOrigin)();
20
+ throw new error_1.FirebaseError(`Your account has not accepted the required Terms of Service for this action. Please accept the Terms of Service and try again. ${console}`);
21
+ }
package/lib/utils.js CHANGED
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.openInBrowser = exports.connectableHostname = exports.randomInt = exports.debounce = exports.last = exports.cloneDeep = exports.groupBy = exports.assertIsStringOrUndefined = exports.assertIsNumber = exports.assertIsString = exports.thirtyDaysFromNow = exports.isRunningInWSL = exports.isVSCodeExtension = exports.isCloudEnvironment = exports.datetimeString = exports.createDestroyer = exports.promiseWithSpinner = exports.setupLoggers = exports.tryParse = exports.tryStringify = exports.promiseProps = exports.withTimeout = exports.promiseWhile = exports.promiseAllSettled = exports.getFunctionsEventProvider = exports.endpoint = exports.makeActiveProject = exports.streamToString = exports.stringToStream = exports.explainStdin = exports.allSettled = exports.reject = exports.logLabeledError = exports.logLabeledWarning = exports.logWarning = exports.logLabeledBullet = exports.logBullet = exports.logLabeledSuccess = exports.logSuccess = exports.addSubdomain = exports.addDatabaseNamespace = exports.getDatabaseViewDataUrl = exports.getDatabaseUrl = exports.envOverride = exports.setVSCodeEnvVars = exports.getInheritedOption = exports.consoleUrl = exports.vscodeEnvVars = exports.envOverrides = exports.IS_WINDOWS = void 0;
4
- exports.readSecretValue = exports.generateId = exports.wrappedSafeLoad = exports.readFileFromDirectory = exports.getHostnameFromUrl = exports.openInBrowserPopup = void 0;
3
+ exports.connectableHostname = exports.randomInt = exports.debounce = exports.last = exports.cloneDeep = exports.groupBy = exports.assertIsStringOrUndefined = exports.assertIsNumber = exports.assertIsString = exports.thirtyDaysFromNow = exports.isRunningInWSL = exports.isVSCodeExtension = exports.isCloudEnvironment = exports.datetimeString = exports.createDestroyer = exports.sleep = exports.promiseWithSpinner = exports.setupLoggers = exports.tryParse = exports.tryStringify = exports.promiseProps = exports.withTimeout = exports.promiseWhile = exports.promiseAllSettled = exports.getFunctionsEventProvider = exports.endpoint = exports.makeActiveProject = exports.streamToString = exports.stringToStream = exports.explainStdin = exports.allSettled = exports.reject = exports.logLabeledError = exports.logLabeledWarning = exports.logWarning = exports.logLabeledBullet = exports.logBullet = exports.logLabeledSuccess = exports.logSuccess = exports.addSubdomain = exports.addDatabaseNamespace = exports.getDatabaseViewDataUrl = exports.getDatabaseUrl = exports.envOverride = exports.setVSCodeEnvVars = exports.getInheritedOption = exports.consoleUrl = exports.vscodeEnvVars = exports.envOverrides = exports.IS_WINDOWS = void 0;
4
+ exports.readSecretValue = exports.generateId = exports.wrappedSafeLoad = exports.readFileFromDirectory = exports.getHostnameFromUrl = exports.openInBrowserPopup = exports.openInBrowser = void 0;
5
5
  const fs = require("fs-extra");
6
6
  const tty = require("tty");
7
7
  const path = require("node:path");
@@ -342,6 +342,10 @@ async function promiseWithSpinner(action, message) {
342
342
  return data;
343
343
  }
344
344
  exports.promiseWithSpinner = promiseWithSpinner;
345
+ function sleep(ms) {
346
+ return new Promise((resolve) => setTimeout(resolve, ms));
347
+ }
348
+ exports.sleep = sleep;
345
349
  function createDestroyer(server) {
346
350
  const connections = new Set();
347
351
  server.on("connection", (conn) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "firebase-tools",
3
- "version": "13.8.0",
3
+ "version": "13.8.1",
4
4
  "description": "Command-Line Interface for Firebase",
5
5
  "main": "./lib/index.js",
6
6
  "bin": {
@@ -1,5 +1,4 @@
1
1
  # # Example mutations
2
- # # TODO: Replace with a really good illustrative example from devrel!
3
2
  # mutation createOrder($name: String!) {
4
3
  # order_insert(data : {name: $name})
5
4
  # }
@@ -1,5 +1,4 @@
1
1
  # # Example query
2
- # # TODO: Replace with a really good illustrative example from devrel!
3
2
  # query listOrders {
4
3
  # orders {
5
4
  # name
@@ -1,5 +1,4 @@
1
1
  # # Example schema
2
- # # TODO: Replace with a really good illustrative example from devrel!
3
2
  # type Product @table {
4
3
  # name: String!
5
4
  # price: Int!