Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Proposal] Adding Support for Custom GraphQL Types and Resolvers in Firebase Data Connect #7894

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
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
7,922 changes: 3,862 additions & 4,060 deletions firebase-vscode/package-lock.json

Large diffs are not rendered by default.

87 changes: 72 additions & 15 deletions firebase-vscode/src/data-connect/service.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import fetch, { Response } from "node-fetch";
import {
ExecutionResult,
IntrospectionQuery,
getIntrospectionQuery,
IntrospectionQuery,
} from "graphql";
import { assertExecutionResult } from "../../common/graphql";
import { DataConnectError } from "../../common/error";
Expand All @@ -14,11 +14,12 @@ import { dataConnectConfigs } from "../data-connect/config";

import { firebaseRC } from "../core/config";
import {
DATACONNECT_API_VERSION,
dataconnectDataplaneClient,
executeGraphQL,
DATACONNECT_API_VERSION,
} from "../../../src/dataconnect/dataplaneClient";
import {
CustomType,
ExecuteGraphqlRequest,
ExecuteGraphqlResponse,
ExecuteGraphqlResponseError,
Expand All @@ -28,6 +29,7 @@ import { Client, ClientResponse } from "../../../src/apiv2";
import { InstanceType } from "./code-lens-provider";
import { pluginLogger } from "../logger-wrapper";
import { DataConnectToolkit } from "./toolkit";
import { validateCustomTypes, validateResolvers } from "../../../src/dataconnect/validate";

/**
* DataConnect Emulator service
Expand All @@ -40,12 +42,12 @@ export class DataConnectService {
) {}

async servicePath(
path: string
path: string,
): Promise<string | undefined> {
const dataConnectConfigsValue = await firstWhereDefined(dataConnectConfigs);
// TODO: avoid calling this here and in getApiServicePathByPath
const serviceId =
dataConnectConfigsValue?.tryReadValue?.findEnclosingServiceForPath(path)?.value.serviceId;
const serviceId = dataConnectConfigsValue?.tryReadValue
?.findEnclosingServiceForPath(path)?.value.serviceId;
const projectId = firebaseRC.value?.tryReadValue?.projects?.default;

if (serviceId === undefined || projectId === undefined) {
Expand Down Expand Up @@ -87,8 +89,9 @@ export class DataConnectService {
>,
): Promise<ExecutionResult> {
if (!(response.status >= 200 && response.status < 300)) {
const errorResponse =
response as ClientResponse<ExecuteGraphqlResponseError>;
const errorResponse = response as ClientResponse<
ExecuteGraphqlResponseError
>;
throw new DataConnectError(
`Prod Request failed with status ${response.status}\nMessage ${errorResponse?.body?.error?.message}`,
);
Expand All @@ -103,8 +106,9 @@ export class DataConnectService {
>,
): Promise<ExecutionResult> {
if (!(response.status >= 200 && response.status < 300)) {
const errorResponse =
response as ClientResponse<ExecuteGraphqlResponseError>;
const errorResponse = response as ClientResponse<
ExecuteGraphqlResponseError
>;
throw new DataConnectError(
`Emulator Request failed with status ${response.status}\nMessage ${errorResponse?.body?.error?.message}`,
);
Expand Down Expand Up @@ -138,10 +142,9 @@ export class DataConnectService {
return {};
}
return {
impersonate:
userMock.kind === UserMockKind.AUTHENTICATED
? { authClaims: JSON.parse(userMock.claims) }
: { unauthenticated: true },
impersonate: userMock.kind === UserMockKind.AUTHENTICATED
? { authClaims: JSON.parse(userMock.claims) }
: { unauthenticated: true },
};
}

Expand Down Expand Up @@ -254,6 +257,60 @@ export class DataConnectService {
}
}

/**
* Configure custom type definitions for the Data Connect service.
*
* @param types - Record of custom type definitions
* @throws {DataConnectError} If validation fails or the server returns an error
*/
async configureCustomTypes(types: Record<string, CustomType>): Promise<void> {
const validationErrors = validateCustomTypes(types);
if (validationErrors.length > 0) {
throw new DataConnectError(
`Invalid custom type definitions:\n${
validationErrors.join("\n")
}`,
);
}

const response = await this.dataConnectToolkit.configureTypes(types);
if (response.status >= 400) {
throw new DataConnectError(
`Failed to configure custom types: ${
response?.body?.error?.message || "Unknown error"
}`,
);
}
}

/**
* Configure GraphQL resolvers for the Data Connect service.
*
* @param resolvers - Record of resolver functions
* @throws {DataConnectError} If validation fails or the server returns an error
*/
async configureResolvers(resolvers: Record<string, string>): Promise<void> {
const validationErrors = validateResolvers(resolvers);
if (validationErrors.length > 0) {
throw new DataConnectError(
`Invalid resolver definitions:\n${
validationErrors.join("\n")
}`,
);
}

const response = await this.dataConnectToolkit.configureResolvers(
resolvers,
);
if (response.status >= 400) {
throw new DataConnectError(
`Failed to configure resolvers: ${
response?.body?.error?.message || "Unknown error"
}`,
);
}
}

docsLink() {
return this.dataConnectToolkit.getGeneratedDocsURL();
}
Expand All @@ -265,9 +322,9 @@ function parseVariableString(variables: string): Record<string, any> {
}
try {
return JSON.parse(variables);
} catch(e: any) {
} catch (e: any) {
throw new Error(
"Unable to parse variables as JSON. Double check that that there are no unmatched braces or quotes, or unqouted keys in the variables pane."
"Unable to parse variables as JSON. Double check that that there are no unmatched braces or quotes, or unqouted keys in the variables pane.",
);
}
}
31 changes: 30 additions & 1 deletion firebase-vscode/src/data-connect/toolkit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,13 @@ import { dataConnectConfigs, firebaseConfig } from "./config";
import { runEmulatorIssuesStream } from "./emulator-stream";
import { runDataConnectCompiler } from "./core-compiler";
import { DataConnectToolkitController } from "../../../src/emulator/dataconnectToolkitController";
import { DataConnectEmulatorArgs } from "../emulator/dataconnectEmulator";
import { DataConnectEmulatorArgs, DataConnectEmulatorClient, ErrorResponse } from "../../../src/emulator/dataconnectEmulator";
import { Config } from "../config";
import { RC } from "../rc";
import { findOpenPort } from "../utils/port_utils";
import { CustomType } from "../../../src/dataconnect/types";
import { ClientResponse } from "../../../src/apiv2";
import { FirebaseError } from "../../../src/error";

const DEFAULT_PORT = 50001;
/** FDC-specific emulator logic; Toolkit and emulator */
Expand Down Expand Up @@ -88,6 +91,32 @@ export class DataConnectToolkit implements vscode.Disposable {
runDataConnectCompiler(configs, this.getFDCToolkitURL());
}

/**
* Configures custom type definitions through the Data Connect emulator
*/
public async configureTypes(
types: Record<string, CustomType>
): Promise<ClientResponse<void | ErrorResponse>> {
if (!DataConnectToolkitController.isRunning) {
throw new FirebaseError("Data Connect emulator is not running");
}
const client = new DataConnectEmulatorClient();
return client.configureTypes(types);
}

/**
* Configures resolver functions through the Data Connect emulator
*/
public async configureResolvers(
resolvers: Record<string, string>
): Promise<ClientResponse<void | ErrorResponse>> {
if (!DataConnectToolkitController.isRunning) {
throw new FirebaseError("Data Connect emulator is not running");
}
const client = new DataConnectEmulatorClient();
return client.configureResolvers(resolvers);
}

dispose() {
for (const sub of this.subs) {
sub();
Expand Down
11 changes: 11 additions & 0 deletions src/dataconnect/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,20 @@ import { Options } from "../options";
import { FirebaseError } from "../error";
import { prettify } from "./graphqlError";
import { DeploymentMetadata } from "./types";
import { validateCustomTypes, validateResolvers } from "./validate";

export async function build(options: Options, configDir: string): Promise<DeploymentMetadata> {
const buildResult = await DataConnectEmulator.build({ configDir });
const errors = [...(buildResult?.errors || [])];

if (buildResult?.customTypes) {
errors.push(...validateCustomTypes(buildResult.customTypes));
}

if (buildResult?.resolvers) {
errors.push(...validateResolvers(buildResult.resolvers));
}

if (buildResult?.errors?.length) {
throw new FirebaseError(
`There are errors in your schema and connector files:\n${buildResult.errors.map(prettify).join("\n")}`,
Expand Down
38 changes: 28 additions & 10 deletions src/dataconnect/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@ export async function getService(serviceName: string): Promise<types.Service> {
return res.body;
}

export async function listAllServices(projectId: string): Promise<types.Service[]> {
export async function listAllServices(
projectId: string,
): Promise<types.Service[]> {
const res = await dataconnectClient().get<{ services: types.Service[] }>(
`/projects/${projectId}/locations/-/services`,
);
Expand All @@ -45,7 +47,8 @@ export async function createService(
const op = await dataconnectClient().post<types.Service, types.Service>(
`/projects/${projectId}/locations/${locationId}/services`,
{
name: `projects/${projectId}/locations/${locationId}/services/${serviceId}`,
name:
`projects/${projectId}/locations/${locationId}/services/${serviceId}`,
},
{
queryParams: {
Expand All @@ -72,7 +75,9 @@ async function deleteService(serviceName: string): Promise<types.Service> {
return pollRes;
}

export async function deleteServiceAndChildResources(serviceName: string): Promise<void> {
export async function deleteServiceAndChildResources(
serviceName: string,
): Promise<void> {
const connectors = await listConnectors(serviceName);
await Promise.all(connectors.map(async (c) => deleteConnector(c.name)));
try {
Expand All @@ -87,7 +92,9 @@ export async function deleteServiceAndChildResources(serviceName: string): Promi

/** Schema methods */

export async function getSchema(serviceName: string): Promise<types.Schema | undefined> {
export async function getSchema(
serviceName: string,
): Promise<types.Schema | undefined> {
try {
const res = await dataconnectClient().get<types.Schema>(
`${serviceName}/schemas/${types.SCHEMA_ID}`,
Expand All @@ -105,12 +112,20 @@ export async function upsertSchema(
schema: types.Schema,
validateOnly: boolean = false,
): Promise<types.Schema | undefined> {
const op = await dataconnectClient().patch<types.Schema, types.Schema>(`${schema.name}`, schema, {
queryParams: {
allowMissing: "true",
validateOnly: validateOnly ? "true" : "false",
const op = await dataconnectClient().patch<types.Schema, types.Schema>(
`${schema.name}`,
{
...schema,
customTypes: schema.customTypes,
resolvers: schema.resolvers,
},
});
{
queryParams: {
allowMissing: "true",
validateOnly: validateOnly ? "true" : "false",
},
},
);
if (validateOnly) {
return;
}
Expand Down Expand Up @@ -150,7 +165,10 @@ export async function deleteConnector(name: string): Promise<void> {
return;
}

export async function listConnectors(serviceName: string, fields: string[] = []) {
export async function listConnectors(
serviceName: string,
fields: string[] = [],
) {
const connectors: types.Connector[] = [];
const getNextPage = async (pageToken = "") => {
const res = await dataconnectClient().get<{
Expand Down
2 changes: 1 addition & 1 deletion src/dataconnect/fileUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import * as fs from "fs-extra";
import * as path from "path";

import { FirebaseError } from "../error";
import { ConnectorYaml, DataConnectYaml, File, Platform, ServiceInfo } from "./types";
import { ConnectorYaml, CustomType, DataConnectYaml, File, Platform, ServiceInfo } from "./types";
import { readFileFromDirectory, wrappedSafeLoad } from "../utils";
import { Config } from "../config";
import { DataConnectMultiple } from "../firebaseConfig";
Expand Down
2 changes: 2 additions & 0 deletions src/dataconnect/load.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ export async function load(
source: {
files: schemaGQLs,
},
customTypes: dataConnectYaml.schema.customTypes,
resolvers: dataConnectYaml.schema.resolvers,
},
dataConnectYaml,
connectorInfo,
Expand Down
13 changes: 13 additions & 0 deletions src/dataconnect/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ export interface Schema extends BaseResource {

datasources: Datasource[];
source: Source;
customTypes?: Record<string, CustomType>;
resolvers?: Record<string, string>;
}

export interface Connector extends BaseResource {
Expand Down Expand Up @@ -85,6 +87,8 @@ export interface GraphqlError {
export interface BuildResult {
errors?: GraphqlError[];
metadata?: DeploymentMetadata;
customTypes?: Record<string, CustomType>;
resolvers?: Record<string, string>;
}

export interface DeploymentMetadata {
Expand All @@ -111,6 +115,15 @@ export interface DataConnectYaml {
export interface SchemaYaml {
source: string;
datasource: DatasourceYaml;
customTypes?: Record<string, CustomType>;
resolvers?: Record<string, string>;
}

export interface CustomType {
sqlType: string;
graphqlType: string;
serialize: string;
parseValue: string;
}

export interface DatasourceYaml {
Expand Down
Loading