Skip to content

Commit 513e809

Browse files
authored
Support webhooks in Data Connect behind an experiment flag. (#9440)
* Support webhooks in Data Connect behind an experiment flag. * Update existing logic to specifically refer to the main schema. * Fix build errors. * lint * Support loading schema sources from either `schema` or `schemas` field. * Fix firebase init * Properly deploy secondary schemas as well. * Make `getSchema` take a schema ID, move secondary schema upsert after main schema upsert, and add some TODOs. * Fix listSchemas in `firebase init`. * Fix unit tests. * Actually fix unit tests. * Set default parameter for getSchema schemaId. * Specify just top-level fields in listSchema call. * Fix VSCode unit tests. * Try to fix import path in VSCode.
1 parent d68aa33 commit 513e809

File tree

25 files changed

+253
-91
lines changed

25 files changed

+253
-91
lines changed

firebase-vscode/src/data-connect/config.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { isPathInside } from "./file-utils";
22
import { DeepReadOnly } from "../metaprogramming";
3-
import { ConnectorYaml, DataConnectYaml } from "../dataconnect/types";
3+
import { ConnectorYaml, DataConnectYaml, mainSchemaYaml } from "../../../src/dataconnect/types";
44
import { Result, ResultValue } from "../result";
55
import { computed, effect, signal } from "@preact/signals-core";
66
import {
@@ -265,7 +265,7 @@ export class ResolvedDataConnectConfig {
265265
}
266266

267267
get schemaDir(): string {
268-
return this.value.schema.source;
268+
return mainSchemaYaml(this.value).source;
269269
}
270270

271271
get relativePath(): string {

src/commands/dataconnect-services-list.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { requirePermissions } from "../requirePermissions";
88
import { ensureApis } from "../dataconnect/ensureApis";
99
import * as Table from "cli-table3";
1010

11+
// TODO: Update this command to also list secondary schema information.
1112
export const command = new Command("dataconnect:services:list")
1213
.description("list all deployed Data Connect services")
1314
.before(requirePermissions, [

src/commands/dataconnect-sql-diff.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { requirePermissions } from "../requirePermissions";
66
import { pickService } from "../dataconnect/load";
77
import { diffSchema } from "../dataconnect/schemaMigration";
88
import { requireAuth } from "../requireAuth";
9+
import { mainSchema, mainSchemaYaml } from "../dataconnect/types";
910

1011
export const command = new Command("dataconnect:sql:diff [serviceId]")
1112
.description(
@@ -24,8 +25,8 @@ export const command = new Command("dataconnect:sql:diff [serviceId]")
2425

2526
const diffs = await diffSchema(
2627
options,
27-
serviceInfo.schema,
28-
serviceInfo.dataConnectYaml.schema.datasource.postgresql?.schemaValidation,
28+
mainSchema(serviceInfo.schemas),
29+
mainSchemaYaml(serviceInfo.dataConnectYaml).datasource.postgresql?.schemaValidation,
2930
);
3031
return { projectId, serviceId, diffs };
3132
});

src/commands/dataconnect-sql-grant.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { requireAuth } from "../requireAuth";
99
import { FirebaseError } from "../error";
1010
import { fdcSqlRoleMap } from "../gcp/cloudsql/permissionsSetup";
1111
import { iamUserIsCSQLAdmin } from "../gcp/cloudsql/cloudsqladmin";
12+
import { mainSchema } from "../dataconnect/types";
1213

1314
const allowedRoles = Object.keys(fdcSqlRoleMap);
1415

@@ -51,6 +52,6 @@ export const command = new Command("dataconnect:sql:grant [serviceId]")
5152
await ensureApis(projectId);
5253
const serviceInfo = await pickService(projectId, options.config, serviceId);
5354

54-
await grantRoleToUserInSchema(options, serviceInfo.schema);
55+
await grantRoleToUserInSchema(options, mainSchema(serviceInfo.schemas));
5556
return { projectId, serviceId };
5657
});

src/commands/dataconnect-sql-migrate.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { requireAuth } from "../requireAuth";
88
import { requirePermissions } from "../requirePermissions";
99
import { ensureApis } from "../dataconnect/ensureApis";
1010
import { logLabeledSuccess } from "../utils";
11+
import { mainSchema, mainSchemaYaml } from "../dataconnect/types";
1112

1213
export const command = new Command("dataconnect:sql:migrate [serviceId]")
1314
.description("migrate your CloudSQL database's schema to match your local Data Connect schema")
@@ -23,18 +24,19 @@ export const command = new Command("dataconnect:sql:migrate [serviceId]")
2324
const projectId = needProjectId(options);
2425
await ensureApis(projectId);
2526
const serviceInfo = await pickService(projectId, options.config, serviceId);
26-
const instanceId =
27-
serviceInfo.dataConnectYaml.schema.datasource.postgresql?.cloudSql.instanceId;
27+
const instanceId = mainSchemaYaml(serviceInfo.dataConnectYaml).datasource.postgresql?.cloudSql
28+
.instanceId;
2829
if (!instanceId) {
2930
throw new FirebaseError(
3031
"dataconnect.yaml is missing field schema.datasource.postgresql.cloudsql.instanceId",
3132
);
3233
}
3334
const diffs = await migrateSchema({
3435
options,
35-
schema: serviceInfo.schema,
36+
schema: mainSchema(serviceInfo.schemas),
3637
validateOnly: true,
37-
schemaValidation: serviceInfo.dataConnectYaml.schema.datasource.postgresql?.schemaValidation,
38+
schemaValidation: mainSchemaYaml(serviceInfo.dataConnectYaml).datasource.postgresql
39+
?.schemaValidation,
3840
});
3941
if (diffs.length) {
4042
logLabeledSuccess(

src/commands/dataconnect-sql-setup.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { setupSQLPermissions, getSchemaMetadata } from "../gcp/cloudsql/permissi
1010
import { DEFAULT_SCHEMA } from "../gcp/cloudsql/permissions";
1111
import { getIdentifiers, ensureServiceIsConnectedToCloudSql } from "../dataconnect/schemaMigration";
1212
import { setupIAMUsers } from "../gcp/cloudsql/connect";
13+
import { mainSchema, mainSchemaYaml } from "../dataconnect/types";
1314

1415
export const command = new Command("dataconnect:sql:setup [serviceId]")
1516
.description("set up your CloudSQL database")
@@ -24,15 +25,17 @@ export const command = new Command("dataconnect:sql:setup [serviceId]")
2425
const projectId = needProjectId(options);
2526
await ensureApis(projectId);
2627
const serviceInfo = await pickService(projectId, options.config, serviceId);
27-
const instanceId =
28-
serviceInfo.dataConnectYaml.schema.datasource.postgresql?.cloudSql.instanceId;
28+
const instanceId = mainSchemaYaml(serviceInfo.dataConnectYaml).datasource.postgresql?.cloudSql
29+
.instanceId;
2930
if (!instanceId) {
3031
throw new FirebaseError(
3132
"dataconnect.yaml is missing field schema.datasource.postgresql.cloudsql.instanceId",
3233
);
3334
}
3435

35-
const { serviceName, instanceName, databaseId } = getIdentifiers(serviceInfo.schema);
36+
const { serviceName, instanceName, databaseId } = getIdentifiers(
37+
mainSchema(serviceInfo.schemas),
38+
);
3639
await ensureServiceIsConnectedToCloudSql(
3740
serviceName,
3841
instanceName,

src/commands/dataconnect-sql-shell.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { logger } from "../logger";
1717
import { FirebaseError } from "../error";
1818
import { FBToolsAuthClient } from "../gcp/cloudsql/fbToolsAuthClient";
1919
import { confirmDangerousQuery, interactiveExecuteQuery } from "../gcp/cloudsql/interactive";
20+
import { mainSchema } from "../dataconnect/types";
2021

2122
// Not a comprehensive list, used for keyword coloring.
2223
const sqlKeywords = [
@@ -91,7 +92,7 @@ export const command = new Command("dataconnect:sql:shell [serviceId]")
9192
const projectId = needProjectId(options);
9293
await ensureApis(projectId);
9394
const serviceInfo = await pickService(projectId, options.config, serviceId);
94-
const { instanceId, databaseId } = getIdentifiers(serviceInfo.schema);
95+
const { instanceId, databaseId } = getIdentifiers(mainSchema(serviceInfo.schemas));
9596
const { user: username } = await getIAMUser(options);
9697
const instance = await cloudSqlAdminClient.getInstance(projectId, instanceId);
9798

src/dataconnect/client.spec.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,13 @@ describe("client", () => {
124124
expect(getStub).to.be.calledWith("projects/p/locations/l/services/s/schemas/main");
125125
});
126126

127+
it("getSchema with schemaId", async () => {
128+
getStub.resolves({ body: { name: "schema" } });
129+
const schema = await client.getSchema("projects/p/locations/l/services/s", "schemaId");
130+
expect(schema).to.deep.equal({ name: "schema" });
131+
expect(getStub).to.be.calledWith("projects/p/locations/l/services/s/schemas/schemaId");
132+
});
133+
127134
it("getSchema returns undefined if not found", async () => {
128135
getStub.rejects(new FirebaseError("err", { status: 404 }));
129136
const schema = await client.getSchema("projects/p/locations/l/services/s");

src/dataconnect/client.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -83,11 +83,12 @@ export async function deleteService(serviceName: string): Promise<types.Service>
8383

8484
/** Schema methods */
8585

86-
export async function getSchema(serviceName: string): Promise<types.Schema | undefined> {
86+
export async function getSchema(
87+
serviceName: string,
88+
schemaId: string = types.MAIN_SCHEMA_ID,
89+
): Promise<types.Schema | undefined> {
8790
try {
88-
const res = await dataconnectClient().get<types.Schema>(
89-
`${serviceName}/schemas/${types.SCHEMA_ID}`,
90-
);
91+
const res = await dataconnectClient().get<types.Schema>(`${serviceName}/schemas/${schemaId}`);
9192
return res.body;
9293
} catch (err: any) {
9394
if (err.status !== 404) {
@@ -146,7 +147,7 @@ export async function upsertSchema(
146147

147148
export async function deleteSchema(serviceName: string): Promise<void> {
148149
const op = await dataconnectClient().delete<types.Schema>(
149-
`${serviceName}/schemas/${types.SCHEMA_ID}`,
150+
`${serviceName}/schemas/${types.MAIN_SCHEMA_ID}`,
150151
);
151152
await operationPoller.pollOperation<void>({
152153
apiOrigin: dataconnectOrigin(),

src/dataconnect/load.ts

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { Config } from "../config";
77
import { FirebaseError } from "../error";
88
import {
99
toDatasource,
10-
SCHEMA_ID,
10+
MAIN_SCHEMA_ID,
1111
ConnectorYaml,
1212
DataConnectYaml,
1313
File,
@@ -16,6 +16,7 @@ import {
1616
} from "./types";
1717
import { readFileFromDirectory, wrappedSafeLoad } from "../utils";
1818
import { DataConnectMultiple } from "../firebaseConfig";
19+
import * as experiments from "../experiments";
1920

2021
// pickService reads firebase.json and returns all services with a given serviceId.
2122
// If serviceID is not provided and there is a single service, return that.
@@ -77,8 +78,20 @@ export async function load(
7778
const resolvedDir = config.path(sourceDirectory);
7879
const dataConnectYaml = await readDataConnectYaml(resolvedDir);
7980
const serviceName = `projects/${projectId}/locations/${dataConnectYaml.location}/services/${dataConnectYaml.serviceId}`;
80-
const schemaDir = path.join(resolvedDir, dataConnectYaml.schema.source);
81-
const schemaGQLs = await readGQLFiles(schemaDir);
81+
const schemaYamls = dataConnectYaml.schema ? [dataConnectYaml.schema] : dataConnectYaml.schemas;
82+
const schemas = await Promise.all(
83+
schemaYamls!.map(async (yaml) => {
84+
const schemaDir = path.join(resolvedDir, yaml.source);
85+
const schemaGQLs = await readGQLFiles(schemaDir);
86+
return {
87+
name: `${serviceName}/schemas/${yaml.id || MAIN_SCHEMA_ID}`,
88+
datasources: [toDatasource(projectId, dataConnectYaml.location, yaml.datasource)],
89+
source: {
90+
files: schemaGQLs,
91+
},
92+
};
93+
}),
94+
);
8295
const connectorInfo = await Promise.all(
8396
dataConnectYaml.connectorDirs.map(async (dir) => {
8497
const connectorDir = path.join(resolvedDir, dir);
@@ -100,15 +113,7 @@ export async function load(
100113
return {
101114
serviceName,
102115
sourceDirectory: resolvedDir,
103-
schema: {
104-
name: `${serviceName}/schemas/${SCHEMA_ID}`,
105-
datasources: [
106-
toDatasource(projectId, dataConnectYaml.location, dataConnectYaml.schema.datasource),
107-
],
108-
source: {
109-
files: schemaGQLs,
110-
},
111-
},
116+
schemas: schemas,
112117
dataConnectYaml,
113118
connectorInfo,
114119
};
@@ -149,6 +154,12 @@ function validateDataConnectYaml(unvalidated: any): DataConnectYaml {
149154
if (!unvalidated["location"]) {
150155
throw new FirebaseError("Missing required field 'location' in dataconnect.yaml");
151156
}
157+
if (!experiments.isEnabled("fdcwebhooks") && unvalidated["schemas"]) {
158+
throw new FirebaseError("Unsupported field 'schemas' in dataconnect.yaml");
159+
}
160+
if (!unvalidated["schema"] && !unvalidated["schemas"]) {
161+
throw new FirebaseError("Either 'schema' or 'schemas' is required in dataconnect.yaml");
162+
}
152163
return unvalidated as DataConnectYaml;
153164
}
154165

0 commit comments

Comments
 (0)