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.
- package/lib/dataconnect/provisionCloudSql.js +7 -0
- package/lib/deploy/functions/prepare.js +27 -1
- package/lib/emulator/dataconnect/pgliteServer.js +7 -0
- package/lib/emulator/downloadableEmulators.js +9 -9
- package/lib/extensions/provisioningHelper.js +1 -1
- package/lib/frameworks/index.js +1 -1
- package/lib/frameworks/utils.js +3 -1
- package/lib/gcp/cloudfunctionsv2.js +3 -0
- package/lib/init/features/dataconnect/index.js +3 -2
- package/lib/init/features/genkit/index.js +8 -2
- package/package.json +2 -2
- package/schema/dataconnect-yaml.json +5 -0
- package/templates/genkit/firebase.1.0.0.template +66 -0
- package/templates/init/dataconnect/mutations.gql +9 -26
- package/templates/init/dataconnect/queries.gql +9 -14
- package/templates/init/dataconnect/schema.gql +28 -21
|
@@ -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.
|
|
52
|
-
expectedSize:
|
|
53
|
-
expectedChecksum: "
|
|
51
|
+
version: "1.7.7",
|
|
52
|
+
expectedSize: 25359104,
|
|
53
|
+
expectedChecksum: "c5481addc04e14d10538add7aabda183",
|
|
54
54
|
}
|
|
55
55
|
: process.platform === "win32"
|
|
56
56
|
? {
|
|
57
|
-
version: "1.7.
|
|
58
|
-
expectedSize:
|
|
59
|
-
expectedChecksum: "
|
|
57
|
+
version: "1.7.7",
|
|
58
|
+
expectedSize: 25788416,
|
|
59
|
+
expectedChecksum: "9f7e5b9bcbca47de509fbc26cc1e0fa8",
|
|
60
60
|
}
|
|
61
61
|
: {
|
|
62
|
-
version: "1.7.
|
|
63
|
-
expectedSize:
|
|
64
|
-
expectedChecksum: "
|
|
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
|
}
|
package/lib/frameworks/index.js
CHANGED
|
@@ -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,
|
package/lib/frameworks/utils.js
CHANGED
|
@@ -123,7 +123,9 @@ function simpleProxy(hostOrRequestHandler) {
|
|
|
123
123
|
});
|
|
124
124
|
}
|
|
125
125
|
else {
|
|
126
|
-
const proxiedRes = proxyResponse(originalReq, originalRes,
|
|
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 =
|
|
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 =
|
|
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,
|
|
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.
|
|
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.
|
|
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
|
-
#
|
|
6
|
-
# $genre:
|
|
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
|
-
#
|
|
21
|
-
#
|
|
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
|
-
#
|
|
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
|
-
#
|
|
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 {
|
|
16
|
+
# users {
|
|
17
|
+
# id
|
|
18
|
+
# username
|
|
19
|
+
# }
|
|
17
20
|
# }
|
|
18
21
|
|
|
19
|
-
# # Logged in
|
|
20
|
-
# # Since the query
|
|
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
|
-
# #
|
|
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
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
# #
|
|
13
|
-
# #
|
|
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
|
-
# #
|
|
14
|
+
# # Movie is keyed by a randomly generated UUID.
|
|
17
15
|
# type Movie @table {
|
|
18
|
-
# #
|
|
19
|
-
# #
|
|
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
|
|
27
|
-
# # Movie
|
|
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
|
|
30
|
-
# movie: Movie! @unique
|
|
31
|
-
# #
|
|
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")
|