firebase-dataconnect-bootstrap 1.2.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/README.md CHANGED
@@ -20,7 +20,28 @@ npx firebase-dataconnect-bootstrap
20
20
  ```
21
21
 
22
22
  対話モードは日本語で質問が表示されます。
23
- 同じリポジトリで再実行すると、保存済み設定を初期値として設定変更できます。
23
+ 同じリポジトリで再実行すると、保存済み設定を初期値として設定変更できます。
24
+ 既存設定がある場合は、対話中に「新しいコレクション設定を追加するか」を選べます。
25
+
26
+ ## 追加コレクションを非対話で追加する例
27
+
28
+ ```bash
29
+ npx firebase-dataconnect-bootstrap \
30
+ --yes \
31
+ --add-collection \
32
+ --target . \
33
+ --project your-firebase-project-id \
34
+ --document 'articles/{articleId}' \
35
+ --function onArticlesWritten \
36
+ --vector-search \
37
+ --vector-collection 'articles' \
38
+ --source-text-field 'body' \
39
+ --vector-field 'embedding' \
40
+ --search-fields 'title,body,updatedAt' \
41
+ --search-function 'searchArticlesByVector' \
42
+ --top-k 8 \
43
+ --no-install
44
+ ```
24
45
 
25
46
  ## 非対話モード例
26
47
 
@@ -47,6 +68,7 @@ npx firebase-dataconnect-bootstrap \
47
68
  ## 主なオプション
48
69
 
49
70
  - `--config <name>`: 設定保存ファイル名(既定: `.firebase-dataconnect-bootstrap.json`)
71
+ - `--add-collection`: 再実行時に既存設定へ新しいコレクション設定を追加
50
72
  - `--vector-search` / `--no-vector-search`: ベクトル検索 scaffold の有効/無効
51
73
  - `--vector-collection <path>`: 検索対象コレクション
52
74
  - `--source-text-field <name>`: 埋め込み更新対象のテキストフィールド
@@ -55,13 +77,13 @@ npx firebase-dataconnect-bootstrap \
55
77
  - `--search-function <name>`: 生成する `onRequest` 関数名
56
78
  - `--top-k <number>`: 検索時のデフォルト上位件数
57
79
 
58
- ## 生成される主な関数
80
+ ## 生成される主な関数(コレクションごと)
59
81
 
60
- - `firestoreOnDocumentWritten.*`
82
+ - `onDocumentWritten_<functionName>.*`
61
83
  - `sourceTextField` の内容を `embedText` に渡し、`vectorField` へ保存
62
- - `vectorSearchOnRequest.*`
84
+ - `vectorSearchOnRequest_<searchFunctionName>.*`
63
85
  - HTTP リクエストの `vector`(または `query`)でコサイン類似度検索
64
- - `vectorSearchEmbedding.*`
86
+ - `vectorSearchEmbedding_<functionName>.*`
65
87
  - `embedText` を実装する差し替えポイント(デフォルトは `null` を返す)
66
88
 
67
89
  ## npm モジュールとして検索クライアントを使う
package/dist/cli.js CHANGED
@@ -15,6 +15,7 @@ Options:
15
15
  --document <path> Firestore document path (default: meetingSummaries/{summaryId})
16
16
  --function <name> Function export name (default: onMeetingSummaryWritten)
17
17
  --config <name> Saved config filename (default: .firebase-dataconnect-bootstrap.json)
18
+ --add-collection Add a new collection setting on re-run
18
19
  --install Run npm install in functions directory
19
20
  --no-install Skip npm install in functions directory
20
21
  --vector-search Enable vector search scaffolding
@@ -40,6 +41,7 @@ function parseArgs(argv) {
40
41
  installDependencies: undefined,
41
42
  configFileName: undefined,
42
43
  vectorSearch: {},
44
+ addCollection: undefined,
43
45
  yes: false,
44
46
  help: false
45
47
  };
@@ -69,6 +71,10 @@ function parseArgs(argv) {
69
71
  args.vectorSearch = { ...(args.vectorSearch ?? {}), enabled: false };
70
72
  continue;
71
73
  }
74
+ if (token === "--add-collection") {
75
+ args.addCollection = true;
76
+ continue;
77
+ }
72
78
  if (token.startsWith("--target=")) {
73
79
  args.targetDir = token.slice("--target=".length);
74
80
  continue;
package/dist/index.d.ts CHANGED
@@ -2,5 +2,5 @@ export { run } from "./main.js";
2
2
  export { runCli } from "./cli.js";
3
3
  export { collectConfig } from "./prompt.js";
4
4
  export { createVectorSearchClient, searchByVector } from "./search-client.js";
5
- export type { CliArgs, RunResult, SetupConfig, VectorSearchConfig } from "./types.js";
5
+ export type { CliArgs, PersistedBootstrapConfig, RunResult, SetupConfig, SetupTargetConfig, VectorSearchConfig } from "./types.js";
6
6
  export type { SearchByVectorInput, VectorSearchClientOptions, VectorSearchHit, VectorSearchResponse } from "./search-client.js";
package/dist/main.js CHANGED
@@ -73,6 +73,10 @@ function buildGeneratedImportPath(moduleStyle, extension, moduleBaseName) {
73
73
  }
74
74
  return `./${moduleBaseName}${extension}`;
75
75
  }
76
+ function toSafeFileSegment(value) {
77
+ const normalized = value.replace(/[^A-Za-z0-9_-]/g, "_");
78
+ return normalized || "generated";
79
+ }
76
80
  function buildDataconnectYaml(config) {
77
81
  return `specVersion: "v1alpha"
78
82
  serviceId: "${config.serviceId}"
@@ -155,7 +159,7 @@ export async function embedText(_text) {
155
159
  }
156
160
  `;
157
161
  }
158
- function buildFunctionModule({ style, config }) {
162
+ function buildFunctionModule({ style, config, embeddingImportPath }) {
159
163
  if (style === "cjs") {
160
164
  if (!config.vectorSearch.enabled) {
161
165
  return `const { onDocumentWritten } = require("firebase-functions/v2/firestore");
@@ -179,7 +183,7 @@ module.exports = { ${config.functionName} };
179
183
  }
180
184
  return `const { onDocumentWritten } = require("firebase-functions/v2/firestore");
181
185
  const logger = require("firebase-functions/logger");
182
- const { embedText } = require("./vectorSearchEmbedding");
186
+ const { embedText } = require("${embeddingImportPath}");
183
187
 
184
188
  const SOURCE_TEXT_FIELD = ${JSON.stringify(config.vectorSearch.sourceTextField)};
185
189
  const VECTOR_FIELD = ${JSON.stringify(config.vectorSearch.vectorField)};
@@ -243,7 +247,7 @@ export const ${config.functionName} = onDocumentWritten(
243
247
  }
244
248
  return `import { onDocumentWritten } from "firebase-functions/v2/firestore";
245
249
  import * as logger from "firebase-functions/logger";
246
- import { embedText } from "./vectorSearchEmbedding.js";
250
+ import { embedText } from "${embeddingImportPath}";
247
251
 
248
252
  const SOURCE_TEXT_FIELD = ${JSON.stringify(config.vectorSearch.sourceTextField)};
249
253
  const VECTOR_FIELD = ${JSON.stringify(config.vectorSearch.vectorField)};
@@ -285,9 +289,8 @@ export const ${config.functionName} = onDocumentWritten(
285
289
  );
286
290
  `;
287
291
  }
288
- function buildVectorSearchOnRequestModule(style, config, extension) {
292
+ function buildVectorSearchOnRequestModule(style, config, embeddingImportPath) {
289
293
  const returnFields = JSON.stringify(config.vectorSearch.returnFields);
290
- const embeddingImportPath = buildGeneratedImportPath(style, extension, "vectorSearchEmbedding");
291
294
  if (style === "cjs") {
292
295
  return `const { onRequest } = require("firebase-functions/v2/https");
293
296
  const logger = require("firebase-functions/logger");
@@ -619,13 +622,17 @@ async function resolveEntryContext(targetDir, entryRelativePath) {
619
622
  };
620
623
  }
621
624
  async function ensureFunctionExport(targetDir, config, context) {
622
- const generatedFileName = `firestoreOnDocumentWritten${context.extension}`;
625
+ const onWriteModuleBaseName = `onDocumentWritten_${toSafeFileSegment(config.functionName)}`;
626
+ const generatedFileName = `${onWriteModuleBaseName}${context.extension}`;
623
627
  const generatedRelativePath = path.join(path.dirname(context.entryRelativePath), generatedFileName);
624
- const generatedImportPath = buildGeneratedImportPath(context.moduleStyle, context.extension, "firestoreOnDocumentWritten");
628
+ const generatedImportPath = buildGeneratedImportPath(context.moduleStyle, context.extension, onWriteModuleBaseName);
629
+ const embeddingModuleBaseName = `vectorSearchEmbedding_${toSafeFileSegment(config.functionName)}`;
630
+ const embeddingImportPath = buildGeneratedImportPath(context.moduleStyle, context.extension, embeddingModuleBaseName);
625
631
  const generatedFullPath = path.join(targetDir, generatedRelativePath);
626
632
  const functionModuleText = buildFunctionModule({
627
633
  style: context.moduleStyle,
628
- config
634
+ config,
635
+ embeddingImportPath
629
636
  });
630
637
  await writeFile(generatedFullPath, functionModuleText, "utf8");
631
638
  const entryText = (await readFile(context.entryFullPath, "utf8")).trimEnd();
@@ -647,14 +654,17 @@ async function ensureVectorSearchScaffold(targetDir, config, context) {
647
654
  return null;
648
655
  }
649
656
  const folder = path.dirname(context.entryRelativePath);
650
- const onRequestFileName = `vectorSearchOnRequest${context.extension}`;
651
- const embeddingFileName = `vectorSearchEmbedding${context.extension}`;
657
+ const onRequestModuleBaseName = `vectorSearchOnRequest_${toSafeFileSegment(config.vectorSearch.functionName)}`;
658
+ const embeddingModuleBaseName = `vectorSearchEmbedding_${toSafeFileSegment(config.functionName)}`;
659
+ const onRequestFileName = `${onRequestModuleBaseName}${context.extension}`;
660
+ const embeddingFileName = `${embeddingModuleBaseName}${context.extension}`;
652
661
  const onRequestRelativePath = path.join(folder, onRequestFileName);
653
662
  const embeddingRelativePath = path.join(folder, embeddingFileName);
654
663
  const onRequestFullPath = path.join(targetDir, onRequestRelativePath);
655
664
  const embeddingFullPath = path.join(targetDir, embeddingRelativePath);
656
- const onRequestImportPath = buildGeneratedImportPath(context.moduleStyle, context.extension, "vectorSearchOnRequest");
657
- await writeFile(onRequestFullPath, buildVectorSearchOnRequestModule(context.moduleStyle, config, context.extension), "utf8");
665
+ const embeddingImportPath = buildGeneratedImportPath(context.moduleStyle, context.extension, embeddingModuleBaseName);
666
+ const onRequestImportPath = buildGeneratedImportPath(context.moduleStyle, context.extension, onRequestModuleBaseName);
667
+ await writeFile(onRequestFullPath, buildVectorSearchOnRequestModule(context.moduleStyle, config, embeddingImportPath), "utf8");
658
668
  const embeddingCreated = await writeTextIfMissing(embeddingFullPath, buildEmbeddingModule(context.moduleStyle));
659
669
  const entryText = (await readFile(context.entryFullPath, "utf8")).trimEnd();
660
670
  const exportLine = context.moduleStyle === "cjs"
@@ -674,16 +684,28 @@ async function ensureVectorSearchScaffold(targetDir, config, context) {
674
684
  }
675
685
  async function persistBootstrapConfig(targetDir, config) {
676
686
  const filePath = path.join(targetDir, config.configFileName);
687
+ const currentTarget = {
688
+ documentPath: config.documentPath,
689
+ functionName: config.functionName,
690
+ vectorSearch: config.vectorSearch
691
+ };
692
+ const mergedTargets = [...config.existingTargets];
693
+ const existingIndex = mergedTargets.findIndex((target) => target.functionName === currentTarget.functionName);
694
+ if (existingIndex >= 0) {
695
+ mergedTargets[existingIndex] = currentTarget;
696
+ }
697
+ else {
698
+ mergedTargets.push(currentTarget);
699
+ }
677
700
  const persisted = {
678
701
  projectId: config.projectId,
679
702
  region: config.region,
680
703
  serviceId: config.serviceId,
681
704
  location: config.location,
682
- documentPath: config.documentPath,
683
- functionName: config.functionName,
684
705
  installDependencies: config.installDependencies,
685
706
  configFileName: config.configFileName,
686
- vectorSearch: config.vectorSearch
707
+ targets: mergedTargets,
708
+ lastTargetFunctionName: currentTarget.functionName
687
709
  };
688
710
  await writeJson(filePath, persisted);
689
711
  return toPosixPath(config.configFileName);
package/dist/prompt.js CHANGED
@@ -4,6 +4,8 @@ import { createInterface } from "node:readline/promises";
4
4
  import { stdin as input, stdout as output } from "node:process";
5
5
  const DEFAULT_CONFIG_FILE = ".firebase-dataconnect-bootstrap.json";
6
6
  const DEFAULT_RETURN_FIELDS = ["title", "summary", "createdAt"];
7
+ const DEFAULT_DOCUMENT_PATH = "meetingSummaries/{summaryId}";
8
+ const NEW_COLLECTION_DOCUMENT_PATH = "newCollection/{docId}";
7
9
  function normalizeFunctionName(name) {
8
10
  const cleaned = name.replace(/[^A-Za-z0-9_$]/g, "_");
9
11
  if (!cleaned) {
@@ -37,13 +39,31 @@ function deriveCollectionPath(documentPath) {
37
39
  }
38
40
  return segments.join("/");
39
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
+ }
40
60
  function normalizeVectorSearch(partialVectorSearch, documentPath) {
41
61
  const source = partialVectorSearch ?? {};
42
62
  const collectionPath = source.collectionPath?.trim() || deriveCollectionPath(documentPath);
43
63
  const sourceTextField = source.sourceTextField?.trim() || "summary";
44
64
  const vectorField = source.vectorField?.trim() || "embedding";
45
- const returnFields = source.returnFields?.length ? source.returnFields : DEFAULT_RETURN_FIELDS;
46
- const functionName = normalizeFunctionName(source.functionName?.trim() || "searchByVector");
65
+ const returnFields = source.returnFields?.length ? source.returnFields : [...DEFAULT_RETURN_FIELDS];
66
+ const functionName = normalizeFunctionName(source.functionName?.trim() || deriveSearchFunctionName(documentPath));
47
67
  const rawTopK = typeof source.defaultTopK === "number" && Number.isFinite(source.defaultTopK)
48
68
  ? Math.floor(source.defaultTopK)
49
69
  : 5;
@@ -59,76 +79,145 @@ function normalizeVectorSearch(partialVectorSearch, documentPath) {
59
79
  defaultTopK
60
80
  };
61
81
  }
62
- function normalizeConfig(partial) {
63
- const targetDir = resolve(partial.targetDir ?? process.cwd());
64
- const projectId = partial.projectId?.trim() || "";
65
- const region = partial.region?.trim() || "us-central1";
66
- const serviceId = partial.serviceId?.trim() || `${projectId || "my-project"}-service`;
67
- const location = partial.location?.trim() || region;
68
- const documentPath = partial.documentPath?.trim() || "meetingSummaries/{summaryId}";
69
- const functionName = normalizeFunctionName(partial.functionName?.trim() || "onMeetingSummaryWritten");
70
- const configFileName = partial.configFileName?.trim() || DEFAULT_CONFIG_FILE;
71
- const installDependencies = typeof partial.installDependencies === "boolean"
72
- ? partial.installDependencies
73
- : true;
74
- const vectorSearch = normalizeVectorSearch(partial.vectorSearch, documentPath);
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);
75
86
  return {
76
- targetDir,
77
- projectId,
78
- region,
79
- serviceId,
80
- location,
81
87
  documentPath,
82
88
  functionName,
83
- installDependencies,
84
- configFileName,
85
89
  vectorSearch
86
90
  };
87
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
+ }
88
121
  async function loadSavedConfig(targetDir, configFileName) {
89
122
  try {
90
123
  const filePath = resolve(targetDir, configFileName);
91
- const raw = await readFile(filePath, "utf8");
92
- const parsed = JSON.parse(raw);
93
- if (typeof parsed !== "object" || parsed === null) {
94
- return {};
95
- }
96
- return parsed;
124
+ const rawText = await readFile(filePath, "utf8");
125
+ const parsed = JSON.parse(rawText);
126
+ return normalizeSavedConfig(parsed, configFileName);
97
127
  }
98
128
  catch {
99
- return {};
129
+ return normalizeSavedConfig({}, configFileName);
100
130
  }
101
131
  }
102
- function mergeConfig(parsed, saved, targetDir) {
103
- return {
104
- ...saved,
105
- targetDir,
106
- projectId: parsed.projectId ?? saved.projectId,
107
- region: parsed.region ?? saved.region,
108
- serviceId: parsed.serviceId ?? saved.serviceId,
109
- location: parsed.location ?? saved.location,
110
- documentPath: parsed.documentPath ?? saved.documentPath,
111
- functionName: parsed.functionName ?? saved.functionName,
112
- installDependencies: typeof parsed.installDependencies === "boolean"
113
- ? parsed.installDependencies
114
- : saved.installDependencies,
115
- configFileName: parsed.configFileName ?? saved.configFileName ?? DEFAULT_CONFIG_FILE,
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,
116
165
  vectorSearch: {
117
- ...(saved.vectorSearch ?? {}),
166
+ ...base.vectorSearch,
118
167
  ...(parsed.vectorSearch ?? {})
119
168
  }
120
- };
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
+ });
121
186
  }
122
187
  export async function collectConfig(parsed) {
123
188
  const targetDir = resolve(parsed.targetDir ?? process.cwd());
124
189
  const configFileName = parsed.configFileName?.trim() || DEFAULT_CONFIG_FILE;
125
190
  const saved = await loadSavedConfig(targetDir, configFileName);
126
- const defaults = normalizeConfig(mergeConfig({ ...parsed, targetDir, configFileName }, { ...saved, targetDir, configFileName: saved.configFileName ?? configFileName }, targetDir));
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;
127
198
  if (parsed.yes) {
128
- if (!defaults.projectId) {
199
+ if (!projectId) {
129
200
  throw new Error("`--yes` を使う場合は `--project` が必須です。");
130
201
  }
131
- 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
+ };
132
221
  }
133
222
  const rl = createInterface({ input, output });
134
223
  try {
@@ -153,46 +242,66 @@ export async function collectConfig(parsed) {
153
242
  };
154
243
  console.log("Firebase Data Connect / Cloud Functions セットアップ");
155
244
  console.log("");
156
- const targetDir = resolve(await ask("対象リポジトリのパス", defaults.targetDir, true));
157
- const projectId = await ask("Firebase プロジェクトID", defaults.projectId || "my-project", true);
158
- const region = await ask("Functions のリージョン", defaults.region, true);
159
- const serviceId = await ask("Data Connect サービスID", `${projectId}-service`, true);
160
- const location = await ask("Data Connect ロケーション", defaults.location || region, true);
161
- const documentPath = await ask("Firestore ドキュメントパス", defaults.documentPath, true);
162
- const rawFunctionName = await ask("関数のエクスポート名", defaults.functionName, true);
163
- const functionName = normalizeFunctionName(rawFunctionName);
164
- const installRaw = await ask("functions ディレクトリで npm install を実行しますか? (y/n)", defaults.installDependencies ? "y" : "n", true);
165
- const installDependencies = installRaw.toLowerCase().startsWith("y");
166
- const vectorEnabled = await askBoolean("ベクトル検索と onRequest 検索関数を有効化しますか? (y/n)", defaults.vectorSearch.enabled);
167
- let vectorCollectionPath = defaults.vectorSearch.collectionPath;
168
- let sourceTextField = defaults.vectorSearch.sourceTextField;
169
- let vectorField = defaults.vectorSearch.vectorField;
170
- let returnFields = defaults.vectorSearch.returnFields;
171
- let searchFunctionName = defaults.vectorSearch.functionName;
172
- let defaultTopK = defaults.vectorSearch.defaultTopK;
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;
173
283
  if (vectorEnabled) {
174
- vectorCollectionPath = await ask("ベクトル検索対象のコレクションパス", defaults.vectorSearch.collectionPath, true);
175
- sourceTextField = await ask("埋め込み作成対象のテキストフィールド", defaults.vectorSearch.sourceTextField, true);
176
- vectorField = await ask("ベクトル保存先フィールド名", defaults.vectorSearch.vectorField, true);
177
- const returnFieldsCsv = await ask("検索結果に返すフィールド(カンマ区切り)", toCsv(defaults.vectorSearch.returnFields), true);
178
- returnFields = parseCsvFields(returnFieldsCsv);
179
- searchFunctionName = normalizeFunctionName(await ask("onRequest 検索関数名", defaults.vectorSearch.functionName, true));
180
- const defaultTopKText = await ask("ベクトル検索のデフォルト topK", String(defaults.vectorSearch.defaultTopK), true);
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);
181
290
  defaultTopK = Number.parseInt(defaultTopKText, 10);
182
291
  if (Number.isNaN(defaultTopK) || defaultTopK <= 0) {
183
292
  throw new Error("topK は 1 以上の整数で入力してください。");
184
293
  }
185
294
  }
186
295
  return {
187
- targetDir,
188
- projectId,
189
- region,
190
- serviceId,
191
- location,
296
+ targetDir: selectedTargetDir,
297
+ projectId: selectedProjectId,
298
+ region: selectedRegion,
299
+ serviceId: selectedServiceId,
300
+ location: selectedLocation,
192
301
  documentPath,
193
302
  functionName,
194
- installDependencies,
195
- configFileName: defaults.configFileName,
303
+ installDependencies: selectedInstallDependencies,
304
+ configFileName,
196
305
  vectorSearch: {
197
306
  enabled: vectorEnabled,
198
307
  collectionPath: vectorCollectionPath,
@@ -201,7 +310,8 @@ export async function collectConfig(parsed) {
201
310
  returnFields,
202
311
  functionName: searchFunctionName,
203
312
  defaultTopK
204
- }
313
+ },
314
+ existingTargets: selectedTargetDir === targetDir ? saved.targets : []
205
315
  };
206
316
  }
207
317
  finally {
package/dist/types.d.ts CHANGED
@@ -9,6 +9,7 @@ export interface CliArgs {
9
9
  installDependencies?: boolean;
10
10
  configFileName?: string;
11
11
  vectorSearch?: Partial<VectorSearchConfig>;
12
+ addCollection?: boolean;
12
13
  yes: boolean;
13
14
  help: boolean;
14
15
  }
@@ -32,8 +33,24 @@ export interface SetupConfig {
32
33
  installDependencies: boolean;
33
34
  configFileName: string;
34
35
  vectorSearch: VectorSearchConfig;
36
+ existingTargets: SetupTargetConfig[];
35
37
  }
36
38
  export interface RunResult {
37
39
  targetDir: string;
38
40
  summaryLines: string[];
39
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,6 +1,6 @@
1
1
  {
2
2
  "name": "firebase-dataconnect-bootstrap",
3
- "version": "1.2.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": {