|
| 1 | +import { z } from "zod"; |
| 2 | +import * as path from "path"; |
| 3 | +import { tool } from "../../tool"; |
| 4 | +import { toContent } from "../../util"; |
| 5 | +import * as client from "../../../dataconnect/client"; |
| 6 | +import { loadAll } from "../../../dataconnect/load"; |
| 7 | +import { Service, Schema, ServiceInfo, Connector } from "../../../dataconnect/types"; |
| 8 | +import { dump } from "js-yaml"; |
| 9 | +import { logger } from "../../../logger"; |
| 10 | + |
| 11 | +interface CombinedServiceInfo { |
| 12 | + local?: ServiceInfo; |
| 13 | + deployed?: DeployServiceInfo; |
| 14 | +} |
| 15 | + |
| 16 | +interface DeployServiceInfo { |
| 17 | + service?: Service; |
| 18 | + schemas?: Schema[]; |
| 19 | + connectors?: Connector[]; |
| 20 | +} |
| 21 | + |
| 22 | +export const info = tool( |
| 23 | + { |
| 24 | + name: "info", |
| 25 | + description: "Get information about the Firebase Data Connect local and deployed resources.", |
| 26 | + inputSchema: z.object({}), |
| 27 | + annotations: { |
| 28 | + title: "Get information about Firebase Data Connect", |
| 29 | + readOnlyHint: true, |
| 30 | + }, |
| 31 | + _meta: { |
| 32 | + requiresProject: false, |
| 33 | + requiresAuth: false, |
| 34 | + }, |
| 35 | + }, |
| 36 | + async (_, { projectId, config }) => { |
| 37 | + const localServiceInfos = await loadAll(projectId, config); |
| 38 | + const serviceInfos = new Map<string, CombinedServiceInfo>(); |
| 39 | + |
| 40 | + for (const l of localServiceInfos) { |
| 41 | + serviceInfos.set( |
| 42 | + `locations/${l.dataConnectYaml.location}/services/${l.dataConnectYaml.serviceId}`, |
| 43 | + { local: l }, |
| 44 | + ); |
| 45 | + } |
| 46 | + |
| 47 | + if (projectId) { |
| 48 | + try { |
| 49 | + const [services, schemas, connectors] = await Promise.all([ |
| 50 | + client.listAllServices(projectId), |
| 51 | + client.listSchemas(`projects/${projectId}/locations/-/services/-`), |
| 52 | + client.listConnectors(`projects/${projectId}/locations/-/services/-`), |
| 53 | + ]); |
| 54 | + console.log(services, schemas, connectors); |
| 55 | + for (const s of services) { |
| 56 | + const k = s.name.split("/").slice(2, 6).join("/"); |
| 57 | + const st = serviceInfos.get(k) || {}; |
| 58 | + st.deployed = st.deployed || {}; |
| 59 | + st.deployed.service = s; |
| 60 | + serviceInfos.set(k, st); |
| 61 | + } |
| 62 | + for (const s of schemas) { |
| 63 | + const k = s.name.split("/").slice(2, 6).join("/"); |
| 64 | + const st = serviceInfos.get(k) || {}; |
| 65 | + st.deployed = st.deployed || {}; |
| 66 | + st.deployed.schemas = st.deployed.schemas || []; |
| 67 | + st.deployed.schemas.push(s); |
| 68 | + serviceInfos.set(k, st); |
| 69 | + } |
| 70 | + for (const s of connectors) { |
| 71 | + const k = s.name.split("/").slice(2, 6).join("/"); |
| 72 | + const st = serviceInfos.get(k) || {}; |
| 73 | + st.deployed = st.deployed || {}; |
| 74 | + st.deployed.connectors = st.deployed.connectors || []; |
| 75 | + st.deployed.connectors.push(s); |
| 76 | + serviceInfos.set(k, st); |
| 77 | + } |
| 78 | + } catch (e: any) { |
| 79 | + logger.debug("cannot fetch dataconnect resources in the backend", e); |
| 80 | + } |
| 81 | + } |
| 82 | + |
| 83 | + const localServices = Array.from(serviceInfos.values()).filter((s) => s.local); |
| 84 | + const remoteOnlyServices = Array.from(serviceInfos.values()).filter((s) => !s.local); |
| 85 | + |
| 86 | + const output: string[] = []; |
| 87 | + |
| 88 | + function includeDeployedServiceInfo(deployed: DeployServiceInfo): void { |
| 89 | + if (deployed.schemas?.length) { |
| 90 | + output.push(`### Schemas`); |
| 91 | + for (const s of deployed.schemas) { |
| 92 | + clearCCFEFields(s); |
| 93 | + output.push(dump(s)); |
| 94 | + } |
| 95 | + } |
| 96 | + if (deployed.connectors?.length) { |
| 97 | + output.push(`### Connectors`); |
| 98 | + for (const c of deployed.connectors) { |
| 99 | + clearCCFEFields(c); |
| 100 | + output.push(dump(c)); |
| 101 | + } |
| 102 | + } |
| 103 | + } |
| 104 | + |
| 105 | + if (localServices.length) { |
| 106 | + output.push(`# Local Data Connect Sources`); |
| 107 | + for (const s of localServices) { |
| 108 | + const local = s.local!; |
| 109 | + output.push(dump(local.dataConnectYaml)); |
| 110 | + const schemaDir = path.join(local.sourceDirectory, local.dataConnectYaml.schema.source); |
| 111 | + output.push(`You can find all of schema sources under ${schemaDir}/`); |
| 112 | + if (s.deployed) { |
| 113 | + output.push(`It's already deployed in the backend:\n`); |
| 114 | + includeDeployedServiceInfo(s.deployed); |
| 115 | + } |
| 116 | + } |
| 117 | + } |
| 118 | + |
| 119 | + if (remoteOnlyServices.length) { |
| 120 | + output.push(`# Data Connect Services in project ${projectId}`); |
| 121 | + for (const s of remoteOnlyServices) { |
| 122 | + if (s.deployed) { |
| 123 | + includeDeployedServiceInfo(s.deployed); |
| 124 | + } |
| 125 | + } |
| 126 | + } |
| 127 | + |
| 128 | + output.push(`\n# What's next?`); |
| 129 | + if (!localServices.length) { |
| 130 | + output.push( |
| 131 | + `- There is no local Data Connect service in the local workspace. Consider use the \`firebase_init\` MCP tool to setup one.`, |
| 132 | + ); |
| 133 | + } |
| 134 | + output.push( |
| 135 | + `- You can use the \`dataconnect_compile\` tool to compile all local Data Connect schemas and query sources.`, |
| 136 | + ); |
| 137 | + output.push( |
| 138 | + `- You run \`firebase deploy\` in command line to deploy the Data Connect schemas, connector and perform SQL migrations.`, |
| 139 | + ); |
| 140 | + return toContent(output.join("\n")); |
| 141 | + }, |
| 142 | +); |
| 143 | + |
| 144 | +function clearCCFEFields(r: any): void { |
| 145 | + const fieldsToClear = ["updateTime", "uid", "etag"]; |
| 146 | + for (const k of fieldsToClear) { |
| 147 | + delete r[k]; |
| 148 | + } |
| 149 | +} |
0 commit comments