@talkpilot/core-db 1.3.44 → 1.3.45
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 +41 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/talkpilot/calls/calls.types.d.ts +1 -0
- package/dist/talkpilot/calls/calls.types.d.ts.map +1 -1
- package/dist/talkpilot/clientsConfig/clientsConfig.getters.d.ts +39 -0
- package/dist/talkpilot/clientsConfig/clientsConfig.getters.d.ts.map +1 -1
- package/dist/talkpilot/clientsConfig/clientsConfig.getters.js +92 -1
- package/dist/talkpilot/clientsConfig/clientsConfig.getters.js.map +1 -1
- package/dist/talkpilot/clientsConfig/clientsConfig.types.d.ts +4 -0
- package/dist/talkpilot/clientsConfig/clientsConfig.types.d.ts.map +1 -1
- package/dist/talkpilot/clientsConfig/index.d.ts +1 -0
- package/dist/talkpilot/clientsConfig/index.d.ts.map +1 -1
- package/dist/talkpilot/clientsConfig/index.js +1 -0
- package/dist/talkpilot/clientsConfig/index.js.map +1 -1
- package/dist/test-utils/factories/index.d.ts +1 -0
- package/dist/test-utils/factories/index.d.ts.map +1 -1
- package/dist/test-utils/factories/index.js +1 -0
- package/dist/test-utils/factories/index.js.map +1 -1
- package/dist/test-utils/factories/websitalk/scans.d.ts.map +1 -1
- package/dist/test-utils/factories/websitalk/scans.js +5 -0
- package/dist/test-utils/factories/websitalk/scans.js.map +1 -1
- package/dist/websitalk/index.d.ts +1 -0
- package/dist/websitalk/index.d.ts.map +1 -1
- package/dist/websitalk/index.js +1 -0
- package/dist/websitalk/index.js.map +1 -1
- package/dist/websitalk/knowledgeDocuments/knowledgeDocuments.types.d.ts +2 -4
- package/dist/websitalk/knowledgeDocuments/knowledgeDocuments.types.d.ts.map +1 -1
- package/dist/websitalk/scans/scans.getters.d.ts.map +1 -1
- package/dist/websitalk/scans/scans.getters.js +8 -2
- package/dist/websitalk/scans/scans.getters.js.map +1 -1
- package/dist/websitalk/scans/scans.types.d.ts +5 -0
- package/dist/websitalk/scans/scans.types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/index.ts +1 -0
- package/src/talkpilot/calls/calls.types.ts +1 -0
- package/src/talkpilot/clientsConfig/__tests__/clientsConfig.getters.spec.ts +318 -0
- package/src/talkpilot/clientsConfig/clientsConfig.constants.ts +2 -0
- package/src/talkpilot/clientsConfig/clientsConfig.getters.ts +116 -2
- package/src/talkpilot/clientsConfig/clientsConfig.types.ts +4 -0
- package/src/talkpilot/clientsConfig/index.ts +1 -0
- package/src/test-utils/factories/index.ts +1 -0
- package/src/test-utils/factories/websitalk/scanResources.ts +26 -0
- package/src/test-utils/factories/websitalk/scans.ts +5 -0
- package/src/websitalk/index.ts +1 -0
- package/src/websitalk/knowledgeDocuments/__tests__/knowledgeDocuments.spec.ts +0 -19
- package/src/websitalk/knowledgeDocuments/knowledgeDocuments.types.ts +2 -2
- package/src/websitalk/scanResources/__tests__/scanResources.spec.ts +136 -0
- package/src/websitalk/scanResources/index.ts +4 -0
- package/src/websitalk/scanResources/scanResources.constants.ts +47 -0
- package/src/websitalk/scanResources/scanResources.getters.ts +94 -0
- package/src/websitalk/scanResources/scanResources.types.ts +90 -0
- package/src/websitalk/scanResources/scanResources.utils.ts +28 -0
- package/src/websitalk/scans/scans.getters.ts +8 -2
- package/src/websitalk/scans/scans.types.ts +9 -0
|
@@ -5,7 +5,15 @@ import {
|
|
|
5
5
|
updateActiveScanId,
|
|
6
6
|
clearActiveScanId,
|
|
7
7
|
updateProductConfig,
|
|
8
|
+
getActiveScanIds,
|
|
9
|
+
addActiveScanId,
|
|
10
|
+
removeActiveScanId,
|
|
11
|
+
getPrimaryScanId,
|
|
12
|
+
setPrimaryScanId,
|
|
13
|
+
clearPrimaryScanId,
|
|
8
14
|
} from "../clientsConfig.getters";
|
|
15
|
+
import { MAX_ACTIVE_SCANS } from "../clientsConfig.constants";
|
|
16
|
+
import { createClientConfig } from "../../../test-utils/factories/talkpilot/clientsConfig";
|
|
9
17
|
|
|
10
18
|
describe("updateProductConfig", () => {
|
|
11
19
|
beforeEach(async () => {
|
|
@@ -103,3 +111,313 @@ describe("activeScanId", () => {
|
|
|
103
111
|
expect(await getActiveScanId(clientId)).toBeUndefined();
|
|
104
112
|
});
|
|
105
113
|
});
|
|
114
|
+
|
|
115
|
+
describe("activeScanIds (CP-1628)", () => {
|
|
116
|
+
beforeEach(async () => {
|
|
117
|
+
await getClientsConfigCollection().deleteMany({});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
afterEach(async () => {
|
|
121
|
+
await getClientsConfigCollection().deleteMany({});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("returns an empty list when nothing is set", async () => {
|
|
125
|
+
const clientId = "client-list-1";
|
|
126
|
+
await getClientsConfigCollection().insertOne(
|
|
127
|
+
createClientConfig({
|
|
128
|
+
clientId,
|
|
129
|
+
products: {
|
|
130
|
+
websiteTalk: { companyName: "X", timezone: "UTC", language: "he" },
|
|
131
|
+
},
|
|
132
|
+
}),
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
expect(await getActiveScanIds(clientId)).toEqual([]);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("falls back to wrapping the legacy activeScanId when activeScanIds is absent", async () => {
|
|
139
|
+
const clientId = "client-list-2";
|
|
140
|
+
await getClientsConfigCollection().insertOne(
|
|
141
|
+
createClientConfig({
|
|
142
|
+
clientId,
|
|
143
|
+
products: {
|
|
144
|
+
websiteTalk: {
|
|
145
|
+
companyName: "X",
|
|
146
|
+
timezone: "UTC",
|
|
147
|
+
language: "he",
|
|
148
|
+
activeScanId: "scan-legacy",
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
}),
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
expect(await getActiveScanIds(clientId)).toEqual(["scan-legacy"]);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it("adds a scan id and returns it from getActiveScanIds", async () => {
|
|
158
|
+
const clientId = "client-list-3";
|
|
159
|
+
await getClientsConfigCollection().insertOne(
|
|
160
|
+
createClientConfig({
|
|
161
|
+
clientId,
|
|
162
|
+
products: {
|
|
163
|
+
websiteTalk: { companyName: "X", timezone: "UTC", language: "he" },
|
|
164
|
+
},
|
|
165
|
+
}),
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
await addActiveScanId(clientId, "scan-a");
|
|
169
|
+
await addActiveScanId(clientId, "scan-b");
|
|
170
|
+
|
|
171
|
+
expect(await getActiveScanIds(clientId)).toEqual(["scan-a", "scan-b"]);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it("does not duplicate an already-present scan id, and does not count it against the cap", async () => {
|
|
175
|
+
const clientId = "client-list-4";
|
|
176
|
+
await getClientsConfigCollection().insertOne(
|
|
177
|
+
createClientConfig({
|
|
178
|
+
clientId,
|
|
179
|
+
products: {
|
|
180
|
+
websiteTalk: {
|
|
181
|
+
companyName: "X",
|
|
182
|
+
timezone: "UTC",
|
|
183
|
+
language: "he",
|
|
184
|
+
activeScanIds: ["scan-a", "scan-b", "scan-c"],
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
}),
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
await expect(addActiveScanId(clientId, "scan-a")).resolves.toBeUndefined();
|
|
191
|
+
expect(await getActiveScanIds(clientId)).toEqual([
|
|
192
|
+
"scan-a",
|
|
193
|
+
"scan-b",
|
|
194
|
+
"scan-c",
|
|
195
|
+
]);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it(`throws when adding a new scan id beyond MAX_ACTIVE_SCANS (${MAX_ACTIVE_SCANS})`, async () => {
|
|
199
|
+
const clientId = "client-list-5";
|
|
200
|
+
await getClientsConfigCollection().insertOne(
|
|
201
|
+
createClientConfig({
|
|
202
|
+
clientId,
|
|
203
|
+
products: {
|
|
204
|
+
websiteTalk: {
|
|
205
|
+
companyName: "X",
|
|
206
|
+
timezone: "UTC",
|
|
207
|
+
language: "he",
|
|
208
|
+
activeScanIds: ["scan-a", "scan-b", "scan-c"],
|
|
209
|
+
},
|
|
210
|
+
},
|
|
211
|
+
}),
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
await expect(addActiveScanId(clientId, "scan-d")).rejects.toThrow(
|
|
215
|
+
`Client ${clientId} already has the maximum of ${MAX_ACTIVE_SCANS} active scans`,
|
|
216
|
+
);
|
|
217
|
+
expect(await getActiveScanIds(clientId)).toEqual([
|
|
218
|
+
"scan-a",
|
|
219
|
+
"scan-b",
|
|
220
|
+
"scan-c",
|
|
221
|
+
]);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it("removes a scan id without affecting the others", async () => {
|
|
225
|
+
const clientId = "client-list-6";
|
|
226
|
+
await getClientsConfigCollection().insertOne(
|
|
227
|
+
createClientConfig({
|
|
228
|
+
clientId,
|
|
229
|
+
products: {
|
|
230
|
+
websiteTalk: {
|
|
231
|
+
companyName: "X",
|
|
232
|
+
timezone: "UTC",
|
|
233
|
+
language: "he",
|
|
234
|
+
activeScanIds: ["scan-a", "scan-b"],
|
|
235
|
+
},
|
|
236
|
+
},
|
|
237
|
+
}),
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
await removeActiveScanId(clientId, "scan-a");
|
|
241
|
+
expect(await getActiveScanIds(clientId)).toEqual(["scan-b"]);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it("removing a scan id that isn't present is a harmless no-op", async () => {
|
|
245
|
+
const clientId = "client-list-7";
|
|
246
|
+
await getClientsConfigCollection().insertOne(
|
|
247
|
+
createClientConfig({
|
|
248
|
+
clientId,
|
|
249
|
+
products: {
|
|
250
|
+
websiteTalk: {
|
|
251
|
+
companyName: "X",
|
|
252
|
+
timezone: "UTC",
|
|
253
|
+
language: "he",
|
|
254
|
+
activeScanIds: ["scan-a"],
|
|
255
|
+
},
|
|
256
|
+
},
|
|
257
|
+
}),
|
|
258
|
+
);
|
|
259
|
+
|
|
260
|
+
await expect(
|
|
261
|
+
removeActiveScanId(clientId, "scan-x"),
|
|
262
|
+
).resolves.toBeUndefined();
|
|
263
|
+
expect(await getActiveScanIds(clientId)).toEqual(["scan-a"]);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it("throws when removing from a client that doesn't exist", async () => {
|
|
267
|
+
await expect(
|
|
268
|
+
removeActiveScanId("no-such-client", "scan-a"),
|
|
269
|
+
).rejects.toThrow("No client config found for clientId: no-such-client");
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it("migrates a legacy-only activeScanId into activeScanIds instead of dropping it when adding a new one", async () => {
|
|
273
|
+
const clientId = "client-list-8";
|
|
274
|
+
await getClientsConfigCollection().insertOne(
|
|
275
|
+
createClientConfig({
|
|
276
|
+
clientId,
|
|
277
|
+
products: {
|
|
278
|
+
websiteTalk: {
|
|
279
|
+
companyName: "X",
|
|
280
|
+
timezone: "UTC",
|
|
281
|
+
language: "he",
|
|
282
|
+
activeScanId: "scan-legacy",
|
|
283
|
+
},
|
|
284
|
+
},
|
|
285
|
+
}),
|
|
286
|
+
);
|
|
287
|
+
|
|
288
|
+
await addActiveScanId(clientId, "scan-new");
|
|
289
|
+
|
|
290
|
+
expect(await getActiveScanIds(clientId)).toEqual(["scan-legacy", "scan-new"]);
|
|
291
|
+
const doc = await getClientsConfigCollection().findOne({ clientId });
|
|
292
|
+
expect(doc?.products?.websiteTalk?.activeScanId).toBeUndefined();
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
it("throws when adding to a client that doesn't exist", async () => {
|
|
296
|
+
await expect(
|
|
297
|
+
addActiveScanId("no-such-client", "scan-a"),
|
|
298
|
+
).rejects.toThrow("No client config found for clientId: no-such-client");
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
it("unsets a legacy-only activeScanId when it is the one being removed", async () => {
|
|
302
|
+
const clientId = "client-list-9";
|
|
303
|
+
await getClientsConfigCollection().insertOne(
|
|
304
|
+
createClientConfig({
|
|
305
|
+
clientId,
|
|
306
|
+
products: {
|
|
307
|
+
websiteTalk: {
|
|
308
|
+
companyName: "X",
|
|
309
|
+
timezone: "UTC",
|
|
310
|
+
language: "he",
|
|
311
|
+
activeScanId: "scan-legacy",
|
|
312
|
+
},
|
|
313
|
+
},
|
|
314
|
+
}),
|
|
315
|
+
);
|
|
316
|
+
|
|
317
|
+
await removeActiveScanId(clientId, "scan-legacy");
|
|
318
|
+
|
|
319
|
+
expect(await getActiveScanIds(clientId)).toEqual([]);
|
|
320
|
+
const doc = await getClientsConfigCollection().findOne({ clientId });
|
|
321
|
+
expect(doc?.products?.websiteTalk?.activeScanId).toBeUndefined();
|
|
322
|
+
});
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
describe("primaryScanId (CP-1628 follow-up)", () => {
|
|
326
|
+
beforeEach(async () => {
|
|
327
|
+
await getClientsConfigCollection().deleteMany({});
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
afterEach(async () => {
|
|
331
|
+
await getClientsConfigCollection().deleteMany({});
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
it("returns undefined when nothing is set", async () => {
|
|
335
|
+
const clientId = "client-primary-1";
|
|
336
|
+
await getClientsConfigCollection().insertOne(
|
|
337
|
+
createClientConfig({
|
|
338
|
+
clientId,
|
|
339
|
+
products: {
|
|
340
|
+
websiteTalk: { companyName: "X", timezone: "UTC", language: "he" },
|
|
341
|
+
},
|
|
342
|
+
}),
|
|
343
|
+
);
|
|
344
|
+
|
|
345
|
+
expect(await getPrimaryScanId(clientId)).toBeUndefined();
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
it("sets and returns the primary scan id, independent of activeScanIds", async () => {
|
|
349
|
+
const clientId = "client-primary-2";
|
|
350
|
+
await getClientsConfigCollection().insertOne(
|
|
351
|
+
createClientConfig({
|
|
352
|
+
clientId,
|
|
353
|
+
products: {
|
|
354
|
+
websiteTalk: {
|
|
355
|
+
companyName: "X",
|
|
356
|
+
timezone: "UTC",
|
|
357
|
+
language: "he",
|
|
358
|
+
activeScanIds: ["scan-a", "scan-b"],
|
|
359
|
+
},
|
|
360
|
+
},
|
|
361
|
+
}),
|
|
362
|
+
);
|
|
363
|
+
|
|
364
|
+
await setPrimaryScanId(clientId, "scan-c");
|
|
365
|
+
|
|
366
|
+
expect(await getPrimaryScanId(clientId)).toBe("scan-c");
|
|
367
|
+
expect(await getActiveScanIds(clientId)).toEqual(["scan-a", "scan-b"]);
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
it("replaces the previous primary scan id when set again", async () => {
|
|
371
|
+
const clientId = "client-primary-3";
|
|
372
|
+
await getClientsConfigCollection().insertOne(
|
|
373
|
+
createClientConfig({
|
|
374
|
+
clientId,
|
|
375
|
+
products: {
|
|
376
|
+
websiteTalk: {
|
|
377
|
+
companyName: "X",
|
|
378
|
+
timezone: "UTC",
|
|
379
|
+
language: "he",
|
|
380
|
+
primaryScanId: "scan-a",
|
|
381
|
+
},
|
|
382
|
+
},
|
|
383
|
+
}),
|
|
384
|
+
);
|
|
385
|
+
|
|
386
|
+
await setPrimaryScanId(clientId, "scan-b");
|
|
387
|
+
|
|
388
|
+
expect(await getPrimaryScanId(clientId)).toBe("scan-b");
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
it("clears the primary scan id", async () => {
|
|
392
|
+
const clientId = "client-primary-4";
|
|
393
|
+
await getClientsConfigCollection().insertOne(
|
|
394
|
+
createClientConfig({
|
|
395
|
+
clientId,
|
|
396
|
+
products: {
|
|
397
|
+
websiteTalk: {
|
|
398
|
+
companyName: "X",
|
|
399
|
+
timezone: "UTC",
|
|
400
|
+
language: "he",
|
|
401
|
+
primaryScanId: "scan-a",
|
|
402
|
+
},
|
|
403
|
+
},
|
|
404
|
+
}),
|
|
405
|
+
);
|
|
406
|
+
|
|
407
|
+
await clearPrimaryScanId(clientId);
|
|
408
|
+
|
|
409
|
+
expect(await getPrimaryScanId(clientId)).toBeUndefined();
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
it("throws when setting the primary scan for a client that doesn't exist", async () => {
|
|
413
|
+
await expect(
|
|
414
|
+
setPrimaryScanId("no-such-client", "scan-a"),
|
|
415
|
+
).rejects.toThrow("No client config found for clientId: no-such-client");
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
it("throws when clearing the primary scan for a client that doesn't exist", async () => {
|
|
419
|
+
await expect(
|
|
420
|
+
clearPrimaryScanId("no-such-client"),
|
|
421
|
+
).rejects.toThrow("No client config found for clientId: no-such-client");
|
|
422
|
+
});
|
|
423
|
+
});
|
|
@@ -6,8 +6,9 @@ import {
|
|
|
6
6
|
KNOWN_PRODUCT_KEYS,
|
|
7
7
|
KnownProductKey,
|
|
8
8
|
} from "../index";
|
|
9
|
-
import { Collection } from "mongodb";
|
|
9
|
+
import { Collection, UpdateFilter } from "mongodb";
|
|
10
10
|
import { resetClientQuotaSettings } from "../clients/clients.quota.getters";
|
|
11
|
+
import { MAX_ACTIVE_SCANS } from "./clientsConfig.constants";
|
|
11
12
|
|
|
12
13
|
export const getClientsConfigCollection = (): Collection<ClientConfigDoc> => {
|
|
13
14
|
return getDb().collection<ClientConfigDoc>("clientsConfig");
|
|
@@ -17,7 +18,9 @@ export const getClientConfig = (clientId: string) => {
|
|
|
17
18
|
return getClientsConfigCollection().findOne({ clientId });
|
|
18
19
|
};
|
|
19
20
|
|
|
20
|
-
export const getClientDisplayName = async (
|
|
21
|
+
export const getClientDisplayName = async (
|
|
22
|
+
clientId: string,
|
|
23
|
+
): Promise<string> => {
|
|
21
24
|
const doc = await getClientsConfigCollection().findOne(
|
|
22
25
|
{ clientId },
|
|
23
26
|
{ projection: { "quota.clientDisplayName": 1, _id: 0 } },
|
|
@@ -71,6 +74,24 @@ export const getActiveScanId = async (
|
|
|
71
74
|
return cfg?.products?.websiteTalk?.activeScanId;
|
|
72
75
|
};
|
|
73
76
|
|
|
77
|
+
/**
|
|
78
|
+
* Returns the client's active scan ids as a list.
|
|
79
|
+
* Falls back to wrapping the legacy singular `activeScanId` field when
|
|
80
|
+
* `activeScanIds` is absent — lets clients on the old field keep working
|
|
81
|
+
* without a data migration.
|
|
82
|
+
*/
|
|
83
|
+
export const getActiveScanIds = async (clientId: string): Promise<string[]> => {
|
|
84
|
+
const cfg = await getClientConfig(clientId);
|
|
85
|
+
const list = cfg?.products?.websiteTalk?.activeScanIds;
|
|
86
|
+
if (list && list.length > 0) return list;
|
|
87
|
+
const legacy = cfg?.products?.websiteTalk?.activeScanId;
|
|
88
|
+
return legacy ? [legacy] : [];
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* @deprecated Use `addActiveScanId` instead — supports multiple simultaneously
|
|
93
|
+
* active scans. This function overwrites the entire single-value field.
|
|
94
|
+
*/
|
|
74
95
|
export const updateActiveScanId = async (
|
|
75
96
|
clientId: string,
|
|
76
97
|
scanId: string,
|
|
@@ -78,6 +99,38 @@ export const updateActiveScanId = async (
|
|
|
78
99
|
await updateProductConfig(clientId, "websiteTalk", { activeScanId: scanId });
|
|
79
100
|
};
|
|
80
101
|
|
|
102
|
+
/**
|
|
103
|
+
* Adds a scan id to the client's active list. Writes the full list back
|
|
104
|
+
* (rather than a bare array append) so a client still on the legacy
|
|
105
|
+
* singular field gets migrated instead of silently losing it.
|
|
106
|
+
*/
|
|
107
|
+
export const addActiveScanId = async (
|
|
108
|
+
clientId: string,
|
|
109
|
+
scanId: string,
|
|
110
|
+
): Promise<void> => {
|
|
111
|
+
const current = await getActiveScanIds(clientId);
|
|
112
|
+
if (!current.includes(scanId) && current.length >= MAX_ACTIVE_SCANS) {
|
|
113
|
+
throw new Error(
|
|
114
|
+
`Client ${clientId} already has the maximum of ${MAX_ACTIVE_SCANS} active scans`,
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
const next = current.includes(scanId) ? current : [...current, scanId];
|
|
118
|
+
const result = await getClientsConfigCollection().updateOne(
|
|
119
|
+
{ clientId },
|
|
120
|
+
{
|
|
121
|
+
$set: { "products.websiteTalk.activeScanIds": next },
|
|
122
|
+
$unset: { "products.websiteTalk.activeScanId": "" },
|
|
123
|
+
},
|
|
124
|
+
);
|
|
125
|
+
if (result.matchedCount === 0) {
|
|
126
|
+
throw new Error(`No client config found for clientId: ${clientId}`);
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* @deprecated Use `removeActiveScanId` instead — removes a single scan from
|
|
132
|
+
* the active list without clearing the others.
|
|
133
|
+
*/
|
|
81
134
|
export const clearActiveScanId = async (clientId: string): Promise<void> => {
|
|
82
135
|
const result = await getClientsConfigCollection().updateOne(
|
|
83
136
|
{ clientId },
|
|
@@ -88,6 +141,67 @@ export const clearActiveScanId = async (clientId: string): Promise<void> => {
|
|
|
88
141
|
}
|
|
89
142
|
};
|
|
90
143
|
|
|
144
|
+
/**
|
|
145
|
+
* Removes a single scan id from the active list. Also clears the legacy
|
|
146
|
+
* singular field when it matches, so a not-yet-migrated client's removal
|
|
147
|
+
* actually takes effect instead of silently no-op'ing.
|
|
148
|
+
*/
|
|
149
|
+
export const removeActiveScanId = async (
|
|
150
|
+
clientId: string,
|
|
151
|
+
scanId: string,
|
|
152
|
+
): Promise<void> => {
|
|
153
|
+
const cfg = await getClientConfig(clientId);
|
|
154
|
+
if (!cfg) {
|
|
155
|
+
throw new Error(`No client config found for clientId: ${clientId}`);
|
|
156
|
+
}
|
|
157
|
+
const updateOps: UpdateFilter<ClientConfigDoc> = {
|
|
158
|
+
$pull: { "products.websiteTalk.activeScanIds": scanId },
|
|
159
|
+
};
|
|
160
|
+
if (cfg.products?.websiteTalk?.activeScanId === scanId) {
|
|
161
|
+
updateOps.$unset = { "products.websiteTalk.activeScanId": "" };
|
|
162
|
+
}
|
|
163
|
+
await getClientsConfigCollection().updateOne({ clientId }, updateOps);
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
/** Returns the scan whose content seeds the phone call's opening description, if one is set. */
|
|
167
|
+
export const getPrimaryScanId = async (
|
|
168
|
+
clientId: string,
|
|
169
|
+
): Promise<string | undefined> => {
|
|
170
|
+
const cfg = await getClientConfig(clientId);
|
|
171
|
+
return cfg?.products?.websiteTalk?.primaryScanId;
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Sets the primary scan, replacing whatever was set before — exactly one
|
|
176
|
+
* primary scan at a time. Throws if the client doesn't exist.
|
|
177
|
+
*/
|
|
178
|
+
export const setPrimaryScanId = async (
|
|
179
|
+
clientId: string,
|
|
180
|
+
scanId: string,
|
|
181
|
+
): Promise<void> => {
|
|
182
|
+
const result = await getClientsConfigCollection().updateOne(
|
|
183
|
+
{ clientId },
|
|
184
|
+
{ $set: { "products.websiteTalk.primaryScanId": scanId } },
|
|
185
|
+
);
|
|
186
|
+
if (result.matchedCount === 0) {
|
|
187
|
+
throw new Error(`No client config found for clientId: ${clientId}`);
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Clears the primary scan. Callers that need the opening description fall
|
|
193
|
+
* back to the first active scan. Throws if the client doesn't exist.
|
|
194
|
+
*/
|
|
195
|
+
export const clearPrimaryScanId = async (clientId: string): Promise<void> => {
|
|
196
|
+
const result = await getClientsConfigCollection().updateOne(
|
|
197
|
+
{ clientId },
|
|
198
|
+
{ $unset: { "products.websiteTalk.primaryScanId": "" } },
|
|
199
|
+
);
|
|
200
|
+
if (result.matchedCount === 0) {
|
|
201
|
+
throw new Error(`No client config found for clientId: ${clientId}`);
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
|
|
91
205
|
export async function listClientIdsWithConfiguredQuota(): Promise<string[]> {
|
|
92
206
|
const docs = await getClientsConfigCollection()
|
|
93
207
|
.find({ "quota.totalQuota": { $exists: true, $ne: null } })
|
|
@@ -64,6 +64,10 @@ export type WebsiteTalkProduct = {
|
|
|
64
64
|
timezone: string;
|
|
65
65
|
language: string;
|
|
66
66
|
activeScanId?: string;
|
|
67
|
+
/** Up to MAX_ACTIVE_SCANS scan ids simultaneously serving RAG for phone calls. */
|
|
68
|
+
activeScanIds?: string[];
|
|
69
|
+
/** The single scan whose content seeds the phone call's opening description. */
|
|
70
|
+
primaryScanId?: string;
|
|
67
71
|
};
|
|
68
72
|
|
|
69
73
|
// ---------------------------------------------------------------------------
|
|
@@ -13,5 +13,6 @@ export * from "./municipal/muniIssues";
|
|
|
13
13
|
export * from "./municipal/streets";
|
|
14
14
|
export * from "./municipal/tickets";
|
|
15
15
|
export * from "./websitalk/scans";
|
|
16
|
+
export * from "./websitalk/scanResources";
|
|
16
17
|
export * from "./websitalk/websiteUrls";
|
|
17
18
|
export * from "./websitalk/knowledgeDocuments";
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { Factory } from "fishery";
|
|
2
|
+
import { faker } from "@faker-js/faker";
|
|
3
|
+
import { ObjectId } from "mongodb";
|
|
4
|
+
import {
|
|
5
|
+
URL_TASK_STATUSES,
|
|
6
|
+
type ScanResource,
|
|
7
|
+
} from "../../../websitalk/scanResources";
|
|
8
|
+
|
|
9
|
+
export const scanResourceFactory = Factory.define<ScanResource>(() => ({
|
|
10
|
+
urlHashId: faker.string.uuid(),
|
|
11
|
+
clientId: faker.string.uuid(),
|
|
12
|
+
scanId: new ObjectId().toString(),
|
|
13
|
+
websiteUrlId: new ObjectId().toString(),
|
|
14
|
+
url: faker.internet.url(),
|
|
15
|
+
status: URL_TASK_STATUSES.URL_PENDING,
|
|
16
|
+
depth: 0,
|
|
17
|
+
retryCount: 0,
|
|
18
|
+
createdAt: new Date(),
|
|
19
|
+
updatedAt: new Date(),
|
|
20
|
+
}));
|
|
21
|
+
|
|
22
|
+
export function createScanResourceFixture(
|
|
23
|
+
overrides?: Partial<ScanResource>,
|
|
24
|
+
): ScanResource {
|
|
25
|
+
return scanResourceFactory.build(overrides);
|
|
26
|
+
}
|
|
@@ -17,6 +17,11 @@ export const scanFactory = Factory.define<Scan>(() => ({
|
|
|
17
17
|
},
|
|
18
18
|
createdAt: new Date(),
|
|
19
19
|
updatedAt: new Date(),
|
|
20
|
+
startedAt: new Date(),
|
|
21
|
+
completedAt: null,
|
|
22
|
+
pagesCompleted: 0,
|
|
23
|
+
pagesFailedPermanent: 0,
|
|
24
|
+
pagesDuplicate: 0,
|
|
20
25
|
}));
|
|
21
26
|
|
|
22
27
|
export function createScanFixture(overrides?: Partial<Scan>): Scan {
|
package/src/websitalk/index.ts
CHANGED
|
@@ -40,21 +40,6 @@ describe("db.websitalk.knowledgeDocuments", () => {
|
|
|
40
40
|
expect(saved?.createdAt).toBeInstanceOf(Date);
|
|
41
41
|
expect(saved?.updatedAt).toBeInstanceOf(Date);
|
|
42
42
|
});
|
|
43
|
-
|
|
44
|
-
it("should create a knowledge document with a preset _id", async () => {
|
|
45
|
-
const presetId = new (await import("mongodb")).ObjectId();
|
|
46
|
-
const id = await createKnowledgeDocument({
|
|
47
|
-
_id: presetId,
|
|
48
|
-
clientId,
|
|
49
|
-
filename: "preset.pdf",
|
|
50
|
-
contentType: KNOWLEDGE_CONTENT_TYPES.PDF,
|
|
51
|
-
fileType: KNOWLEDGE_FILE_TYPES.PDF,
|
|
52
|
-
storagePath: `websitalk/knowledge_files/test/${presetId.toString()}/original.pdf`,
|
|
53
|
-
byteSize: 512,
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
expect(id.toString()).toBe(presetId.toString());
|
|
57
|
-
});
|
|
58
43
|
});
|
|
59
44
|
|
|
60
45
|
describe("getKnowledgeDocumentById", () => {
|
|
@@ -160,15 +145,11 @@ describe("db.websitalk.knowledgeDocuments", () => {
|
|
|
160
145
|
status: KNOWLEDGE_DOCUMENT_STATUSES.READY,
|
|
161
146
|
progressPercent: 100,
|
|
162
147
|
pageCount: 12,
|
|
163
|
-
byteSize: 2048,
|
|
164
|
-
storagePath: "websitalk/knowledge_files/test/doc-updated.pdf",
|
|
165
148
|
});
|
|
166
149
|
|
|
167
150
|
expect(updated?.status).toBe(KNOWLEDGE_DOCUMENT_STATUSES.READY);
|
|
168
151
|
expect(updated?.progressPercent).toBe(100);
|
|
169
152
|
expect(updated?.pageCount).toBe(12);
|
|
170
|
-
expect(updated?.byteSize).toBe(2048);
|
|
171
|
-
expect(updated?.storagePath).toBe("websitalk/knowledge_files/test/doc-updated.pdf");
|
|
172
153
|
expect(updated?.updatedAt.getTime()).toBeGreaterThanOrEqual(
|
|
173
154
|
before!.updatedAt.getTime(),
|
|
174
155
|
);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ObjectId as MongoObjectId } from "mongodb";
|
|
1
|
+
import { WithId, ObjectId as MongoObjectId } from "mongodb";
|
|
2
2
|
|
|
3
3
|
import type {
|
|
4
4
|
KNOWLEDGE_CONTENT_TYPES,
|
|
@@ -34,7 +34,7 @@ export type KnowledgeDocument = {
|
|
|
34
34
|
updatedAt: Date;
|
|
35
35
|
};
|
|
36
36
|
|
|
37
|
-
export type KnowledgeDocumentDoc = KnowledgeDocument
|
|
37
|
+
export type KnowledgeDocumentDoc = WithId<KnowledgeDocument>;
|
|
38
38
|
|
|
39
39
|
export type CreateKnowledgeDocumentInput = Omit<
|
|
40
40
|
KnowledgeDocument,
|