@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
|
@@ -11,7 +11,7 @@ import * as dealRepository from '../deals/repository.ts'
|
|
|
11
11
|
import * as peopleRepository from '../people/repository.ts'
|
|
12
12
|
import * as positionRepository from '../positions/repository.ts'
|
|
13
13
|
import { IMPORT_DEAL_CURRENCY, NEW_COMPANY_DEFAULTS, NEW_PERSON_DEFAULTS } from './drafts.ts'
|
|
14
|
-
import type { ImportWrite, RowPlan } from './plan.ts'
|
|
14
|
+
import type { ImportWrite, PlannedAffiliation, RowPlan } from './plan.ts'
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
17
|
* Applying one planned row: the insert or update, and the timeline entry that
|
|
@@ -34,6 +34,21 @@ export interface WriteDependencies {
|
|
|
34
34
|
readonly sourceName: string
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
/**
|
|
38
|
+
* A record a row wrote besides its primary one.
|
|
39
|
+
*
|
|
40
|
+
* A People row with an affiliation makes a Position, and under
|
|
41
|
+
* `on_missing_company: create` a Company as well. Each needs its own module
|
|
42
|
+
* event, which the service emits from these.
|
|
43
|
+
*/
|
|
44
|
+
export interface SideRecord {
|
|
45
|
+
readonly objectType: RecordObjectType
|
|
46
|
+
readonly recordId: string
|
|
47
|
+
readonly created: boolean
|
|
48
|
+
/** The columns an update moved. Empty on a create. */
|
|
49
|
+
readonly changedFields: readonly string[]
|
|
50
|
+
}
|
|
51
|
+
|
|
37
52
|
export interface WriteOutcome {
|
|
38
53
|
readonly recordId: string
|
|
39
54
|
readonly objectType: RecordObjectType
|
|
@@ -43,6 +58,8 @@ export interface WriteOutcome {
|
|
|
43
58
|
* nothing at all.
|
|
44
59
|
*/
|
|
45
60
|
readonly changedFields: readonly string[]
|
|
61
|
+
/** Records the row wrote besides its primary one: a People affiliation's company and position. */
|
|
62
|
+
readonly sideRecords?: readonly SideRecord[]
|
|
46
63
|
}
|
|
47
64
|
|
|
48
65
|
/** A create or update plan. `skip` and `error` never reach here. */
|
|
@@ -102,6 +119,92 @@ async function writeCompany(
|
|
|
102
119
|
return { recordId: id, objectType: 'company', changedFields }
|
|
103
120
|
}
|
|
104
121
|
|
|
122
|
+
/**
|
|
123
|
+
* Writes the position a People row named, and the company it belongs to when the
|
|
124
|
+
* job was told to create one.
|
|
125
|
+
*
|
|
126
|
+
* The company is created before the position so the link has something to point
|
|
127
|
+
* at. The position is renamed in place when the person already holds one at that
|
|
128
|
+
* company, and created otherwise, which is the two-column-key behaviour the
|
|
129
|
+
* People affiliation chose. Files the `created Company` and `linked to company`
|
|
130
|
+
* activities as it goes, the same sentences the Companies and Positions imports
|
|
131
|
+
* write.
|
|
132
|
+
*/
|
|
133
|
+
async function applyAffiliation(
|
|
134
|
+
dependencies: WriteDependencies,
|
|
135
|
+
personId: string,
|
|
136
|
+
affiliation: PlannedAffiliation,
|
|
137
|
+
): Promise<readonly SideRecord[]> {
|
|
138
|
+
const { tx, workspaceId, actor, sourceName } = dependencies
|
|
139
|
+
const sideRecords: SideRecord[] = []
|
|
140
|
+
|
|
141
|
+
const companyId = await (async (): Promise<string> => {
|
|
142
|
+
if (affiliation.kind === 'link') {
|
|
143
|
+
return affiliation.companyId
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const created = await companyRepository.insertCompany(tx, {
|
|
147
|
+
...NEW_COMPANY_DEFAULTS,
|
|
148
|
+
...affiliation.company,
|
|
149
|
+
tags: affiliation.company.tags === undefined ? undefined : [...affiliation.company.tags],
|
|
150
|
+
id: dependencies.createId('company'),
|
|
151
|
+
workspaceId,
|
|
152
|
+
name: affiliation.company.name ?? '',
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
sideRecords.push({ objectType: 'company', recordId: created.id, created: true, changedFields: [] })
|
|
156
|
+
await dependencies.recordActivity(tx, workspaceId, actor, {
|
|
157
|
+
targetType: 'company',
|
|
158
|
+
targetId: created.id,
|
|
159
|
+
kind: 'created',
|
|
160
|
+
...describeCreationVia('Company', sourceName),
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
return created.id
|
|
164
|
+
})()
|
|
165
|
+
|
|
166
|
+
const held = await positionRepository.listPositionsAt(tx, workspaceId, personId, companyId)
|
|
167
|
+
const current = held[0]
|
|
168
|
+
|
|
169
|
+
if (current === undefined) {
|
|
170
|
+
const created = await positionRepository.insertPosition(tx, {
|
|
171
|
+
id: dependencies.createId('position'),
|
|
172
|
+
workspaceId,
|
|
173
|
+
personId,
|
|
174
|
+
companyId,
|
|
175
|
+
title: affiliation.title,
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
sideRecords.push({ objectType: 'position', recordId: created.id, created: true, changedFields: [] })
|
|
179
|
+
|
|
180
|
+
for (const end of [
|
|
181
|
+
{ targetType: 'person', targetId: personId, related: 'company' },
|
|
182
|
+
{ targetType: 'company', targetId: companyId, related: 'person' },
|
|
183
|
+
] as const) {
|
|
184
|
+
await dependencies.recordActivity(tx, workspaceId, actor, {
|
|
185
|
+
targetType: end.targetType,
|
|
186
|
+
targetId: end.targetId,
|
|
187
|
+
kind: 'linked',
|
|
188
|
+
...describeLink(end.related, sourceName),
|
|
189
|
+
})
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return sideRecords
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const changedFields = changedKeys(current, { title: affiliation.title })
|
|
196
|
+
|
|
197
|
+
if (changedFields.length > 0) {
|
|
198
|
+
await positionRepository.updatePosition(tx, workspaceId, current.id, {
|
|
199
|
+
title: affiliation.title,
|
|
200
|
+
updatedAt: dependencies.now,
|
|
201
|
+
})
|
|
202
|
+
sideRecords.push({ objectType: 'position', recordId: current.id, created: false, changedFields })
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return sideRecords
|
|
206
|
+
}
|
|
207
|
+
|
|
105
208
|
async function writePerson(
|
|
106
209
|
dependencies: WriteDependencies,
|
|
107
210
|
plan: WritablePlan,
|
|
@@ -120,7 +223,12 @@ async function writePerson(
|
|
|
120
223
|
name: write.draft.name ?? '',
|
|
121
224
|
})
|
|
122
225
|
|
|
123
|
-
|
|
226
|
+
const sideRecords =
|
|
227
|
+
write.affiliation === undefined
|
|
228
|
+
? []
|
|
229
|
+
: await applyAffiliation(dependencies, created.id, write.affiliation)
|
|
230
|
+
|
|
231
|
+
return { recordId: created.id, objectType: 'person', changedFields: [], sideRecords }
|
|
124
232
|
}
|
|
125
233
|
|
|
126
234
|
const id = requireTarget(plan)
|
|
@@ -144,7 +252,12 @@ async function writePerson(
|
|
|
144
252
|
})
|
|
145
253
|
}
|
|
146
254
|
|
|
147
|
-
|
|
255
|
+
const sideRecords =
|
|
256
|
+
write.affiliation === undefined
|
|
257
|
+
? []
|
|
258
|
+
: await applyAffiliation(dependencies, id, write.affiliation)
|
|
259
|
+
|
|
260
|
+
return { recordId: id, objectType: 'person', changedFields, sideRecords }
|
|
148
261
|
}
|
|
149
262
|
|
|
150
263
|
/**
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
|
|
3
|
+
import type { ModuleEventCatalog } from '../../runtime/module.ts'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Domain events published by the lists module.
|
|
7
|
+
*
|
|
8
|
+
* `target` on a `lists.list.*` event is the list itself. `lists.member.*` events
|
|
9
|
+
* name the record that changed membership, so a consumer watching records hears
|
|
10
|
+
* about a list touching it without a second lookup.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export const listsEvents = {
|
|
14
|
+
'lists.list.created': z.object({}).strict(),
|
|
15
|
+
'lists.list.updated': z.object({ changed: z.array(z.string()).readonly() }),
|
|
16
|
+
'lists.list.deleted': z.object({}).strict(),
|
|
17
|
+
'lists.member.added': z.object({ listId: z.string() }),
|
|
18
|
+
'lists.member.removed': z.object({ listId: z.string() }),
|
|
19
|
+
} satisfies ModuleEventCatalog
|
|
20
|
+
|
|
21
|
+
export type ListCreatedData = Record<string, never>
|
|
22
|
+
export interface ListUpdatedData {
|
|
23
|
+
readonly changed: readonly string[]
|
|
24
|
+
}
|
|
25
|
+
export type ListDeletedData = Record<string, never>
|
|
26
|
+
export interface ListMemberAddedData {
|
|
27
|
+
readonly listId: string
|
|
28
|
+
}
|
|
29
|
+
export interface ListMemberRemovedData {
|
|
30
|
+
readonly listId: string
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
declare module '../../runtime/events.ts' {
|
|
34
|
+
interface KelpieEventMap {
|
|
35
|
+
'lists.list.created': ListCreatedData
|
|
36
|
+
'lists.list.updated': ListUpdatedData
|
|
37
|
+
'lists.list.deleted': ListDeletedData
|
|
38
|
+
'lists.member.added': ListMemberAddedData
|
|
39
|
+
'lists.member.removed': ListMemberRemovedData
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { KelpieModule } from '../../runtime/module.ts'
|
|
2
|
+
import { listsEvents } from './events.ts'
|
|
3
|
+
import { mountListsRoutes } from './routes.ts'
|
|
4
|
+
import * as schema from './schema.ts'
|
|
5
|
+
import { createListsService } from './service.ts'
|
|
6
|
+
import { registerListsTools } from './tools.ts'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Lists: named collections of records of one type.
|
|
10
|
+
*
|
|
11
|
+
* Requires `workspace` for the tenancy column. Every other record type is
|
|
12
|
+
* addressed polymorphically through `target_type` + `target_id`, so this module
|
|
13
|
+
* does not depend on the modules whose records it references.
|
|
14
|
+
*/
|
|
15
|
+
export function createListsModule(migrationsDirectory: string): KelpieModule {
|
|
16
|
+
return {
|
|
17
|
+
id: 'lists',
|
|
18
|
+
requires: ['workspace'],
|
|
19
|
+
structural: true,
|
|
20
|
+
events: listsEvents,
|
|
21
|
+
|
|
22
|
+
register(context) {
|
|
23
|
+
const service = createListsService({
|
|
24
|
+
db: context.db,
|
|
25
|
+
transaction: context.transaction,
|
|
26
|
+
createId: context.createId,
|
|
27
|
+
now: context.now,
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
context.schema(schema, migrationsDirectory)
|
|
31
|
+
|
|
32
|
+
context.routes((router) => {
|
|
33
|
+
mountListsRoutes(router, { db: context.db, now: context.now, service })
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
registerListsTools(context.mcp, service)
|
|
37
|
+
|
|
38
|
+
return Promise.resolve()
|
|
39
|
+
},
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
import { and, eq, ilike, inArray, sql } from 'drizzle-orm'
|
|
2
|
+
import type { SQL } from 'drizzle-orm'
|
|
3
|
+
|
|
4
|
+
import { keysetCondition, orderByWindow, textSort, timestampSort } from '../../lib/pagination.ts'
|
|
5
|
+
import type { ListWindow, SortableFields } from '../../lib/pagination.ts'
|
|
6
|
+
import { containsPattern } from '../../lib/search.ts'
|
|
7
|
+
import type { Queryable } from '../../runtime/transaction.ts'
|
|
8
|
+
import type { RecordTargetType } from '../recordTargets.ts'
|
|
9
|
+
import { listMembers, lists } from './schema.ts'
|
|
10
|
+
|
|
11
|
+
export type ListRecord = typeof lists.$inferSelect
|
|
12
|
+
export type ListColumns = typeof lists.$inferInsert
|
|
13
|
+
|
|
14
|
+
export type ListMemberRecord = typeof listMembers.$inferSelect
|
|
15
|
+
export type ListMemberColumns = typeof listMembers.$inferInsert
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The stored row with the rollup a UI index page needs. `member_count` is
|
|
19
|
+
* computed at read time rather than maintained on write so an unrelated bug
|
|
20
|
+
* cannot leave the count out of step with the actual rows.
|
|
21
|
+
*/
|
|
22
|
+
export type ListWithCount = ListRecord & { readonly memberCount: number }
|
|
23
|
+
|
|
24
|
+
export const LIST_SORTS: SortableFields<ListWithCount> = {
|
|
25
|
+
name: textSort(lists.name, (list) => list.name),
|
|
26
|
+
created_at: timestampSort(lists.createdAt, (list) => list.createdAt),
|
|
27
|
+
updated_at: timestampSort(lists.updatedAt, (list) => list.updatedAt),
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const DEFAULT_LIST_SORT = '-updated_at'
|
|
31
|
+
|
|
32
|
+
export interface ListFilters {
|
|
33
|
+
readonly term?: string | undefined
|
|
34
|
+
readonly targetType?: RecordTargetType | undefined
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function conditionsFor(workspaceId: string, filters: ListFilters): (SQL | undefined)[] {
|
|
38
|
+
const term = filters.term
|
|
39
|
+
const targetType = filters.targetType
|
|
40
|
+
|
|
41
|
+
return [
|
|
42
|
+
eq(lists.workspaceId, workspaceId),
|
|
43
|
+
term === undefined ? undefined : ilike(lists.name, containsPattern(term)),
|
|
44
|
+
targetType === undefined ? undefined : eq(lists.targetType, targetType),
|
|
45
|
+
]
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* @returns Up to `window.fetchLimit` rows: one more than the page, so the
|
|
50
|
+
* caller can tell there is a next one. Each row carries the count of members
|
|
51
|
+
* attached to it.
|
|
52
|
+
*/
|
|
53
|
+
export async function listLists(
|
|
54
|
+
db: Queryable,
|
|
55
|
+
workspaceId: string,
|
|
56
|
+
filters: ListFilters,
|
|
57
|
+
window: ListWindow<ListWithCount>,
|
|
58
|
+
): Promise<ListWithCount[]> {
|
|
59
|
+
const rows = await db
|
|
60
|
+
.select()
|
|
61
|
+
.from(lists)
|
|
62
|
+
.where(and(...conditionsFor(workspaceId, filters), keysetCondition(window, lists.id)))
|
|
63
|
+
.orderBy(...orderByWindow(window, lists.id))
|
|
64
|
+
.limit(window.fetchLimit)
|
|
65
|
+
|
|
66
|
+
const counts = await countMembersFor(db, rows.map((row) => row.id))
|
|
67
|
+
|
|
68
|
+
return rows.map((row) => ({ ...row, memberCount: counts.get(row.id) ?? 0 }))
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function countMembersFor(
|
|
72
|
+
db: Queryable,
|
|
73
|
+
listIds: readonly string[],
|
|
74
|
+
): Promise<ReadonlyMap<string, number>> {
|
|
75
|
+
if (listIds.length === 0) {
|
|
76
|
+
return new Map()
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const rows = await db
|
|
80
|
+
.select({ listId: listMembers.listId, count: sql<number>`count(*)::int` })
|
|
81
|
+
.from(listMembers)
|
|
82
|
+
.where(inArray(listMembers.listId, [...listIds]))
|
|
83
|
+
.groupBy(listMembers.listId)
|
|
84
|
+
|
|
85
|
+
return new Map(rows.map((row) => [row.listId, Number(row.count)]))
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export async function findList(
|
|
89
|
+
db: Queryable,
|
|
90
|
+
workspaceId: string,
|
|
91
|
+
id: string,
|
|
92
|
+
): Promise<ListWithCount | undefined> {
|
|
93
|
+
const [found] = await db
|
|
94
|
+
.select()
|
|
95
|
+
.from(lists)
|
|
96
|
+
.where(and(eq(lists.workspaceId, workspaceId), eq(lists.id, id)))
|
|
97
|
+
.limit(1)
|
|
98
|
+
|
|
99
|
+
if (found === undefined) {
|
|
100
|
+
return undefined
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return { ...found, memberCount: await countMembers(db, id) }
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async function countMembers(db: Queryable, listId: string): Promise<number> {
|
|
107
|
+
const [row] = await db
|
|
108
|
+
.select({ count: sql<number>`count(*)::int` })
|
|
109
|
+
.from(listMembers)
|
|
110
|
+
.where(eq(listMembers.listId, listId))
|
|
111
|
+
|
|
112
|
+
return Number(row?.count ?? 0)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export async function insertList(db: Queryable, values: ListColumns): Promise<ListRecord> {
|
|
116
|
+
const [created] = await db.insert(lists).values(values).returning()
|
|
117
|
+
|
|
118
|
+
if (created === undefined) {
|
|
119
|
+
throw new Error(`Inserting list ${values.id} returned no row`)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return created
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export async function updateList(
|
|
126
|
+
db: Queryable,
|
|
127
|
+
workspaceId: string,
|
|
128
|
+
id: string,
|
|
129
|
+
changes: Partial<ListColumns>,
|
|
130
|
+
): Promise<ListRecord | undefined> {
|
|
131
|
+
const [updated] = await db
|
|
132
|
+
.update(lists)
|
|
133
|
+
.set(changes)
|
|
134
|
+
.where(and(eq(lists.workspaceId, workspaceId), eq(lists.id, id)))
|
|
135
|
+
.returning()
|
|
136
|
+
|
|
137
|
+
return updated
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export async function deleteList(db: Queryable, workspaceId: string, id: string): Promise<number> {
|
|
141
|
+
const deleted = await db
|
|
142
|
+
.delete(lists)
|
|
143
|
+
.where(and(eq(lists.workspaceId, workspaceId), eq(lists.id, id)))
|
|
144
|
+
.returning({ id: lists.id })
|
|
145
|
+
|
|
146
|
+
return deleted.length
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** Members: no cursor pagination yet. A list's members render on one page today. */
|
|
150
|
+
|
|
151
|
+
export const LIST_MEMBER_SORTS: SortableFields<ListMemberRecord> = {
|
|
152
|
+
added_at: timestampSort(listMembers.addedAt, (row) => row.addedAt),
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export const DEFAULT_LIST_MEMBER_SORT = '-added_at'
|
|
156
|
+
|
|
157
|
+
export interface ListMemberFilters {
|
|
158
|
+
readonly listId: string
|
|
159
|
+
readonly targetType?: RecordTargetType | undefined
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function memberConditionsFor(
|
|
163
|
+
workspaceId: string,
|
|
164
|
+
filters: ListMemberFilters,
|
|
165
|
+
): (SQL | undefined)[] {
|
|
166
|
+
const targetType = filters.targetType
|
|
167
|
+
|
|
168
|
+
return [
|
|
169
|
+
eq(listMembers.workspaceId, workspaceId),
|
|
170
|
+
eq(listMembers.listId, filters.listId),
|
|
171
|
+
targetType === undefined ? undefined : eq(listMembers.targetType, targetType),
|
|
172
|
+
]
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function listListMembers(
|
|
176
|
+
db: Queryable,
|
|
177
|
+
workspaceId: string,
|
|
178
|
+
filters: ListMemberFilters,
|
|
179
|
+
window: ListWindow<ListMemberRecord>,
|
|
180
|
+
): Promise<ListMemberRecord[]> {
|
|
181
|
+
return db
|
|
182
|
+
.select()
|
|
183
|
+
.from(listMembers)
|
|
184
|
+
.where(and(...memberConditionsFor(workspaceId, filters), keysetCondition(window, listMembers.id)))
|
|
185
|
+
.orderBy(...orderByWindow(window, listMembers.id))
|
|
186
|
+
.limit(window.fetchLimit)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export async function findListMember(
|
|
190
|
+
db: Queryable,
|
|
191
|
+
workspaceId: string,
|
|
192
|
+
listId: string,
|
|
193
|
+
id: string,
|
|
194
|
+
): Promise<ListMemberRecord | undefined> {
|
|
195
|
+
const [found] = await db
|
|
196
|
+
.select()
|
|
197
|
+
.from(listMembers)
|
|
198
|
+
.where(
|
|
199
|
+
and(
|
|
200
|
+
eq(listMembers.workspaceId, workspaceId),
|
|
201
|
+
eq(listMembers.listId, listId),
|
|
202
|
+
eq(listMembers.id, id),
|
|
203
|
+
),
|
|
204
|
+
)
|
|
205
|
+
.limit(1)
|
|
206
|
+
|
|
207
|
+
return found
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export async function insertListMember(
|
|
211
|
+
db: Queryable,
|
|
212
|
+
values: ListMemberColumns,
|
|
213
|
+
): Promise<ListMemberRecord> {
|
|
214
|
+
const [created] = await db.insert(listMembers).values(values).returning()
|
|
215
|
+
|
|
216
|
+
if (created === undefined) {
|
|
217
|
+
throw new Error(`Inserting list_member ${values.id} returned no row`)
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return created
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export async function deleteListMember(
|
|
224
|
+
db: Queryable,
|
|
225
|
+
workspaceId: string,
|
|
226
|
+
listId: string,
|
|
227
|
+
id: string,
|
|
228
|
+
): Promise<number> {
|
|
229
|
+
const deleted = await db
|
|
230
|
+
.delete(listMembers)
|
|
231
|
+
.where(
|
|
232
|
+
and(
|
|
233
|
+
eq(listMembers.workspaceId, workspaceId),
|
|
234
|
+
eq(listMembers.listId, listId),
|
|
235
|
+
eq(listMembers.id, id),
|
|
236
|
+
),
|
|
237
|
+
)
|
|
238
|
+
.returning({ id: listMembers.id })
|
|
239
|
+
|
|
240
|
+
return deleted.length
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/** A membership row enriched with the list it points at. */
|
|
244
|
+
export interface MembershipWithList {
|
|
245
|
+
readonly id: string
|
|
246
|
+
readonly listId: string
|
|
247
|
+
readonly listName: string
|
|
248
|
+
readonly listTargetType: string
|
|
249
|
+
readonly targetType: string
|
|
250
|
+
readonly targetId: string
|
|
251
|
+
readonly addedAt: Date
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* "Which lists is this record on?"
|
|
256
|
+
*
|
|
257
|
+
* Ordered by list name so a record's Lists tab reads alphabetically and the
|
|
258
|
+
* order does not shift as memberships are added.
|
|
259
|
+
*/
|
|
260
|
+
export async function listMembershipsForTarget(
|
|
261
|
+
db: Queryable,
|
|
262
|
+
workspaceId: string,
|
|
263
|
+
targetType: string,
|
|
264
|
+
targetId: string,
|
|
265
|
+
): Promise<MembershipWithList[]> {
|
|
266
|
+
return db
|
|
267
|
+
.select({
|
|
268
|
+
id: listMembers.id,
|
|
269
|
+
listId: listMembers.listId,
|
|
270
|
+
listName: lists.name,
|
|
271
|
+
listTargetType: lists.targetType,
|
|
272
|
+
targetType: listMembers.targetType,
|
|
273
|
+
targetId: listMembers.targetId,
|
|
274
|
+
addedAt: listMembers.addedAt,
|
|
275
|
+
})
|
|
276
|
+
.from(listMembers)
|
|
277
|
+
.innerJoin(lists, eq(listMembers.listId, lists.id))
|
|
278
|
+
.where(
|
|
279
|
+
and(
|
|
280
|
+
eq(listMembers.workspaceId, workspaceId),
|
|
281
|
+
eq(listMembers.targetType, targetType),
|
|
282
|
+
eq(listMembers.targetId, targetId),
|
|
283
|
+
),
|
|
284
|
+
)
|
|
285
|
+
.orderBy(lists.name)
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Removes every list membership for one target.
|
|
290
|
+
*
|
|
291
|
+
* The target is polymorphic and carries no foreign key of its own, so nothing
|
|
292
|
+
* in the database deletes these when the target goes. The service that deletes
|
|
293
|
+
* the target calls this inside the same transaction.
|
|
294
|
+
*/
|
|
295
|
+
export async function deleteMembershipsForTarget(
|
|
296
|
+
db: Queryable,
|
|
297
|
+
workspaceId: string,
|
|
298
|
+
targetType: string,
|
|
299
|
+
targetId: string,
|
|
300
|
+
): Promise<number> {
|
|
301
|
+
const deleted = await db
|
|
302
|
+
.delete(listMembers)
|
|
303
|
+
.where(
|
|
304
|
+
and(
|
|
305
|
+
eq(listMembers.workspaceId, workspaceId),
|
|
306
|
+
eq(listMembers.targetType, targetType),
|
|
307
|
+
eq(listMembers.targetId, targetId),
|
|
308
|
+
),
|
|
309
|
+
)
|
|
310
|
+
.returning({ id: listMembers.id })
|
|
311
|
+
|
|
312
|
+
return deleted.length
|
|
313
|
+
}
|