Skip to content

Commit 0f06446

Browse files
authored
Extract a loadAll helper (#9027)
1 parent 3170b27 commit 0f06446

18 files changed

+157
-164
lines changed

src/commands/dataconnect-sdk-generate.ts

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,7 @@ import { Command } from "../command";
44
import { Options } from "../options";
55
import { DataConnectEmulator } from "../emulator/dataconnectEmulator";
66
import { needProjectId } from "../projectUtils";
7-
import { load } from "../dataconnect/load";
8-
import { readFirebaseJson } from "../dataconnect/fileUtils";
7+
import { loadAll } from "../dataconnect/load";
98
import { logger } from "../logger";
109
import { getProjectDefaultAccount } from "../auth";
1110

@@ -20,10 +19,9 @@ export const command = new Command("dataconnect:sdk:generate")
2019
.action(async (options: GenerateOptions) => {
2120
const projectId = needProjectId(options);
2221

23-
const services = readFirebaseJson(options.config);
24-
for (const service of services) {
25-
const configDir = service.source;
26-
const serviceInfo = await load(projectId, options.config, configDir);
22+
const serviceInfos = await loadAll(projectId, options.config);
23+
for (const serviceInfo of serviceInfos) {
24+
const configDir = serviceInfo.sourceDirectory;
2725
const hasGeneratables = serviceInfo.connectorInfo.some((c) => {
2826
return (
2927
c.connectorYaml.generate?.javascriptSdk ||

src/commands/dataconnect-sql-diff.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { Options } from "../options";
33
import { needProjectId } from "../projectUtils";
44
import { ensureApis } from "../dataconnect/ensureApis";
55
import { requirePermissions } from "../requirePermissions";
6-
import { pickService } from "../dataconnect/fileUtils";
6+
import { pickService } from "../dataconnect/load";
77
import { diffSchema } from "../dataconnect/schemaMigration";
88
import { requireAuth } from "../requireAuth";
99

src/commands/dataconnect-sql-grant.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { Options } from "../options";
33
import { needProjectId } from "../projectUtils";
44
import { ensureApis } from "../dataconnect/ensureApis";
55
import { requirePermissions } from "../requirePermissions";
6-
import { pickService } from "../dataconnect/fileUtils";
6+
import { pickService } from "../dataconnect/load";
77
import { grantRoleToUserInSchema } from "../dataconnect/schemaMigration";
88
import { requireAuth } from "../requireAuth";
99
import { FirebaseError } from "../error";

src/commands/dataconnect-sql-migrate.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Command } from "../command";
22
import { Options } from "../options";
33
import { needProjectId } from "../projectUtils";
4-
import { pickService } from "../dataconnect/fileUtils";
4+
import { pickService } from "../dataconnect/load";
55
import { FirebaseError } from "../error";
66
import { migrateSchema } from "../dataconnect/schemaMigration";
77
import { requireAuth } from "../requireAuth";

src/commands/dataconnect-sql-setup.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Command } from "../command";
22
import { Options } from "../options";
33
import { needProjectId } from "../projectUtils";
4-
import { pickService } from "../dataconnect/fileUtils";
4+
import { pickService } from "../dataconnect/load";
55
import { FirebaseError } from "../error";
66
import { requireAuth } from "../requireAuth";
77
import { requirePermissions } from "../requirePermissions";

src/commands/dataconnect-sql-shell.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { Options } from "../options";
77
import { needProjectId } from "../projectUtils";
88
import { ensureApis } from "../dataconnect/ensureApis";
99
import { requirePermissions } from "../requirePermissions";
10-
import { pickService } from "../dataconnect/fileUtils";
10+
import { pickService } from "../dataconnect/load";
1111
import { getIdentifiers } from "../dataconnect/schemaMigration";
1212
import { requireAuth } from "../requireAuth";
1313
import { getIAMUser } from "../gcp/cloudsql/connect";

src/dataconnect/fileUtils.ts

Lines changed: 1 addition & 126 deletions
Original file line numberDiff line numberDiff line change
@@ -1,134 +1,9 @@
11
import * as fs from "fs-extra";
22
import * as path from "path";
3-
import * as clc from "colorette";
4-
import { glob } from "glob";
53

6-
import { FirebaseError } from "../error";
7-
import {
8-
ConnectorYaml,
9-
DataConnectYaml,
10-
File,
11-
Platform,
12-
ServiceInfo,
13-
SupportedFrameworks,
14-
} from "./types";
15-
import { readFileFromDirectory, wrappedSafeLoad } from "../utils";
16-
import { Config } from "../config";
17-
import { DataConnectMultiple } from "../firebaseConfig";
18-
import { load } from "./load";
4+
import { Platform, SupportedFrameworks } from "./types";
195
import { PackageJSON } from "../frameworks/compose/discover/runtime/node";
206

21-
export function readFirebaseJson(config?: Config): DataConnectMultiple {
22-
if (!config?.has("dataconnect")) {
23-
return [];
24-
}
25-
const validator = (cfg: any) => {
26-
if (!cfg["source"]) {
27-
throw new FirebaseError("Invalid firebase.json: DataConnect requires `source`");
28-
}
29-
return {
30-
source: cfg["source"],
31-
};
32-
};
33-
const configs = config.get("dataconnect");
34-
if (typeof configs === "object" && !Array.isArray(configs)) {
35-
return [validator(configs)];
36-
} else if (Array.isArray(configs)) {
37-
return configs.map(validator);
38-
} else {
39-
throw new FirebaseError(
40-
"Invalid firebase.json: dataconnect should be of the form { source: string }",
41-
);
42-
}
43-
}
44-
45-
export async function readDataConnectYaml(sourceDirectory: string): Promise<DataConnectYaml> {
46-
const file = await readFileFromDirectory(sourceDirectory, "dataconnect.yaml");
47-
const dataconnectYaml = await wrappedSafeLoad(file.source);
48-
return validateDataConnectYaml(dataconnectYaml);
49-
}
50-
51-
function validateDataConnectYaml(unvalidated: any): DataConnectYaml {
52-
// TODO: Use json schema for validation here!
53-
if (!unvalidated["location"]) {
54-
throw new FirebaseError("Missing required field 'location' in dataconnect.yaml");
55-
}
56-
return unvalidated as DataConnectYaml;
57-
}
58-
59-
export async function readConnectorYaml(sourceDirectory: string): Promise<ConnectorYaml> {
60-
const file = await readFileFromDirectory(sourceDirectory, "connector.yaml");
61-
const connectorYaml = await wrappedSafeLoad(file.source);
62-
return validateConnectorYaml(connectorYaml);
63-
}
64-
65-
function validateConnectorYaml(unvalidated: any): ConnectorYaml {
66-
// TODO: Add validation
67-
return unvalidated as ConnectorYaml;
68-
}
69-
70-
export async function readGQLFiles(sourceDir: string): Promise<File[]> {
71-
if (!fs.existsSync(sourceDir)) {
72-
return [];
73-
}
74-
const files = await glob("**/*.{gql,graphql}", { cwd: sourceDir, absolute: true, nodir: true });
75-
return files.map((f) => toFile(sourceDir, f));
76-
}
77-
78-
function toFile(sourceDir: string, fullPath: string): File {
79-
const relPath = path.relative(sourceDir, fullPath);
80-
if (!fs.existsSync(fullPath)) {
81-
throw new FirebaseError(`file ${fullPath} not found`);
82-
}
83-
const content = fs.readFileSync(fullPath).toString();
84-
return {
85-
path: relPath,
86-
content,
87-
};
88-
}
89-
90-
// pickService reads firebase.json and returns all services with a given serviceId.
91-
// If serviceID is not provided and there is a single service, return that.
92-
export async function pickService(
93-
projectId: string,
94-
config: Config,
95-
serviceId?: string,
96-
): Promise<ServiceInfo> {
97-
const serviceCfgs = readFirebaseJson(config);
98-
let serviceInfo: ServiceInfo;
99-
if (serviceCfgs.length === 0) {
100-
throw new FirebaseError(
101-
"No Data Connect services found in firebase.json." +
102-
`\nYou can run ${clc.bold("firebase init dataconnect")} to add a Data Connect service.`,
103-
);
104-
} else if (serviceCfgs.length === 1) {
105-
serviceInfo = await load(projectId, config, serviceCfgs[0].source);
106-
if (serviceId && serviceId !== serviceInfo.dataConnectYaml.serviceId) {
107-
throw new FirebaseError(
108-
`No service named ${serviceId} declared in firebase.json. Found ${serviceInfo.dataConnectYaml.serviceId}.` +
109-
`\nYou can run ${clc.bold("firebase init dataconnect")} to add this Data Connect service.`,
110-
);
111-
}
112-
return serviceInfo;
113-
} else {
114-
if (!serviceId) {
115-
throw new FirebaseError(
116-
"Multiple Data Connect services found in firebase.json. Please specify a service ID to use.",
117-
);
118-
}
119-
const infos = await Promise.all(serviceCfgs.map((c) => load(projectId, config, c.source)));
120-
// TODO: handle cases where there are services with the same ID in 2 locations.
121-
const maybe = infos.find((i) => i.dataConnectYaml.serviceId === serviceId);
122-
if (!maybe) {
123-
throw new FirebaseError(
124-
`No service named ${serviceId} declared in firebase.json. Found ${infos.map((i) => i.dataConnectYaml.serviceId).join(", ")}.` +
125-
`\nYou can run ${clc.bold("firebase init dataconnect")} to add this Data Connect service.`,
126-
);
127-
}
128-
return maybe;
129-
}
130-
}
131-
1327
// case insensitive exact match indicators for supported app platforms
1338
const WEB_INDICATORS = ["package.json", "package-lock.json", "node_modules"];
1349
const IOS_INDICATORS = ["info.plist", "podfile", "package.swift", ".xcodeproj"];

src/dataconnect/load.ts

Lines changed: 135 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,67 @@
11
import * as path from "path";
2-
import * as fileUtils from "./fileUtils";
2+
import * as fs from "fs-extra";
3+
import * as clc from "colorette";
4+
import { glob } from "glob";
35
import { Config } from "../config";
4-
import { ServiceInfo, toDatasource, SCHEMA_ID } from "./types";
6+
import { FirebaseError } from "../error";
7+
import {
8+
toDatasource,
9+
SCHEMA_ID,
10+
ConnectorYaml,
11+
DataConnectYaml,
12+
File,
13+
ServiceInfo,
14+
} from "./types";
15+
import { readFileFromDirectory, wrappedSafeLoad } from "../utils";
16+
import { DataConnectMultiple } from "../firebaseConfig";
17+
18+
// pickService reads firebase.json and returns all services with a given serviceId.
19+
// If serviceID is not provided and there is a single service, return that.
20+
export async function pickService(
21+
projectId: string,
22+
config: Config,
23+
serviceId?: string,
24+
): Promise<ServiceInfo> {
25+
const serviceInfos = await loadAll(projectId, config);
26+
if (serviceInfos.length === 0) {
27+
throw new FirebaseError(
28+
"No Data Connect services found in firebase.json." +
29+
`\nYou can run ${clc.bold("firebase init dataconnect")} to add a Data Connect service.`,
30+
);
31+
} else if (serviceInfos.length === 1) {
32+
if (serviceId && serviceId !== serviceInfos[0].dataConnectYaml.serviceId) {
33+
throw new FirebaseError(
34+
`No service named ${serviceId} declared in firebase.json. Found ${serviceInfos[0].dataConnectYaml.serviceId}.` +
35+
`\nYou can run ${clc.bold("firebase init dataconnect")} to add this Data Connect service.`,
36+
);
37+
}
38+
return serviceInfos[0];
39+
} else {
40+
if (!serviceId) {
41+
throw new FirebaseError(
42+
"Multiple Data Connect services found in firebase.json. Please specify a service ID to use.",
43+
);
44+
}
45+
// TODO: handle cases where there are services with the same ID in 2 locations.
46+
const maybe = serviceInfos.find((i) => i.dataConnectYaml.serviceId === serviceId);
47+
if (!maybe) {
48+
const serviceIds = serviceInfos.map((i) => i.dataConnectYaml.serviceId);
49+
throw new FirebaseError(
50+
`No service named ${serviceId} declared in firebase.json. Found ${serviceIds.join(", ")}.` +
51+
`\nYou can run ${clc.bold("firebase init dataconnect")} to add this Data Connect service.`,
52+
);
53+
}
54+
return maybe;
55+
}
56+
}
57+
58+
/**
59+
* Loads all Data Connect service configurations from the firebase.json file.
60+
*/
61+
export async function loadAll(projectId: string, config: Config): Promise<ServiceInfo[]> {
62+
const serviceCfgs = readFirebaseJson(config);
63+
return await Promise.all(serviceCfgs.map((c) => load(projectId, config, c.source)));
64+
}
565

666
/**
767
* loads schemas and connectors from {sourceDirectory}/dataconnect.yaml
@@ -13,15 +73,15 @@ export async function load(
1373
): Promise<ServiceInfo> {
1474
// TODO: better error handling when config read fails
1575
const resolvedDir = config.path(sourceDirectory);
16-
const dataConnectYaml = await fileUtils.readDataConnectYaml(resolvedDir);
76+
const dataConnectYaml = await readDataConnectYaml(resolvedDir);
1777
const serviceName = `projects/${projectId}/locations/${dataConnectYaml.location}/services/${dataConnectYaml.serviceId}`;
1878
const schemaDir = path.join(resolvedDir, dataConnectYaml.schema.source);
19-
const schemaGQLs = await fileUtils.readGQLFiles(schemaDir);
79+
const schemaGQLs = await readGQLFiles(schemaDir);
2080
const connectorInfo = await Promise.all(
2181
dataConnectYaml.connectorDirs.map(async (dir) => {
2282
const connectorDir = path.join(resolvedDir, dir);
23-
const connectorYaml = await fileUtils.readConnectorYaml(connectorDir);
24-
const connectorGqls = await fileUtils.readGQLFiles(connectorDir);
83+
const connectorYaml = await readConnectorYaml(connectorDir);
84+
const connectorGqls = await readGQLFiles(connectorDir);
2585
return {
2686
directory: connectorDir,
2787
connectorYaml,
@@ -51,3 +111,72 @@ export async function load(
51111
connectorInfo,
52112
};
53113
}
114+
115+
export function readFirebaseJson(config?: Config): DataConnectMultiple {
116+
if (!config?.has("dataconnect")) {
117+
return [];
118+
}
119+
const validator = (cfg: any) => {
120+
if (!cfg["source"]) {
121+
throw new FirebaseError("Invalid firebase.json: DataConnect requires `source`");
122+
}
123+
return {
124+
source: cfg["source"],
125+
};
126+
};
127+
const configs = config.get("dataconnect");
128+
if (typeof configs === "object" && !Array.isArray(configs)) {
129+
return [validator(configs)];
130+
} else if (Array.isArray(configs)) {
131+
return configs.map(validator);
132+
} else {
133+
throw new FirebaseError(
134+
"Invalid firebase.json: dataconnect should be of the form { source: string }",
135+
);
136+
}
137+
}
138+
139+
async function readDataConnectYaml(sourceDirectory: string): Promise<DataConnectYaml> {
140+
const file = await readFileFromDirectory(sourceDirectory, "dataconnect.yaml");
141+
const dataconnectYaml = await wrappedSafeLoad(file.source);
142+
return validateDataConnectYaml(dataconnectYaml);
143+
}
144+
145+
function validateDataConnectYaml(unvalidated: any): DataConnectYaml {
146+
// TODO: Use json schema for validation here!
147+
if (!unvalidated["location"]) {
148+
throw new FirebaseError("Missing required field 'location' in dataconnect.yaml");
149+
}
150+
return unvalidated as DataConnectYaml;
151+
}
152+
153+
async function readConnectorYaml(sourceDirectory: string): Promise<ConnectorYaml> {
154+
const file = await readFileFromDirectory(sourceDirectory, "connector.yaml");
155+
const connectorYaml = await wrappedSafeLoad(file.source);
156+
return validateConnectorYaml(connectorYaml);
157+
}
158+
159+
function validateConnectorYaml(unvalidated: any): ConnectorYaml {
160+
// TODO: Add validation
161+
return unvalidated as ConnectorYaml;
162+
}
163+
164+
async function readGQLFiles(sourceDir: string): Promise<File[]> {
165+
if (!fs.existsSync(sourceDir)) {
166+
return [];
167+
}
168+
const files = await glob("**/*.{gql,graphql}", { cwd: sourceDir, absolute: true, nodir: true });
169+
return files.map((f) => toFile(sourceDir, f));
170+
}
171+
172+
function toFile(sourceDir: string, fullPath: string): File {
173+
const relPath = path.relative(sourceDir, fullPath);
174+
if (!fs.existsSync(fullPath)) {
175+
throw new FirebaseError(`file ${fullPath} not found`);
176+
}
177+
const content = fs.readFileSync(fullPath).toString();
178+
return {
179+
path: relPath,
180+
content,
181+
};
182+
}

src/deploy/dataconnect/prepare.ts

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
import * as clc from "colorette";
22

33
import { DeployOptions } from "../";
4-
import { load } from "../../dataconnect/load";
5-
import { readFirebaseJson } from "../../dataconnect/fileUtils";
4+
import { loadAll } from "../../dataconnect/load";
65
import { logger } from "../../logger";
76
import * as utils from "../../utils";
87
import { needProjectId } from "../../projectUtils";
@@ -31,11 +30,8 @@ export default async function (context: any, options: DeployOptions): Promise<vo
3130
}
3231
await ensureApis(projectId);
3332
await requireTosAcceptance(DATA_CONNECT_TOS_ID)(options);
34-
const serviceCfgs = readFirebaseJson(options.config);
3533
const filters = getResourceFilters(options);
36-
const serviceInfos = await Promise.all(
37-
serviceCfgs.map((c) => load(projectId, options.config, c.source)),
38-
);
34+
const serviceInfos = await loadAll(projectId, options.config);
3935
for (const si of serviceInfos) {
4036
si.deploymentMetadata = await build(options, si.sourceDirectory, options.dryRun);
4137
}

src/emulator/controller.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ import { FirestoreEmulator, FirestoreEmulatorArgs } from "./firestoreEmulator";
5757
import { HostingEmulator } from "./hostingEmulator";
5858
import { PubsubEmulator } from "./pubsubEmulator";
5959
import { StorageEmulator } from "./storage";
60-
import { readFirebaseJson } from "../dataconnect/fileUtils";
60+
import { readFirebaseJson } from "../dataconnect/load";
6161
import { TasksEmulator } from "./tasksEmulator";
6262
import { AppHostingEmulator } from "./apphosting";
6363
import { sendVSCodeMessage, VSCODE_MESSAGE } from "../dataconnect/webhook";

0 commit comments

Comments
 (0)