@hasna/contacts 0.1.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.
- package/LICENSE +180 -0
- package/README.md +93 -0
- package/dashboard/dist/assets/index-B4ndI7Qt.js +49 -0
- package/dashboard/dist/assets/index-C5bn2HWO.css +1 -0
- package/dashboard/dist/index.html +13 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +3992 -0
- package/dist/db/activity.d.ts +15 -0
- package/dist/db/activity.d.ts.map +1 -0
- package/dist/db/companies.d.ts +13 -0
- package/dist/db/companies.d.ts.map +1 -0
- package/dist/db/contacts.d.ts +13 -0
- package/dist/db/contacts.d.ts.map +1 -0
- package/dist/db/database.d.ts +6 -0
- package/dist/db/database.d.ts.map +1 -0
- package/dist/db/relationships.d.ts +11 -0
- package/dist/db/relationships.d.ts.map +1 -0
- package/dist/db/tags.d.ts +14 -0
- package/dist/db/tags.d.ts.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +999 -0
- package/dist/lib/export.d.ts +8 -0
- package/dist/lib/export.d.ts.map +1 -0
- package/dist/lib/import.d.ts +9 -0
- package/dist/lib/import.d.ts.map +1 -0
- package/dist/mcp/index.d.ts +3 -0
- package/dist/mcp/index.d.ts.map +1 -0
- package/dist/mcp/index.js +1875 -0
- package/dist/server/index.d.ts +3 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +1476 -0
- package/dist/server/serve.d.ts +2 -0
- package/dist/server/serve.d.ts.map +1 -0
- package/dist/types/index.d.ts +375 -0
- package/dist/types/index.d.ts.map +1 -0
- package/package.json +60 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,999 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// src/db/database.ts
|
|
3
|
+
import { Database } from "bun:sqlite";
|
|
4
|
+
import { existsSync, mkdirSync } from "fs";
|
|
5
|
+
import { dirname, join, resolve } from "path";
|
|
6
|
+
function getDbPath() {
|
|
7
|
+
if (process.env["CONTACTS_DB_PATH"])
|
|
8
|
+
return process.env["CONTACTS_DB_PATH"];
|
|
9
|
+
const home = process.env["HOME"] || "~";
|
|
10
|
+
return join(home, ".contacts", "contacts.db");
|
|
11
|
+
}
|
|
12
|
+
function ensureDir(filePath) {
|
|
13
|
+
if (filePath === ":memory:")
|
|
14
|
+
return;
|
|
15
|
+
const dir = dirname(resolve(filePath));
|
|
16
|
+
if (!existsSync(dir))
|
|
17
|
+
mkdirSync(dir, { recursive: true });
|
|
18
|
+
}
|
|
19
|
+
var MIGRATIONS = [
|
|
20
|
+
`
|
|
21
|
+
CREATE TABLE IF NOT EXISTS companies (
|
|
22
|
+
id TEXT PRIMARY KEY,
|
|
23
|
+
name TEXT NOT NULL,
|
|
24
|
+
domain TEXT,
|
|
25
|
+
logo_url TEXT,
|
|
26
|
+
description TEXT,
|
|
27
|
+
industry TEXT,
|
|
28
|
+
size TEXT,
|
|
29
|
+
founded_year INTEGER,
|
|
30
|
+
notes TEXT,
|
|
31
|
+
custom_fields TEXT NOT NULL DEFAULT '{}',
|
|
32
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
33
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
CREATE TABLE IF NOT EXISTS contacts (
|
|
37
|
+
id TEXT PRIMARY KEY,
|
|
38
|
+
first_name TEXT NOT NULL DEFAULT '',
|
|
39
|
+
last_name TEXT NOT NULL DEFAULT '',
|
|
40
|
+
display_name TEXT NOT NULL,
|
|
41
|
+
nickname TEXT,
|
|
42
|
+
avatar_url TEXT,
|
|
43
|
+
notes TEXT,
|
|
44
|
+
birthday TEXT,
|
|
45
|
+
company_id TEXT REFERENCES companies(id) ON DELETE SET NULL,
|
|
46
|
+
job_title TEXT,
|
|
47
|
+
source TEXT NOT NULL DEFAULT 'manual',
|
|
48
|
+
custom_fields TEXT NOT NULL DEFAULT '{}',
|
|
49
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
50
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
CREATE TABLE IF NOT EXISTS tags (
|
|
54
|
+
id TEXT PRIMARY KEY,
|
|
55
|
+
name TEXT NOT NULL UNIQUE,
|
|
56
|
+
color TEXT NOT NULL DEFAULT '#6366f1',
|
|
57
|
+
description TEXT,
|
|
58
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
CREATE TABLE IF NOT EXISTS contact_tags (
|
|
62
|
+
contact_id TEXT NOT NULL REFERENCES contacts(id) ON DELETE CASCADE,
|
|
63
|
+
tag_id TEXT NOT NULL REFERENCES tags(id) ON DELETE CASCADE,
|
|
64
|
+
PRIMARY KEY (contact_id, tag_id)
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
CREATE TABLE IF NOT EXISTS company_tags (
|
|
68
|
+
company_id TEXT NOT NULL REFERENCES companies(id) ON DELETE CASCADE,
|
|
69
|
+
tag_id TEXT NOT NULL REFERENCES tags(id) ON DELETE CASCADE,
|
|
70
|
+
PRIMARY KEY (company_id, tag_id)
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
CREATE TABLE IF NOT EXISTS emails (
|
|
74
|
+
id TEXT PRIMARY KEY,
|
|
75
|
+
contact_id TEXT REFERENCES contacts(id) ON DELETE CASCADE,
|
|
76
|
+
company_id TEXT REFERENCES companies(id) ON DELETE CASCADE,
|
|
77
|
+
address TEXT NOT NULL,
|
|
78
|
+
type TEXT NOT NULL DEFAULT 'work' CHECK(type IN ('work','personal','other')),
|
|
79
|
+
is_primary INTEGER NOT NULL DEFAULT 0,
|
|
80
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
CREATE TABLE IF NOT EXISTS phones (
|
|
84
|
+
id TEXT PRIMARY KEY,
|
|
85
|
+
contact_id TEXT REFERENCES contacts(id) ON DELETE CASCADE,
|
|
86
|
+
company_id TEXT REFERENCES companies(id) ON DELETE CASCADE,
|
|
87
|
+
number TEXT NOT NULL,
|
|
88
|
+
country_code TEXT,
|
|
89
|
+
type TEXT NOT NULL DEFAULT 'mobile' CHECK(type IN ('mobile','work','home','fax','whatsapp','other')),
|
|
90
|
+
is_primary INTEGER NOT NULL DEFAULT 0,
|
|
91
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
CREATE TABLE IF NOT EXISTS addresses (
|
|
95
|
+
id TEXT PRIMARY KEY,
|
|
96
|
+
contact_id TEXT REFERENCES contacts(id) ON DELETE CASCADE,
|
|
97
|
+
company_id TEXT REFERENCES companies(id) ON DELETE CASCADE,
|
|
98
|
+
type TEXT NOT NULL DEFAULT 'physical' CHECK(type IN ('physical','mailing','billing','virtual','other')),
|
|
99
|
+
street TEXT,
|
|
100
|
+
city TEXT,
|
|
101
|
+
state TEXT,
|
|
102
|
+
zip TEXT,
|
|
103
|
+
country TEXT,
|
|
104
|
+
is_primary INTEGER NOT NULL DEFAULT 0,
|
|
105
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
CREATE TABLE IF NOT EXISTS social_profiles (
|
|
109
|
+
id TEXT PRIMARY KEY,
|
|
110
|
+
contact_id TEXT REFERENCES contacts(id) ON DELETE CASCADE,
|
|
111
|
+
company_id TEXT REFERENCES companies(id) ON DELETE CASCADE,
|
|
112
|
+
platform TEXT NOT NULL CHECK(platform IN ('twitter','linkedin','github','instagram','telegram','discord','youtube','tiktok','bluesky','facebook','whatsapp','snapchat','reddit','other')),
|
|
113
|
+
handle TEXT,
|
|
114
|
+
url TEXT,
|
|
115
|
+
is_primary INTEGER NOT NULL DEFAULT 0,
|
|
116
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
CREATE TABLE IF NOT EXISTS contact_relationships (
|
|
120
|
+
id TEXT PRIMARY KEY,
|
|
121
|
+
contact_a_id TEXT NOT NULL REFERENCES contacts(id) ON DELETE CASCADE,
|
|
122
|
+
contact_b_id TEXT NOT NULL REFERENCES contacts(id) ON DELETE CASCADE,
|
|
123
|
+
relationship_type TEXT NOT NULL CHECK(relationship_type IN ('colleague','friend','family','reports_to','mentor','investor','partner','client','vendor','other')),
|
|
124
|
+
notes TEXT,
|
|
125
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
CREATE TABLE IF NOT EXISTS activity_log (
|
|
129
|
+
id TEXT PRIMARY KEY,
|
|
130
|
+
contact_id TEXT REFERENCES contacts(id) ON DELETE CASCADE,
|
|
131
|
+
company_id TEXT REFERENCES companies(id) ON DELETE CASCADE,
|
|
132
|
+
action TEXT NOT NULL,
|
|
133
|
+
details TEXT,
|
|
134
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
CREATE TABLE IF NOT EXISTS webhooks (
|
|
138
|
+
id TEXT PRIMARY KEY,
|
|
139
|
+
url TEXT NOT NULL,
|
|
140
|
+
events TEXT NOT NULL DEFAULT '["*"]',
|
|
141
|
+
secret TEXT,
|
|
142
|
+
active INTEGER NOT NULL DEFAULT 1,
|
|
143
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS contacts_fts USING fts5(
|
|
147
|
+
id UNINDEXED,
|
|
148
|
+
display_name,
|
|
149
|
+
first_name,
|
|
150
|
+
last_name,
|
|
151
|
+
nickname,
|
|
152
|
+
notes,
|
|
153
|
+
job_title,
|
|
154
|
+
content='contacts',
|
|
155
|
+
content_rowid='rowid'
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
CREATE TRIGGER IF NOT EXISTS contacts_fts_insert AFTER INSERT ON contacts BEGIN
|
|
159
|
+
INSERT INTO contacts_fts(rowid, id, display_name, first_name, last_name, nickname, notes, job_title)
|
|
160
|
+
VALUES (new.rowid, new.id, new.display_name, new.first_name, new.last_name, new.nickname, new.notes, new.job_title);
|
|
161
|
+
END;
|
|
162
|
+
|
|
163
|
+
CREATE TRIGGER IF NOT EXISTS contacts_fts_update AFTER UPDATE ON contacts BEGIN
|
|
164
|
+
DELETE FROM contacts_fts WHERE rowid = old.rowid;
|
|
165
|
+
INSERT INTO contacts_fts(rowid, id, display_name, first_name, last_name, nickname, notes, job_title)
|
|
166
|
+
VALUES (new.rowid, new.id, new.display_name, new.first_name, new.last_name, new.nickname, new.notes, new.job_title);
|
|
167
|
+
END;
|
|
168
|
+
|
|
169
|
+
CREATE TRIGGER IF NOT EXISTS contacts_fts_delete AFTER DELETE ON contacts BEGIN
|
|
170
|
+
DELETE FROM contacts_fts WHERE rowid = old.rowid;
|
|
171
|
+
END;
|
|
172
|
+
|
|
173
|
+
CREATE TABLE IF NOT EXISTS _migrations (version INTEGER PRIMARY KEY);
|
|
174
|
+
`
|
|
175
|
+
];
|
|
176
|
+
var _db = null;
|
|
177
|
+
function getDatabase(path) {
|
|
178
|
+
if (_db)
|
|
179
|
+
return _db;
|
|
180
|
+
const dbPath = path || getDbPath();
|
|
181
|
+
ensureDir(dbPath);
|
|
182
|
+
const db = new Database(dbPath, { create: true });
|
|
183
|
+
db.exec("PRAGMA journal_mode=WAL");
|
|
184
|
+
db.exec("PRAGMA foreign_keys=ON");
|
|
185
|
+
runMigrations(db);
|
|
186
|
+
_db = db;
|
|
187
|
+
return db;
|
|
188
|
+
}
|
|
189
|
+
function resetDatabase() {
|
|
190
|
+
_db = null;
|
|
191
|
+
}
|
|
192
|
+
function uuid() {
|
|
193
|
+
return crypto.randomUUID();
|
|
194
|
+
}
|
|
195
|
+
function now() {
|
|
196
|
+
return new Date().toISOString();
|
|
197
|
+
}
|
|
198
|
+
function runMigrations(db) {
|
|
199
|
+
try {
|
|
200
|
+
const row = db.query("SELECT MAX(version) as v FROM _migrations").get();
|
|
201
|
+
const current = row?.v ?? -1;
|
|
202
|
+
for (let i = current + 1;i < MIGRATIONS.length; i++) {
|
|
203
|
+
db.exec(MIGRATIONS[i]);
|
|
204
|
+
db.exec(`INSERT OR REPLACE INTO _migrations(version) VALUES(${i})`);
|
|
205
|
+
}
|
|
206
|
+
} catch {
|
|
207
|
+
for (const m of MIGRATIONS) {
|
|
208
|
+
try {
|
|
209
|
+
db.exec(m);
|
|
210
|
+
} catch {}
|
|
211
|
+
}
|
|
212
|
+
try {
|
|
213
|
+
db.exec(`INSERT OR REPLACE INTO _migrations(version) VALUES(${MIGRATIONS.length - 1})`);
|
|
214
|
+
} catch {}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
// src/types/index.ts
|
|
218
|
+
class ContactNotFoundError extends Error {
|
|
219
|
+
constructor(id) {
|
|
220
|
+
super(`Contact not found: ${id}`);
|
|
221
|
+
this.name = "ContactNotFoundError";
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
class CompanyNotFoundError extends Error {
|
|
226
|
+
constructor(id) {
|
|
227
|
+
super(`Company not found: ${id}`);
|
|
228
|
+
this.name = "CompanyNotFoundError";
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
class TagNotFoundError extends Error {
|
|
233
|
+
constructor(id) {
|
|
234
|
+
super(`Tag not found: ${id}`);
|
|
235
|
+
this.name = "TagNotFoundError";
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
class DuplicateTagNameError extends Error {
|
|
240
|
+
constructor(name) {
|
|
241
|
+
super(`Tag with name already exists: ${name}`);
|
|
242
|
+
this.name = "DuplicateTagNameError";
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// src/db/activity.ts
|
|
247
|
+
function rowToActivity(row) {
|
|
248
|
+
return { ...row };
|
|
249
|
+
}
|
|
250
|
+
function logActivity(db, input) {
|
|
251
|
+
const id = uuid();
|
|
252
|
+
db.run(`INSERT INTO activity_log (id, contact_id, company_id, action, details) VALUES (?, ?, ?, ?, ?)`, [id, input.contact_id ?? null, input.company_id ?? null, input.action, input.details ?? null]);
|
|
253
|
+
return db.query(`SELECT * FROM activity_log WHERE id = ?`).get(id);
|
|
254
|
+
}
|
|
255
|
+
function listActivity(opts = {}, db) {
|
|
256
|
+
const d = db || getDatabase();
|
|
257
|
+
const { limit = 50, offset = 0, contact_id, company_id } = opts;
|
|
258
|
+
const conditions = [];
|
|
259
|
+
const params = [];
|
|
260
|
+
if (contact_id) {
|
|
261
|
+
conditions.push("contact_id = ?");
|
|
262
|
+
params.push(contact_id);
|
|
263
|
+
}
|
|
264
|
+
if (company_id) {
|
|
265
|
+
conditions.push("company_id = ?");
|
|
266
|
+
params.push(company_id);
|
|
267
|
+
}
|
|
268
|
+
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
269
|
+
const totalRow = d.query(`SELECT COUNT(*) as total FROM activity_log ${where}`).get(...params);
|
|
270
|
+
const rows = d.query(`SELECT * FROM activity_log ${where} ORDER BY created_at DESC LIMIT ? OFFSET ?`).all(...params, limit, offset);
|
|
271
|
+
return { entries: rows.map(rowToActivity), total: totalRow.total };
|
|
272
|
+
}
|
|
273
|
+
function getActivity(id, db) {
|
|
274
|
+
const d = db || getDatabase();
|
|
275
|
+
const row = d.query(`SELECT * FROM activity_log WHERE id = ?`).get(id);
|
|
276
|
+
return row ? rowToActivity(row) : null;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// src/db/contacts.ts
|
|
280
|
+
function rowToContact(row) {
|
|
281
|
+
return {
|
|
282
|
+
...row,
|
|
283
|
+
source: row.source,
|
|
284
|
+
custom_fields: JSON.parse(row.custom_fields || "{}")
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
function rowToEmail(row) {
|
|
288
|
+
return {
|
|
289
|
+
...row,
|
|
290
|
+
type: row.type,
|
|
291
|
+
is_primary: !!row.is_primary
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
function rowToPhone(row) {
|
|
295
|
+
return {
|
|
296
|
+
...row,
|
|
297
|
+
type: row.type,
|
|
298
|
+
is_primary: !!row.is_primary
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
function rowToAddress(row) {
|
|
302
|
+
return {
|
|
303
|
+
...row,
|
|
304
|
+
type: row.type,
|
|
305
|
+
is_primary: !!row.is_primary
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
function rowToSocialProfile(row) {
|
|
309
|
+
return {
|
|
310
|
+
...row,
|
|
311
|
+
platform: row.platform,
|
|
312
|
+
is_primary: !!row.is_primary
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
function rowToTag(row) {
|
|
316
|
+
return { ...row };
|
|
317
|
+
}
|
|
318
|
+
function rowToCompany(row) {
|
|
319
|
+
return {
|
|
320
|
+
...row,
|
|
321
|
+
custom_fields: JSON.parse(row.custom_fields || "{}")
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
function insertEmails(db, contactId, companyId, emails) {
|
|
325
|
+
for (const e of emails) {
|
|
326
|
+
db.run(`INSERT INTO emails (id, contact_id, company_id, address, type, is_primary) VALUES (?, ?, ?, ?, ?, ?)`, [uuid(), contactId, companyId, e.address, e.type ?? "work", e.is_primary ? 1 : 0]);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
function insertPhones(db, contactId, companyId, phones) {
|
|
330
|
+
for (const p of phones) {
|
|
331
|
+
db.run(`INSERT INTO phones (id, contact_id, company_id, number, country_code, type, is_primary) VALUES (?, ?, ?, ?, ?, ?, ?)`, [uuid(), contactId, companyId, p.number, p.country_code ?? null, p.type ?? "mobile", p.is_primary ? 1 : 0]);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
function insertAddresses(db, contactId, companyId, addresses) {
|
|
335
|
+
for (const a of addresses) {
|
|
336
|
+
db.run(`INSERT INTO addresses (id, contact_id, company_id, type, street, city, state, zip, country, is_primary) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [uuid(), contactId, companyId, a.type ?? "physical", a.street ?? null, a.city ?? null, a.state ?? null, a.zip ?? null, a.country ?? null, a.is_primary ? 1 : 0]);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
function insertSocialProfiles(db, contactId, companyId, profiles) {
|
|
340
|
+
for (const s of profiles) {
|
|
341
|
+
db.run(`INSERT INTO social_profiles (id, contact_id, company_id, platform, handle, url, is_primary) VALUES (?, ?, ?, ?, ?, ?, ?)`, [uuid(), contactId, companyId, s.platform, s.handle ?? null, s.url ?? null, s.is_primary ? 1 : 0]);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
function loadContactDetails(db, contact) {
|
|
345
|
+
const emails = db.query(`SELECT * FROM emails WHERE contact_id = ?`).all(contact.id).map(rowToEmail);
|
|
346
|
+
const phones = db.query(`SELECT * FROM phones WHERE contact_id = ?`).all(contact.id).map(rowToPhone);
|
|
347
|
+
const addresses = db.query(`SELECT * FROM addresses WHERE contact_id = ?`).all(contact.id).map(rowToAddress);
|
|
348
|
+
const social_profiles = db.query(`SELECT * FROM social_profiles WHERE contact_id = ?`).all(contact.id).map(rowToSocialProfile);
|
|
349
|
+
const tags = db.query(`
|
|
350
|
+
SELECT t.* FROM tags t
|
|
351
|
+
JOIN contact_tags ct ON ct.tag_id = t.id
|
|
352
|
+
WHERE ct.contact_id = ?
|
|
353
|
+
`).all(contact.id).map(rowToTag);
|
|
354
|
+
const companyRow = contact.company_id ? db.query(`SELECT * FROM companies WHERE id = ?`).get(contact.company_id) : null;
|
|
355
|
+
const company = companyRow ? rowToCompany(companyRow) : null;
|
|
356
|
+
return { ...contact, emails, phones, addresses, social_profiles, tags, company };
|
|
357
|
+
}
|
|
358
|
+
function createContact(input, db) {
|
|
359
|
+
const d = db || getDatabase();
|
|
360
|
+
const id = uuid();
|
|
361
|
+
const timestamp = now();
|
|
362
|
+
const firstName = input.first_name ?? "";
|
|
363
|
+
const lastName = input.last_name ?? "";
|
|
364
|
+
const displayName = input.display_name ?? (firstName || lastName ? `${firstName} ${lastName}`.trim() : "Unnamed Contact");
|
|
365
|
+
d.run(`INSERT INTO contacts (id, first_name, last_name, display_name, nickname, avatar_url, notes, birthday, company_id, job_title, source, custom_fields, created_at, updated_at)
|
|
366
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
367
|
+
id,
|
|
368
|
+
firstName,
|
|
369
|
+
lastName,
|
|
370
|
+
displayName,
|
|
371
|
+
input.nickname ?? null,
|
|
372
|
+
input.avatar_url ?? null,
|
|
373
|
+
input.notes ?? null,
|
|
374
|
+
input.birthday ?? null,
|
|
375
|
+
input.company_id ?? null,
|
|
376
|
+
input.job_title ?? null,
|
|
377
|
+
input.source ?? "manual",
|
|
378
|
+
JSON.stringify(input.custom_fields ?? {}),
|
|
379
|
+
timestamp,
|
|
380
|
+
timestamp
|
|
381
|
+
]);
|
|
382
|
+
if (input.emails?.length)
|
|
383
|
+
insertEmails(d, id, null, input.emails);
|
|
384
|
+
if (input.phones?.length)
|
|
385
|
+
insertPhones(d, id, null, input.phones);
|
|
386
|
+
if (input.addresses?.length)
|
|
387
|
+
insertAddresses(d, id, null, input.addresses);
|
|
388
|
+
if (input.social_profiles?.length)
|
|
389
|
+
insertSocialProfiles(d, id, null, input.social_profiles);
|
|
390
|
+
if (input.tag_ids?.length) {
|
|
391
|
+
for (const tagId of input.tag_ids) {
|
|
392
|
+
d.run(`INSERT OR IGNORE INTO contact_tags (contact_id, tag_id) VALUES (?, ?)`, [id, tagId]);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
logActivity(d, { contact_id: id, action: "contact.created", details: `Created contact: ${displayName}` });
|
|
396
|
+
const row = d.query(`SELECT * FROM contacts WHERE id = ?`).get(id);
|
|
397
|
+
return loadContactDetails(d, rowToContact(row));
|
|
398
|
+
}
|
|
399
|
+
function getContact(id, db) {
|
|
400
|
+
const d = db || getDatabase();
|
|
401
|
+
const row = d.query(`SELECT * FROM contacts WHERE id = ?`).get(id);
|
|
402
|
+
if (!row)
|
|
403
|
+
throw new ContactNotFoundError(id);
|
|
404
|
+
return loadContactDetails(d, rowToContact(row));
|
|
405
|
+
}
|
|
406
|
+
function listContacts(opts = {}, db) {
|
|
407
|
+
const d = db || getDatabase();
|
|
408
|
+
const {
|
|
409
|
+
limit = 50,
|
|
410
|
+
offset = 0,
|
|
411
|
+
company_id,
|
|
412
|
+
tag_id,
|
|
413
|
+
source,
|
|
414
|
+
order_by = "display_name",
|
|
415
|
+
order_dir = "asc"
|
|
416
|
+
} = opts;
|
|
417
|
+
const conditions = [];
|
|
418
|
+
const params = [];
|
|
419
|
+
if (company_id) {
|
|
420
|
+
conditions.push("c.company_id = ?");
|
|
421
|
+
params.push(company_id);
|
|
422
|
+
}
|
|
423
|
+
if (source) {
|
|
424
|
+
conditions.push("c.source = ?");
|
|
425
|
+
params.push(source);
|
|
426
|
+
}
|
|
427
|
+
if (tag_id) {
|
|
428
|
+
conditions.push("EXISTS (SELECT 1 FROM contact_tags ct WHERE ct.contact_id = c.id AND ct.tag_id = ?)");
|
|
429
|
+
params.push(tag_id);
|
|
430
|
+
}
|
|
431
|
+
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
432
|
+
const validOrderBy = ["display_name", "created_at", "updated_at"].includes(order_by) ? order_by : "display_name";
|
|
433
|
+
const validOrderDir = order_dir === "desc" ? "DESC" : "ASC";
|
|
434
|
+
const totalRow = d.query(`SELECT COUNT(*) as total FROM contacts c ${where}`).get(...params);
|
|
435
|
+
const rows = d.query(`SELECT c.* FROM contacts c ${where} ORDER BY c.${validOrderBy} ${validOrderDir} LIMIT ? OFFSET ?`).all(...params, limit, offset);
|
|
436
|
+
const contacts = rows.map((row) => loadContactDetails(d, rowToContact(row)));
|
|
437
|
+
return { contacts, total: totalRow.total };
|
|
438
|
+
}
|
|
439
|
+
function updateContact(id, input, db) {
|
|
440
|
+
const d = db || getDatabase();
|
|
441
|
+
const existing = d.query(`SELECT * FROM contacts WHERE id = ?`).get(id);
|
|
442
|
+
if (!existing)
|
|
443
|
+
throw new ContactNotFoundError(id);
|
|
444
|
+
const setClauses = ["updated_at = ?"];
|
|
445
|
+
const params = [now()];
|
|
446
|
+
if (input.first_name !== undefined) {
|
|
447
|
+
setClauses.push("first_name = ?");
|
|
448
|
+
params.push(input.first_name);
|
|
449
|
+
}
|
|
450
|
+
if (input.last_name !== undefined) {
|
|
451
|
+
setClauses.push("last_name = ?");
|
|
452
|
+
params.push(input.last_name);
|
|
453
|
+
}
|
|
454
|
+
if (input.display_name !== undefined) {
|
|
455
|
+
setClauses.push("display_name = ?");
|
|
456
|
+
params.push(input.display_name);
|
|
457
|
+
}
|
|
458
|
+
if (input.nickname !== undefined) {
|
|
459
|
+
setClauses.push("nickname = ?");
|
|
460
|
+
params.push(input.nickname);
|
|
461
|
+
}
|
|
462
|
+
if (input.avatar_url !== undefined) {
|
|
463
|
+
setClauses.push("avatar_url = ?");
|
|
464
|
+
params.push(input.avatar_url);
|
|
465
|
+
}
|
|
466
|
+
if (input.notes !== undefined) {
|
|
467
|
+
setClauses.push("notes = ?");
|
|
468
|
+
params.push(input.notes);
|
|
469
|
+
}
|
|
470
|
+
if (input.birthday !== undefined) {
|
|
471
|
+
setClauses.push("birthday = ?");
|
|
472
|
+
params.push(input.birthday);
|
|
473
|
+
}
|
|
474
|
+
if (input.company_id !== undefined) {
|
|
475
|
+
setClauses.push("company_id = ?");
|
|
476
|
+
params.push(input.company_id);
|
|
477
|
+
}
|
|
478
|
+
if (input.job_title !== undefined) {
|
|
479
|
+
setClauses.push("job_title = ?");
|
|
480
|
+
params.push(input.job_title);
|
|
481
|
+
}
|
|
482
|
+
if (input.source !== undefined) {
|
|
483
|
+
setClauses.push("source = ?");
|
|
484
|
+
params.push(input.source);
|
|
485
|
+
}
|
|
486
|
+
if (input.custom_fields !== undefined) {
|
|
487
|
+
setClauses.push("custom_fields = ?");
|
|
488
|
+
params.push(JSON.stringify(input.custom_fields));
|
|
489
|
+
}
|
|
490
|
+
params.push(id);
|
|
491
|
+
d.run(`UPDATE contacts SET ${setClauses.join(", ")} WHERE id = ?`, params);
|
|
492
|
+
logActivity(d, { contact_id: id, action: "contact.updated", details: `Updated contact: ${existing.display_name}` });
|
|
493
|
+
const row = d.query(`SELECT * FROM contacts WHERE id = ?`).get(id);
|
|
494
|
+
return loadContactDetails(d, rowToContact(row));
|
|
495
|
+
}
|
|
496
|
+
function deleteContact(id, db) {
|
|
497
|
+
const d = db || getDatabase();
|
|
498
|
+
const row = d.query(`SELECT * FROM contacts WHERE id = ?`).get(id);
|
|
499
|
+
if (!row)
|
|
500
|
+
throw new ContactNotFoundError(id);
|
|
501
|
+
logActivity(d, { contact_id: id, action: "contact.deleted", details: `Deleted contact: ${row.display_name}` });
|
|
502
|
+
d.run(`DELETE FROM contacts WHERE id = ?`, [id]);
|
|
503
|
+
}
|
|
504
|
+
function searchContacts(query, db) {
|
|
505
|
+
const d = db || getDatabase();
|
|
506
|
+
const ftsRows = d.query(`
|
|
507
|
+
SELECT c.* FROM contacts c
|
|
508
|
+
JOIN contacts_fts fts ON fts.id = c.id
|
|
509
|
+
WHERE contacts_fts MATCH ?
|
|
510
|
+
ORDER BY rank
|
|
511
|
+
LIMIT 50
|
|
512
|
+
`).all(`"${query.replace(/"/g, '""')}"*`);
|
|
513
|
+
const emailRows = d.query(`
|
|
514
|
+
SELECT DISTINCT c.* FROM contacts c
|
|
515
|
+
JOIN emails e ON e.contact_id = c.id
|
|
516
|
+
WHERE e.address LIKE ?
|
|
517
|
+
LIMIT 20
|
|
518
|
+
`).all(`%${query}%`);
|
|
519
|
+
const phoneRows = d.query(`
|
|
520
|
+
SELECT DISTINCT c.* FROM contacts c
|
|
521
|
+
JOIN phones p ON p.contact_id = c.id
|
|
522
|
+
WHERE p.number LIKE ?
|
|
523
|
+
LIMIT 20
|
|
524
|
+
`).all(`%${query}%`);
|
|
525
|
+
const seen = new Set;
|
|
526
|
+
const allRows = [];
|
|
527
|
+
for (const row of [...ftsRows, ...emailRows, ...phoneRows]) {
|
|
528
|
+
if (!seen.has(row.id)) {
|
|
529
|
+
seen.add(row.id);
|
|
530
|
+
allRows.push(row);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
return allRows.map((row) => loadContactDetails(d, rowToContact(row)));
|
|
534
|
+
}
|
|
535
|
+
function mergeContacts(keepId, mergeId, db) {
|
|
536
|
+
const d = db || getDatabase();
|
|
537
|
+
const keepRow = d.query(`SELECT * FROM contacts WHERE id = ?`).get(keepId);
|
|
538
|
+
if (!keepRow)
|
|
539
|
+
throw new ContactNotFoundError(keepId);
|
|
540
|
+
const mergeRow = d.query(`SELECT * FROM contacts WHERE id = ?`).get(mergeId);
|
|
541
|
+
if (!mergeRow)
|
|
542
|
+
throw new ContactNotFoundError(mergeId);
|
|
543
|
+
d.run(`UPDATE emails SET contact_id = ? WHERE contact_id = ?`, [keepId, mergeId]);
|
|
544
|
+
d.run(`UPDATE phones SET contact_id = ? WHERE contact_id = ?`, [keepId, mergeId]);
|
|
545
|
+
d.run(`UPDATE addresses SET contact_id = ? WHERE contact_id = ?`, [keepId, mergeId]);
|
|
546
|
+
d.run(`UPDATE social_profiles SET contact_id = ? WHERE contact_id = ?`, [keepId, mergeId]);
|
|
547
|
+
const mergeTags = d.query(`SELECT tag_id FROM contact_tags WHERE contact_id = ?`).all(mergeId);
|
|
548
|
+
for (const { tag_id } of mergeTags) {
|
|
549
|
+
d.run(`INSERT OR IGNORE INTO contact_tags (contact_id, tag_id) VALUES (?, ?)`, [keepId, tag_id]);
|
|
550
|
+
}
|
|
551
|
+
d.run(`UPDATE contact_relationships SET contact_a_id = ? WHERE contact_a_id = ?`, [keepId, mergeId]);
|
|
552
|
+
d.run(`UPDATE contact_relationships SET contact_b_id = ? WHERE contact_b_id = ?`, [keepId, mergeId]);
|
|
553
|
+
d.run(`UPDATE activity_log SET contact_id = ? WHERE contact_id = ?`, [keepId, mergeId]);
|
|
554
|
+
d.run(`DELETE FROM contacts WHERE id = ?`, [mergeId]);
|
|
555
|
+
const updates = ["updated_at = ?"];
|
|
556
|
+
const params = [now()];
|
|
557
|
+
if (!keepRow.notes && mergeRow.notes) {
|
|
558
|
+
updates.push("notes = ?");
|
|
559
|
+
params.push(mergeRow.notes);
|
|
560
|
+
}
|
|
561
|
+
if (!keepRow.nickname && mergeRow.nickname) {
|
|
562
|
+
updates.push("nickname = ?");
|
|
563
|
+
params.push(mergeRow.nickname);
|
|
564
|
+
}
|
|
565
|
+
if (!keepRow.avatar_url && mergeRow.avatar_url) {
|
|
566
|
+
updates.push("avatar_url = ?");
|
|
567
|
+
params.push(mergeRow.avatar_url);
|
|
568
|
+
}
|
|
569
|
+
if (!keepRow.birthday && mergeRow.birthday) {
|
|
570
|
+
updates.push("birthday = ?");
|
|
571
|
+
params.push(mergeRow.birthday);
|
|
572
|
+
}
|
|
573
|
+
if (!keepRow.company_id && mergeRow.company_id) {
|
|
574
|
+
updates.push("company_id = ?");
|
|
575
|
+
params.push(mergeRow.company_id);
|
|
576
|
+
}
|
|
577
|
+
if (!keepRow.job_title && mergeRow.job_title) {
|
|
578
|
+
updates.push("job_title = ?");
|
|
579
|
+
params.push(mergeRow.job_title);
|
|
580
|
+
}
|
|
581
|
+
params.push(keepId);
|
|
582
|
+
d.run(`UPDATE contacts SET ${updates.join(", ")} WHERE id = ?`, params);
|
|
583
|
+
logActivity(d, {
|
|
584
|
+
contact_id: keepId,
|
|
585
|
+
action: "contact.merged",
|
|
586
|
+
details: `Merged contact ${mergeRow.display_name} (${mergeId}) into ${keepRow.display_name} (${keepId})`
|
|
587
|
+
});
|
|
588
|
+
const finalRow = d.query(`SELECT * FROM contacts WHERE id = ?`).get(keepId);
|
|
589
|
+
return loadContactDetails(d, rowToContact(finalRow));
|
|
590
|
+
}
|
|
591
|
+
// src/db/companies.ts
|
|
592
|
+
function rowToCompany2(row) {
|
|
593
|
+
return {
|
|
594
|
+
...row,
|
|
595
|
+
custom_fields: JSON.parse(row.custom_fields || "{}")
|
|
596
|
+
};
|
|
597
|
+
}
|
|
598
|
+
function insertEmails2(db, companyId, emails) {
|
|
599
|
+
for (const e of emails) {
|
|
600
|
+
db.run(`INSERT INTO emails (id, contact_id, company_id, address, type, is_primary) VALUES (?, ?, ?, ?, ?, ?)`, [uuid(), null, companyId, e.address, e.type ?? "work", e.is_primary ? 1 : 0]);
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
function insertPhones2(db, companyId, phones) {
|
|
604
|
+
for (const p of phones) {
|
|
605
|
+
db.run(`INSERT INTO phones (id, contact_id, company_id, number, country_code, type, is_primary) VALUES (?, ?, ?, ?, ?, ?, ?)`, [uuid(), null, companyId, p.number, p.country_code ?? null, p.type ?? "work", p.is_primary ? 1 : 0]);
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
function insertAddresses2(db, companyId, addresses) {
|
|
609
|
+
for (const a of addresses) {
|
|
610
|
+
db.run(`INSERT INTO addresses (id, contact_id, company_id, type, street, city, state, zip, country, is_primary) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [uuid(), null, companyId, a.type ?? "physical", a.street ?? null, a.city ?? null, a.state ?? null, a.zip ?? null, a.country ?? null, a.is_primary ? 1 : 0]);
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
function insertSocialProfiles2(db, companyId, profiles) {
|
|
614
|
+
for (const s of profiles) {
|
|
615
|
+
db.run(`INSERT INTO social_profiles (id, contact_id, company_id, platform, handle, url, is_primary) VALUES (?, ?, ?, ?, ?, ?, ?)`, [uuid(), null, companyId, s.platform, s.handle ?? null, s.url ?? null, s.is_primary ? 1 : 0]);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
function loadCompanyDetails(db, company) {
|
|
619
|
+
const emails = db.query(`SELECT * FROM emails WHERE company_id = ?`).all(company.id).map((row) => ({
|
|
620
|
+
...row,
|
|
621
|
+
type: row.type,
|
|
622
|
+
is_primary: !!row.is_primary
|
|
623
|
+
}));
|
|
624
|
+
const phones = db.query(`SELECT * FROM phones WHERE company_id = ?`).all(company.id).map((row) => ({
|
|
625
|
+
...row,
|
|
626
|
+
type: row.type,
|
|
627
|
+
is_primary: !!row.is_primary
|
|
628
|
+
}));
|
|
629
|
+
const addresses = db.query(`SELECT * FROM addresses WHERE company_id = ?`).all(company.id).map((row) => ({
|
|
630
|
+
...row,
|
|
631
|
+
type: row.type,
|
|
632
|
+
is_primary: !!row.is_primary
|
|
633
|
+
}));
|
|
634
|
+
const social_profiles = db.query(`SELECT * FROM social_profiles WHERE company_id = ?`).all(company.id).map((row) => ({
|
|
635
|
+
...row,
|
|
636
|
+
platform: row.platform,
|
|
637
|
+
is_primary: !!row.is_primary
|
|
638
|
+
}));
|
|
639
|
+
const tags = db.query(`
|
|
640
|
+
SELECT t.* FROM tags t
|
|
641
|
+
JOIN company_tags ct ON ct.tag_id = t.id
|
|
642
|
+
WHERE ct.company_id = ?
|
|
643
|
+
`).all(company.id);
|
|
644
|
+
const empCount = db.query(`SELECT COUNT(*) as count FROM contacts WHERE company_id = ?`).get(company.id);
|
|
645
|
+
return {
|
|
646
|
+
...company,
|
|
647
|
+
emails,
|
|
648
|
+
phones,
|
|
649
|
+
addresses,
|
|
650
|
+
social_profiles,
|
|
651
|
+
tags,
|
|
652
|
+
employee_count: empCount.count
|
|
653
|
+
};
|
|
654
|
+
}
|
|
655
|
+
function createCompany(input, db) {
|
|
656
|
+
const d = db || getDatabase();
|
|
657
|
+
const id = uuid();
|
|
658
|
+
const timestamp = now();
|
|
659
|
+
d.run(`INSERT INTO companies (id, name, domain, logo_url, description, industry, size, founded_year, notes, custom_fields, created_at, updated_at)
|
|
660
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
661
|
+
id,
|
|
662
|
+
input.name,
|
|
663
|
+
input.domain ?? null,
|
|
664
|
+
input.logo_url ?? null,
|
|
665
|
+
input.description ?? null,
|
|
666
|
+
input.industry ?? null,
|
|
667
|
+
input.size ?? null,
|
|
668
|
+
input.founded_year ?? null,
|
|
669
|
+
input.notes ?? null,
|
|
670
|
+
JSON.stringify(input.custom_fields ?? {}),
|
|
671
|
+
timestamp,
|
|
672
|
+
timestamp
|
|
673
|
+
]);
|
|
674
|
+
if (input.emails?.length)
|
|
675
|
+
insertEmails2(d, id, input.emails);
|
|
676
|
+
if (input.phones?.length)
|
|
677
|
+
insertPhones2(d, id, input.phones);
|
|
678
|
+
if (input.addresses?.length)
|
|
679
|
+
insertAddresses2(d, id, input.addresses);
|
|
680
|
+
if (input.social_profiles?.length)
|
|
681
|
+
insertSocialProfiles2(d, id, input.social_profiles);
|
|
682
|
+
if (input.tag_ids?.length) {
|
|
683
|
+
for (const tagId of input.tag_ids) {
|
|
684
|
+
d.run(`INSERT OR IGNORE INTO company_tags (company_id, tag_id) VALUES (?, ?)`, [id, tagId]);
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
logActivity(d, { company_id: id, action: "company.created", details: `Created company: ${input.name}` });
|
|
688
|
+
const row = d.query(`SELECT * FROM companies WHERE id = ?`).get(id);
|
|
689
|
+
return loadCompanyDetails(d, rowToCompany2(row));
|
|
690
|
+
}
|
|
691
|
+
function getCompany(id, db) {
|
|
692
|
+
const d = db || getDatabase();
|
|
693
|
+
const row = d.query(`SELECT * FROM companies WHERE id = ?`).get(id);
|
|
694
|
+
if (!row)
|
|
695
|
+
throw new CompanyNotFoundError(id);
|
|
696
|
+
return loadCompanyDetails(d, rowToCompany2(row));
|
|
697
|
+
}
|
|
698
|
+
function listCompanies(opts = {}, db) {
|
|
699
|
+
const d = db || getDatabase();
|
|
700
|
+
const {
|
|
701
|
+
limit = 50,
|
|
702
|
+
offset = 0,
|
|
703
|
+
industry,
|
|
704
|
+
tag_id,
|
|
705
|
+
order_by = "name",
|
|
706
|
+
order_dir = "asc"
|
|
707
|
+
} = opts;
|
|
708
|
+
const conditions = [];
|
|
709
|
+
const params = [];
|
|
710
|
+
if (industry) {
|
|
711
|
+
conditions.push("co.industry = ?");
|
|
712
|
+
params.push(industry);
|
|
713
|
+
}
|
|
714
|
+
if (tag_id) {
|
|
715
|
+
conditions.push("EXISTS (SELECT 1 FROM company_tags ct WHERE ct.company_id = co.id AND ct.tag_id = ?)");
|
|
716
|
+
params.push(tag_id);
|
|
717
|
+
}
|
|
718
|
+
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
719
|
+
const validOrderBy = ["name", "created_at", "updated_at"].includes(order_by) ? order_by : "name";
|
|
720
|
+
const validOrderDir = order_dir === "desc" ? "DESC" : "ASC";
|
|
721
|
+
const totalRow = d.query(`SELECT COUNT(*) as total FROM companies co ${where}`).get(...params);
|
|
722
|
+
const rows = d.query(`SELECT co.* FROM companies co ${where} ORDER BY co.${validOrderBy} ${validOrderDir} LIMIT ? OFFSET ?`).all(...params, limit, offset);
|
|
723
|
+
const companies = rows.map((row) => loadCompanyDetails(d, rowToCompany2(row)));
|
|
724
|
+
return { companies, total: totalRow.total };
|
|
725
|
+
}
|
|
726
|
+
function updateCompany(id, input, db) {
|
|
727
|
+
const d = db || getDatabase();
|
|
728
|
+
const existing = d.query(`SELECT * FROM companies WHERE id = ?`).get(id);
|
|
729
|
+
if (!existing)
|
|
730
|
+
throw new CompanyNotFoundError(id);
|
|
731
|
+
const setClauses = ["updated_at = ?"];
|
|
732
|
+
const params = [now()];
|
|
733
|
+
if (input.name !== undefined) {
|
|
734
|
+
setClauses.push("name = ?");
|
|
735
|
+
params.push(input.name);
|
|
736
|
+
}
|
|
737
|
+
if (input.domain !== undefined) {
|
|
738
|
+
setClauses.push("domain = ?");
|
|
739
|
+
params.push(input.domain);
|
|
740
|
+
}
|
|
741
|
+
if (input.logo_url !== undefined) {
|
|
742
|
+
setClauses.push("logo_url = ?");
|
|
743
|
+
params.push(input.logo_url);
|
|
744
|
+
}
|
|
745
|
+
if (input.description !== undefined) {
|
|
746
|
+
setClauses.push("description = ?");
|
|
747
|
+
params.push(input.description);
|
|
748
|
+
}
|
|
749
|
+
if (input.industry !== undefined) {
|
|
750
|
+
setClauses.push("industry = ?");
|
|
751
|
+
params.push(input.industry);
|
|
752
|
+
}
|
|
753
|
+
if (input.size !== undefined) {
|
|
754
|
+
setClauses.push("size = ?");
|
|
755
|
+
params.push(input.size);
|
|
756
|
+
}
|
|
757
|
+
if (input.founded_year !== undefined) {
|
|
758
|
+
setClauses.push("founded_year = ?");
|
|
759
|
+
params.push(input.founded_year);
|
|
760
|
+
}
|
|
761
|
+
if (input.notes !== undefined) {
|
|
762
|
+
setClauses.push("notes = ?");
|
|
763
|
+
params.push(input.notes);
|
|
764
|
+
}
|
|
765
|
+
if (input.custom_fields !== undefined) {
|
|
766
|
+
setClauses.push("custom_fields = ?");
|
|
767
|
+
params.push(JSON.stringify(input.custom_fields));
|
|
768
|
+
}
|
|
769
|
+
params.push(id);
|
|
770
|
+
d.run(`UPDATE companies SET ${setClauses.join(", ")} WHERE id = ?`, params);
|
|
771
|
+
logActivity(d, { company_id: id, action: "company.updated", details: `Updated company: ${existing.name}` });
|
|
772
|
+
const row = d.query(`SELECT * FROM companies WHERE id = ?`).get(id);
|
|
773
|
+
return loadCompanyDetails(d, rowToCompany2(row));
|
|
774
|
+
}
|
|
775
|
+
function deleteCompany(id, db) {
|
|
776
|
+
const d = db || getDatabase();
|
|
777
|
+
const row = d.query(`SELECT * FROM companies WHERE id = ?`).get(id);
|
|
778
|
+
if (!row)
|
|
779
|
+
throw new CompanyNotFoundError(id);
|
|
780
|
+
logActivity(d, { company_id: id, action: "company.deleted", details: `Deleted company: ${row.name}` });
|
|
781
|
+
d.run(`DELETE FROM companies WHERE id = ?`, [id]);
|
|
782
|
+
}
|
|
783
|
+
function searchCompanies(query, db) {
|
|
784
|
+
const d = db || getDatabase();
|
|
785
|
+
const rows = d.query(`
|
|
786
|
+
SELECT * FROM companies
|
|
787
|
+
WHERE name LIKE ? OR domain LIKE ? OR description LIKE ? OR industry LIKE ?
|
|
788
|
+
LIMIT 50
|
|
789
|
+
`).all(`%${query}%`, `%${query}%`, `%${query}%`, `%${query}%`);
|
|
790
|
+
return rows.map((row) => loadCompanyDetails(d, rowToCompany2(row)));
|
|
791
|
+
}
|
|
792
|
+
function listCompanyEmployees(companyId, db) {
|
|
793
|
+
const d = db || getDatabase();
|
|
794
|
+
const row = d.query(`SELECT id FROM companies WHERE id = ?`).get(companyId);
|
|
795
|
+
if (!row)
|
|
796
|
+
throw new CompanyNotFoundError(companyId);
|
|
797
|
+
const rows = d.query(`SELECT * FROM contacts WHERE company_id = ? ORDER BY display_name ASC`).all(companyId);
|
|
798
|
+
return rows.map((r) => ({
|
|
799
|
+
...r,
|
|
800
|
+
source: r.source,
|
|
801
|
+
custom_fields: JSON.parse(r.custom_fields || "{}")
|
|
802
|
+
}));
|
|
803
|
+
}
|
|
804
|
+
// src/db/tags.ts
|
|
805
|
+
function rowToTag2(row) {
|
|
806
|
+
return { ...row };
|
|
807
|
+
}
|
|
808
|
+
function createTag(input, db) {
|
|
809
|
+
const d = db || getDatabase();
|
|
810
|
+
const existing = d.query(`SELECT id FROM tags WHERE name = ?`).get(input.name);
|
|
811
|
+
if (existing)
|
|
812
|
+
throw new DuplicateTagNameError(input.name);
|
|
813
|
+
const id = uuid();
|
|
814
|
+
d.run(`INSERT INTO tags (id, name, color, description) VALUES (?, ?, ?, ?)`, [id, input.name, input.color ?? "#6366f1", input.description ?? null]);
|
|
815
|
+
return rowToTag2(d.query(`SELECT * FROM tags WHERE id = ?`).get(id));
|
|
816
|
+
}
|
|
817
|
+
function getTag(id, db) {
|
|
818
|
+
const d = db || getDatabase();
|
|
819
|
+
const row = d.query(`SELECT * FROM tags WHERE id = ?`).get(id);
|
|
820
|
+
if (!row)
|
|
821
|
+
throw new TagNotFoundError(id);
|
|
822
|
+
return rowToTag2(row);
|
|
823
|
+
}
|
|
824
|
+
function getTagByName(name, db) {
|
|
825
|
+
const d = db || getDatabase();
|
|
826
|
+
const row = d.query(`SELECT * FROM tags WHERE name = ?`).get(name);
|
|
827
|
+
return row ? rowToTag2(row) : null;
|
|
828
|
+
}
|
|
829
|
+
function listTags(db) {
|
|
830
|
+
const d = db || getDatabase();
|
|
831
|
+
return d.query(`SELECT * FROM tags ORDER BY name ASC`).all().map(rowToTag2);
|
|
832
|
+
}
|
|
833
|
+
function updateTag(id, input, db) {
|
|
834
|
+
const d = db || getDatabase();
|
|
835
|
+
const existing = d.query(`SELECT * FROM tags WHERE id = ?`).get(id);
|
|
836
|
+
if (!existing)
|
|
837
|
+
throw new TagNotFoundError(id);
|
|
838
|
+
if (input.name && input.name !== existing.name) {
|
|
839
|
+
const dupe = d.query(`SELECT id FROM tags WHERE name = ? AND id != ?`).get(input.name, id);
|
|
840
|
+
if (dupe)
|
|
841
|
+
throw new DuplicateTagNameError(input.name);
|
|
842
|
+
}
|
|
843
|
+
const setClauses = [];
|
|
844
|
+
const params = [];
|
|
845
|
+
if (input.name !== undefined) {
|
|
846
|
+
setClauses.push("name = ?");
|
|
847
|
+
params.push(input.name);
|
|
848
|
+
}
|
|
849
|
+
if (input.color !== undefined) {
|
|
850
|
+
setClauses.push("color = ?");
|
|
851
|
+
params.push(input.color);
|
|
852
|
+
}
|
|
853
|
+
if (input.description !== undefined) {
|
|
854
|
+
setClauses.push("description = ?");
|
|
855
|
+
params.push(input.description);
|
|
856
|
+
}
|
|
857
|
+
if (setClauses.length > 0) {
|
|
858
|
+
params.push(id);
|
|
859
|
+
d.run(`UPDATE tags SET ${setClauses.join(", ")} WHERE id = ?`, params);
|
|
860
|
+
}
|
|
861
|
+
return rowToTag2(d.query(`SELECT * FROM tags WHERE id = ?`).get(id));
|
|
862
|
+
}
|
|
863
|
+
function deleteTag(id, db) {
|
|
864
|
+
const d = db || getDatabase();
|
|
865
|
+
const row = d.query(`SELECT id FROM tags WHERE id = ?`).get(id);
|
|
866
|
+
if (!row)
|
|
867
|
+
throw new TagNotFoundError(id);
|
|
868
|
+
d.run(`DELETE FROM tags WHERE id = ?`, [id]);
|
|
869
|
+
}
|
|
870
|
+
function addTagToContact(contactId, tagId, db) {
|
|
871
|
+
const d = db || getDatabase();
|
|
872
|
+
const contact = d.query(`SELECT id FROM contacts WHERE id = ?`).get(contactId);
|
|
873
|
+
if (!contact)
|
|
874
|
+
throw new ContactNotFoundError(contactId);
|
|
875
|
+
const tag = d.query(`SELECT id FROM tags WHERE id = ?`).get(tagId);
|
|
876
|
+
if (!tag)
|
|
877
|
+
throw new TagNotFoundError(tagId);
|
|
878
|
+
d.run(`INSERT OR IGNORE INTO contact_tags (contact_id, tag_id) VALUES (?, ?)`, [contactId, tagId]);
|
|
879
|
+
}
|
|
880
|
+
function removeTagFromContact(contactId, tagId, db) {
|
|
881
|
+
const d = db || getDatabase();
|
|
882
|
+
d.run(`DELETE FROM contact_tags WHERE contact_id = ? AND tag_id = ?`, [contactId, tagId]);
|
|
883
|
+
}
|
|
884
|
+
function listContactsByTag(tagId, db) {
|
|
885
|
+
const d = db || getDatabase();
|
|
886
|
+
const tag = d.query(`SELECT id FROM tags WHERE id = ?`).get(tagId);
|
|
887
|
+
if (!tag)
|
|
888
|
+
throw new TagNotFoundError(tagId);
|
|
889
|
+
const rows = d.query(`
|
|
890
|
+
SELECT c.* FROM contacts c
|
|
891
|
+
JOIN contact_tags ct ON ct.contact_id = c.id
|
|
892
|
+
WHERE ct.tag_id = ?
|
|
893
|
+
ORDER BY c.display_name ASC
|
|
894
|
+
`).all(tagId);
|
|
895
|
+
return rows.map((r) => ({
|
|
896
|
+
...r,
|
|
897
|
+
source: r.source,
|
|
898
|
+
custom_fields: JSON.parse(r.custom_fields || "{}")
|
|
899
|
+
}));
|
|
900
|
+
}
|
|
901
|
+
function addTagToCompany(companyId, tagId, db) {
|
|
902
|
+
const d = db || getDatabase();
|
|
903
|
+
const company = d.query(`SELECT id FROM companies WHERE id = ?`).get(companyId);
|
|
904
|
+
if (!company)
|
|
905
|
+
throw new CompanyNotFoundError(companyId);
|
|
906
|
+
const tag = d.query(`SELECT id FROM tags WHERE id = ?`).get(tagId);
|
|
907
|
+
if (!tag)
|
|
908
|
+
throw new TagNotFoundError(tagId);
|
|
909
|
+
d.run(`INSERT OR IGNORE INTO company_tags (company_id, tag_id) VALUES (?, ?)`, [companyId, tagId]);
|
|
910
|
+
}
|
|
911
|
+
function removeTagFromCompany(companyId, tagId, db) {
|
|
912
|
+
const d = db || getDatabase();
|
|
913
|
+
d.run(`DELETE FROM company_tags WHERE company_id = ? AND tag_id = ?`, [companyId, tagId]);
|
|
914
|
+
}
|
|
915
|
+
// src/db/relationships.ts
|
|
916
|
+
function rowToRelationship(row) {
|
|
917
|
+
return {
|
|
918
|
+
...row,
|
|
919
|
+
relationship_type: row.relationship_type
|
|
920
|
+
};
|
|
921
|
+
}
|
|
922
|
+
function createRelationship(input, db) {
|
|
923
|
+
const d = db || getDatabase();
|
|
924
|
+
const a = d.query(`SELECT id FROM contacts WHERE id = ?`).get(input.contact_a_id);
|
|
925
|
+
if (!a)
|
|
926
|
+
throw new ContactNotFoundError(input.contact_a_id);
|
|
927
|
+
const b = d.query(`SELECT id FROM contacts WHERE id = ?`).get(input.contact_b_id);
|
|
928
|
+
if (!b)
|
|
929
|
+
throw new ContactNotFoundError(input.contact_b_id);
|
|
930
|
+
const id = uuid();
|
|
931
|
+
d.run(`INSERT INTO contact_relationships (id, contact_a_id, contact_b_id, relationship_type, notes) VALUES (?, ?, ?, ?, ?)`, [id, input.contact_a_id, input.contact_b_id, input.relationship_type, input.notes ?? null]);
|
|
932
|
+
return rowToRelationship(d.query(`SELECT * FROM contact_relationships WHERE id = ?`).get(id));
|
|
933
|
+
}
|
|
934
|
+
function listRelationships(opts = {}, db) {
|
|
935
|
+
const d = db || getDatabase();
|
|
936
|
+
const { contact_id, relationship_type } = opts;
|
|
937
|
+
const conditions = [];
|
|
938
|
+
const params = [];
|
|
939
|
+
if (contact_id) {
|
|
940
|
+
conditions.push("(contact_a_id = ? OR contact_b_id = ?)");
|
|
941
|
+
params.push(contact_id, contact_id);
|
|
942
|
+
}
|
|
943
|
+
if (relationship_type) {
|
|
944
|
+
conditions.push("relationship_type = ?");
|
|
945
|
+
params.push(relationship_type);
|
|
946
|
+
}
|
|
947
|
+
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
948
|
+
const rows = d.query(`SELECT * FROM contact_relationships ${where} ORDER BY created_at DESC`).all(...params);
|
|
949
|
+
return rows.map(rowToRelationship);
|
|
950
|
+
}
|
|
951
|
+
function getRelationship(id, db) {
|
|
952
|
+
const d = db || getDatabase();
|
|
953
|
+
const row = d.query(`SELECT * FROM contact_relationships WHERE id = ?`).get(id);
|
|
954
|
+
return row ? rowToRelationship(row) : null;
|
|
955
|
+
}
|
|
956
|
+
function deleteRelationship(id, db) {
|
|
957
|
+
const d = db || getDatabase();
|
|
958
|
+
d.run(`DELETE FROM contact_relationships WHERE id = ?`, [id]);
|
|
959
|
+
}
|
|
960
|
+
export {
|
|
961
|
+
updateTag,
|
|
962
|
+
updateContact,
|
|
963
|
+
updateCompany,
|
|
964
|
+
searchContacts,
|
|
965
|
+
searchCompanies,
|
|
966
|
+
resetDatabase,
|
|
967
|
+
removeTagFromContact,
|
|
968
|
+
removeTagFromCompany,
|
|
969
|
+
mergeContacts,
|
|
970
|
+
logActivity,
|
|
971
|
+
listTags,
|
|
972
|
+
listRelationships,
|
|
973
|
+
listContactsByTag,
|
|
974
|
+
listContacts,
|
|
975
|
+
listCompanyEmployees,
|
|
976
|
+
listCompanies,
|
|
977
|
+
listActivity,
|
|
978
|
+
getTagByName,
|
|
979
|
+
getTag,
|
|
980
|
+
getRelationship,
|
|
981
|
+
getDatabase,
|
|
982
|
+
getContact,
|
|
983
|
+
getCompany,
|
|
984
|
+
getActivity,
|
|
985
|
+
deleteTag,
|
|
986
|
+
deleteRelationship,
|
|
987
|
+
deleteContact,
|
|
988
|
+
deleteCompany,
|
|
989
|
+
createTag,
|
|
990
|
+
createRelationship,
|
|
991
|
+
createContact,
|
|
992
|
+
createCompany,
|
|
993
|
+
addTagToContact,
|
|
994
|
+
addTagToCompany,
|
|
995
|
+
TagNotFoundError,
|
|
996
|
+
DuplicateTagNameError,
|
|
997
|
+
ContactNotFoundError,
|
|
998
|
+
CompanyNotFoundError
|
|
999
|
+
};
|