@voyantjs/crm 0.1.1 → 0.3.0

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.
Files changed (37) hide show
  1. package/dist/routes/accounts.d.ts +1 -1
  2. package/dist/routes/index.d.ts +2 -2
  3. package/dist/routes/opportunities.d.ts +1 -1
  4. package/dist/schema-accounts.d.ts +1175 -0
  5. package/dist/schema-accounts.d.ts.map +1 -0
  6. package/dist/schema-accounts.js +112 -0
  7. package/dist/schema-activities.d.ts +821 -0
  8. package/dist/schema-activities.d.ts.map +1 -0
  9. package/dist/schema-activities.js +83 -0
  10. package/dist/schema-relations.d.ts +81 -0
  11. package/dist/schema-relations.d.ts.map +1 -0
  12. package/dist/schema-relations.js +123 -0
  13. package/dist/schema-sales.d.ts +1392 -0
  14. package/dist/schema-sales.d.ts.map +1 -0
  15. package/dist/schema-sales.js +142 -0
  16. package/dist/schema-shared.d.ts +13 -0
  17. package/dist/schema-shared.d.ts.map +1 -0
  18. package/dist/schema-shared.js +63 -0
  19. package/dist/schema.d.ts +5 -3477
  20. package/dist/schema.d.ts.map +1 -1
  21. package/dist/schema.js +5 -515
  22. package/dist/service/accounts-organizations.d.ts +93 -0
  23. package/dist/service/accounts-organizations.d.ts.map +1 -0
  24. package/dist/service/accounts-organizations.js +49 -0
  25. package/dist/service/accounts-people.d.ts +868 -0
  26. package/dist/service/accounts-people.d.ts.map +1 -0
  27. package/dist/service/accounts-people.js +311 -0
  28. package/dist/service/accounts-shared.d.ts +54 -0
  29. package/dist/service/accounts-shared.d.ts.map +1 -0
  30. package/dist/service/accounts-shared.js +152 -0
  31. package/dist/service/accounts.d.ts +121 -149
  32. package/dist/service/accounts.d.ts.map +1 -1
  33. package/dist/service/accounts.js +4 -507
  34. package/dist/service/index.d.ts +106 -297
  35. package/dist/service/index.d.ts.map +1 -1
  36. package/dist/service/opportunities.d.ts +1 -1
  37. package/package.json +5 -5
@@ -0,0 +1 @@
1
+ {"version":3,"file":"accounts-people.d.ts","sourceRoot":"","sources":["../../src/service/accounts-people.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAA;AAUjE,OAAO,EACL,KAAK,sBAAsB,EAC3B,KAAK,kBAAkB,EACvB,KAAK,2BAA2B,EAChC,KAAK,uBAAuB,EAC5B,KAAK,2BAA2B,EAChC,KAAK,iBAAiB,EACtB,KAAK,qBAAqB,EAC1B,KAAK,kBAAkB,EAIvB,KAAK,eAAe,EAIpB,KAAK,kBAAkB,EACvB,KAAK,uBAAuB,EAC5B,KAAK,iBAAiB,EACvB,MAAM,sBAAsB,CAAA;AAG7B,eAAO,MAAM,qBAAqB;mBACX,kBAAkB,SAAS,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;sBAsCvC,kBAAkB,MAAM,MAAM;;;;;;;;;;;;;;;;;;;;qBAO/B,kBAAkB,QAAQ,iBAAiB;;;;;;;;;;;;;;;;;;;;qBAgB3C,kBAAkB,MAAM,MAAM,QAAQ,iBAAiB;;;;;;;;;;;;;;;;;;;;qBAqBvD,kBAAkB,MAAM,MAAM;;;2BAO/C,kBAAkB,cACV,cAAc,GAAG,QAAQ,YAC3B,MAAM;;;;;;;;;;;;;;4BAUZ,kBAAkB,cACV,cAAc,GAAG,QAAQ,YAC3B,MAAM,QACV,uBAAuB;;;;;;;;;;;;;;4BASD,kBAAkB,MAAM,MAAM,QAAQ,uBAAuB;;;;;;;;;;;;;;4BAI7D,kBAAkB,MAAM,MAAM;;;sBAI1C,kBAAkB,cAAc,cAAc,GAAG,QAAQ,YAAY,MAAM;;;;;;;;;;;;;;;;;;;;;sBASvF,kBAAkB,cACV,cAAc,GAAG,QAAQ,YAC3B,MAAM,QACV,kBAAkB;;;;;;;;;;;;;;;;;;;;;sBASF,kBAAkB,MAAM,MAAM,QAAQ,kBAAkB;;;;;;;;;;;;;;;;;;;;;sBAIxD,kBAAkB,MAAM,MAAM;;;wBAIlC,kBAAkB,YAAY,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;yBASlD,kBAAkB,YACZ,MAAM,UACR,MAAM,QACR,qBAAqB;;;;;;;8BAgBH,kBAAkB,kBAAkB,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;+BAS9D,kBAAkB,kBACN,MAAM,UACd,MAAM,QACR,2BAA2B;;;;;;;yBAgBR,kBAAkB,MAAM,MAAM,WAAW,MAAM;;;;;;;yBAS/C,kBAAkB,MAAM,MAAM;;;;;;;+BAKxB,kBAAkB,MAAM,MAAM,WAAW,MAAM;;;;;;;+BAS/C,kBAAkB,MAAM,MAAM;;;;;;;2BAMzD,kBAAkB,YACZ,MAAM,SACT,sBAAsB;;;;;;;;;;;4BAmBzB,kBAAkB,YACZ,MAAM,QACV,2BAA2B;;;;;;;;;;;qBAwBlB,kBAAkB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;sBAIX,kBAAkB,QAAQ,kBAAkB;;;;;;;;sBAK5C,kBAAkB,aAAa,MAAM;;;wBAQnC,kBAAkB;wBAoClB,kBAAkB,WAAW,MAAM;;;;;;;iBAuBtC,MAAM;mBAAS,MAAM;;;;CA+B7C,CAAA"}
@@ -0,0 +1,311 @@
1
+ import { identityService } from "@voyantjs/identity/service";
2
+ import { and, desc, eq, gte, ilike, lte, or, sql } from "drizzle-orm";
3
+ import { communicationLog, organizationNotes, organizations, people, personNotes, segments, } from "../schema.js";
4
+ import { deletePersonIdentity, hydratePeople, organizationEntityType, personBaseFields, personEntityType, syncPersonIdentity, } from "./accounts-shared.js";
5
+ import { paginate } from "./helpers.js";
6
+ export const peopleAccountsService = {
7
+ async listPeople(db, query) {
8
+ const conditions = [];
9
+ if (query.organizationId)
10
+ conditions.push(eq(people.organizationId, query.organizationId));
11
+ if (query.ownerId)
12
+ conditions.push(eq(people.ownerId, query.ownerId));
13
+ if (query.relation)
14
+ conditions.push(eq(people.relation, query.relation));
15
+ if (query.status)
16
+ conditions.push(eq(people.status, query.status));
17
+ if (query.search) {
18
+ const term = `%${query.search}%`;
19
+ conditions.push(or(ilike(people.firstName, term), ilike(people.lastName, term), ilike(people.jobTitle, term)));
20
+ }
21
+ const where = conditions.length ? and(...conditions) : undefined;
22
+ const result = await paginate(db
23
+ .select()
24
+ .from(people)
25
+ .where(where)
26
+ .limit(query.limit)
27
+ .offset(query.offset)
28
+ .orderBy(desc(people.updatedAt)), db.select({ count: sql `count(*)::int` }).from(people).where(where), query.limit, query.offset);
29
+ return {
30
+ ...result,
31
+ data: await hydratePeople(db, result.data),
32
+ };
33
+ },
34
+ async getPersonById(db, id) {
35
+ const [row] = await db.select().from(people).where(eq(people.id, id)).limit(1);
36
+ if (!row)
37
+ return null;
38
+ const [hydrated] = await hydratePeople(db, [row]);
39
+ return hydrated ?? null;
40
+ },
41
+ async createPerson(db, data) {
42
+ const [row] = await db
43
+ .insert(people)
44
+ .values({
45
+ ...personBaseFields(data),
46
+ firstName: data.firstName,
47
+ lastName: data.lastName,
48
+ })
49
+ .returning();
50
+ if (!row) {
51
+ throw new Error("Failed to create person");
52
+ }
53
+ await syncPersonIdentity(db, row.id, data);
54
+ return this.getPersonById(db, row.id);
55
+ },
56
+ async updatePerson(db, id, data) {
57
+ const existing = await this.getPersonById(db, id);
58
+ if (!existing)
59
+ return null;
60
+ await db
61
+ .update(people)
62
+ .set({ ...personBaseFields(data), updatedAt: new Date() })
63
+ .where(eq(people.id, id));
64
+ await syncPersonIdentity(db, id, {
65
+ email: data.email === undefined ? existing.email : data.email,
66
+ phone: data.phone === undefined ? existing.phone : data.phone,
67
+ website: data.website === undefined ? existing.website : data.website,
68
+ address: data.address === undefined ? existing.address : data.address,
69
+ city: data.city === undefined ? existing.city : data.city,
70
+ country: data.country === undefined ? existing.country : data.country,
71
+ });
72
+ return this.getPersonById(db, id);
73
+ },
74
+ async deletePerson(db, id) {
75
+ await deletePersonIdentity(db, id);
76
+ const [row] = await db.delete(people).where(eq(people.id, id)).returning({ id: people.id });
77
+ return row ?? null;
78
+ },
79
+ listContactMethods(db, entityType, entityId) {
80
+ return identityService.listContactPointsForEntity(db, entityType === "organization" ? organizationEntityType : personEntityType, entityId);
81
+ },
82
+ async createContactMethod(db, entityType, entityId, data) {
83
+ return identityService.createContactPoint(db, {
84
+ ...data,
85
+ entityType: entityType === "organization" ? organizationEntityType : personEntityType,
86
+ entityId,
87
+ });
88
+ },
89
+ async updateContactMethod(db, id, data) {
90
+ return identityService.updateContactPoint(db, id, data);
91
+ },
92
+ async deleteContactMethod(db, id) {
93
+ return identityService.deleteContactPoint(db, id);
94
+ },
95
+ listAddresses(db, entityType, entityId) {
96
+ return identityService.listAddressesForEntity(db, entityType === "organization" ? organizationEntityType : personEntityType, entityId);
97
+ },
98
+ async createAddress(db, entityType, entityId, data) {
99
+ return identityService.createAddress(db, {
100
+ ...data,
101
+ entityType: entityType === "organization" ? organizationEntityType : personEntityType,
102
+ entityId,
103
+ });
104
+ },
105
+ async updateAddress(db, id, data) {
106
+ return identityService.updateAddress(db, id, data);
107
+ },
108
+ async deleteAddress(db, id) {
109
+ return identityService.deleteAddress(db, id);
110
+ },
111
+ listPersonNotes(db, personId) {
112
+ return db
113
+ .select()
114
+ .from(personNotes)
115
+ .where(eq(personNotes.personId, personId))
116
+ .orderBy(personNotes.createdAt);
117
+ },
118
+ async createPersonNote(db, personId, userId, data) {
119
+ const [existing] = await db
120
+ .select({ id: people.id })
121
+ .from(people)
122
+ .where(eq(people.id, personId))
123
+ .limit(1);
124
+ if (!existing)
125
+ return null;
126
+ const [row] = await db
127
+ .insert(personNotes)
128
+ .values({ personId, authorId: userId, content: data.content })
129
+ .returning();
130
+ return row;
131
+ },
132
+ listOrganizationNotes(db, organizationId) {
133
+ return db
134
+ .select()
135
+ .from(organizationNotes)
136
+ .where(eq(organizationNotes.organizationId, organizationId))
137
+ .orderBy(organizationNotes.createdAt);
138
+ },
139
+ async createOrganizationNote(db, organizationId, userId, data) {
140
+ const [existing] = await db
141
+ .select({ id: organizations.id })
142
+ .from(organizations)
143
+ .where(eq(organizations.id, organizationId))
144
+ .limit(1);
145
+ if (!existing)
146
+ return null;
147
+ const [row] = await db
148
+ .insert(organizationNotes)
149
+ .values({ organizationId, authorId: userId, content: data.content })
150
+ .returning();
151
+ return row;
152
+ },
153
+ async updatePersonNote(db, id, content) {
154
+ const [row] = await db
155
+ .update(personNotes)
156
+ .set({ content })
157
+ .where(eq(personNotes.id, id))
158
+ .returning();
159
+ return row ?? null;
160
+ },
161
+ async deletePersonNote(db, id) {
162
+ const [row] = await db.delete(personNotes).where(eq(personNotes.id, id)).returning();
163
+ return row ?? null;
164
+ },
165
+ async updateOrganizationNote(db, id, content) {
166
+ const [row] = await db
167
+ .update(organizationNotes)
168
+ .set({ content })
169
+ .where(eq(organizationNotes.id, id))
170
+ .returning();
171
+ return row ?? null;
172
+ },
173
+ async deleteOrganizationNote(db, id) {
174
+ const [row] = await db.delete(organizationNotes).where(eq(organizationNotes.id, id)).returning();
175
+ return row ?? null;
176
+ },
177
+ async listCommunications(db, personId, query) {
178
+ const conditions = [eq(communicationLog.personId, personId)];
179
+ if (query.channel)
180
+ conditions.push(eq(communicationLog.channel, query.channel));
181
+ if (query.direction)
182
+ conditions.push(eq(communicationLog.direction, query.direction));
183
+ if (query.dateFrom)
184
+ conditions.push(gte(communicationLog.createdAt, new Date(query.dateFrom)));
185
+ if (query.dateTo)
186
+ conditions.push(lte(communicationLog.createdAt, new Date(query.dateTo)));
187
+ return db
188
+ .select()
189
+ .from(communicationLog)
190
+ .where(and(...conditions))
191
+ .limit(query.limit)
192
+ .offset(query.offset)
193
+ .orderBy(desc(communicationLog.createdAt));
194
+ },
195
+ async createCommunication(db, personId, data) {
196
+ const [existing] = await db
197
+ .select({ id: people.id })
198
+ .from(people)
199
+ .where(eq(people.id, personId))
200
+ .limit(1);
201
+ if (!existing)
202
+ return null;
203
+ const [row] = await db
204
+ .insert(communicationLog)
205
+ .values({
206
+ personId,
207
+ organizationId: data.organizationId ?? null,
208
+ channel: data.channel,
209
+ direction: data.direction,
210
+ subject: data.subject ?? null,
211
+ content: data.content ?? null,
212
+ sentAt: data.sentAt ? new Date(data.sentAt) : null,
213
+ })
214
+ .returning();
215
+ return row;
216
+ },
217
+ listSegments(db) {
218
+ return db.select().from(segments).orderBy(segments.createdAt);
219
+ },
220
+ async createSegment(db, data) {
221
+ const [row] = await db.insert(segments).values(data).returning();
222
+ return row;
223
+ },
224
+ async deleteSegment(db, segmentId) {
225
+ const [row] = await db
226
+ .delete(segments)
227
+ .where(eq(segments.id, segmentId))
228
+ .returning({ id: segments.id });
229
+ return row ?? null;
230
+ },
231
+ async exportPeopleCsv(db) {
232
+ const rows = await hydratePeople(db, await db.select().from(people).orderBy(people.createdAt));
233
+ const headers = [
234
+ "id",
235
+ "firstName",
236
+ "lastName",
237
+ "jobTitle",
238
+ "relation",
239
+ "preferredLanguage",
240
+ "preferredCurrency",
241
+ "email",
242
+ "phone",
243
+ "website",
244
+ "address",
245
+ "city",
246
+ "country",
247
+ "organizationId",
248
+ ];
249
+ const csvLines = [headers.join(",")];
250
+ for (const row of rows) {
251
+ const values = headers.map((header) => {
252
+ const value = row[header];
253
+ if (value === null || value === undefined)
254
+ return "";
255
+ const stringValue = String(value);
256
+ return stringValue.includes(",") || stringValue.includes('"') || stringValue.includes("\n")
257
+ ? `"${stringValue.replace(/"/g, '""')}"`
258
+ : stringValue;
259
+ });
260
+ csvLines.push(values.join(","));
261
+ }
262
+ return csvLines.join("\n");
263
+ },
264
+ async importPeopleCsv(db, csvText) {
265
+ const lines = csvText.split("\n").filter((line) => line.trim());
266
+ if (lines.length < 2) {
267
+ return { error: "CSV must have a header row and at least one data row" };
268
+ }
269
+ const headers = lines[0].split(",").map((header) => header.trim());
270
+ const rows = [];
271
+ for (let i = 1; i < lines.length; i++) {
272
+ const values = lines[i].split(",").map((value) => value.trim());
273
+ const row = {};
274
+ for (let j = 0; j < headers.length; j++) {
275
+ const header = headers[j];
276
+ const value = values[j];
277
+ if (header && value) {
278
+ row[header] = value;
279
+ }
280
+ }
281
+ rows.push(row);
282
+ }
283
+ const imported = [];
284
+ const errors = [];
285
+ for (let i = 0; i < rows.length; i++) {
286
+ const row = rows[i];
287
+ const result = (await import("../validation.js")).insertPersonSchema.safeParse({
288
+ firstName: row.firstName || "",
289
+ lastName: row.lastName || "",
290
+ jobTitle: row.jobTitle || null,
291
+ relation: row.relation || null,
292
+ preferredLanguage: row.preferredLanguage || null,
293
+ preferredCurrency: row.preferredCurrency || null,
294
+ email: row.email || null,
295
+ phone: row.phone || null,
296
+ website: row.website || null,
297
+ address: row.address || null,
298
+ city: row.city || null,
299
+ country: row.country || null,
300
+ organizationId: row.organizationId || null,
301
+ tags: [],
302
+ });
303
+ if (!result.success) {
304
+ errors.push({ row: i + 2, error: result.error.message });
305
+ continue;
306
+ }
307
+ imported.push(await this.createPerson(db, result.data));
308
+ }
309
+ return { imported: imported.length, errors };
310
+ },
311
+ };
@@ -0,0 +1,54 @@
1
+ import type { insertAddressSchema, insertContactPointSchema, updateAddressSchema, updateContactPointSchema } from "@voyantjs/identity/validation";
2
+ import type { PostgresJsDatabase } from "drizzle-orm/postgres-js";
3
+ import type { z } from "zod";
4
+ import type { communicationListQuerySchema, insertCommunicationLogSchema, insertOrganizationNoteSchema, insertOrganizationSchema, insertPersonNoteSchema, insertPersonSchema, insertSegmentSchema, organizationListQuerySchema, personListQuerySchema, updateOrganizationSchema, updatePersonSchema } from "../validation.js";
5
+ export type OrganizationListQuery = z.infer<typeof organizationListQuerySchema>;
6
+ export type CreateOrganizationInput = z.infer<typeof insertOrganizationSchema>;
7
+ export type UpdateOrganizationInput = z.infer<typeof updateOrganizationSchema>;
8
+ export type PersonListQuery = z.infer<typeof personListQuerySchema>;
9
+ export type CreatePersonInput = z.infer<typeof insertPersonSchema>;
10
+ export type UpdatePersonInput = z.infer<typeof updatePersonSchema>;
11
+ export type CreateContactPointInput = z.infer<typeof insertContactPointSchema>;
12
+ export type UpdateContactPointInput = z.infer<typeof updateContactPointSchema>;
13
+ export type CreateAddressInput = z.infer<typeof insertAddressSchema>;
14
+ export type UpdateAddressInput = z.infer<typeof updateAddressSchema>;
15
+ export type CreatePersonNoteInput = z.infer<typeof insertPersonNoteSchema>;
16
+ export type CreateOrganizationNoteInput = z.infer<typeof insertOrganizationNoteSchema>;
17
+ export type CreateCommunicationLogInput = z.infer<typeof insertCommunicationLogSchema>;
18
+ export type CommunicationListQuery = z.infer<typeof communicationListQuerySchema>;
19
+ export type CreateSegmentInput = z.infer<typeof insertSegmentSchema>;
20
+ export declare const organizationEntityType = "organization";
21
+ export declare const personEntityType = "person";
22
+ export declare const personBaseIdentitySource = "crm.person.base";
23
+ type PersonIdentityInput = Pick<CreatePersonInput, "email" | "phone" | "website" | "address" | "city" | "country">;
24
+ export type PersonHydratedFields = {
25
+ email: string | null;
26
+ phone: string | null;
27
+ website: string | null;
28
+ address: string | null;
29
+ city: string | null;
30
+ country: string | null;
31
+ };
32
+ export declare function personBaseFields(data: CreatePersonInput | UpdatePersonInput): {
33
+ organizationId: string | null | undefined;
34
+ firstName: string | undefined;
35
+ lastName: string | undefined;
36
+ jobTitle: string | null | undefined;
37
+ relation: "partner" | "supplier" | "other" | "client" | null | undefined;
38
+ preferredLanguage: string | null | undefined;
39
+ preferredCurrency: string | null | undefined;
40
+ ownerId: string | null | undefined;
41
+ status: "active" | "inactive" | "archived" | undefined;
42
+ source: string | null | undefined;
43
+ sourceRef: string | null | undefined;
44
+ tags: string[] | undefined;
45
+ birthday: string | null | undefined;
46
+ notes: string | null | undefined;
47
+ };
48
+ export declare function syncPersonIdentity(db: PostgresJsDatabase, personId: string, data: PersonIdentityInput): Promise<void>;
49
+ export declare function deletePersonIdentity(db: PostgresJsDatabase, personId: string): Promise<void>;
50
+ export declare function hydratePeople<T extends {
51
+ id: string;
52
+ }>(db: PostgresJsDatabase, rows: T[]): Promise<Array<T & PersonHydratedFields>>;
53
+ export {};
54
+ //# sourceMappingURL=accounts-shared.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"accounts-shared.d.ts","sourceRoot":"","sources":["../../src/service/accounts-shared.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,152 @@
1
+ import { identityAddresses, identityContactPoints } from "@voyantjs/identity/schema";
2
+ import { identityService } from "@voyantjs/identity/service";
3
+ import { and, eq, inArray } from "drizzle-orm";
4
+ import { formatAddress, isManagedBySource, normalizeContactValue, toNullableTrimmed, } from "./helpers.js";
5
+ export const organizationEntityType = "organization";
6
+ export const personEntityType = "person";
7
+ export const personBaseIdentitySource = "crm.person.base";
8
+ export function personBaseFields(data) {
9
+ return {
10
+ organizationId: data.organizationId,
11
+ firstName: data.firstName,
12
+ lastName: data.lastName,
13
+ jobTitle: data.jobTitle,
14
+ relation: data.relation,
15
+ preferredLanguage: data.preferredLanguage,
16
+ preferredCurrency: data.preferredCurrency,
17
+ ownerId: data.ownerId,
18
+ status: data.status,
19
+ source: data.source,
20
+ sourceRef: data.sourceRef,
21
+ tags: data.tags,
22
+ birthday: data.birthday,
23
+ notes: data.notes,
24
+ };
25
+ }
26
+ export async function syncPersonIdentity(db, personId, data) {
27
+ const existingContactPoints = await identityService.listContactPointsForEntity(db, personEntityType, personId);
28
+ const existingAddresses = await identityService.listAddressesForEntity(db, personEntityType, personId);
29
+ const managedContactPoints = existingContactPoints.filter((point) => isManagedBySource(point.metadata, personBaseIdentitySource));
30
+ const managedAddress = existingAddresses.find((address) => isManagedBySource(address.metadata, personBaseIdentitySource));
31
+ for (const [kind, rawValue] of Object.entries({
32
+ email: data.email,
33
+ phone: data.phone,
34
+ website: data.website,
35
+ })) {
36
+ const value = toNullableTrimmed(rawValue);
37
+ const existing = managedContactPoints.find((point) => point.kind === kind) ??
38
+ existingContactPoints.find((point) => point.kind === kind && point.isPrimary);
39
+ if (!value) {
40
+ if (existing) {
41
+ await identityService.deleteContactPoint(db, existing.id);
42
+ }
43
+ continue;
44
+ }
45
+ const payload = {
46
+ entityType: personEntityType,
47
+ entityId: personId,
48
+ kind,
49
+ label: kind === "website" ? "website" : "primary",
50
+ value,
51
+ normalizedValue: normalizeContactValue(kind, value),
52
+ isPrimary: true,
53
+ metadata: { managedBy: personBaseIdentitySource },
54
+ };
55
+ if (existing) {
56
+ await identityService.updateContactPoint(db, existing.id, payload);
57
+ }
58
+ else {
59
+ await identityService.createContactPoint(db, payload);
60
+ }
61
+ }
62
+ const addressLine = toNullableTrimmed(data.address);
63
+ const city = toNullableTrimmed(data.city);
64
+ const country = toNullableTrimmed(data.country);
65
+ const hasAddress = Boolean(addressLine || city || country);
66
+ if (!hasAddress) {
67
+ if (managedAddress) {
68
+ await identityService.deleteAddress(db, managedAddress.id);
69
+ }
70
+ return;
71
+ }
72
+ const addressPayload = {
73
+ entityType: personEntityType,
74
+ entityId: personId,
75
+ label: "primary",
76
+ fullText: addressLine,
77
+ line1: addressLine,
78
+ city,
79
+ country,
80
+ isPrimary: true,
81
+ metadata: { managedBy: personBaseIdentitySource },
82
+ };
83
+ if (managedAddress) {
84
+ await identityService.updateAddress(db, managedAddress.id, addressPayload);
85
+ }
86
+ else {
87
+ await identityService.createAddress(db, addressPayload);
88
+ }
89
+ }
90
+ export async function deletePersonIdentity(db, personId) {
91
+ const [contactPoints, addresses] = await Promise.all([
92
+ identityService.listContactPointsForEntity(db, personEntityType, personId),
93
+ identityService.listAddressesForEntity(db, personEntityType, personId),
94
+ ]);
95
+ await Promise.all([
96
+ ...contactPoints.map((point) => identityService.deleteContactPoint(db, point.id)),
97
+ ...addresses.map((address) => identityService.deleteAddress(db, address.id)),
98
+ ]);
99
+ }
100
+ export async function hydratePeople(db, rows) {
101
+ if (rows.length === 0) {
102
+ return rows.map((row) => ({
103
+ ...row,
104
+ email: null,
105
+ phone: null,
106
+ website: null,
107
+ address: null,
108
+ city: null,
109
+ country: null,
110
+ }));
111
+ }
112
+ const ids = rows.map((row) => row.id);
113
+ const [contactPoints, addresses] = await Promise.all([
114
+ db
115
+ .select()
116
+ .from(identityContactPoints)
117
+ .where(and(eq(identityContactPoints.entityType, personEntityType), inArray(identityContactPoints.entityId, ids))),
118
+ db
119
+ .select()
120
+ .from(identityAddresses)
121
+ .where(and(eq(identityAddresses.entityType, personEntityType), inArray(identityAddresses.entityId, ids))),
122
+ ]);
123
+ const contactPointMap = new Map();
124
+ const addressMap = new Map();
125
+ for (const point of contactPoints) {
126
+ const bucket = contactPointMap.get(point.entityId) ?? [];
127
+ bucket.push(point);
128
+ contactPointMap.set(point.entityId, bucket);
129
+ }
130
+ for (const address of addresses) {
131
+ const bucket = addressMap.get(address.entityId) ?? [];
132
+ bucket.push(address);
133
+ addressMap.set(address.entityId, bucket);
134
+ }
135
+ return rows.map((row) => {
136
+ const entityContactPoints = contactPointMap.get(row.id) ?? [];
137
+ const entityAddresses = addressMap.get(row.id) ?? [];
138
+ const findPrimaryContactPoint = (kind) => entityContactPoints.find((point) => point.kind === kind && point.isPrimary) ??
139
+ entityContactPoints.find((point) => point.kind === kind) ??
140
+ null;
141
+ const primaryAddress = entityAddresses.find((address) => address.isPrimary) ?? entityAddresses[0] ?? null;
142
+ return {
143
+ ...row,
144
+ email: findPrimaryContactPoint("email")?.value ?? null,
145
+ phone: findPrimaryContactPoint("phone")?.value ?? null,
146
+ website: findPrimaryContactPoint("website")?.value ?? null,
147
+ address: primaryAddress ? formatAddress(primaryAddress) : null,
148
+ city: primaryAddress?.city ?? null,
149
+ country: primaryAddress?.country ?? null,
150
+ };
151
+ });
152
+ }