@hasna/contacts 0.2.2 → 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
@@ -188,6 +188,20 @@ var MIGRATIONS = [
188
188
  group_id TEXT NOT NULL REFERENCES groups(id) ON DELETE CASCADE,
189
189
  PRIMARY KEY (contact_id, group_id)
190
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
+ );
191
205
  `
192
206
  ];
193
207
  var _db = null;
@@ -299,7 +313,11 @@ function rowToContact(row) {
299
313
  ...row,
300
314
  source: row.source,
301
315
  custom_fields: JSON.parse(row.custom_fields || "{}"),
302
- 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
303
321
  };
304
322
  }
305
323
  function rowToEmail(row) {
@@ -336,7 +354,9 @@ function rowToTag(row) {
336
354
  function rowToCompany(row) {
337
355
  return {
338
356
  ...row,
339
- 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
340
360
  };
341
361
  }
342
362
  function insertEmails(db, contactId, companyId, emails) {
@@ -380,8 +400,8 @@ function createContact(input, db) {
380
400
  const firstName = input.first_name ?? "";
381
401
  const lastName = input.last_name ?? "";
382
402
  const displayName = input.display_name ?? (firstName || lastName ? `${firstName} ${lastName}`.trim() : "Unnamed Contact");
383
- 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)
384
- 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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
385
405
  id,
386
406
  firstName,
387
407
  lastName,
@@ -397,6 +417,9 @@ function createContact(input, db) {
397
417
  input.last_contacted_at ?? null,
398
418
  input.website ?? null,
399
419
  input.preferred_contact_method ?? null,
420
+ input.status ?? "active",
421
+ input.follow_up_at ?? null,
422
+ input.project_id ?? null,
400
423
  timestamp,
401
424
  timestamp
402
425
  ]);
@@ -431,12 +454,21 @@ function listContacts(opts = {}, db) {
431
454
  offset = 0,
432
455
  company_id,
433
456
  tag_id,
457
+ tag_ids,
434
458
  source,
459
+ status,
460
+ project_id,
461
+ archived = false,
462
+ follow_up_due,
463
+ last_contacted_after,
464
+ last_contacted_before,
435
465
  order_by = "display_name",
436
466
  order_dir = "asc"
437
467
  } = opts;
438
468
  const conditions = [];
439
469
  const params = [];
470
+ conditions.push("c.archived = ?");
471
+ params.push(archived ? 1 : 0);
440
472
  if (company_id) {
441
473
  conditions.push("c.company_id = ?");
442
474
  params.push(company_id);
@@ -445,12 +477,37 @@ function listContacts(opts = {}, db) {
445
477
  conditions.push("c.source = ?");
446
478
  params.push(source);
447
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
+ }
448
488
  if (tag_id) {
449
489
  conditions.push("EXISTS (SELECT 1 FROM contact_tags ct WHERE ct.contact_id = c.id AND ct.tag_id = ?)");
450
490
  params.push(tag_id);
451
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
+ }
452
509
  const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
453
- 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";
454
511
  const validOrderDir = order_dir === "desc" ? "DESC" : "ASC";
455
512
  const totalRow = d.query(`SELECT COUNT(*) as total FROM contacts c ${where}`).get(...params);
456
513
  const rows = d.query(`SELECT c.* FROM contacts c ${where} ORDER BY c.${validOrderBy} ${validOrderDir} LIMIT ? OFFSET ?`).all(...params, limit, offset);
@@ -520,8 +577,36 @@ function updateContact(id, input, db) {
520
577
  setClauses.push("preferred_contact_method = ?");
521
578
  params.push(input.preferred_contact_method);
522
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
+ }
523
592
  params.push(id);
524
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
+ }
525
610
  logActivity(d, { contact_id: id, action: "contact.updated", details: `Updated contact: ${existing.display_name}` });
526
611
  const row = d.query(`SELECT * FROM contacts WHERE id = ?`).get(id);
527
612
  return loadContactDetails(d, rowToContact(row));
@@ -539,25 +624,31 @@ function searchContacts(query, db) {
539
624
  const ftsRows = d.query(`
540
625
  SELECT c.* FROM contacts c
541
626
  JOIN contacts_fts fts ON fts.id = c.id
542
- WHERE contacts_fts MATCH ?
627
+ WHERE contacts_fts MATCH ? AND c.archived = 0
543
628
  ORDER BY rank
544
629
  LIMIT 50
545
630
  `).all(`"${query.replace(/"/g, '""')}"*`);
546
631
  const emailRows = d.query(`
547
632
  SELECT DISTINCT c.* FROM contacts c
548
633
  JOIN emails e ON e.contact_id = c.id
549
- WHERE e.address LIKE ?
634
+ WHERE e.address LIKE ? AND c.archived = 0
550
635
  LIMIT 20
551
636
  `).all(`%${query}%`);
552
637
  const phoneRows = d.query(`
553
638
  SELECT DISTINCT c.* FROM contacts c
554
639
  JOIN phones p ON p.contact_id = c.id
555
- 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
556
647
  LIMIT 20
557
648
  `).all(`%${query}%`);
558
649
  const seen = new Set;
559
650
  const allRows = [];
560
- for (const row of [...ftsRows, ...emailRows, ...phoneRows]) {
651
+ for (const row of [...ftsRows, ...emailRows, ...phoneRows, ...companyRows]) {
561
652
  if (!seen.has(row.id)) {
562
653
  seen.add(row.id);
563
654
  allRows.push(row);
@@ -578,8 +669,24 @@ function mergeContacts(keepId, mergeId, db) {
578
669
  const mergeRow = d.query(`SELECT * FROM contacts WHERE id = ?`).get(mergeId);
579
670
  if (!mergeRow)
580
671
  throw new ContactNotFoundError(mergeId);
581
- d.run(`UPDATE emails SET contact_id = ? WHERE contact_id = ?`, [keepId, mergeId]);
582
- 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
+ }
583
690
  d.run(`UPDATE addresses SET contact_id = ? WHERE contact_id = ?`, [keepId, mergeId]);
584
691
  d.run(`UPDATE social_profiles SET contact_id = ? WHERE contact_id = ?`, [keepId, mergeId]);
585
692
  const mergeTags = d.query(`SELECT tag_id FROM contact_tags WHERE contact_id = ?`).all(mergeId);
@@ -626,11 +733,86 @@ function mergeContacts(keepId, mergeId, db) {
626
733
  const finalRow = d.query(`SELECT * FROM contacts WHERE id = ?`).get(keepId);
627
734
  return loadContactDetails(d, rowToContact(finalRow));
628
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
+ }
629
809
  // src/db/companies.ts
630
810
  function rowToCompany2(row) {
631
811
  return {
632
812
  ...row,
633
- 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
634
816
  };
635
817
  }
636
818
  function insertEmails2(db, companyId, emails) {
@@ -740,15 +922,23 @@ function listCompanies(opts = {}, db) {
740
922
  offset = 0,
741
923
  industry,
742
924
  tag_id,
925
+ project_id,
926
+ archived = false,
743
927
  order_by = "name",
744
928
  order_dir = "asc"
745
929
  } = opts;
746
930
  const conditions = [];
747
931
  const params = [];
932
+ conditions.push("co.archived = ?");
933
+ params.push(archived ? 1 : 0);
748
934
  if (industry) {
749
935
  conditions.push("co.industry = ?");
750
936
  params.push(industry);
751
937
  }
938
+ if (project_id) {
939
+ conditions.push("co.project_id = ?");
940
+ params.push(project_id);
941
+ }
752
942
  if (tag_id) {
753
943
  conditions.push("EXISTS (SELECT 1 FROM company_tags ct WHERE ct.company_id = co.id AND ct.tag_id = ?)");
754
944
  params.push(tag_id);
@@ -804,6 +994,10 @@ function updateCompany(id, input, db) {
804
994
  setClauses.push("custom_fields = ?");
805
995
  params.push(JSON.stringify(input.custom_fields));
806
996
  }
997
+ if ("project_id" in input && input.project_id !== undefined) {
998
+ setClauses.push("project_id = ?");
999
+ params.push(input.project_id);
1000
+ }
807
1001
  params.push(id);
808
1002
  d.run(`UPDATE companies SET ${setClauses.join(", ")} WHERE id = ?`, params);
809
1003
  logActivity(d, { company_id: id, action: "company.updated", details: `Updated company: ${existing.name}` });
@@ -837,9 +1031,33 @@ function listCompanyEmployees(companyId, db) {
837
1031
  ...r,
838
1032
  source: r.source,
839
1033
  custom_fields: JSON.parse(r.custom_fields || "{}"),
840
- 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
841
1039
  }));
842
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
+ }
843
1061
  // src/db/tags.ts
844
1062
  function rowToTag2(row) {
845
1063
  return { ...row };
@@ -935,7 +1153,11 @@ function listContactsByTag(tagId, db) {
935
1153
  ...r,
936
1154
  source: r.source,
937
1155
  custom_fields: JSON.parse(r.custom_fields || "{}"),
938
- 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
939
1161
  }));
940
1162
  }
941
1163
  function addTagToCompany(companyId, tagId, db) {
@@ -1007,7 +1229,10 @@ function getGroup(db, id) {
1007
1229
  return db.query(`SELECT * FROM groups WHERE id = ?`).get(id);
1008
1230
  }
1009
1231
  function listGroups(db) {
1010
- 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();
1011
1236
  }
1012
1237
  function updateGroup(db, id, input) {
1013
1238
  const fields = [];
@@ -1030,7 +1255,11 @@ function deleteGroup(db, id) {
1030
1255
  db.query(`DELETE FROM groups WHERE id = ?`).run(id);
1031
1256
  }
1032
1257
  function addContactToGroup(db, contactId, groupId) {
1033
- 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 };
1034
1263
  }
1035
1264
  function removeContactFromGroup(db, contactId, groupId) {
1036
1265
  db.query(`DELETE FROM contact_groups WHERE contact_id = ? AND group_id = ?`).run(contactId, groupId);
@@ -1042,28 +1271,50 @@ function listContactsInGroup(db, groupId) {
1042
1271
  function listGroupsForContact(db, contactId) {
1043
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);
1044
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
+ }
1045
1291
  export {
1046
1292
  updateTag,
1047
1293
  updateGroup,
1048
1294
  updateContact,
1049
1295
  updateCompany,
1296
+ unarchiveContact,
1297
+ unarchiveCompany,
1050
1298
  searchContacts,
1051
1299
  searchCompanies,
1052
1300
  resetDatabase,
1053
1301
  removeTagFromContact,
1054
1302
  removeTagFromCompany,
1055
1303
  removeContactFromGroup,
1304
+ removeCompanyFromGroup,
1056
1305
  mergeContacts,
1057
1306
  logActivity,
1058
1307
  listTags,
1059
1308
  listRelationships,
1060
1309
  listRecentContacts,
1061
1310
  listGroupsForContact,
1311
+ listGroupsForCompany,
1062
1312
  listGroups,
1063
1313
  listContactsInGroup,
1064
1314
  listContactsByTag,
1065
1315
  listContacts,
1066
1316
  listCompanyEmployees,
1317
+ listCompaniesInGroup,
1067
1318
  listCompanies,
1068
1319
  listActivity,
1069
1320
  getTagByName,
@@ -1071,6 +1322,7 @@ export {
1071
1322
  getRelationship,
1072
1323
  getGroup,
1073
1324
  getDatabase,
1325
+ getContactByEmail,
1074
1326
  getContact,
1075
1327
  getCompany,
1076
1328
  getActivity,
@@ -1084,9 +1336,15 @@ export {
1084
1336
  createGroup,
1085
1337
  createContact,
1086
1338
  createCompany,
1339
+ autoLinkContactToCompany,
1340
+ archiveContact,
1341
+ archiveCompany,
1087
1342
  addTagToContact,
1088
1343
  addTagToCompany,
1344
+ addPhoneToContact,
1345
+ addEmailToContact,
1089
1346
  addContactToGroup,
1347
+ addCompanyToGroup,
1090
1348
  TagNotFoundError,
1091
1349
  DuplicateTagNameError,
1092
1350
  ContactNotFoundError,