contacts: my book, the company book, vCard/CSV import, sharing with Accept/Deny

C1 (spec 11): /api/me/contacts and /api/contacts (admin), our own vCard 2.1/3.0/4.0 and CSV
readers with a preview, contact_shares (migration 0009) for offers, and contact names in call
history, voicemail, Home, the softphone and the call report email. Admin → Contacts.
This commit is contained in:
iamdoubz committed 2026-10-23 14:53:21 -05:00
1 parent 32f4590369
commit a754974a90
26 files changed
+6103 -23

No files matched your search

@@ -0,0 +1,16 @@
CREATE TABLE "contact_shares" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"from_user_id" uuid,
"to_user_id" uuid NOT NULL,
"source_id" text NOT NULL,
"contact" jsonb NOT NULL,
"status" text DEFAULT 'pending' NOT NULL,
"decided_at" timestamp with time zone,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
CONSTRAINT "contact_shares_status_check" CHECK ("contact_shares"."status" in ('pending', 'accepted', 'denied'))
);
--> statement-breakpoint
ALTER TABLE "contact_shares" ADD CONSTRAINT "contact_shares_from_user_id_users_id_fk" FOREIGN KEY ("from_user_id") REFERENCES "public"."users"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "contact_shares" ADD CONSTRAINT "contact_shares_to_user_id_users_id_fk" FOREIGN KEY ("to_user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
CREATE INDEX "contact_shares_to_user_id_index" ON "contact_shares" USING btree ("to_user_id") WHERE "contact_shares"."status" = 'pending';
File diff suppressed because it is too large. Load diff
@@ -64,6 +64,13 @@
"when": 1792781321577,
"tag": "0008_call_reports",
"breakpoints": true
},
{
"idx": 9,
"version": "7",
"when": 1792784242837,
"tag": "0009_contact_shares",
"breakpoints": true
}
]
}
+25
View File
@@ -620,6 +620,31 @@ export const contacts = pgTable('contacts', {
...timestamps,
});
/** A contact offered to a colleague, waiting for Accept or Deny (11 §2.1). */
export const contactShares = pgTable(
'contact_shares',
{
id: id(),
fromUserId: uuid().references(() => users.id, { onDelete: 'set null' }),
toUserId: uuid()
.notNull()
.references(() => users.id, { onDelete: 'cascade' }),
/** The shared contact's id in the sender's book: offering it again replaces a pending offer. */
sourceId: text().notNull(),
/** A copy taken when shared: { name, company, email, numbers, notes }. */
contact: jsonb().notNull(),
status: text().notNull().default('pending'),
decidedAt: tstz(),
...timestamps,
},
(t) => [
index()
.on(t.toUserId)
.where(sql`${t.status} = 'pending'`),
check('contact_shares_status_check', sql`${t.status} in ('pending', 'accepted', 'denied')`),
],
);
export const smsMessages = pgTable(
'sms_messages',
{