firebase-tools 13.29.2 → 13.30.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.
@@ -33,6 +33,7 @@ async function provisionCloudSql(args) {
33
33
  if (err.status !== 404) {
34
34
  throw err;
35
35
  }
36
+ cmekWarning();
36
37
  const cta = dryRun ? "It will be created on your next deploy" : "Creating it now.";
37
38
  silent ||
38
39
  utils.logLabeledBullet("dataconnect", `CloudSQL instance '${instanceId}' not found.` +
@@ -112,3 +113,9 @@ function getUpdateReason(instance, requireGoogleMlIntegration) {
112
113
  return reason;
113
114
  }
114
115
  exports.getUpdateReason = getUpdateReason;
116
+ function cmekWarning() {
117
+ const message = "The no-cost Cloud SQL trial instance does not support customer managed encryption keys.\n" +
118
+ "If you'd like to use a CMEK to encrypt your data, first create a CMEK encrypted instance (https://cloud.google.com/sql/docs/postgres/configure-cmek#createcmekinstance).\n" +
119
+ "Then, edit your `dataconnect.yaml` file to use the encrypted instance and redeploy.";
120
+ utils.logLabeledWarning("dataconnect", message);
121
+ }
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.loadCodebases = exports.resolveCpuAndConcurrency = exports.inferBlockingDetails = exports.updateEndpointTargetedStatus = exports.inferDetailsFromExisting = exports.prepare = exports.EVENTARC_SOURCE_ENV = void 0;
3
+ exports.warnIfNewGenkitFunctionIsMissingSecrets = exports.loadCodebases = exports.resolveCpuAndConcurrency = exports.inferBlockingDetails = exports.updateEndpointTargetedStatus = exports.inferDetailsFromExisting = exports.prepare = exports.EVENTARC_SOURCE_ENV = void 0;
4
4
  const clc = require("colorette");
5
5
  const backend = require("./backend");
6
6
  const build = require("./build");
@@ -28,6 +28,7 @@ const applyHash_1 = require("./cache/applyHash");
28
28
  const backend_1 = require("./backend");
29
29
  const functional_1 = require("../../functional");
30
30
  const prepare_1 = require("../extensions/prepare");
31
+ const prompt = require("../../prompt");
31
32
  exports.EVENTARC_SOURCE_ENV = "EVENTARC_CLOUD_EVENT_SOURCE";
32
33
  async function prepare(context, options, payload) {
33
34
  var _a, _b;
@@ -170,6 +171,7 @@ async function prepare(context, options, payload) {
170
171
  }
171
172
  const wantBackend = backend.merge(...Object.values(wantBackends));
172
173
  const haveBackend = backend.merge(...Object.values(haveBackends));
174
+ await warnIfNewGenkitFunctionIsMissingSecrets(wantBackend, haveBackend, options);
173
175
  await Promise.all(Object.values(wantBackend.requiredAPIs).map(({ api }) => {
174
176
  return ensureApiEnabled.ensure(projectId, api, "functions", false);
175
177
  }));
@@ -324,3 +326,27 @@ async function loadCodebases(config, options, firebaseConfig, runtimeConfig, fil
324
326
  return wantBuilds;
325
327
  }
326
328
  exports.loadCodebases = loadCodebases;
329
+ async function warnIfNewGenkitFunctionIsMissingSecrets(have, want, options) {
330
+ if (options.force) {
331
+ return;
332
+ }
333
+ const newAndMissingSecrets = backend.allEndpoints(backend.matchingBackend(want, (e) => {
334
+ var _a;
335
+ if (!backend.isCallableTriggered(e) || !e.callableTrigger.genkitAction) {
336
+ return false;
337
+ }
338
+ if ((_a = e.secretEnvironmentVariables) === null || _a === void 0 ? void 0 : _a.length) {
339
+ return false;
340
+ }
341
+ return !backend.hasEndpoint(have)(e);
342
+ }));
343
+ if (newAndMissingSecrets.length) {
344
+ const message = `The function(s) ${newAndMissingSecrets.map((e) => e.id).join(", ")} use Genkit but do not have access to a secret. ` +
345
+ "This may cause the function to fail if it depends on an API key. To learn more about granting a function access to " +
346
+ "secrets, see https://firebase.google.com/docs/functions/config-env?gen=2nd#secret_parameters. Continue?";
347
+ if (!(await prompt.confirm({ message, nonInteractive: options.nonInteractive }))) {
348
+ throw new error_1.FirebaseError("Aborted");
349
+ }
350
+ }
351
+ }
352
+ exports.warnIfNewGenkitFunctionIsMissingSecrets = warnIfNewGenkitFunctionIsMissingSecrets;
@@ -28,6 +28,7 @@ const index_1 = require("./pg-gateway/index");
28
28
  const node_1 = require("./pg-gateway/platforms/node");
29
29
  const logger_1 = require("../../logger");
30
30
  const error_1 = require("../../error");
31
+ const node_string_decoder_1 = require("node:string_decoder");
31
32
  exports.TRUNCATE_TABLES_SQL = `
32
33
  DO $do$
33
34
  DECLARE _clear text;
@@ -52,6 +53,9 @@ class PostgresServer {
52
53
  return;
53
54
  }
54
55
  const db = await getDb();
56
+ if (data[0] === index_1.FrontendMessageCode.Terminate) {
57
+ await db.query("DEALLOCATE ALL");
58
+ }
55
59
  const result = await db.execProtocolRaw(data);
56
60
  return extendedQueryPatch.filterResponse(data, result);
57
61
  },
@@ -151,6 +155,9 @@ class PGliteExtendedQueryPatch {
151
155
  index_1.FrontendMessageCode.Bind,
152
156
  index_1.FrontendMessageCode.Close,
153
157
  ];
158
+ const decoder = new node_string_decoder_1.StringDecoder();
159
+ const decoded = decoder.write(message);
160
+ logger_1.logger.debug(decoded);
154
161
  if (pipelineStartMessages.includes(message[0])) {
155
162
  this.isExtendedQuery = true;
156
163
  }
@@ -48,20 +48,20 @@ const EMULATOR_UPDATE_DETAILS = {
48
48
  },
49
49
  dataconnect: process.platform === "darwin"
50
50
  ? {
51
- version: "1.7.6",
52
- expectedSize: 25322240,
53
- expectedChecksum: "2dda7394330fd1aba37605466941eef0",
51
+ version: "1.7.7",
52
+ expectedSize: 25359104,
53
+ expectedChecksum: "c5481addc04e14d10538add7aabda183",
54
54
  }
55
55
  : process.platform === "win32"
56
56
  ? {
57
- version: "1.7.6",
58
- expectedSize: 25752064,
59
- expectedChecksum: "283c11e28a0072b596531b79462a8e94",
57
+ version: "1.7.7",
58
+ expectedSize: 25788416,
59
+ expectedChecksum: "9f7e5b9bcbca47de509fbc26cc1e0fa8",
60
60
  }
61
61
  : {
62
- version: "1.7.6",
63
- expectedSize: 25235608,
64
- expectedChecksum: "f66e24b3726df57cd1f1685a64a87904",
62
+ version: "1.7.7",
63
+ expectedSize: 25268376,
64
+ expectedChecksum: "fb239ecf5dcbf87b762d12a3e9dee012",
65
65
  },
66
66
  };
67
67
  exports.DownloadDetails = {
@@ -97,7 +97,7 @@ async function isStorageProvisioned(projectId) {
97
97
  return !!((_b = (_a = resp.body) === null || _a === void 0 ? void 0 : _a.buckets) === null || _b === void 0 ? void 0 : _b.find((bucket) => {
98
98
  const bucketResourceName = bucket.name;
99
99
  const bucketResourceNameTokens = bucketResourceName.split("/");
100
- const pattern = "^" + projectId + "(.[[a-z0-9]+)*.appspot.com$";
100
+ const pattern = "^" + projectId + "(.[[a-z0-9]+)*.(appspot.com|firebasestorage.app)$";
101
101
  return new RegExp(pattern).test(bucketResourceNameTokens[bucketResourceNameTokens.length - 1]);
102
102
  }));
103
103
  }
@@ -366,7 +366,7 @@ async function prepareFrameworks(purpose, targetNames, context, options, emulato
366
366
  await (0, promises_1.writeFile)((0, path_1.join)(functionsDist, ".env"), `${dotEnvContents}
367
367
  __FIREBASE_FRAMEWORKS_ENTRY__=${frameworksEntry}
368
368
  ${firebaseDefaults ? `__FIREBASE_DEFAULTS__=${JSON.stringify(firebaseDefaults)}\n` : ""}`.trimStart());
369
- const envs = await (0, glob_1.glob)(getProjectPath(".env.*"));
369
+ const envs = await (0, glob_1.glob)(getProjectPath(".env.*"), { windowsPathsNoEscape: utils_2.IS_WINDOWS });
370
370
  await Promise.all(envs.map((path) => (0, promises_1.copyFile)(path, (0, path_1.join)(functionsDist, (0, path_1.basename)(path)))));
371
371
  (0, child_process_1.execSync)(`npm i --omit dev --no-audit`, {
372
372
  cwd: functionsDist,
@@ -123,7 +123,9 @@ function simpleProxy(hostOrRequestHandler) {
123
123
  });
124
124
  }
125
125
  else {
126
- const proxiedRes = proxyResponse(originalReq, originalRes, next);
126
+ const proxiedRes = proxyResponse(originalReq, originalRes, () => {
127
+ void hostOrRequestHandler(originalReq, originalRes, next);
128
+ });
127
129
  await hostOrRequestHandler(originalReq, proxiedRes, next);
128
130
  }
129
131
  };
@@ -259,6 +259,9 @@ function functionFromEndpoint(endpoint) {
259
259
  }
260
260
  else if (backend.isCallableTriggered(endpoint)) {
261
261
  gcfFunction.labels = Object.assign(Object.assign({}, gcfFunction.labels), { "deployment-callable": "true" });
262
+ if (endpoint.callableTrigger.genkitAction) {
263
+ gcfFunction.labels["genkit-action"] = "true";
264
+ }
262
265
  }
263
266
  else if (backend.isBlockingTriggered(endpoint)) {
264
267
  gcfFunction.labels = Object.assign(Object.assign({}, gcfFunction.labels), { [constants_1.BLOCKING_LABEL]: constants_1.BLOCKING_EVENT_TO_LABEL_KEY[endpoint.blockingTrigger.eventType] });
@@ -102,7 +102,8 @@ async function askQuestions(setup, isBillingEnabled) {
102
102
  else {
103
103
  const defaultServiceId = toDNSCompatibleId((0, path_1.basename)(process.cwd()));
104
104
  info.serviceId = info.serviceId || defaultServiceId;
105
- info.cloudSqlInstanceId = info.cloudSqlInstanceId || `${info.serviceId || "app"}-fdc`;
105
+ info.cloudSqlInstanceId =
106
+ info.cloudSqlInstanceId || `${info.serviceId.toLowerCase() || "app"}-fdc`;
106
107
  info.locationId = info.locationId || `us-central1`;
107
108
  info.cloudSqlDatabase = info.cloudSqlDatabase || `fdcdb`;
108
109
  }
@@ -270,7 +271,7 @@ async function promptForCloudSQL(setup, info) {
270
271
  info.cloudSqlInstanceId = await (0, prompt_1.promptOnce)({
271
272
  message: `What ID would you like to use for your new CloudSQL instance?`,
272
273
  type: "input",
273
- default: `${info.serviceId || "app"}-fdc`,
274
+ default: `${info.serviceId.toLowerCase() || "app"}-fdc`,
274
275
  });
275
276
  }
276
277
  if (info.locationId === "") {
@@ -14,9 +14,11 @@ const ensureApiEnabled_1 = require("../../../ensureApiEnabled");
14
14
  const logger_1 = require("../../../logger");
15
15
  const error_1 = require("../../../error");
16
16
  const utils_1 = require("../../../utils");
17
+ const UNKNOWN_VERSION_TOO_HIGH = "2.0.0";
18
+ const LATEST_TEMPLATE = "1.0.0";
17
19
  async function getGenkitVersion() {
18
20
  let genkitVersion;
19
- let templateVersion = "0.9.0";
21
+ let templateVersion = LATEST_TEMPLATE;
20
22
  let useInit = false;
21
23
  let stopInstall = false;
22
24
  if (process.env.GENKIT_DEV_VERSION && typeof process.env.GENKIT_DEV_VERSION === "string") {
@@ -39,7 +41,7 @@ async function getGenkitVersion() {
39
41
  if (!genkitVersion) {
40
42
  throw new error_1.FirebaseError("Unable to determine genkit version to install");
41
43
  }
42
- if (semver.gte(genkitVersion, "1.0.0")) {
44
+ if (semver.gte(genkitVersion, UNKNOWN_VERSION_TOO_HIGH)) {
43
45
  const continueInstall = await (0, prompt_1.confirm)({
44
46
  message: clc.yellow(`WARNING: The latest version of Genkit (${genkitVersion}) isn't supported by this\n` +
45
47
  "version of firebase-tools. You can proceed, but the provided sample code may\n" +
@@ -51,7 +53,11 @@ async function getGenkitVersion() {
51
53
  stopInstall = true;
52
54
  }
53
55
  }
56
+ else if (semver.gte(genkitVersion, "1.0.0-rc.1")) {
57
+ templateVersion = "1.0.0";
58
+ }
54
59
  else if (semver.gte(genkitVersion, "0.6.0")) {
60
+ templateVersion = "0.9.0";
55
61
  }
56
62
  else {
57
63
  templateVersion = "";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "firebase-tools",
3
- "version": "13.29.2",
3
+ "version": "13.30.0",
4
4
  "description": "Command-Line Interface for Firebase",
5
5
  "main": "./lib/index.js",
6
6
  "bin": {
@@ -60,7 +60,7 @@
60
60
  ]
61
61
  },
62
62
  "dependencies": {
63
- "@electric-sql/pglite": "^0.2.0",
63
+ "@electric-sql/pglite": "^0.2.16",
64
64
  "@google-cloud/cloud-sql-connector": "^1.3.3",
65
65
  "@google-cloud/pubsub": "^4.5.0",
66
66
  "abort-controller": "^3.0.0",
@@ -17,6 +17,11 @@
17
17
  "instanceId": {
18
18
  "type": "string",
19
19
  "description": "The ID of the CloudSQL instance for this database"
20
+ },
21
+ "schemaValidation": {
22
+ "type": "string",
23
+ "enum": ["COMPATIBLE", "STRICT"],
24
+ "description": "Schema validation mode for schema migrations"
20
25
  }
21
26
  }
22
27
  }
@@ -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(),
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
@@ -1,44 +1,51 @@
1
1
  # # Example schema for simple movie review app
2
2
 
3
- # # Users
4
- # # Suppose a user can leave reviews for movies
5
- # # user -> reviews is a one to many relationship,
6
- # # movie -> reviews is a one to many relationship
7
- # # movie <-> user is a many to many relationship
3
+ # # User table is keyed by Firebase Auth UID.
8
4
  # type User @table {
9
- # id: String! @col(name: "user_auth")
10
- # username: String! @col(name: "username", dataType: "varchar(50)")
11
- # # The following are generated by the user: User! field in the Review table
12
- # # reviews_on_user
13
- # # movies_via_Review
5
+ # # `@default(expr: "auth.uid")` sets it to Firebase Auth UID during insert and upsert.
6
+ # id: String! @default(expr: "auth.uid")
7
+ # username: String! @col(dataType: "varchar(50)")
8
+ # # The `user: User!` field in the Review table generates the following one-to-many query field.
9
+ # # reviews_on_user: [Review!]!
10
+ # # The `Review` join table the following many-to-many query field.
11
+ # # movies_via_Review: [Movie!]!
14
12
  # }
15
13
 
16
- # # Movies
14
+ # # Movie is keyed by a randomly generated UUID.
17
15
  # type Movie @table {
18
- # # The below parameter values are generated by default with @table, and can be edited manually.
19
- # # implies directive `@col(name: "movie_id")`, generating a column name
20
- # id: UUID! @default(expr: "uuidV4()")
16
+ # # If you do not pass a 'key' to `@table`, Data Connect automatically adds the following 'id' column.
17
+ # # Feel free to uncomment and customize it.
18
+ # # id: UUID! @default(expr: "uuidV4()")
21
19
  # title: String!
22
20
  # imageUrl: String!
23
21
  # genre: String
24
22
  # }
25
23
 
26
- # # Movie Metadata
27
- # # Movie - MovieMetadata is a one-to-one relationship
24
+ # # MovieMetadata is a metadata attached to a Movie.
25
+ # # Movie <-> MovieMetadata is a one-to-one relationship
28
26
  # type MovieMetadata @table {
29
- # # @unique indicates a 1-1 relationship
30
- # movie: Movie! @unique
31
- # # movieId: UUID <- this is created by the above reference
27
+ # # @unique ensures each Movie can only one MovieMetadata.
28
+ # movie: Movie! @unique
29
+ # # The movie field adds the following foreign key field. Feel free to uncomment and customize it.
30
+ # # movieId: UUID!
32
31
  # rating: Float
33
32
  # releaseYear: Int
34
33
  # description: String
35
34
  # }
36
35
 
37
- # # Reviews
36
+ # # Reviews is a join table between User and Movie.
37
+ # # It has a composite primary keys `userUid` and `movieId`.
38
+ # # A user can leave reviews for many movies. A movie can have reviews from many users.
39
+ # # User <-> Review is a one-to-many relationship
40
+ # # Movie <-> Review is a one-to-many relationship
41
+ # # Movie <-> User is a many-to-many relationship
38
42
  # type Review @table(name: "Reviews", key: ["movie", "user"]) {
39
- # id: UUID! @default(expr: "uuidV4()")
40
43
  # user: User!
44
+ # # The user field adds the following foreign key field. Feel free to uncomment and customize it.
45
+ # # userUid: String!
41
46
  # movie: Movie!
47
+ # # The movie field adds the following foreign key field. Feel free to uncomment and customize it.
48
+ # # movieId: UUID!
42
49
  # rating: Int
43
50
  # reviewText: String
44
51
  # reviewDate: Date! @default(expr: "request.time")