From 7b90d5300a824df7c0a772223af3aa68696c5846 Mon Sep 17 00:00:00 2001 From: Maneesh Tewani Date: Tue, 21 Oct 2025 14:54:25 -0700 Subject: [PATCH 1/9] Added firebase admin connector --- schema/connector-yaml.json | 30 +++++++++++ src/appUtils.spec.ts | 64 ++++++++++++++++++++++- src/appUtils.ts | 52 ++++++++++++++---- src/commands/dataconnect-sdk-generate.ts | 3 +- src/dataconnect/types.ts | 7 +++ src/init/features/dataconnect/sdk.spec.ts | 12 +++++ src/init/features/dataconnect/sdk.ts | 23 ++++++++ 7 files changed, 178 insertions(+), 13 deletions(-) diff --git a/schema/connector-yaml.json b/schema/connector-yaml.json index 729a747d128..e399f4bbebc 100644 --- a/schema/connector-yaml.json +++ b/schema/connector-yaml.json @@ -2,6 +2,24 @@ "$schema": "http://json-schema.org/draft-07/schema#", "additionalProperties": false, "definitions": { + "adminNodeSdk": { + "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." + } + } + }, "javascriptSdk": { "additionalProperties": true, "type": "object", @@ -90,6 +108,18 @@ ], "description": "Configuration for a generated Javascript SDK" }, + "adminNodeSdk": { + "oneOf": [ + { "$ref": "#/definitions/adminNodeSdk" }, + { + "type": "array", + "items": { + "$ref": "#/definitions/adminNodeSdk" + } + } + ], + "description": "Configuration for a generated Javascript SDK" + }, "dartSdk": { "oneOf": [ { "$ref": "#/definitions/dartSdk" }, diff --git a/src/appUtils.spec.ts b/src/appUtils.spec.ts index 3289df025df..351a7ae1681 100644 --- a/src/appUtils.spec.ts +++ b/src/appUtils.spec.ts @@ -244,7 +244,7 @@ function cleanUndefinedFields(apps: App[]): App[] { }); } -describe("appUtils", () => { +describe.only("appUtils", () => { describe("getPlatformsFromFolder", () => { const testDir = "test-dir"; @@ -620,6 +620,68 @@ describe("appUtils", () => { ]); }); + it("should detect an admin app with firebase-admin dependency", async () => { + mockfs({ + [testDir]: { + "package.json": JSON.stringify({ + dependencies: { + "firebase-admin": "1.0.0", + }, + }), + }, + }); + const apps = cleanUndefinedFields(await detectApps(testDir)); + expect(apps).to.have.deep.members([ + { + platform: Platform.ADMIN_NODE, + directory: ".", + }, + ]); + }); + + it("should detect an admin app with firebase-functions dependency", async () => { + mockfs({ + [testDir]: { + "package.json": JSON.stringify({ + dependencies: { + "firebase-functions": "1.0.0", + }, + }), + }, + }); + const apps = cleanUndefinedFields(await detectApps(testDir)); + expect(apps).to.have.deep.members([ + { + platform: Platform.ADMIN_NODE, + directory: ".", + }, + ]); + }); + + it("should detect an admin and client app", async () => { + mockfs({ + [testDir]: { + "package.json": JSON.stringify({ + dependencies: { + "firebase-admin": "1.0.0", + firebase: "1.0.0", + }, + }), + }, + }); + const apps = cleanUndefinedFields(await detectApps(testDir)); + expect(apps).to.have.deep.members([ + { + platform: Platform.ADMIN_NODE, + directory: ".", + }, + { + platform: Platform.WEB, + directory: ".", + }, + ]); + }); + it("should detect angular web framework", async () => { mockfs({ [testDir]: { diff --git a/src/appUtils.ts b/src/appUtils.ts index 35b72ac7e69..4da25991dd9 100644 --- a/src/appUtils.ts +++ b/src/appUtils.ts @@ -11,6 +11,7 @@ export enum Platform { WEB = "WEB", IOS = "IOS", FLUTTER = "FLUTTER", + ADMIN_NODE = "ADMIN_NODE", } /** @@ -62,7 +63,11 @@ export async function detectApps(dirPath: string): Promise { const pubSpecYamlFiles = await detectFiles(dirPath, "pubspec.yaml"); const srcMainFolders = await detectFiles(dirPath, "src/main/"); const xCodeProjects = await detectFiles(dirPath, "*.xcodeproj/"); - const webApps = await Promise.all(packageJsonFiles.map((p) => packageJsonToWebApp(dirPath, p))); + const adminAndWebApps = []; + for (const packageJson of packageJsonFiles) { + const apps = await packageJsonToAdminOrWebApp(dirPath, packageJson); + adminAndWebApps.push(...apps); + } const flutterAppPromises = await Promise.all( pubSpecYamlFiles.map((f) => processFlutterDir(dirPath, f)), @@ -80,7 +85,7 @@ export async function detectApps(dirPath: string): Promise { const iosApps = iosAppPromises .flat() .filter((a) => !flutterApps.some((f) => isPathInside(f.directory, a.directory))); - return [...webApps, ...flutterApps, ...androidApps, ...iosApps]; + return [...flutterApps, ...androidApps, ...iosApps, ...adminAndWebApps]; } async function processIosDir(dirPath: string, filePath: string): Promise { @@ -164,14 +169,41 @@ function isPathInside(parent: string, child: string): boolean { return !relativePath.startsWith(`..`); } -async function packageJsonToWebApp(dirPath: string, packageJsonFile: string): Promise { +export function getAllDepsFromPackageJson(packageJson: PackageJSON) { + const devDependencies = Object.keys(packageJson.devDependencies ?? {}); + const dependencies = Object.keys(packageJson.dependencies ?? {}); + const allDeps = Array.from(new Set([...devDependencies, ...dependencies])); + return allDeps; +} + +async function packageJsonToAdminOrWebApp( + dirPath: string, + packageJsonFile: string, +): Promise { const fullPath = path.join(dirPath, packageJsonFile); const packageJson = JSON.parse((await fs.readFile(fullPath)).toString()) as PackageJSON; - return { - platform: Platform.WEB, - directory: path.dirname(packageJsonFile), - frameworks: getFrameworksFromPackageJson(packageJson), - }; + const allDeps = getAllDepsFromPackageJson(packageJson); + const detectedApps = []; + if (allDeps.includes("firebase-admin") || allDeps.includes("firebase-functions")) { + detectedApps.push({ + platform: Platform.ADMIN_NODE, + directory: path.dirname(packageJsonFile), + }); + } + if (allDeps.includes("firebase")) { + detectedApps.push({ + platform: Platform.WEB, + directory: path.dirname(packageJsonFile), + }); + } + if (detectedApps.length === 0) { + detectedApps.push({ + platform: Platform.WEB, + directory: path.dirname(packageJsonFile), + frameworks: getFrameworksFromPackageJson(packageJson), + }); + } + return detectedApps; } const WEB_FRAMEWORKS: Framework[] = Object.values(Framework); @@ -215,9 +247,7 @@ async function detectAppIdsForPlatform( } function getFrameworksFromPackageJson(packageJson: PackageJSON): Framework[] { - const devDependencies = Object.keys(packageJson.devDependencies ?? {}); - const dependencies = Object.keys(packageJson.dependencies ?? {}); - const allDeps = Array.from(new Set([...devDependencies, ...dependencies])); + const allDeps = getAllDepsFromPackageJson(packageJson); return WEB_FRAMEWORKS.filter((framework) => WEB_FRAMEWORKS_SIGNALS[framework].find((dep) => allDeps.includes(dep)), ); diff --git a/src/commands/dataconnect-sdk-generate.ts b/src/commands/dataconnect-sdk-generate.ts index c9370de208c..db587df5176 100644 --- a/src/commands/dataconnect-sdk-generate.ts +++ b/src/commands/dataconnect-sdk-generate.ts @@ -99,7 +99,8 @@ async function loadAllWithSDKs( c.connectorYaml.generate?.javascriptSdk || c.connectorYaml.generate?.kotlinSdk || c.connectorYaml.generate?.swiftSdk || - c.connectorYaml.generate?.dartSdk + c.connectorYaml.generate?.dartSdk || + c.connectorYaml.generate?.adminNodeSdk ); }), ); diff --git a/src/dataconnect/types.ts b/src/dataconnect/types.ts index e990b2422d5..c9b7dfeaa2d 100644 --- a/src/dataconnect/types.ts +++ b/src/dataconnect/types.ts @@ -146,6 +146,7 @@ export interface Generate { swiftSdk?: SwiftSDK | SwiftSDK[]; kotlinSdk?: KotlinSDK | KotlinSDK[]; dartSdk?: DartSDK | DartSDK[]; + adminNodeSdk?: AdminNodeSDK | AdminNodeSDK[]; } export interface SupportedFrameworks { @@ -153,6 +154,12 @@ export interface SupportedFrameworks { angular?: boolean; } +export interface AdminNodeSDK { + outputDir: string; + package: string; + packageJsonDir?: string; +} + export interface JavascriptSDK extends SupportedFrameworks { outputDir: string; package: string; diff --git a/src/init/features/dataconnect/sdk.spec.ts b/src/init/features/dataconnect/sdk.spec.ts index 47ff26b3fa2..9329ad16e0f 100644 --- a/src/init/features/dataconnect/sdk.spec.ts +++ b/src/init/features/dataconnect/sdk.spec.ts @@ -108,6 +108,17 @@ describe("addSdkGenerateToConnectorYaml", () => { }, ]); }); + it("should add swiftSdk for admin node platform", () => { + app.platform = Platform.ADMIN_NODE; + addSdkGenerateToConnectorYaml(connectorInfo, connectorYaml, app); + expect(connectorYaml.generate?.adminNodeSdk).to.deep.equal([ + { + outputDir: "../app/src/dataconnect-admin-generated", + package: "@dataconnect/admin-generated", + packageJsonDir: "../app", + }, + ]); + }); }); describe("chooseApp", () => { @@ -192,6 +203,7 @@ describe("chooseApp", () => { expect(promptStub.called).to.be.false; }); + // TODO: Add a test for admin node. it("should deduplicate apps with the same platform and directory", async () => { const apps: App[] = [ { platform: Platform.WEB, directory: "web", frameworks: [Framework.REACT], appId: "app1" }, diff --git a/src/init/features/dataconnect/sdk.ts b/src/init/features/dataconnect/sdk.ts index 13461bacd53..6d0969d6579 100644 --- a/src/init/features/dataconnect/sdk.ts +++ b/src/init/features/dataconnect/sdk.ts @@ -9,6 +9,7 @@ import { Config } from "../../../config"; import { Setup } from "../.."; import { loadAll } from "../../../dataconnect/load"; import { + AdminNodeSDK, ConnectorInfo, ConnectorYaml, DartSDK, @@ -191,10 +192,14 @@ export function initAppCounters(info: SdkRequiredInfo): { [key: string]: number num_android_apps: 0, num_ios_apps: 0, num_flutter_apps: 0, + num_admin_node_apps: 0, }; for (const app of info.apps ?? []) { switch (app.platform) { + case Platform.ADMIN_NODE: + counts.num_admin_node_apps++; + break; case Platform.WEB: counts.num_web_apps++; break; @@ -348,6 +353,24 @@ export function addSdkGenerateToConnectorYaml( const generate = connectorYaml.generate; switch (app.platform) { + case Platform.ADMIN_NODE: { + const adminNodeSdk: AdminNodeSDK = { + outputDir: path.relative( + connectorDir, + path.join(appDir, `src/dataconnect-admin-generated`), + ), + package: `@dataconnect/admin-generated`, + packageJsonDir: path.relative(connectorDir, appDir), + }; + if (!isArray(generate?.adminNodeSdk)) { + generate.adminNodeSdk = generate.adminNodeSdk ? [generate.adminNodeSdk] : []; + } + if (!generate.adminNodeSdk.some((s) => s.outputDir === adminNodeSdk.outputDir)) { + generate.adminNodeSdk.push(adminNodeSdk); + } + break; + } + case Platform.WEB: { const javascriptSdk: JavascriptSDK = { outputDir: path.relative(connectorDir, path.join(appDir, `src/dataconnect-generated`)), From 75483ab5d4f2402445e7c2de3003cbce76d11a67 Mon Sep 17 00:00:00 2001 From: Maneesh Tewani Date: Tue, 21 Oct 2025 14:59:19 -0700 Subject: [PATCH 2/9] Removed only --- src/appUtils.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/appUtils.spec.ts b/src/appUtils.spec.ts index 351a7ae1681..3248469121b 100644 --- a/src/appUtils.spec.ts +++ b/src/appUtils.spec.ts @@ -244,7 +244,7 @@ function cleanUndefinedFields(apps: App[]): App[] { }); } -describe.only("appUtils", () => { +describe("appUtils", () => { describe("getPlatformsFromFolder", () => { const testDir = "test-dir"; From 8223d293f90706a3189826c73ec1129c9587ae69 Mon Sep 17 00:00:00 2001 From: Maneesh Tewani Date: Tue, 21 Oct 2025 15:00:22 -0700 Subject: [PATCH 3/9] Fixed bug where getFrameworksFromPackageJson isn't called with firebase as the dependency --- src/appUtils.ts | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/appUtils.ts b/src/appUtils.ts index 4da25991dd9..3a68e3714b7 100644 --- a/src/appUtils.ts +++ b/src/appUtils.ts @@ -190,13 +190,7 @@ async function packageJsonToAdminOrWebApp( directory: path.dirname(packageJsonFile), }); } - if (allDeps.includes("firebase")) { - detectedApps.push({ - platform: Platform.WEB, - directory: path.dirname(packageJsonFile), - }); - } - if (detectedApps.length === 0) { + if (allDeps.includes("firebase") || detectApps.length === 0) { detectedApps.push({ platform: Platform.WEB, directory: path.dirname(packageJsonFile), From 2daca778d0fed4c11536b839c4097c84ae64c0f5 Mon Sep 17 00:00:00 2001 From: Maneesh Tewani Date: Tue, 21 Oct 2025 15:01:01 -0700 Subject: [PATCH 4/9] Fixed test name --- src/init/features/dataconnect/sdk.spec.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/init/features/dataconnect/sdk.spec.ts b/src/init/features/dataconnect/sdk.spec.ts index 9329ad16e0f..3986fa9404d 100644 --- a/src/init/features/dataconnect/sdk.spec.ts +++ b/src/init/features/dataconnect/sdk.spec.ts @@ -108,7 +108,8 @@ describe("addSdkGenerateToConnectorYaml", () => { }, ]); }); - it("should add swiftSdk for admin node platform", () => { + + it("should add adminSdk for admin node platform", () => { app.platform = Platform.ADMIN_NODE; addSdkGenerateToConnectorYaml(connectorInfo, connectorYaml, app); expect(connectorYaml.generate?.adminNodeSdk).to.deep.equal([ From b2ee2db3b3eda8932583f1b748aab7f61b341fff Mon Sep 17 00:00:00 2001 From: Maneesh Tewani Date: Tue, 21 Oct 2025 15:02:13 -0700 Subject: [PATCH 5/9] Used flat --- src/appUtils.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/appUtils.ts b/src/appUtils.ts index 3a68e3714b7..ab238780465 100644 --- a/src/appUtils.ts +++ b/src/appUtils.ts @@ -63,11 +63,9 @@ export async function detectApps(dirPath: string): Promise { const pubSpecYamlFiles = await detectFiles(dirPath, "pubspec.yaml"); const srcMainFolders = await detectFiles(dirPath, "src/main/"); const xCodeProjects = await detectFiles(dirPath, "*.xcodeproj/"); - const adminAndWebApps = []; - for (const packageJson of packageJsonFiles) { - const apps = await packageJsonToAdminOrWebApp(dirPath, packageJson); - adminAndWebApps.push(...apps); - } + const adminAndWebApps = ( + await Promise.all(packageJsonFiles.map((p) => packageJsonToAdminOrWebApp(dirPath, p))) + ).flat(); const flutterAppPromises = await Promise.all( pubSpecYamlFiles.map((f) => processFlutterDir(dirPath, f)), From 28536036590846e03a27a8f77a4e9340282b94dd Mon Sep 17 00:00:00 2001 From: Maneesh Tewani Date: Tue, 21 Oct 2025 15:02:37 -0700 Subject: [PATCH 6/9] Renamed json file --- schema/connector-yaml.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/schema/connector-yaml.json b/schema/connector-yaml.json index e399f4bbebc..d741dc77adc 100644 --- a/schema/connector-yaml.json +++ b/schema/connector-yaml.json @@ -118,7 +118,7 @@ } } ], - "description": "Configuration for a generated Javascript SDK" + "description": "Configuration for a generated Admin Node SDK" }, "dartSdk": { "oneOf": [ From 85c92eceda98266f1a3e6718eb5ed712d9d447be Mon Sep 17 00:00:00 2001 From: Maneesh Tewani Date: Thu, 30 Oct 2025 12:58:05 -0700 Subject: [PATCH 7/9] Update schema/connector-yaml.json Co-authored-by: Joe Hanley --- schema/connector-yaml.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/schema/connector-yaml.json b/schema/connector-yaml.json index d741dc77adc..20bbfc76c78 100644 --- a/schema/connector-yaml.json +++ b/schema/connector-yaml.json @@ -16,7 +16,7 @@ }, "packageJSONDir": { "type": "string", - "description": "The directory containining the package.json to install the generated package in." + "description": "The directory containing the package.json to install the generated package in." } } }, From 91704e89cee4590e4c9fab0609d2b0f109588e06 Mon Sep 17 00:00:00 2001 From: Maneesh Tewani Date: Thu, 30 Oct 2025 15:59:14 -0700 Subject: [PATCH 8/9] Removed todo --- src/init/features/dataconnect/sdk.spec.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/init/features/dataconnect/sdk.spec.ts b/src/init/features/dataconnect/sdk.spec.ts index 3986fa9404d..a85ca37f916 100644 --- a/src/init/features/dataconnect/sdk.spec.ts +++ b/src/init/features/dataconnect/sdk.spec.ts @@ -204,7 +204,6 @@ describe("chooseApp", () => { expect(promptStub.called).to.be.false; }); - // TODO: Add a test for admin node. it("should deduplicate apps with the same platform and directory", async () => { const apps: App[] = [ { platform: Platform.WEB, directory: "web", frameworks: [Framework.REACT], appId: "app1" }, From 2ca702246da51f0d9d4656335f872756e3b5af83 Mon Sep 17 00:00:00 2001 From: Maneesh Tewani Date: Wed, 5 Nov 2025 09:58:26 -0800 Subject: [PATCH 9/9] Fixed test --- src/appUtils.spec.ts | 1 + src/appUtils.ts | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/appUtils.spec.ts b/src/appUtils.spec.ts index 3248469121b..78ac9276d90 100644 --- a/src/appUtils.spec.ts +++ b/src/appUtils.spec.ts @@ -678,6 +678,7 @@ describe("appUtils", () => { { platform: Platform.WEB, directory: ".", + frameworks: [], }, ]); }); diff --git a/src/appUtils.ts b/src/appUtils.ts index ab238780465..31dc79f4d54 100644 --- a/src/appUtils.ts +++ b/src/appUtils.ts @@ -66,6 +66,8 @@ export async function detectApps(dirPath: string): Promise { const adminAndWebApps = ( await Promise.all(packageJsonFiles.map((p) => packageJsonToAdminOrWebApp(dirPath, p))) ).flat(); + console.log("packageJsonFiles", packageJsonFiles); + console.log("adminAndWebApps", adminAndWebApps); const flutterAppPromises = await Promise.all( pubSpecYamlFiles.map((f) => processFlutterDir(dirPath, f)), @@ -188,7 +190,7 @@ async function packageJsonToAdminOrWebApp( directory: path.dirname(packageJsonFile), }); } - if (allDeps.includes("firebase") || detectApps.length === 0) { + if (allDeps.includes("firebase") || detectedApps.length === 0) { detectedApps.push({ platform: Platform.WEB, directory: path.dirname(packageJsonFile),