@voyant-travel/relationships 0.119.2
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 +201 -0
- package/README.md +36 -0
- package/dist/action-ledger-capabilities.d.ts +20 -0
- package/dist/action-ledger-capabilities.d.ts.map +1 -0
- package/dist/action-ledger-capabilities.js +16 -0
- package/dist/events.d.ts +23 -0
- package/dist/events.d.ts.map +1 -0
- package/dist/events.js +9 -0
- package/dist/index.d.ts +32 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +49 -0
- package/dist/route-runtime.d.ts +21 -0
- package/dist/route-runtime.d.ts.map +1 -0
- package/dist/route-runtime.js +28 -0
- package/dist/routes/accounts.d.ts +1460 -0
- package/dist/routes/accounts.d.ts.map +1 -0
- package/dist/routes/accounts.js +274 -0
- package/dist/routes/activities.d.ts +299 -0
- package/dist/routes/activities.d.ts.map +1 -0
- package/dist/routes/activities.js +64 -0
- package/dist/routes/custom-fields.d.ts +256 -0
- package/dist/routes/custom-fields.d.ts.map +1 -0
- package/dist/routes/custom-fields.js +47 -0
- package/dist/routes/customer-signals.d.ts +281 -0
- package/dist/routes/customer-signals.d.ts.map +1 -0
- package/dist/routes/customer-signals.js +45 -0
- package/dist/routes/index.d.ts +2945 -0
- package/dist/routes/index.d.ts.map +1 -0
- package/dist/routes/index.js +14 -0
- package/dist/routes/person-documents.d.ts +519 -0
- package/dist/routes/person-documents.d.ts.map +1 -0
- package/dist/routes/person-documents.js +240 -0
- package/dist/routes/person-relationships.d.ts +189 -0
- package/dist/routes/person-relationships.d.ts.map +1 -0
- package/dist/routes/person-relationships.js +36 -0
- package/dist/schema-accounts.d.ts +2099 -0
- package/dist/schema-accounts.d.ts.map +1 -0
- package/dist/schema-accounts.js +312 -0
- package/dist/schema-activities.d.ts +821 -0
- package/dist/schema-activities.d.ts.map +1 -0
- package/dist/schema-activities.js +92 -0
- package/dist/schema-relations.d.ts +47 -0
- package/dist/schema-relations.d.ts.map +1 -0
- package/dist/schema-relations.js +70 -0
- package/dist/schema-shared.d.ts +10 -0
- package/dist/schema-shared.d.ts.map +1 -0
- package/dist/schema-shared.js +36 -0
- package/dist/schema-signals.d.ts +324 -0
- package/dist/schema-signals.d.ts.map +1 -0
- package/dist/schema-signals.js +80 -0
- package/dist/schema.d.ts +6 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +5 -0
- package/dist/service/accounts-merge.d.ts +63 -0
- package/dist/service/accounts-merge.d.ts.map +1 -0
- package/dist/service/accounts-merge.js +382 -0
- package/dist/service/accounts-organizations.d.ts +97 -0
- package/dist/service/accounts-organizations.d.ts.map +1 -0
- package/dist/service/accounts-organizations.js +70 -0
- package/dist/service/accounts-people.d.ts +1315 -0
- package/dist/service/accounts-people.d.ts.map +1 -0
- package/dist/service/accounts-people.js +409 -0
- package/dist/service/accounts-resolve.d.ts +76 -0
- package/dist/service/accounts-resolve.d.ts.map +1 -0
- package/dist/service/accounts-resolve.js +103 -0
- package/dist/service/accounts-shared.d.ts +68 -0
- package/dist/service/accounts-shared.d.ts.map +1 -0
- package/dist/service/accounts-shared.js +149 -0
- package/dist/service/accounts.d.ts +1465 -0
- package/dist/service/accounts.d.ts.map +1 -0
- package/dist/service/accounts.js +13 -0
- package/dist/service/activities.d.ts +486 -0
- package/dist/service/activities.d.ts.map +1 -0
- package/dist/service/activities.js +114 -0
- package/dist/service/custom-fields.d.ts +118 -0
- package/dist/service/custom-fields.d.ts.map +1 -0
- package/dist/service/custom-fields.js +88 -0
- package/dist/service/customer-signals.d.ts +733 -0
- package/dist/service/customer-signals.d.ts.map +1 -0
- package/dist/service/customer-signals.js +112 -0
- package/dist/service/helpers.d.ts +22 -0
- package/dist/service/helpers.d.ts.map +1 -0
- package/dist/service/helpers.js +39 -0
- package/dist/service/index.d.ts +4434 -0
- package/dist/service/index.d.ts.map +1 -0
- package/dist/service/index.js +17 -0
- package/dist/service/person-documents.d.ts +1201 -0
- package/dist/service/person-documents.d.ts.map +1 -0
- package/dist/service/person-documents.js +240 -0
- package/dist/service/person-relationships.d.ts +502 -0
- package/dist/service/person-relationships.d.ts.map +1 -0
- package/dist/service/person-relationships.js +121 -0
- package/dist/validation.d.ts +3 -0
- package/dist/validation.d.ts.map +1 -0
- package/dist/validation.js +1 -0
- package/package.json +80 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"schema-accounts.d.ts","sourceRoot":"","sources":["../src/schema-accounts.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
import { typeId, typeIdRef } from "@voyant-travel/db/lib/typeid-column";
|
|
2
|
+
import { sql } from "drizzle-orm";
|
|
3
|
+
import { boolean, check, date, index, integer, jsonb, pgEnum, pgTable, pgView, text, timestamp, uniqueIndex, } from "drizzle-orm/pg-core";
|
|
4
|
+
import { communicationChannelEnum, communicationDirectionEnum, recordStatusEnum, relationTypeEnum, } from "./schema-shared.js";
|
|
5
|
+
/**
|
|
6
|
+
* Identity-document types stored on `person_documents`. Open-ended via
|
|
7
|
+
* "other" so we don't force a schema migration for every regional
|
|
8
|
+
* variant; the structured fields cover the international shape.
|
|
9
|
+
*/
|
|
10
|
+
export const personDocumentTypeEnum = pgEnum("person_document_type", [
|
|
11
|
+
"passport",
|
|
12
|
+
"id_card",
|
|
13
|
+
"driver_license",
|
|
14
|
+
"visa",
|
|
15
|
+
"other",
|
|
16
|
+
]);
|
|
17
|
+
/**
|
|
18
|
+
* Person-to-person relationship kinds. Directed: each row records a
|
|
19
|
+
* single edge `from → to` of a specific kind. The optional inverse
|
|
20
|
+
* edge (e.g. parent ↔ child) is kept as a separate row so list
|
|
21
|
+
* queries against either side return the same shape; the service
|
|
22
|
+
* layer auto-writes the inverse on insert when an `inverseKind` is
|
|
23
|
+
* provided.
|
|
24
|
+
*/
|
|
25
|
+
export const personRelationshipKindEnum = pgEnum("person_relationship_kind", [
|
|
26
|
+
"spouse",
|
|
27
|
+
"partner",
|
|
28
|
+
"parent",
|
|
29
|
+
"child",
|
|
30
|
+
"sibling",
|
|
31
|
+
"guardian",
|
|
32
|
+
"ward",
|
|
33
|
+
"emergency_contact",
|
|
34
|
+
"friend",
|
|
35
|
+
"travel_companion",
|
|
36
|
+
"other",
|
|
37
|
+
]);
|
|
38
|
+
export const organizations = pgTable("organizations", {
|
|
39
|
+
id: typeId("organizations"),
|
|
40
|
+
name: text("name").notNull(),
|
|
41
|
+
legalName: text("legal_name"),
|
|
42
|
+
website: text("website"),
|
|
43
|
+
/** Tax / VAT identification number — used for billing + e-invoicing. */
|
|
44
|
+
taxId: text("tax_id"),
|
|
45
|
+
industry: text("industry"),
|
|
46
|
+
relation: relationTypeEnum("relation"),
|
|
47
|
+
ownerId: text("owner_id"),
|
|
48
|
+
defaultCurrency: text("default_currency"),
|
|
49
|
+
preferredLanguage: text("preferred_language"),
|
|
50
|
+
paymentTerms: integer("payment_terms"),
|
|
51
|
+
status: recordStatusEnum("status").notNull().default("active"),
|
|
52
|
+
source: text("source"),
|
|
53
|
+
sourceRef: text("source_ref"),
|
|
54
|
+
tags: jsonb("tags").$type().notNull().default([]),
|
|
55
|
+
notes: text("notes"),
|
|
56
|
+
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
|
57
|
+
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
|
|
58
|
+
archivedAt: timestamp("archived_at", { withTimezone: true }),
|
|
59
|
+
}, (table) => [
|
|
60
|
+
index("idx_organizations_name").on(table.name),
|
|
61
|
+
index("idx_organizations_owner").on(table.ownerId),
|
|
62
|
+
index("idx_organizations_status").on(table.status),
|
|
63
|
+
index("idx_organizations_tax_id").on(table.taxId),
|
|
64
|
+
index("idx_organizations_owner_updated").on(table.ownerId, table.updatedAt),
|
|
65
|
+
index("idx_organizations_relation_updated").on(table.relation, table.updatedAt),
|
|
66
|
+
index("idx_organizations_status_updated").on(table.status, table.updatedAt),
|
|
67
|
+
]);
|
|
68
|
+
export const people = pgTable("people", {
|
|
69
|
+
id: typeId("people"),
|
|
70
|
+
organizationId: typeIdRef("organization_id").references(() => organizations.id, {
|
|
71
|
+
onDelete: "set null",
|
|
72
|
+
}),
|
|
73
|
+
firstName: text("first_name").notNull(),
|
|
74
|
+
middleName: text("middle_name"),
|
|
75
|
+
lastName: text("last_name").notNull(),
|
|
76
|
+
/** ISO-style "M" / "F" / "X" — used by airline + travel-doc workflows. */
|
|
77
|
+
gender: text("gender"),
|
|
78
|
+
jobTitle: text("job_title"),
|
|
79
|
+
relation: relationTypeEnum("relation"),
|
|
80
|
+
preferredLanguage: text("preferred_language"),
|
|
81
|
+
preferredCurrency: text("preferred_currency"),
|
|
82
|
+
ownerId: text("owner_id"),
|
|
83
|
+
status: recordStatusEnum("status").notNull().default("active"),
|
|
84
|
+
source: text("source"),
|
|
85
|
+
sourceRef: text("source_ref"),
|
|
86
|
+
tags: jsonb("tags").$type().notNull().default([]),
|
|
87
|
+
dateOfBirth: date("date_of_birth"),
|
|
88
|
+
notes: text("notes"),
|
|
89
|
+
/**
|
|
90
|
+
* Encrypted PII slots — canonical store for person-level travel
|
|
91
|
+
* preferences. Documents live in their own structured table
|
|
92
|
+
* (`person_documents`); these four are kept as KMS envelopes
|
|
93
|
+
* because their internal shape is small and rarely queried.
|
|
94
|
+
*
|
|
95
|
+
* Booking-traveler rows snapshot dietary/accessibility at create
|
|
96
|
+
* time; the person record remains the source of truth for
|
|
97
|
+
* pre-fill on subsequent bookings.
|
|
98
|
+
*/
|
|
99
|
+
accessibilityEncrypted: jsonb("accessibility_encrypted").$type(),
|
|
100
|
+
dietaryEncrypted: jsonb("dietary_encrypted").$type(),
|
|
101
|
+
loyaltyEncrypted: jsonb("loyalty_encrypted").$type(),
|
|
102
|
+
insuranceEncrypted: jsonb("insurance_encrypted").$type(),
|
|
103
|
+
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
|
104
|
+
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
|
|
105
|
+
archivedAt: timestamp("archived_at", { withTimezone: true }),
|
|
106
|
+
}, (table) => [
|
|
107
|
+
index("idx_people_org").on(table.organizationId),
|
|
108
|
+
index("idx_people_owner").on(table.ownerId),
|
|
109
|
+
index("idx_people_status").on(table.status),
|
|
110
|
+
index("idx_people_name").on(table.firstName, table.lastName),
|
|
111
|
+
index("idx_people_org_updated").on(table.organizationId, table.updatedAt),
|
|
112
|
+
index("idx_people_owner_updated").on(table.ownerId, table.updatedAt),
|
|
113
|
+
index("idx_people_relation_updated").on(table.relation, table.updatedAt),
|
|
114
|
+
index("idx_people_status_updated").on(table.status, table.updatedAt),
|
|
115
|
+
]);
|
|
116
|
+
/**
|
|
117
|
+
* `person_directory` is a Postgres VIEW (not a table) that exposes
|
|
118
|
+
* each person's primary email / phone / website by `LATERAL` lookup
|
|
119
|
+
* against `identity_contact_points`. The view is created in the
|
|
120
|
+
* 0028 migration; this binding gives Drizzle a typed read surface.
|
|
121
|
+
*
|
|
122
|
+
* The previous `person_directory_projections` table was a denormalized
|
|
123
|
+
* cache that had to be rebuilt on every contact-point change — see
|
|
124
|
+
* #446 for the discussion of why we replaced it. Indexed lateral
|
|
125
|
+
* joins (`idx_identity_contact_points_entity_kind_primary_created`
|
|
126
|
+
* already exists) keep view reads sub-millisecond at realistic CRM
|
|
127
|
+
* volumes.
|
|
128
|
+
*/
|
|
129
|
+
export const personDirectoryView = pgView("person_directory", {
|
|
130
|
+
personId: typeIdRef("person_id").notNull(),
|
|
131
|
+
email: text("email"),
|
|
132
|
+
phone: text("phone"),
|
|
133
|
+
website: text("website"),
|
|
134
|
+
}).existing();
|
|
135
|
+
export const personNotes = pgTable("person_notes", {
|
|
136
|
+
id: typeId("person_notes"),
|
|
137
|
+
personId: typeIdRef("person_id")
|
|
138
|
+
.notNull()
|
|
139
|
+
.references(() => people.id, { onDelete: "cascade" }),
|
|
140
|
+
authorId: text("author_id").notNull(),
|
|
141
|
+
content: text("content").notNull(),
|
|
142
|
+
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
|
143
|
+
}, (table) => [
|
|
144
|
+
index("idx_person_notes_person").on(table.personId),
|
|
145
|
+
index("idx_person_notes_person_created").on(table.personId, table.createdAt),
|
|
146
|
+
]);
|
|
147
|
+
/**
|
|
148
|
+
* Structured identity documents owned by a person. Replaces the
|
|
149
|
+
* single `documentsEncrypted` blob shape with a row per document so
|
|
150
|
+
* we can track type / expiry / issuing authority / attachment + run
|
|
151
|
+
* "expiring soon" sweeps without parsing JSON.
|
|
152
|
+
*
|
|
153
|
+
* `numberEncrypted` is the only field encrypted at rest — the rest
|
|
154
|
+
* is non-toxic identity metadata. `attachmentId` is a free-form key
|
|
155
|
+
* (typically an object-storage path) until a general media table
|
|
156
|
+
* exists; FK is intentionally deferred.
|
|
157
|
+
*
|
|
158
|
+
* Booking-traveler rows snapshot the primary passport at create time;
|
|
159
|
+
* this table remains the source of truth for next-trip pre-fill.
|
|
160
|
+
*/
|
|
161
|
+
export const personDocuments = pgTable("person_documents", {
|
|
162
|
+
id: typeId("person_documents"),
|
|
163
|
+
personId: typeIdRef("person_id")
|
|
164
|
+
.notNull()
|
|
165
|
+
.references(() => people.id, { onDelete: "cascade" }),
|
|
166
|
+
type: personDocumentTypeEnum("type").notNull(),
|
|
167
|
+
numberEncrypted: jsonb("number_encrypted").$type(),
|
|
168
|
+
issuingAuthority: text("issuing_authority"),
|
|
169
|
+
issuingCountry: text("issuing_country"),
|
|
170
|
+
issueDate: date("issue_date"),
|
|
171
|
+
expiryDate: date("expiry_date"),
|
|
172
|
+
attachmentId: text("attachment_id"),
|
|
173
|
+
isPrimary: boolean("is_primary").notNull().default(false),
|
|
174
|
+
notes: text("notes"),
|
|
175
|
+
metadata: jsonb("metadata").$type(),
|
|
176
|
+
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
|
177
|
+
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
|
|
178
|
+
}, (table) => [
|
|
179
|
+
index("idx_person_documents_person").on(table.personId),
|
|
180
|
+
index("idx_person_documents_person_type").on(table.personId, table.type),
|
|
181
|
+
index("idx_person_documents_expiry").on(table.expiryDate),
|
|
182
|
+
uniqueIndex("uidx_person_documents_primary_per_type")
|
|
183
|
+
.on(table.personId, table.type)
|
|
184
|
+
// agent-quality: raw-sql reviewed -- owner: crm; dynamic SQL interpolation uses Drizzle parameter binding or vetted SQL identifiers.
|
|
185
|
+
.where(sql `${table.isPrimary} = true`),
|
|
186
|
+
]);
|
|
187
|
+
/**
|
|
188
|
+
* Directed person-to-person edges (kinship, emergency contacts,
|
|
189
|
+
* travel companions). Each row is a single edge `fromPerson → toPerson`
|
|
190
|
+
* of one `kind`; the reverse edge — when meaningful — is a separate
|
|
191
|
+
* row written by the service layer's auto-inverse helper. Self-edges
|
|
192
|
+
* are rejected via a CHECK constraint; the unique index on
|
|
193
|
+
* `(from, to, kind)` prevents the same directional edge from being
|
|
194
|
+
* recorded twice.
|
|
195
|
+
*
|
|
196
|
+
* `metadata` is a free-form jsonb bag for module-specific extensions
|
|
197
|
+
* (e.g. emergency-contact relationship to traveler). Operators that
|
|
198
|
+
* need richer structure should add their own typed columns.
|
|
199
|
+
*/
|
|
200
|
+
export const personRelationships = pgTable("person_relationships", {
|
|
201
|
+
id: typeId("person_relationships"),
|
|
202
|
+
fromPersonId: typeIdRef("from_person_id")
|
|
203
|
+
.notNull()
|
|
204
|
+
.references(() => people.id, { onDelete: "cascade" }),
|
|
205
|
+
toPersonId: typeIdRef("to_person_id")
|
|
206
|
+
.notNull()
|
|
207
|
+
.references(() => people.id, { onDelete: "cascade" }),
|
|
208
|
+
kind: personRelationshipKindEnum("kind").notNull(),
|
|
209
|
+
/**
|
|
210
|
+
* The kind that should label the reverse edge (e.g. parent ↔
|
|
211
|
+
* child). When set on insert, the service layer writes the
|
|
212
|
+
* inverse edge in the same transaction unless `autoInverse` is
|
|
213
|
+
* explicitly disabled.
|
|
214
|
+
*/
|
|
215
|
+
inverseKind: personRelationshipKindEnum("inverse_kind"),
|
|
216
|
+
startDate: date("start_date"),
|
|
217
|
+
endDate: date("end_date"),
|
|
218
|
+
isPrimary: boolean("is_primary").notNull().default(false),
|
|
219
|
+
notes: text("notes"),
|
|
220
|
+
metadata: jsonb("metadata").$type(),
|
|
221
|
+
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
|
222
|
+
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
|
|
223
|
+
}, (table) => [
|
|
224
|
+
index("idx_person_relationships_from").on(table.fromPersonId),
|
|
225
|
+
index("idx_person_relationships_to").on(table.toPersonId),
|
|
226
|
+
uniqueIndex("uidx_person_relationships_pair_kind").on(table.fromPersonId, table.toPersonId, table.kind),
|
|
227
|
+
// agent-quality: raw-sql reviewed -- owner: crm; dynamic SQL interpolation uses Drizzle parameter binding or vetted SQL identifiers.
|
|
228
|
+
check("person_relationships_no_self", sql `${table.fromPersonId} <> ${table.toPersonId}`),
|
|
229
|
+
]);
|
|
230
|
+
/**
|
|
231
|
+
* Saved payment methods on file for a person. Stores processor-issued
|
|
232
|
+
* tokens (never raw card numbers) so the booking flow can charge the
|
|
233
|
+
* customer without re-entering card details. Cards have last4 + expiry +
|
|
234
|
+
* brand; bank-transfer "methods" carry a brand of "bank_transfer" with
|
|
235
|
+
* last4 / expiry omitted.
|
|
236
|
+
*/
|
|
237
|
+
export const personPaymentMethods = pgTable("person_payment_methods", {
|
|
238
|
+
id: typeId("person_payment_methods"),
|
|
239
|
+
personId: typeIdRef("person_id")
|
|
240
|
+
.notNull()
|
|
241
|
+
.references(() => people.id, { onDelete: "cascade" }),
|
|
242
|
+
/** "visa" | "mastercard" | "amex" | "revolut" | "bank_transfer" — kept as text to stay open. */
|
|
243
|
+
brand: text("brand").notNull(),
|
|
244
|
+
/** Last four digits — null for non-card methods. */
|
|
245
|
+
last4: text("last4"),
|
|
246
|
+
holderName: text("holder_name"),
|
|
247
|
+
/** 1-12; null for non-card methods. */
|
|
248
|
+
expMonth: integer("exp_month"),
|
|
249
|
+
expYear: integer("exp_year"),
|
|
250
|
+
/** Opaque processor token — used to charge the customer. */
|
|
251
|
+
processorToken: text("processor_token").notNull(),
|
|
252
|
+
isDefault: boolean("is_default").notNull().default(false),
|
|
253
|
+
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
|
254
|
+
}, (table) => [
|
|
255
|
+
index("idx_person_payment_methods_person").on(table.personId),
|
|
256
|
+
index("idx_person_payment_methods_person_default").on(table.personId, table.isDefault),
|
|
257
|
+
]);
|
|
258
|
+
export const organizationNotes = pgTable("organization_notes", {
|
|
259
|
+
id: typeId("organization_notes"),
|
|
260
|
+
organizationId: typeIdRef("organization_id")
|
|
261
|
+
.notNull()
|
|
262
|
+
.references(() => organizations.id, { onDelete: "cascade" }),
|
|
263
|
+
authorId: text("author_id").notNull(),
|
|
264
|
+
content: text("content").notNull(),
|
|
265
|
+
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
|
266
|
+
}, (table) => [
|
|
267
|
+
index("idx_organization_notes_org").on(table.organizationId),
|
|
268
|
+
index("idx_organization_notes_org_created").on(table.organizationId, table.createdAt),
|
|
269
|
+
]);
|
|
270
|
+
export const communicationLog = pgTable("communication_log", {
|
|
271
|
+
id: typeId("communication_log"),
|
|
272
|
+
personId: typeIdRef("person_id")
|
|
273
|
+
.notNull()
|
|
274
|
+
.references(() => people.id, { onDelete: "cascade" }),
|
|
275
|
+
organizationId: typeIdRef("organization_id").references(() => organizations.id, {
|
|
276
|
+
onDelete: "set null",
|
|
277
|
+
}),
|
|
278
|
+
channel: communicationChannelEnum("channel").notNull(),
|
|
279
|
+
direction: communicationDirectionEnum("direction").notNull(),
|
|
280
|
+
subject: text("subject"),
|
|
281
|
+
content: text("content"),
|
|
282
|
+
sentAt: timestamp("sent_at", { withTimezone: true }),
|
|
283
|
+
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
|
284
|
+
}, (table) => [
|
|
285
|
+
index("idx_communication_log_person").on(table.personId),
|
|
286
|
+
index("idx_communication_log_person_created").on(table.personId, table.createdAt),
|
|
287
|
+
index("idx_communication_log_person_channel_created").on(table.personId, table.channel, table.createdAt),
|
|
288
|
+
index("idx_communication_log_person_direction_created").on(table.personId, table.direction, table.createdAt),
|
|
289
|
+
index("idx_communication_log_org").on(table.organizationId),
|
|
290
|
+
index("idx_communication_log_channel").on(table.channel),
|
|
291
|
+
]);
|
|
292
|
+
export const segments = pgTable("segments", {
|
|
293
|
+
id: typeId("segments"),
|
|
294
|
+
name: text("name").notNull(),
|
|
295
|
+
description: text("description"),
|
|
296
|
+
conditions: jsonb("conditions").$type(),
|
|
297
|
+
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
|
298
|
+
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
|
|
299
|
+
}, (table) => [index("idx_segments_created").on(table.createdAt)]);
|
|
300
|
+
export const segmentMembers = pgTable("segment_members", {
|
|
301
|
+
id: typeId("segment_members"),
|
|
302
|
+
segmentId: typeIdRef("segment_id")
|
|
303
|
+
.notNull()
|
|
304
|
+
.references(() => segments.id, { onDelete: "cascade" }),
|
|
305
|
+
personId: typeIdRef("person_id")
|
|
306
|
+
.notNull()
|
|
307
|
+
.references(() => people.id, { onDelete: "cascade" }),
|
|
308
|
+
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
|
309
|
+
}, (table) => [
|
|
310
|
+
index("idx_segment_members_segment").on(table.segmentId),
|
|
311
|
+
index("idx_segment_members_person").on(table.personId),
|
|
312
|
+
]);
|