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/README.md +71 -32
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +131 -2
- package/dist/index.d.ts +6 -0
- package/dist/index.js +4 -0
- package/dist/main.d.ts +2 -0
- package/dist/main.js +458 -34
- package/dist/prompt.d.ts +2 -0
- package/dist/prompt.js +276 -34
- package/dist/search-client.d.ts +40 -0
- package/dist/search-client.js +56 -0
- package/dist/types.d.ts +56 -0
- package/package.json +16 -3
package/dist/main.js
CHANGED
|
@@ -50,10 +50,7 @@ function appendIfMissing(content, appendLine) {
|
|
|
50
50
|
}
|
|
51
51
|
function detectModuleStyle(entryPath, packageJson) {
|
|
52
52
|
const ext = path.extname(entryPath);
|
|
53
|
-
if (ext === ".ts") {
|
|
54
|
-
return "esm";
|
|
55
|
-
}
|
|
56
|
-
if (ext === ".mjs") {
|
|
53
|
+
if (ext === ".ts" || ext === ".mjs") {
|
|
57
54
|
return "esm";
|
|
58
55
|
}
|
|
59
56
|
if (ext === ".cjs") {
|
|
@@ -64,6 +61,22 @@ function detectModuleStyle(entryPath, packageJson) {
|
|
|
64
61
|
}
|
|
65
62
|
return "cjs";
|
|
66
63
|
}
|
|
64
|
+
function buildGeneratedImportPath(moduleStyle, extension, moduleBaseName) {
|
|
65
|
+
if (moduleStyle === "cjs") {
|
|
66
|
+
if (extension === ".cjs") {
|
|
67
|
+
return `./${moduleBaseName}.cjs`;
|
|
68
|
+
}
|
|
69
|
+
return `./${moduleBaseName}`;
|
|
70
|
+
}
|
|
71
|
+
if (extension === ".ts") {
|
|
72
|
+
return `./${moduleBaseName}.js`;
|
|
73
|
+
}
|
|
74
|
+
return `./${moduleBaseName}${extension}`;
|
|
75
|
+
}
|
|
76
|
+
function toSafeFileSegment(value) {
|
|
77
|
+
const normalized = value.replace(/[^A-Za-z0-9_-]/g, "_");
|
|
78
|
+
return normalized || "generated";
|
|
79
|
+
}
|
|
67
80
|
function buildDataconnectYaml(config) {
|
|
68
81
|
return `specVersion: "v1alpha"
|
|
69
82
|
serviceId: "${config.serviceId}"
|
|
@@ -124,13 +137,36 @@ function buildMutationsGql() {
|
|
|
124
137
|
}
|
|
125
138
|
`;
|
|
126
139
|
}
|
|
127
|
-
function
|
|
140
|
+
function buildEmbeddingModule(style) {
|
|
128
141
|
if (style === "cjs") {
|
|
129
|
-
return
|
|
142
|
+
return `/**
|
|
143
|
+
* Replace this function with your embedding provider implementation.
|
|
144
|
+
* Return a numeric vector (number[]) for the given text.
|
|
145
|
+
*/
|
|
146
|
+
async function embedText(_text) {
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
module.exports = { embedText };
|
|
151
|
+
`;
|
|
152
|
+
}
|
|
153
|
+
return `/**
|
|
154
|
+
* Replace this function with your embedding provider implementation.
|
|
155
|
+
* Return a numeric vector (number[]) for the given text.
|
|
156
|
+
*/
|
|
157
|
+
export async function embedText(_text) {
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
`;
|
|
161
|
+
}
|
|
162
|
+
function buildFunctionModule({ style, config, embeddingImportPath }) {
|
|
163
|
+
if (style === "cjs") {
|
|
164
|
+
if (!config.vectorSearch.enabled) {
|
|
165
|
+
return `const { onDocumentWritten } = require("firebase-functions/v2/firestore");
|
|
130
166
|
const logger = require("firebase-functions/logger");
|
|
131
167
|
|
|
132
|
-
const ${functionName} = onDocumentWritten(
|
|
133
|
-
{ document: "${documentPath}", region: "${region}" },
|
|
168
|
+
const ${config.functionName} = onDocumentWritten(
|
|
169
|
+
{ document: "${config.documentPath}", region: "${config.region}" },
|
|
134
170
|
async (event) => {
|
|
135
171
|
logger.info("Firestore onDocumentWritten fired", {
|
|
136
172
|
params: event.params,
|
|
@@ -142,14 +178,61 @@ const ${functionName} = onDocumentWritten(
|
|
|
142
178
|
}
|
|
143
179
|
);
|
|
144
180
|
|
|
145
|
-
module.exports = { ${functionName} };
|
|
181
|
+
module.exports = { ${config.functionName} };
|
|
182
|
+
`;
|
|
183
|
+
}
|
|
184
|
+
return `const { onDocumentWritten } = require("firebase-functions/v2/firestore");
|
|
185
|
+
const logger = require("firebase-functions/logger");
|
|
186
|
+
const { embedText } = require("${embeddingImportPath}");
|
|
187
|
+
|
|
188
|
+
const SOURCE_TEXT_FIELD = ${JSON.stringify(config.vectorSearch.sourceTextField)};
|
|
189
|
+
const VECTOR_FIELD = ${JSON.stringify(config.vectorSearch.vectorField)};
|
|
190
|
+
|
|
191
|
+
const ${config.functionName} = onDocumentWritten(
|
|
192
|
+
{ document: "${config.documentPath}", region: "${config.region}" },
|
|
193
|
+
async (event) => {
|
|
194
|
+
if (!event.data?.after?.exists) {
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const afterData = event.data.after.data();
|
|
199
|
+
const sourceText = String(afterData?.[SOURCE_TEXT_FIELD] ?? "").trim();
|
|
200
|
+
if (!sourceText) {
|
|
201
|
+
logger.info("Skip embedding update because source text field is empty.", {
|
|
202
|
+
field: SOURCE_TEXT_FIELD,
|
|
203
|
+
document: event.data.after.ref.path
|
|
204
|
+
});
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const vector = await embedText(sourceText);
|
|
209
|
+
const validVector =
|
|
210
|
+
Array.isArray(vector) && vector.length > 0 && vector.every((value) => Number.isFinite(value));
|
|
211
|
+
if (!validVector) {
|
|
212
|
+
logger.warn("Embedding provider returned an invalid vector.", {
|
|
213
|
+
field: SOURCE_TEXT_FIELD,
|
|
214
|
+
document: event.data.after.ref.path
|
|
215
|
+
});
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
await event.data.after.ref.set({ [VECTOR_FIELD]: vector }, { merge: true });
|
|
220
|
+
logger.info("Updated vector field from source text.", {
|
|
221
|
+
field: VECTOR_FIELD,
|
|
222
|
+
document: event.data.after.ref.path
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
module.exports = { ${config.functionName} };
|
|
146
228
|
`;
|
|
147
229
|
}
|
|
148
|
-
|
|
230
|
+
if (!config.vectorSearch.enabled) {
|
|
231
|
+
return `import { onDocumentWritten } from "firebase-functions/v2/firestore";
|
|
149
232
|
import * as logger from "firebase-functions/logger";
|
|
150
233
|
|
|
151
|
-
export const ${functionName} = onDocumentWritten(
|
|
152
|
-
{ document: "${documentPath}", region: "${region}" },
|
|
234
|
+
export const ${config.functionName} = onDocumentWritten(
|
|
235
|
+
{ document: "${config.documentPath}", region: "${config.region}" },
|
|
153
236
|
async (event) => {
|
|
154
237
|
logger.info("Firestore onDocumentWritten fired", {
|
|
155
238
|
params: event.params,
|
|
@@ -160,6 +243,266 @@ export const ${functionName} = onDocumentWritten(
|
|
|
160
243
|
return;
|
|
161
244
|
}
|
|
162
245
|
);
|
|
246
|
+
`;
|
|
247
|
+
}
|
|
248
|
+
return `import { onDocumentWritten } from "firebase-functions/v2/firestore";
|
|
249
|
+
import * as logger from "firebase-functions/logger";
|
|
250
|
+
import { embedText } from "${embeddingImportPath}";
|
|
251
|
+
|
|
252
|
+
const SOURCE_TEXT_FIELD = ${JSON.stringify(config.vectorSearch.sourceTextField)};
|
|
253
|
+
const VECTOR_FIELD = ${JSON.stringify(config.vectorSearch.vectorField)};
|
|
254
|
+
|
|
255
|
+
export const ${config.functionName} = onDocumentWritten(
|
|
256
|
+
{ document: "${config.documentPath}", region: "${config.region}" },
|
|
257
|
+
async (event) => {
|
|
258
|
+
if (!event.data?.after?.exists) {
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const afterData = event.data.after.data();
|
|
263
|
+
const sourceText = String(afterData?.[SOURCE_TEXT_FIELD] ?? "").trim();
|
|
264
|
+
if (!sourceText) {
|
|
265
|
+
logger.info("Skip embedding update because source text field is empty.", {
|
|
266
|
+
field: SOURCE_TEXT_FIELD,
|
|
267
|
+
document: event.data.after.ref.path
|
|
268
|
+
});
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const vector = await embedText(sourceText);
|
|
273
|
+
const validVector =
|
|
274
|
+
Array.isArray(vector) && vector.length > 0 && vector.every((value) => Number.isFinite(value));
|
|
275
|
+
if (!validVector) {
|
|
276
|
+
logger.warn("Embedding provider returned an invalid vector.", {
|
|
277
|
+
field: SOURCE_TEXT_FIELD,
|
|
278
|
+
document: event.data.after.ref.path
|
|
279
|
+
});
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
await event.data.after.ref.set({ [VECTOR_FIELD]: vector }, { merge: true });
|
|
284
|
+
logger.info("Updated vector field from source text.", {
|
|
285
|
+
field: VECTOR_FIELD,
|
|
286
|
+
document: event.data.after.ref.path
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
);
|
|
290
|
+
`;
|
|
291
|
+
}
|
|
292
|
+
function buildVectorSearchOnRequestModule(style, config, embeddingImportPath) {
|
|
293
|
+
const returnFields = JSON.stringify(config.vectorSearch.returnFields);
|
|
294
|
+
if (style === "cjs") {
|
|
295
|
+
return `const { onRequest } = require("firebase-functions/v2/https");
|
|
296
|
+
const logger = require("firebase-functions/logger");
|
|
297
|
+
const admin = require("firebase-admin");
|
|
298
|
+
const { embedText } = require("${embeddingImportPath}");
|
|
299
|
+
|
|
300
|
+
if (admin.apps.length === 0) {
|
|
301
|
+
admin.initializeApp();
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const db = admin.firestore();
|
|
305
|
+
const COLLECTION_PATH = ${JSON.stringify(config.vectorSearch.collectionPath)};
|
|
306
|
+
const VECTOR_FIELD = ${JSON.stringify(config.vectorSearch.vectorField)};
|
|
307
|
+
const RETURN_FIELDS = ${returnFields};
|
|
308
|
+
const DEFAULT_TOP_K = ${config.vectorSearch.defaultTopK};
|
|
309
|
+
|
|
310
|
+
function normalizeVector(raw) {
|
|
311
|
+
if (!Array.isArray(raw)) {
|
|
312
|
+
return null;
|
|
313
|
+
}
|
|
314
|
+
if (raw.length === 0) {
|
|
315
|
+
return null;
|
|
316
|
+
}
|
|
317
|
+
const vector = raw.map((value) => Number(value));
|
|
318
|
+
if (vector.some((value) => !Number.isFinite(value))) {
|
|
319
|
+
return null;
|
|
320
|
+
}
|
|
321
|
+
return vector;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function cosineSimilarity(left, right) {
|
|
325
|
+
let dot = 0;
|
|
326
|
+
let leftNorm = 0;
|
|
327
|
+
let rightNorm = 0;
|
|
328
|
+
for (let i = 0; i < left.length; i += 1) {
|
|
329
|
+
dot += left[i] * right[i];
|
|
330
|
+
leftNorm += left[i] * left[i];
|
|
331
|
+
rightNorm += right[i] * right[i];
|
|
332
|
+
}
|
|
333
|
+
const denom = Math.sqrt(leftNorm) * Math.sqrt(rightNorm);
|
|
334
|
+
if (denom === 0) {
|
|
335
|
+
return Number.NaN;
|
|
336
|
+
}
|
|
337
|
+
return dot / denom;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function pickFields(data) {
|
|
341
|
+
const picked = {};
|
|
342
|
+
for (const field of RETURN_FIELDS) {
|
|
343
|
+
if (field in data) {
|
|
344
|
+
picked[field] = data[field];
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
return picked;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const ${config.vectorSearch.functionName} = onRequest(
|
|
351
|
+
{ region: ${JSON.stringify(config.region)}, cors: true },
|
|
352
|
+
async (req, res) => {
|
|
353
|
+
try {
|
|
354
|
+
const payload = req.method === "GET" ? req.query : req.body ?? {};
|
|
355
|
+
let queryVector = normalizeVector(payload.vector);
|
|
356
|
+
const queryText = typeof payload.query === "string" ? payload.query.trim() : "";
|
|
357
|
+
const rawTopK = Number.parseInt(String(payload.topK ?? DEFAULT_TOP_K), 10);
|
|
358
|
+
const topK = Number.isFinite(rawTopK) && rawTopK > 0 ? rawTopK : DEFAULT_TOP_K;
|
|
359
|
+
|
|
360
|
+
if (!queryVector && queryText) {
|
|
361
|
+
queryVector = normalizeVector(await embedText(queryText));
|
|
362
|
+
}
|
|
363
|
+
if (!queryVector) {
|
|
364
|
+
res.status(400).json({
|
|
365
|
+
error: "Provide 'vector' (number[]) or implement embedText and send 'query'."
|
|
366
|
+
});
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const snapshot = await db.collection(COLLECTION_PATH).get();
|
|
371
|
+
const scored = [];
|
|
372
|
+
snapshot.forEach((doc) => {
|
|
373
|
+
const data = doc.data();
|
|
374
|
+
const candidate = normalizeVector(data[VECTOR_FIELD]);
|
|
375
|
+
if (!candidate || candidate.length !== queryVector.length) {
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
const score = cosineSimilarity(queryVector, candidate);
|
|
379
|
+
if (!Number.isFinite(score)) {
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
scored.push({
|
|
383
|
+
id: doc.id,
|
|
384
|
+
score,
|
|
385
|
+
data: pickFields(data)
|
|
386
|
+
});
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
scored.sort((a, b) => b.score - a.score);
|
|
390
|
+
res.status(200).json({
|
|
391
|
+
total: scored.length,
|
|
392
|
+
results: scored.slice(0, topK)
|
|
393
|
+
});
|
|
394
|
+
} catch (error) {
|
|
395
|
+
logger.error("Vector search request failed", error);
|
|
396
|
+
res.status(500).json({ error: "Vector search failed." });
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
);
|
|
400
|
+
|
|
401
|
+
module.exports = { ${config.vectorSearch.functionName} };
|
|
402
|
+
`;
|
|
403
|
+
}
|
|
404
|
+
return `import { onRequest } from "firebase-functions/v2/https";
|
|
405
|
+
import * as logger from "firebase-functions/logger";
|
|
406
|
+
import admin from "firebase-admin";
|
|
407
|
+
import { embedText } from "${embeddingImportPath}";
|
|
408
|
+
|
|
409
|
+
if (admin.apps.length === 0) {
|
|
410
|
+
admin.initializeApp();
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const db = admin.firestore();
|
|
414
|
+
const COLLECTION_PATH = ${JSON.stringify(config.vectorSearch.collectionPath)};
|
|
415
|
+
const VECTOR_FIELD = ${JSON.stringify(config.vectorSearch.vectorField)};
|
|
416
|
+
const RETURN_FIELDS = ${returnFields};
|
|
417
|
+
const DEFAULT_TOP_K = ${config.vectorSearch.defaultTopK};
|
|
418
|
+
|
|
419
|
+
function normalizeVector(raw) {
|
|
420
|
+
if (!Array.isArray(raw) || raw.length === 0) {
|
|
421
|
+
return null;
|
|
422
|
+
}
|
|
423
|
+
const vector = raw.map((value) => Number(value));
|
|
424
|
+
if (vector.some((value) => !Number.isFinite(value))) {
|
|
425
|
+
return null;
|
|
426
|
+
}
|
|
427
|
+
return vector;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function cosineSimilarity(left, right) {
|
|
431
|
+
let dot = 0;
|
|
432
|
+
let leftNorm = 0;
|
|
433
|
+
let rightNorm = 0;
|
|
434
|
+
for (let i = 0; i < left.length; i += 1) {
|
|
435
|
+
dot += left[i] * right[i];
|
|
436
|
+
leftNorm += left[i] * left[i];
|
|
437
|
+
rightNorm += right[i] * right[i];
|
|
438
|
+
}
|
|
439
|
+
const denom = Math.sqrt(leftNorm) * Math.sqrt(rightNorm);
|
|
440
|
+
if (denom === 0) {
|
|
441
|
+
return Number.NaN;
|
|
442
|
+
}
|
|
443
|
+
return dot / denom;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
function pickFields(data) {
|
|
447
|
+
const picked = {};
|
|
448
|
+
for (const field of RETURN_FIELDS) {
|
|
449
|
+
if (field in data) {
|
|
450
|
+
picked[field] = data[field];
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
return picked;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
export const ${config.vectorSearch.functionName} = onRequest(
|
|
457
|
+
{ region: ${JSON.stringify(config.region)}, cors: true },
|
|
458
|
+
async (req, res) => {
|
|
459
|
+
try {
|
|
460
|
+
const payload = req.method === "GET" ? req.query : req.body ?? {};
|
|
461
|
+
let queryVector = normalizeVector(payload.vector);
|
|
462
|
+
const queryText = typeof payload.query === "string" ? payload.query.trim() : "";
|
|
463
|
+
const rawTopK = Number.parseInt(String(payload.topK ?? DEFAULT_TOP_K), 10);
|
|
464
|
+
const topK = Number.isFinite(rawTopK) && rawTopK > 0 ? rawTopK : DEFAULT_TOP_K;
|
|
465
|
+
|
|
466
|
+
if (!queryVector && queryText) {
|
|
467
|
+
queryVector = normalizeVector(await embedText(queryText));
|
|
468
|
+
}
|
|
469
|
+
if (!queryVector) {
|
|
470
|
+
res.status(400).json({
|
|
471
|
+
error: "Provide 'vector' (number[]) or implement embedText and send 'query'."
|
|
472
|
+
});
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
const snapshot = await db.collection(COLLECTION_PATH).get();
|
|
477
|
+
const scored = [];
|
|
478
|
+
snapshot.forEach((doc) => {
|
|
479
|
+
const data = doc.data();
|
|
480
|
+
const candidate = normalizeVector(data[VECTOR_FIELD]);
|
|
481
|
+
if (!candidate || candidate.length !== queryVector.length) {
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
const score = cosineSimilarity(queryVector, candidate);
|
|
485
|
+
if (!Number.isFinite(score)) {
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
scored.push({
|
|
489
|
+
id: doc.id,
|
|
490
|
+
score,
|
|
491
|
+
data: pickFields(data)
|
|
492
|
+
});
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
scored.sort((a, b) => b.score - a.score);
|
|
496
|
+
res.status(200).json({
|
|
497
|
+
total: scored.length,
|
|
498
|
+
results: scored.slice(0, topK)
|
|
499
|
+
});
|
|
500
|
+
} catch (error) {
|
|
501
|
+
logger.error("Vector search request failed", error);
|
|
502
|
+
res.status(500).json({ error: "Vector search failed." });
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
);
|
|
163
506
|
`;
|
|
164
507
|
}
|
|
165
508
|
async function ensureFirebaseRc(targetDir, projectId) {
|
|
@@ -266,41 +609,107 @@ admin.initializeApp();
|
|
|
266
609
|
`, "utf8");
|
|
267
610
|
return { entryPath: defaultEntry, created: defaultEntry };
|
|
268
611
|
}
|
|
269
|
-
async function
|
|
612
|
+
async function resolveEntryContext(targetDir, entryRelativePath) {
|
|
270
613
|
const functionsPackage = await readJsonIfExists(path.join(targetDir, "functions", "package.json"));
|
|
271
614
|
const entryFullPath = path.join(targetDir, entryRelativePath);
|
|
272
615
|
const moduleStyle = detectModuleStyle(entryFullPath, functionsPackage);
|
|
273
616
|
const extension = path.extname(entryRelativePath) || ".js";
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
617
|
+
return {
|
|
618
|
+
moduleStyle,
|
|
619
|
+
extension,
|
|
620
|
+
entryRelativePath,
|
|
621
|
+
entryFullPath
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
async function ensureFunctionExport(targetDir, config, context) {
|
|
625
|
+
const onWriteModuleBaseName = `onDocumentWritten_${toSafeFileSegment(config.functionName)}`;
|
|
626
|
+
const generatedFileName = `${onWriteModuleBaseName}${context.extension}`;
|
|
627
|
+
const generatedRelativePath = path.join(path.dirname(context.entryRelativePath), generatedFileName);
|
|
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);
|
|
282
631
|
const generatedFullPath = path.join(targetDir, generatedRelativePath);
|
|
283
632
|
const functionModuleText = buildFunctionModule({
|
|
284
|
-
style: moduleStyle,
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
region: config.region
|
|
633
|
+
style: context.moduleStyle,
|
|
634
|
+
config,
|
|
635
|
+
embeddingImportPath
|
|
288
636
|
});
|
|
289
637
|
await writeFile(generatedFullPath, functionModuleText, "utf8");
|
|
290
|
-
const entryText = (await readFile(entryFullPath, "utf8")).trimEnd();
|
|
291
|
-
const exportLine = moduleStyle === "cjs"
|
|
638
|
+
const entryText = (await readFile(context.entryFullPath, "utf8")).trimEnd();
|
|
639
|
+
const exportLine = context.moduleStyle === "cjs"
|
|
292
640
|
? `exports.${config.functionName} = require("${generatedImportPath}").${config.functionName};`
|
|
293
641
|
: `export { ${config.functionName} } from "${generatedImportPath}";`;
|
|
294
642
|
const updated = appendIfMissing(entryText, exportLine);
|
|
295
643
|
if (updated.changed) {
|
|
296
|
-
await writeFile(entryFullPath,
|
|
644
|
+
await writeFile(context.entryFullPath, updated.content, "utf8");
|
|
297
645
|
}
|
|
298
646
|
return {
|
|
299
647
|
generatedRelativePath: toPosixPath(generatedRelativePath),
|
|
300
|
-
entryRelativePath: toPosixPath(entryRelativePath),
|
|
648
|
+
entryRelativePath: toPosixPath(context.entryRelativePath),
|
|
301
649
|
entryUpdated: updated.changed
|
|
302
650
|
};
|
|
303
651
|
}
|
|
652
|
+
async function ensureVectorSearchScaffold(targetDir, config, context) {
|
|
653
|
+
if (!config.vectorSearch.enabled) {
|
|
654
|
+
return null;
|
|
655
|
+
}
|
|
656
|
+
const folder = path.dirname(context.entryRelativePath);
|
|
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}`;
|
|
661
|
+
const onRequestRelativePath = path.join(folder, onRequestFileName);
|
|
662
|
+
const embeddingRelativePath = path.join(folder, embeddingFileName);
|
|
663
|
+
const onRequestFullPath = path.join(targetDir, onRequestRelativePath);
|
|
664
|
+
const embeddingFullPath = path.join(targetDir, embeddingRelativePath);
|
|
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");
|
|
668
|
+
const embeddingCreated = await writeTextIfMissing(embeddingFullPath, buildEmbeddingModule(context.moduleStyle));
|
|
669
|
+
const entryText = (await readFile(context.entryFullPath, "utf8")).trimEnd();
|
|
670
|
+
const exportLine = context.moduleStyle === "cjs"
|
|
671
|
+
? `exports.${config.vectorSearch.functionName} = require("${onRequestImportPath}").${config.vectorSearch.functionName};`
|
|
672
|
+
: `export { ${config.vectorSearch.functionName} } from "${onRequestImportPath}";`;
|
|
673
|
+
const updated = appendIfMissing(entryText, exportLine);
|
|
674
|
+
if (updated.changed) {
|
|
675
|
+
await writeFile(context.entryFullPath, updated.content, "utf8");
|
|
676
|
+
}
|
|
677
|
+
return {
|
|
678
|
+
generatedRelativePath: toPosixPath(onRequestRelativePath),
|
|
679
|
+
embeddingRelativePath: toPosixPath(embeddingRelativePath),
|
|
680
|
+
entryRelativePath: toPosixPath(context.entryRelativePath),
|
|
681
|
+
entryUpdated: updated.changed,
|
|
682
|
+
embeddingCreated
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
async function persistBootstrapConfig(targetDir, config) {
|
|
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
|
+
}
|
|
700
|
+
const persisted = {
|
|
701
|
+
projectId: config.projectId,
|
|
702
|
+
region: config.region,
|
|
703
|
+
serviceId: config.serviceId,
|
|
704
|
+
location: config.location,
|
|
705
|
+
installDependencies: config.installDependencies,
|
|
706
|
+
configFileName: config.configFileName,
|
|
707
|
+
targets: mergedTargets,
|
|
708
|
+
lastTargetFunctionName: currentTarget.functionName
|
|
709
|
+
};
|
|
710
|
+
await writeJson(filePath, persisted);
|
|
711
|
+
return toPosixPath(config.configFileName);
|
|
712
|
+
}
|
|
304
713
|
async function runNpmInstall(functionsDir) {
|
|
305
714
|
await new Promise((resolvePromise, rejectPromise) => {
|
|
306
715
|
const child = spawn("npm", ["install"], {
|
|
@@ -350,11 +759,26 @@ export async function run(config) {
|
|
|
350
759
|
else {
|
|
351
760
|
summaryLines.push(`Using existing ${entryPath}`);
|
|
352
761
|
}
|
|
353
|
-
const
|
|
354
|
-
|
|
355
|
-
summaryLines.push(
|
|
356
|
-
|
|
357
|
-
|
|
762
|
+
const context = await resolveEntryContext(targetDir, entryPath);
|
|
763
|
+
const documentWriteResult = await ensureFunctionExport(targetDir, config, context);
|
|
764
|
+
summaryLines.push(`Updated ${documentWriteResult.generatedRelativePath}`);
|
|
765
|
+
summaryLines.push(documentWriteResult.entryUpdated
|
|
766
|
+
? `Appended export in ${documentWriteResult.entryRelativePath}`
|
|
767
|
+
: `Export already present in ${documentWriteResult.entryRelativePath}`);
|
|
768
|
+
const vectorSearchResult = await ensureVectorSearchScaffold(targetDir, config, context);
|
|
769
|
+
if (vectorSearchResult) {
|
|
770
|
+
summaryLines.push(`Updated ${vectorSearchResult.generatedRelativePath}`);
|
|
771
|
+
summaryLines.push(vectorSearchResult.embeddingCreated
|
|
772
|
+
? `Created ${vectorSearchResult.embeddingRelativePath}`
|
|
773
|
+
: `Using existing ${vectorSearchResult.embeddingRelativePath}`);
|
|
774
|
+
summaryLines.push(vectorSearchResult.entryUpdated
|
|
775
|
+
? `Appended export in ${vectorSearchResult.entryRelativePath}`
|
|
776
|
+
: `Vector search export already present in ${vectorSearchResult.entryRelativePath}`);
|
|
777
|
+
}
|
|
778
|
+
else {
|
|
779
|
+
summaryLines.push("Skipped vector search scaffolding");
|
|
780
|
+
}
|
|
781
|
+
summaryLines.push(`Updated ${await persistBootstrapConfig(targetDir, config)}`);
|
|
358
782
|
if (config.installDependencies) {
|
|
359
783
|
const functionsDir = path.join(targetDir, "functions");
|
|
360
784
|
await runNpmInstall(functionsDir);
|
package/dist/prompt.d.ts
ADDED