Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions firebase-vscode/src/data-connect/config.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { isPathInside } from "./file-utils";
import { DeepReadOnly } from "../metaprogramming";
import { ConnectorYaml, DataConnectYaml } from "../dataconnect/types";
import { ConnectorYaml, DataConnectYaml, mainSchemaYaml } from "../../../src/dataconnect/types";
import { Result, ResultValue } from "../result";
import { computed, effect, signal } from "@preact/signals-core";
import {
Expand Down Expand Up @@ -265,7 +265,7 @@ export class ResolvedDataConnectConfig {
}

get schemaDir(): string {
return this.value.schema.source;
return mainSchemaYaml(this.value).source;
}

get relativePath(): string {
Expand Down
1 change: 1 addition & 0 deletions src/commands/dataconnect-services-list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import { ensureApis } from "../dataconnect/ensureApis";
import * as Table from "cli-table3";

// TODO: Update this command to also list secondary schema information.
export const command = new Command("dataconnect:services:list")
.description("list all deployed Data Connect services")
.before(requirePermissions, [
Expand All @@ -19,7 +20,7 @@
const projectId = needProjectId(options);
await ensureApis(projectId);
const services = await client.listAllServices(projectId);
const table: Record<string, any>[] = new Table({

Check warning on line 23 in src/commands/dataconnect-services-list.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unexpected any. Specify a different type
head: [
"Service ID",
"Location",
Expand All @@ -30,7 +31,7 @@
],
style: { head: ["yellow"] },
});
const jsonOutput: { services: Record<string, any>[] } = { services: [] };

Check warning on line 34 in src/commands/dataconnect-services-list.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unexpected any. Specify a different type
for (const service of services) {
const schema = (await client.getSchema(service.name)) ?? {
name: "",
Expand All @@ -43,7 +44,7 @@
const instanceName = postgresDatasource?.postgresql?.cloudSql?.instance ?? "";
const instanceId = instanceName.split("/").pop();
const dbId = postgresDatasource?.postgresql?.database ?? "";
const dbName = `CloudSQL Instance: ${instanceId}\nDatabase: ${dbId}`;

Check warning on line 47 in src/commands/dataconnect-services-list.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Invalid type "string | undefined" of template literal expression
table.push([
serviceName.serviceId,
serviceName.location,
Expand Down
5 changes: 3 additions & 2 deletions src/commands/dataconnect-sql-diff.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { requirePermissions } from "../requirePermissions";
import { pickService } from "../dataconnect/load";
import { diffSchema } from "../dataconnect/schemaMigration";
import { requireAuth } from "../requireAuth";
import { mainSchema, mainSchemaYaml } from "../dataconnect/types";

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

const diffs = await diffSchema(
options,
serviceInfo.schema,
serviceInfo.dataConnectYaml.schema.datasource.postgresql?.schemaValidation,
mainSchema(serviceInfo.schemas),
mainSchemaYaml(serviceInfo.dataConnectYaml).datasource.postgresql?.schemaValidation,
);
return { projectId, serviceId, diffs };
});
3 changes: 2 additions & 1 deletion src/commands/dataconnect-sql-grant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { requireAuth } from "../requireAuth";
import { FirebaseError } from "../error";
import { fdcSqlRoleMap } from "../gcp/cloudsql/permissionsSetup";
import { iamUserIsCSQLAdmin } from "../gcp/cloudsql/cloudsqladmin";
import { mainSchema } from "../dataconnect/types";

const allowedRoles = Object.keys(fdcSqlRoleMap);

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

await grantRoleToUserInSchema(options, serviceInfo.schema);
await grantRoleToUserInSchema(options, mainSchema(serviceInfo.schemas));
return { projectId, serviceId };
});
10 changes: 6 additions & 4 deletions src/commands/dataconnect-sql-migrate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { requireAuth } from "../requireAuth";
import { requirePermissions } from "../requirePermissions";
import { ensureApis } from "../dataconnect/ensureApis";
import { logLabeledSuccess } from "../utils";
import { mainSchema, mainSchemaYaml } from "../dataconnect/types";

export const command = new Command("dataconnect:sql:migrate [serviceId]")
.description("migrate your CloudSQL database's schema to match your local Data Connect schema")
Expand All @@ -23,18 +24,19 @@ export const command = new Command("dataconnect:sql:migrate [serviceId]")
const projectId = needProjectId(options);
await ensureApis(projectId);
const serviceInfo = await pickService(projectId, options.config, serviceId);
const instanceId =
serviceInfo.dataConnectYaml.schema.datasource.postgresql?.cloudSql.instanceId;
const instanceId = mainSchemaYaml(serviceInfo.dataConnectYaml).datasource.postgresql?.cloudSql
.instanceId;
if (!instanceId) {
throw new FirebaseError(
"dataconnect.yaml is missing field schema.datasource.postgresql.cloudsql.instanceId",
);
}
const diffs = await migrateSchema({
options,
schema: serviceInfo.schema,
schema: mainSchema(serviceInfo.schemas),
validateOnly: true,
schemaValidation: serviceInfo.dataConnectYaml.schema.datasource.postgresql?.schemaValidation,
schemaValidation: mainSchemaYaml(serviceInfo.dataConnectYaml).datasource.postgresql
?.schemaValidation,
});
if (diffs.length) {
logLabeledSuccess(
Expand Down
9 changes: 6 additions & 3 deletions src/commands/dataconnect-sql-setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { setupSQLPermissions, getSchemaMetadata } from "../gcp/cloudsql/permissi
import { DEFAULT_SCHEMA } from "../gcp/cloudsql/permissions";
import { getIdentifiers, ensureServiceIsConnectedToCloudSql } from "../dataconnect/schemaMigration";
import { setupIAMUsers } from "../gcp/cloudsql/connect";
import { mainSchema, mainSchemaYaml } from "../dataconnect/types";

export const command = new Command("dataconnect:sql:setup [serviceId]")
.description("set up your CloudSQL database")
Expand All @@ -24,15 +25,17 @@ export const command = new Command("dataconnect:sql:setup [serviceId]")
const projectId = needProjectId(options);
await ensureApis(projectId);
const serviceInfo = await pickService(projectId, options.config, serviceId);
const instanceId =
serviceInfo.dataConnectYaml.schema.datasource.postgresql?.cloudSql.instanceId;
const instanceId = mainSchemaYaml(serviceInfo.dataConnectYaml).datasource.postgresql?.cloudSql
.instanceId;
if (!instanceId) {
throw new FirebaseError(
"dataconnect.yaml is missing field schema.datasource.postgresql.cloudsql.instanceId",
);
}

const { serviceName, instanceName, databaseId } = getIdentifiers(serviceInfo.schema);
const { serviceName, instanceName, databaseId } = getIdentifiers(
mainSchema(serviceInfo.schemas),
);
await ensureServiceIsConnectedToCloudSql(
serviceName,
instanceName,
Expand Down
3 changes: 2 additions & 1 deletion src/commands/dataconnect-sql-shell.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import { FirebaseError } from "../error";
import { FBToolsAuthClient } from "../gcp/cloudsql/fbToolsAuthClient";
import { confirmDangerousQuery, interactiveExecuteQuery } from "../gcp/cloudsql/interactive";
import { mainSchema } from "../dataconnect/types";

// Not a comprehensive list, used for keyword coloring.
const sqlKeywords = [
Expand Down Expand Up @@ -62,8 +63,8 @@
return query;
}

async function mainShellLoop(conn: pg.PoolClient) {

Check warning on line 66 in src/commands/dataconnect-sql-shell.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Missing return type on function
while (true) {

Check warning on line 67 in src/commands/dataconnect-sql-shell.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unexpected constant condition
const query = await promptForQuery();
if (query.toLowerCase() === ".exit") {
break;
Expand Down Expand Up @@ -91,7 +92,7 @@
const projectId = needProjectId(options);
await ensureApis(projectId);
const serviceInfo = await pickService(projectId, options.config, serviceId);
const { instanceId, databaseId } = getIdentifiers(serviceInfo.schema);
const { instanceId, databaseId } = getIdentifiers(mainSchema(serviceInfo.schemas));
const { user: username } = await getIAMUser(options);
const instance = await cloudSqlAdminClient.getInstance(projectId, instanceId);

Expand All @@ -99,7 +100,7 @@
const connectionName = instance.connectionName;
if (!connectionName) {
throw new FirebaseError(
`Could not get instance connection string for ${options.instanceId}:${options.databaseId}`,

Check warning on line 103 in src/commands/dataconnect-sql-shell.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Invalid type "unknown" of template literal expression

Check warning on line 103 in src/commands/dataconnect-sql-shell.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Invalid type "unknown" of template literal expression
);
}
const connector: Connector = new Connector({
Expand Down
7 changes: 7 additions & 0 deletions src/dataconnect/client.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import { FirebaseError } from "../error";
import * as types from "./types";

chai.use(require("chai-as-promised"));

Check warning on line 10 in src/dataconnect/client.spec.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Require statement not part of import statement

Check warning on line 10 in src/dataconnect/client.spec.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe argument of type `any` assigned to a parameter of type `ChaiPlugin`

describe("client", () => {
let sandbox: sinon.SinonSandbox;
Expand Down Expand Up @@ -124,6 +124,13 @@
expect(getStub).to.be.calledWith("projects/p/locations/l/services/s/schemas/main");
});

it("getSchema with schemaId", async () => {
getStub.resolves({ body: { name: "schema" } });
const schema = await client.getSchema("projects/p/locations/l/services/s", "schemaId");
expect(schema).to.deep.equal({ name: "schema" });
expect(getStub).to.be.calledWith("projects/p/locations/l/services/s/schemas/schemaId");
});

it("getSchema returns undefined if not found", async () => {
getStub.rejects(new FirebaseError("err", { status: 404 }));
const schema = await client.getSchema("projects/p/locations/l/services/s");
Expand Down
11 changes: 6 additions & 5 deletions src/dataconnect/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
const DATACONNECT_API_VERSION = "v1";
const PAGE_SIZE_MAX = 100;

const dataconnectClient = () =>

Check warning on line 9 in src/dataconnect/client.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Missing return type on function
new Client({
urlPrefix: dataconnectOrigin(),
apiVersion: DATACONNECT_API_VERSION,
Expand Down Expand Up @@ -83,11 +83,12 @@

/** Schema methods */

export async function getSchema(serviceName: string): Promise<types.Schema | undefined> {
export async function getSchema(
serviceName: string,
schemaId: string = types.MAIN_SCHEMA_ID,
): Promise<types.Schema | undefined> {
try {
const res = await dataconnectClient().get<types.Schema>(
`${serviceName}/schemas/${types.SCHEMA_ID}`,
);
const res = await dataconnectClient().get<types.Schema>(`${serviceName}/schemas/${schemaId}`);
return res.body;
} catch (err: any) {
if (err.status !== 404) {
Expand Down Expand Up @@ -146,7 +147,7 @@

export async function deleteSchema(serviceName: string): Promise<void> {
const op = await dataconnectClient().delete<types.Schema>(
`${serviceName}/schemas/${types.SCHEMA_ID}`,
`${serviceName}/schemas/${types.MAIN_SCHEMA_ID}`,
);
await operationPoller.pollOperation<void>({
apiOrigin: dataconnectOrigin(),
Expand Down
35 changes: 23 additions & 12 deletions src/dataconnect/load.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { Config } from "../config";
import { FirebaseError } from "../error";
import {
toDatasource,
SCHEMA_ID,
MAIN_SCHEMA_ID,
ConnectorYaml,
DataConnectYaml,
File,
Expand All @@ -16,6 +16,7 @@ import {
} from "./types";
import { readFileFromDirectory, wrappedSafeLoad } from "../utils";
import { DataConnectMultiple } from "../firebaseConfig";
import * as experiments from "../experiments";

// pickService reads firebase.json and returns all services with a given serviceId.
// If serviceID is not provided and there is a single service, return that.
Expand Down Expand Up @@ -77,8 +78,20 @@ export async function load(
const resolvedDir = config.path(sourceDirectory);
const dataConnectYaml = await readDataConnectYaml(resolvedDir);
const serviceName = `projects/${projectId}/locations/${dataConnectYaml.location}/services/${dataConnectYaml.serviceId}`;
const schemaDir = path.join(resolvedDir, dataConnectYaml.schema.source);
const schemaGQLs = await readGQLFiles(schemaDir);
const schemaYamls = dataConnectYaml.schema ? [dataConnectYaml.schema] : dataConnectYaml.schemas;
const schemas = await Promise.all(
schemaYamls!.map(async (yaml) => {
const schemaDir = path.join(resolvedDir, yaml.source);
const schemaGQLs = await readGQLFiles(schemaDir);
return {
name: `${serviceName}/schemas/${yaml.id || MAIN_SCHEMA_ID}`,
datasources: [toDatasource(projectId, dataConnectYaml.location, yaml.datasource)],
source: {
files: schemaGQLs,
},
};
}),
);
const connectorInfo = await Promise.all(
dataConnectYaml.connectorDirs.map(async (dir) => {
const connectorDir = path.join(resolvedDir, dir);
Expand All @@ -100,15 +113,7 @@ export async function load(
return {
serviceName,
sourceDirectory: resolvedDir,
schema: {
name: `${serviceName}/schemas/${SCHEMA_ID}`,
datasources: [
toDatasource(projectId, dataConnectYaml.location, dataConnectYaml.schema.datasource),
],
source: {
files: schemaGQLs,
},
},
schemas: schemas,
dataConnectYaml,
connectorInfo,
};
Expand Down Expand Up @@ -149,6 +154,12 @@ function validateDataConnectYaml(unvalidated: any): DataConnectYaml {
if (!unvalidated["location"]) {
throw new FirebaseError("Missing required field 'location' in dataconnect.yaml");
}
if (!experiments.isEnabled("fdcwebhooks") && unvalidated["schemas"]) {
throw new FirebaseError("Unsupported field 'schemas' in dataconnect.yaml");
}
if (!unvalidated["schema"] && !unvalidated["schemas"]) {
throw new FirebaseError("Either 'schema' or 'schemas' is required in dataconnect.yaml");
}
return unvalidated as DataConnectYaml;
}

Expand Down
8 changes: 4 additions & 4 deletions src/dataconnect/schemaMigration.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as clc from "colorette";
import { format } from "sql-formatter";

import { IncompatibleSqlSchemaError, Diff, SCHEMA_ID, SchemaValidation } from "./types";
import { IncompatibleSqlSchemaError, Diff, MAIN_SCHEMA_ID, SchemaValidation } from "./types";
import { getSchema, upsertSchema, deleteConnector } from "./client";
import {
getIAMUser,
Expand Down Expand Up @@ -178,7 +178,7 @@ export async function migrateSchema(args: {
const postgresql = schema.datasources.find((d) => d.postgresql)?.postgresql;
if (!postgresql) {
throw new FirebaseError(
`Cannot find Postgres datasource in the schema to deploy: ${serviceName}/schemas/${SCHEMA_ID}.\nIts datasources: ${JSON.stringify(schema.datasources)}`,
`Cannot find Postgres datasource in the schema to deploy: ${serviceName}/schemas/${MAIN_SCHEMA_ID}.\nIts datasources: ${JSON.stringify(schema.datasources)}`,
);
}
postgresql.schemaValidation = "NONE";
Expand Down Expand Up @@ -374,7 +374,7 @@ export function getIdentifiers(schema: Schema): {
);
}
const instanceId = instanceName.split("/").pop()!;
const serviceName = schema.name.replace(`/schemas/${SCHEMA_ID}`, "");
const serviceName = schema.name.replace(`/schemas/${MAIN_SCHEMA_ID}`, "");
return {
databaseId,
instanceId,
Expand Down Expand Up @@ -626,7 +626,7 @@ export async function ensureServiceIsConnectedToCloudSql(
logLabeledBullet("dataconnect", `Linking the Cloud SQL instance...`);
// If no schema has been deployed yet, deploy an empty one to get connectivity.
currentSchema = {
name: `${serviceName}/schemas/${SCHEMA_ID}`,
name: `${serviceName}/schemas/${MAIN_SCHEMA_ID}`,
source: {
files: [],
},
Expand Down
56 changes: 52 additions & 4 deletions src/dataconnect/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// Schema is a singleton, so we always call it 'main'
export const SCHEMA_ID = "main";
// The database schema ID is always 'main'
export const MAIN_SCHEMA_ID = "main";

// API Types
interface BaseResource {
Expand All @@ -26,7 +26,9 @@ export interface Connector extends BaseResource {
}

export interface Datasource {
// One of postgresql or httpGraphql must be set.
postgresql?: PostgreSql;
httpGraphql?: HttpGraphql;
}

export type SchemaValidation = "STRICT" | "COMPATIBLE";
Expand All @@ -39,6 +41,11 @@ export interface PostgreSql {
schemaMigration?: "MIGRATE_COMPATIBLE";
}

export interface HttpGraphql {
uri: string;
timeout?: string;
}

export interface CloudSqlInstance {
instance: string;
}
Expand Down Expand Up @@ -116,13 +123,16 @@ export function requiresVector(dm?: DeploymentMetadata): boolean {
export interface DataConnectYaml {
specVersion?: string;
serviceId: string;
schema: SchemaYaml;
// One of `schema` or `schemas` is required.
schema?: SchemaYaml;
schemas?: SchemaYaml[];
location: string;
connectorDirs: string[];
}

export interface SchemaYaml {
source: string;
id?: string;
datasource: DatasourceYaml;
}

Expand All @@ -134,6 +144,10 @@ export interface DatasourceYaml {
};
schemaValidation?: SchemaValidation;
};
httpGraphql?: {
uri: string;
timeout?: string;
};
}

export interface ConnectorYaml {
Expand Down Expand Up @@ -183,7 +197,7 @@ export interface DartSDK {
export interface ServiceInfo {
serviceName: string;
sourceDirectory: string;
schema: Schema;
schemas: Schema[];
connectorInfo: ConnectorInfo[];
dataConnectYaml: DataConnectYaml;
deploymentMetadata?: DeploymentMetadata;
Expand Down Expand Up @@ -211,9 +225,43 @@ export function toDatasource(
},
};
}
if (ds?.httpGraphql) {
return {
httpGraphql: {
uri: ds.httpGraphql.uri,
timeout: ds.httpGraphql.timeout,
},
};
}
return {};
}

/** Returns the main schema YAML for a Data Connect YAML */
export function mainSchemaYaml(dataconnectYaml: DataConnectYaml): SchemaYaml {
if (dataconnectYaml.schema) {
return dataconnectYaml.schema;
}
const mainSch = dataconnectYaml.schemas?.find((s) => s.id === MAIN_SCHEMA_ID || !s.id);
if (!mainSch) {
throw new Error(`Service ${dataconnectYaml.serviceId} has no main schema defined`);
}
return mainSch;
}

/** Returns the main schema from a list of schemas */
export function mainSchema(schemas: Schema[]): Schema {
const mainSch = schemas.find((s) => isMainSchema(s));
if (!mainSch) {
throw new Error(`No main schema is defined`);
}
return mainSch;
}

/** Returns true if the schema is the main schema */
export function isMainSchema(schema: Schema): boolean {
return schema.name.endsWith(`/schemas/${MAIN_SCHEMA_ID}`);
}

/** Start Dataplane Client Types */
export interface ExecuteGraphqlRequest {
query: string;
Expand Down
Loading
Loading