From 22bb6467be5b4c251114cba10976206a24e98d65 Mon Sep 17 00:00:00 2001 From: joehan Date: Tue, 25 Jun 2024 21:21:27 -0400 Subject: [PATCH] init dataconnect:sdk and other SDK onboarding improvements (#7299) * Draft of init dataconnect:sdk * Polishing * Recommend command during dataconnect:sdk:generate * Added changelog * PR fixes * Fixing link --- CHANGELOG.md | 1 + schema/connector-yaml.json | 40 ++++++- src/commands/dataconnect-sdk-generate.ts | 6 +- src/commands/init.ts | 7 +- src/dataconnect/load.ts | 1 + src/dataconnect/types.ts | 20 ++-- src/init/features/dataconnect/index.ts | 7 ++ src/init/features/dataconnect/sdk.ts | 137 ++++++++++++++++++++++ src/init/features/index.ts | 1 + src/init/index.ts | 1 + templates/_gitignore | 3 + templates/init/dataconnect/connector.yaml | 11 ++ 12 files changed, 222 insertions(+), 13 deletions(-) create mode 100644 src/init/features/dataconnect/sdk.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index a5dabcad2f2..ac4f3a3b48a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,2 +1,3 @@ - Moved `dataconnect.location` key in `firebase.json` to `dataconnect.yaml`. - Fixes issue where files were not properly being discovered and deployed to Firebase Hosting (#7363, #7378) +- Added new command `init dataconnect:sdk`, which interactively configures a generated SDK for a Data Connect connector. diff --git a/schema/connector-yaml.json b/schema/connector-yaml.json index a1e7af383e8..a0a667c1fdc 100644 --- a/schema/connector-yaml.json +++ b/schema/connector-yaml.json @@ -2,7 +2,39 @@ "$schema": "http://json-schema.org/draft-07/schema#", "additionalProperties": false, "definitions": { - "generatable": { + "javascriptSdk": { + "additionalProperties": true, + "type": "object", + "properties": { + "outputDir": { + "type": "string", + "description": "Path to the directory where generated files should be written to." + }, + "package": { + "type": "string", + "description": "The package name to use for the generated code." + }, + "packageJSONDir": { + "type": "string", + "description": "The directory containining the package.json to install the generated package in." + } + } + }, + "kotlinSdk": { + "additionalProperties": true, + "type": "object", + "properties": { + "outputDir": { + "type": "string", + "description": "Path to the directory where generated files should be written to." + }, + "package": { + "type": "string", + "description": "The package name to use for the generated code." + } + } + }, + "swiftSdk": { "additionalProperties": true, "type": "object", "properties": { @@ -30,21 +62,21 @@ "javascriptSdk": { "type": "array", "items": { - "$ref": "#/definitions/generatable" + "$ref": "#/definitions/javascriptSdk" }, "description": "Configuration for a generated Javascript SDK" }, "kotlinSdk": { "type": "array", "items": { - "$ref": "#/definitions/generatable" + "$ref": "#/definitions/kotlinSdk" }, "description": "Configuration for a generated Kotlin SDK" }, "swiftSdk": { "type": "array", "items": { - "$ref": "#/definitions/generatable" + "$ref": "#/definitions/swiftSdk" }, "description": "Configuration for a generated Swift SDK" } diff --git a/src/commands/dataconnect-sdk-generate.ts b/src/commands/dataconnect-sdk-generate.ts index 6436530af60..36032274520 100644 --- a/src/commands/dataconnect-sdk-generate.ts +++ b/src/commands/dataconnect-sdk-generate.ts @@ -1,4 +1,5 @@ import * as path from "path"; +import * as clc from "colorette"; import { Command } from "../command"; import { Options } from "../options"; @@ -31,7 +32,10 @@ export const command = new Command("dataconnect:sdk:generate") if (!hasGeneratables) { logger.warn("No generated SDKs have been declared in connector.yaml files."); logger.warn( - "See https://firebase.google.com/docs/data-connect/quickstart#configure-sdk-outputs for examples of how to configure generated SDKs.", + `Run ${clc.bold("firebase init dataconnect:sdk")} to configure a generated SDK.`, + ); + logger.warn( + `See https://firebase.google.com/docs/data-connect/gp/web-sdk for more details of how to configure generated SDKs.`, ); return; } diff --git a/src/commands/init.ts b/src/commands/init.ts index 31243caf6ed..0cb1cf60d6a 100644 --- a/src/commands/init.ts +++ b/src/commands/init.ts @@ -82,7 +82,12 @@ if (isEnabled("genkit")) { choices.push({ value: "dataconnect", - name: "Data Connect: Set up a Firebase Data Connect service.", + name: "Data Connect: Set up a Firebase Data Connect service", + checked: false, +}); +choices.push({ + value: "dataconnect:sdk", + name: "Data Connect: Set up a generated SDK for your Firebase Data Connect service", checked: false, }); diff --git a/src/dataconnect/load.ts b/src/dataconnect/load.ts index fe2258f43be..30fbed87457 100644 --- a/src/dataconnect/load.ts +++ b/src/dataconnect/load.ts @@ -16,6 +16,7 @@ export async function load(projectId: string, sourceDirectory: string): Promise< const connectorYaml = await fileUtils.readConnectorYaml(connectorDir); const connectorGqls = await fileUtils.readGQLFiles(connectorDir); return { + directory: connectorDir, connectorYaml, connector: { name: `${serviceName}/connectors/${connectorYaml.connectorId}`, diff --git a/src/dataconnect/types.ts b/src/dataconnect/types.ts index 1fe95d7d42d..2d8ec7cdc39 100644 --- a/src/dataconnect/types.ts +++ b/src/dataconnect/types.ts @@ -127,13 +127,15 @@ export interface ConnectorYaml { } export interface Generate { - javascriptSdk?: JavascriptSDK[]; - swiftSdk?: SwiftSDK[]; - kotlinSdk?: KotlinSDK[]; + javascriptSdk?: JavascriptSDK; + swiftSdk?: SwiftSDK; + kotlinSdk?: KotlinSDK; } export interface JavascriptSDK { outputDir: string; + package?: string; + packageJSONDir?: string; } export interface SwiftSDK { // Optional for Swift becasue XCode makes you import files. @@ -141,6 +143,7 @@ export interface SwiftSDK { } export interface KotlinSDK { outputDir: string; + package?: string; } // Helper types && converters @@ -148,14 +151,17 @@ export interface ServiceInfo { serviceName: string; sourceDirectory: string; schema: Schema; - connectorInfo: { - connector: Connector; - connectorYaml: ConnectorYaml; - }[]; + connectorInfo: ConnectorInfo[]; dataConnectYaml: DataConnectYaml; deploymentMetadata?: DeploymentMetadata; } +export interface ConnectorInfo { + directory: string; + connector: Connector; + connectorYaml: ConnectorYaml; +} + export function toDatasource( projectId: string, locationId: string, diff --git a/src/init/features/dataconnect/index.ts b/src/init/features/dataconnect/index.ts index 7ae74df24ac..549768c54f6 100644 --- a/src/init/features/dataconnect/index.ts +++ b/src/init/features/dataconnect/index.ts @@ -1,4 +1,6 @@ import { join } from "path"; +import * as clc from "colorette"; + import { confirm, promptOnce } from "../../../prompt"; import { Config } from "../../../config"; import { Setup } from "../.."; @@ -12,6 +14,7 @@ import { DEFAULT_POSTGRES_CONNECTION } from "../emulators"; import { parseCloudSQLInstanceName, parseServiceName } from "../../../dataconnect/names"; import { logger } from "../../../logger"; import { readTemplateSync } from "../../../templates"; +import { logSuccess } from "../../../utils"; const DATACONNECT_YAML_TEMPLATE = readTemplateSync("init/dataconnect/dataconnect.yaml"); const CONNECTOR_YAML_TEMPLATE = readTemplateSync("init/dataconnect/connector.yaml"); @@ -96,6 +99,10 @@ export async function doSetup(setup: Setup, config: Config): Promise { waitForCreation: false, }); } + logger.info(""); + logSuccess( + `If you'd like to generate an SDK for your new connector, run ${clc.bold("firebase init dataconnect:sdk")}`, + ); } function subValues( diff --git a/src/init/features/dataconnect/sdk.ts b/src/init/features/dataconnect/sdk.ts new file mode 100644 index 00000000000..a2dd999639c --- /dev/null +++ b/src/init/features/dataconnect/sdk.ts @@ -0,0 +1,137 @@ +import * as yaml from "yaml"; +import * as fs from "fs-extra"; +import { confirm, promptOnce } from "../../../prompt"; +import * as clc from "colorette"; +import * as path from "path"; +import { readFirebaseJson } from "../../../dataconnect/fileUtils"; +import { Config } from "../../../config"; +import { Setup } from "../.."; +import { load } from "../../../dataconnect/load"; +import { logger } from "../../../logger"; +import { ConnectorInfo, ConnectorYaml, JavascriptSDK, KotlinSDK } from "../../../dataconnect/types"; +import { DataConnectEmulator } from "../../../emulator/dataconnectEmulator"; + +const IOS = "ios"; +const WEB = "web"; +const ANDROID = "android"; +export async function doSetup(setup: Setup, config: Config): Promise { + const serviceCfgs = readFirebaseJson(config); + const serviceInfos = await Promise.all( + serviceCfgs.map((c) => load(setup.projectId || "", path.join(process.cwd(), c.source))), + ); + const connectorChoices: { name: string; value: ConnectorInfo }[] = serviceInfos + .map((si) => { + return si.connectorInfo.map((ci) => { + return { + name: `${si.dataConnectYaml.serviceId}/${ci.connectorYaml.connectorId}`, + value: ci, + }; + }); + }) + .flat(); + if (!connectorChoices.length) { + logger.info( + `Your config has no connectors to set up SDKs for. Run ${clc.bold( + "firebase init dataconnect", + )} to set up a service and conenctors.`, + ); + return; + } + const connectorInfo: ConnectorInfo = await promptOnce({ + message: "Which connector do you want set up a generated SDK for?", + type: "list", + choices: connectorChoices, + }); + + const platforms = await promptOnce({ + message: "Which platforms do you want to set up a generated SDK for?", + type: "checkbox", + choices: [ + { name: "iOS (Swift)", value: IOS }, + { name: "Web (JavaScript)", value: WEB }, + { name: "Androd (Kotlin)", value: ANDROID }, + ], + }); + + const newConnectorYaml = JSON.parse(JSON.stringify(connectorInfo.connectorYaml)) as ConnectorYaml; + if (!newConnectorYaml.generate) { + newConnectorYaml.generate = {}; + } + + if (platforms.includes(IOS)) { + const defaultOutputDir = newConnectorYaml.generate.swiftSdk?.outputDir; + const outputDir = await promptOnce({ + message: `What directory do you want to write your Swift SDK code to? (If not absolute, path will be relative to '${connectorInfo.directory}')`, + type: "input", + default: defaultOutputDir, + }); + const swiftSdk = { outputDir }; + newConnectorYaml.generate.swiftSdk = swiftSdk; + } + if (platforms.includes(WEB)) { + const outputDir = await promptOnce({ + message: `What directory do you want to write your JavaScript SDK code to? (If not absolute, path will be relative to '${connectorInfo.directory}')`, + type: "input", + default: newConnectorYaml.generate.javascriptSdk?.outputDir, + }); + const pkg = await promptOnce({ + message: "What package name do you want to use for your JavaScript SDK?", + type: "input", + default: + newConnectorYaml.generate.javascriptSdk?.package ?? + `@firebasegen/${connectorInfo.connectorYaml.connectorId}`, + }); + const packageJSONDir = await promptOnce({ + message: + "Which directory contains the package.json that you would like to add the JavaScript SDK dependency to? (Leave blank to skip)", + type: "input", + default: newConnectorYaml.generate.javascriptSdk?.packageJSONDir, + }); + // ../.. since we ask relative to connector.yaml + const javascriptSdk: JavascriptSDK = { + outputDir, + package: pkg, + }; + if (packageJSONDir) { + javascriptSdk.packageJSONDir = packageJSONDir; + } + newConnectorYaml.generate.javascriptSdk = javascriptSdk; + } + if (platforms.includes(ANDROID)) { + const outputDir = await promptOnce({ + message: `What directory do you want to write your Kotlin SDK code to? (If not absolute, path will be relative to '${connectorInfo.directory}')`, + type: "input", + default: newConnectorYaml.generate.kotlinSdk?.outputDir, + }); + const pkg = await promptOnce({ + message: "What package name do you want to use for your Kotlin SDK?", + type: "input", + default: + newConnectorYaml.generate.kotlinSdk?.package ?? + `com.google.firebase.dataconnect.connectors.${connectorInfo.connectorYaml.connectorId}`, + }); + const kotlinSdk: KotlinSDK = { + outputDir, + package: pkg, + }; + newConnectorYaml.generate.kotlinSdk = kotlinSdk; + } + // TODO: Prompt user about adding generated paths to .gitignore + const connectorYamlContents = yaml.stringify(newConnectorYaml); + const connectorYamlPath = `${connectorInfo.directory}/connector.yaml`; + fs.writeFileSync(connectorYamlPath, connectorYamlContents, "utf8"); + logger.info(`Wrote new config to ${connectorYamlPath}`); + if ( + setup.projectId && + (await confirm({ + message: "Would you like to generate SDK code now?", + default: true, + })) + ) { + await DataConnectEmulator.generate({ + configDir: connectorInfo.directory, + connectorId: connectorInfo.connectorYaml.connectorId, + }); + logger.info(`Generated SDK code for ${connectorInfo.connectorYaml.connectorId}`); + } +} diff --git a/src/init/features/index.ts b/src/init/features/index.ts index a2f4fc77246..9673ab64206 100644 --- a/src/init/features/index.ts +++ b/src/init/features/index.ts @@ -11,5 +11,6 @@ export { doSetup as project } from "./project"; export { doSetup as remoteconfig } from "./remoteconfig"; export { initGitHub as hostingGithub } from "./hosting/github"; export { doSetup as dataconnect } from "./dataconnect"; +export { doSetup as dataconnectSdk } from "./dataconnect/sdk"; export { doSetup as apphosting } from "../../apphosting"; export { doSetup as genkit } from "./genkit"; diff --git a/src/init/index.ts b/src/init/index.ts index 04e3620ac98..df1ac749976 100644 --- a/src/init/index.ts +++ b/src/init/index.ts @@ -21,6 +21,7 @@ const featureFns = new Map P ["database", features.database], ["firestore", features.firestore], ["dataconnect", features.dataconnect], + ["dataconnect:sdk", features.dataconnectSdk], ["functions", features.functions], ["hosting", features.hosting], ["storage", features.storage], diff --git a/templates/_gitignore b/templates/_gitignore index dbb58ffbfa3..b17f6310755 100644 --- a/templates/_gitignore +++ b/templates/_gitignore @@ -64,3 +64,6 @@ node_modules/ # dotenv environment variables file .env + +# dataconnect generated files +.dataconnect diff --git a/templates/init/dataconnect/connector.yaml b/templates/init/dataconnect/connector.yaml index 009eabe29a4..de62ea7766b 100644 --- a/templates/init/dataconnect/connector.yaml +++ b/templates/init/dataconnect/connector.yaml @@ -1,2 +1,13 @@ connectorId: "__connectorId__" authMode: "PUBLIC" +## ## Here's an example of how to add generated SDKs. +## ## You'll need to replace the outputDirs with ones pointing to where you want the generated code in your app. +# generate: +# javascriptSdk: +# outputDir: +# package: "@firebasegen/my-connector" +# packageJSONDir: < Optional. Path to your Javascript app's package.json> +# swiftSdk: +# outputDir: +# kotlinSdk: +# outputDir: