eggi-ai-db-schema-2 0.1.1

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 +660 -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 +714 -0
  12. package/dist/lib/database-service.d.ts.map +1 -0
  13. package/dist/lib/database-service.js +1394 -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 +160 -0
  89. package/dist/utils/introduction-request-operations.d.ts.map +1 -0
  90. package/dist/utils/introduction-request-operations.js +492 -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 +267 -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 +284 -0
  145. package/dist/utils/organization-operations.d.ts.map +1 -0
  146. package/dist/utils/organization-operations.js +1030 -0
  147. package/dist/utils/organization-operations.js.map +1 -0
  148. package/dist/utils/organization-relationship-operations.d.ts +79 -0
  149. package/dist/utils/organization-relationship-operations.d.ts.map +1 -0
  150. package/dist/utils/organization-relationship-operations.js +294 -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 +163 -0
  165. package/dist/utils/sales-pipeline-operations.d.ts.map +1 -0
  166. package/dist/utils/sales-pipeline-operations.js +725 -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,1030 @@
1
+ "use strict";
2
+ /**
3
+ * Organization Operations
4
+ *
5
+ * Database operations for organizations and organization memberships.
6
+ * Provides functions for querying organization data and user memberships.
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.getOrganizationMembershipsForUser = getOrganizationMembershipsForUser;
10
+ exports.getOrganizationById = getOrganizationById;
11
+ exports.findOrganizationByLinkedInCompanyId = findOrganizationByLinkedInCompanyId;
12
+ exports.findOrganizationByLinkedInPublicIdentifier = findOrganizationByLinkedInPublicIdentifier;
13
+ exports.findOrganizationByDedupKey = findOrganizationByDedupKey;
14
+ exports.getOrganizationMembers = getOrganizationMembers;
15
+ exports.getOrganizationMembersCount = getOrganizationMembersCount;
16
+ exports.getOrganizationContributorsCount = getOrganizationContributorsCount;
17
+ exports.getOrganizationContributors = getOrganizationContributors;
18
+ exports.getOrganizationMembershipForUser = getOrganizationMembershipForUser;
19
+ exports.getOrganizationContributorByUserId = getOrganizationContributorByUserId;
20
+ exports.findContributorPage = findContributorPage;
21
+ exports.updateOrganizationMemberRole = updateOrganizationMemberRole;
22
+ exports.getOrganizationMemberRelationships = getOrganizationMemberRelationships;
23
+ exports.getOrganizationMemberRelationshipsCount = getOrganizationMemberRelationshipsCount;
24
+ exports.createOrganization = createOrganization;
25
+ exports.updateOrganizationLinkedInPublicIdentifier = updateOrganizationLinkedInPublicIdentifier;
26
+ exports.findOrCreateOrganizationMember = findOrCreateOrganizationMember;
27
+ exports.findPersonalWorkspaceForUser = findPersonalWorkspaceForUser;
28
+ exports.getOrganizationMemberById = getOrganizationMemberById;
29
+ exports.updateOrganizationSubscriptionTier = updateOrganizationSubscriptionTier;
30
+ const pg_client_1 = require("../lib/pg-client");
31
+ const db_1 = require("../lib/db");
32
+ /**
33
+ * Get all organization memberships for a user with full organization details
34
+ *
35
+ * @param db - Database instance
36
+ * @param userId - User ID to get memberships for
37
+ * @returns Array of organization memberships with organization details
38
+ */
39
+ async function getOrganizationMembershipsForUser(db, userId) {
40
+ const sql = `
41
+ SELECT
42
+ om.id,
43
+ om.user_id,
44
+ om.organization_id,
45
+ om.contribution_role,
46
+ om.permissions_role,
47
+ om.created_at,
48
+ o.id as org_id,
49
+ o.name as org_name,
50
+ o.workspace_type as org_workspace_type,
51
+ o.subscription_tier as org_subscription_tier,
52
+ o.image_url as org_image_url,
53
+ o.created_at as org_created_at,
54
+ o.created_by_user_id as org_created_by_user_id
55
+ FROM public.organization_members om
56
+ INNER JOIN public.organizations o ON om.organization_id = o.id
57
+ WHERE om.user_id = $1
58
+ ORDER BY om.created_at
59
+ `;
60
+ const results = await (0, pg_client_1.query)(db, sql, [userId]);
61
+ return results.map(r => ({
62
+ id: r.id,
63
+ userId: r.user_id,
64
+ organizationId: r.organization_id,
65
+ contributionRole: r.contribution_role,
66
+ permissionsRole: r.permissions_role,
67
+ createdAt: r.created_at,
68
+ organization: {
69
+ id: r.org_id,
70
+ name: r.org_name,
71
+ workspaceType: r.org_workspace_type,
72
+ subscriptionTier: r.org_subscription_tier,
73
+ imageUrl: r.org_image_url,
74
+ createdAt: r.org_created_at,
75
+ createdByUserId: r.org_created_by_user_id,
76
+ },
77
+ }));
78
+ }
79
+ /**
80
+ * Get organization by ID
81
+ *
82
+ * @param db - Database instance
83
+ * @param organizationId - Organization ID
84
+ * @returns Organization or null if not found
85
+ */
86
+ async function getOrganizationById(db, organizationId) {
87
+ const sql = `
88
+ SELECT id, name, workspace_type, subscription_tier, image_url, created_at, created_by_user_id
89
+ FROM public.organizations
90
+ WHERE id = $1
91
+ LIMIT 1
92
+ `;
93
+ const result = await (0, pg_client_1.queryOne)(db, sql, [organizationId]);
94
+ if (!result)
95
+ return null;
96
+ return {
97
+ id: result.id,
98
+ name: result.name,
99
+ workspaceType: result.workspace_type,
100
+ subscriptionTier: result.subscription_tier,
101
+ imageUrl: result.image_url,
102
+ createdAt: result.created_at,
103
+ createdByUserId: result.created_by_user_id,
104
+ };
105
+ }
106
+ /**
107
+ * Find organization by LinkedIn company ID (Forager organization ID)
108
+ *
109
+ * @param db - Database instance
110
+ * @param linkedinCompanyId - Forager organization ID
111
+ * @returns Organization or null if not found
112
+ */
113
+ async function findOrganizationByLinkedInCompanyId(db, linkedinCompanyId) {
114
+ const sql = `
115
+ SELECT id, name, workspace_type, subscription_tier, image_url, created_at, created_by_user_id
116
+ FROM public.organizations
117
+ WHERE linkedin_company_id = $1
118
+ LIMIT 1
119
+ `;
120
+ const result = await (0, pg_client_1.queryOne)(db, sql, [linkedinCompanyId]);
121
+ if (!result)
122
+ return null;
123
+ return {
124
+ id: result.id,
125
+ name: result.name,
126
+ workspaceType: result.workspace_type,
127
+ subscriptionTier: result.subscription_tier,
128
+ imageUrl: result.image_url,
129
+ createdAt: result.created_at,
130
+ createdByUserId: result.created_by_user_id,
131
+ };
132
+ }
133
+ /**
134
+ * Find organization by LinkedIn public identifier
135
+ *
136
+ * @param db - Database instance
137
+ * @param linkedinPublicIdentifier - LinkedIn public identifier (e.g., "eggi")
138
+ * @returns Organization or null if not found
139
+ */
140
+ async function findOrganizationByLinkedInPublicIdentifier(db, linkedinPublicIdentifier) {
141
+ (0, db_1.debugLogDbOperation)("select", "organizations", { linkedinPublicIdentifier });
142
+ const sql = `
143
+ SELECT
144
+ id,
145
+ name,
146
+ workspace_type,
147
+ subscription_tier,
148
+ image_url,
149
+ created_at,
150
+ created_by_user_id,
151
+ linkedin_public_identifier,
152
+ linkedin_company_id
153
+ FROM public.organizations
154
+ WHERE linkedin_public_identifier = $1
155
+ LIMIT 1
156
+ `;
157
+ const result = await (0, pg_client_1.queryOne)(db, sql, [linkedinPublicIdentifier]);
158
+ if (!result)
159
+ return null;
160
+ return {
161
+ id: result.id,
162
+ name: result.name,
163
+ workspaceType: result.workspace_type,
164
+ subscriptionTier: result.subscription_tier,
165
+ imageUrl: result.image_url,
166
+ createdAt: result.created_at,
167
+ createdByUserId: result.created_by_user_id,
168
+ linkedinPublicIdentifier: result.linkedin_public_identifier,
169
+ linkedinCompanyId: result.linkedin_company_id,
170
+ };
171
+ }
172
+ /**
173
+ * Find organization by dedup_key (for manual organizations)
174
+ *
175
+ * @param db - Database instance
176
+ * @param dedupKey - Deduplication key
177
+ * @returns Organization or null if not found
178
+ */
179
+ async function findOrganizationByDedupKey(db, dedupKey) {
180
+ const sql = `
181
+ SELECT id, name, workspace_type, subscription_tier, image_url, created_at, created_by_user_id
182
+ FROM public.organizations
183
+ WHERE dedup_key = $1
184
+ LIMIT 1
185
+ `;
186
+ const result = await (0, pg_client_1.queryOne)(db, sql, [dedupKey]);
187
+ if (!result)
188
+ return null;
189
+ return {
190
+ id: result.id,
191
+ name: result.name,
192
+ workspaceType: result.workspace_type,
193
+ subscriptionTier: result.subscription_tier,
194
+ imageUrl: result.image_url,
195
+ createdAt: result.created_at,
196
+ createdByUserId: result.created_by_user_id,
197
+ };
198
+ }
199
+ /**
200
+ * Get all members of an organization
201
+ *
202
+ * @param db - Database instance
203
+ * @param organizationId - Organization ID
204
+ * @returns Array of organization memberships
205
+ */
206
+ async function getOrganizationMembers(db, organizationId) {
207
+ const sql = `
208
+ SELECT id, user_id, organization_id, contribution_role, permissions_role, created_at
209
+ FROM public.organization_members
210
+ WHERE organization_id = $1
211
+ ORDER BY created_at
212
+ `;
213
+ const results = await (0, pg_client_1.query)(db, sql, [organizationId]);
214
+ return results.map(r => ({
215
+ id: r.id,
216
+ userId: r.user_id,
217
+ organizationId: r.organization_id,
218
+ contributionRole: r.contribution_role,
219
+ permissionsRole: r.permissions_role,
220
+ createdAt: r.created_at,
221
+ }));
222
+ }
223
+ /**
224
+ * Get total count of organization members by contribution role
225
+ *
226
+ * @param db - Database instance
227
+ * @param organizationId - Organization ID
228
+ * @param contributionRole - Contribution role to filter by (optional)
229
+ * @returns Total count of members
230
+ */
231
+ async function getOrganizationMembersCount(db, organizationId, contributionRole) {
232
+ const sql = contributionRole
233
+ ? `
234
+ SELECT COUNT(*) as count
235
+ FROM public.organization_members
236
+ WHERE organization_id = $1 AND contribution_role = $2
237
+ `
238
+ : `
239
+ SELECT COUNT(*) as count
240
+ FROM public.organization_members
241
+ WHERE organization_id = $1
242
+ `;
243
+ const params = contributionRole ? [organizationId, contributionRole] : [organizationId];
244
+ const results = await (0, pg_client_1.query)(db, sql, params);
245
+ return parseInt(results[0]?.count || "0", 10);
246
+ }
247
+ /**
248
+ * Get total count of contributors for an organization
249
+ *
250
+ * CRITICAL: This must use the same filtering logic as getOrganizationContributors
251
+ * to ensure accurate pagination. Only counts contributors with relationships_count > 0.
252
+ *
253
+ * @param db - Database instance
254
+ * @param organizationId - Organization ID
255
+ * @returns Total count of contributors with relationships
256
+ */
257
+ async function getOrganizationContributorsCount(db, organizationId) {
258
+ const sql = `
259
+ WITH relationship_counts AS (
260
+ -- Count distinct relationships for each contributor using organization_member_id
261
+ -- This ensures we only count relationships assigned to the specific contributor,
262
+ -- not relationships where they appear as the "other person" in someone else's mapping job
263
+ SELECT
264
+ om.user_id,
265
+ COUNT(DISTINCT
266
+ CASE
267
+ WHEN r.linkedin_account_id_a = la.id THEN r.linkedin_account_id_b
268
+ ELSE r.linkedin_account_id_a
269
+ END
270
+ ) as relationships_count
271
+ FROM public.organization_members om
272
+ INNER JOIN linkedin.accounts la ON la.user_id = om.user_id
273
+ INNER JOIN public.organization_user_relationships_assignments oura
274
+ ON oura.organization_member_id = om.id -- Use organization_member_id for faster lookup
275
+ INNER JOIN linkedin.relationships r
276
+ ON r.id = oura.linkedin_relationship_id
277
+ AND (r.linkedin_account_id_a = la.id OR r.linkedin_account_id_b = la.id)
278
+ INNER JOIN monitoring.mapping_jobs mj
279
+ ON mj.id = r.mapping_job_id
280
+ AND mj.linkedin_account_id = la.id -- CRITICAL: Only count relationships from mapping jobs that belong to this contributor
281
+ WHERE oura.organization_id = $1
282
+ AND om.organization_id = $1
283
+ AND om.contribution_role = 'CONTRIBUTOR'
284
+ GROUP BY om.user_id
285
+ )
286
+ SELECT COUNT(*) as count
287
+ FROM relationship_counts rc
288
+ WHERE rc.relationships_count > 0
289
+ `;
290
+ const results = await (0, pg_client_1.query)(db, sql, [organizationId]);
291
+ return parseInt(results[0]?.count || "0", 10);
292
+ }
293
+ /**
294
+ * Get contributors for an organization with pagination
295
+ * Sorted by:
296
+ * 1. LLM inference job status (descending) - completed jobs first, then pending/in-progress
297
+ * 2. Relationship count (descending) - most relationships first within each status group
298
+ * 3. Created date (ascending) - oldest first when status and relationships are equal
299
+ *
300
+ * @param db - Database instance
301
+ * @param organizationId - Organization ID
302
+ * @param limit - Maximum number of contributors to return (default: 50)
303
+ * @param offset - Number of contributors to skip (default: 0)
304
+ * @returns Array of contributor memberships
305
+ */
306
+ async function getOrganizationContributors(db, organizationId, limit = 50, offset = 0) {
307
+ const sql = `
308
+ WITH relationship_counts AS (
309
+ -- Count distinct relationships for each contributor using organization_member_id
310
+ -- This ensures we only count relationships assigned to the specific contributor,
311
+ -- not relationships where they appear as the "other person" in someone else's mapping job
312
+ SELECT
313
+ om.user_id,
314
+ COUNT(DISTINCT
315
+ CASE
316
+ WHEN r.linkedin_account_id_a = la.id THEN r.linkedin_account_id_b
317
+ ELSE r.linkedin_account_id_a
318
+ END
319
+ ) as relationships_count
320
+ FROM public.organization_members om
321
+ INNER JOIN linkedin.accounts la ON la.user_id = om.user_id
322
+ INNER JOIN public.organization_user_relationships_assignments oura
323
+ ON oura.organization_member_id = om.id -- Use organization_member_id for faster lookup
324
+ INNER JOIN linkedin.relationships r
325
+ ON r.id = oura.linkedin_relationship_id
326
+ AND (r.linkedin_account_id_a = la.id OR r.linkedin_account_id_b = la.id)
327
+ INNER JOIN monitoring.mapping_jobs mj
328
+ ON mj.id = r.mapping_job_id
329
+ AND mj.linkedin_account_id = la.id -- CRITICAL: Only count relationships from mapping jobs that belong to this contributor
330
+ WHERE oura.organization_id = $1
331
+ AND om.organization_id = $1
332
+ AND om.contribution_role = 'CONTRIBUTOR'
333
+ GROUP BY om.user_id
334
+ ),
335
+ llm_job_status AS (
336
+ -- Get LLM inference job completion status for each contributor
337
+ -- Only includes users who have organization_assignment_jobs
338
+ -- Users without assignment jobs will have NULL (treated as false)
339
+ SELECT DISTINCT ON (om.user_id)
340
+ om.user_id,
341
+ CASE
342
+ WHEN lij.completed_at IS NOT NULL THEN true
343
+ ELSE false
344
+ END as llm_job_completed
345
+ FROM public.organization_members om
346
+ INNER JOIN monitoring.organization_assignment_jobs oaj
347
+ ON oaj.organization_id = om.organization_id
348
+ INNER JOIN monitoring.mapping_jobs mj
349
+ ON mj.id = oaj.mapping_job_id
350
+ INNER JOIN linkedin.accounts la
351
+ ON la.id = mj.linkedin_account_id
352
+ LEFT JOIN monitoring.llm_inference_jobs lij
353
+ ON lij.mapping_job_id = oaj.mapping_job_id
354
+ WHERE om.organization_id = $1
355
+ AND om.contribution_role = 'CONTRIBUTOR'
356
+ AND la.user_id = om.user_id
357
+ ORDER BY om.user_id, oaj.created_at DESC, lij.created_at DESC NULLS LAST
358
+ )
359
+ SELECT
360
+ om.id,
361
+ om.user_id,
362
+ om.organization_id,
363
+ om.contribution_role,
364
+ om.permissions_role,
365
+ om.created_at,
366
+ COALESCE(rc.relationships_count, 0) as relationships_count,
367
+ COALESCE(ljs.llm_job_completed, false) as llm_job_completed
368
+ FROM public.organization_members om
369
+ INNER JOIN relationship_counts rc ON rc.user_id = om.user_id
370
+ LEFT JOIN llm_job_status ljs ON ljs.user_id = om.user_id
371
+ WHERE om.organization_id = $1
372
+ AND om.contribution_role = 'CONTRIBUTOR'
373
+ AND rc.relationships_count > 0
374
+ ORDER BY
375
+ -- Primary sort: LLM job completed status (descending) - completed jobs first, then pending/in-progress
376
+ -- true (completed) comes before false (pending/not started)
377
+ COALESCE(ljs.llm_job_completed, false) DESC,
378
+ -- Secondary sort: relationship count (descending) - most relationships first within each status group
379
+ COALESCE(rc.relationships_count, 0) DESC,
380
+ -- Tertiary sort: created_at (ascending) - oldest first when status and relationship count are equal
381
+ om.created_at ASC
382
+ LIMIT $2 OFFSET $3
383
+ `;
384
+ const results = await (0, pg_client_1.query)(db, sql, [organizationId, limit, offset]);
385
+ return results.map(r => ({
386
+ id: r.id,
387
+ userId: r.user_id,
388
+ organizationId: r.organization_id,
389
+ contributionRole: r.contribution_role,
390
+ permissionsRole: r.permissions_role,
391
+ createdAt: r.created_at,
392
+ }));
393
+ }
394
+ /**
395
+ * Get a specific organization membership for a user
396
+ *
397
+ * @param db - Database instance
398
+ * @param userId - User ID
399
+ * @param organizationId - Organization ID
400
+ * @returns Organization membership or null if not found
401
+ */
402
+ async function getOrganizationMembershipForUser(db, userId, organizationId) {
403
+ const sql = `
404
+ SELECT id, user_id, organization_id, contribution_role, permissions_role, created_at
405
+ FROM public.organization_members
406
+ WHERE user_id = $1 AND organization_id = $2
407
+ LIMIT 1
408
+ `;
409
+ const result = await (0, pg_client_1.queryOne)(db, sql, [userId, organizationId]);
410
+ if (!result)
411
+ return null;
412
+ return {
413
+ id: result.id,
414
+ userId: result.user_id,
415
+ organizationId: result.organization_id,
416
+ contributionRole: result.contribution_role,
417
+ permissionsRole: result.permissions_role,
418
+ createdAt: result.created_at,
419
+ };
420
+ }
421
+ /**
422
+ * Get a specific contributor by user_id with full details
423
+ * Returns the same data structure as getOrganizationContributors but for a single user
424
+ * This is useful for direct lookups without pagination
425
+ *
426
+ * @param db - Database instance
427
+ * @param organizationId - Organization ID
428
+ * @param userId - User ID to fetch
429
+ * @returns Contributor data or null if not found or not a contributor with relationships
430
+ */
431
+ async function getOrganizationContributorByUserId(db, organizationId, userId) {
432
+ // Use the same query logic as getOrganizationContributors but filter by user_id
433
+ // Updated to use organization_member_id for faster, more accurate lookups
434
+ const sql = `
435
+ WITH relationship_counts AS (
436
+ -- Count distinct relationships for each contributor using organization_member_id
437
+ -- This ensures we only count relationships assigned to the specific contributor,
438
+ -- not relationships where they appear as the "other person" in someone else's mapping job
439
+ SELECT
440
+ om.user_id,
441
+ COUNT(DISTINCT
442
+ CASE
443
+ WHEN r.linkedin_account_id_a = la.id THEN r.linkedin_account_id_b
444
+ ELSE r.linkedin_account_id_a
445
+ END
446
+ ) as relationships_count
447
+ FROM public.organization_members om
448
+ INNER JOIN linkedin.accounts la ON la.user_id = om.user_id
449
+ INNER JOIN public.organization_user_relationships_assignments oura
450
+ ON oura.organization_member_id = om.id -- Use organization_member_id for faster lookup
451
+ INNER JOIN linkedin.relationships r
452
+ ON r.id = oura.linkedin_relationship_id
453
+ AND (r.linkedin_account_id_a = la.id OR r.linkedin_account_id_b = la.id)
454
+ INNER JOIN monitoring.mapping_jobs mj
455
+ ON mj.id = r.mapping_job_id
456
+ AND mj.linkedin_account_id = la.id -- CRITICAL: Only count relationships from mapping jobs that belong to this contributor
457
+ WHERE oura.organization_id = $1
458
+ AND om.organization_id = $1
459
+ AND om.contribution_role = 'CONTRIBUTOR'
460
+ GROUP BY om.user_id
461
+ ),
462
+ llm_job_status AS (
463
+ -- Get LLM inference job completion status for each contributor
464
+ SELECT DISTINCT ON (om.user_id)
465
+ om.user_id,
466
+ CASE
467
+ WHEN lij.completed_at IS NOT NULL THEN true
468
+ ELSE false
469
+ END as llm_job_completed
470
+ FROM public.organization_members om
471
+ INNER JOIN monitoring.organization_assignment_jobs oaj
472
+ ON oaj.organization_id = om.organization_id
473
+ INNER JOIN monitoring.mapping_jobs mj
474
+ ON mj.id = oaj.mapping_job_id
475
+ INNER JOIN linkedin.accounts la
476
+ ON la.id = mj.linkedin_account_id
477
+ LEFT JOIN monitoring.llm_inference_jobs lij
478
+ ON lij.mapping_job_id = oaj.mapping_job_id
479
+ WHERE om.organization_id = $1
480
+ AND om.contribution_role = 'CONTRIBUTOR'
481
+ AND la.user_id = om.user_id
482
+ ORDER BY om.user_id, oaj.created_at DESC, lij.created_at DESC NULLS LAST
483
+ )
484
+ SELECT
485
+ om.id,
486
+ om.user_id,
487
+ om.organization_id,
488
+ om.contribution_role,
489
+ om.permissions_role,
490
+ om.created_at,
491
+ COALESCE(rc.relationships_count, 0) as relationships_count,
492
+ COALESCE(ljs.llm_job_completed, false) as llm_job_completed
493
+ FROM public.organization_members om
494
+ INNER JOIN relationship_counts rc ON rc.user_id = om.user_id
495
+ LEFT JOIN llm_job_status ljs ON ljs.user_id = om.user_id
496
+ WHERE om.organization_id = $1
497
+ AND om.contribution_role = 'CONTRIBUTOR'
498
+ AND om.user_id = $2
499
+ AND rc.relationships_count > 0
500
+ LIMIT 1
501
+ `;
502
+ const result = await (0, pg_client_1.queryOne)(db, sql, [organizationId, userId]);
503
+ if (!result)
504
+ return null;
505
+ return {
506
+ id: result.id,
507
+ userId: result.user_id,
508
+ organizationId: result.organization_id,
509
+ contributionRole: result.contribution_role,
510
+ permissionsRole: result.permissions_role,
511
+ createdAt: result.created_at,
512
+ };
513
+ }
514
+ /**
515
+ * Find which page a contributor appears on in the paginated contributors list
516
+ * Uses the same sorting logic as getOrganizationContributors
517
+ *
518
+ * @param db - Database instance
519
+ * @param organizationId - Organization ID
520
+ * @param userId - User ID to find
521
+ * @param limit - Page size (default: 50)
522
+ * @returns Page number (1-indexed) or null if user is not a contributor with relationships
523
+ */
524
+ async function findContributorPage(db, organizationId, userId, limit = 50) {
525
+ const sql = `
526
+ WITH relationship_counts AS (
527
+ -- Count distinct relationships for each contributor's LinkedIn account
528
+ SELECT
529
+ la.user_id,
530
+ COUNT(DISTINCT
531
+ CASE
532
+ WHEN r.linkedin_account_id_a = la.id THEN r.linkedin_account_id_b
533
+ ELSE r.linkedin_account_id_a
534
+ END
535
+ ) as relationships_count
536
+ FROM linkedin.accounts la
537
+ INNER JOIN linkedin.relationships r
538
+ ON (r.linkedin_account_id_a = la.id OR r.linkedin_account_id_b = la.id)
539
+ INNER JOIN public.organization_user_relationships_assignments oura
540
+ ON oura.linkedin_relationship_id = r.id
541
+ WHERE oura.organization_id = $1
542
+ GROUP BY la.user_id
543
+ ),
544
+ llm_job_status AS (
545
+ -- Get LLM inference job completion status for each contributor
546
+ SELECT DISTINCT ON (om.user_id)
547
+ om.user_id,
548
+ CASE
549
+ WHEN lij.completed_at IS NOT NULL THEN true
550
+ ELSE false
551
+ END as llm_job_completed
552
+ FROM public.organization_members om
553
+ INNER JOIN monitoring.organization_assignment_jobs oaj
554
+ ON oaj.organization_id = om.organization_id
555
+ INNER JOIN monitoring.mapping_jobs mj
556
+ ON mj.id = oaj.mapping_job_id
557
+ INNER JOIN linkedin.accounts la
558
+ ON la.id = mj.linkedin_account_id
559
+ LEFT JOIN monitoring.llm_inference_jobs lij
560
+ ON lij.mapping_job_id = oaj.mapping_job_id
561
+ WHERE om.organization_id = $1
562
+ AND om.contribution_role = 'CONTRIBUTOR'
563
+ AND la.user_id = om.user_id
564
+ ORDER BY om.user_id, oaj.created_at DESC, lij.created_at DESC NULLS LAST
565
+ ),
566
+ ranked_contributors AS (
567
+ SELECT
568
+ om.user_id,
569
+ ROW_NUMBER() OVER (
570
+ ORDER BY
571
+ -- Primary sort: LLM job completed status (descending)
572
+ COALESCE(ljs.llm_job_completed, false) DESC,
573
+ -- Secondary sort: relationship count (descending)
574
+ COALESCE(rc.relationships_count, 0) DESC,
575
+ -- Tertiary sort: created_at (ascending)
576
+ om.created_at ASC
577
+ ) as rank
578
+ FROM public.organization_members om
579
+ INNER JOIN relationship_counts rc ON rc.user_id = om.user_id
580
+ LEFT JOIN llm_job_status ljs ON ljs.user_id = om.user_id
581
+ WHERE om.organization_id = $1
582
+ AND om.contribution_role = 'CONTRIBUTOR'
583
+ AND rc.relationships_count > 0
584
+ )
585
+ SELECT rank
586
+ FROM ranked_contributors
587
+ WHERE user_id = $2
588
+ `;
589
+ const result = await (0, pg_client_1.queryOne)(db, sql, [
590
+ organizationId,
591
+ userId,
592
+ ]);
593
+ if (!result)
594
+ return null;
595
+ // Calculate page number (1-indexed)
596
+ const page = Math.ceil(result.rank / limit);
597
+ return page;
598
+ }
599
+ /**
600
+ * Update organization member role
601
+ *
602
+ * Updates the contribution role and/or permissions role for an existing organization member.
603
+ * This is useful for promoting VISITOR to CONTRIBUTOR or changing admin permissions.
604
+ *
605
+ * @param db - Database instance
606
+ * @param userId - User ID
607
+ * @param organizationId - Organization ID
608
+ * @param contributionRole - New contribution role (optional)
609
+ * @param permissionsRole - New permissions role (optional)
610
+ * @returns Updated organization membership or null if not found
611
+ */
612
+ async function updateOrganizationMemberRole(db, userId, organizationId, params) {
613
+ (0, db_1.debugLogDbOperation)("update", "organization_members", { userId, organizationId, ...params }, undefined, {
614
+ operation: "updateOrganizationMemberRole",
615
+ });
616
+ if (!params.contributionRole && !params.permissionsRole) {
617
+ // Nothing to update, return existing
618
+ return getOrganizationMembershipForUser(db, userId, organizationId);
619
+ }
620
+ const updates = [];
621
+ const queryParams = [userId, organizationId];
622
+ let paramIndex = 3;
623
+ if (params.contributionRole !== undefined) {
624
+ updates.push(`contribution_role = $${paramIndex}`);
625
+ queryParams.push(params.contributionRole);
626
+ paramIndex++;
627
+ }
628
+ if (params.permissionsRole !== undefined) {
629
+ updates.push(`permissions_role = $${paramIndex}`);
630
+ queryParams.push(params.permissionsRole);
631
+ paramIndex++;
632
+ }
633
+ const sql = `
634
+ UPDATE public.organization_members
635
+ SET ${updates.join(", ")}
636
+ WHERE user_id = $1 AND organization_id = $2
637
+ RETURNING id, user_id, organization_id, contribution_role, permissions_role, created_at
638
+ `;
639
+ const result = await (0, pg_client_1.queryOne)(db, sql, queryParams);
640
+ if (!result)
641
+ return null;
642
+ return {
643
+ id: result.id,
644
+ userId: result.user_id,
645
+ organizationId: result.organization_id,
646
+ contributionRole: result.contribution_role,
647
+ permissionsRole: result.permissions_role,
648
+ createdAt: result.created_at,
649
+ };
650
+ }
651
+ /**
652
+ * Get assigned relationships for an organization member
653
+ *
654
+ * Returns relationships from organization_user_relationships_assignments
655
+ * where one of the LinkedIn accounts matches the target user's LinkedIn account.
656
+ * Deduplicates by connection person, keeping only the highest-scoring relationship
657
+ * for each unique connection.
658
+ *
659
+ * @param db - Database instance
660
+ * @param organizationId - Organization ID
661
+ * @param linkedinAccountId - LinkedIn account ID of the target member
662
+ * @param limit - Maximum number of relationships to return (default 50)
663
+ * @returns Array of relationships with scores, deduplicated by connection person
664
+ */
665
+ async function getOrganizationMemberRelationships(db, organizationId, linkedinAccountId, limit = 50) {
666
+ // Updated to use organization_member_id for faster, more accurate lookups
667
+ // First get the organization_member_id for this user
668
+ const memberSql = `
669
+ SELECT om.id as organization_member_id
670
+ FROM public.organization_members om
671
+ INNER JOIN linkedin.accounts la ON la.user_id = om.user_id
672
+ WHERE om.organization_id = $1 AND la.id = $2
673
+ LIMIT 1
674
+ `;
675
+ const member = await (0, pg_client_1.queryOne)(db, memberSql, [organizationId, linkedinAccountId]);
676
+ if (!member) {
677
+ return [];
678
+ }
679
+ const sql = `
680
+ SELECT DISTINCT ON (
681
+ CASE
682
+ WHEN r.linkedin_account_id_a = $1 THEN r.linkedin_account_id_b
683
+ ELSE r.linkedin_account_id_a
684
+ END
685
+ )
686
+ r.id,
687
+ r.linkedin_account_id_a,
688
+ r.linkedin_account_id_b,
689
+ r.score,
690
+ r.model_version,
691
+ r.analysis_type,
692
+ r.mapping_job_id,
693
+ r.metadata,
694
+ r.created_at,
695
+ oura.id AS organization_user_relationship_assignment_id
696
+ FROM linkedin.relationships r
697
+ INNER JOIN public.organization_user_relationships_assignments oura
698
+ ON oura.linkedin_relationship_id = r.id
699
+ WHERE
700
+ oura.organization_member_id = $2
701
+ AND (r.linkedin_account_id_a = $1 OR r.linkedin_account_id_b = $1)
702
+ ORDER BY
703
+ CASE
704
+ WHEN r.linkedin_account_id_a = $1 THEN r.linkedin_account_id_b
705
+ ELSE r.linkedin_account_id_a
706
+ END,
707
+ r.score DESC,
708
+ r.created_at DESC
709
+ LIMIT $3
710
+ `;
711
+ const results = await (0, pg_client_1.query)(db, sql, [linkedinAccountId, member.organization_member_id, limit]);
712
+ return results.map(r => ({
713
+ id: r.id,
714
+ linkedinAccountIdA: r.linkedin_account_id_a,
715
+ linkedinAccountIdB: r.linkedin_account_id_b,
716
+ score: r.score,
717
+ modelVersion: r.model_version,
718
+ analysisType: r.analysis_type,
719
+ mappingJobId: r.mapping_job_id,
720
+ metadata: r.metadata,
721
+ createdAt: r.created_at,
722
+ organizationUserRelationshipAssignmentId: r.organization_user_relationship_assignment_id,
723
+ }));
724
+ }
725
+ /**
726
+ * Get the count of assigned relationships for an organization member.
727
+ * This counts relationships from the organization_user_relationships_assignments table,
728
+ * which represents the actual connections assigned to the organization for this member.
729
+ *
730
+ * Counts only the top 50 relationships by score (highest first) to match
731
+ * the limit enforced in getOrganizationMemberRelationships.
732
+ *
733
+ * @param db - Database instance
734
+ * @param organizationId - Organization ID
735
+ * @param linkedinAccountId - LinkedIn account ID of the target member
736
+ * @returns Count of assigned relationships for this member in this organization (capped at 50)
737
+ */
738
+ async function getOrganizationMemberRelationshipsCount(db, organizationId, linkedinAccountId) {
739
+ // Updated to use organization_member_id for faster, more accurate lookups
740
+ // First get the organization_member_id for this user
741
+ const memberSql = `
742
+ SELECT om.id as organization_member_id
743
+ FROM public.organization_members om
744
+ INNER JOIN linkedin.accounts la ON la.user_id = om.user_id
745
+ WHERE om.organization_id = $1 AND la.id = $2
746
+ LIMIT 1
747
+ `;
748
+ const member = await (0, pg_client_1.queryOne)(db, memberSql, [organizationId, linkedinAccountId]);
749
+ if (!member) {
750
+ return 0;
751
+ }
752
+ const sql = `
753
+ SELECT COUNT(*) as count
754
+ FROM (
755
+ SELECT DISTINCT ON (
756
+ CASE
757
+ WHEN r.linkedin_account_id_a = $1 THEN r.linkedin_account_id_b
758
+ ELSE r.linkedin_account_id_a
759
+ END
760
+ )
761
+ r.id
762
+ FROM linkedin.relationships r
763
+ INNER JOIN public.organization_user_relationships_assignments oura
764
+ ON oura.linkedin_relationship_id = r.id
765
+ WHERE
766
+ oura.organization_member_id = $2
767
+ AND (r.linkedin_account_id_a = $1 OR r.linkedin_account_id_b = $1)
768
+ ORDER BY
769
+ CASE
770
+ WHEN r.linkedin_account_id_a = $1 THEN r.linkedin_account_id_b
771
+ ELSE r.linkedin_account_id_a
772
+ END,
773
+ r.score DESC,
774
+ r.created_at DESC
775
+ LIMIT 50
776
+ ) AS top_relationships
777
+ `;
778
+ const result = await (0, pg_client_1.queryOne)(db, sql, [
779
+ linkedinAccountId,
780
+ member.organization_member_id,
781
+ ]);
782
+ return result ? parseInt(result.count, 10) : 0;
783
+ }
784
+ /**
785
+ * Create a new organization
786
+ *
787
+ * @param db - Database instance
788
+ * @param organizationData - Organization data to create
789
+ * @returns Created organization
790
+ */
791
+ async function createOrganization(db, organizationData) {
792
+ (0, db_1.debugLogDbOperation)("insert", "organizations", { organizationData });
793
+ // Generate deterministic dedup_key if not provided
794
+ // This ensures idempotency - same input = same dedup_key (no duplicates on retries)
795
+ // For manual orgs: dedup_key MUST be provided (used for deduplication)
796
+ // For LinkedIn orgs: Generate deterministic key from linkedin_company_id (not used for dedup, linkedin_company_id is)
797
+ // For personal workspaces: Generate deterministic key from created_by_user_id
798
+ let dedupKey;
799
+ if (organizationData.dedupKey) {
800
+ // Manual orgs: use provided dedup_key
801
+ dedupKey = organizationData.dedupKey;
802
+ }
803
+ else if (organizationData.linkedinCompanyId) {
804
+ // LinkedIn orgs: generate deterministic key from company_id
805
+ dedupKey = `linkedin-${organizationData.linkedinCompanyId}`;
806
+ }
807
+ else if (organizationData.createdByUserId) {
808
+ // Personal workspaces: generate deterministic key from user_id
809
+ dedupKey = `personal-${organizationData.createdByUserId}`;
810
+ }
811
+ else {
812
+ // This should never be reached:
813
+ // - Manual orgs: dedup_key is required
814
+ // - LinkedIn orgs: linkedin_company_id is always available (from Forager or provided)
815
+ // - Personal workspaces: created_by_user_id is always provided
816
+ throw new Error("Cannot generate dedup_key: missing required data. " +
817
+ "Manual orgs require dedup_key, LinkedIn orgs require linkedin_company_id, " +
818
+ "and personal workspaces require created_by_user_id.");
819
+ }
820
+ const sql = `
821
+ INSERT INTO public.organizations (
822
+ name,
823
+ workspace_type,
824
+ subscription_tier,
825
+ created_by_user_id,
826
+ image_url,
827
+ linkedin_public_identifier,
828
+ linkedin_company_id,
829
+ dedup_key,
830
+ created_at
831
+ )
832
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, NOW())
833
+ RETURNING id, name, workspace_type, subscription_tier, image_url, created_at, created_by_user_id
834
+ `;
835
+ const result = await (0, pg_client_1.queryOne)(db, sql, [
836
+ organizationData.name,
837
+ organizationData.workspaceType,
838
+ organizationData.subscriptionTier,
839
+ organizationData.createdByUserId ?? null,
840
+ organizationData.imageUrl || null,
841
+ organizationData.linkedinPublicIdentifier || null,
842
+ organizationData.linkedinCompanyId || null,
843
+ dedupKey,
844
+ ]);
845
+ if (!result) {
846
+ throw new Error("Failed to create organization");
847
+ }
848
+ return {
849
+ id: result.id,
850
+ name: result.name,
851
+ workspaceType: result.workspace_type,
852
+ subscriptionTier: result.subscription_tier,
853
+ imageUrl: result.image_url,
854
+ createdAt: result.created_at,
855
+ createdByUserId: result.created_by_user_id,
856
+ };
857
+ }
858
+ /**
859
+ * Update linkedin_public_identifier for an existing organization
860
+ *
861
+ * @param db - Database instance
862
+ * @param organizationId - Organization ID
863
+ * @param linkedinPublicIdentifier - LinkedIn public identifier to set
864
+ * @returns true if update was successful, false if no rows were updated
865
+ */
866
+ async function updateOrganizationLinkedInPublicIdentifier(db, organizationId, linkedinPublicIdentifier) {
867
+ (0, db_1.debugLogDbOperation)("update", "organizations", { organizationId, linkedinPublicIdentifier });
868
+ const sql = `
869
+ UPDATE public.organizations
870
+ SET linkedin_public_identifier = $1
871
+ WHERE id = $2 AND (linkedin_public_identifier IS NULL OR linkedin_public_identifier != $1)
872
+ RETURNING id
873
+ `;
874
+ const result = await (0, pg_client_1.queryOne)(db, sql, [
875
+ linkedinPublicIdentifier,
876
+ organizationId,
877
+ ]);
878
+ return result !== null;
879
+ }
880
+ /**
881
+ * Find or create an organization member
882
+ *
883
+ * @param db - Database instance
884
+ * @param membershipData - Membership data
885
+ * @returns Created or existing organization member
886
+ */
887
+ async function findOrCreateOrganizationMember(db, membershipData) {
888
+ (0, db_1.debugLogDbOperation)("select", "organization_members", { membershipData });
889
+ // Check if membership already exists
890
+ const existingSql = `
891
+ SELECT id, user_id, organization_id, contribution_role, permissions_role, created_at
892
+ FROM public.organization_members
893
+ WHERE organization_id = $1 AND user_id = $2
894
+ LIMIT 1
895
+ `;
896
+ const existing = await (0, pg_client_1.queryOne)(db, existingSql, [membershipData.organizationId, membershipData.userId]);
897
+ if (existing) {
898
+ return {
899
+ id: existing.id,
900
+ userId: existing.user_id,
901
+ organizationId: existing.organization_id,
902
+ contributionRole: existing.contribution_role,
903
+ permissionsRole: existing.permissions_role,
904
+ createdAt: existing.created_at,
905
+ };
906
+ }
907
+ // Create new membership
908
+ const insertSql = `
909
+ INSERT INTO public.organization_members (organization_id, user_id, contribution_role, permissions_role, created_at)
910
+ VALUES ($1, $2, $3, $4, NOW())
911
+ RETURNING id, user_id, organization_id, contribution_role, permissions_role, created_at
912
+ `;
913
+ const member = await (0, pg_client_1.queryOne)(db, insertSql, [
914
+ membershipData.organizationId,
915
+ membershipData.userId,
916
+ membershipData.contributionRole,
917
+ membershipData.permissionsRole,
918
+ ]);
919
+ if (!member) {
920
+ throw new Error("Failed to create organization member");
921
+ }
922
+ return {
923
+ id: member.id,
924
+ userId: member.user_id,
925
+ organizationId: member.organization_id,
926
+ contributionRole: member.contribution_role,
927
+ permissionsRole: member.permissions_role,
928
+ createdAt: member.created_at,
929
+ };
930
+ }
931
+ /**
932
+ * Find existing personal workspace for a user
933
+ * Each user can only have one personal workspace
934
+ *
935
+ * @param db - Database instance
936
+ * @param userId - User ID to find personal workspace for
937
+ * @returns Existing personal workspace or null
938
+ */
939
+ async function findPersonalWorkspaceForUser(db, userId) {
940
+ (0, db_1.debugLogDbOperation)("select", "organizations", { userId });
941
+ const sql = `
942
+ SELECT o.id, o.name, o.workspace_type, o.subscription_tier, o.image_url, o.created_at, o.created_by_user_id
943
+ FROM public.organizations o
944
+ INNER JOIN public.organization_members om ON o.id = om.organization_id
945
+ WHERE o.workspace_type = 'INDIVIDUALS'
946
+ AND om.user_id = $1
947
+ AND om.contribution_role = 'CONTRIBUTOR'
948
+ LIMIT 1
949
+ `;
950
+ const result = await (0, pg_client_1.queryOne)(db, sql, [userId]);
951
+ if (!result)
952
+ return null;
953
+ return {
954
+ id: result.id,
955
+ name: result.name,
956
+ workspaceType: result.workspace_type,
957
+ subscriptionTier: result.subscription_tier,
958
+ imageUrl: result.image_url,
959
+ createdAt: result.created_at,
960
+ createdByUserId: result.created_by_user_id,
961
+ };
962
+ }
963
+ /**
964
+ * Get organization member by ID
965
+ *
966
+ * @param db - Database instance
967
+ * @param memberId - Organization member ID
968
+ * @returns Organization member or null if not found
969
+ */
970
+ async function getOrganizationMemberById(db, memberId) {
971
+ (0, db_1.debugLogDbOperation)("select", "organization_members", { memberId });
972
+ const sql = `
973
+ SELECT
974
+ id,
975
+ user_id,
976
+ organization_id,
977
+ contribution_role,
978
+ permissions_role,
979
+ created_at
980
+ FROM public.organization_members
981
+ WHERE id = $1
982
+ LIMIT 1
983
+ `;
984
+ const result = await (0, pg_client_1.queryOne)(db, sql, [memberId]);
985
+ if (!result) {
986
+ return null;
987
+ }
988
+ return {
989
+ id: result.id,
990
+ userId: result.user_id,
991
+ organizationId: result.organization_id,
992
+ contributionRole: result.contribution_role,
993
+ permissionsRole: result.permissions_role,
994
+ createdAt: result.created_at,
995
+ };
996
+ }
997
+ /**
998
+ * Update organization subscription tier
999
+ *
1000
+ * @param db - Database instance
1001
+ * @param organizationId - Organization ID
1002
+ * @param subscriptionTier - New subscription tier
1003
+ * @returns Updated organization or null if not found
1004
+ */
1005
+ async function updateOrganizationSubscriptionTier(db, organizationId, subscriptionTier) {
1006
+ (0, db_1.debugLogDbOperation)("update", "organizations", {
1007
+ organizationId,
1008
+ subscriptionTier,
1009
+ });
1010
+ const sql = `
1011
+ UPDATE public.organizations
1012
+ SET subscription_tier = $1
1013
+ WHERE id = $2
1014
+ RETURNING id, name, workspace_type, subscription_tier, image_url, created_at, created_by_user_id
1015
+ `;
1016
+ const result = await (0, pg_client_1.queryOne)(db, sql, [subscriptionTier, organizationId]);
1017
+ if (!result) {
1018
+ return null;
1019
+ }
1020
+ return {
1021
+ id: result.id,
1022
+ name: result.name,
1023
+ workspaceType: result.workspace_type,
1024
+ subscriptionTier: result.subscription_tier,
1025
+ imageUrl: result.image_url,
1026
+ createdAt: result.created_at,
1027
+ createdByUserId: result.created_by_user_id,
1028
+ };
1029
+ }
1030
+ //# sourceMappingURL=organization-operations.js.map