eggi-ai-db-schema-2 12.54.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.
Files changed (188) hide show
  1. package/CHANGELOG.md +750 -0
  2. package/README.md +655 -0
  3. package/dist/config/database.d.ts +28 -0
  4. package/dist/config/database.d.ts.map +1 -0
  5. package/dist/config/database.js +72 -0
  6. package/dist/config/database.js.map +1 -0
  7. package/dist/index.d.ts +28 -0
  8. package/dist/index.d.ts.map +1 -0
  9. package/dist/index.js +199 -0
  10. package/dist/index.js.map +1 -0
  11. package/dist/lib/database-service.d.ts +689 -0
  12. package/dist/lib/database-service.d.ts.map +1 -0
  13. package/dist/lib/database-service.js +1362 -0
  14. package/dist/lib/database-service.js.map +1 -0
  15. package/dist/lib/db-types.d.ts +167 -0
  16. package/dist/lib/db-types.d.ts.map +1 -0
  17. package/dist/lib/db-types.js +28 -0
  18. package/dist/lib/db-types.js.map +1 -0
  19. package/dist/lib/db.d.ts +58 -0
  20. package/dist/lib/db.d.ts.map +1 -0
  21. package/dist/lib/db.js +292 -0
  22. package/dist/lib/db.js.map +1 -0
  23. package/dist/lib/index.d.ts +11 -0
  24. package/dist/lib/index.d.ts.map +1 -0
  25. package/dist/lib/index.js +26 -0
  26. package/dist/lib/index.js.map +1 -0
  27. package/dist/lib/pg-client.d.ts +50 -0
  28. package/dist/lib/pg-client.d.ts.map +1 -0
  29. package/dist/lib/pg-client.js +106 -0
  30. package/dist/lib/pg-client.js.map +1 -0
  31. package/dist/lib/schema.d.ts +298 -0
  32. package/dist/lib/schema.d.ts.map +1 -0
  33. package/dist/lib/schema.js +12 -0
  34. package/dist/lib/schema.js.map +1 -0
  35. package/dist/migration-manager.d.ts +49 -0
  36. package/dist/migration-manager.d.ts.map +1 -0
  37. package/dist/migration-manager.js +282 -0
  38. package/dist/migration-manager.js.map +1 -0
  39. package/dist/queries/minimal-connections.d.ts +31 -0
  40. package/dist/queries/minimal-connections.d.ts.map +1 -0
  41. package/dist/queries/minimal-connections.js +143 -0
  42. package/dist/queries/minimal-connections.js.map +1 -0
  43. package/dist/schema.ts +340 -0
  44. package/dist/seed.d.ts +8 -0
  45. package/dist/seed.d.ts.map +1 -0
  46. package/dist/seed.js +40 -0
  47. package/dist/seed.js.map +1 -0
  48. package/dist/types/index.d.ts +7 -0
  49. package/dist/types/index.d.ts.map +1 -0
  50. package/dist/types/index.js +23 -0
  51. package/dist/types/index.js.map +1 -0
  52. package/dist/types/types.d.ts +77 -0
  53. package/dist/types/types.d.ts.map +1 -0
  54. package/dist/types/types.js +3 -0
  55. package/dist/types/types.js.map +1 -0
  56. package/dist/utils/authenticated-user-operations.d.ts +110 -0
  57. package/dist/utils/authenticated-user-operations.d.ts.map +1 -0
  58. package/dist/utils/authenticated-user-operations.js +292 -0
  59. package/dist/utils/authenticated-user-operations.js.map +1 -0
  60. package/dist/utils/authentication-operations.d.ts +48 -0
  61. package/dist/utils/authentication-operations.d.ts.map +1 -0
  62. package/dist/utils/authentication-operations.js +172 -0
  63. package/dist/utils/authentication-operations.js.map +1 -0
  64. package/dist/utils/company-mapping-job-operations.d.ts +103 -0
  65. package/dist/utils/company-mapping-job-operations.d.ts.map +1 -0
  66. package/dist/utils/company-mapping-job-operations.js +413 -0
  67. package/dist/utils/company-mapping-job-operations.js.map +1 -0
  68. package/dist/utils/company-sheet-upload-operations.d.ts +53 -0
  69. package/dist/utils/company-sheet-upload-operations.d.ts.map +1 -0
  70. package/dist/utils/company-sheet-upload-operations.js +135 -0
  71. package/dist/utils/company-sheet-upload-operations.js.map +1 -0
  72. package/dist/utils/contact-operations.d.ts +70 -0
  73. package/dist/utils/contact-operations.d.ts.map +1 -0
  74. package/dist/utils/contact-operations.js +294 -0
  75. package/dist/utils/contact-operations.js.map +1 -0
  76. package/dist/utils/forager-linkedin-operations.d.ts +74 -0
  77. package/dist/utils/forager-linkedin-operations.d.ts.map +1 -0
  78. package/dist/utils/forager-linkedin-operations.js +778 -0
  79. package/dist/utils/forager-linkedin-operations.js.map +1 -0
  80. package/dist/utils/ghost-genius-linkedin-operations.d.ts +23 -0
  81. package/dist/utils/ghost-genius-linkedin-operations.d.ts.map +1 -0
  82. package/dist/utils/ghost-genius-linkedin-operations.js +282 -0
  83. package/dist/utils/ghost-genius-linkedin-operations.js.map +1 -0
  84. package/dist/utils/index.d.ts +29 -0
  85. package/dist/utils/index.d.ts.map +1 -0
  86. package/dist/utils/index.js +77 -0
  87. package/dist/utils/index.js.map +1 -0
  88. package/dist/utils/introduction-request-operations.d.ts +159 -0
  89. package/dist/utils/introduction-request-operations.d.ts.map +1 -0
  90. package/dist/utils/introduction-request-operations.js +481 -0
  91. package/dist/utils/introduction-request-operations.js.map +1 -0
  92. package/dist/utils/invitation-operations.d.ts +141 -0
  93. package/dist/utils/invitation-operations.d.ts.map +1 -0
  94. package/dist/utils/invitation-operations.js +749 -0
  95. package/dist/utils/invitation-operations.js.map +1 -0
  96. package/dist/utils/linkedin-account-operations.d.ts +45 -0
  97. package/dist/utils/linkedin-account-operations.d.ts.map +1 -0
  98. package/dist/utils/linkedin-account-operations.js +279 -0
  99. package/dist/utils/linkedin-account-operations.js.map +1 -0
  100. package/dist/utils/linkedin-account-relationship-operations.d.ts +77 -0
  101. package/dist/utils/linkedin-account-relationship-operations.d.ts.map +1 -0
  102. package/dist/utils/linkedin-account-relationship-operations.js +274 -0
  103. package/dist/utils/linkedin-account-relationship-operations.js.map +1 -0
  104. package/dist/utils/linkedin-data-operations.d.ts +102 -0
  105. package/dist/utils/linkedin-data-operations.d.ts.map +1 -0
  106. package/dist/utils/linkedin-data-operations.js +613 -0
  107. package/dist/utils/linkedin-data-operations.js.map +1 -0
  108. package/dist/utils/linkedin-identifier-utils.d.ts +31 -0
  109. package/dist/utils/linkedin-identifier-utils.d.ts.map +1 -0
  110. package/dist/utils/linkedin-identifier-utils.js +63 -0
  111. package/dist/utils/linkedin-identifier-utils.js.map +1 -0
  112. package/dist/utils/linkedin-profile-cache.d.ts +131 -0
  113. package/dist/utils/linkedin-profile-cache.d.ts.map +1 -0
  114. package/dist/utils/linkedin-profile-cache.js +418 -0
  115. package/dist/utils/linkedin-profile-cache.js.map +1 -0
  116. package/dist/utils/llm-inference-job-operations.d.ts +116 -0
  117. package/dist/utils/llm-inference-job-operations.d.ts.map +1 -0
  118. package/dist/utils/llm-inference-job-operations.js +266 -0
  119. package/dist/utils/llm-inference-job-operations.js.map +1 -0
  120. package/dist/utils/mapping-job-operations.d.ts +272 -0
  121. package/dist/utils/mapping-job-operations.d.ts.map +1 -0
  122. package/dist/utils/mapping-job-operations.js +833 -0
  123. package/dist/utils/mapping-job-operations.js.map +1 -0
  124. package/dist/utils/mapping-operations.d.ts +80 -0
  125. package/dist/utils/mapping-operations.d.ts.map +1 -0
  126. package/dist/utils/mapping-operations.js +318 -0
  127. package/dist/utils/mapping-operations.js.map +1 -0
  128. package/dist/utils/on-demand-mapping-operations.d.ts +199 -0
  129. package/dist/utils/on-demand-mapping-operations.d.ts.map +1 -0
  130. package/dist/utils/on-demand-mapping-operations.js +728 -0
  131. package/dist/utils/on-demand-mapping-operations.js.map +1 -0
  132. package/dist/utils/onboarding-operations.d.ts +53 -0
  133. package/dist/utils/onboarding-operations.d.ts.map +1 -0
  134. package/dist/utils/onboarding-operations.js +223 -0
  135. package/dist/utils/onboarding-operations.js.map +1 -0
  136. package/dist/utils/organization-assignment-job-operations.d.ts +258 -0
  137. package/dist/utils/organization-assignment-job-operations.d.ts.map +1 -0
  138. package/dist/utils/organization-assignment-job-operations.js +881 -0
  139. package/dist/utils/organization-assignment-job-operations.js.map +1 -0
  140. package/dist/utils/organization-assignment-operations.d.ts +59 -0
  141. package/dist/utils/organization-assignment-operations.d.ts.map +1 -0
  142. package/dist/utils/organization-assignment-operations.js +130 -0
  143. package/dist/utils/organization-assignment-operations.js.map +1 -0
  144. package/dist/utils/organization-operations.d.ts +275 -0
  145. package/dist/utils/organization-operations.d.ts.map +1 -0
  146. package/dist/utils/organization-operations.js +993 -0
  147. package/dist/utils/organization-operations.js.map +1 -0
  148. package/dist/utils/organization-relationship-operations.d.ts +59 -0
  149. package/dist/utils/organization-relationship-operations.d.ts.map +1 -0
  150. package/dist/utils/organization-relationship-operations.js +240 -0
  151. package/dist/utils/organization-relationship-operations.js.map +1 -0
  152. package/dist/utils/quota-operations.d.ts +107 -0
  153. package/dist/utils/quota-operations.d.ts.map +1 -0
  154. package/dist/utils/quota-operations.js +692 -0
  155. package/dist/utils/quota-operations.js.map +1 -0
  156. package/dist/utils/recursive-mapping-job-operations.d.ts +42 -0
  157. package/dist/utils/recursive-mapping-job-operations.d.ts.map +1 -0
  158. package/dist/utils/recursive-mapping-job-operations.js +169 -0
  159. package/dist/utils/recursive-mapping-job-operations.js.map +1 -0
  160. package/dist/utils/relationship-operations.d.ts +130 -0
  161. package/dist/utils/relationship-operations.d.ts.map +1 -0
  162. package/dist/utils/relationship-operations.js +329 -0
  163. package/dist/utils/relationship-operations.js.map +1 -0
  164. package/dist/utils/sales-pipeline-operations.d.ts +143 -0
  165. package/dist/utils/sales-pipeline-operations.d.ts.map +1 -0
  166. package/dist/utils/sales-pipeline-operations.js +649 -0
  167. package/dist/utils/sales-pipeline-operations.js.map +1 -0
  168. package/dist/utils/skills-operations.d.ts +117 -0
  169. package/dist/utils/skills-operations.d.ts.map +1 -0
  170. package/dist/utils/skills-operations.js +487 -0
  171. package/dist/utils/skills-operations.js.map +1 -0
  172. package/dist/utils/subscription-operations.d.ts +123 -0
  173. package/dist/utils/subscription-operations.d.ts.map +1 -0
  174. package/dist/utils/subscription-operations.js +391 -0
  175. package/dist/utils/subscription-operations.js.map +1 -0
  176. package/dist/utils/unipile-account-operations.d.ts +96 -0
  177. package/dist/utils/unipile-account-operations.d.ts.map +1 -0
  178. package/dist/utils/unipile-account-operations.js +255 -0
  179. package/dist/utils/unipile-account-operations.js.map +1 -0
  180. package/dist/utils/user-industry-operations.d.ts +80 -0
  181. package/dist/utils/user-industry-operations.d.ts.map +1 -0
  182. package/dist/utils/user-industry-operations.js +237 -0
  183. package/dist/utils/user-industry-operations.js.map +1 -0
  184. package/dist/utils/user-operations.d.ts +87 -0
  185. package/dist/utils/user-operations.d.ts.map +1 -0
  186. package/dist/utils/user-operations.js +212 -0
  187. package/dist/utils/user-operations.js.map +1 -0
  188. package/package.json +98 -0
@@ -0,0 +1,749 @@
1
+ "use strict";
2
+ /**
3
+ * =============================================================================
4
+ * INVITATION OPERATIONS - Database Operations for User Invitations
5
+ * =============================================================================
6
+ * Helper functions for managing user invitations to organizations
7
+ */
8
+ var __importDefault = (this && this.__importDefault) || function (mod) {
9
+ return (mod && mod.__esModule) ? mod : { "default": mod };
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.validateInvitationEligibility = validateInvitationEligibility;
13
+ exports.hasActiveInvitationByEmail = hasActiveInvitationByEmail;
14
+ exports.hasActiveInvitationByEmailForOrg = hasActiveInvitationByEmailForOrg;
15
+ exports.isEmailUsedByAuthenticatedUser = isEmailUsedByAuthenticatedUser;
16
+ exports.isEmailReservedByContactInfo = isEmailReservedByContactInfo;
17
+ exports.generateInvitationToken = generateInvitationToken;
18
+ exports.createInvitation = createInvitation;
19
+ exports.validateInvitationToken = validateInvitationToken;
20
+ exports.markInvitationAsUsed = markInvitationAsUsed;
21
+ exports.revokeInvitation = revokeInvitation;
22
+ exports.getInvitationById = getInvitationById;
23
+ exports.getInvitationByToken = getInvitationByToken;
24
+ exports.getPendingInvitationsByOrganization = getPendingInvitationsByOrganization;
25
+ exports.getAllInvitationsByOrganization = getAllInvitationsByOrganization;
26
+ exports.hasPendingInvitation = hasPendingInvitation;
27
+ exports.hasActiveInvitationForShadowUser = hasActiveInvitationForShadowUser;
28
+ exports.getInvitationsSentByUser = getInvitationsSentByUser;
29
+ exports.getPendingInvitationsSentByUser = getPendingInvitationsSentByUser;
30
+ exports.cleanupExpiredInvitations = cleanupExpiredInvitations;
31
+ const pg_client_1 = require("../lib/pg-client");
32
+ const crypto_1 = __importDefault(require("crypto"));
33
+ // =============================================================================
34
+ // ELIGIBILITY CHECKS
35
+ // =============================================================================
36
+ /**
37
+ * Comprehensive validation for invitation eligibility
38
+ * Checks all three scenarios:
39
+ * a) No active invitation exists for this email
40
+ * b) Email is not already used by an authenticated user
41
+ * c) Email is not reserved by contact_info
42
+ */
43
+ async function validateInvitationEligibility(db, email) {
44
+ const normalizedEmail = email.toLowerCase().trim();
45
+ // Check a) No active invitation exists for this email
46
+ const hasActiveInvitation = await hasActiveInvitationByEmail(db, normalizedEmail);
47
+ if (hasActiveInvitation) {
48
+ return { eligible: false, reason: "active_invitation" };
49
+ }
50
+ // Check b) Email is not already used by an authenticated user
51
+ const isEmailUsed = await isEmailUsedByAuthenticatedUser(db, normalizedEmail);
52
+ if (isEmailUsed) {
53
+ return { eligible: false, reason: "email_registered" };
54
+ }
55
+ // Check c) Email is not reserved by contact_info
56
+ const isEmailReserved = await isEmailReservedByContactInfo(db, normalizedEmail);
57
+ if (isEmailReserved) {
58
+ return { eligible: false, reason: "email_reserved" };
59
+ }
60
+ return { eligible: true };
61
+ }
62
+ /**
63
+ * Returns true if there is a still-active (pending) invitation for this email anywhere
64
+ * Active means: not used, not revoked, and not expired
65
+ */
66
+ async function hasActiveInvitationByEmail(db, email) {
67
+ const normalized = email.toLowerCase().trim();
68
+ const sql = `
69
+ SELECT id FROM authentication.invitations
70
+ WHERE email = $1
71
+ AND used_at IS NULL
72
+ AND revoked_at IS NULL
73
+ AND expires_at > NOW()
74
+ LIMIT 1
75
+ `;
76
+ const row = await (0, pg_client_1.queryOne)(db, sql, [normalized]);
77
+ return !!row;
78
+ }
79
+ /**
80
+ * Returns true if there is a still-active (pending) invitation for this email in the given organization
81
+ */
82
+ async function hasActiveInvitationByEmailForOrg(db, email, organizationId) {
83
+ const normalized = email.toLowerCase().trim();
84
+ const sql = `
85
+ SELECT id FROM authentication.invitations
86
+ WHERE email = $1
87
+ AND organization_id = $2
88
+ AND used_at IS NULL
89
+ AND revoked_at IS NULL
90
+ AND expires_at > NOW()
91
+ LIMIT 1
92
+ `;
93
+ const row = await (0, pg_client_1.queryOne)(db, sql, [normalized, organizationId]);
94
+ return !!row;
95
+ }
96
+ /**
97
+ * Email is already used by an authenticated user if there exists authentication.users → contact_infos value match (case-insensitive)
98
+ */
99
+ async function isEmailUsedByAuthenticatedUser(db, email) {
100
+ const normalized = email.toLowerCase().trim();
101
+ const sql = `
102
+ SELECT 1 FROM authentication.users au
103
+ JOIN public.contact_infos ci ON ci.id = au.contact_info_id
104
+ WHERE LOWER(ci.value) = $1
105
+ LIMIT 1
106
+ `;
107
+ const row = await (0, pg_client_1.queryOne)(db, sql, [normalized]);
108
+ return !!row;
109
+ }
110
+ /**
111
+ * Returns true if any contact_info indicates the email is reserved/owned and should not be invited again.
112
+ * Criteria: ci.type = 'EMAIL' OR ci.source IN ('MANUAL', 'INVITATION') for the same email value.
113
+ */
114
+ async function isEmailReservedByContactInfo(db, email) {
115
+ const normalized = email.toLowerCase().trim();
116
+ const sql = `
117
+ SELECT 1 FROM public.contact_infos ci
118
+ LEFT JOIN authentication.users au ON au.contact_info_id = ci.id
119
+ WHERE LOWER(ci.value) = $1
120
+ AND (
121
+ (ci.type = 'EMAIL' AND au.id IS NOT NULL)
122
+ OR (ci.source IN ('MANUAL', 'INVITATION') AND au.id IS NOT NULL)
123
+ )
124
+ LIMIT 1
125
+ `;
126
+ const row = await (0, pg_client_1.queryOne)(db, sql, [normalized]);
127
+ return !!row;
128
+ }
129
+ // =============================================================================
130
+ // TOKEN GENERATION
131
+ // =============================================================================
132
+ /**
133
+ * Generate a cryptographically secure invitation token
134
+ * @returns 32-character hex string
135
+ */
136
+ function generateInvitationToken() {
137
+ return crypto_1.default.randomBytes(16).toString("hex");
138
+ }
139
+ /**
140
+ * Helper function to fetch shadow user details with profile picture
141
+ */
142
+ async function fetchShadowUserWithProfilePicture(db, shadowUserId) {
143
+ const userSql = `
144
+ SELECT id, given_name, family_name FROM public.users WHERE id = $1 LIMIT 1
145
+ `;
146
+ const shadowUserRecord = await (0, pg_client_1.queryOne)(db, userSql, [shadowUserId]);
147
+ if (!shadowUserRecord) {
148
+ throw new Error(`Shadow user with ID ${shadowUserId} not found`);
149
+ }
150
+ // Get LinkedIn profile picture if available (prefer CDN URL over regular LinkedIn URL)
151
+ const linkedinSql = `
152
+ SELECT
153
+ profile_picture_url,
154
+ profile_image_cloudfront_url
155
+ FROM linkedin.accounts
156
+ WHERE user_id = $1
157
+ LIMIT 1
158
+ `;
159
+ const linkedinAccount = await (0, pg_client_1.queryOne)(db, linkedinSql, [shadowUserId]);
160
+ // Prefer CDN URL over regular LinkedIn URL
161
+ const profilePictureUrl = linkedinAccount?.profile_image_cloudfront_url || linkedinAccount?.profile_picture_url || null;
162
+ return {
163
+ id: shadowUserRecord.id,
164
+ givenName: shadowUserRecord.given_name,
165
+ familyName: shadowUserRecord.family_name,
166
+ profilePictureUrl: profilePictureUrl,
167
+ };
168
+ }
169
+ // =============================================================================
170
+ // CREATE INVITATION
171
+ // =============================================================================
172
+ /**
173
+ * Create a new invitation
174
+ */
175
+ async function createInvitation(db, input) {
176
+ const token = generateInvitationToken();
177
+ const expiresInDays = input.expiresInDays ?? 7;
178
+ const normalizedEmail = input.email.toLowerCase().trim();
179
+ // Comprehensive validation: Check all three scenarios
180
+ const eligibilityCheck = await validateInvitationEligibility(db, normalizedEmail);
181
+ if (!eligibilityCheck.eligible) {
182
+ switch (eligibilityCheck.reason) {
183
+ case "active_invitation":
184
+ throw new Error("DUPLICATE_ACTIVE_INVITATION");
185
+ case "email_registered":
186
+ throw new Error("EMAIL_ALREADY_REGISTERED");
187
+ case "email_reserved":
188
+ throw new Error("EMAIL_ALREADY_RESERVED");
189
+ default:
190
+ throw new Error("INVITATION_NOT_ELIGIBLE");
191
+ }
192
+ }
193
+ // Handle contact info creation/retrieval for invitation
194
+ const contactInfoId = await ensureContactInfoForInvitation(db, normalizedEmail, input.shadowUserId);
195
+ const sql = `
196
+ INSERT INTO authentication.invitations (
197
+ email, token, invitation_message, invited_user_permissions_role,
198
+ invited_by_user_id, shadow_user_id, organization_id, contact_info_id,
199
+ expires_at, created_at
200
+ )
201
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, NOW() + ($9 * INTERVAL '1 day'), NOW())
202
+ RETURNING
203
+ id, email, token, invitation_message, invited_user_permissions_role,
204
+ invited_by_user_id, shadow_user_id, organization_id,
205
+ created_at, expires_at, used_at, revoked_at
206
+ `;
207
+ const invitation = await (0, pg_client_1.queryOne)(db, sql, [
208
+ normalizedEmail,
209
+ token,
210
+ input.invitationMessage || null,
211
+ input.invitedUserPermissionsRole || "REGULAR",
212
+ input.invitedByUserId,
213
+ input.shadowUserId,
214
+ input.organizationId,
215
+ contactInfoId,
216
+ expiresInDays
217
+ ]);
218
+ if (!invitation) {
219
+ throw new Error("Failed to create invitation");
220
+ }
221
+ return {
222
+ id: invitation.id,
223
+ email: invitation.email,
224
+ token: invitation.token,
225
+ invitationMessage: invitation.invitation_message,
226
+ invitedUserPermissionsRole: invitation.invited_user_permissions_role,
227
+ invitedByUserId: invitation.invited_by_user_id,
228
+ shadowUserId: invitation.shadow_user_id,
229
+ organizationId: invitation.organization_id,
230
+ createdAt: invitation.created_at,
231
+ expiresAt: invitation.expires_at,
232
+ usedAt: invitation.used_at,
233
+ revokedAt: invitation.revoked_at,
234
+ };
235
+ }
236
+ // =============================================================================
237
+ // VALIDATE TOKEN
238
+ // =============================================================================
239
+ /**
240
+ * Validate an invitation token and return invitation details
241
+ */
242
+ async function validateInvitationToken(db, token) {
243
+ // First get the invitation
244
+ const invitationSql = `
245
+ SELECT
246
+ id, email, token, invitation_message, invited_user_permissions_role,
247
+ invited_by_user_id, shadow_user_id, organization_id,
248
+ created_at, expires_at, used_at, revoked_at
249
+ FROM authentication.invitations
250
+ WHERE token = $1
251
+ LIMIT 1
252
+ `;
253
+ const invitationRecord = await (0, pg_client_1.queryOne)(db, invitationSql, [token]);
254
+ if (!invitationRecord) {
255
+ return { valid: false, reason: "not_found" };
256
+ }
257
+ // Check if already used
258
+ if (invitationRecord.used_at) {
259
+ return { valid: false, reason: "already_used" };
260
+ }
261
+ // Check if revoked
262
+ if (invitationRecord.revoked_at) {
263
+ return { valid: false, reason: "revoked" };
264
+ }
265
+ // Check if expired
266
+ if (new Date() > new Date(invitationRecord.expires_at)) {
267
+ return { valid: false, reason: "expired" };
268
+ }
269
+ // Get inviter user details with LinkedIn profile picture
270
+ const invitedByUserRecord = await fetchShadowUserWithProfilePicture(db, invitationRecord.invited_by_user_id);
271
+ // Get shadow user details with LinkedIn profile picture
272
+ const shadowUserRecord = await fetchShadowUserWithProfilePicture(db, invitationRecord.shadow_user_id);
273
+ const orgSql = `SELECT id, name FROM public.organizations WHERE id = $1 LIMIT 1`;
274
+ const organizationRecord = await (0, pg_client_1.queryOne)(db, orgSql, [invitationRecord.organization_id]);
275
+ if (!organizationRecord) {
276
+ throw new Error(`Organization ${invitationRecord.organization_id} not found`);
277
+ }
278
+ const invitation = {
279
+ id: invitationRecord.id,
280
+ email: invitationRecord.email,
281
+ token: invitationRecord.token,
282
+ invitationMessage: invitationRecord.invitation_message,
283
+ invitedUserPermissionsRole: invitationRecord.invited_user_permissions_role,
284
+ invitedByUserId: invitationRecord.invited_by_user_id,
285
+ shadowUserId: invitationRecord.shadow_user_id,
286
+ organizationId: invitationRecord.organization_id,
287
+ createdAt: invitationRecord.created_at,
288
+ expiresAt: invitationRecord.expires_at,
289
+ usedAt: invitationRecord.used_at,
290
+ revokedAt: invitationRecord.revoked_at,
291
+ invitedByUser: invitedByUserRecord,
292
+ shadowUser: shadowUserRecord,
293
+ organization: organizationRecord,
294
+ };
295
+ return {
296
+ valid: true,
297
+ invitation,
298
+ };
299
+ }
300
+ // =============================================================================
301
+ // MARK AS USED
302
+ // =============================================================================
303
+ /**
304
+ * Mark an invitation as used
305
+ */
306
+ async function markInvitationAsUsed(db, token) {
307
+ const sql = `
308
+ UPDATE authentication.invitations
309
+ SET used_at = NOW()
310
+ WHERE token = $1
311
+ RETURNING
312
+ id, email, token, invitation_message, invited_user_permissions_role,
313
+ invited_by_user_id, shadow_user_id, organization_id,
314
+ created_at, expires_at, used_at, revoked_at
315
+ `;
316
+ const invitation = await (0, pg_client_1.queryOne)(db, sql, [token]);
317
+ if (!invitation)
318
+ return null;
319
+ return {
320
+ id: invitation.id,
321
+ email: invitation.email,
322
+ token: invitation.token,
323
+ invitationMessage: invitation.invitation_message,
324
+ invitedUserPermissionsRole: invitation.invited_user_permissions_role,
325
+ invitedByUserId: invitation.invited_by_user_id,
326
+ shadowUserId: invitation.shadow_user_id,
327
+ organizationId: invitation.organization_id,
328
+ createdAt: invitation.created_at,
329
+ expiresAt: invitation.expires_at,
330
+ usedAt: invitation.used_at,
331
+ revokedAt: invitation.revoked_at,
332
+ };
333
+ }
334
+ // =============================================================================
335
+ // REVOKE INVITATION
336
+ // =============================================================================
337
+ /**
338
+ * Revoke an invitation
339
+ */
340
+ async function revokeInvitation(db, invitationId) {
341
+ const sql = `
342
+ UPDATE authentication.invitations
343
+ SET revoked_at = NOW()
344
+ WHERE id = $1
345
+ AND used_at IS NULL
346
+ AND revoked_at IS NULL
347
+ RETURNING
348
+ id, email, token, invitation_message, invited_user_permissions_role,
349
+ invited_by_user_id, shadow_user_id, organization_id,
350
+ created_at, expires_at, used_at, revoked_at
351
+ `;
352
+ const invitation = await (0, pg_client_1.queryOne)(db, sql, [invitationId]);
353
+ if (!invitation)
354
+ return null;
355
+ return {
356
+ id: invitation.id,
357
+ email: invitation.email,
358
+ token: invitation.token,
359
+ invitationMessage: invitation.invitation_message,
360
+ invitedUserPermissionsRole: invitation.invited_user_permissions_role,
361
+ invitedByUserId: invitation.invited_by_user_id,
362
+ shadowUserId: invitation.shadow_user_id,
363
+ organizationId: invitation.organization_id,
364
+ createdAt: invitation.created_at,
365
+ expiresAt: invitation.expires_at,
366
+ usedAt: invitation.used_at,
367
+ revokedAt: invitation.revoked_at,
368
+ };
369
+ }
370
+ // =============================================================================
371
+ // GET INVITATION
372
+ // =============================================================================
373
+ /**
374
+ * Get invitation by ID
375
+ */
376
+ async function getInvitationById(db, invitationId) {
377
+ const invitationSql = `
378
+ SELECT
379
+ id, email, token, invitation_message, invited_user_permissions_role,
380
+ invited_by_user_id, shadow_user_id, organization_id,
381
+ created_at, expires_at, used_at, revoked_at
382
+ FROM authentication.invitations
383
+ WHERE id = $1
384
+ LIMIT 1
385
+ `;
386
+ const invitationRecord = await (0, pg_client_1.queryOne)(db, invitationSql, [invitationId]);
387
+ if (!invitationRecord) {
388
+ return null;
389
+ }
390
+ const userSql = `SELECT id, given_name, family_name FROM public.users WHERE id = $1 LIMIT 1`;
391
+ const invitedByUserRecord = await (0, pg_client_1.queryOne)(db, userSql, [invitationRecord.invited_by_user_id]);
392
+ const orgSql = `SELECT id, name FROM public.organizations WHERE id = $1 LIMIT 1`;
393
+ const organizationRecord = await (0, pg_client_1.queryOne)(db, orgSql, [invitationRecord.organization_id]);
394
+ const shadowUserRecord = await fetchShadowUserWithProfilePicture(db, invitationRecord.shadow_user_id);
395
+ if (!invitedByUserRecord || !organizationRecord) {
396
+ throw new Error("Missing related records for invitation");
397
+ }
398
+ return {
399
+ id: invitationRecord.id,
400
+ email: invitationRecord.email,
401
+ token: invitationRecord.token,
402
+ invitationMessage: invitationRecord.invitation_message,
403
+ invitedUserPermissionsRole: invitationRecord.invited_user_permissions_role,
404
+ invitedByUserId: invitationRecord.invited_by_user_id,
405
+ shadowUserId: invitationRecord.shadow_user_id,
406
+ organizationId: invitationRecord.organization_id,
407
+ createdAt: invitationRecord.created_at,
408
+ expiresAt: invitationRecord.expires_at,
409
+ usedAt: invitationRecord.used_at,
410
+ revokedAt: invitationRecord.revoked_at,
411
+ invitedByUser: {
412
+ id: invitedByUserRecord.id,
413
+ givenName: invitedByUserRecord.given_name,
414
+ familyName: invitedByUserRecord.family_name,
415
+ },
416
+ shadowUser: shadowUserRecord,
417
+ organization: organizationRecord,
418
+ };
419
+ }
420
+ /**
421
+ * Get invitation by token
422
+ */
423
+ async function getInvitationByToken(db, token) {
424
+ const invitationSql = `
425
+ SELECT
426
+ id, email, token, invitation_message, invited_user_permissions_role,
427
+ invited_by_user_id, shadow_user_id, organization_id,
428
+ created_at, expires_at, used_at, revoked_at
429
+ FROM authentication.invitations
430
+ WHERE token = $1
431
+ LIMIT 1
432
+ `;
433
+ const invitationRecord = await (0, pg_client_1.queryOne)(db, invitationSql, [token]);
434
+ if (!invitationRecord) {
435
+ return null;
436
+ }
437
+ const userSql = `SELECT id, given_name, family_name FROM public.users WHERE id = $1 LIMIT 1`;
438
+ const invitedByUserRecord = await (0, pg_client_1.queryOne)(db, userSql, [invitationRecord.invited_by_user_id]);
439
+ // Get shadow user details with LinkedIn profile picture
440
+ const shadowUserRecord = await fetchShadowUserWithProfilePicture(db, invitationRecord.shadow_user_id);
441
+ const orgSql = `SELECT id, name FROM public.organizations WHERE id = $1 LIMIT 1`;
442
+ const organizationRecord = await (0, pg_client_1.queryOne)(db, orgSql, [invitationRecord.organization_id]);
443
+ if (!invitedByUserRecord || !organizationRecord) {
444
+ throw new Error("Missing related records for invitation");
445
+ }
446
+ return {
447
+ id: invitationRecord.id,
448
+ email: invitationRecord.email,
449
+ token: invitationRecord.token,
450
+ invitationMessage: invitationRecord.invitation_message,
451
+ invitedUserPermissionsRole: invitationRecord.invited_user_permissions_role,
452
+ invitedByUserId: invitationRecord.invited_by_user_id,
453
+ shadowUserId: invitationRecord.shadow_user_id,
454
+ organizationId: invitationRecord.organization_id,
455
+ createdAt: invitationRecord.created_at,
456
+ expiresAt: invitationRecord.expires_at,
457
+ usedAt: invitationRecord.used_at,
458
+ revokedAt: invitationRecord.revoked_at,
459
+ invitedByUser: {
460
+ id: invitedByUserRecord.id,
461
+ givenName: invitedByUserRecord.given_name,
462
+ familyName: invitedByUserRecord.family_name,
463
+ },
464
+ shadowUser: shadowUserRecord,
465
+ organization: organizationRecord,
466
+ };
467
+ }
468
+ // =============================================================================
469
+ // LIST INVITATIONS
470
+ // =============================================================================
471
+ /**
472
+ * Get all pending invitations for an organization
473
+ */
474
+ async function getPendingInvitationsByOrganization(db, organizationId) {
475
+ const invitationsSql = `
476
+ SELECT
477
+ id, email, token, invitation_message, invited_user_permissions_role,
478
+ invited_by_user_id, shadow_user_id, organization_id,
479
+ created_at, expires_at, used_at, revoked_at
480
+ FROM authentication.invitations
481
+ WHERE organization_id = $1
482
+ AND used_at IS NULL
483
+ AND revoked_at IS NULL
484
+ AND expires_at > NOW()
485
+ ORDER BY created_at DESC
486
+ `;
487
+ const invitationRecords = await (0, pg_client_1.query)(db, invitationsSql, [organizationId]);
488
+ // Get all related users and organizations
489
+ const result = [];
490
+ for (const invitationRecord of invitationRecords) {
491
+ const userSql = `SELECT id, given_name, family_name FROM public.users WHERE id = $1 LIMIT 1`;
492
+ const invitedByUserRecord = await (0, pg_client_1.queryOne)(db, userSql, [invitationRecord.invited_by_user_id]);
493
+ const orgSql = `SELECT id, name FROM public.organizations WHERE id = $1 LIMIT 1`;
494
+ const organizationRecord = await (0, pg_client_1.queryOne)(db, orgSql, [invitationRecord.organization_id]);
495
+ const shadowUserRecord = await fetchShadowUserWithProfilePicture(db, invitationRecord.shadow_user_id);
496
+ if (!invitedByUserRecord || !organizationRecord)
497
+ continue;
498
+ result.push({
499
+ id: invitationRecord.id,
500
+ email: invitationRecord.email,
501
+ token: invitationRecord.token,
502
+ invitationMessage: invitationRecord.invitation_message,
503
+ invitedUserPermissionsRole: invitationRecord.invited_user_permissions_role,
504
+ invitedByUserId: invitationRecord.invited_by_user_id,
505
+ shadowUserId: invitationRecord.shadow_user_id,
506
+ organizationId: invitationRecord.organization_id,
507
+ createdAt: invitationRecord.created_at,
508
+ expiresAt: invitationRecord.expires_at,
509
+ usedAt: invitationRecord.used_at,
510
+ revokedAt: invitationRecord.revoked_at,
511
+ invitedByUser: {
512
+ id: invitedByUserRecord.id,
513
+ givenName: invitedByUserRecord.given_name,
514
+ familyName: invitedByUserRecord.family_name,
515
+ },
516
+ shadowUser: shadowUserRecord,
517
+ organization: organizationRecord,
518
+ });
519
+ }
520
+ return result;
521
+ }
522
+ /**
523
+ * Get all invitations (pending, used, expired, revoked) for an organization
524
+ */
525
+ async function getAllInvitationsByOrganization(db, organizationId) {
526
+ const invitationsSql = `
527
+ SELECT
528
+ id, email, token, invitation_message, invited_user_permissions_role,
529
+ invited_by_user_id, shadow_user_id, organization_id,
530
+ created_at, expires_at, used_at, revoked_at
531
+ FROM authentication.invitations
532
+ WHERE organization_id = $1
533
+ ORDER BY created_at DESC
534
+ `;
535
+ const invitationRecords = await (0, pg_client_1.query)(db, invitationsSql, [organizationId]);
536
+ // Get all related users and organizations
537
+ const result = [];
538
+ for (const invitationRecord of invitationRecords) {
539
+ const userSql = `SELECT id, given_name, family_name FROM public.users WHERE id = $1 LIMIT 1`;
540
+ const invitedByUserRecord = await (0, pg_client_1.queryOne)(db, userSql, [invitationRecord.invited_by_user_id]);
541
+ const orgSql = `SELECT id, name FROM public.organizations WHERE id = $1 LIMIT 1`;
542
+ const organizationRecord = await (0, pg_client_1.queryOne)(db, orgSql, [invitationRecord.organization_id]);
543
+ const shadowUserRecord = await fetchShadowUserWithProfilePicture(db, invitationRecord.shadow_user_id);
544
+ if (!invitedByUserRecord || !organizationRecord)
545
+ continue;
546
+ result.push({
547
+ id: invitationRecord.id,
548
+ email: invitationRecord.email,
549
+ token: invitationRecord.token,
550
+ invitationMessage: invitationRecord.invitation_message,
551
+ invitedUserPermissionsRole: invitationRecord.invited_user_permissions_role,
552
+ invitedByUserId: invitationRecord.invited_by_user_id,
553
+ shadowUserId: invitationRecord.shadow_user_id,
554
+ organizationId: invitationRecord.organization_id,
555
+ createdAt: invitationRecord.created_at,
556
+ expiresAt: invitationRecord.expires_at,
557
+ usedAt: invitationRecord.used_at,
558
+ revokedAt: invitationRecord.revoked_at,
559
+ invitedByUser: {
560
+ id: invitedByUserRecord.id,
561
+ givenName: invitedByUserRecord.given_name,
562
+ familyName: invitedByUserRecord.family_name,
563
+ },
564
+ shadowUser: shadowUserRecord,
565
+ organization: organizationRecord,
566
+ });
567
+ }
568
+ return result;
569
+ }
570
+ /**
571
+ * Check if a pending invitation exists for an email in an organization
572
+ */
573
+ async function hasPendingInvitation(db, email, organizationId) {
574
+ const sql = `
575
+ SELECT id FROM authentication.invitations
576
+ WHERE email = $1
577
+ AND organization_id = $2
578
+ AND used_at IS NULL
579
+ AND revoked_at IS NULL
580
+ AND expires_at > NOW()
581
+ LIMIT 1
582
+ `;
583
+ const invitation = await (0, pg_client_1.queryOne)(db, sql, [email.toLowerCase().trim(), organizationId]);
584
+ return !!invitation;
585
+ }
586
+ /**
587
+ * Check if a shadow user has an active invitation for an organization
588
+ */
589
+ async function hasActiveInvitationForShadowUser(db, shadowUserId, organizationId) {
590
+ const sql = `
591
+ SELECT id FROM authentication.invitations
592
+ WHERE shadow_user_id = $1
593
+ AND organization_id = $2
594
+ AND used_at IS NULL
595
+ AND revoked_at IS NULL
596
+ AND expires_at > NOW()
597
+ LIMIT 1
598
+ `;
599
+ const invitation = await (0, pg_client_1.queryOne)(db, sql, [shadowUserId, organizationId]);
600
+ return !!invitation;
601
+ }
602
+ /**
603
+ * Get all invitations sent by a specific user
604
+ */
605
+ async function getInvitationsSentByUser(db, invitedByUserId) {
606
+ const invitationsSql = `
607
+ SELECT
608
+ id, email, token, invitation_message, invited_user_permissions_role,
609
+ invited_by_user_id, shadow_user_id, organization_id,
610
+ created_at, expires_at, used_at, revoked_at
611
+ FROM authentication.invitations
612
+ WHERE invited_by_user_id = $1
613
+ ORDER BY created_at DESC
614
+ `;
615
+ const invitationRecords = await (0, pg_client_1.query)(db, invitationsSql, [invitedByUserId]);
616
+ // Get all related users and organizations
617
+ const result = [];
618
+ for (const invitationRecord of invitationRecords) {
619
+ const userSql = `SELECT id, given_name, family_name FROM public.users WHERE id = $1 LIMIT 1`;
620
+ const invitedByUserRecord = await (0, pg_client_1.queryOne)(db, userSql, [invitationRecord.invited_by_user_id]);
621
+ // Get shadow user details with LinkedIn profile picture
622
+ const shadowUserRecord = await fetchShadowUserWithProfilePicture(db, invitationRecord.shadow_user_id);
623
+ const orgSql = `SELECT id, name FROM public.organizations WHERE id = $1 LIMIT 1`;
624
+ const organizationRecord = await (0, pg_client_1.queryOne)(db, orgSql, [invitationRecord.organization_id]);
625
+ if (!invitedByUserRecord || !organizationRecord)
626
+ continue;
627
+ result.push({
628
+ id: invitationRecord.id,
629
+ email: invitationRecord.email,
630
+ token: invitationRecord.token,
631
+ invitationMessage: invitationRecord.invitation_message,
632
+ invitedUserPermissionsRole: invitationRecord.invited_user_permissions_role,
633
+ invitedByUserId: invitationRecord.invited_by_user_id,
634
+ shadowUserId: invitationRecord.shadow_user_id,
635
+ organizationId: invitationRecord.organization_id,
636
+ createdAt: invitationRecord.created_at,
637
+ expiresAt: invitationRecord.expires_at,
638
+ usedAt: invitationRecord.used_at,
639
+ revokedAt: invitationRecord.revoked_at,
640
+ invitedByUser: {
641
+ id: invitedByUserRecord.id,
642
+ givenName: invitedByUserRecord.given_name,
643
+ familyName: invitedByUserRecord.family_name,
644
+ },
645
+ shadowUser: shadowUserRecord,
646
+ organization: organizationRecord,
647
+ });
648
+ }
649
+ return result;
650
+ }
651
+ /**
652
+ * Get all pending invitations sent by a specific user
653
+ */
654
+ async function getPendingInvitationsSentByUser(db, invitedByUserId) {
655
+ const invitationsSql = `
656
+ SELECT
657
+ id, email, token, invitation_message, invited_user_permissions_role,
658
+ invited_by_user_id, shadow_user_id, organization_id,
659
+ created_at, expires_at, used_at, revoked_at
660
+ FROM authentication.invitations
661
+ WHERE invited_by_user_id = $1
662
+ AND used_at IS NULL
663
+ AND revoked_at IS NULL
664
+ AND expires_at > NOW()
665
+ ORDER BY created_at DESC
666
+ `;
667
+ const invitationRecords = await (0, pg_client_1.query)(db, invitationsSql, [invitedByUserId]);
668
+ // Get all related users and organizations
669
+ const result = [];
670
+ for (const invitationRecord of invitationRecords) {
671
+ const userSql = `SELECT id, given_name, family_name FROM public.users WHERE id = $1 LIMIT 1`;
672
+ const invitedByUserRecord = await (0, pg_client_1.queryOne)(db, userSql, [invitationRecord.invited_by_user_id]);
673
+ const orgSql = `SELECT id, name FROM public.organizations WHERE id = $1 LIMIT 1`;
674
+ const organizationRecord = await (0, pg_client_1.queryOne)(db, orgSql, [invitationRecord.organization_id]);
675
+ const shadowUserRecord = await fetchShadowUserWithProfilePicture(db, invitationRecord.shadow_user_id);
676
+ if (!invitedByUserRecord || !organizationRecord)
677
+ continue;
678
+ result.push({
679
+ id: invitationRecord.id,
680
+ email: invitationRecord.email,
681
+ token: invitationRecord.token,
682
+ invitationMessage: invitationRecord.invitation_message,
683
+ invitedUserPermissionsRole: invitationRecord.invited_user_permissions_role,
684
+ invitedByUserId: invitationRecord.invited_by_user_id,
685
+ shadowUserId: invitationRecord.shadow_user_id,
686
+ organizationId: invitationRecord.organization_id,
687
+ createdAt: invitationRecord.created_at,
688
+ expiresAt: invitationRecord.expires_at,
689
+ usedAt: invitationRecord.used_at,
690
+ revokedAt: invitationRecord.revoked_at,
691
+ invitedByUser: {
692
+ id: invitedByUserRecord.id,
693
+ givenName: invitedByUserRecord.given_name,
694
+ familyName: invitedByUserRecord.family_name,
695
+ },
696
+ shadowUser: shadowUserRecord,
697
+ organization: organizationRecord,
698
+ });
699
+ }
700
+ return result;
701
+ }
702
+ // =============================================================================
703
+ // CLEANUP
704
+ // =============================================================================
705
+ /**
706
+ * Delete expired invitations older than specified days
707
+ */
708
+ async function cleanupExpiredInvitations(db, olderThanDays = 30) {
709
+ const sql = `
710
+ DELETE FROM authentication.invitations
711
+ WHERE (NOW() - expires_at) > INTERVAL '${olderThanDays} days'
712
+ AND used_at IS NULL
713
+ RETURNING id
714
+ `;
715
+ const result = await (0, pg_client_1.query)(db, sql, []);
716
+ return result.length;
717
+ }
718
+ // =============================================================================
719
+ // HELPER FUNCTIONS
720
+ // =============================================================================
721
+ /**
722
+ * Ensure contact info exists for invitation email
723
+ */
724
+ async function ensureContactInfoForInvitation(db, email, shadowUserId) {
725
+ // Try to find existing contact info for this email with INVITATION source
726
+ const existingSql = `
727
+ SELECT id FROM public.contact_infos
728
+ WHERE value = $1
729
+ AND type = 'EMAIL'
730
+ AND source = 'INVITATION'
731
+ LIMIT 1
732
+ `;
733
+ const existingContactInfo = await (0, pg_client_1.queryOne)(db, existingSql, [email]);
734
+ if (existingContactInfo) {
735
+ return existingContactInfo.id;
736
+ }
737
+ // Create new contact info record
738
+ const insertSql = `
739
+ INSERT INTO public.contact_infos (user_id, value, type, source, created_at)
740
+ VALUES ($1, $2, 'EMAIL', 'INVITATION', NOW())
741
+ RETURNING id
742
+ `;
743
+ const contactInfoResult = await (0, pg_client_1.queryOne)(db, insertSql, [shadowUserId, email]);
744
+ if (!contactInfoResult?.id) {
745
+ throw new Error("CONTACT_INFO_NOT_FOUND_FOR_EMAIL");
746
+ }
747
+ return contactInfoResult.id;
748
+ }
749
+ //# sourceMappingURL=invitation-operations.js.map