firebase-dataconnect-bootstrap 1.1.0 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/prompt.js CHANGED
@@ -1,6 +1,11 @@
1
1
  import { resolve } from "node:path";
2
+ import { readFile } from "node:fs/promises";
2
3
  import { createInterface } from "node:readline/promises";
3
4
  import { stdin as input, stdout as output } from "node:process";
5
+ const DEFAULT_CONFIG_FILE = ".firebase-dataconnect-bootstrap.json";
6
+ const DEFAULT_RETURN_FIELDS = ["title", "summary", "createdAt"];
7
+ const DEFAULT_DOCUMENT_PATH = "meetingSummaries/{summaryId}";
8
+ const NEW_COLLECTION_DOCUMENT_PATH = "newCollection/{docId}";
4
9
  function normalizeFunctionName(name) {
5
10
  const cleaned = name.replace(/[^A-Za-z0-9_$]/g, "_");
6
11
  if (!cleaned) {
@@ -11,33 +16,208 @@ function normalizeFunctionName(name) {
11
16
  }
12
17
  return `fn_${cleaned}`;
13
18
  }
14
- function normalizeConfig(partial) {
15
- const targetDir = resolve(partial.targetDir ?? process.cwd());
16
- const region = partial.region?.trim() || "us-central1";
17
- const projectId = partial.projectId?.trim() || "";
18
- const serviceId = partial.serviceId?.trim() || `${projectId || "my-project"}-service`;
19
- const location = partial.location?.trim() || region;
20
- const documentPath = partial.documentPath?.trim() || "meetingSummaries/{summaryId}";
21
- const functionName = normalizeFunctionName(partial.functionName?.trim() || "onMeetingSummaryWritten");
22
- const installDependencies = typeof partial.installDependencies === "boolean" ? partial.installDependencies : true;
19
+ function toCsv(fields) {
20
+ return fields.join(",");
21
+ }
22
+ function parseCsvFields(value) {
23
+ const fields = value
24
+ .split(",")
25
+ .map((field) => field.trim())
26
+ .filter(Boolean);
27
+ return fields.length > 0 ? fields : [...DEFAULT_RETURN_FIELDS];
28
+ }
29
+ function deriveCollectionPath(documentPath) {
30
+ const segments = documentPath.split("/").filter(Boolean);
31
+ if (segments.length === 0) {
32
+ return "meetingSummaries";
33
+ }
34
+ while (segments.length > 0 && segments[segments.length - 1].startsWith("{")) {
35
+ segments.pop();
36
+ }
37
+ if (segments.length === 0) {
38
+ return "meetingSummaries";
39
+ }
40
+ return segments.join("/");
41
+ }
42
+ function toPascalCase(input) {
43
+ const tokens = input
44
+ .split(/[^A-Za-z0-9]+/)
45
+ .map((token) => token.trim())
46
+ .filter(Boolean);
47
+ if (tokens.length === 0) {
48
+ return "Collection";
49
+ }
50
+ return tokens.map((token) => `${token[0].toUpperCase()}${token.slice(1)}`).join("");
51
+ }
52
+ function deriveWriteFunctionName(documentPath) {
53
+ const collection = deriveCollectionPath(documentPath);
54
+ return normalizeFunctionName(`on${toPascalCase(collection)}Written`);
55
+ }
56
+ function deriveSearchFunctionName(documentPath) {
57
+ const collection = deriveCollectionPath(documentPath);
58
+ return normalizeFunctionName(`search${toPascalCase(collection)}ByVector`);
59
+ }
60
+ function normalizeVectorSearch(partialVectorSearch, documentPath) {
61
+ const source = partialVectorSearch ?? {};
62
+ const collectionPath = source.collectionPath?.trim() || deriveCollectionPath(documentPath);
63
+ const sourceTextField = source.sourceTextField?.trim() || "summary";
64
+ const vectorField = source.vectorField?.trim() || "embedding";
65
+ const returnFields = source.returnFields?.length ? source.returnFields : [...DEFAULT_RETURN_FIELDS];
66
+ const functionName = normalizeFunctionName(source.functionName?.trim() || deriveSearchFunctionName(documentPath));
67
+ const rawTopK = typeof source.defaultTopK === "number" && Number.isFinite(source.defaultTopK)
68
+ ? Math.floor(source.defaultTopK)
69
+ : 5;
70
+ const defaultTopK = rawTopK > 0 ? rawTopK : 5;
71
+ const enabled = typeof source.enabled === "boolean" ? source.enabled : false;
72
+ return {
73
+ enabled,
74
+ collectionPath,
75
+ sourceTextField,
76
+ vectorField,
77
+ returnFields,
78
+ functionName,
79
+ defaultTopK
80
+ };
81
+ }
82
+ function normalizeTarget(partialTarget) {
83
+ const documentPath = partialTarget.documentPath?.trim() || DEFAULT_DOCUMENT_PATH;
84
+ const functionName = normalizeFunctionName(partialTarget.functionName?.trim() || deriveWriteFunctionName(documentPath));
85
+ const vectorSearch = normalizeVectorSearch(partialTarget.vectorSearch, documentPath);
23
86
  return {
24
- targetDir,
25
- projectId,
26
- region,
27
- serviceId,
28
- location,
29
87
  documentPath,
30
88
  functionName,
31
- installDependencies
89
+ vectorSearch
32
90
  };
33
91
  }
92
+ function normalizeSavedConfig(raw, configFileName) {
93
+ const source = typeof raw === "object" && raw !== null ? raw : {};
94
+ const rawTargets = Array.isArray(source.targets)
95
+ ? source.targets
96
+ : source.documentPath
97
+ ? [
98
+ {
99
+ documentPath: source.documentPath,
100
+ functionName: source.functionName,
101
+ vectorSearch: source.vectorSearch
102
+ }
103
+ ]
104
+ : [];
105
+ const targets = rawTargets
106
+ .filter((entry) => typeof entry === "object" && entry !== null)
107
+ .map((entry) => normalizeTarget(entry));
108
+ return {
109
+ projectId: typeof source.projectId === "string" ? source.projectId : "",
110
+ region: typeof source.region === "string" ? source.region : "us-central1",
111
+ serviceId: typeof source.serviceId === "string" ? source.serviceId : "",
112
+ location: typeof source.location === "string" ? source.location : "",
113
+ installDependencies: typeof source.installDependencies === "boolean" ? source.installDependencies : true,
114
+ configFileName: typeof source.configFileName === "string" && source.configFileName
115
+ ? source.configFileName
116
+ : configFileName,
117
+ targets,
118
+ lastTargetFunctionName: typeof source.lastTargetFunctionName === "string" ? source.lastTargetFunctionName : undefined
119
+ };
120
+ }
121
+ async function loadSavedConfig(targetDir, configFileName) {
122
+ try {
123
+ const filePath = resolve(targetDir, configFileName);
124
+ const rawText = await readFile(filePath, "utf8");
125
+ const parsed = JSON.parse(rawText);
126
+ return normalizeSavedConfig(parsed, configFileName);
127
+ }
128
+ catch {
129
+ return normalizeSavedConfig({}, configFileName);
130
+ }
131
+ }
132
+ function pickDefaultTarget(saved) {
133
+ if (saved.targets.length === 0) {
134
+ return normalizeTarget({});
135
+ }
136
+ if (saved.lastTargetFunctionName) {
137
+ const last = saved.targets.find((target) => target.functionName === saved.lastTargetFunctionName);
138
+ if (last) {
139
+ return last;
140
+ }
141
+ }
142
+ return saved.targets[0];
143
+ }
144
+ function hasTargetOverrides(parsed) {
145
+ if (parsed.documentPath || parsed.functionName) {
146
+ return true;
147
+ }
148
+ if (!parsed.vectorSearch) {
149
+ return false;
150
+ }
151
+ const vector = parsed.vectorSearch;
152
+ return Boolean(vector.enabled !== undefined ||
153
+ vector.collectionPath ||
154
+ vector.sourceTextField ||
155
+ vector.vectorField ||
156
+ vector.returnFields?.length ||
157
+ vector.functionName ||
158
+ vector.defaultTopK !== undefined);
159
+ }
160
+ function mergeTarget(base, parsed) {
161
+ return normalizeTarget({
162
+ ...base,
163
+ documentPath: parsed.documentPath ?? base.documentPath,
164
+ functionName: parsed.functionName ?? base.functionName,
165
+ vectorSearch: {
166
+ ...base.vectorSearch,
167
+ ...(parsed.vectorSearch ?? {})
168
+ }
169
+ });
170
+ }
171
+ function buildAddTargetDefaults(parsed) {
172
+ const documentPath = parsed.documentPath ?? NEW_COLLECTION_DOCUMENT_PATH;
173
+ return normalizeTarget({
174
+ documentPath,
175
+ functionName: parsed.functionName ?? deriveWriteFunctionName(documentPath),
176
+ vectorSearch: {
177
+ enabled: parsed.vectorSearch?.enabled ?? false,
178
+ collectionPath: parsed.vectorSearch?.collectionPath ?? deriveCollectionPath(documentPath),
179
+ sourceTextField: parsed.vectorSearch?.sourceTextField ?? "summary",
180
+ vectorField: parsed.vectorSearch?.vectorField ?? "embedding",
181
+ returnFields: parsed.vectorSearch?.returnFields ?? [...DEFAULT_RETURN_FIELDS],
182
+ functionName: parsed.vectorSearch?.functionName ?? deriveSearchFunctionName(documentPath),
183
+ defaultTopK: parsed.vectorSearch?.defaultTopK ?? 5
184
+ }
185
+ });
186
+ }
34
187
  export async function collectConfig(parsed) {
35
- const defaults = normalizeConfig(parsed);
188
+ const targetDir = resolve(parsed.targetDir ?? process.cwd());
189
+ const configFileName = parsed.configFileName?.trim() || DEFAULT_CONFIG_FILE;
190
+ const saved = await loadSavedConfig(targetDir, configFileName);
191
+ const projectId = parsed.projectId?.trim() || saved.projectId || "";
192
+ const region = parsed.region?.trim() || saved.region || "us-central1";
193
+ const serviceId = parsed.serviceId?.trim() || saved.serviceId || `${projectId || "my-project"}-service`;
194
+ const location = parsed.location?.trim() || saved.location || region;
195
+ const installDependencies = typeof parsed.installDependencies === "boolean" ? parsed.installDependencies : saved.installDependencies;
196
+ const defaultTarget = pickDefaultTarget(saved);
197
+ const addCollectionDefault = parsed.addCollection ?? false;
36
198
  if (parsed.yes) {
37
- if (!defaults.projectId) {
199
+ if (!projectId) {
38
200
  throw new Error("`--yes` を使う場合は `--project` が必須です。");
39
201
  }
40
- return defaults;
202
+ if (parsed.addCollection && !hasTargetOverrides(parsed)) {
203
+ throw new Error("`--add-collection --yes` を使う場合は --document など追加ターゲットの情報を指定してください。");
204
+ }
205
+ const resolvedTarget = parsed.addCollection
206
+ ? mergeTarget(buildAddTargetDefaults(parsed), parsed)
207
+ : mergeTarget(defaultTarget, parsed);
208
+ return {
209
+ targetDir,
210
+ projectId,
211
+ region,
212
+ serviceId,
213
+ location,
214
+ documentPath: resolvedTarget.documentPath,
215
+ functionName: resolvedTarget.functionName,
216
+ installDependencies,
217
+ configFileName,
218
+ vectorSearch: resolvedTarget.vectorSearch,
219
+ existingTargets: saved.targets
220
+ };
41
221
  }
42
222
  const rl = createInterface({ input, output });
43
223
  try {
@@ -49,27 +229,89 @@ export async function collectConfig(parsed) {
49
229
  }
50
230
  return value;
51
231
  };
232
+ const askBoolean = async (label, fallback) => {
233
+ const base = fallback ? "y" : "n";
234
+ const value = (await ask(label, base, true)).toLowerCase();
235
+ if (["y", "yes", "true", "1", "はい"].includes(value)) {
236
+ return true;
237
+ }
238
+ if (["n", "no", "false", "0", "いいえ"].includes(value)) {
239
+ return false;
240
+ }
241
+ throw new Error(`${label} は y か n を入力してください。`);
242
+ };
52
243
  console.log("Firebase Data Connect / Cloud Functions セットアップ");
53
244
  console.log("");
54
- const targetDir = resolve(await ask("対象リポジトリのパス", defaults.targetDir, true));
55
- const projectId = await ask("Firebase プロジェクトID", defaults.projectId || "my-project", true);
56
- const region = await ask("Functions のリージョン", defaults.region, true);
57
- const serviceId = await ask("Data Connect サービスID", `${projectId}-service`, true);
58
- const location = await ask("Data Connect ロケーション", defaults.location || region, true);
59
- const documentPath = await ask("Firestore ドキュメントパス", defaults.documentPath, true);
60
- const rawFunctionName = await ask("関数のエクスポート名", defaults.functionName, true);
61
- const functionName = normalizeFunctionName(rawFunctionName);
62
- const installRaw = await ask("functions ディレクトリで npm install を実行しますか? (y/n)", defaults.installDependencies ? "y" : "n", true);
63
- const installDependencies = installRaw.toLowerCase().startsWith("y");
245
+ const selectedTargetDir = resolve(await ask("対象リポジトリのパス", targetDir, true));
246
+ const selectedProjectId = await ask("Firebase プロジェクトID", projectId || "my-project", true);
247
+ const selectedRegion = await ask("Functions のリージョン", region, true);
248
+ const selectedServiceId = await ask("Data Connect サービスID", serviceId || `${selectedProjectId}-service`, true);
249
+ const selectedLocation = await ask("Data Connect ロケーション", location || selectedRegion, true);
250
+ const selectedInstallDependencies = await askBoolean("functions ディレクトリで npm install を実行しますか? (y/n)", installDependencies);
251
+ let addCollection = addCollectionDefault;
252
+ if (saved.targets.length > 0 && parsed.addCollection !== true) {
253
+ addCollection = await askBoolean("既存設定に新しいコレクション設定を追加しますか? (y/n)", true);
254
+ }
255
+ let targetDefaults = defaultTarget;
256
+ if (addCollection) {
257
+ targetDefaults = buildAddTargetDefaults(parsed);
258
+ }
259
+ else if (saved.targets.length > 1) {
260
+ console.log("");
261
+ console.log("既存ターゲット:");
262
+ saved.targets.forEach((target, index) => {
263
+ console.log(` ${index + 1}. ${target.functionName} (${target.documentPath})`);
264
+ });
265
+ const defaultIndex = Math.max(0, saved.targets.findIndex((target) => target.functionName === saved.lastTargetFunctionName));
266
+ const selectedIndexRaw = await ask("編集するターゲット番号", String(defaultIndex + 1), true);
267
+ const selectedIndex = Number.parseInt(selectedIndexRaw, 10);
268
+ if (!Number.isInteger(selectedIndex) || selectedIndex < 1 || selectedIndex > saved.targets.length) {
269
+ throw new Error("ターゲット番号が不正です。");
270
+ }
271
+ targetDefaults = saved.targets[selectedIndex - 1];
272
+ }
273
+ const baseTarget = mergeTarget(targetDefaults, parsed);
274
+ const documentPath = await ask("Firestore ドキュメントパス", baseTarget.documentPath, true);
275
+ const functionName = normalizeFunctionName(await ask("onDocumentWritten 関数のエクスポート名", baseTarget.functionName, true));
276
+ const vectorEnabled = await askBoolean("ベクトル検索と onRequest 検索関数を有効化しますか? (y/n)", baseTarget.vectorSearch.enabled);
277
+ let vectorCollectionPath = baseTarget.vectorSearch.collectionPath;
278
+ let sourceTextField = baseTarget.vectorSearch.sourceTextField;
279
+ let vectorField = baseTarget.vectorSearch.vectorField;
280
+ let returnFields = baseTarget.vectorSearch.returnFields;
281
+ let searchFunctionName = baseTarget.vectorSearch.functionName;
282
+ let defaultTopK = baseTarget.vectorSearch.defaultTopK;
283
+ if (vectorEnabled) {
284
+ vectorCollectionPath = await ask("ベクトル検索対象のコレクションパス", baseTarget.vectorSearch.collectionPath, true);
285
+ sourceTextField = await ask("埋め込み作成対象のテキストフィールド", baseTarget.vectorSearch.sourceTextField, true);
286
+ vectorField = await ask("ベクトル保存先フィールド名", baseTarget.vectorSearch.vectorField, true);
287
+ returnFields = parseCsvFields(await ask("検索結果に返すフィールド(カンマ区切り)", toCsv(baseTarget.vectorSearch.returnFields), true));
288
+ searchFunctionName = normalizeFunctionName(await ask("onRequest 検索関数名", baseTarget.vectorSearch.functionName, true));
289
+ const defaultTopKText = await ask("ベクトル検索のデフォルト topK", String(baseTarget.vectorSearch.defaultTopK), true);
290
+ defaultTopK = Number.parseInt(defaultTopKText, 10);
291
+ if (Number.isNaN(defaultTopK) || defaultTopK <= 0) {
292
+ throw new Error("topK は 1 以上の整数で入力してください。");
293
+ }
294
+ }
64
295
  return {
65
- targetDir,
66
- projectId,
67
- region,
68
- serviceId,
69
- location,
296
+ targetDir: selectedTargetDir,
297
+ projectId: selectedProjectId,
298
+ region: selectedRegion,
299
+ serviceId: selectedServiceId,
300
+ location: selectedLocation,
70
301
  documentPath,
71
302
  functionName,
72
- installDependencies
303
+ installDependencies: selectedInstallDependencies,
304
+ configFileName,
305
+ vectorSearch: {
306
+ enabled: vectorEnabled,
307
+ collectionPath: vectorCollectionPath,
308
+ sourceTextField,
309
+ vectorField,
310
+ returnFields,
311
+ functionName: searchFunctionName,
312
+ defaultTopK
313
+ },
314
+ existingTargets: selectedTargetDir === targetDir ? saved.targets : []
73
315
  };
74
316
  }
75
317
  finally {
@@ -0,0 +1,40 @@
1
+ export interface VectorSearchHit {
2
+ id: string;
3
+ score: number;
4
+ data: Record<string, unknown>;
5
+ }
6
+ export interface VectorSearchResponse {
7
+ total: number;
8
+ results: VectorSearchHit[];
9
+ }
10
+ export interface SearchByVectorInput {
11
+ endpoint: string;
12
+ vector?: number[];
13
+ query?: string;
14
+ topK?: number;
15
+ headers?: Record<string, string>;
16
+ signal?: AbortSignal;
17
+ }
18
+ interface MinimalResponse {
19
+ ok: boolean;
20
+ status: number;
21
+ statusText: string;
22
+ json(): Promise<unknown>;
23
+ text(): Promise<string>;
24
+ }
25
+ type MinimalFetch = (url: string, init: {
26
+ method: string;
27
+ headers: Record<string, string>;
28
+ body: string;
29
+ signal?: AbortSignal;
30
+ }) => Promise<MinimalResponse>;
31
+ export declare function searchByVector(input: SearchByVectorInput, fetchImpl?: MinimalFetch): Promise<VectorSearchResponse>;
32
+ export interface VectorSearchClientOptions {
33
+ endpoint: string;
34
+ headers?: Record<string, string>;
35
+ fetchImpl?: MinimalFetch;
36
+ }
37
+ export declare function createVectorSearchClient(options: VectorSearchClientOptions): {
38
+ search: (input: Omit<SearchByVectorInput, "endpoint">) => Promise<VectorSearchResponse>;
39
+ };
40
+ export {};
@@ -0,0 +1,56 @@
1
+ const defaultFetch = (url, init) => fetch(url, init);
2
+ function validateVector(vector) {
3
+ if (!vector) {
4
+ return undefined;
5
+ }
6
+ if (vector.length === 0 || vector.some((value) => !Number.isFinite(value))) {
7
+ throw new Error("`vector` must be a non-empty number array.");
8
+ }
9
+ return vector;
10
+ }
11
+ export async function searchByVector(input, fetchImpl = defaultFetch) {
12
+ const endpoint = input.endpoint.trim();
13
+ if (!endpoint) {
14
+ throw new Error("`endpoint` is required.");
15
+ }
16
+ const payload = {
17
+ topK: input.topK
18
+ };
19
+ const vector = validateVector(input.vector);
20
+ if (vector) {
21
+ payload.vector = vector;
22
+ }
23
+ if (input.query) {
24
+ payload.query = input.query;
25
+ }
26
+ const response = await fetchImpl(endpoint, {
27
+ method: "POST",
28
+ headers: {
29
+ "content-type": "application/json",
30
+ ...(input.headers ?? {})
31
+ },
32
+ body: JSON.stringify(payload),
33
+ signal: input.signal
34
+ });
35
+ if (!response.ok) {
36
+ const message = await response.text();
37
+ throw new Error(`Vector search request failed (${response.status} ${response.statusText}): ${message}`);
38
+ }
39
+ const body = (await response.json());
40
+ return {
41
+ total: typeof body.total === "number" ? body.total : 0,
42
+ results: Array.isArray(body.results) ? body.results : []
43
+ };
44
+ }
45
+ export function createVectorSearchClient(options) {
46
+ return {
47
+ search: (input) => searchByVector({
48
+ ...input,
49
+ endpoint: options.endpoint,
50
+ headers: {
51
+ ...(options.headers ?? {}),
52
+ ...(input.headers ?? {})
53
+ }
54
+ }, options.fetchImpl)
55
+ };
56
+ }
@@ -0,0 +1,56 @@
1
+ export interface CliArgs {
2
+ targetDir?: string;
3
+ projectId?: string;
4
+ region?: string;
5
+ serviceId?: string;
6
+ location?: string;
7
+ documentPath?: string;
8
+ functionName?: string;
9
+ installDependencies?: boolean;
10
+ configFileName?: string;
11
+ vectorSearch?: Partial<VectorSearchConfig>;
12
+ addCollection?: boolean;
13
+ yes: boolean;
14
+ help: boolean;
15
+ }
16
+ export interface VectorSearchConfig {
17
+ enabled: boolean;
18
+ collectionPath: string;
19
+ sourceTextField: string;
20
+ vectorField: string;
21
+ returnFields: string[];
22
+ functionName: string;
23
+ defaultTopK: number;
24
+ }
25
+ export interface SetupConfig {
26
+ targetDir: string;
27
+ projectId: string;
28
+ region: string;
29
+ serviceId: string;
30
+ location: string;
31
+ documentPath: string;
32
+ functionName: string;
33
+ installDependencies: boolean;
34
+ configFileName: string;
35
+ vectorSearch: VectorSearchConfig;
36
+ existingTargets: SetupTargetConfig[];
37
+ }
38
+ export interface RunResult {
39
+ targetDir: string;
40
+ summaryLines: string[];
41
+ }
42
+ export interface SetupTargetConfig {
43
+ documentPath: string;
44
+ functionName: string;
45
+ vectorSearch: VectorSearchConfig;
46
+ }
47
+ export interface PersistedBootstrapConfig {
48
+ projectId: string;
49
+ region: string;
50
+ serviceId: string;
51
+ location: string;
52
+ installDependencies: boolean;
53
+ configFileName: string;
54
+ targets: SetupTargetConfig[];
55
+ lastTargetFunctionName?: string;
56
+ }
package/package.json CHANGED
@@ -1,12 +1,24 @@
1
1
  {
2
2
  "name": "firebase-dataconnect-bootstrap",
3
- "version": "1.1.0",
3
+ "version": "1.2.1",
4
4
  "description": "Bootstrap Firebase Data Connect and Firestore onDocumentWritten function setup in any repository.",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "firebase-dataconnect-bootstrap": "bin/firebase-dataconnect-bootstrap.js"
8
8
  },
9
- "main": "dist/main.js",
9
+ "main": "dist/index.js",
10
+ "types": "dist/index.d.ts",
11
+ "exports": {
12
+ ".": {
13
+ "types": "./dist/index.d.ts",
14
+ "import": "./dist/index.js"
15
+ },
16
+ "./search-client": {
17
+ "types": "./dist/search-client.d.ts",
18
+ "import": "./dist/search-client.js"
19
+ },
20
+ "./package.json": "./package.json"
21
+ },
10
22
  "files": [
11
23
  "bin",
12
24
  "dist",
@@ -26,7 +38,8 @@
26
38
  "dataconnect",
27
39
  "firestore",
28
40
  "cloud-functions",
29
- "cli"
41
+ "cli",
42
+ "vector-search"
30
43
  ],
31
44
  "author": "",
32
45
  "license": "MIT",