@hasna/contacts 0.2.1 → 0.2.3

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/dist/index.js CHANGED
@@ -150,9 +150,7 @@ var MIGRATIONS = [
150
150
  last_name,
151
151
  nickname,
152
152
  notes,
153
- job_title,
154
- content='contacts',
155
- content_rowid='rowid'
153
+ job_title
156
154
  );
157
155
 
158
156
  CREATE TRIGGER IF NOT EXISTS contacts_fts_insert AFTER INSERT ON contacts BEGIN
@@ -190,6 +188,20 @@ var MIGRATIONS = [
190
188
  group_id TEXT NOT NULL REFERENCES groups(id) ON DELETE CASCADE,
191
189
  PRIMARY KEY (contact_id, group_id)
192
190
  );
191
+ `,
192
+ `
193
+ ALTER TABLE contacts ADD COLUMN status TEXT DEFAULT 'active';
194
+ ALTER TABLE contacts ADD COLUMN follow_up_at TEXT;
195
+ ALTER TABLE contacts ADD COLUMN archived INTEGER NOT NULL DEFAULT 0;
196
+ ALTER TABLE contacts ADD COLUMN project_id TEXT;
197
+ ALTER TABLE companies ADD COLUMN archived INTEGER NOT NULL DEFAULT 0;
198
+ ALTER TABLE companies ADD COLUMN project_id TEXT;
199
+
200
+ CREATE TABLE IF NOT EXISTS company_groups (
201
+ company_id TEXT NOT NULL REFERENCES companies(id) ON DELETE CASCADE,
202
+ group_id TEXT NOT NULL REFERENCES groups(id) ON DELETE CASCADE,
203
+ PRIMARY KEY (company_id, group_id)
204
+ );
193
205
  `
194
206
  ];
195
207
  var _db = null;
@@ -301,7 +313,11 @@ function rowToContact(row) {
301
313
  ...row,
302
314
  source: row.source,
303
315
  custom_fields: JSON.parse(row.custom_fields || "{}"),
304
- preferred_contact_method: row.preferred_contact_method ?? null
316
+ preferred_contact_method: row.preferred_contact_method ?? null,
317
+ status: row.status ?? "active",
318
+ follow_up_at: row.follow_up_at ?? null,
319
+ archived: !!row.archived,
320
+ project_id: row.project_id ?? null
305
321
  };
306
322
  }
307
323
  function rowToEmail(row) {
@@ -338,7 +354,9 @@ function rowToTag(row) {
338
354
  function rowToCompany(row) {
339
355
  return {
340
356
  ...row,
341
- custom_fields: JSON.parse(row.custom_fields || "{}")
357
+ custom_fields: JSON.parse(row.custom_fields || "{}"),
358
+ archived: !!row.archived,
359
+ project_id: row.project_id ?? null
342
360
  };
343
361
  }
344
362
  function insertEmails(db, contactId, companyId, emails) {
@@ -382,8 +400,8 @@ function createContact(input, db) {
382
400
  const firstName = input.first_name ?? "";
383
401
  const lastName = input.last_name ?? "";
384
402
  const displayName = input.display_name ?? (firstName || lastName ? `${firstName} ${lastName}`.trim() : "Unnamed Contact");
385
- d.run(`INSERT INTO contacts (id, first_name, last_name, display_name, nickname, avatar_url, notes, birthday, company_id, job_title, source, custom_fields, last_contacted_at, website, preferred_contact_method, created_at, updated_at)
386
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
403
+ d.run(`INSERT INTO contacts (id, first_name, last_name, display_name, nickname, avatar_url, notes, birthday, company_id, job_title, source, custom_fields, last_contacted_at, website, preferred_contact_method, status, follow_up_at, project_id, created_at, updated_at)
404
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
387
405
  id,
388
406
  firstName,
389
407
  lastName,
@@ -399,6 +417,9 @@ function createContact(input, db) {
399
417
  input.last_contacted_at ?? null,
400
418
  input.website ?? null,
401
419
  input.preferred_contact_method ?? null,
420
+ input.status ?? "active",
421
+ input.follow_up_at ?? null,
422
+ input.project_id ?? null,
402
423
  timestamp,
403
424
  timestamp
404
425
  ]);
@@ -433,12 +454,21 @@ function listContacts(opts = {}, db) {
433
454
  offset = 0,
434
455
  company_id,
435
456
  tag_id,
457
+ tag_ids,
436
458
  source,
459
+ status,
460
+ project_id,
461
+ archived = false,
462
+ follow_up_due,
463
+ last_contacted_after,
464
+ last_contacted_before,
437
465
  order_by = "display_name",
438
466
  order_dir = "asc"
439
467
  } = opts;
440
468
  const conditions = [];
441
469
  const params = [];
470
+ conditions.push("c.archived = ?");
471
+ params.push(archived ? 1 : 0);
442
472
  if (company_id) {
443
473
  conditions.push("c.company_id = ?");
444
474
  params.push(company_id);
@@ -447,12 +477,37 @@ function listContacts(opts = {}, db) {
447
477
  conditions.push("c.source = ?");
448
478
  params.push(source);
449
479
  }
480
+ if (status) {
481
+ conditions.push("c.status = ?");
482
+ params.push(status);
483
+ }
484
+ if (project_id) {
485
+ conditions.push("c.project_id = ?");
486
+ params.push(project_id);
487
+ }
450
488
  if (tag_id) {
451
489
  conditions.push("EXISTS (SELECT 1 FROM contact_tags ct WHERE ct.contact_id = c.id AND ct.tag_id = ?)");
452
490
  params.push(tag_id);
453
491
  }
492
+ if (tag_ids?.length) {
493
+ for (const tid of tag_ids) {
494
+ conditions.push("EXISTS (SELECT 1 FROM contact_tags ct WHERE ct.contact_id = c.id AND ct.tag_id = ?)");
495
+ params.push(tid);
496
+ }
497
+ }
498
+ if (follow_up_due) {
499
+ conditions.push("c.follow_up_at IS NOT NULL AND c.follow_up_at <= datetime('now')");
500
+ }
501
+ if (last_contacted_after) {
502
+ conditions.push("c.last_contacted_at >= ?");
503
+ params.push(last_contacted_after);
504
+ }
505
+ if (last_contacted_before) {
506
+ conditions.push("c.last_contacted_at <= ?");
507
+ params.push(last_contacted_before);
508
+ }
454
509
  const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
455
- const validOrderBy = ["display_name", "created_at", "updated_at"].includes(order_by) ? order_by : "display_name";
510
+ const validOrderBy = ["display_name", "created_at", "updated_at", "last_contacted_at", "follow_up_at"].includes(order_by) ? order_by : "display_name";
456
511
  const validOrderDir = order_dir === "desc" ? "DESC" : "ASC";
457
512
  const totalRow = d.query(`SELECT COUNT(*) as total FROM contacts c ${where}`).get(...params);
458
513
  const rows = d.query(`SELECT c.* FROM contacts c ${where} ORDER BY c.${validOrderBy} ${validOrderDir} LIMIT ? OFFSET ?`).all(...params, limit, offset);
@@ -522,8 +577,36 @@ function updateContact(id, input, db) {
522
577
  setClauses.push("preferred_contact_method = ?");
523
578
  params.push(input.preferred_contact_method);
524
579
  }
580
+ if (input.status !== undefined) {
581
+ setClauses.push("status = ?");
582
+ params.push(input.status);
583
+ }
584
+ if (input.follow_up_at !== undefined) {
585
+ setClauses.push("follow_up_at = ?");
586
+ params.push(input.follow_up_at);
587
+ }
588
+ if (input.project_id !== undefined) {
589
+ setClauses.push("project_id = ?");
590
+ params.push(input.project_id);
591
+ }
525
592
  params.push(id);
526
593
  d.run(`UPDATE contacts SET ${setClauses.join(", ")} WHERE id = ?`, params);
594
+ if (input.emails_add?.length) {
595
+ for (const e of input.emails_add) {
596
+ const exists = d.query(`SELECT id FROM emails WHERE contact_id = ? AND LOWER(address) = LOWER(?)`).get(id, e.address);
597
+ if (!exists) {
598
+ d.run(`INSERT INTO emails (id, contact_id, company_id, address, type, is_primary) VALUES (?, ?, NULL, ?, ?, ?)`, [uuid(), id, e.address, e.type ?? "work", e.is_primary ? 1 : 0]);
599
+ }
600
+ }
601
+ }
602
+ if (input.phones_add?.length) {
603
+ for (const p of input.phones_add) {
604
+ const exists = d.query(`SELECT id FROM phones WHERE contact_id = ? AND number = ?`).get(id, p.number);
605
+ if (!exists) {
606
+ d.run(`INSERT INTO phones (id, contact_id, company_id, number, country_code, type, is_primary) VALUES (?, ?, NULL, ?, ?, ?, ?)`, [uuid(), id, p.number, p.country_code ?? null, p.type ?? "mobile", p.is_primary ? 1 : 0]);
607
+ }
608
+ }
609
+ }
527
610
  logActivity(d, { contact_id: id, action: "contact.updated", details: `Updated contact: ${existing.display_name}` });
528
611
  const row = d.query(`SELECT * FROM contacts WHERE id = ?`).get(id);
529
612
  return loadContactDetails(d, rowToContact(row));
@@ -541,25 +624,31 @@ function searchContacts(query, db) {
541
624
  const ftsRows = d.query(`
542
625
  SELECT c.* FROM contacts c
543
626
  JOIN contacts_fts fts ON fts.id = c.id
544
- WHERE contacts_fts MATCH ?
627
+ WHERE contacts_fts MATCH ? AND c.archived = 0
545
628
  ORDER BY rank
546
629
  LIMIT 50
547
630
  `).all(`"${query.replace(/"/g, '""')}"*`);
548
631
  const emailRows = d.query(`
549
632
  SELECT DISTINCT c.* FROM contacts c
550
633
  JOIN emails e ON e.contact_id = c.id
551
- WHERE e.address LIKE ?
634
+ WHERE e.address LIKE ? AND c.archived = 0
552
635
  LIMIT 20
553
636
  `).all(`%${query}%`);
554
637
  const phoneRows = d.query(`
555
638
  SELECT DISTINCT c.* FROM contacts c
556
639
  JOIN phones p ON p.contact_id = c.id
557
- WHERE p.number LIKE ?
640
+ WHERE p.number LIKE ? AND c.archived = 0
641
+ LIMIT 20
642
+ `).all(`%${query}%`);
643
+ const companyRows = d.query(`
644
+ SELECT DISTINCT c.* FROM contacts c
645
+ JOIN companies co ON co.id = c.company_id
646
+ WHERE co.name LIKE ? AND c.archived = 0
558
647
  LIMIT 20
559
648
  `).all(`%${query}%`);
560
649
  const seen = new Set;
561
650
  const allRows = [];
562
- for (const row of [...ftsRows, ...emailRows, ...phoneRows]) {
651
+ for (const row of [...ftsRows, ...emailRows, ...phoneRows, ...companyRows]) {
563
652
  if (!seen.has(row.id)) {
564
653
  seen.add(row.id);
565
654
  allRows.push(row);
@@ -580,8 +669,24 @@ function mergeContacts(keepId, mergeId, db) {
580
669
  const mergeRow = d.query(`SELECT * FROM contacts WHERE id = ?`).get(mergeId);
581
670
  if (!mergeRow)
582
671
  throw new ContactNotFoundError(mergeId);
583
- d.run(`UPDATE emails SET contact_id = ? WHERE contact_id = ?`, [keepId, mergeId]);
584
- d.run(`UPDATE phones SET contact_id = ? WHERE contact_id = ?`, [keepId, mergeId]);
672
+ const mergeEmailRows = d.query(`SELECT * FROM emails WHERE contact_id = ?`).all(mergeId);
673
+ for (const e of mergeEmailRows) {
674
+ const exists = d.query(`SELECT id FROM emails WHERE contact_id = ? AND LOWER(address) = LOWER(?)`).get(keepId, e.address);
675
+ if (exists) {
676
+ d.run(`DELETE FROM emails WHERE id = ?`, [e.id]);
677
+ } else {
678
+ d.run(`UPDATE emails SET contact_id = ? WHERE id = ?`, [keepId, e.id]);
679
+ }
680
+ }
681
+ const mergePhoneRows = d.query(`SELECT * FROM phones WHERE contact_id = ?`).all(mergeId);
682
+ for (const p of mergePhoneRows) {
683
+ const exists = d.query(`SELECT id FROM phones WHERE contact_id = ? AND number = ?`).get(keepId, p.number);
684
+ if (exists) {
685
+ d.run(`DELETE FROM phones WHERE id = ?`, [p.id]);
686
+ } else {
687
+ d.run(`UPDATE phones SET contact_id = ? WHERE id = ?`, [keepId, p.id]);
688
+ }
689
+ }
585
690
  d.run(`UPDATE addresses SET contact_id = ? WHERE contact_id = ?`, [keepId, mergeId]);
586
691
  d.run(`UPDATE social_profiles SET contact_id = ? WHERE contact_id = ?`, [keepId, mergeId]);
587
692
  const mergeTags = d.query(`SELECT tag_id FROM contact_tags WHERE contact_id = ?`).all(mergeId);
@@ -628,11 +733,86 @@ function mergeContacts(keepId, mergeId, db) {
628
733
  const finalRow = d.query(`SELECT * FROM contacts WHERE id = ?`).get(keepId);
629
734
  return loadContactDetails(d, rowToContact(finalRow));
630
735
  }
736
+ function getContactByEmail(email, db) {
737
+ const d = db || getDatabase();
738
+ const emailRow = d.query(`SELECT contact_id FROM emails WHERE LOWER(address) = LOWER(?) AND contact_id IS NOT NULL LIMIT 1`).get(email);
739
+ if (!emailRow)
740
+ return null;
741
+ const row = d.query(`SELECT * FROM contacts WHERE id = ?`).get(emailRow.contact_id);
742
+ if (!row)
743
+ return null;
744
+ return loadContactDetails(d, rowToContact(row));
745
+ }
746
+ function addEmailToContact(contactId, email, db) {
747
+ const d = db || getDatabase();
748
+ const row = d.query(`SELECT * FROM contacts WHERE id = ?`).get(contactId);
749
+ if (!row)
750
+ throw new ContactNotFoundError(contactId);
751
+ const exists = d.query(`SELECT id FROM emails WHERE contact_id = ? AND LOWER(address) = LOWER(?)`).get(contactId, email.address);
752
+ if (!exists) {
753
+ d.run(`INSERT INTO emails (id, contact_id, company_id, address, type, is_primary) VALUES (?, ?, NULL, ?, ?, ?)`, [uuid(), contactId, email.address, email.type ?? "work", email.is_primary ? 1 : 0]);
754
+ }
755
+ const updated = d.query(`SELECT * FROM contacts WHERE id = ?`).get(contactId);
756
+ return loadContactDetails(d, rowToContact(updated));
757
+ }
758
+ function addPhoneToContact(contactId, phone, db) {
759
+ const d = db || getDatabase();
760
+ const row = d.query(`SELECT * FROM contacts WHERE id = ?`).get(contactId);
761
+ if (!row)
762
+ throw new ContactNotFoundError(contactId);
763
+ const exists = d.query(`SELECT id FROM phones WHERE contact_id = ? AND number = ?`).get(contactId, phone.number);
764
+ if (!exists) {
765
+ d.run(`INSERT INTO phones (id, contact_id, company_id, number, country_code, type, is_primary) VALUES (?, ?, NULL, ?, ?, ?, ?)`, [uuid(), contactId, phone.number, phone.country_code ?? null, phone.type ?? "mobile", phone.is_primary ? 1 : 0]);
766
+ }
767
+ const updated = d.query(`SELECT * FROM contacts WHERE id = ?`).get(contactId);
768
+ return loadContactDetails(d, rowToContact(updated));
769
+ }
770
+ function archiveContact(id, db) {
771
+ const d = db || getDatabase();
772
+ const row = d.query(`SELECT * FROM contacts WHERE id = ?`).get(id);
773
+ if (!row)
774
+ throw new ContactNotFoundError(id);
775
+ d.run(`UPDATE contacts SET archived = 1, updated_at = ? WHERE id = ?`, [now(), id]);
776
+ logActivity(d, { contact_id: id, action: "contact.archived", details: `Archived contact: ${row.display_name}` });
777
+ const updated = d.query(`SELECT * FROM contacts WHERE id = ?`).get(id);
778
+ return loadContactDetails(d, rowToContact(updated));
779
+ }
780
+ function unarchiveContact(id, db) {
781
+ const d = db || getDatabase();
782
+ const row = d.query(`SELECT * FROM contacts WHERE id = ?`).get(id);
783
+ if (!row)
784
+ throw new ContactNotFoundError(id);
785
+ d.run(`UPDATE contacts SET archived = 0, updated_at = ? WHERE id = ?`, [now(), id]);
786
+ logActivity(d, { contact_id: id, action: "contact.unarchived", details: `Unarchived contact: ${row.display_name}` });
787
+ const updated = d.query(`SELECT * FROM contacts WHERE id = ?`).get(id);
788
+ return loadContactDetails(d, rowToContact(updated));
789
+ }
790
+ function autoLinkContactToCompany(contactId, db) {
791
+ const d = db || getDatabase();
792
+ const row = d.query(`SELECT * FROM contacts WHERE id = ?`).get(contactId);
793
+ if (!row || row.company_id)
794
+ return null;
795
+ const emailRow = d.query(`SELECT address FROM emails WHERE contact_id = ? AND contact_id IS NOT NULL LIMIT 1`).get(contactId);
796
+ if (!emailRow)
797
+ return null;
798
+ const domain = emailRow.address.split("@")[1];
799
+ if (!domain)
800
+ return null;
801
+ const companyRow = d.query(`SELECT id FROM companies WHERE domain = ? LIMIT 1`).get(domain);
802
+ if (!companyRow)
803
+ return null;
804
+ d.run(`UPDATE contacts SET company_id = ?, updated_at = ? WHERE id = ?`, [companyRow.id, now(), contactId]);
805
+ logActivity(d, { contact_id: contactId, action: "contact.auto_linked", details: `Auto-linked to company via email domain: ${domain}` });
806
+ const updated = d.query(`SELECT * FROM contacts WHERE id = ?`).get(contactId);
807
+ return loadContactDetails(d, rowToContact(updated));
808
+ }
631
809
  // src/db/companies.ts
632
810
  function rowToCompany2(row) {
633
811
  return {
634
812
  ...row,
635
- custom_fields: JSON.parse(row.custom_fields || "{}")
813
+ custom_fields: JSON.parse(row.custom_fields || "{}"),
814
+ archived: !!row.archived,
815
+ project_id: row.project_id ?? null
636
816
  };
637
817
  }
638
818
  function insertEmails2(db, companyId, emails) {
@@ -742,15 +922,23 @@ function listCompanies(opts = {}, db) {
742
922
  offset = 0,
743
923
  industry,
744
924
  tag_id,
925
+ project_id,
926
+ archived = false,
745
927
  order_by = "name",
746
928
  order_dir = "asc"
747
929
  } = opts;
748
930
  const conditions = [];
749
931
  const params = [];
932
+ conditions.push("co.archived = ?");
933
+ params.push(archived ? 1 : 0);
750
934
  if (industry) {
751
935
  conditions.push("co.industry = ?");
752
936
  params.push(industry);
753
937
  }
938
+ if (project_id) {
939
+ conditions.push("co.project_id = ?");
940
+ params.push(project_id);
941
+ }
754
942
  if (tag_id) {
755
943
  conditions.push("EXISTS (SELECT 1 FROM company_tags ct WHERE ct.company_id = co.id AND ct.tag_id = ?)");
756
944
  params.push(tag_id);
@@ -806,6 +994,10 @@ function updateCompany(id, input, db) {
806
994
  setClauses.push("custom_fields = ?");
807
995
  params.push(JSON.stringify(input.custom_fields));
808
996
  }
997
+ if ("project_id" in input && input.project_id !== undefined) {
998
+ setClauses.push("project_id = ?");
999
+ params.push(input.project_id);
1000
+ }
809
1001
  params.push(id);
810
1002
  d.run(`UPDATE companies SET ${setClauses.join(", ")} WHERE id = ?`, params);
811
1003
  logActivity(d, { company_id: id, action: "company.updated", details: `Updated company: ${existing.name}` });
@@ -839,9 +1031,33 @@ function listCompanyEmployees(companyId, db) {
839
1031
  ...r,
840
1032
  source: r.source,
841
1033
  custom_fields: JSON.parse(r.custom_fields || "{}"),
842
- preferred_contact_method: r.preferred_contact_method ?? null
1034
+ preferred_contact_method: r.preferred_contact_method ?? null,
1035
+ status: r.status ?? "active",
1036
+ follow_up_at: r.follow_up_at ?? null,
1037
+ archived: !!r.archived,
1038
+ project_id: r.project_id ?? null
843
1039
  }));
844
1040
  }
1041
+ function archiveCompany(id, db) {
1042
+ const d = db || getDatabase();
1043
+ const row = d.query(`SELECT * FROM companies WHERE id = ?`).get(id);
1044
+ if (!row)
1045
+ throw new CompanyNotFoundError(id);
1046
+ d.run(`UPDATE companies SET archived = 1, updated_at = ? WHERE id = ?`, [now(), id]);
1047
+ logActivity(d, { company_id: id, action: "company.archived", details: `Archived company: ${row.name}` });
1048
+ const updated = d.query(`SELECT * FROM companies WHERE id = ?`).get(id);
1049
+ return loadCompanyDetails(d, rowToCompany2(updated));
1050
+ }
1051
+ function unarchiveCompany(id, db) {
1052
+ const d = db || getDatabase();
1053
+ const row = d.query(`SELECT * FROM companies WHERE id = ?`).get(id);
1054
+ if (!row)
1055
+ throw new CompanyNotFoundError(id);
1056
+ d.run(`UPDATE companies SET archived = 0, updated_at = ? WHERE id = ?`, [now(), id]);
1057
+ logActivity(d, { company_id: id, action: "company.unarchived", details: `Unarchived company: ${row.name}` });
1058
+ const updated = d.query(`SELECT * FROM companies WHERE id = ?`).get(id);
1059
+ return loadCompanyDetails(d, rowToCompany2(updated));
1060
+ }
845
1061
  // src/db/tags.ts
846
1062
  function rowToTag2(row) {
847
1063
  return { ...row };
@@ -937,7 +1153,11 @@ function listContactsByTag(tagId, db) {
937
1153
  ...r,
938
1154
  source: r.source,
939
1155
  custom_fields: JSON.parse(r.custom_fields || "{}"),
940
- preferred_contact_method: r.preferred_contact_method ?? null
1156
+ preferred_contact_method: r.preferred_contact_method ?? null,
1157
+ status: r.status ?? "active",
1158
+ follow_up_at: r.follow_up_at ?? null,
1159
+ archived: !!r.archived,
1160
+ project_id: r.project_id ?? null
941
1161
  }));
942
1162
  }
943
1163
  function addTagToCompany(companyId, tagId, db) {
@@ -1009,7 +1229,10 @@ function getGroup(db, id) {
1009
1229
  return db.query(`SELECT * FROM groups WHERE id = ?`).get(id);
1010
1230
  }
1011
1231
  function listGroups(db) {
1012
- return db.query(`SELECT g.*, COUNT(cg.contact_id) as member_count FROM groups g LEFT JOIN contact_groups cg ON g.id = cg.group_id GROUP BY g.id ORDER BY g.name`).all();
1232
+ return db.query(`SELECT g.*,
1233
+ (SELECT COUNT(*) FROM contact_groups cg WHERE cg.group_id = g.id) as member_count,
1234
+ (SELECT COUNT(*) FROM company_groups cog WHERE cog.group_id = g.id) as company_count
1235
+ FROM groups g ORDER BY g.name`).all();
1013
1236
  }
1014
1237
  function updateGroup(db, id, input) {
1015
1238
  const fields = [];
@@ -1032,7 +1255,11 @@ function deleteGroup(db, id) {
1032
1255
  db.query(`DELETE FROM groups WHERE id = ?`).run(id);
1033
1256
  }
1034
1257
  function addContactToGroup(db, contactId, groupId) {
1035
- db.query(`INSERT OR IGNORE INTO contact_groups(contact_id, group_id) VALUES(?,?)`).run(contactId, groupId);
1258
+ const existing = db.query(`SELECT 1 FROM contact_groups WHERE contact_id = ? AND group_id = ?`).get(contactId, groupId);
1259
+ if (existing)
1260
+ return { added: false, already_member: true };
1261
+ db.query(`INSERT INTO contact_groups(contact_id, group_id) VALUES(?,?)`).run(contactId, groupId);
1262
+ return { added: true, already_member: false };
1036
1263
  }
1037
1264
  function removeContactFromGroup(db, contactId, groupId) {
1038
1265
  db.query(`DELETE FROM contact_groups WHERE contact_id = ? AND group_id = ?`).run(contactId, groupId);
@@ -1044,28 +1271,50 @@ function listContactsInGroup(db, groupId) {
1044
1271
  function listGroupsForContact(db, contactId) {
1045
1272
  return db.query(`SELECT g.* FROM groups g JOIN contact_groups cg ON g.id = cg.group_id WHERE cg.contact_id = ? ORDER BY g.name`).all(contactId);
1046
1273
  }
1274
+ function addCompanyToGroup(db, companyId, groupId) {
1275
+ const existing = db.query(`SELECT 1 FROM company_groups WHERE company_id = ? AND group_id = ?`).get(companyId, groupId);
1276
+ if (existing)
1277
+ return { added: false, already_member: true };
1278
+ db.query(`INSERT INTO company_groups(company_id, group_id) VALUES(?,?)`).run(companyId, groupId);
1279
+ return { added: true, already_member: false };
1280
+ }
1281
+ function removeCompanyFromGroup(db, companyId, groupId) {
1282
+ db.query(`DELETE FROM company_groups WHERE company_id = ? AND group_id = ?`).run(companyId, groupId);
1283
+ }
1284
+ function listCompaniesInGroup(db, groupId) {
1285
+ const rows = db.query(`SELECT company_id FROM company_groups WHERE group_id = ?`).all(groupId);
1286
+ return rows.map((r) => r.company_id);
1287
+ }
1288
+ function listGroupsForCompany(db, companyId) {
1289
+ return db.query(`SELECT g.* FROM groups g JOIN company_groups cog ON g.id = cog.group_id WHERE cog.company_id = ? ORDER BY g.name`).all(companyId);
1290
+ }
1047
1291
  export {
1048
1292
  updateTag,
1049
1293
  updateGroup,
1050
1294
  updateContact,
1051
1295
  updateCompany,
1296
+ unarchiveContact,
1297
+ unarchiveCompany,
1052
1298
  searchContacts,
1053
1299
  searchCompanies,
1054
1300
  resetDatabase,
1055
1301
  removeTagFromContact,
1056
1302
  removeTagFromCompany,
1057
1303
  removeContactFromGroup,
1304
+ removeCompanyFromGroup,
1058
1305
  mergeContacts,
1059
1306
  logActivity,
1060
1307
  listTags,
1061
1308
  listRelationships,
1062
1309
  listRecentContacts,
1063
1310
  listGroupsForContact,
1311
+ listGroupsForCompany,
1064
1312
  listGroups,
1065
1313
  listContactsInGroup,
1066
1314
  listContactsByTag,
1067
1315
  listContacts,
1068
1316
  listCompanyEmployees,
1317
+ listCompaniesInGroup,
1069
1318
  listCompanies,
1070
1319
  listActivity,
1071
1320
  getTagByName,
@@ -1073,6 +1322,7 @@ export {
1073
1322
  getRelationship,
1074
1323
  getGroup,
1075
1324
  getDatabase,
1325
+ getContactByEmail,
1076
1326
  getContact,
1077
1327
  getCompany,
1078
1328
  getActivity,
@@ -1086,9 +1336,15 @@ export {
1086
1336
  createGroup,
1087
1337
  createContact,
1088
1338
  createCompany,
1339
+ autoLinkContactToCompany,
1340
+ archiveContact,
1341
+ archiveCompany,
1089
1342
  addTagToContact,
1090
1343
  addTagToCompany,
1344
+ addPhoneToContact,
1345
+ addEmailToContact,
1091
1346
  addContactToGroup,
1347
+ addCompanyToGroup,
1092
1348
  TagNotFoundError,
1093
1349
  DuplicateTagNameError,
1094
1350
  ContactNotFoundError,