Skip to content
28 changes: 23 additions & 5 deletions src/dataconnect/build.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
import { DataConnectBuildArgs, DataConnectEmulator } from "../emulator/dataconnectEmulator";
import { Options } from "../options";
import { FirebaseError } from "../error";
import { select } from "../prompt";
import * as utils from "../utils";
import { prettify, prettifyTable } from "./graphqlError";
import { DeploymentMetadata, GraphqlError } from "./types";
import { getProjectDefaultAccount } from "../auth";
import { DeployOptions } from "../deploy";
import { DeployStats } from "../deploy/dataconnect/context";
import { DeploymentMetadata, GraphqlError } from "./types";

export async function build(

Check warning on line 11 in src/dataconnect/build.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Missing JSDoc comment
options: Options,
options: DeployOptions,
configDir: string,
dryRun?: boolean,
deployStats: DeployStats,
): Promise<DeploymentMetadata> {
const account = getProjectDefaultAccount(options.projectRoot);
const args: DataConnectBuildArgs = { configDir, account };
Expand All @@ -19,12 +20,29 @@
}
const buildResult = await DataConnectEmulator.build(args);
if (buildResult?.errors?.length) {
await handleBuildErrors(buildResult.errors, options.nonInteractive, options.force, dryRun);
buildResult.errors.forEach((e) => {
if (e.extensions?.warningLevel) {
let key = e.extensions.warningLevel.toLowerCase();
const msgSp = e.message.split(": ");
if (msgSp.length >= 2) {
key += `_${msgSp[0].toLowerCase()}`;
}
deployStats.numBuildWarnings.set(key, (deployStats.numBuildWarnings.get(key) ?? 0) + 1);
} else {
deployStats.numBuildErrors += 1;
}
});
await handleBuildErrors(
buildResult.errors,
options.nonInteractive,
options.force,
options.dryRun,
);
}
return buildResult?.metadata ?? {};
}

export async function handleBuildErrors(

Check warning on line 45 in src/dataconnect/build.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Missing JSDoc comment

Check warning on line 45 in src/dataconnect/build.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Missing return type on function
errors: GraphqlError[],
nonInteractive: boolean,
force: boolean,
Expand Down
21 changes: 18 additions & 3 deletions src/dataconnect/schemaMigration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import { select, confirm } from "../prompt";
import { logger } from "../logger";
import { Schema } from "./types";
import { DeployStats } from "../deploy/dataconnect/context";
import { Options } from "../options";
import { FirebaseError } from "../error";
import { logLabeledBullet, logLabeledWarning, logLabeledSuccess } from "../utils";
Expand Down Expand Up @@ -56,16 +57,16 @@
/* silent=*/ true,
);
default:
throw new FirebaseError(`Unexpected schema setup status: ${schemaInfo.setupStatus}`);

Check warning on line 60 in src/dataconnect/schemaMigration.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Invalid type "never" of template literal expression
}
} catch (err: any) {

Check warning on line 62 in src/dataconnect/schemaMigration.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unexpected any. Specify a different type
throw new FirebaseError(
`Cannot setup Postgres SQL permissions of Cloud SQL database ${instanceId}:${databaseId}\n${err}`,

Check warning on line 64 in src/dataconnect/schemaMigration.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Invalid type "any" of template literal expression
);
}
}

export async function diffSchema(

Check warning on line 69 in src/dataconnect/schemaMigration.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Missing JSDoc comment
options: Options,
schema: Schema,
schemaValidation?: SchemaValidation,
Expand All @@ -87,8 +88,8 @@
try {
await upsertSchema(schema, /** validateOnly=*/ true);
displayNoSchemaDiff(instanceId, databaseId, validationMode);
} catch (err: any) {

Check warning on line 91 in src/dataconnect/schemaMigration.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unexpected any. Specify a different type
if (err?.status !== 400) {

Check warning on line 92 in src/dataconnect/schemaMigration.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe member access .status on an `any` value
throw err;
}
incompatible = errors.getIncompatibleSchemaError(err);
Expand Down Expand Up @@ -123,7 +124,7 @@
displayStartSchemaDiff(validationMode);
await upsertSchema(schema, /** validateOnly=*/ true);
displayNoSchemaDiff(instanceId, databaseId, validationMode);
} catch (err: any) {

Check warning on line 127 in src/dataconnect/schemaMigration.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unexpected any. Specify a different type
if (err?.status !== 400) {
throw err;
}
Expand All @@ -150,8 +151,9 @@
/** true for `dataconnect:sql:migrate`, false for `deploy` */
validateOnly: boolean;
schemaValidation?: SchemaValidation;
stats?: DeployStats;
}): Promise<Diff[]> {
const { options, schema, validateOnly, schemaValidation } = args;
const { options, schema, validateOnly, schemaValidation, stats } = args;

// If the schema validation mode is unset, we prompt COMPATIBLE SQL diffs and then STRICT diffs.
let validationMode: SchemaValidation = schemaValidation ?? "COMPATIBLE";
Expand All @@ -170,6 +172,9 @@
// Check if Cloud SQL instance is still being created.
const existingInstance = await cloudSqlAdminClient.getInstance(projectId, instanceId);
if (existingInstance.state === "PENDING_CREATE") {
if (stats) {
stats.numSchemaSkippedDueToPendingCreate++;
}
const postgresql = schema.datasources.find((d) => d.postgresql)?.postgresql;
if (!postgresql) {
throw new FirebaseError(
Expand Down Expand Up @@ -211,6 +216,14 @@
}
throw err;
}
if (stats) {
if (incompatible) {
stats.numSqlSchemaDiffs += incompatible.diffs.length;
}
if (invalidConnectors.length) {
stats.numInvalidConnectors += invalidConnectors.length;
}
}

const migrationMode = await promptForSchemaMigration(
options,
Expand Down Expand Up @@ -259,11 +272,13 @@
}
// Parse and handle failed precondition errors, then retry.
const incompatible = errors.getIncompatibleSchemaError(err);
const invalidConnectors = errors.getInvalidConnectors(err);
if (!incompatible && !invalidConnectors.length) {
if (!incompatible) {
// If we got a different type of error, throw it
throw err;
}
if (stats && incompatible) {
stats.numSqlSchemaDiffs += incompatible.diffs.length;
}

const migrationMode = await promptForSchemaMigration(
options,
Expand Down
70 changes: 70 additions & 0 deletions src/deploy/dataconnect/context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { DeployOptions } from "..";
import { ResourceFilter } from "../../dataconnect/filters";
import { ServiceInfo } from "../../dataconnect/types";
import { AnalyticsParams } from "../../track";

export interface Context {
dataconnect?: {
serviceInfos: ServiceInfo[];
filters?: ResourceFilter[];
deployStats: DeployStats;
};
}

export interface DeployStats {
options?: DeployOptions;

// prepare.ts
missingBilling?: boolean;
numBuildErrors: number;
numBuildWarnings: Map<string, number>;

// deploy.ts
numServiceCreated: number;
numServiceDeleted: number;

// release.ts
numSchemaMigrated: number;
numConnectorUpdatedBeforeSchema: number;
numConnectorUpdatedAfterSchema: number;

// migrateSchema.ts
numSchemaSkippedDueToPendingCreate: number;
numSqlSchemaDiffs: number;
numInvalidConnectors: number;
}

export function initDeployStats(): DeployStats {
return {
numBuildErrors: 0,
numBuildWarnings: new Map<string, number>(),
numServiceCreated: 0,
numServiceDeleted: 0,
numSchemaMigrated: 0,
numConnectorUpdatedBeforeSchema: 0,
numConnectorUpdatedAfterSchema: 0,
numSchemaSkippedDueToPendingCreate: 0,
numSqlSchemaDiffs: 0,
numInvalidConnectors: 0,
};
}

export function deployStatsParams(stats: DeployStats): AnalyticsParams {
const buildWarnings: AnalyticsParams = {};
for (const [type, num] of stats.numBuildWarnings.entries()) {
buildWarnings[`num_build_warnings_${type}`] = num;
}
return {
missing_billing: (!!stats.missingBilling).toString(),
num_service_created: stats.numServiceCreated,
num_service_deleted: stats.numServiceDeleted,
num_schema_migrated: stats.numSchemaMigrated,
num_connector_updated_before_schema: stats.numConnectorUpdatedBeforeSchema,
num_connector_updated_after_schema: stats.numConnectorUpdatedAfterSchema,
num_schema_skipped_due_to_pending_create: stats.numSchemaSkippedDueToPendingCreate,
num_schema_with_incompatible_schema: stats.numSqlSchemaDiffs,
num_schema_with_invalid_connector: stats.numInvalidConnectors,
num_build_errors: stats.numBuildErrors,
...buildWarnings,
};
}
13 changes: 8 additions & 5 deletions src/deploy/dataconnect/deploy.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import * as ensureApiEnabled from "../../ensureApiEnabled";
import * as prompt from "../../prompt";
import * as poller from "../../operation-poller";
import { dataconnectOrigin } from "../../api";
import { initDeployStats } from "./context";

describe("dataconnect deploy", () => {
let sandbox: sinon.SinonSandbox;
Expand Down Expand Up @@ -47,7 +48,7 @@ describe("dataconnect deploy", () => {
dataConnectYaml: { serviceId: "s1" },
},
];
const context = { dataconnect: { serviceInfos } };
const context = { dataconnect: { serviceInfos, deployStats: initDeployStats() } };
const options = {} as any;

await deploy.default(context as any, options);
Expand All @@ -66,7 +67,7 @@ describe("dataconnect deploy", () => {

confirmStub.resolves(true);
const serviceInfos: any[] = [];
const context = { dataconnect: { serviceInfos } };
const context = { dataconnect: { serviceInfos, deployStats: initDeployStats() } };
const options = {} as any;

await deploy.default(context as any, options);
Expand All @@ -83,7 +84,7 @@ describe("dataconnect deploy", () => {

confirmStub.resolves(false);
const serviceInfos: any[] = [];
const context = { dataconnect: { serviceInfos } };
const context = { dataconnect: { serviceInfos, deployStats: initDeployStats() } };
const options = {} as any;

await deploy.default(context as any, options);
Expand Down Expand Up @@ -116,7 +117,7 @@ describe("dataconnect deploy", () => {
dataConnectYaml: { serviceId: "s1" },
},
];
const context = { dataconnect: { serviceInfos } };
const context = { dataconnect: { serviceInfos, deployStats: initDeployStats() } };
const options = {} as any;

await deploy.default(context as any, options);
Expand All @@ -131,7 +132,9 @@ describe("dataconnect deploy", () => {
.reply(200, { services: existingServices });

const serviceInfos: any[] = [];
const context = { dataconnect: { serviceInfos, filters: [{ serviceId: "s1" }] } };
const context = {
dataconnect: { serviceInfos, filters: [{ serviceId: "s1" }], deployStats: initDeployStats() },
};
const options = {} as any;

await deploy.default(context as any, options);
Expand Down
31 changes: 18 additions & 13 deletions src/deploy/dataconnect/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,26 +9,23 @@ import { ResourceFilter } from "../../dataconnect/filters";
import { vertexAIOrigin } from "../../api";
import * as ensureApiEnabled from "../../ensureApiEnabled";
import { confirm } from "../../prompt";
import { Context } from "./context";

/**
* Checks for and creates a Firebase DataConnect service, if needed.
* TODO: Also checks for and creates a CloudSQL instance and database.
* @param context The deploy context.
* @param options The CLI options object.
*/
export default async function (
context: {
dataconnect: {
serviceInfos: ServiceInfo[];
filters?: ResourceFilter[];
};
},
options: Options,
): Promise<void> {
export default async function (context: Context, options: Options): Promise<void> {
const dataconnect = context.dataconnect;
if (!dataconnect) {
throw new Error("dataconnect.prepare must be run before dataconnect.deploy");
}
const projectId = needProjectId(options);
const serviceInfos = context.dataconnect.serviceInfos as ServiceInfo[];
const serviceInfos = dataconnect.serviceInfos as ServiceInfo[];
const services = await client.listAllServices(projectId);
const filters = context.dataconnect.filters;
const filters = dataconnect.filters;

if (
serviceInfos.some((si) => {
Expand All @@ -41,12 +38,17 @@ export default async function (
const servicesToCreate = serviceInfos
.filter((si) => !services.some((s) => matches(si, s)))
.filter((si) => {
return !filters || filters?.some((f) => si.dataConnectYaml.serviceId === f.serviceId);
return (
!filters ||
filters?.some((f: ResourceFilter) => si.dataConnectYaml.serviceId === f.serviceId)
);
});
dataconnect.deployStats.numServiceCreated = servicesToCreate.length;

const servicesToDelete = filters
? []
: services.filter((s) => !serviceInfos.some((si) => matches(si, s)));
dataconnect.deployStats.numServiceDeleted = servicesToDelete.length;
await Promise.all(
servicesToCreate.map(async (s) => {
const { projectId, locationId, serviceId } = splitName(s.serviceName);
Expand Down Expand Up @@ -80,7 +82,10 @@ export default async function (
await Promise.all(
serviceInfos
.filter((si) => {
return !filters || filters?.some((f) => si.dataConnectYaml.serviceId === f.serviceId);
return (
!filters ||
filters?.some((f: ResourceFilter) => si.dataConnectYaml.serviceId === f.serviceId)
);
})
.map(async (s) => {
const postgresDatasource = s.schema.datasources.find((d) => d.postgresql);
Expand Down
2 changes: 2 additions & 0 deletions src/deploy/dataconnect/prepare.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ describe("dataconnect prepare", () => {
dataconnect: {
serviceInfos: [],
filters: undefined,
deployStats: (context as any).dataconnect.deployStats,
},
});
});
Expand All @@ -77,6 +78,7 @@ describe("dataconnect prepare", () => {
dataconnect: {
serviceInfos: serviceInfos,
filters: undefined,
deployStats: (context as any).dataconnect.deployStats,
},
});
});
Expand Down
8 changes: 6 additions & 2 deletions src/deploy/dataconnect/prepare.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,23 +17,26 @@ import { FirebaseError } from "../../error";
import { requiresVector } from "../../dataconnect/types";
import { diffSchema } from "../../dataconnect/schemaMigration";
import { upgradeInstructions } from "../../dataconnect/freeTrial";
import { Context, initDeployStats } from "./context";

/**
* Prepares for a Firebase DataConnect deployment by loading schemas and connectors from file.
* @param context The deploy context.
* @param options The CLI options object.
*/
export default async function (context: any, options: DeployOptions): Promise<void> {
export default async function (context: Context, options: DeployOptions): Promise<void> {
const projectId = needProjectId(options);
const deployStats = initDeployStats();
if (!(await checkBillingEnabled(projectId))) {
deployStats.missingBilling = true;
throw new FirebaseError(upgradeInstructions(projectId));
}
await ensureApis(projectId);
await requireTosAcceptance(DATA_CONNECT_TOS_ID)(options);
const filters = getResourceFilters(options);
const serviceInfos = await loadAll(projectId, options.config);
for (const si of serviceInfos) {
si.deploymentMetadata = await build(options, si.sourceDirectory, options.dryRun);
si.deploymentMetadata = await build(options, si.sourceDirectory, deployStats);
}
const unmatchedFilters = filters?.filter((f) => {
// filter out all filters that match no service
Expand All @@ -57,6 +60,7 @@ export default async function (context: any, options: DeployOptions): Promise<vo
context.dataconnect = {
serviceInfos,
filters,
deployStats,
};
utils.logLabeledBullet("dataconnect", `Successfully compiled schema and connectors`);
if (options.dryRun) {
Expand Down
11 changes: 7 additions & 4 deletions src/deploy/dataconnect/release.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import * as prompts from "../../dataconnect/prompts";
import { logger } from "../../logger";
import * as poller from "../../operation-poller";
import { dataconnectOrigin } from "../../api";
import { initDeployStats } from "./context";

describe("dataconnect release", () => {
let sandbox: sinon.SinonSandbox;
Expand Down Expand Up @@ -58,7 +59,7 @@ describe("dataconnect release", () => {
],
},
];
const context = { dataconnect: { serviceInfos } };
const context = { dataconnect: { serviceInfos, deployStats: initDeployStats() } };
const options = {} as any;

await release.default(context as any, options);
Expand Down Expand Up @@ -96,7 +97,7 @@ describe("dataconnect release", () => {
],
},
];
const context = { dataconnect: { serviceInfos } };
const context = { dataconnect: { serviceInfos, deployStats: initDeployStats() } };
const options = {} as any;

await release.default(context as any, options);
Expand Down Expand Up @@ -132,7 +133,7 @@ describe("dataconnect release", () => {
],
},
];
const context = { dataconnect: { serviceInfos } };
const context = { dataconnect: { serviceInfos, deployStats: initDeployStats() } };
const options = {} as any;

await release.default(context as any, options);
Expand Down Expand Up @@ -171,7 +172,9 @@ describe("dataconnect release", () => {
],
},
];
const context = { dataconnect: { serviceInfos, filters: [{ serviceId: "s1" }] } };
const context = {
dataconnect: { serviceInfos, filters: [{ serviceId: "s1" }], deployStats: initDeployStats() },
};
const options = {} as any;

await release.default(context as any, options);
Expand Down
Loading
Loading