@kelpie/server 0.7.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/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/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 +1 -0
- package/dist/modules/eventCatalogs.d.ts.map +1 -1
- package/dist/modules/eventCatalogs.js +1 -0
- package/dist/modules/eventCatalogs.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/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/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 +21 -0
- package/package.json +3 -2
- 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/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 +1 -0
- 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
|
@@ -0,0 +1,49 @@
|
|
|
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 { SampleDataCounts, SampleDataService } from './service.ts'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Wire shape for `POST /v1/workspaces/:id/sample-data`.
|
|
10
|
+
*
|
|
11
|
+
* No request body: the installer takes no options today. Any option we add
|
|
12
|
+
* later goes on the body, not the URL.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
export interface SampleDataRoutesDependencies extends CredentialDependencies {
|
|
16
|
+
readonly service: SampleDataService
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function sampleDataResponse(counts: SampleDataCounts): Record<string, unknown> {
|
|
20
|
+
return {
|
|
21
|
+
companies: counts.companies,
|
|
22
|
+
people: counts.people,
|
|
23
|
+
positions: counts.positions,
|
|
24
|
+
deals: counts.deals,
|
|
25
|
+
plan_items: counts.planItems,
|
|
26
|
+
notes: counts.notes,
|
|
27
|
+
opportunities: counts.opportunities,
|
|
28
|
+
raises: counts.raises,
|
|
29
|
+
partnerships: counts.partnerships,
|
|
30
|
+
roles: counts.roles,
|
|
31
|
+
candidates: counts.candidates,
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function mountSampleDataRoutes(
|
|
36
|
+
router: Hono,
|
|
37
|
+
dependencies: SampleDataRoutesDependencies,
|
|
38
|
+
): void {
|
|
39
|
+
const requireActor = (context: Context): Promise<Actor> => resolveActorFrom(dependencies, context)
|
|
40
|
+
|
|
41
|
+
router.post('/workspaces/:id/sample-data', async (context) => {
|
|
42
|
+
const counts = await dependencies.service.install(
|
|
43
|
+
await requireActor(context),
|
|
44
|
+
context.req.param('id'),
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
return context.json(sampleDataResponse(counts), 201)
|
|
48
|
+
})
|
|
49
|
+
}
|
|
@@ -0,0 +1,590 @@
|
|
|
1
|
+
import { and, count, eq, inArray } from 'drizzle-orm'
|
|
2
|
+
|
|
3
|
+
import { toEventActor } from '../../lib/actor.ts'
|
|
4
|
+
import type { Database } from '../../lib/database.ts'
|
|
5
|
+
import { AppError } from '../../lib/errors.ts'
|
|
6
|
+
import type { IdFactory } from '../../lib/ids.ts'
|
|
7
|
+
import type { TransactionScope } from '../../runtime/transaction.ts'
|
|
8
|
+
import type { Actor } from '../auth/actor.ts'
|
|
9
|
+
import { requireWorkspaceId } from '../auth/actor.ts'
|
|
10
|
+
import * as authRepository from '../auth/repository.ts'
|
|
11
|
+
import { companies } from '../companies/schema.ts'
|
|
12
|
+
import { dealPeople, deals } from '../deals/schema.ts'
|
|
13
|
+
import { candidates, roles } from '../hiring/schema.ts'
|
|
14
|
+
import { notes } from '../notes/schema.ts'
|
|
15
|
+
import { opportunities } from '../opportunities/schema.ts'
|
|
16
|
+
import { partnershipPeople, partnerships } from '../partnerships/schema.ts'
|
|
17
|
+
import { people } from '../people/schema.ts'
|
|
18
|
+
import { pipelineStages } from '../pipelines/schema.ts'
|
|
19
|
+
import { planItems } from '../plans/schema.ts'
|
|
20
|
+
import { positions } from '../positions/schema.ts'
|
|
21
|
+
import { raisePeople, raises } from '../raises/schema.ts'
|
|
22
|
+
import { parseMemberRole, roleAllows } from '../workspace/roles.ts'
|
|
23
|
+
import { SAMPLE_DATA_FIXTURE } from './fixture.ts'
|
|
24
|
+
import type { Fixture } from './fixture.ts'
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Installs the sample workspace, in one transaction.
|
|
28
|
+
*
|
|
29
|
+
* The whole seed is one write. A failure part-way through rolls the whole
|
|
30
|
+
* thing back, so a workspace never ends up with half of it.
|
|
31
|
+
*
|
|
32
|
+
* Idempotent by refusal: a workspace that already holds any companies or
|
|
33
|
+
* people gets a 409. Installing twice would double the fixture, which is not
|
|
34
|
+
* what an "install once" button says.
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
export interface SampleDataDependencies {
|
|
38
|
+
readonly db: Database
|
|
39
|
+
readonly transaction: TransactionScope
|
|
40
|
+
readonly createId: IdFactory
|
|
41
|
+
readonly now: () => Date
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface SampleDataCounts {
|
|
45
|
+
readonly companies: number
|
|
46
|
+
readonly people: number
|
|
47
|
+
readonly positions: number
|
|
48
|
+
readonly deals: number
|
|
49
|
+
readonly planItems: number
|
|
50
|
+
readonly notes: number
|
|
51
|
+
readonly opportunities: number
|
|
52
|
+
readonly raises: number
|
|
53
|
+
readonly partnerships: number
|
|
54
|
+
readonly roles: number
|
|
55
|
+
readonly candidates: number
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface SampleDataService {
|
|
59
|
+
install(actor: Actor, workspaceId: string): Promise<SampleDataCounts>
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function createSampleDataService(dependencies: SampleDataDependencies): SampleDataService {
|
|
63
|
+
async function requireAdmin(actor: Actor, workspaceId: string): Promise<void> {
|
|
64
|
+
if (actor.workspaceId !== workspaceId) {
|
|
65
|
+
throw AppError.notFound('Workspace not found')
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (actor.kind === 'api_key' && actor.userId === null) {
|
|
69
|
+
if (!roleAllows(actor.role, 'admin')) {
|
|
70
|
+
throw new AppError('forbidden', 'This action needs the admin role')
|
|
71
|
+
}
|
|
72
|
+
return
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const userId = actor.userId
|
|
76
|
+
|
|
77
|
+
if (userId === null) {
|
|
78
|
+
throw AppError.notFound('Workspace not found')
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const membership = await authRepository.findMembership(dependencies.db, workspaceId, userId)
|
|
82
|
+
|
|
83
|
+
if (membership === undefined) {
|
|
84
|
+
throw AppError.notFound('Workspace not found')
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const role = parseMemberRole(membership.role)
|
|
88
|
+
|
|
89
|
+
if (role === undefined) {
|
|
90
|
+
throw new Error(`workspace_members.role holds "${membership.role}", which its check forbids`)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (!roleAllows(role, 'admin')) {
|
|
94
|
+
throw new AppError('forbidden', 'This action needs the admin role')
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
async install(actor, workspaceId) {
|
|
100
|
+
// The path parameter must match the actor's workspace, and it must be an
|
|
101
|
+
// admin. `requireWorkspaceId` is what CRM endpoints call to reject an
|
|
102
|
+
// actor with no workspace at all.
|
|
103
|
+
requireWorkspaceId(actor)
|
|
104
|
+
await requireAdmin(actor, workspaceId)
|
|
105
|
+
|
|
106
|
+
const fixture = SAMPLE_DATA_FIXTURE
|
|
107
|
+
|
|
108
|
+
return dependencies.transaction(
|
|
109
|
+
async ({ tx }) => {
|
|
110
|
+
const [companiesTally] = await tx
|
|
111
|
+
.select({ value: count() })
|
|
112
|
+
.from(companies)
|
|
113
|
+
.where(eq(companies.workspaceId, workspaceId))
|
|
114
|
+
|
|
115
|
+
const [peopleTally] = await tx
|
|
116
|
+
.select({ value: count() })
|
|
117
|
+
.from(people)
|
|
118
|
+
.where(eq(people.workspaceId, workspaceId))
|
|
119
|
+
|
|
120
|
+
const alreadyHasData =
|
|
121
|
+
(companiesTally?.value ?? 0) > 0 || (peopleTally?.value ?? 0) > 0
|
|
122
|
+
|
|
123
|
+
if (alreadyHasData) {
|
|
124
|
+
throw AppError.conflict('This workspace already has CRM data')
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Every stage this workspace carries, grouped by kind so a deal, an
|
|
128
|
+
// opportunity, a raise and a partnership all resolve their fixture
|
|
129
|
+
// stage slugs against the same read.
|
|
130
|
+
const allStages = await tx
|
|
131
|
+
.select({
|
|
132
|
+
id: pipelineStages.id,
|
|
133
|
+
slug: pipelineStages.slug,
|
|
134
|
+
kind: pipelineStages.kind,
|
|
135
|
+
})
|
|
136
|
+
.from(pipelineStages)
|
|
137
|
+
.where(
|
|
138
|
+
and(
|
|
139
|
+
eq(pipelineStages.workspaceId, workspaceId),
|
|
140
|
+
inArray(pipelineStages.kind, ['deal', 'opportunity', 'raise', 'partnership']),
|
|
141
|
+
),
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
const stageIdByKindAndSlug = new Map<string, string>()
|
|
145
|
+
|
|
146
|
+
for (const row of allStages) {
|
|
147
|
+
stageIdByKindAndSlug.set(stageLookupKey(row.kind, row.slug), row.id)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const now = dependencies.now()
|
|
151
|
+
const companyIds = new Map<string, string>()
|
|
152
|
+
const personIds = new Map<string, string>()
|
|
153
|
+
const dealIds = new Map<string, string>()
|
|
154
|
+
const opportunityIds = new Map<string, string>()
|
|
155
|
+
const raiseIds = new Map<string, string>()
|
|
156
|
+
const partnershipIds = new Map<string, string>()
|
|
157
|
+
const roleIds = new Map<string, string>()
|
|
158
|
+
const candidateIds = new Map<string, string>()
|
|
159
|
+
|
|
160
|
+
for (const record of fixture.companies) {
|
|
161
|
+
const id = dependencies.createId('company')
|
|
162
|
+
companyIds.set(record.key, id)
|
|
163
|
+
|
|
164
|
+
await tx.insert(companies).values({
|
|
165
|
+
id,
|
|
166
|
+
workspaceId,
|
|
167
|
+
name: record.name,
|
|
168
|
+
domain: record.domain,
|
|
169
|
+
industry: record.industry,
|
|
170
|
+
description: record.description,
|
|
171
|
+
stage: record.stage,
|
|
172
|
+
sizeBand: record.sizeBand,
|
|
173
|
+
hq: record.hq,
|
|
174
|
+
website: record.website,
|
|
175
|
+
accountType: record.accountType,
|
|
176
|
+
icpFit: record.icpFit,
|
|
177
|
+
techStack: [...record.techStack],
|
|
178
|
+
summary: record.summary,
|
|
179
|
+
tags: [...record.tags],
|
|
180
|
+
createdAt: now,
|
|
181
|
+
updatedAt: now,
|
|
182
|
+
})
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
for (const record of fixture.people) {
|
|
186
|
+
const id = dependencies.createId('person')
|
|
187
|
+
personIds.set(record.key, id)
|
|
188
|
+
|
|
189
|
+
await tx.insert(people).values({
|
|
190
|
+
id,
|
|
191
|
+
workspaceId,
|
|
192
|
+
name: record.name,
|
|
193
|
+
email: record.email,
|
|
194
|
+
location: record.location,
|
|
195
|
+
preferredChannel: record.preferredChannel,
|
|
196
|
+
influence: record.influence,
|
|
197
|
+
relationship: record.relationship,
|
|
198
|
+
summary: record.summary,
|
|
199
|
+
tags: [...record.tags],
|
|
200
|
+
createdAt: now,
|
|
201
|
+
updatedAt: now,
|
|
202
|
+
})
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
for (const record of fixture.positions) {
|
|
206
|
+
const personId = personIds.get(record.personKey)
|
|
207
|
+
const companyId = companyIds.get(record.companyKey)
|
|
208
|
+
|
|
209
|
+
if (personId === undefined || companyId === undefined) {
|
|
210
|
+
throw new Error(
|
|
211
|
+
`Sample position links unknown keys: person=${record.personKey}, company=${record.companyKey}`,
|
|
212
|
+
)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
await tx.insert(positions).values({
|
|
216
|
+
id: dependencies.createId('position'),
|
|
217
|
+
workspaceId,
|
|
218
|
+
personId,
|
|
219
|
+
companyId,
|
|
220
|
+
title: record.title,
|
|
221
|
+
createdAt: now,
|
|
222
|
+
updatedAt: now,
|
|
223
|
+
})
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
for (const record of fixture.deals) {
|
|
227
|
+
const stageId = stageIdByKindAndSlug.get(stageLookupKey('deal', record.stageSlug))
|
|
228
|
+
const companyId = companyIds.get(record.companyKey)
|
|
229
|
+
|
|
230
|
+
if (stageId === undefined) {
|
|
231
|
+
throw new Error(
|
|
232
|
+
`Sample deal "${record.name}" names stage "${record.stageSlug}", which this workspace's deal pipeline does not carry`,
|
|
233
|
+
)
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (companyId === undefined) {
|
|
237
|
+
throw new Error(
|
|
238
|
+
`Sample deal "${record.name}" names unknown company "${record.companyKey}"`,
|
|
239
|
+
)
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const id = dependencies.createId('deal')
|
|
243
|
+
dealIds.set(record.key, id)
|
|
244
|
+
|
|
245
|
+
await tx.insert(deals).values({
|
|
246
|
+
id,
|
|
247
|
+
workspaceId,
|
|
248
|
+
name: record.name,
|
|
249
|
+
companyId,
|
|
250
|
+
stageId,
|
|
251
|
+
valueCents: record.valueCents,
|
|
252
|
+
currency: record.currency,
|
|
253
|
+
expectedClose: record.expectedClose,
|
|
254
|
+
competitors: [],
|
|
255
|
+
risks: record.risks,
|
|
256
|
+
whyWin: record.whyWin,
|
|
257
|
+
summary: record.summary,
|
|
258
|
+
tags: [...record.tags],
|
|
259
|
+
createdAt: now,
|
|
260
|
+
updatedAt: now,
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
for (const personKey of record.peopleKeys) {
|
|
264
|
+
const personId = personIds.get(personKey)
|
|
265
|
+
|
|
266
|
+
if (personId === undefined) {
|
|
267
|
+
throw new Error(
|
|
268
|
+
`Sample deal "${record.name}" names unknown person "${personKey}"`,
|
|
269
|
+
)
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
await tx.insert(dealPeople).values({ dealId: id, personId })
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
for (const record of fixture.opportunities) {
|
|
277
|
+
const stageId = stageIdByKindAndSlug.get(
|
|
278
|
+
stageLookupKey('opportunity', record.stageSlug),
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
if (stageId === undefined) {
|
|
282
|
+
throw new Error(
|
|
283
|
+
`Sample opportunity "${record.name}" names stage "${record.stageSlug}", which this workspace's opportunity pipeline does not carry`,
|
|
284
|
+
)
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const companyId =
|
|
288
|
+
record.companyKey === null ? null : (companyIds.get(record.companyKey) ?? undefined)
|
|
289
|
+
|
|
290
|
+
if (companyId === undefined) {
|
|
291
|
+
throw new Error(
|
|
292
|
+
`Sample opportunity "${record.name}" names unknown company "${String(record.companyKey)}"`,
|
|
293
|
+
)
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const id = dependencies.createId('opportunity')
|
|
297
|
+
opportunityIds.set(record.key, id)
|
|
298
|
+
|
|
299
|
+
await tx.insert(opportunities).values({
|
|
300
|
+
id,
|
|
301
|
+
workspaceId,
|
|
302
|
+
name: record.name,
|
|
303
|
+
kind: record.kind,
|
|
304
|
+
stageId,
|
|
305
|
+
companyId,
|
|
306
|
+
expectedClose: record.expectedClose,
|
|
307
|
+
summary: record.summary,
|
|
308
|
+
tags: [...record.tags],
|
|
309
|
+
createdAt: now,
|
|
310
|
+
updatedAt: now,
|
|
311
|
+
})
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
for (const record of fixture.raises) {
|
|
315
|
+
const stageId = stageIdByKindAndSlug.get(stageLookupKey('raise', record.stageSlug))
|
|
316
|
+
const companyId = companyIds.get(record.companyKey)
|
|
317
|
+
|
|
318
|
+
if (stageId === undefined) {
|
|
319
|
+
throw new Error(
|
|
320
|
+
`Sample raise "${record.name}" names stage "${record.stageSlug}", which this workspace's raise pipeline does not carry`,
|
|
321
|
+
)
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
if (companyId === undefined) {
|
|
325
|
+
throw new Error(
|
|
326
|
+
`Sample raise "${record.name}" names unknown company "${record.companyKey}"`,
|
|
327
|
+
)
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const id = dependencies.createId('raise')
|
|
331
|
+
raiseIds.set(record.key, id)
|
|
332
|
+
|
|
333
|
+
await tx.insert(raises).values({
|
|
334
|
+
id,
|
|
335
|
+
workspaceId,
|
|
336
|
+
name: record.name,
|
|
337
|
+
companyId,
|
|
338
|
+
stageId,
|
|
339
|
+
checkSizeCents: record.checkSizeCents,
|
|
340
|
+
currency: record.currency,
|
|
341
|
+
thesisFit: record.thesisFit,
|
|
342
|
+
expectedClose: record.expectedClose,
|
|
343
|
+
summary: record.summary,
|
|
344
|
+
tags: [...record.tags],
|
|
345
|
+
createdAt: now,
|
|
346
|
+
updatedAt: now,
|
|
347
|
+
})
|
|
348
|
+
|
|
349
|
+
for (const personKey of record.peopleKeys) {
|
|
350
|
+
const personId = personIds.get(personKey)
|
|
351
|
+
|
|
352
|
+
if (personId === undefined) {
|
|
353
|
+
throw new Error(
|
|
354
|
+
`Sample raise "${record.name}" names unknown person "${personKey}"`,
|
|
355
|
+
)
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
await tx.insert(raisePeople).values({ raiseId: id, personId })
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
for (const record of fixture.partnerships) {
|
|
363
|
+
const stageId = stageIdByKindAndSlug.get(
|
|
364
|
+
stageLookupKey('partnership', record.stageSlug),
|
|
365
|
+
)
|
|
366
|
+
const companyId = companyIds.get(record.companyKey)
|
|
367
|
+
|
|
368
|
+
if (stageId === undefined) {
|
|
369
|
+
throw new Error(
|
|
370
|
+
`Sample partnership "${record.name}" names stage "${record.stageSlug}", which this workspace's partnership pipeline does not carry`,
|
|
371
|
+
)
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
if (companyId === undefined) {
|
|
375
|
+
throw new Error(
|
|
376
|
+
`Sample partnership "${record.name}" names unknown company "${record.companyKey}"`,
|
|
377
|
+
)
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const id = dependencies.createId('partnership')
|
|
381
|
+
partnershipIds.set(record.key, id)
|
|
382
|
+
|
|
383
|
+
await tx.insert(partnerships).values({
|
|
384
|
+
id,
|
|
385
|
+
workspaceId,
|
|
386
|
+
name: record.name,
|
|
387
|
+
companyId,
|
|
388
|
+
stageId,
|
|
389
|
+
kind: record.kind,
|
|
390
|
+
nextTouchpoint: record.nextTouchpoint,
|
|
391
|
+
goals: record.goals,
|
|
392
|
+
successLooksLike: record.successLooksLike,
|
|
393
|
+
summary: record.summary,
|
|
394
|
+
tags: [...record.tags],
|
|
395
|
+
createdAt: now,
|
|
396
|
+
updatedAt: now,
|
|
397
|
+
})
|
|
398
|
+
|
|
399
|
+
for (const personKey of record.peopleKeys) {
|
|
400
|
+
const personId = personIds.get(personKey)
|
|
401
|
+
|
|
402
|
+
if (personId === undefined) {
|
|
403
|
+
throw new Error(
|
|
404
|
+
`Sample partnership "${record.name}" names unknown person "${personKey}"`,
|
|
405
|
+
)
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
await tx.insert(partnershipPeople).values({ partnershipId: id, personId })
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
for (const record of fixture.roles) {
|
|
413
|
+
const id = dependencies.createId('role')
|
|
414
|
+
roleIds.set(record.key, id)
|
|
415
|
+
|
|
416
|
+
await tx.insert(roles).values({
|
|
417
|
+
id,
|
|
418
|
+
workspaceId,
|
|
419
|
+
title: record.title,
|
|
420
|
+
status: record.status,
|
|
421
|
+
createdAt: now,
|
|
422
|
+
updatedAt: now,
|
|
423
|
+
})
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
for (const record of fixture.candidates) {
|
|
427
|
+
const roleId = roleIds.get(record.roleKey)
|
|
428
|
+
const personId = personIds.get(record.personKey)
|
|
429
|
+
const referrerPersonId =
|
|
430
|
+
record.referrerPersonKey === null
|
|
431
|
+
? null
|
|
432
|
+
: (personIds.get(record.referrerPersonKey) ?? undefined)
|
|
433
|
+
|
|
434
|
+
if (roleId === undefined) {
|
|
435
|
+
throw new Error(
|
|
436
|
+
`Sample candidate "${record.key}" names unknown role "${record.roleKey}"`,
|
|
437
|
+
)
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
if (personId === undefined) {
|
|
441
|
+
throw new Error(
|
|
442
|
+
`Sample candidate "${record.key}" names unknown person "${record.personKey}"`,
|
|
443
|
+
)
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
if (referrerPersonId === undefined) {
|
|
447
|
+
throw new Error(
|
|
448
|
+
`Sample candidate "${record.key}" names unknown referrer "${String(record.referrerPersonKey)}"`,
|
|
449
|
+
)
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
const id = dependencies.createId('candidate')
|
|
453
|
+
candidateIds.set(record.key, id)
|
|
454
|
+
|
|
455
|
+
await tx.insert(candidates).values({
|
|
456
|
+
id,
|
|
457
|
+
workspaceId,
|
|
458
|
+
roleId,
|
|
459
|
+
personId,
|
|
460
|
+
status: record.status,
|
|
461
|
+
// The interview_stage column carries a value only while the
|
|
462
|
+
// candidate is in process. Every other status clears it, which
|
|
463
|
+
// the service does the same way `hiring` does for a PATCH.
|
|
464
|
+
interviewStage: record.status === 'in_process' ? record.interviewStage : null,
|
|
465
|
+
referrerPersonId,
|
|
466
|
+
createdAt: now,
|
|
467
|
+
updatedAt: now,
|
|
468
|
+
})
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
for (const record of fixture.plans) {
|
|
472
|
+
const dealId = dealIds.get(record.targetDealKey)
|
|
473
|
+
|
|
474
|
+
if (dealId === undefined) {
|
|
475
|
+
throw new Error(
|
|
476
|
+
`Sample plan item "${record.title}" names unknown deal "${record.targetDealKey}"`,
|
|
477
|
+
)
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
await tx.insert(planItems).values({
|
|
481
|
+
id: dependencies.createId('planItem'),
|
|
482
|
+
workspaceId,
|
|
483
|
+
targetType: record.targetType,
|
|
484
|
+
targetId: dealId,
|
|
485
|
+
date: record.date,
|
|
486
|
+
title: record.title,
|
|
487
|
+
status: record.status,
|
|
488
|
+
createdAt: now,
|
|
489
|
+
updatedAt: now,
|
|
490
|
+
})
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
for (const record of fixture.notes) {
|
|
494
|
+
const targetId = resolveNoteTargetId(record.targetType, record.targetKey, {
|
|
495
|
+
companyIds,
|
|
496
|
+
personIds,
|
|
497
|
+
dealIds,
|
|
498
|
+
opportunityIds,
|
|
499
|
+
raiseIds,
|
|
500
|
+
partnershipIds,
|
|
501
|
+
candidateIds,
|
|
502
|
+
})
|
|
503
|
+
|
|
504
|
+
if (targetId === undefined) {
|
|
505
|
+
throw new Error(
|
|
506
|
+
`Sample note names unknown ${record.targetType} "${record.targetKey}"`,
|
|
507
|
+
)
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
await tx.insert(notes).values({
|
|
511
|
+
id: dependencies.createId('note'),
|
|
512
|
+
workspaceId,
|
|
513
|
+
targetType: record.targetType,
|
|
514
|
+
targetId,
|
|
515
|
+
body: record.body,
|
|
516
|
+
pinned: record.pinned,
|
|
517
|
+
createdAt: now,
|
|
518
|
+
updatedAt: now,
|
|
519
|
+
})
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
return countsFor(fixture)
|
|
523
|
+
},
|
|
524
|
+
{ workspaceId, actor: toEventActor(actor) },
|
|
525
|
+
)
|
|
526
|
+
},
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
interface KeyMaps {
|
|
531
|
+
readonly companyIds: ReadonlyMap<string, string>
|
|
532
|
+
readonly personIds: ReadonlyMap<string, string>
|
|
533
|
+
readonly dealIds: ReadonlyMap<string, string>
|
|
534
|
+
readonly opportunityIds: ReadonlyMap<string, string>
|
|
535
|
+
readonly raiseIds: ReadonlyMap<string, string>
|
|
536
|
+
readonly partnershipIds: ReadonlyMap<string, string>
|
|
537
|
+
readonly candidateIds: ReadonlyMap<string, string>
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
function resolveNoteTargetId(
|
|
541
|
+
targetType: string,
|
|
542
|
+
targetKey: string,
|
|
543
|
+
maps: KeyMaps,
|
|
544
|
+
): string | undefined {
|
|
545
|
+
switch (targetType) {
|
|
546
|
+
case 'company':
|
|
547
|
+
return maps.companyIds.get(targetKey)
|
|
548
|
+
case 'person':
|
|
549
|
+
return maps.personIds.get(targetKey)
|
|
550
|
+
case 'deal':
|
|
551
|
+
return maps.dealIds.get(targetKey)
|
|
552
|
+
case 'opportunity':
|
|
553
|
+
return maps.opportunityIds.get(targetKey)
|
|
554
|
+
case 'raise':
|
|
555
|
+
return maps.raiseIds.get(targetKey)
|
|
556
|
+
case 'partnership':
|
|
557
|
+
return maps.partnershipIds.get(targetKey)
|
|
558
|
+
case 'candidate':
|
|
559
|
+
return maps.candidateIds.get(targetKey)
|
|
560
|
+
default:
|
|
561
|
+
// A new RECORD_TARGET_TYPES value in the fixture requires a case here;
|
|
562
|
+
// falling through silently would drop a note on the floor.
|
|
563
|
+
return undefined
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
/**
|
|
568
|
+
* The lookup key `stageIdByKindAndSlug` uses. A slug is only unique per kind
|
|
569
|
+
* (`won` is a stage in both `deal` and `opportunity`), so the map is keyed by
|
|
570
|
+
* both fields joined.
|
|
571
|
+
*/
|
|
572
|
+
function stageLookupKey(kind: string, slug: string): string {
|
|
573
|
+
return `${kind}::${slug}`
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
function countsFor(fixture: Fixture): SampleDataCounts {
|
|
577
|
+
return {
|
|
578
|
+
companies: fixture.companies.length,
|
|
579
|
+
people: fixture.people.length,
|
|
580
|
+
positions: fixture.positions.length,
|
|
581
|
+
deals: fixture.deals.length,
|
|
582
|
+
planItems: fixture.plans.length,
|
|
583
|
+
notes: fixture.notes.length,
|
|
584
|
+
opportunities: fixture.opportunities.length,
|
|
585
|
+
raises: fixture.raises.length,
|
|
586
|
+
partnerships: fixture.partnerships.length,
|
|
587
|
+
roles: fixture.roles.length,
|
|
588
|
+
candidates: fixture.candidates.length,
|
|
589
|
+
}
|
|
590
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
|
|
3
|
+
import { requireWorkspaceId } from '../auth/actor.ts'
|
|
4
|
+
import type { McpToolRegistry } from '../../runtime/module.ts'
|
|
5
|
+
import { sampleDataResponse } from './routes.ts'
|
|
6
|
+
import type { SampleDataService } from './service.ts'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* `sample_data_install`: the MCP mirror of `POST /v1/workspaces/:id/sample-data`.
|
|
10
|
+
*
|
|
11
|
+
* Same admin check, same conflict on a workspace that already has data. The
|
|
12
|
+
* tool takes no arguments: it installs into the actor's own workspace.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const installArgs = z.strictObject({})
|
|
16
|
+
|
|
17
|
+
export function registerSampleDataTools(
|
|
18
|
+
mcp: McpToolRegistry,
|
|
19
|
+
service: SampleDataService,
|
|
20
|
+
): void {
|
|
21
|
+
mcp.tool({
|
|
22
|
+
name: 'sample_data_install',
|
|
23
|
+
description:
|
|
24
|
+
'Populate this workspace with a small sample of companies, people, positions, deals, plans and notes. Refuses if the workspace already has data.',
|
|
25
|
+
inputSchema: installArgs,
|
|
26
|
+
async invoke(_args, actor) {
|
|
27
|
+
const counts = await service.install(actor, requireWorkspaceId(actor))
|
|
28
|
+
|
|
29
|
+
return sampleDataResponse(counts)
|
|
30
|
+
},
|
|
31
|
+
})
|
|
32
|
+
}
|
|
@@ -44,7 +44,13 @@ export type SmtpEmailConfig = z.infer<typeof smtpEmailConfigSchema>
|
|
|
44
44
|
* sender is verifiable without a container running a real SMTP conversation.
|
|
45
45
|
*/
|
|
46
46
|
export interface SmtpTransport {
|
|
47
|
-
sendMail(message: {
|
|
47
|
+
sendMail(message: {
|
|
48
|
+
from: string
|
|
49
|
+
to: string
|
|
50
|
+
subject: string
|
|
51
|
+
text: string
|
|
52
|
+
html?: string
|
|
53
|
+
}): Promise<unknown>
|
|
48
54
|
}
|
|
49
55
|
|
|
50
56
|
function createNodemailerTransport(config: SmtpEmailConfig): SmtpTransport {
|
|
@@ -70,6 +76,7 @@ export function createSmtpEmailSender(
|
|
|
70
76
|
to: message.to,
|
|
71
77
|
subject: message.subject,
|
|
72
78
|
text: message.body,
|
|
79
|
+
...(message.html === undefined ? {} : { html: message.html }),
|
|
73
80
|
})
|
|
74
81
|
} catch (cause) {
|
|
75
82
|
const reason = describeThrown(cause)
|