@kelpie/server 0.6.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/app.d.ts +13 -0
- package/dist/app.d.ts.map +1 -1
- package/dist/app.js +9 -0
- package/dist/app.js.map +1 -1
- package/dist/boot.d.ts +42 -0
- package/dist/boot.d.ts.map +1 -0
- package/dist/boot.js +57 -0
- package/dist/boot.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/config.d.ts +7 -0
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +2 -0
- package/dist/lib/config.js.map +1 -1
- package/dist/lib/consumerEmailDomains.d.ts +2 -0
- package/dist/lib/consumerEmailDomains.d.ts.map +1 -0
- package/dist/lib/consumerEmailDomains.js +62 -0
- package/dist/lib/consumerEmailDomains.js.map +1 -0
- package/dist/lib/email.d.ts +7 -0
- package/dist/lib/email.d.ts.map +1 -1
- package/dist/lib/email.js.map +1 -1
- package/dist/lib/emailContent.d.ts +33 -0
- package/dist/lib/emailContent.d.ts.map +1 -0
- package/dist/lib/emailContent.js +54 -0
- package/dist/lib/emailContent.js.map +1 -0
- package/dist/lib/emailDomainAutoLink.d.ts +27 -0
- package/dist/lib/emailDomainAutoLink.d.ts.map +1 -0
- package/dist/lib/emailDomainAutoLink.js +119 -0
- package/dist/lib/emailDomainAutoLink.js.map +1 -0
- package/dist/lib/ids.d.ts +2 -0
- package/dist/lib/ids.d.ts.map +1 -1
- package/dist/lib/ids.js +2 -0
- package/dist/lib/ids.js.map +1 -1
- package/dist/lib/kelpieConfigFile.d.ts +6 -0
- package/dist/lib/kelpieConfigFile.d.ts.map +1 -1
- package/dist/lib/kelpieConfigFile.js +1 -0
- package/dist/lib/kelpieConfigFile.js.map +1 -1
- package/dist/modules/attachedRecords.d.ts.map +1 -1
- package/dist/modules/attachedRecords.js +4 -2
- package/dist/modules/attachedRecords.js.map +1 -1
- package/dist/modules/auth/preferences.d.ts +11 -1
- package/dist/modules/auth/preferences.d.ts.map +1 -1
- package/dist/modules/auth/preferences.js +11 -0
- package/dist/modules/auth/preferences.js.map +1 -1
- package/dist/modules/auth/routes.d.ts.map +1 -1
- package/dist/modules/auth/routes.js +18 -0
- package/dist/modules/auth/routes.js.map +1 -1
- package/dist/modules/auth/schema.d.ts +20 -0
- package/dist/modules/auth/schema.d.ts.map +1 -1
- package/dist/modules/auth/schema.js +11 -1
- package/dist/modules/auth/schema.js.map +1 -1
- package/dist/modules/auth/service.d.ts.map +1 -1
- package/dist/modules/auth/service.js +27 -3
- package/dist/modules/auth/service.js.map +1 -1
- package/dist/modules/companies/repository.d.ts +2 -0
- package/dist/modules/companies/repository.d.ts.map +1 -1
- package/dist/modules/companies/repository.js +1 -0
- package/dist/modules/companies/repository.js.map +1 -1
- package/dist/modules/companies/routes.d.ts +2 -0
- package/dist/modules/companies/routes.d.ts.map +1 -1
- package/dist/modules/companies/routes.js +11 -0
- package/dist/modules/companies/routes.js.map +1 -1
- package/dist/modules/companies/schema.d.ts +17 -0
- package/dist/modules/companies/schema.d.ts.map +1 -1
- package/dist/modules/companies/schema.js +5 -1
- package/dist/modules/companies/schema.js.map +1 -1
- package/dist/modules/companies/service.d.ts +1 -0
- package/dist/modules/companies/service.d.ts.map +1 -1
- package/dist/modules/companies/service.js +22 -0
- package/dist/modules/companies/service.js.map +1 -1
- package/dist/modules/core.d.ts.map +1 -1
- package/dist/modules/core.js +11 -0
- package/dist/modules/core.js.map +1 -1
- package/dist/modules/email-domain-linker/index.d.ts +12 -0
- package/dist/modules/email-domain-linker/index.d.ts.map +1 -0
- package/dist/modules/email-domain-linker/index.js +34 -0
- package/dist/modules/email-domain-linker/index.js.map +1 -0
- package/dist/modules/email-domain-linker/routes.d.ts +14 -0
- package/dist/modules/email-domain-linker/routes.d.ts.map +1 -0
- package/dist/modules/email-domain-linker/routes.js +15 -0
- package/dist/modules/email-domain-linker/routes.js.map +1 -0
- package/dist/modules/email-domain-linker/service.d.ts +37 -0
- package/dist/modules/email-domain-linker/service.d.ts.map +1 -0
- package/dist/modules/email-domain-linker/service.js +63 -0
- package/dist/modules/email-domain-linker/service.js.map +1 -0
- package/dist/modules/eventCatalogs.d.ts +36 -0
- package/dist/modules/eventCatalogs.d.ts.map +1 -0
- package/dist/modules/eventCatalogs.js +36 -0
- package/dist/modules/eventCatalogs.js.map +1 -0
- package/dist/modules/import-export/drafts.d.ts +9 -0
- package/dist/modules/import-export/drafts.d.ts.map +1 -1
- package/dist/modules/import-export/drafts.js +25 -0
- package/dist/modules/import-export/drafts.js.map +1 -1
- package/dist/modules/import-export/exportRows.d.ts +9 -0
- package/dist/modules/import-export/exportRows.d.ts.map +1 -1
- package/dist/modules/import-export/exportRows.js +10 -1
- package/dist/modules/import-export/exportRows.js.map +1 -1
- package/dist/modules/import-export/plan.d.ts +23 -1
- package/dist/modules/import-export/plan.d.ts.map +1 -1
- package/dist/modules/import-export/plan.js +62 -3
- package/dist/modules/import-export/plan.js.map +1 -1
- package/dist/modules/import-export/presets.d.ts.map +1 -1
- package/dist/modules/import-export/presets.js +25 -0
- package/dist/modules/import-export/presets.js.map +1 -1
- package/dist/modules/import-export/repository.d.ts +17 -0
- package/dist/modules/import-export/repository.d.ts.map +1 -1
- package/dist/modules/import-export/repository.js +21 -1
- package/dist/modules/import-export/repository.js.map +1 -1
- package/dist/modules/import-export/routes.d.ts.map +1 -1
- package/dist/modules/import-export/routes.js +5 -1
- package/dist/modules/import-export/routes.js.map +1 -1
- package/dist/modules/import-export/schema.d.ts +55 -0
- package/dist/modules/import-export/schema.d.ts.map +1 -1
- package/dist/modules/import-export/schema.js +13 -1
- package/dist/modules/import-export/schema.js.map +1 -1
- package/dist/modules/import-export/service.d.ts +4 -1
- package/dist/modules/import-export/service.d.ts.map +1 -1
- package/dist/modules/import-export/service.js +51 -3
- package/dist/modules/import-export/service.js.map +1 -1
- package/dist/modules/import-export/tools.d.ts.map +1 -1
- package/dist/modules/import-export/tools.js +7 -1
- package/dist/modules/import-export/tools.js.map +1 -1
- package/dist/modules/import-export/writes.d.ts +16 -0
- package/dist/modules/import-export/writes.d.ts.map +1 -1
- package/dist/modules/import-export/writes.js +77 -2
- package/dist/modules/import-export/writes.js.map +1 -1
- package/dist/modules/lists/events.d.ts +42 -0
- package/dist/modules/lists/events.d.ts.map +1 -0
- package/dist/modules/lists/events.js +16 -0
- package/dist/modules/lists/events.js.map +1 -0
- package/dist/modules/lists/index.d.ts +10 -0
- package/dist/modules/lists/index.d.ts.map +1 -0
- package/dist/modules/lists/index.js +35 -0
- package/dist/modules/lists/index.js.map +1 -0
- package/dist/modules/lists/repository.d.ts +69 -0
- package/dist/modules/lists/repository.d.ts.map +1 -0
- package/dist/modules/lists/repository.js +165 -0
- package/dist/modules/lists/repository.js.map +1 -0
- package/dist/modules/lists/routes.d.ts +51 -0
- package/dist/modules/lists/routes.d.ts.map +1 -0
- package/dist/modules/lists/routes.js +160 -0
- package/dist/modules/lists/routes.js.map +1 -0
- package/dist/modules/lists/schema.d.ts +264 -0
- package/dist/modules/lists/schema.d.ts.map +1 -0
- package/dist/modules/lists/schema.js +73 -0
- package/dist/modules/lists/schema.js.map +1 -0
- package/dist/modules/lists/service.d.ts +70 -0
- package/dist/modules/lists/service.d.ts.map +1 -0
- package/dist/modules/lists/service.js +214 -0
- package/dist/modules/lists/service.js.map +1 -0
- package/dist/modules/lists/tools.d.ts +4 -0
- package/dist/modules/lists/tools.d.ts.map +1 -0
- package/dist/modules/lists/tools.js +86 -0
- package/dist/modules/lists/tools.js.map +1 -0
- package/dist/modules/people/repository.d.ts +11 -0
- package/dist/modules/people/repository.d.ts.map +1 -1
- package/dist/modules/people/repository.js +16 -0
- package/dist/modules/people/repository.js.map +1 -1
- package/dist/modules/people/service.d.ts.map +1 -1
- package/dist/modules/people/service.js +18 -0
- package/dist/modules/people/service.js.map +1 -1
- package/dist/modules/positions/routes.d.ts.map +1 -1
- package/dist/modules/positions/routes.js +4 -2
- package/dist/modules/positions/routes.js.map +1 -1
- package/dist/modules/sample-data/fixture.d.ts +139 -0
- package/dist/modules/sample-data/fixture.d.ts.map +1 -0
- package/dist/modules/sample-data/fixture.js +461 -0
- package/dist/modules/sample-data/fixture.js.map +1 -0
- package/dist/modules/sample-data/index.d.ts +11 -0
- package/dist/modules/sample-data/index.d.ts.map +1 -0
- package/dist/modules/sample-data/index.js +43 -0
- package/dist/modules/sample-data/index.js.map +1 -0
- package/dist/modules/sample-data/routes.d.ts +15 -0
- package/dist/modules/sample-data/routes.d.ts.map +1 -0
- package/dist/modules/sample-data/routes.js +24 -0
- package/dist/modules/sample-data/routes.js.map +1 -0
- package/dist/modules/sample-data/service.d.ts +38 -0
- package/dist/modules/sample-data/service.d.ts.map +1 -0
- package/dist/modules/sample-data/service.js +412 -0
- package/dist/modules/sample-data/service.js.map +1 -0
- package/dist/modules/sample-data/tools.d.ts +4 -0
- package/dist/modules/sample-data/tools.d.ts.map +1 -0
- package/dist/modules/sample-data/tools.js +22 -0
- package/dist/modules/sample-data/tools.js.map +1 -0
- package/dist/modules/smtp-email/index.d.ts +1 -0
- package/dist/modules/smtp-email/index.d.ts.map +1 -1
- package/dist/modules/smtp-email/index.js +1 -0
- package/dist/modules/smtp-email/index.js.map +1 -1
- package/dist/modules/workspace/service.d.ts.map +1 -1
- package/dist/modules/workspace/service.js +10 -1
- package/dist/modules/workspace/service.js.map +1 -1
- package/dist/schema/index.d.ts +1 -0
- package/dist/schema/index.d.ts.map +1 -1
- package/dist/schema/index.js +1 -0
- package/dist/schema/index.js.map +1 -1
- package/dist/testing/app.d.ts +5 -1
- package/dist/testing/app.d.ts.map +1 -1
- package/dist/testing/app.js +2 -0
- package/dist/testing/app.js.map +1 -1
- package/migrations/0019_sharp_red_skull.sql +4 -0
- package/migrations/0020_early_nemesis.sql +2 -0
- package/migrations/0021_certain_maggott.sql +30 -0
- package/migrations/0022_stormy_manta.sql +1 -0
- package/migrations/0023_funny_maddog.sql +1 -0
- package/migrations/meta/0019_snapshot.json +5138 -0
- package/migrations/meta/0020_snapshot.json +5138 -0
- package/migrations/meta/0021_snapshot.json +5389 -0
- package/migrations/meta/0022_snapshot.json +5396 -0
- package/migrations/meta/0023_snapshot.json +5403 -0
- package/migrations/meta/_journal.json +35 -0
- package/package.json +3 -2
- package/src/app.ts +26 -0
- package/src/boot.ts +89 -0
- package/src/index.ts +8 -0
- package/src/lib/config.ts +9 -0
- package/src/lib/consumerEmailDomains.ts +62 -0
- package/src/lib/email.ts +7 -0
- package/src/lib/emailContent.ts +91 -0
- package/src/lib/emailDomainAutoLink.ts +165 -0
- package/src/lib/ids.ts +2 -0
- package/src/lib/kelpieConfigFile.ts +8 -0
- package/src/modules/attachedRecords.ts +9 -2
- package/src/modules/auth/preferences.ts +13 -1
- package/src/modules/auth/routes.ts +22 -0
- package/src/modules/auth/schema.ts +12 -2
- package/src/modules/auth/service.ts +37 -3
- package/src/modules/companies/repository.ts +3 -0
- package/src/modules/companies/routes.ts +13 -0
- package/src/modules/companies/schema.ts +5 -1
- package/src/modules/companies/service.ts +25 -0
- package/src/modules/core.ts +11 -0
- package/src/modules/email-domain-linker/index.ts +40 -0
- package/src/modules/email-domain-linker/routes.ts +40 -0
- package/src/modules/email-domain-linker/service.ts +122 -0
- package/src/modules/eventCatalogs.ts +36 -0
- package/src/modules/import-export/drafts.ts +27 -0
- package/src/modules/import-export/exportRows.ts +10 -1
- package/src/modules/import-export/plan.ts +107 -5
- package/src/modules/import-export/presets.ts +25 -0
- package/src/modules/import-export/repository.ts +35 -1
- package/src/modules/import-export/routes.ts +5 -0
- package/src/modules/import-export/schema.ts +13 -0
- package/src/modules/import-export/service.ts +70 -4
- package/src/modules/import-export/tools.ts +9 -0
- package/src/modules/import-export/writes.ts +116 -3
- package/src/modules/lists/events.ts +41 -0
- package/src/modules/lists/index.ts +41 -0
- package/src/modules/lists/repository.ts +313 -0
- package/src/modules/lists/routes.ts +247 -0
- package/src/modules/lists/schema.ts +85 -0
- package/src/modules/lists/service.ts +404 -0
- package/src/modules/lists/tools.ts +128 -0
- package/src/modules/people/repository.ts +26 -0
- package/src/modules/people/service.ts +20 -0
- package/src/modules/positions/routes.ts +4 -2
- package/src/modules/sample-data/fixture.ts +627 -0
- package/src/modules/sample-data/index.ts +50 -0
- package/src/modules/sample-data/routes.ts +49 -0
- package/src/modules/sample-data/service.ts +590 -0
- package/src/modules/sample-data/tools.ts +32 -0
- package/src/modules/smtp-email/index.ts +8 -1
- package/src/modules/workspace/service.ts +13 -1
- package/src/schema/index.ts +1 -0
- package/src/testing/app.ts +7 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ThemePreference } from '@kelpie/schemas'
|
|
1
|
+
import type { ListViewPreference, ThemePreference } from '@kelpie/schemas'
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* What an account's preferences are before anyone has saved any, and how a
|
|
@@ -17,12 +17,17 @@ export interface PreferenceValues {
|
|
|
17
17
|
readonly emailDigest: boolean
|
|
18
18
|
readonly mentionEmails: boolean
|
|
19
19
|
readonly productUpdates: boolean
|
|
20
|
+
readonly listViews: Readonly<Record<string, ListViewPreference>>
|
|
20
21
|
}
|
|
21
22
|
|
|
22
23
|
/**
|
|
23
24
|
* UTC, not the workspace's zone. An account is global and may belong to several
|
|
24
25
|
* workspaces in different zones, so deriving from one of them would be picking
|
|
25
26
|
* arbitrarily and calling it the person's answer.
|
|
27
|
+
*
|
|
28
|
+
* `listViews` starts empty. Each list page falls back to its own default when
|
|
29
|
+
* the map holds no entry for it, so the person sees the page's built-in shape
|
|
30
|
+
* until they change it.
|
|
26
31
|
*/
|
|
27
32
|
export const DEFAULT_PREFERENCES: PreferenceValues = {
|
|
28
33
|
timezone: 'UTC',
|
|
@@ -30,6 +35,7 @@ export const DEFAULT_PREFERENCES: PreferenceValues = {
|
|
|
30
35
|
emailDigest: true,
|
|
31
36
|
mentionEmails: true,
|
|
32
37
|
productUpdates: false,
|
|
38
|
+
listViews: {},
|
|
33
39
|
}
|
|
34
40
|
|
|
35
41
|
export type PreferenceChanges = Partial<PreferenceValues>
|
|
@@ -40,6 +46,11 @@ export type PreferenceChanges = Partial<PreferenceValues>
|
|
|
40
46
|
* The result is always a complete row, which is what makes the write an upsert
|
|
41
47
|
* with no read-modify-write hazard in the caller: the same request applied twice
|
|
42
48
|
* produces the same row.
|
|
49
|
+
*
|
|
50
|
+
* `listViews` is treated as one field, so the caller replaces the whole map
|
|
51
|
+
* rather than merging by key. The UI already holds the current map in the
|
|
52
|
+
* account-preferences query, so a per-view change becomes a PATCH of the merged
|
|
53
|
+
* map — the merge lives where the choice was made.
|
|
43
54
|
*/
|
|
44
55
|
export function applyPreferenceChanges(
|
|
45
56
|
stored: PreferenceValues | undefined,
|
|
@@ -53,5 +64,6 @@ export function applyPreferenceChanges(
|
|
|
53
64
|
emailDigest: changes.emailDigest ?? current.emailDigest,
|
|
54
65
|
mentionEmails: changes.mentionEmails ?? current.mentionEmails,
|
|
55
66
|
productUpdates: changes.productUpdates ?? current.productUpdates,
|
|
67
|
+
listViews: changes.listViews ?? current.listViews,
|
|
56
68
|
}
|
|
57
69
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { THEME_PREFERENCES } from '@kelpie/schemas'
|
|
2
|
+
import type { ListViewPreference } from '@kelpie/schemas'
|
|
2
3
|
import type { Context, Hono } from 'hono'
|
|
3
4
|
import { z } from 'zod'
|
|
4
5
|
|
|
@@ -59,6 +60,24 @@ const updateAccountBody = z
|
|
|
59
60
|
path: ['current_password'],
|
|
60
61
|
})
|
|
61
62
|
|
|
63
|
+
const listViewBody: z.ZodType<ListViewPreference, unknown> = z
|
|
64
|
+
.strictObject({
|
|
65
|
+
columns: z.array(z.string()).optional(),
|
|
66
|
+
mode: z.enum(['list', 'columns']).optional(),
|
|
67
|
+
grouping: z.string().optional(),
|
|
68
|
+
scope: z.string().optional(),
|
|
69
|
+
sort: z.string().optional(),
|
|
70
|
+
})
|
|
71
|
+
.transform(
|
|
72
|
+
(wire): ListViewPreference => ({
|
|
73
|
+
...(wire.columns === undefined ? {} : { columns: wire.columns }),
|
|
74
|
+
...(wire.mode === undefined ? {} : { mode: wire.mode }),
|
|
75
|
+
...(wire.grouping === undefined ? {} : { grouping: wire.grouping }),
|
|
76
|
+
...(wire.scope === undefined ? {} : { scope: wire.scope }),
|
|
77
|
+
...(wire.sort === undefined ? {} : { sort: wire.sort }),
|
|
78
|
+
}),
|
|
79
|
+
)
|
|
80
|
+
|
|
62
81
|
const updatePreferencesBody = z
|
|
63
82
|
.strictObject({
|
|
64
83
|
timezone: timezoneSchema,
|
|
@@ -66,6 +85,7 @@ const updatePreferencesBody = z
|
|
|
66
85
|
email_digest: z.boolean(),
|
|
67
86
|
mention_emails: z.boolean(),
|
|
68
87
|
product_updates: z.boolean(),
|
|
88
|
+
list_views: z.record(z.string(), listViewBody),
|
|
69
89
|
})
|
|
70
90
|
.partial()
|
|
71
91
|
|
|
@@ -118,6 +138,7 @@ function preferencesResponse(preferences: PreferenceValues): Record<string, unkn
|
|
|
118
138
|
email_digest: preferences.emailDigest,
|
|
119
139
|
mention_emails: preferences.mentionEmails,
|
|
120
140
|
product_updates: preferences.productUpdates,
|
|
141
|
+
list_views: preferences.listViews,
|
|
121
142
|
}
|
|
122
143
|
}
|
|
123
144
|
|
|
@@ -213,6 +234,7 @@ export function mountAuthRoutes(router: Hono, dependencies: AuthRoutesDependenci
|
|
|
213
234
|
...(body.email_digest === undefined ? {} : { emailDigest: body.email_digest }),
|
|
214
235
|
...(body.mention_emails === undefined ? {} : { mentionEmails: body.mention_emails }),
|
|
215
236
|
...(body.product_updates === undefined ? {} : { productUpdates: body.product_updates }),
|
|
237
|
+
...(body.list_views === undefined ? {} : { listViews: body.list_views }),
|
|
216
238
|
})
|
|
217
239
|
|
|
218
240
|
return context.json(preferencesResponse(preferences))
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { THEME_PREFERENCES } from '@kelpie/schemas'
|
|
2
|
-
import type { ThemePreference } from '@kelpie/schemas'
|
|
3
|
-
import { boolean, pgTable, text } from 'drizzle-orm/pg-core'
|
|
2
|
+
import type { ListViewPreference, ThemePreference } from '@kelpie/schemas'
|
|
3
|
+
import { boolean, jsonb, pgTable, text } from 'drizzle-orm/pg-core'
|
|
4
4
|
|
|
5
5
|
import { checkOneOf, citext, createdAt, moment, primaryId, updatedAt } from '../../lib/columns.ts'
|
|
6
6
|
|
|
@@ -57,6 +57,16 @@ export const userPreferences = pgTable(
|
|
|
57
57
|
emailDigest: boolean('email_digest').notNull(),
|
|
58
58
|
mentionEmails: boolean('mention_emails').notNull(),
|
|
59
59
|
productUpdates: boolean('product_updates').notNull(),
|
|
60
|
+
/**
|
|
61
|
+
* Per-list column choices, keyed on a stable view id (`people`, `deals`, …).
|
|
62
|
+
* A view id the client no longer recognises stays untouched here rather than
|
|
63
|
+
* being cleaned up: the map is small, and the write path is the client
|
|
64
|
+
* merging its current choice into what it read.
|
|
65
|
+
*/
|
|
66
|
+
listViews: jsonb('list_views')
|
|
67
|
+
.$type<Readonly<Record<string, ListViewPreference>>>()
|
|
68
|
+
.notNull()
|
|
69
|
+
.default({}),
|
|
60
70
|
updatedAt: updatedAt(),
|
|
61
71
|
},
|
|
62
72
|
(table) => [checkOneOf('user_preferences_theme_check', table.theme, THEME_PREFERENCES)],
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { APP_LINK_PATHS, buildAppLink } from '../../lib/appUrl.ts'
|
|
2
2
|
import type { EmailSender } from '../../lib/email.ts'
|
|
3
|
+
import { renderEmail } from '../../lib/emailContent.ts'
|
|
3
4
|
import { AppError } from '../../lib/errors.ts'
|
|
4
5
|
import type { IdFactory } from '../../lib/ids.ts'
|
|
5
6
|
import { UNIQUE_VIOLATION, postgresErrorCode } from '../../lib/database.ts'
|
|
@@ -95,6 +96,7 @@ function toPreferenceValues(record: repository.UserPreferencesRecord): Preferenc
|
|
|
95
96
|
emailDigest: record.emailDigest,
|
|
96
97
|
mentionEmails: record.mentionEmails,
|
|
97
98
|
productUpdates: record.productUpdates,
|
|
99
|
+
listViews: record.listViews,
|
|
98
100
|
}
|
|
99
101
|
}
|
|
100
102
|
|
|
@@ -192,11 +194,23 @@ export function createAuthService(dependencies: AuthDependencies): AuthService {
|
|
|
192
194
|
|
|
193
195
|
function sendVerificationEmail(to: string, token: string): Promise<void> {
|
|
194
196
|
const link = buildAppLink(dependencies.appBaseUrl, APP_LINK_PATHS.verifyEmail, token)
|
|
197
|
+
const { text, html } = renderEmail(
|
|
198
|
+
{
|
|
199
|
+
action: {
|
|
200
|
+
instructions: 'Confirm this address to finish setting up your account:',
|
|
201
|
+
buttonText: 'Verify email address',
|
|
202
|
+
link,
|
|
203
|
+
},
|
|
204
|
+
outro: 'The link expires in 24 hours.',
|
|
205
|
+
},
|
|
206
|
+
dependencies.appBaseUrl,
|
|
207
|
+
)
|
|
195
208
|
|
|
196
209
|
return dependencies.email.send({
|
|
197
210
|
to,
|
|
198
211
|
subject: 'Verify your Kelpie email address',
|
|
199
|
-
body:
|
|
212
|
+
body: text,
|
|
213
|
+
html,
|
|
200
214
|
})
|
|
201
215
|
}
|
|
202
216
|
|
|
@@ -320,10 +334,18 @@ export function createAuthService(dependencies: AuthDependencies): AuthService {
|
|
|
320
334
|
})
|
|
321
335
|
|
|
322
336
|
if (changes.email !== undefined) {
|
|
337
|
+
const { text, html } = renderEmail(
|
|
338
|
+
{
|
|
339
|
+
intro: `Your Kelpie sign-in email changed from ${previousEmail} to ${updated.email}. If you did not make this change, reset your password right away.`,
|
|
340
|
+
},
|
|
341
|
+
dependencies.appBaseUrl,
|
|
342
|
+
)
|
|
343
|
+
|
|
323
344
|
await dependencies.email.send({
|
|
324
345
|
to: previousEmail,
|
|
325
346
|
subject: 'Your Kelpie sign-in email changed',
|
|
326
|
-
body:
|
|
347
|
+
body: text,
|
|
348
|
+
html,
|
|
327
349
|
})
|
|
328
350
|
}
|
|
329
351
|
|
|
@@ -418,11 +440,23 @@ export function createAuthService(dependencies: AuthDependencies): AuthService {
|
|
|
418
440
|
})
|
|
419
441
|
|
|
420
442
|
const link = buildAppLink(dependencies.appBaseUrl, APP_LINK_PATHS.passwordReset, token)
|
|
443
|
+
const { text, html } = renderEmail(
|
|
444
|
+
{
|
|
445
|
+
action: {
|
|
446
|
+
instructions: 'Use this link within the hour to choose a new password:',
|
|
447
|
+
buttonText: 'Reset password',
|
|
448
|
+
link,
|
|
449
|
+
},
|
|
450
|
+
outro: 'If you did not ask for this, ignore it.',
|
|
451
|
+
},
|
|
452
|
+
dependencies.appBaseUrl,
|
|
453
|
+
)
|
|
421
454
|
|
|
422
455
|
await dependencies.email.send({
|
|
423
456
|
to: user.email,
|
|
424
457
|
subject: 'Reset your Kelpie password',
|
|
425
|
-
body:
|
|
458
|
+
body: text,
|
|
459
|
+
html,
|
|
426
460
|
})
|
|
427
461
|
},
|
|
428
462
|
|
|
@@ -27,6 +27,8 @@ export interface CompanyFilters {
|
|
|
27
27
|
readonly term?: string | undefined
|
|
28
28
|
/** `?person_id=`, repeatable: companies where any of these people holds a position. */
|
|
29
29
|
readonly personIds?: readonly string[] | undefined
|
|
30
|
+
/** `?is_own=true` returns only the workspace's own companies; `false` returns everything else. */
|
|
31
|
+
readonly isOwn?: boolean | undefined
|
|
30
32
|
}
|
|
31
33
|
|
|
32
34
|
/**
|
|
@@ -64,6 +66,7 @@ function conditionsFor(workspaceId: string, filters: CompanyFilters): (SQL | und
|
|
|
64
66
|
eq(companies.workspaceId, workspaceId),
|
|
65
67
|
filters.term === undefined ? undefined : matchesTerm(filters.term),
|
|
66
68
|
filters.personIds === undefined ? undefined : employsAnyOf(filters.personIds),
|
|
69
|
+
filters.isOwn === undefined ? undefined : eq(companies.isOwn, filters.isOwn),
|
|
67
70
|
]
|
|
68
71
|
}
|
|
69
72
|
|
|
@@ -30,6 +30,7 @@ const companyShape = {
|
|
|
30
30
|
tech_stack: z.array(z.string().min(1)),
|
|
31
31
|
summary: z.string(),
|
|
32
32
|
tags: z.array(z.string().min(1)),
|
|
33
|
+
is_own: z.boolean(),
|
|
33
34
|
}
|
|
34
35
|
|
|
35
36
|
/**
|
|
@@ -51,6 +52,7 @@ export const createBody = z.strictObject({
|
|
|
51
52
|
tech_stack: companyShape.tech_stack.default([]),
|
|
52
53
|
summary: companyShape.summary.default(''),
|
|
53
54
|
tags: companyShape.tags.default([]),
|
|
55
|
+
is_own: companyShape.is_own.default(false),
|
|
54
56
|
})
|
|
55
57
|
|
|
56
58
|
export const updateBody = z.strictObject(companyShape).partial()
|
|
@@ -74,6 +76,7 @@ export function toCreateInput(body: z.infer<typeof createBody>): CreateCompanyIn
|
|
|
74
76
|
techStack: body.tech_stack,
|
|
75
77
|
summary: body.summary,
|
|
76
78
|
tags: body.tags,
|
|
79
|
+
isOwn: body.is_own,
|
|
77
80
|
}
|
|
78
81
|
}
|
|
79
82
|
|
|
@@ -92,6 +95,7 @@ export function toUpdateInput(body: z.infer<typeof updateBody>): UpdateCompanyIn
|
|
|
92
95
|
...(body.tech_stack === undefined ? {} : { techStack: body.tech_stack }),
|
|
93
96
|
...(body.summary === undefined ? {} : { summary: body.summary }),
|
|
94
97
|
...(body.tags === undefined ? {} : { tags: body.tags }),
|
|
98
|
+
...(body.is_own === undefined ? {} : { isOwn: body.is_own }),
|
|
95
99
|
}
|
|
96
100
|
}
|
|
97
101
|
|
|
@@ -111,6 +115,7 @@ export function companyResponse(company: CompanyView): Record<string, unknown> {
|
|
|
111
115
|
tech_stack: company.techStack,
|
|
112
116
|
summary: company.summary,
|
|
113
117
|
tags: company.tags,
|
|
118
|
+
is_own: company.isOwn,
|
|
114
119
|
created_at: company.createdAt.toISOString(),
|
|
115
120
|
updated_at: company.updatedAt.toISOString(),
|
|
116
121
|
}
|
|
@@ -120,11 +125,19 @@ export function mountCompaniesRoutes(router: Hono, dependencies: CompaniesRoutes
|
|
|
120
125
|
const requireActor = (context: Context): Promise<Actor> => resolveActorFrom(dependencies, context)
|
|
121
126
|
|
|
122
127
|
router.get('/companies', async (context) => {
|
|
128
|
+
// `?is_own=true|false` narrows to the workspace's own companies (or the
|
|
129
|
+
// rest). Anything else is treated as absent, so an unknown value never
|
|
130
|
+
// returns an accidental "everything".
|
|
131
|
+
const isOwnParam = context.req.query('is_own')
|
|
132
|
+
const isOwn =
|
|
133
|
+
isOwnParam === 'true' ? true : isOwnParam === 'false' ? false : undefined
|
|
134
|
+
|
|
123
135
|
const page = await dependencies.service.list(
|
|
124
136
|
await requireActor(context),
|
|
125
137
|
{
|
|
126
138
|
term: context.req.query('q'),
|
|
127
139
|
personIds: readIdFilter(context, 'person_id'),
|
|
140
|
+
isOwn,
|
|
128
141
|
},
|
|
129
142
|
readListParameters(context),
|
|
130
143
|
)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ACCOUNT_TYPES, COMPANY_STAGES, ICP_FITS, SIZE_BANDS } from '@kelpie/schemas'
|
|
2
|
-
import { index, pgTable, text, unique } from 'drizzle-orm/pg-core'
|
|
2
|
+
import { boolean, index, pgTable, text, unique } from 'drizzle-orm/pg-core'
|
|
3
3
|
|
|
4
4
|
import {
|
|
5
5
|
checkOneOf,
|
|
@@ -42,6 +42,10 @@ export const companies = pgTable(
|
|
|
42
42
|
techStack: text('tech_stack').array().notNull().default([]),
|
|
43
43
|
summary: text('summary').notNull().default(''),
|
|
44
44
|
tags: text('tags').array().notNull().default([]),
|
|
45
|
+
// "This is us": a workspace marks its own company (or several, e.g. parent
|
|
46
|
+
// and subsidiary) so downstream views can distinguish self from prospects.
|
|
47
|
+
// Loose semantics on purpose — zero or many rows may carry it.
|
|
48
|
+
isOwn: boolean('is_own').notNull().default(false),
|
|
45
49
|
createdAt: createdAt(),
|
|
46
50
|
updatedAt: updatedAt(),
|
|
47
51
|
searchVector: searchVector((): readonly SearchVectorPart[] => [
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { changedKeys } from '../../lib/changes.ts'
|
|
2
2
|
import { UNIQUE_VIOLATION, isReferenceViolation, postgresErrorCode } from '../../lib/database.ts'
|
|
3
3
|
import type { Database } from '../../lib/database.ts'
|
|
4
|
+
import { autoLinkCompanyByDomain } from '../../lib/emailDomainAutoLink.ts'
|
|
4
5
|
import { AppError } from '../../lib/errors.ts'
|
|
5
6
|
import type { IdFactory } from '../../lib/ids.ts'
|
|
6
7
|
import { normaliseDomain } from '../../lib/normalisation.ts'
|
|
@@ -51,6 +52,7 @@ const COMPANY_FIELD_LABELS: FieldLabels = {
|
|
|
51
52
|
techStack: 'Tech stack',
|
|
52
53
|
summary: 'Summary',
|
|
53
54
|
tags: 'Tags',
|
|
55
|
+
isOwn: 'Own company',
|
|
54
56
|
}
|
|
55
57
|
|
|
56
58
|
/** A company as the API returns one: the stored row minus the tenancy column. */
|
|
@@ -70,6 +72,7 @@ export interface CreateCompanyInput {
|
|
|
70
72
|
readonly techStack: readonly string[]
|
|
71
73
|
readonly summary: string
|
|
72
74
|
readonly tags: readonly string[]
|
|
75
|
+
readonly isOwn: boolean
|
|
73
76
|
}
|
|
74
77
|
|
|
75
78
|
/** PATCH semantics: an absent field is left alone, and null clears a nullable one. */
|
|
@@ -112,6 +115,7 @@ function toStoredColumns(input: UpdateCompanyInput): Partial<repository.CompanyC
|
|
|
112
115
|
...(input.techStack === undefined ? {} : { techStack: [...input.techStack] }),
|
|
113
116
|
...(input.summary === undefined ? {} : { summary: input.summary }),
|
|
114
117
|
...(input.tags === undefined ? {} : { tags: [...input.tags] }),
|
|
118
|
+
...(input.isOwn === undefined ? {} : { isOwn: input.isOwn }),
|
|
115
119
|
}
|
|
116
120
|
}
|
|
117
121
|
|
|
@@ -171,6 +175,7 @@ export function createCompaniesService(dependencies: CompaniesDependencies): Com
|
|
|
171
175
|
techStack: [...input.techStack],
|
|
172
176
|
summary: input.summary,
|
|
173
177
|
tags: [...input.tags],
|
|
178
|
+
isOwn: input.isOwn,
|
|
174
179
|
})
|
|
175
180
|
} catch (error: unknown) {
|
|
176
181
|
if (postgresErrorCode(error) === UNIQUE_VIOLATION) {
|
|
@@ -189,6 +194,15 @@ export function createCompaniesService(dependencies: CompaniesDependencies): Com
|
|
|
189
194
|
|
|
190
195
|
events.emit('companies.company.created', { type: 'company', id: created.id }, {})
|
|
191
196
|
|
|
197
|
+
// Sync sweep of the workspace's people: everyone whose email domain
|
|
198
|
+
// matches this Company gets a titleless Position where none exists yet.
|
|
199
|
+
// Inside the same transaction so a follow-up read sees the new links.
|
|
200
|
+
await autoLinkCompanyByDomain(tx, events, workspaceId, created, {
|
|
201
|
+
createId: dependencies.createId,
|
|
202
|
+
now: dependencies.now,
|
|
203
|
+
recordActivity: dependencies.recordActivity,
|
|
204
|
+
})
|
|
205
|
+
|
|
192
206
|
return toView(created)
|
|
193
207
|
}, { workspaceId, actor: toEventActor(actor) })
|
|
194
208
|
},
|
|
@@ -232,6 +246,17 @@ export function createCompaniesService(dependencies: CompaniesDependencies): Com
|
|
|
232
246
|
|
|
233
247
|
events.emit('companies.company.updated', { type: 'company', id }, { changed })
|
|
234
248
|
|
|
249
|
+
// Sync sweep when the domain moved. A newly-set domain gains matches;
|
|
250
|
+
// a changed domain gains new matches without touching the old links
|
|
251
|
+
// (never-remove — a person may still work at the previous domain).
|
|
252
|
+
if (changed.includes('domain')) {
|
|
253
|
+
await autoLinkCompanyByDomain(tx, events, workspaceId, updated, {
|
|
254
|
+
createId: dependencies.createId,
|
|
255
|
+
now: dependencies.now,
|
|
256
|
+
recordActivity: dependencies.recordActivity,
|
|
257
|
+
})
|
|
258
|
+
}
|
|
259
|
+
|
|
235
260
|
return toView(updated)
|
|
236
261
|
}, { workspaceId, actor: toEventActor(actor) })
|
|
237
262
|
},
|
package/src/modules/core.ts
CHANGED
|
@@ -9,10 +9,12 @@ import { createCompaniesModule } from './companies/index.ts'
|
|
|
9
9
|
import { createDashboardModule } from './dashboard/index.ts'
|
|
10
10
|
import { createDealsModule } from './deals/index.ts'
|
|
11
11
|
import { createDecisionsModule } from './decisions/index.ts'
|
|
12
|
+
import { createEmailDomainLinkerModule } from './email-domain-linker/index.ts'
|
|
12
13
|
import { createFormsModule } from './forms/index.ts'
|
|
13
14
|
import { createHandbookModule } from './handbook/index.ts'
|
|
14
15
|
import { createHiringModule } from './hiring/index.ts'
|
|
15
16
|
import { createImportExportModule } from './import-export/index.ts'
|
|
17
|
+
import { createListsModule } from './lists/index.ts'
|
|
16
18
|
import { createNotesModule } from './notes/index.ts'
|
|
17
19
|
import { createOpportunitiesModule } from './opportunities/index.ts'
|
|
18
20
|
import { createPartnershipsModule } from './partnerships/index.ts'
|
|
@@ -21,6 +23,7 @@ import { createPipelinesModule } from './pipelines/index.ts'
|
|
|
21
23
|
import { createPlansModule } from './plans/index.ts'
|
|
22
24
|
import { createPositionsModule } from './positions/index.ts'
|
|
23
25
|
import { createRaisesModule } from './raises/index.ts'
|
|
26
|
+
import { createSampleDataModule } from './sample-data/index.ts'
|
|
24
27
|
import { createSearchModule } from './search/index.ts'
|
|
25
28
|
import { createSmtpEmailModule } from './smtp-email/index.ts'
|
|
26
29
|
import { createWebhooksModule } from './webhooks/index.ts'
|
|
@@ -57,6 +60,7 @@ export const coreModules: readonly KelpieModule[] = [
|
|
|
57
60
|
createHiringModule(coreMigrationsDirectory),
|
|
58
61
|
createPlansModule(coreMigrationsDirectory),
|
|
59
62
|
createDecisionsModule(coreMigrationsDirectory),
|
|
63
|
+
createListsModule(coreMigrationsDirectory),
|
|
60
64
|
createHandbookModule(coreMigrationsDirectory),
|
|
61
65
|
// Neither of these takes a migrations directory: they own no tables and read
|
|
62
66
|
// other modules'.
|
|
@@ -66,6 +70,13 @@ export const coreModules: readonly KelpieModule[] = [
|
|
|
66
70
|
createImportExportModule(coreMigrationsDirectory),
|
|
67
71
|
createAgentTasksModule(coreMigrationsDirectory),
|
|
68
72
|
createWebhooksModule(coreMigrationsDirectory),
|
|
73
|
+
// Owns no tables. One-shot fixture install for a new workspace, called from
|
|
74
|
+
// the setup wizard's checkbox and the admin data page.
|
|
75
|
+
createSampleDataModule(),
|
|
76
|
+
// Owns no tables. Admin action that reruns the email-domain sweep across
|
|
77
|
+
// every workspace Company — backfills links for data that predates the
|
|
78
|
+
// auto-linker, or after a bulk import.
|
|
79
|
+
createEmailDomainLinkerModule(),
|
|
69
80
|
// Owns no tables. Registers a `'smtp'` provider with core's email runtime;
|
|
70
81
|
// the factory only runs if the assembly's `email.provider` picks it.
|
|
71
82
|
createSmtpEmailModule(),
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { KelpieModule } from '../../runtime/module.ts'
|
|
2
|
+
import { createActivityRecorder } from '../activities/index.ts'
|
|
3
|
+
import { mountEmailDomainLinkerRoutes } from './routes.ts'
|
|
4
|
+
import { createEmailDomainLinkerService } from './service.ts'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Rebuild every email-domain link across the workspace.
|
|
8
|
+
*
|
|
9
|
+
* Owns no tables. Requires the modules whose data the sweep reads and writes.
|
|
10
|
+
* The sweep is the same one the companies service runs on a domain change,
|
|
11
|
+
* applied to every workspace Company at once — used to backfill links for
|
|
12
|
+
* data that predates the auto-linker, or after a bulk import.
|
|
13
|
+
*/
|
|
14
|
+
export function createEmailDomainLinkerModule(): KelpieModule {
|
|
15
|
+
return {
|
|
16
|
+
id: 'email-domain-linker',
|
|
17
|
+
requires: ['workspace', 'companies', 'people', 'positions', 'activities'],
|
|
18
|
+
|
|
19
|
+
register(context) {
|
|
20
|
+
const service = createEmailDomainLinkerService({
|
|
21
|
+
db: context.db,
|
|
22
|
+
transaction: context.transaction,
|
|
23
|
+
createId: context.createId,
|
|
24
|
+
now: context.now,
|
|
25
|
+
recordActivity: createActivityRecorder({
|
|
26
|
+
createId: context.createId,
|
|
27
|
+
now: context.now,
|
|
28
|
+
}),
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
context.routes((router) => {
|
|
32
|
+
mountEmailDomainLinkerRoutes(router, { db: context.db, now: context.now, service })
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
return Promise.resolve()
|
|
36
|
+
},
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export type { EmailDomainLinkerService, RelinkCounts } from './service.ts'
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { Context, Hono } from 'hono'
|
|
2
|
+
|
|
3
|
+
import type { Actor } from '../auth/actor.ts'
|
|
4
|
+
import { resolveActorFrom } from '../auth/credentials.ts'
|
|
5
|
+
import type { CredentialDependencies } from '../auth/credentials.ts'
|
|
6
|
+
import type { EmailDomainLinkerService, RelinkCounts } from './service.ts'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Wire shape for `POST /v1/workspaces/:id/relink-email-domains`.
|
|
10
|
+
*
|
|
11
|
+
* No request body. The response counts what the sweep did.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export interface EmailDomainLinkerRoutesDependencies extends CredentialDependencies {
|
|
15
|
+
readonly service: EmailDomainLinkerService
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function relinkResponse(counts: RelinkCounts): Record<string, unknown> {
|
|
19
|
+
return {
|
|
20
|
+
companies_scanned: counts.companiesScanned,
|
|
21
|
+
positions_created: counts.positionsCreated,
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function mountEmailDomainLinkerRoutes(
|
|
26
|
+
router: Hono,
|
|
27
|
+
dependencies: EmailDomainLinkerRoutesDependencies,
|
|
28
|
+
): void {
|
|
29
|
+
const requireActor = (context: Context): Promise<Actor> =>
|
|
30
|
+
resolveActorFrom(dependencies, context)
|
|
31
|
+
|
|
32
|
+
router.post('/workspaces/:id/relink-email-domains', async (context) => {
|
|
33
|
+
const counts = await dependencies.service.relink(
|
|
34
|
+
await requireActor(context),
|
|
35
|
+
context.req.param('id'),
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
return context.json(relinkResponse(counts))
|
|
39
|
+
})
|
|
40
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { and, eq, isNotNull } from 'drizzle-orm'
|
|
2
|
+
|
|
3
|
+
import { toEventActor } from '../../lib/actor.ts'
|
|
4
|
+
import type { Database } from '../../lib/database.ts'
|
|
5
|
+
import { autoLinkCompanyByDomain } from '../../lib/emailDomainAutoLink.ts'
|
|
6
|
+
import { AppError } from '../../lib/errors.ts'
|
|
7
|
+
import type { IdFactory } from '../../lib/ids.ts'
|
|
8
|
+
import type { TransactionScope } from '../../runtime/transaction.ts'
|
|
9
|
+
import type { ActivityRecorder } from '../activities/recorder.ts'
|
|
10
|
+
import type { Actor } from '../auth/actor.ts'
|
|
11
|
+
import { requireWorkspaceId } from '../auth/actor.ts'
|
|
12
|
+
import * as authRepository from '../auth/repository.ts'
|
|
13
|
+
import { companies } from '../companies/schema.ts'
|
|
14
|
+
import { parseMemberRole, roleAllows } from '../workspace/roles.ts'
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Rebuild every email-domain link across the workspace.
|
|
18
|
+
*
|
|
19
|
+
* Iterates each Company that carries a domain and runs the same sweep the
|
|
20
|
+
* companies service runs on a domain change — every workspace Person whose
|
|
21
|
+
* email is at that domain gets a titleless Position where none exists yet.
|
|
22
|
+
*
|
|
23
|
+
* Idempotent: a follow-up run adds nothing. Add-only, consumer-host-skipping,
|
|
24
|
+
* and workspace-scoped, same as the inline auto-linker.
|
|
25
|
+
*
|
|
26
|
+
* One transaction per Company, not one for the whole sweep. A workspace can
|
|
27
|
+
* carry thousands of Companies, and a single transaction that long would
|
|
28
|
+
* hold row-share locks against every write elsewhere for the whole run.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
export interface EmailDomainLinkerDependencies {
|
|
32
|
+
readonly db: Database
|
|
33
|
+
readonly transaction: TransactionScope
|
|
34
|
+
readonly createId: IdFactory
|
|
35
|
+
readonly now: () => Date
|
|
36
|
+
readonly recordActivity: ActivityRecorder
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface RelinkCounts {
|
|
40
|
+
/** Companies with a non-null domain that were considered. Consumer hosts are counted but skipped. */
|
|
41
|
+
readonly companiesScanned: number
|
|
42
|
+
/** New Positions the sweep inserted this run. */
|
|
43
|
+
readonly positionsCreated: number
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface EmailDomainLinkerService {
|
|
47
|
+
relink(actor: Actor, workspaceId: string): Promise<RelinkCounts>
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function createEmailDomainLinkerService(
|
|
51
|
+
dependencies: EmailDomainLinkerDependencies,
|
|
52
|
+
): EmailDomainLinkerService {
|
|
53
|
+
async function requireAdmin(actor: Actor, workspaceId: string): Promise<void> {
|
|
54
|
+
if (actor.workspaceId !== workspaceId) {
|
|
55
|
+
throw AppError.notFound('Workspace not found')
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (actor.kind === 'api_key' && actor.userId === null) {
|
|
59
|
+
if (!roleAllows(actor.role, 'admin')) {
|
|
60
|
+
throw new AppError('forbidden', 'This action needs the admin role')
|
|
61
|
+
}
|
|
62
|
+
return
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const userId = actor.userId
|
|
66
|
+
|
|
67
|
+
if (userId === null) {
|
|
68
|
+
throw AppError.notFound('Workspace not found')
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const membership = await authRepository.findMembership(dependencies.db, workspaceId, userId)
|
|
72
|
+
|
|
73
|
+
if (membership === undefined) {
|
|
74
|
+
throw AppError.notFound('Workspace not found')
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const role = parseMemberRole(membership.role)
|
|
78
|
+
|
|
79
|
+
if (role === undefined) {
|
|
80
|
+
throw new Error(`workspace_members.role holds "${membership.role}", which its check forbids`)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (!roleAllows(role, 'admin')) {
|
|
84
|
+
throw new AppError('forbidden', 'This action needs the admin role')
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
async relink(actor, workspaceId) {
|
|
90
|
+
requireWorkspaceId(actor)
|
|
91
|
+
await requireAdmin(actor, workspaceId)
|
|
92
|
+
|
|
93
|
+
// Every workspace Company that carries a domain — the ones the sweep
|
|
94
|
+
// can match against. Pooled read outside the per-company transactions:
|
|
95
|
+
// the work-list is one query, each write opens its own scope.
|
|
96
|
+
const rows = await dependencies.db
|
|
97
|
+
.select()
|
|
98
|
+
.from(companies)
|
|
99
|
+
.where(and(eq(companies.workspaceId, workspaceId), isNotNull(companies.domain)))
|
|
100
|
+
|
|
101
|
+
let created = 0
|
|
102
|
+
|
|
103
|
+
// One transaction per Company, not one for the whole sweep. A workspace
|
|
104
|
+
// can carry thousands of Companies; a single transaction that long would
|
|
105
|
+
// hold row-share locks against every write elsewhere for the whole run.
|
|
106
|
+
for (const company of rows) {
|
|
107
|
+
const inserted = await dependencies.transaction(
|
|
108
|
+
async ({ tx, events }) =>
|
|
109
|
+
autoLinkCompanyByDomain(tx, events, workspaceId, company, {
|
|
110
|
+
createId: dependencies.createId,
|
|
111
|
+
now: dependencies.now,
|
|
112
|
+
recordActivity: dependencies.recordActivity,
|
|
113
|
+
}),
|
|
114
|
+
{ workspaceId, actor: toEventActor(actor) },
|
|
115
|
+
)
|
|
116
|
+
created += inserted.length
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return { companiesScanned: rows.length, positionsCreated: created }
|
|
120
|
+
},
|
|
121
|
+
}
|
|
122
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Loads every core module's `KelpieEventMap` augmentation at the package entry.
|
|
3
|
+
*
|
|
4
|
+
* Each module declares its event payload types in `<module>/events.ts` through
|
|
5
|
+
* `declare module '../../runtime/events.ts'`. Inside the monorepo those merge
|
|
6
|
+
* because the source graph reaches every file. A consumer that installs
|
|
7
|
+
* `@kelpie/server` from npm typechecks against `dist`, and declaration emit
|
|
8
|
+
* keeps only the imports a file actually references. Nothing in `dist/index.d.ts`
|
|
9
|
+
* referenced the per-module `events.d.ts` files, so `KelpieEventMap` stayed empty
|
|
10
|
+
* for consumers and every `subscribe` handler saw `event.data: unknown`.
|
|
11
|
+
*
|
|
12
|
+
* These side-effect imports give `index.ts` one reference into each catalog. The
|
|
13
|
+
* emitted `index.d.ts` carries them, so a consumer loads the augmentations and
|
|
14
|
+
* gets a typed `event.data` without a cast at the read site.
|
|
15
|
+
*
|
|
16
|
+
* List only modules whose `events.ts` augments `KelpieEventMap`. A module that
|
|
17
|
+
* publishes no events has no `events.ts` and does not belong here. Order mirrors
|
|
18
|
+
* `coreModules` in `core.ts` for readability; it has no runtime effect.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import './workspace/events.ts'
|
|
22
|
+
import './people/events.ts'
|
|
23
|
+
import './companies/events.ts'
|
|
24
|
+
import './positions/events.ts'
|
|
25
|
+
import './notes/events.ts'
|
|
26
|
+
import './lists/events.ts'
|
|
27
|
+
import './deals/events.ts'
|
|
28
|
+
import './opportunities/events.ts'
|
|
29
|
+
import './partnerships/events.ts'
|
|
30
|
+
import './raises/events.ts'
|
|
31
|
+
import './hiring/events.ts'
|
|
32
|
+
import './plans/events.ts'
|
|
33
|
+
import './decisions/events.ts'
|
|
34
|
+
import './handbook/events.ts'
|
|
35
|
+
import './forms/events.ts'
|
|
36
|
+
import './import-export/events.ts'
|