@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
|
@@ -134,6 +134,41 @@
|
|
|
134
134
|
"when": 1786656430307,
|
|
135
135
|
"tag": "0018_hard_mystique",
|
|
136
136
|
"breakpoints": true
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
"idx": 19,
|
|
140
|
+
"version": "7",
|
|
141
|
+
"when": 1787446086593,
|
|
142
|
+
"tag": "0019_sharp_red_skull",
|
|
143
|
+
"breakpoints": true
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
"idx": 20,
|
|
147
|
+
"version": "7",
|
|
148
|
+
"when": 1787447911296,
|
|
149
|
+
"tag": "0020_early_nemesis",
|
|
150
|
+
"breakpoints": true
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
"idx": 21,
|
|
154
|
+
"version": "7",
|
|
155
|
+
"when": 1787527717966,
|
|
156
|
+
"tag": "0021_certain_maggott",
|
|
157
|
+
"breakpoints": true
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
"idx": 22,
|
|
161
|
+
"version": "7",
|
|
162
|
+
"when": 1787626585359,
|
|
163
|
+
"tag": "0022_stormy_manta",
|
|
164
|
+
"breakpoints": true
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
"idx": 23,
|
|
168
|
+
"version": "7",
|
|
169
|
+
"when": 1787651539093,
|
|
170
|
+
"tag": "0023_funny_maddog",
|
|
171
|
+
"breakpoints": true
|
|
137
172
|
}
|
|
138
173
|
]
|
|
139
174
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kelpie/server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "The Kelpie service as a library: module runtime, core CRM modules, REST API, MCP surface, and the shared migration pipeline.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"kelpie",
|
|
@@ -52,11 +52,12 @@
|
|
|
52
52
|
},
|
|
53
53
|
"dependencies": {
|
|
54
54
|
"@hono/node-server": "^2.0.12",
|
|
55
|
-
"@kelpie/schemas": "^0.
|
|
55
|
+
"@kelpie/schemas": "^0.8.0",
|
|
56
56
|
"@node-rs/argon2": "^2.0.2",
|
|
57
57
|
"drizzle-orm": "^0.45.2",
|
|
58
58
|
"emittery": "^2.0.0",
|
|
59
59
|
"hono": "^4.12.32",
|
|
60
|
+
"mailgen": "^2.0.36",
|
|
60
61
|
"nodemailer": "^9.0.5",
|
|
61
62
|
"postgres": "^3.4.9",
|
|
62
63
|
"ulid": "^3.0.2",
|
package/src/app.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { Hono } from 'hono'
|
|
|
2
2
|
import type { Context } from 'hono'
|
|
3
3
|
import { cors } from 'hono/cors'
|
|
4
4
|
|
|
5
|
+
import type { RuntimeMode } from './lib/config.ts'
|
|
5
6
|
import type { DatabaseProbe } from './lib/database.ts'
|
|
6
7
|
import { AppError, internalErrorBody, toErrorBody } from './lib/errors.ts'
|
|
7
8
|
import { PUBLIC_ROUTE_PREFIX } from './lib/http.ts'
|
|
@@ -51,6 +52,18 @@ export interface AppDependencies {
|
|
|
51
52
|
* control (`testing/app.ts`).
|
|
52
53
|
*/
|
|
53
54
|
readonly resolveClientIp: (context: Context) => string
|
|
55
|
+
/**
|
|
56
|
+
* Reported through `GET /v1/public/config` so the browser knows which
|
|
57
|
+
* runtime it is talking to. Defaults to `'production'` so a caller that has
|
|
58
|
+
* not thought about it fails safe: no banner shows.
|
|
59
|
+
*/
|
|
60
|
+
readonly runtimeMode?: RuntimeMode
|
|
61
|
+
/**
|
|
62
|
+
* Reported through `GET /v1/public/config` so a non-production UI can name
|
|
63
|
+
* the site it is on. Undefined is fine and means "this deployment did not
|
|
64
|
+
* name itself".
|
|
65
|
+
*/
|
|
66
|
+
readonly siteName?: string | undefined
|
|
54
67
|
}
|
|
55
68
|
|
|
56
69
|
/** Per-request values the middleware chain sets and handlers read. */
|
|
@@ -161,6 +174,19 @@ export function createApp(dependencies: AppDependencies): Hono<AppBindings> {
|
|
|
161
174
|
app.route(PUBLIC_ROUTE_PREFIX, router)
|
|
162
175
|
}
|
|
163
176
|
|
|
177
|
+
// Public deployment metadata the browser reads once at boot. Sits under the
|
|
178
|
+
// public prefix so the CORS and rate-limit layers above cover it, and takes
|
|
179
|
+
// no credentials for the same reason /healthz does not: the pages that read
|
|
180
|
+
// it include the sign-in page, which runs before any session exists. The
|
|
181
|
+
// runtime mode is already visible in error messages, and the site name
|
|
182
|
+
// exists to be visible; nothing here is sensitive.
|
|
183
|
+
const runtimeMode: RuntimeMode = dependencies.runtimeMode ?? 'production'
|
|
184
|
+
const siteName = dependencies.siteName ?? null
|
|
185
|
+
|
|
186
|
+
app.get(`${PUBLIC_ROUTE_PREFIX}/config`, (context) =>
|
|
187
|
+
context.json({ runtime_mode: runtimeMode, site_name: siteName }, 200),
|
|
188
|
+
)
|
|
189
|
+
|
|
164
190
|
for (const { router } of dependencies.contributions.routers) {
|
|
165
191
|
app.route('/v1', router)
|
|
166
192
|
}
|
package/src/boot.ts
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import type { Environment, KelpieConfig } from './lib/config.ts'
|
|
2
|
+
import { connectDatabase } from './lib/database.ts'
|
|
3
|
+
import type { DatabaseConnection } from './lib/database.ts'
|
|
4
|
+
import { createIdFactory } from './lib/ids.ts'
|
|
5
|
+
import type { IdFactory } from './lib/ids.ts'
|
|
6
|
+
import { resolveKelpieConfig } from './lib/kelpieConfigFile.ts'
|
|
7
|
+
import type { KelpieConfigInput } from './lib/kelpieConfigFile.ts'
|
|
8
|
+
import { createLogger, createTransportForDestination } from './lib/logger.ts'
|
|
9
|
+
import type { Logger } from './lib/logger.ts'
|
|
10
|
+
import { readModuleConfigFile } from './lib/moduleConfig.ts'
|
|
11
|
+
import { resolveActorFrom } from './modules/auth/credentials.ts'
|
|
12
|
+
import type { CredentialDependencies } from './modules/auth/credentials.ts'
|
|
13
|
+
import { createEventBus } from './runtime/events.ts'
|
|
14
|
+
import { registerModules } from './runtime/registry.ts'
|
|
15
|
+
import type { ModuleContributions } from './runtime/registry.ts'
|
|
16
|
+
import { createTransactionScope } from './runtime/transaction.ts'
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* A resolved assembly: config parsed, database connected, modules registered.
|
|
20
|
+
*
|
|
21
|
+
* This is the common prelude every entry point needs before it does its own
|
|
22
|
+
* work. `server.ts` goes on to build the app and serve; `migrate.ts` goes on to
|
|
23
|
+
* apply migrations and exit. Both start here, so the wiring lives in one place
|
|
24
|
+
* rather than drifting between two copies.
|
|
25
|
+
*
|
|
26
|
+
* The database is connected but not opened: postgres.js connects lazily, and
|
|
27
|
+
* registration issues no query, so a caller that only needs the contributions
|
|
28
|
+
* pays for no connection. The caller owns `database.close()`.
|
|
29
|
+
*/
|
|
30
|
+
export interface AssemblyBoot {
|
|
31
|
+
readonly config: KelpieConfig
|
|
32
|
+
readonly logger: Logger
|
|
33
|
+
readonly database: DatabaseConnection
|
|
34
|
+
readonly createId: IdFactory
|
|
35
|
+
readonly credentials: CredentialDependencies
|
|
36
|
+
readonly contributions: ModuleContributions
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Resolves an assembly's config, connects the database, and runs the module
|
|
41
|
+
* registration pass.
|
|
42
|
+
*
|
|
43
|
+
* Registration touches no database. It only collects what the modules
|
|
44
|
+
* contribute, migrations directories included, which is why an entry point can
|
|
45
|
+
* call this and then decide whether to serve, migrate, or both.
|
|
46
|
+
*
|
|
47
|
+
* @throws ConfigurationError when a required variable is missing or malformed.
|
|
48
|
+
* @throws ModuleConfigFileError when the module override file cannot be read.
|
|
49
|
+
* @throws ModuleBootError when the module list is invalid or a module fails to
|
|
50
|
+
* register. The caller maps these to a fix hint and an exit code.
|
|
51
|
+
*/
|
|
52
|
+
export async function bootAssembly(
|
|
53
|
+
kelpieConfig: KelpieConfigInput,
|
|
54
|
+
environment: Environment,
|
|
55
|
+
): Promise<AssemblyBoot> {
|
|
56
|
+
const config = resolveKelpieConfig(kelpieConfig, environment)
|
|
57
|
+
const logger = createLogger({
|
|
58
|
+
level: config.logging.level,
|
|
59
|
+
transports: config.logging.destinations.map(createTransportForDestination),
|
|
60
|
+
})
|
|
61
|
+
const database = connectDatabase(config.databaseUrl, logger)
|
|
62
|
+
const events = createEventBus(logger)
|
|
63
|
+
const createId = createIdFactory()
|
|
64
|
+
const credentials: CredentialDependencies = { db: database.db, now: () => new Date() }
|
|
65
|
+
const moduleConfig = readModuleConfigFile(config.moduleConfigPath)
|
|
66
|
+
const contributions = await registerModules({
|
|
67
|
+
modules: kelpieConfig.modules,
|
|
68
|
+
environment: config.env,
|
|
69
|
+
logger,
|
|
70
|
+
events,
|
|
71
|
+
moduleConfig,
|
|
72
|
+
resolveActor: (context) => resolveActorFrom(credentials, context),
|
|
73
|
+
services: {
|
|
74
|
+
db: database.db,
|
|
75
|
+
transaction: createTransactionScope({ db: database.db, bus: events, logger, createId }),
|
|
76
|
+
createId,
|
|
77
|
+
now: () => new Date(),
|
|
78
|
+
appBaseUrl: config.appBaseUrl,
|
|
79
|
+
secretEncryption: config.secretEncryption,
|
|
80
|
+
},
|
|
81
|
+
// `provider` picks a named sender from the runtime's registry. `'log'` is
|
|
82
|
+
// built in; `'smtp'` is registered by the built-in `smtp-email` core
|
|
83
|
+
// module; other names come from third-party provider modules listed in
|
|
84
|
+
// `kelpie.config.ts`. `from` is the address on every outgoing message.
|
|
85
|
+
email: { provider: config.email.EMAIL_PROVIDER, from: config.email.EMAIL_FROM },
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
return { config, logger, database, createId, credentials, contributions }
|
|
89
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
|
+
// Carries each module's `KelpieEventMap` augmentation into the emitted
|
|
2
|
+
// `index.d.ts` so consumers installed from npm get a typed `event.data`. See
|
|
3
|
+
// `modules/eventCatalogs.ts` for why the entry point has to reference them.
|
|
4
|
+
import './modules/eventCatalogs.ts'
|
|
5
|
+
|
|
1
6
|
export { createApp } from './app.ts'
|
|
2
7
|
export type { AppDependencies, AppBindings } from './app.ts'
|
|
3
8
|
|
|
9
|
+
export { bootAssembly } from './boot.ts'
|
|
10
|
+
export type { AssemblyBoot } from './boot.ts'
|
|
11
|
+
|
|
4
12
|
export { WebBundleError, serveWebBundle } from './webBundle.ts'
|
|
5
13
|
export type { WebBundleOptions } from './webBundle.ts'
|
|
6
14
|
|
package/src/lib/config.ts
CHANGED
|
@@ -39,6 +39,13 @@ export interface KelpieConfig {
|
|
|
39
39
|
readonly runtimeMode: RuntimeMode
|
|
40
40
|
readonly port: number
|
|
41
41
|
readonly databaseUrl: string
|
|
42
|
+
/**
|
|
43
|
+
* Human-readable name of this deployment ("dev", "demo", "cloud", or
|
|
44
|
+
* whatever the self-hoster picks). Undefined is normal. Exposed to the
|
|
45
|
+
* browser through `GET /v1/public/config` so a non-production UI can name
|
|
46
|
+
* the site it is on. Ignored when `runtimeMode` is `production`.
|
|
47
|
+
*/
|
|
48
|
+
readonly siteName: string | undefined
|
|
42
49
|
readonly logging: LoggingConfig
|
|
43
50
|
/**
|
|
44
51
|
* The provider name the module runtime looks up in its registry, and the
|
|
@@ -119,6 +126,7 @@ const environmentSchema = z.object({
|
|
|
119
126
|
.refine(isPostgresUrl, { message: 'must be a postgres:// or postgresql:// connection string' }),
|
|
120
127
|
LOG_LEVEL: z.enum(['debug', 'info', 'warn', 'error']),
|
|
121
128
|
KELPIE_MODULE_CONFIG_PATH: z.string().min(1).optional(),
|
|
129
|
+
KELPIE_SITE_NAME: z.string().min(1).optional(),
|
|
122
130
|
WEB_BUNDLE_DIR: z.string().min(1).optional(),
|
|
123
131
|
TRUSTED_PROXY_HOP_COUNT: z.coerce.number().int().nonnegative().default(0),
|
|
124
132
|
...rateLimitConfigSchema.shape,
|
|
@@ -156,6 +164,7 @@ export function loadConfig(environment: Environment): KelpieConfig {
|
|
|
156
164
|
},
|
|
157
165
|
email: emailResult.data,
|
|
158
166
|
moduleConfigPath: environmentResult.data.KELPIE_MODULE_CONFIG_PATH,
|
|
167
|
+
siteName: environmentResult.data.KELPIE_SITE_NAME,
|
|
159
168
|
webBundleDirectory: environmentResult.data.WEB_BUNDLE_DIR,
|
|
160
169
|
rateLimit: rateLimitConfigFrom(environmentResult.data),
|
|
161
170
|
trustedProxyHopCount: environmentResult.data.TRUSTED_PROXY_HOP_COUNT,
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Consumer email hosts, checked case-insensitively.
|
|
3
|
+
*
|
|
4
|
+
* The email-domain listener refuses to auto-link a Person to a Company on any
|
|
5
|
+
* domain in this list. A consumer address belongs to no one company, and a
|
|
6
|
+
* Company row entered by mistake against, say, `gmail.com` would otherwise
|
|
7
|
+
* auto-attach every consumer address after.
|
|
8
|
+
*
|
|
9
|
+
* A starter list of the common providers. Add locale variants and less common
|
|
10
|
+
* hosts as the need arrives.
|
|
11
|
+
*/
|
|
12
|
+
const CONSUMER_EMAIL_DOMAINS: ReadonlySet<string> = new Set([
|
|
13
|
+
'gmail.com',
|
|
14
|
+
'googlemail.com',
|
|
15
|
+
'yahoo.com',
|
|
16
|
+
'yahoo.co.uk',
|
|
17
|
+
'yahoo.co.jp',
|
|
18
|
+
'yahoo.fr',
|
|
19
|
+
'yahoo.de',
|
|
20
|
+
'yahoo.ca',
|
|
21
|
+
'yahoo.com.au',
|
|
22
|
+
'ymail.com',
|
|
23
|
+
'rocketmail.com',
|
|
24
|
+
'hotmail.com',
|
|
25
|
+
'hotmail.co.uk',
|
|
26
|
+
'hotmail.fr',
|
|
27
|
+
'hotmail.de',
|
|
28
|
+
'hotmail.it',
|
|
29
|
+
'hotmail.es',
|
|
30
|
+
'outlook.com',
|
|
31
|
+
'outlook.co.uk',
|
|
32
|
+
'live.com',
|
|
33
|
+
'live.co.uk',
|
|
34
|
+
'msn.com',
|
|
35
|
+
'aol.com',
|
|
36
|
+
'aim.com',
|
|
37
|
+
'icloud.com',
|
|
38
|
+
'me.com',
|
|
39
|
+
'mac.com',
|
|
40
|
+
'protonmail.com',
|
|
41
|
+
'proton.me',
|
|
42
|
+
'pm.me',
|
|
43
|
+
'gmx.com',
|
|
44
|
+
'gmx.net',
|
|
45
|
+
'gmx.de',
|
|
46
|
+
'mail.com',
|
|
47
|
+
'fastmail.com',
|
|
48
|
+
'fastmail.fm',
|
|
49
|
+
'zoho.com',
|
|
50
|
+
'yandex.com',
|
|
51
|
+
'yandex.ru',
|
|
52
|
+
'tutanota.com',
|
|
53
|
+
'tuta.io',
|
|
54
|
+
'qq.com',
|
|
55
|
+
'163.com',
|
|
56
|
+
'126.com',
|
|
57
|
+
'naver.com',
|
|
58
|
+
])
|
|
59
|
+
|
|
60
|
+
export function isConsumerEmailDomain(domain: string): boolean {
|
|
61
|
+
return CONSUMER_EMAIL_DOMAINS.has(domain.trim().toLowerCase())
|
|
62
|
+
}
|
package/src/lib/email.ts
CHANGED
|
@@ -21,7 +21,14 @@ import type { Logger } from './logger.ts'
|
|
|
21
21
|
export interface EmailMessage {
|
|
22
22
|
readonly to: string
|
|
23
23
|
readonly subject: string
|
|
24
|
+
/** The plaintext part. Always present: text-only clients and logs read it. */
|
|
24
25
|
readonly body: string
|
|
26
|
+
/**
|
|
27
|
+
* The HTML alternative, rendered by `lib/emailContent.ts`. Optional so
|
|
28
|
+
* provider modules written against the older port keep compiling and a
|
|
29
|
+
* plain-text message stays expressible.
|
|
30
|
+
*/
|
|
31
|
+
readonly html?: string
|
|
25
32
|
}
|
|
26
33
|
|
|
27
34
|
export interface EmailSender {
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import Mailgen from 'mailgen'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Renders transactional mail bodies: a plaintext part and an HTML part from
|
|
5
|
+
* one description of the message.
|
|
6
|
+
*
|
|
7
|
+
* Email clients need table-based, inline-styled HTML; mailgen owns that
|
|
8
|
+
* problem so Kelpie does not. The plaintext part is built here by hand rather
|
|
9
|
+
* than by mailgen's plaintext generator, because that generator pads lines
|
|
10
|
+
* with trailing whitespace and the plaintext body is the part tests and
|
|
11
|
+
* text-only clients read token links out of. The wording of the two parts
|
|
12
|
+
* comes from the same fields, so they cannot drift.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/** A link the message asks the reader to follow, rendered as a button in HTML. */
|
|
16
|
+
export interface EmailAction {
|
|
17
|
+
readonly instructions: string
|
|
18
|
+
readonly buttonText: string
|
|
19
|
+
readonly link: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface EmailContent {
|
|
23
|
+
readonly intro?: string
|
|
24
|
+
readonly action?: EmailAction
|
|
25
|
+
readonly outro?: string
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface RenderedEmail {
|
|
29
|
+
readonly text: string
|
|
30
|
+
readonly html: string
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Fixed so rendering is deterministic. Mailgen's default footer stamps the
|
|
35
|
+
* current year, which the sender has no claim to: the workspace, not Kelpie,
|
|
36
|
+
* owns the relationship with the recipient.
|
|
37
|
+
*/
|
|
38
|
+
const FOOTER_LINE = 'Sent by Kelpie.'
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Renders one transactional message. `appBaseUrl` becomes the link on the
|
|
42
|
+
* product name in the header and footer, so every part of the mail points at
|
|
43
|
+
* the deployment that sent it.
|
|
44
|
+
*/
|
|
45
|
+
export function renderEmail(content: EmailContent, appBaseUrl: string): RenderedEmail {
|
|
46
|
+
const generator = new Mailgen({
|
|
47
|
+
theme: 'default',
|
|
48
|
+
product: {
|
|
49
|
+
name: 'Kelpie',
|
|
50
|
+
link: appBaseUrl,
|
|
51
|
+
copyright: FOOTER_LINE,
|
|
52
|
+
},
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
const html: string = generator.generate({
|
|
56
|
+
body: {
|
|
57
|
+
greeting: false,
|
|
58
|
+
signature: false,
|
|
59
|
+
...(content.intro === undefined ? {} : { intro: content.intro }),
|
|
60
|
+
...(content.action === undefined
|
|
61
|
+
? {}
|
|
62
|
+
: {
|
|
63
|
+
action: {
|
|
64
|
+
instructions: content.action.instructions,
|
|
65
|
+
button: {
|
|
66
|
+
text: content.action.buttonText,
|
|
67
|
+
link: content.action.link,
|
|
68
|
+
fallback: true,
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
}),
|
|
72
|
+
...(content.outro === undefined ? {} : { outro: content.outro }),
|
|
73
|
+
},
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
const paragraphs: string[] = []
|
|
77
|
+
|
|
78
|
+
if (content.intro !== undefined) {
|
|
79
|
+
paragraphs.push(content.intro)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (content.action !== undefined) {
|
|
83
|
+
paragraphs.push(`${content.action.instructions}\n\n${content.action.link}`)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (content.outro !== undefined) {
|
|
87
|
+
paragraphs.push(content.outro)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return { text: paragraphs.join('\n\n'), html }
|
|
91
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { UNIQUE_VIOLATION, postgresErrorCode } from './database.ts'
|
|
2
|
+
import type { IdFactory } from './ids.ts'
|
|
3
|
+
import { isConsumerEmailDomain } from './consumerEmailDomains.ts'
|
|
4
|
+
import type { BufferedEvents, Transaction } from '../runtime/transaction.ts'
|
|
5
|
+
import type { ActivityRecorder, SystemActor } from '../modules/activities/recorder.ts'
|
|
6
|
+
import { describeLink } from '../modules/activities/wording.ts'
|
|
7
|
+
import * as companyRepository from '../modules/companies/repository.ts'
|
|
8
|
+
import type { CompanyRecord } from '../modules/companies/repository.ts'
|
|
9
|
+
import * as peopleRepository from '../modules/people/repository.ts'
|
|
10
|
+
import type { PersonRecord } from '../modules/people/repository.ts'
|
|
11
|
+
import '../modules/positions/events.ts'
|
|
12
|
+
import * as positionRepository from '../modules/positions/repository.ts'
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Auto-link a Person to a workspace Company whose domain matches the person's
|
|
16
|
+
* email. Runs inside the caller's transaction, so the new Position is present
|
|
17
|
+
* in the response the API returns for the same request.
|
|
18
|
+
*
|
|
19
|
+
* Skips: no email, consumer email host (`isConsumerEmailDomain`), no Company
|
|
20
|
+
* on that domain in the workspace, or a Position already linking the two. A
|
|
21
|
+
* unique-key race with a concurrent linker is absorbed.
|
|
22
|
+
*
|
|
23
|
+
* The created Position carries an empty title. `positions.title` is `NOT NULL`,
|
|
24
|
+
* and empty reads honestly as "the domain matched; no title yet".
|
|
25
|
+
*
|
|
26
|
+
* Split from the people service so the layering (people below positions) is
|
|
27
|
+
* carried by imports only: this helper depends on both repositories, and the
|
|
28
|
+
* people service depends on this helper. The `positions` module still owns the
|
|
29
|
+
* event catalogue for the emitted event.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
/** What the timeline calls this side effect. */
|
|
33
|
+
const LINKER_ACTOR: SystemActor = { kind: 'system', label: 'Email domain match' }
|
|
34
|
+
|
|
35
|
+
export interface AutoLinkDependencies {
|
|
36
|
+
readonly createId: IdFactory
|
|
37
|
+
readonly now: () => Date
|
|
38
|
+
readonly recordActivity: ActivityRecorder
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Considers `person` for a matching Company in `workspaceId` and inserts a
|
|
43
|
+
* titleless Position if none links them yet.
|
|
44
|
+
*
|
|
45
|
+
* @returns The new Position's id, or `undefined` if nothing was written.
|
|
46
|
+
*/
|
|
47
|
+
export async function autoLinkPersonByEmailDomain(
|
|
48
|
+
tx: Transaction,
|
|
49
|
+
events: BufferedEvents,
|
|
50
|
+
workspaceId: string,
|
|
51
|
+
person: PersonRecord,
|
|
52
|
+
dependencies: AutoLinkDependencies,
|
|
53
|
+
): Promise<string | undefined> {
|
|
54
|
+
if (person.email === null) {
|
|
55
|
+
return undefined
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const at = person.email.indexOf('@')
|
|
59
|
+
if (at < 0) {
|
|
60
|
+
return undefined
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const domain = person.email.slice(at + 1)
|
|
64
|
+
if (domain.length === 0 || isConsumerEmailDomain(domain)) {
|
|
65
|
+
return undefined
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const company = await companyRepository.findCompanyByDomain(tx, workspaceId, domain)
|
|
69
|
+
if (company === undefined) {
|
|
70
|
+
return undefined
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const held = await positionRepository.listPositionsAt(tx, workspaceId, person.id, company.id)
|
|
74
|
+
if (held.length > 0) {
|
|
75
|
+
return undefined
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return insertStubPosition(tx, events, workspaceId, person, company, dependencies)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Sweeps every Person in `workspaceId` whose email domain matches `company`'s
|
|
83
|
+
* domain and stubs a Position where none exists yet. Called when a Company is
|
|
84
|
+
* created or its domain is set.
|
|
85
|
+
*
|
|
86
|
+
* @returns The ids of every Position this call inserted.
|
|
87
|
+
*/
|
|
88
|
+
export async function autoLinkCompanyByDomain(
|
|
89
|
+
tx: Transaction,
|
|
90
|
+
events: BufferedEvents,
|
|
91
|
+
workspaceId: string,
|
|
92
|
+
company: CompanyRecord,
|
|
93
|
+
dependencies: AutoLinkDependencies,
|
|
94
|
+
): Promise<readonly string[]> {
|
|
95
|
+
if (company.domain === null || company.domain.length === 0) {
|
|
96
|
+
return []
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (isConsumerEmailDomain(company.domain)) {
|
|
100
|
+
return []
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const matches = await peopleRepository.findPeopleByEmailDomain(tx, workspaceId, company.domain)
|
|
104
|
+
const created: string[] = []
|
|
105
|
+
|
|
106
|
+
for (const person of matches) {
|
|
107
|
+
const held = await positionRepository.listPositionsAt(tx, workspaceId, person.id, company.id)
|
|
108
|
+
if (held.length > 0) {
|
|
109
|
+
continue
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const id = await insertStubPosition(tx, events, workspaceId, person, company, dependencies)
|
|
113
|
+
if (id !== undefined) {
|
|
114
|
+
created.push(id)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return created
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* The write half. Wraps the unique-constraint race the two entry points share:
|
|
123
|
+
* a concurrent linker that beat us to `(personId, companyId, '')` is treated
|
|
124
|
+
* as success (the link is there, either way).
|
|
125
|
+
*/
|
|
126
|
+
async function insertStubPosition(
|
|
127
|
+
tx: Transaction,
|
|
128
|
+
events: BufferedEvents,
|
|
129
|
+
workspaceId: string,
|
|
130
|
+
person: PersonRecord,
|
|
131
|
+
company: CompanyRecord,
|
|
132
|
+
dependencies: AutoLinkDependencies,
|
|
133
|
+
): Promise<string | undefined> {
|
|
134
|
+
try {
|
|
135
|
+
const created = await positionRepository.insertPosition(tx, {
|
|
136
|
+
id: dependencies.createId('position'),
|
|
137
|
+
workspaceId,
|
|
138
|
+
personId: person.id,
|
|
139
|
+
companyId: company.id,
|
|
140
|
+
title: '',
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
await dependencies.recordActivity(tx, workspaceId, LINKER_ACTOR, {
|
|
144
|
+
targetType: 'person',
|
|
145
|
+
targetId: person.id,
|
|
146
|
+
kind: 'linked',
|
|
147
|
+
...describeLink('company', company.name),
|
|
148
|
+
})
|
|
149
|
+
await dependencies.recordActivity(tx, workspaceId, LINKER_ACTOR, {
|
|
150
|
+
targetType: 'company',
|
|
151
|
+
targetId: company.id,
|
|
152
|
+
kind: 'linked',
|
|
153
|
+
...describeLink('person', person.name),
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
events.emit('positions.position.created', { type: 'position', id: created.id }, {})
|
|
157
|
+
|
|
158
|
+
return created.id
|
|
159
|
+
} catch (error: unknown) {
|
|
160
|
+
if (postgresErrorCode(error) === UNIQUE_VIOLATION) {
|
|
161
|
+
return undefined
|
|
162
|
+
}
|
|
163
|
+
throw error
|
|
164
|
+
}
|
|
165
|
+
}
|
package/src/lib/ids.ts
CHANGED
|
@@ -27,6 +27,12 @@ export interface KelpieConfigInput {
|
|
|
27
27
|
readonly webBundleDirectory?: ConfigValue<string | undefined>
|
|
28
28
|
/** Optional; unset in development, set to the deploy-time module override file. */
|
|
29
29
|
readonly moduleConfigPath?: ConfigValue<string | undefined>
|
|
30
|
+
/**
|
|
31
|
+
* Human-readable name of this deployment ("dev", "demo", "cloud"). Optional.
|
|
32
|
+
* Read by the browser through `GET /v1/public/config` so a non-production UI
|
|
33
|
+
* can name the site it is on. Ignored when `runtimeMode` is `production`.
|
|
34
|
+
*/
|
|
35
|
+
readonly siteName?: ConfigValue<string | undefined>
|
|
30
36
|
/** Optional; defaults to 0 (no proxy in front). */
|
|
31
37
|
readonly trustedProxyHopCount?: ConfigValue<number>
|
|
32
38
|
/**
|
|
@@ -173,6 +179,7 @@ export function resolveKelpieConfig(input: KelpieConfigInput, environment: Envir
|
|
|
173
179
|
},
|
|
174
180
|
email,
|
|
175
181
|
moduleConfigPath: resolved.moduleConfigPath,
|
|
182
|
+
siteName: resolved.siteName,
|
|
176
183
|
webBundleDirectory: resolved.webBundleDirectory,
|
|
177
184
|
rateLimit,
|
|
178
185
|
trustedProxyHopCount: resolved.trustedProxyHopCount ?? 0,
|
|
@@ -208,6 +215,7 @@ interface ResolvedInput {
|
|
|
208
215
|
}
|
|
209
216
|
readonly webBundleDirectory?: string | undefined
|
|
210
217
|
readonly moduleConfigPath?: string | undefined
|
|
218
|
+
readonly siteName?: string | undefined
|
|
211
219
|
readonly trustedProxyHopCount?: number
|
|
212
220
|
readonly appBaseUrl?: string
|
|
213
221
|
readonly secretEncryption?: ResolvedSecretEncryption
|
|
@@ -6,6 +6,7 @@ import type { Queryable } from '../runtime/transaction.ts'
|
|
|
6
6
|
import * as activityRepository from './activities/repository.ts'
|
|
7
7
|
import * as decisionRepository from './decisions/repository.ts'
|
|
8
8
|
import { candidates } from './hiring/schema.ts'
|
|
9
|
+
import * as listRepository from './lists/repository.ts'
|
|
9
10
|
import * as noteRepository from './notes/repository.ts'
|
|
10
11
|
import * as planRepository from './plans/repository.ts'
|
|
11
12
|
|
|
@@ -37,7 +38,7 @@ export async function deleteRecordsAttachedTo(
|
|
|
37
38
|
targetType: AttachableTargetType,
|
|
38
39
|
targetId: string,
|
|
39
40
|
): Promise<number> {
|
|
40
|
-
// Sequential, not concurrent: a transaction is one connection, and
|
|
41
|
+
// Sequential, not concurrent: a transaction is one connection, and five
|
|
41
42
|
// statements racing down it is not something to rely on for a cheap delete.
|
|
42
43
|
const notes = await noteRepository.deleteForTarget(db, workspaceId, targetType, targetId)
|
|
43
44
|
const activities = await activityRepository.deleteForTarget(db, workspaceId, targetType, targetId)
|
|
@@ -45,8 +46,14 @@ export async function deleteRecordsAttachedTo(
|
|
|
45
46
|
const plans = PLAN_TARGET_TYPES.has(targetType)
|
|
46
47
|
? await planRepository.deleteForTarget(db, workspaceId, targetType, targetId)
|
|
47
48
|
: 0
|
|
49
|
+
const memberships = await listRepository.deleteMembershipsForTarget(
|
|
50
|
+
db,
|
|
51
|
+
workspaceId,
|
|
52
|
+
targetType,
|
|
53
|
+
targetId,
|
|
54
|
+
)
|
|
48
55
|
|
|
49
|
-
return notes + activities + decisions + plans
|
|
56
|
+
return notes + activities + decisions + plans + memberships
|
|
50
57
|
}
|
|
51
58
|
|
|
52
59
|
/**
|