@talkpilot/core-db 1.3.89 → 1.3.91
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 +12 -65
- package/dist/configuration/gptLive.seed.d.ts +2 -0
- package/dist/configuration/gptLive.seed.d.ts.map +1 -0
- package/dist/configuration/gptLive.seed.js +59 -0
- package/dist/configuration/gptLive.seed.js.map +1 -0
- package/dist/talkpilot/calls/calls.constants.d.ts +1 -11
- package/dist/talkpilot/calls/calls.constants.d.ts.map +1 -1
- package/dist/talkpilot/calls/calls.constants.js +1 -11
- package/dist/talkpilot/calls/calls.constants.js.map +1 -1
- package/dist/talkpilot/calls/calls.getters.d.ts +1 -8
- package/dist/talkpilot/calls/calls.getters.d.ts.map +1 -1
- package/dist/talkpilot/calls/calls.getters.js +1 -216
- package/dist/talkpilot/calls/calls.getters.js.map +1 -1
- package/dist/talkpilot/calls/calls.types.d.ts +0 -45
- package/dist/talkpilot/calls/calls.types.d.ts.map +1 -1
- package/dist/talkpilot/clients/clients.getters.d.ts +0 -6
- package/dist/talkpilot/clients/clients.getters.d.ts.map +1 -1
- package/dist/talkpilot/clients/clients.getters.js +1 -62
- package/dist/talkpilot/clients/clients.getters.js.map +1 -1
- package/dist/talkpilot/clients/clients.quota.constants.d.ts +2 -0
- package/dist/talkpilot/clients/clients.quota.constants.d.ts.map +1 -0
- package/dist/talkpilot/clients/clients.quota.constants.js +5 -0
- package/dist/talkpilot/clients/clients.quota.constants.js.map +1 -0
- package/dist/talkpilot/clients/clients.quota.utils.d.ts +8 -0
- package/dist/talkpilot/clients/clients.quota.utils.d.ts.map +1 -0
- package/dist/talkpilot/clients/clients.quota.utils.js +45 -0
- package/dist/talkpilot/clients/clients.quota.utils.js.map +1 -0
- package/dist/talkpilot/clients/clients.types.d.ts +0 -53
- package/dist/talkpilot/clients/clients.types.d.ts.map +1 -1
- package/dist/talkpilot/clients/index.d.ts +0 -2
- package/dist/talkpilot/clients/index.d.ts.map +1 -1
- package/dist/talkpilot/clients/index.js +0 -2
- package/dist/talkpilot/clients/index.js.map +1 -1
- package/dist/talkpilot/contextNotes/contextNotes.getters.d.ts +7 -1
- package/dist/talkpilot/contextNotes/contextNotes.getters.d.ts.map +1 -1
- package/dist/talkpilot/contextNotes/contextNotes.getters.js +37 -1
- package/dist/talkpilot/contextNotes/contextNotes.getters.js.map +1 -1
- package/dist/talkpilot/contextNotes/contextNotes.types.d.ts +6 -0
- package/dist/talkpilot/contextNotes/contextNotes.types.d.ts.map +1 -1
- package/dist/talkpilot/index.d.ts +0 -1
- package/dist/talkpilot/index.d.ts.map +1 -1
- package/dist/talkpilot/index.js +0 -1
- package/dist/talkpilot/index.js.map +1 -1
- package/dist/talkpilot/phone_numbers/phone_numbers.getter.d.ts +1 -9
- package/dist/talkpilot/phone_numbers/phone_numbers.getter.d.ts.map +1 -1
- package/dist/talkpilot/phone_numbers/phone_numbers.getter.js +1 -70
- package/dist/talkpilot/phone_numbers/phone_numbers.getter.js.map +1 -1
- package/dist/talkpilot/phone_numbers/phone_numbers.types.d.ts +0 -5
- package/dist/talkpilot/phone_numbers/phone_numbers.types.d.ts.map +1 -1
- package/dist/test-utils/factories/index.d.ts +0 -2
- package/dist/test-utils/factories/index.d.ts.map +1 -1
- package/dist/test-utils/factories/index.js +0 -2
- package/dist/test-utils/factories/index.js.map +1 -1
- package/dist/websitalk/scanResources/scanResources.getters.d.ts.map +1 -1
- package/dist/websitalk/scanResources/scanResources.getters.js +1 -2
- package/dist/websitalk/scanResources/scanResources.getters.js.map +1 -1
- package/dist/websitalk/scanResources/scanResources.utils.d.ts +1 -1
- package/dist/websitalk/scanResources/scanResources.utils.d.ts.map +1 -1
- package/dist/websitalk/scanResources/scanResources.utils.js +3 -3
- package/dist/websitalk/scanResources/scanResources.utils.js.map +1 -1
- package/package.json +1 -1
- package/src/talkpilot/calls/calls.constants.ts +0 -14
- package/src/talkpilot/calls/calls.getters.ts +0 -320
- package/src/talkpilot/calls/calls.types.ts +0 -52
- package/src/talkpilot/clients/clients.getters.ts +2 -91
- package/src/talkpilot/clients/clients.types.ts +0 -58
- package/src/talkpilot/clients/index.ts +0 -2
- package/src/talkpilot/contextNotes/__tests__/contextNotes.getters.spec.ts +141 -0
- package/src/talkpilot/contextNotes/contextNotes.getters.ts +75 -1
- package/src/talkpilot/contextNotes/contextNotes.types.ts +7 -0
- package/src/talkpilot/index.ts +0 -1
- package/src/talkpilot/phone_numbers/__tests__/phone_numbers.spec.ts +13 -284
- package/src/talkpilot/phone_numbers/phone_numbers.getter.ts +1 -89
- package/src/talkpilot/phone_numbers/phone_numbers.types.ts +0 -6
- package/src/test-utils/factories/index.ts +0 -2
- package/src/websitalk/scanResources/scanResources.getters.ts +6 -2
- package/src/websitalk/scanResources/scanResources.utils.ts +3 -2
- package/dist/talkpilot/auditLogs/auditLogs.getters.d.ts +0 -8
- package/dist/talkpilot/auditLogs/auditLogs.getters.d.ts.map +0 -1
- package/dist/talkpilot/auditLogs/auditLogs.getters.js +0 -26
- package/dist/talkpilot/auditLogs/auditLogs.getters.js.map +0 -1
- package/dist/talkpilot/auditLogs/auditLogs.types.d.ts +0 -11
- package/dist/talkpilot/auditLogs/auditLogs.types.d.ts.map +0 -1
- package/dist/talkpilot/auditLogs/auditLogs.types.js +0 -3
- package/dist/talkpilot/auditLogs/auditLogs.types.js.map +0 -1
- package/dist/talkpilot/auditLogs/index.d.ts +0 -3
- package/dist/talkpilot/auditLogs/index.d.ts.map +0 -1
- package/dist/talkpilot/auditLogs/index.js +0 -19
- package/dist/talkpilot/auditLogs/index.js.map +0 -1
- package/dist/talkpilot/clients/clients.constants.d.ts +0 -4
- package/dist/talkpilot/clients/clients.constants.d.ts.map +0 -1
- package/dist/talkpilot/clients/clients.constants.js +0 -7
- package/dist/talkpilot/clients/clients.constants.js.map +0 -1
- package/dist/talkpilot/clients/clients.lifecycle.d.ts +0 -10
- package/dist/talkpilot/clients/clients.lifecycle.d.ts.map +0 -1
- package/dist/talkpilot/clients/clients.lifecycle.js +0 -109
- package/dist/talkpilot/clients/clients.lifecycle.js.map +0 -1
- package/dist/test-utils/factories/talkpilot/clients.d.ts +0 -14
- package/dist/test-utils/factories/talkpilot/clients.d.ts.map +0 -1
- package/dist/test-utils/factories/talkpilot/clients.js +0 -19
- package/dist/test-utils/factories/talkpilot/clients.js.map +0 -1
- package/dist/test-utils/factories/talkpilot/results.d.ts +0 -16
- package/dist/test-utils/factories/talkpilot/results.d.ts.map +0 -1
- package/dist/test-utils/factories/talkpilot/results.js +0 -40
- package/dist/test-utils/factories/talkpilot/results.js.map +0 -1
- package/dist/utils/regex.utils.d.ts +0 -2
- package/dist/utils/regex.utils.d.ts.map +0 -1
- package/dist/utils/regex.utils.js +0 -6
- package/dist/utils/regex.utils.js.map +0 -1
- package/src/talkpilot/auditLogs/__tests__/auditLogs.spec.ts +0 -91
- package/src/talkpilot/auditLogs/auditLogs.getters.ts +0 -26
- package/src/talkpilot/auditLogs/auditLogs.types.ts +0 -12
- package/src/talkpilot/auditLogs/index.ts +0 -2
- package/src/talkpilot/calls/__tests__/calls.statistics.getters.spec.ts +0 -559
- package/src/talkpilot/clients/__tests__/clients.getters.spec.ts +0 -224
- package/src/talkpilot/clients/__tests__/clients.lifecycle.spec.ts +0 -194
- package/src/talkpilot/clients/clients.constants.ts +0 -4
- package/src/talkpilot/clients/clients.lifecycle.ts +0 -132
- package/src/test-utils/factories/talkpilot/clients.ts +0 -21
- package/src/test-utils/factories/talkpilot/results.ts +0 -43
- package/src/utils/regex.utils.ts +0 -2
|
@@ -166,55 +166,3 @@ export type DateRange = {
|
|
|
166
166
|
since?: Date;
|
|
167
167
|
until?: Date;
|
|
168
168
|
};
|
|
169
|
-
|
|
170
|
-
export type StatsDateRangeParams = {
|
|
171
|
-
dateFrom?: string;
|
|
172
|
-
dateTo?: string;
|
|
173
|
-
clientId?: string;
|
|
174
|
-
/** Restricts to these clients. Combined with clientId, both must match. */
|
|
175
|
-
clientIds?: string[];
|
|
176
|
-
timezone?: string;
|
|
177
|
-
};
|
|
178
|
-
|
|
179
|
-
export type ClientCallStatsParams = StatsDateRangeParams & {
|
|
180
|
-
skip?: number;
|
|
181
|
-
limit?: number;
|
|
182
|
-
};
|
|
183
|
-
|
|
184
|
-
export type ActiveCallsSummary = {
|
|
185
|
-
total: number;
|
|
186
|
-
incoming: number;
|
|
187
|
-
outgoing: number;
|
|
188
|
-
activeClientIds: string[];
|
|
189
|
-
};
|
|
190
|
-
|
|
191
|
-
export type TodayCallsSummary = {
|
|
192
|
-
total: number;
|
|
193
|
-
incoming: number;
|
|
194
|
-
outgoing: number;
|
|
195
|
-
completed: number;
|
|
196
|
-
failed: number;
|
|
197
|
-
busy: number;
|
|
198
|
-
noAnswer: number;
|
|
199
|
-
totalMinutes: number;
|
|
200
|
-
};
|
|
201
|
-
|
|
202
|
-
export type ClientCallStats = {
|
|
203
|
-
clientId: string;
|
|
204
|
-
totalCalls: number;
|
|
205
|
-
statusCounts: Record<string, number>;
|
|
206
|
-
totalMinutes: number;
|
|
207
|
-
lastCallDate: Date;
|
|
208
|
-
};
|
|
209
|
-
|
|
210
|
-
export type ClientCallStatsPage = {
|
|
211
|
-
rows: ClientCallStats[];
|
|
212
|
-
total: number;
|
|
213
|
-
};
|
|
214
|
-
|
|
215
|
-
export type SessionCallStats = {
|
|
216
|
-
sessionId: string;
|
|
217
|
-
sessionName: string;
|
|
218
|
-
totalCalls: number;
|
|
219
|
-
statusCounts: Record<string, number>;
|
|
220
|
-
};
|
|
@@ -1,13 +1,5 @@
|
|
|
1
|
-
import { getDb,
|
|
2
|
-
import { Collection
|
|
3
|
-
import { applyQueryOptions } from "../utils/query.utils";
|
|
4
|
-
import { escapeRegex } from "../../utils/regex.utils";
|
|
5
|
-
import {
|
|
6
|
-
BackofficeRole,
|
|
7
|
-
ClientsPage,
|
|
8
|
-
ClientsPageParams,
|
|
9
|
-
ClientsPageRow,
|
|
10
|
-
} from "./clients.types";
|
|
1
|
+
import { getDb, Client } from "../index";
|
|
2
|
+
import { Collection } from "mongodb";
|
|
11
3
|
|
|
12
4
|
export const getClientsCollection = (): Collection<Client> => {
|
|
13
5
|
return getDb().collection<Client>("clients");
|
|
@@ -23,86 +15,5 @@ export const getClientByAPIKey = async (
|
|
|
23
15
|
return client ? client : null;
|
|
24
16
|
};
|
|
25
17
|
|
|
26
|
-
export const getAllClientIds = async (): Promise<string[]> => {
|
|
27
|
-
const docs = await getClientsCollection()
|
|
28
|
-
.find({}, { projection: { clientId: 1 } })
|
|
29
|
-
.toArray();
|
|
30
|
-
return docs.map((c) => c.clientId);
|
|
31
|
-
};
|
|
32
18
|
export const getClientByClientId = (clientId: string): Promise<Client | null> =>
|
|
33
19
|
getClientsCollection().findOne({ clientId });
|
|
34
|
-
|
|
35
|
-
export const getBackofficeRole = async (
|
|
36
|
-
clientId: string,
|
|
37
|
-
): Promise<BackofficeRole | null> => {
|
|
38
|
-
const client = await getClientsCollection().findOne(
|
|
39
|
-
{ clientId },
|
|
40
|
-
{ projection: { "roles.backoffice": 1 } },
|
|
41
|
-
);
|
|
42
|
-
const flags = client?.roles?.backoffice;
|
|
43
|
-
if (flags?.admin) return "admin";
|
|
44
|
-
if (flags?.viewer) return "viewer";
|
|
45
|
-
return null;
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
export const setBackofficeRole = async (
|
|
49
|
-
clientId: string,
|
|
50
|
-
role: BackofficeRole | null,
|
|
51
|
-
): Promise<boolean> => {
|
|
52
|
-
const { matchedCount } = await getClientsCollection().updateOne(
|
|
53
|
-
{ clientId },
|
|
54
|
-
{
|
|
55
|
-
$set: {
|
|
56
|
-
"roles.backoffice.admin": role === "admin",
|
|
57
|
-
"roles.backoffice.viewer": role === "viewer",
|
|
58
|
-
},
|
|
59
|
-
},
|
|
60
|
-
);
|
|
61
|
-
return matchedCount > 0;
|
|
62
|
-
};
|
|
63
|
-
|
|
64
|
-
export const countBackofficeAdmins = (): Promise<number> =>
|
|
65
|
-
getClientsCollection().countDocuments({ "roles.backoffice.admin": true });
|
|
66
|
-
|
|
67
|
-
export const findClientsPage = async ({
|
|
68
|
-
skip,
|
|
69
|
-
limit,
|
|
70
|
-
clientId,
|
|
71
|
-
clientIds,
|
|
72
|
-
excludeClientIds,
|
|
73
|
-
search,
|
|
74
|
-
}: ClientsPageParams): Promise<ClientsPage> => {
|
|
75
|
-
const conditions: Filter<Client>[] = [];
|
|
76
|
-
if (clientId) {
|
|
77
|
-
conditions.push({ clientId });
|
|
78
|
-
}
|
|
79
|
-
if (clientIds) {
|
|
80
|
-
conditions.push({ clientId: { $in: clientIds } });
|
|
81
|
-
}
|
|
82
|
-
if (excludeClientIds) {
|
|
83
|
-
conditions.push({ clientId: { $nin: excludeClientIds } });
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
const text = search?.trim();
|
|
87
|
-
if (text) {
|
|
88
|
-
const pattern = new RegExp(escapeRegex(text), "i");
|
|
89
|
-
const clientIdsByPhone = await getPhoneNumbersCollection().distinct("client_id", {
|
|
90
|
-
phone_number: pattern,
|
|
91
|
-
});
|
|
92
|
-
conditions.push({
|
|
93
|
-
$or: [{ clientId: pattern }, { clientId: { $in: clientIdsByPhone } }],
|
|
94
|
-
});
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
const filter: Filter<Client> =
|
|
98
|
-
conditions.length > 0 ? { $and: conditions } : {};
|
|
99
|
-
const collection = getClientsCollection();
|
|
100
|
-
const [clients, total] = await Promise.all([
|
|
101
|
-
applyQueryOptions(
|
|
102
|
-
collection.find<ClientsPageRow>(filter, { projection: { clientId: 1, credits: 1 } }),
|
|
103
|
-
{ sort: { createdAt: -1, _id: -1 }, skip, limit },
|
|
104
|
-
).toArray(),
|
|
105
|
-
collection.countDocuments(filter),
|
|
106
|
-
]);
|
|
107
|
-
return { clients, total };
|
|
108
|
-
};
|
|
@@ -1,21 +1,8 @@
|
|
|
1
1
|
import { ObjectId } from "mongodb";
|
|
2
2
|
import { ClientQuotaSettings } from "./clients.quota.types";
|
|
3
|
-
import { BACKOFFICE_ROLES } from "./clients.constants";
|
|
4
3
|
|
|
5
4
|
export type { QuotaThresholdAlert, ClientQuotaSettings, QuotaAlertInput, ClientQuotaUsage, ClientQuotaState } from "./clients.quota.types";
|
|
6
5
|
|
|
7
|
-
export type BackofficeRole = (typeof BACKOFFICE_ROLES)[number];
|
|
8
|
-
|
|
9
|
-
export type BackofficeRoleFlags = {
|
|
10
|
-
admin?: boolean;
|
|
11
|
-
viewer?: boolean;
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
export type ClientRoles = {
|
|
15
|
-
admin?: boolean;
|
|
16
|
-
backoffice?: BackofficeRoleFlags;
|
|
17
|
-
};
|
|
18
|
-
|
|
19
6
|
export type Client = {
|
|
20
7
|
_id?: ObjectId;
|
|
21
8
|
createdAt: Date;
|
|
@@ -26,51 +13,6 @@ export type Client = {
|
|
|
26
13
|
credits: number;
|
|
27
14
|
managedBy?: string;
|
|
28
15
|
quota?: ClientQuotaSettings;
|
|
29
|
-
roles?: ClientRoles;
|
|
30
16
|
};
|
|
31
17
|
|
|
32
18
|
export type ClientDoc = Client & { _id: ObjectId };
|
|
33
|
-
|
|
34
|
-
export type ClientsPageParams = {
|
|
35
|
-
skip: number;
|
|
36
|
-
limit: number;
|
|
37
|
-
/** Exact clientId match. */
|
|
38
|
-
clientId?: string;
|
|
39
|
-
/** Only these clients, e.g. the ones with calls in a date range. */
|
|
40
|
-
clientIds?: string[];
|
|
41
|
-
/** Every client except these, e.g. the ones that called since an inactivity cutoff. */
|
|
42
|
-
excludeClientIds?: string[];
|
|
43
|
-
/** Case-insensitive "contains" on the clientId or on any of the client's phone numbers. */
|
|
44
|
-
search?: string;
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
export type ClientsPageRow = Pick<ClientDoc, "_id" | "clientId" | "credits">;
|
|
48
|
-
|
|
49
|
-
export type ClientsPage = {
|
|
50
|
-
clients: ClientsPageRow[];
|
|
51
|
-
total: number;
|
|
52
|
-
};
|
|
53
|
-
|
|
54
|
-
export type CollectionMatch = { collectionName: string; matched: boolean };
|
|
55
|
-
|
|
56
|
-
export type CollectionDeleteResult = {
|
|
57
|
-
collectionName: string;
|
|
58
|
-
deletedCount: number;
|
|
59
|
-
success: boolean;
|
|
60
|
-
error?: string;
|
|
61
|
-
};
|
|
62
|
-
|
|
63
|
-
export type AuditLogResult = {
|
|
64
|
-
auditLogged: boolean;
|
|
65
|
-
auditError?: string;
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
/** Discriminated union: `deleted` tells you which shape you got without guessing. */
|
|
69
|
-
export type DeleteClientDataSummary =
|
|
70
|
-
| { clientId: string; deleted: false; reason: string }
|
|
71
|
-
| ({
|
|
72
|
-
clientId: string;
|
|
73
|
-
deleted: true;
|
|
74
|
-
results: CollectionDeleteResult[];
|
|
75
|
-
allSucceeded: boolean;
|
|
76
|
-
} & AuditLogResult);
|
|
@@ -6,6 +6,9 @@ import {
|
|
|
6
6
|
setContextNoteEntries,
|
|
7
7
|
setContextNoteConfig,
|
|
8
8
|
deleteContextNoteEntry,
|
|
9
|
+
appendContextNoteEntry,
|
|
10
|
+
updateContextNoteEntryFields,
|
|
11
|
+
pullContextNoteEntry,
|
|
9
12
|
} from "../contextNotes.getters";
|
|
10
13
|
import { ObjectId } from "../../index";
|
|
11
14
|
import {
|
|
@@ -203,6 +206,144 @@ describe("contextNotes getters", () => {
|
|
|
203
206
|
expect(saved?.notes[0]._id.toString()).toBe(noteToKeep._id.toString());
|
|
204
207
|
});
|
|
205
208
|
|
|
209
|
+
it("should append one note without replacing the others", async () => {
|
|
210
|
+
const kept = createContextNoteEntry({ title: "kept" });
|
|
211
|
+
const doc = buildContextNote({ notes: [kept] });
|
|
212
|
+
await createContextNote(doc);
|
|
213
|
+
const now = new Date("2026-10-18T12:00:00.000Z");
|
|
214
|
+
|
|
215
|
+
const added = await appendContextNoteEntry(
|
|
216
|
+
doc.clientId,
|
|
217
|
+
doc.product,
|
|
218
|
+
{
|
|
219
|
+
title: "added",
|
|
220
|
+
content: "new content",
|
|
221
|
+
activeFrom: now,
|
|
222
|
+
expiresAt: null,
|
|
223
|
+
},
|
|
224
|
+
now,
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
const saved = await getContextNotesCollection().findOne({
|
|
228
|
+
clientId: doc.clientId,
|
|
229
|
+
product: doc.product,
|
|
230
|
+
});
|
|
231
|
+
expect(saved?.notes).toHaveLength(2);
|
|
232
|
+
expect(saved?.notes[0].title).toBe("added");
|
|
233
|
+
expect(saved?.notes[1]._id.toString()).toBe(kept._id.toString());
|
|
234
|
+
expect(added._id.toString()).toBe(saved?.notes[0]._id.toString());
|
|
235
|
+
expect(added.createdAt).toEqual(now);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it("should create the document when appending the first note", async () => {
|
|
239
|
+
const clientId = new ObjectId().toString();
|
|
240
|
+
const now = new Date("2026-10-18T12:00:00.000Z");
|
|
241
|
+
|
|
242
|
+
const added = await appendContextNoteEntry(
|
|
243
|
+
clientId,
|
|
244
|
+
"Municipal",
|
|
245
|
+
{
|
|
246
|
+
title: "first",
|
|
247
|
+
content: "content",
|
|
248
|
+
activeFrom: now,
|
|
249
|
+
expiresAt: null,
|
|
250
|
+
},
|
|
251
|
+
now,
|
|
252
|
+
);
|
|
253
|
+
|
|
254
|
+
const saved = await getContextNotes(clientId, "Municipal");
|
|
255
|
+
expect(saved).toHaveLength(1);
|
|
256
|
+
expect(saved[0].notes).toHaveLength(1);
|
|
257
|
+
expect(saved[0].notes[0]._id.toString()).toBe(added._id.toString());
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it("should update one note and leave the others unchanged", async () => {
|
|
261
|
+
const kept = createContextNoteEntry({ title: "kept" });
|
|
262
|
+
const target = createContextNoteEntry({ title: "before" });
|
|
263
|
+
const doc = buildContextNote({ notes: [kept, target] });
|
|
264
|
+
await createContextNote(doc);
|
|
265
|
+
const now = new Date("2026-10-18T13:00:00.000Z");
|
|
266
|
+
const activeFrom = new Date("2026-10-01T00:00:00.000Z");
|
|
267
|
+
|
|
268
|
+
const updated = await updateContextNoteEntryFields(
|
|
269
|
+
doc.clientId,
|
|
270
|
+
doc.product,
|
|
271
|
+
target._id,
|
|
272
|
+
{
|
|
273
|
+
title: "after",
|
|
274
|
+
content: "changed",
|
|
275
|
+
activeFrom,
|
|
276
|
+
expiresAt: null,
|
|
277
|
+
},
|
|
278
|
+
now,
|
|
279
|
+
);
|
|
280
|
+
|
|
281
|
+
const saved = await getContextNotesCollection().findOne({
|
|
282
|
+
clientId: doc.clientId,
|
|
283
|
+
product: doc.product,
|
|
284
|
+
});
|
|
285
|
+
expect(updated?.title).toBe("after");
|
|
286
|
+
expect(updated?.updatedAt).toEqual(now);
|
|
287
|
+
expect(saved?.notes).toHaveLength(2);
|
|
288
|
+
expect(saved?.notes[0].title).toBe("kept");
|
|
289
|
+
expect(saved?.notes[1].title).toBe("after");
|
|
290
|
+
expect(saved?.notes[1].content).toBe("changed");
|
|
291
|
+
expect(saved?.notes[1].createdAt).toEqual(target.createdAt);
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
it("should return null when updating a missing note", async () => {
|
|
295
|
+
const doc = buildContextNote({ notes: [createContextNoteEntry()] });
|
|
296
|
+
await createContextNote(doc);
|
|
297
|
+
|
|
298
|
+
const updated = await updateContextNoteEntryFields(
|
|
299
|
+
doc.clientId,
|
|
300
|
+
doc.product,
|
|
301
|
+
new ObjectId(),
|
|
302
|
+
{
|
|
303
|
+
title: "missing",
|
|
304
|
+
content: "missing",
|
|
305
|
+
activeFrom: new Date(),
|
|
306
|
+
expiresAt: null,
|
|
307
|
+
},
|
|
308
|
+
);
|
|
309
|
+
|
|
310
|
+
expect(updated).toBeNull();
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
it("should pull one note and leave the others", async () => {
|
|
314
|
+
const kept = createContextNoteEntry();
|
|
315
|
+
const removed = createContextNoteEntry();
|
|
316
|
+
const doc = buildContextNote({ notes: [kept, removed] });
|
|
317
|
+
await createContextNote(doc);
|
|
318
|
+
|
|
319
|
+
const pulled = await pullContextNoteEntry(
|
|
320
|
+
doc.clientId,
|
|
321
|
+
doc.product,
|
|
322
|
+
removed._id,
|
|
323
|
+
);
|
|
324
|
+
|
|
325
|
+
const saved = await getContextNotesCollection().findOne({
|
|
326
|
+
clientId: doc.clientId,
|
|
327
|
+
product: doc.product,
|
|
328
|
+
});
|
|
329
|
+
expect(pulled).toBe(true);
|
|
330
|
+
expect(saved?.notes).toHaveLength(1);
|
|
331
|
+
expect(saved?.notes[0]._id.toString()).toBe(kept._id.toString());
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
it("should return false when pulling a missing note", async () => {
|
|
335
|
+
const doc = buildContextNote({ notes: [createContextNoteEntry()] });
|
|
336
|
+
await createContextNote(doc);
|
|
337
|
+
|
|
338
|
+
const pulled = await pullContextNoteEntry(
|
|
339
|
+
doc.clientId,
|
|
340
|
+
doc.product,
|
|
341
|
+
new ObjectId(),
|
|
342
|
+
);
|
|
343
|
+
|
|
344
|
+
expect(pulled).toBe(false);
|
|
345
|
+
});
|
|
346
|
+
|
|
206
347
|
it("should return false when deleteContextNoteEntry targets a non-existent note", async () => {
|
|
207
348
|
const doc = buildContextNote({ notes: [] });
|
|
208
349
|
const id = await createContextNote(doc);
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { Collection, ObjectId, WithId } from "mongodb";
|
|
2
2
|
import { getDb } from "../index";
|
|
3
|
-
import type {
|
|
3
|
+
import type {
|
|
4
|
+
ContextNote,
|
|
5
|
+
ContextNoteEntry,
|
|
6
|
+
ContextNoteEntryWrite,
|
|
7
|
+
} from "./contextNotes.types";
|
|
4
8
|
|
|
5
9
|
export const getContextNotesCollection = (): Collection<ContextNote> => {
|
|
6
10
|
return getDb().collection<ContextNote>("contextNotes");
|
|
@@ -71,6 +75,76 @@ export const setContextNoteConfig = async (
|
|
|
71
75
|
return result.matchedCount > 0;
|
|
72
76
|
};
|
|
73
77
|
|
|
78
|
+
const findNoteEntryById = (
|
|
79
|
+
doc: WithId<ContextNote> | null,
|
|
80
|
+
noteId: ObjectId,
|
|
81
|
+
): ContextNoteEntry | null =>
|
|
82
|
+
doc?.notes.find((note) => note._id.equals(noteId)) ?? null;
|
|
83
|
+
|
|
84
|
+
/** Inserts one note at the start. Other notes on the document are left untouched. */
|
|
85
|
+
export const appendContextNoteEntry = async (
|
|
86
|
+
clientId: string,
|
|
87
|
+
product: string,
|
|
88
|
+
entry: ContextNoteEntryWrite,
|
|
89
|
+
now: Date = new Date(),
|
|
90
|
+
): Promise<ContextNoteEntry> => {
|
|
91
|
+
const note: ContextNoteEntry = {
|
|
92
|
+
_id: new ObjectId(),
|
|
93
|
+
title: entry.title,
|
|
94
|
+
content: entry.content,
|
|
95
|
+
activeFrom: entry.activeFrom,
|
|
96
|
+
expiresAt: entry.expiresAt,
|
|
97
|
+
createdAt: now,
|
|
98
|
+
updatedAt: now,
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const saved = await getContextNotesCollection().findOneAndUpdate(
|
|
102
|
+
{ clientId, product },
|
|
103
|
+
{ $push: { notes: { $each: [note], $position: 0 } } },
|
|
104
|
+
{ upsert: true, returnDocument: "after" },
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
return findNoteEntryById(saved, note._id) ?? note;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
/** Replaces one note's fields. Other notes on the document are left untouched. */
|
|
111
|
+
export const updateContextNoteEntryFields = async (
|
|
112
|
+
clientId: string,
|
|
113
|
+
product: string,
|
|
114
|
+
noteId: ObjectId,
|
|
115
|
+
entry: ContextNoteEntryWrite,
|
|
116
|
+
now: Date = new Date(),
|
|
117
|
+
): Promise<ContextNoteEntry | null> => {
|
|
118
|
+
const saved = await getContextNotesCollection().findOneAndUpdate(
|
|
119
|
+
{ clientId, product, "notes._id": noteId },
|
|
120
|
+
{
|
|
121
|
+
$set: {
|
|
122
|
+
"notes.$.title": entry.title,
|
|
123
|
+
"notes.$.content": entry.content,
|
|
124
|
+
"notes.$.activeFrom": entry.activeFrom,
|
|
125
|
+
"notes.$.expiresAt": entry.expiresAt,
|
|
126
|
+
"notes.$.updatedAt": now,
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
{ returnDocument: "after" },
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
return findNoteEntryById(saved, noteId);
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
/** Removes one note by id. Other notes on the document are left untouched. */
|
|
136
|
+
export const pullContextNoteEntry = async (
|
|
137
|
+
clientId: string,
|
|
138
|
+
product: string,
|
|
139
|
+
noteId: ObjectId,
|
|
140
|
+
): Promise<boolean> => {
|
|
141
|
+
const result = await getContextNotesCollection().updateOne(
|
|
142
|
+
{ clientId, product, "notes._id": noteId },
|
|
143
|
+
{ $pull: { notes: { _id: noteId } } },
|
|
144
|
+
);
|
|
145
|
+
return result.modifiedCount > 0;
|
|
146
|
+
};
|
|
147
|
+
|
|
74
148
|
/** Removes a single note entry from a document by its _id. */
|
|
75
149
|
export const deleteContextNoteEntry = async (
|
|
76
150
|
id: ObjectId,
|
|
@@ -10,6 +10,13 @@ export type ContextNoteEntry = {
|
|
|
10
10
|
updatedAt: Date;
|
|
11
11
|
};
|
|
12
12
|
|
|
13
|
+
export type ContextNoteEntryWrite = {
|
|
14
|
+
title: string;
|
|
15
|
+
content: string;
|
|
16
|
+
activeFrom: Date;
|
|
17
|
+
expiresAt: Date | null;
|
|
18
|
+
};
|
|
19
|
+
|
|
13
20
|
export type ContextNote = {
|
|
14
21
|
_id: ObjectId;
|
|
15
22
|
clientId: string;
|
package/src/talkpilot/index.ts
CHANGED