@kelpie/server 0.5.1 → 0.6.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/README.md +5 -2
- package/dist/index.d.ts +17 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -4
- package/dist/index.js.map +1 -1
- package/dist/lib/actor.d.ts +9 -1
- package/dist/lib/actor.d.ts.map +1 -1
- package/dist/lib/actor.js +13 -0
- package/dist/lib/actor.js.map +1 -1
- package/dist/lib/config.d.ts +44 -6
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +9 -5
- package/dist/lib/config.js.map +1 -1
- package/dist/lib/email.d.ts +15 -63
- package/dist/lib/email.d.ts.map +1 -1
- package/dist/lib/email.js +13 -65
- package/dist/lib/email.js.map +1 -1
- package/dist/lib/fromEnv.d.ts +60 -0
- package/dist/lib/fromEnv.d.ts.map +1 -0
- package/dist/lib/fromEnv.js +94 -0
- package/dist/lib/fromEnv.js.map +1 -0
- package/dist/lib/ids.d.ts +6 -0
- package/dist/lib/ids.d.ts.map +1 -1
- package/dist/lib/ids.js +6 -0
- package/dist/lib/ids.js.map +1 -1
- package/dist/lib/kelpieConfigFile.d.ts +123 -0
- package/dist/lib/kelpieConfigFile.d.ts.map +1 -0
- package/dist/lib/kelpieConfigFile.js +111 -0
- package/dist/lib/kelpieConfigFile.js.map +1 -0
- package/dist/lib/logger.d.ts +38 -7
- package/dist/lib/logger.d.ts.map +1 -1
- package/dist/lib/logger.js +58 -20
- package/dist/lib/logger.js.map +1 -1
- package/dist/modules/agent-tasks/index.d.ts.map +1 -1
- package/dist/modules/agent-tasks/index.js +4 -1
- package/dist/modules/agent-tasks/index.js.map +1 -1
- package/dist/modules/agent-tasks/prompt.d.ts +36 -6
- package/dist/modules/agent-tasks/prompt.d.ts.map +1 -1
- package/dist/modules/agent-tasks/prompt.js +33 -4
- package/dist/modules/agent-tasks/prompt.js.map +1 -1
- package/dist/modules/agent-tasks/service.d.ts.map +1 -1
- package/dist/modules/agent-tasks/service.js +3 -2
- package/dist/modules/agent-tasks/service.js.map +1 -1
- package/dist/modules/agent-tasks/wire.d.ts +11 -0
- package/dist/modules/agent-tasks/wire.d.ts.map +1 -1
- package/dist/modules/agent-tasks/wire.js +1 -0
- package/dist/modules/agent-tasks/wire.js.map +1 -1
- package/dist/modules/auth/index.d.ts.map +1 -1
- package/dist/modules/auth/index.js +8 -7
- package/dist/modules/auth/index.js.map +1 -1
- package/dist/modules/companies/events.d.ts +22 -0
- package/dist/modules/companies/events.d.ts.map +1 -0
- package/dist/modules/companies/events.js +8 -0
- package/dist/modules/companies/events.js.map +1 -0
- package/dist/modules/companies/index.d.ts.map +1 -1
- package/dist/modules/companies/index.js +2 -0
- package/dist/modules/companies/index.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 +8 -11
- package/dist/modules/companies/service.js.map +1 -1
- package/dist/modules/core.d.ts.map +1 -1
- package/dist/modules/core.js +4 -0
- package/dist/modules/core.js.map +1 -1
- package/dist/modules/deals/events.d.ts +31 -0
- package/dist/modules/deals/events.d.ts.map +1 -0
- package/dist/modules/deals/events.js +12 -0
- package/dist/modules/deals/events.js.map +1 -0
- package/dist/modules/deals/index.d.ts.map +1 -1
- package/dist/modules/deals/index.js +2 -0
- package/dist/modules/deals/index.js.map +1 -1
- package/dist/modules/deals/service.d.ts +1 -0
- package/dist/modules/deals/service.d.ts.map +1 -1
- package/dist/modules/deals/service.js +9 -18
- package/dist/modules/deals/service.js.map +1 -1
- package/dist/modules/decisions/events.d.ts +21 -0
- package/dist/modules/decisions/events.d.ts.map +1 -0
- package/dist/modules/decisions/events.js +11 -0
- package/dist/modules/decisions/events.js.map +1 -0
- package/dist/modules/decisions/index.d.ts.map +1 -1
- package/dist/modules/decisions/index.js +2 -0
- package/dist/modules/decisions/index.js.map +1 -1
- package/dist/modules/decisions/service.d.ts +1 -0
- package/dist/modules/decisions/service.d.ts.map +1 -1
- package/dist/modules/decisions/service.js +4 -7
- package/dist/modules/decisions/service.js.map +1 -1
- package/dist/modules/forms/events.d.ts +37 -0
- package/dist/modules/forms/events.d.ts.map +1 -0
- package/dist/modules/forms/events.js +15 -0
- package/dist/modules/forms/events.js.map +1 -0
- package/dist/modules/forms/index.d.ts.map +1 -1
- package/dist/modules/forms/index.js +2 -0
- package/dist/modules/forms/index.js.map +1 -1
- package/dist/modules/forms/service.d.ts +1 -0
- package/dist/modules/forms/service.d.ts.map +1 -1
- package/dist/modules/forms/service.js +8 -11
- package/dist/modules/forms/service.js.map +1 -1
- package/dist/modules/forms/submission.d.ts +5 -0
- package/dist/modules/forms/submission.d.ts.map +1 -1
- package/dist/modules/forms/submission.js +54 -28
- package/dist/modules/forms/submission.js.map +1 -1
- package/dist/modules/handbook/events.d.ts +28 -0
- package/dist/modules/handbook/events.d.ts.map +1 -0
- package/dist/modules/handbook/events.js +14 -0
- package/dist/modules/handbook/events.js.map +1 -0
- package/dist/modules/handbook/index.d.ts.map +1 -1
- package/dist/modules/handbook/index.js +2 -0
- package/dist/modules/handbook/index.js.map +1 -1
- package/dist/modules/handbook/service.d.ts +1 -0
- package/dist/modules/handbook/service.d.ts.map +1 -1
- package/dist/modules/handbook/service.js +8 -15
- package/dist/modules/handbook/service.js.map +1 -1
- package/dist/modules/hiring/candidates.d.ts +1 -0
- package/dist/modules/hiring/candidates.d.ts.map +1 -1
- package/dist/modules/hiring/candidates.js +8 -11
- package/dist/modules/hiring/candidates.js.map +1 -1
- package/dist/modules/hiring/events.d.ts +35 -0
- package/dist/modules/hiring/events.d.ts.map +1 -0
- package/dist/modules/hiring/events.js +11 -0
- package/dist/modules/hiring/events.js.map +1 -0
- package/dist/modules/hiring/index.d.ts.map +1 -1
- package/dist/modules/hiring/index.js +2 -0
- package/dist/modules/hiring/index.js.map +1 -1
- package/dist/modules/hiring/roles.d.ts +1 -0
- package/dist/modules/hiring/roles.d.ts.map +1 -1
- package/dist/modules/hiring/roles.js +8 -11
- package/dist/modules/hiring/roles.js.map +1 -1
- package/dist/modules/import-export/events.d.ts +22 -0
- package/dist/modules/import-export/events.d.ts.map +1 -0
- package/dist/modules/import-export/events.js +12 -0
- package/dist/modules/import-export/events.js.map +1 -0
- package/dist/modules/import-export/index.d.ts.map +1 -1
- package/dist/modules/import-export/index.js +2 -0
- package/dist/modules/import-export/index.js.map +1 -1
- package/dist/modules/import-export/service.d.ts +5 -0
- package/dist/modules/import-export/service.d.ts.map +1 -1
- package/dist/modules/import-export/service.js +53 -14
- package/dist/modules/import-export/service.js.map +1 -1
- package/dist/modules/notes/events.d.ts +22 -0
- package/dist/modules/notes/events.d.ts.map +1 -0
- package/dist/modules/notes/events.js +12 -0
- package/dist/modules/notes/events.js.map +1 -0
- package/dist/modules/notes/index.d.ts.map +1 -1
- package/dist/modules/notes/index.js +2 -0
- package/dist/modules/notes/index.js.map +1 -1
- package/dist/modules/notes/service.d.ts +1 -0
- package/dist/modules/notes/service.d.ts.map +1 -1
- package/dist/modules/notes/service.js +4 -7
- package/dist/modules/notes/service.js.map +1 -1
- package/dist/modules/opportunities/events.d.ts +31 -0
- package/dist/modules/opportunities/events.d.ts.map +1 -0
- package/dist/modules/opportunities/events.js +12 -0
- package/dist/modules/opportunities/events.js.map +1 -0
- package/dist/modules/opportunities/index.d.ts.map +1 -1
- package/dist/modules/opportunities/index.js +2 -0
- package/dist/modules/opportunities/index.js.map +1 -1
- package/dist/modules/opportunities/service.d.ts +1 -0
- package/dist/modules/opportunities/service.d.ts.map +1 -1
- package/dist/modules/opportunities/service.js +9 -18
- package/dist/modules/opportunities/service.js.map +1 -1
- package/dist/modules/partnerships/events.d.ts +31 -0
- package/dist/modules/partnerships/events.d.ts.map +1 -0
- package/dist/modules/partnerships/events.js +12 -0
- package/dist/modules/partnerships/events.js.map +1 -0
- package/dist/modules/partnerships/index.d.ts.map +1 -1
- package/dist/modules/partnerships/index.js +2 -0
- package/dist/modules/partnerships/index.js.map +1 -1
- package/dist/modules/partnerships/service.d.ts +1 -0
- package/dist/modules/partnerships/service.d.ts.map +1 -1
- package/dist/modules/partnerships/service.js +9 -18
- package/dist/modules/partnerships/service.js.map +1 -1
- package/dist/modules/people/events.d.ts +28 -0
- package/dist/modules/people/events.d.ts.map +1 -0
- package/dist/modules/people/events.js +18 -0
- package/dist/modules/people/events.js.map +1 -0
- package/dist/modules/people/index.d.ts.map +1 -1
- package/dist/modules/people/index.js +2 -0
- package/dist/modules/people/index.js.map +1 -1
- package/dist/modules/people/service.d.ts +1 -0
- package/dist/modules/people/service.d.ts.map +1 -1
- package/dist/modules/people/service.js +8 -11
- package/dist/modules/people/service.js.map +1 -1
- package/dist/modules/pipelines/service.d.ts +4 -0
- package/dist/modules/pipelines/service.d.ts.map +1 -1
- package/dist/modules/pipelines/service.js +31 -8
- package/dist/modules/pipelines/service.js.map +1 -1
- package/dist/modules/plans/events.d.ts +21 -0
- package/dist/modules/plans/events.d.ts.map +1 -0
- package/dist/modules/plans/events.js +11 -0
- package/dist/modules/plans/events.js.map +1 -0
- package/dist/modules/plans/index.d.ts.map +1 -1
- package/dist/modules/plans/index.js +2 -0
- package/dist/modules/plans/index.js.map +1 -1
- package/dist/modules/plans/service.d.ts +1 -0
- package/dist/modules/plans/service.d.ts.map +1 -1
- package/dist/modules/plans/service.js +6 -14
- package/dist/modules/plans/service.js.map +1 -1
- package/dist/modules/positions/events.d.ts +22 -0
- package/dist/modules/positions/events.d.ts.map +1 -0
- package/dist/modules/positions/events.js +8 -0
- package/dist/modules/positions/events.js.map +1 -0
- package/dist/modules/positions/index.d.ts.map +1 -1
- package/dist/modules/positions/index.js +2 -0
- package/dist/modules/positions/index.js.map +1 -1
- package/dist/modules/positions/service.d.ts +1 -0
- package/dist/modules/positions/service.d.ts.map +1 -1
- package/dist/modules/positions/service.js +8 -11
- package/dist/modules/positions/service.js.map +1 -1
- package/dist/modules/raises/events.d.ts +31 -0
- package/dist/modules/raises/events.d.ts.map +1 -0
- package/dist/modules/raises/events.js +12 -0
- package/dist/modules/raises/events.js.map +1 -0
- package/dist/modules/raises/index.d.ts.map +1 -1
- package/dist/modules/raises/index.js +2 -0
- package/dist/modules/raises/index.js.map +1 -1
- package/dist/modules/raises/service.d.ts +1 -0
- package/dist/modules/raises/service.d.ts.map +1 -1
- package/dist/modules/raises/service.js +9 -18
- package/dist/modules/raises/service.js.map +1 -1
- package/dist/modules/reseal.d.ts +55 -0
- package/dist/modules/reseal.d.ts.map +1 -1
- package/dist/modules/reseal.js +90 -1
- package/dist/modules/reseal.js.map +1 -1
- package/dist/modules/smtp-email/index.d.ts +69 -0
- package/dist/modules/smtp-email/index.d.ts.map +1 -0
- package/dist/modules/smtp-email/index.js +82 -0
- package/dist/modules/smtp-email/index.js.map +1 -0
- package/dist/modules/webhooks/index.d.ts.map +1 -1
- package/dist/modules/webhooks/index.js +5 -1
- package/dist/modules/webhooks/index.js.map +1 -1
- package/dist/modules/webhooks/payloads.d.ts +22 -7
- package/dist/modules/webhooks/payloads.d.ts.map +1 -1
- package/dist/modules/webhooks/payloads.js +63 -44
- package/dist/modules/webhooks/payloads.js.map +1 -1
- package/dist/modules/workspace/events.d.ts +52 -0
- package/dist/modules/workspace/events.d.ts.map +1 -0
- package/dist/modules/workspace/events.js +16 -0
- package/dist/modules/workspace/events.js.map +1 -0
- package/dist/modules/workspace/index.d.ts.map +1 -1
- package/dist/modules/workspace/index.js +6 -2
- package/dist/modules/workspace/index.js.map +1 -1
- package/dist/modules/workspace/service.d.ts +1 -0
- package/dist/modules/workspace/service.d.ts.map +1 -1
- package/dist/modules/workspace/service.js +13 -15
- package/dist/modules/workspace/service.js.map +1 -1
- package/dist/modules/workspace/workspaceAccessMiddleware.d.ts.map +1 -1
- package/dist/modules/workspace/workspaceAccessMiddleware.js +11 -3
- package/dist/modules/workspace/workspaceAccessMiddleware.js.map +1 -1
- package/dist/runtime/events.d.ts +86 -108
- package/dist/runtime/events.d.ts.map +1 -1
- package/dist/runtime/events.js +123 -52
- package/dist/runtime/events.js.map +1 -1
- package/dist/runtime/module.d.ts +54 -2
- package/dist/runtime/module.d.ts.map +1 -1
- package/dist/runtime/registry.d.ts +23 -0
- package/dist/runtime/registry.d.ts.map +1 -1
- package/dist/runtime/registry.js +102 -2
- package/dist/runtime/registry.js.map +1 -1
- package/dist/runtime/transaction.d.ts +29 -4
- package/dist/runtime/transaction.d.ts.map +1 -1
- package/dist/runtime/transaction.js +48 -7
- package/dist/runtime/transaction.js.map +1 -1
- package/dist/testing/app.d.ts.map +1 -1
- package/dist/testing/app.js +11 -3
- package/dist/testing/app.js.map +1 -1
- package/dist/testing/database.d.ts.map +1 -1
- package/dist/testing/database.js +6 -3
- package/dist/testing/database.js.map +1 -1
- package/dist/testing/services.d.ts +21 -1
- package/dist/testing/services.d.ts.map +1 -1
- package/dist/testing/services.js +20 -5
- package/dist/testing/services.js.map +1 -1
- package/package.json +5 -2
- package/src/index.ts +44 -9
- package/src/lib/actor.ts +15 -1
- package/src/lib/config.ts +56 -7
- package/src/lib/email.ts +22 -92
- package/src/lib/fromEnv.ts +143 -0
- package/src/lib/ids.ts +6 -0
- package/src/lib/kelpieConfigFile.ts +283 -0
- package/src/lib/logger.ts +92 -34
- package/src/modules/agent-tasks/index.ts +4 -1
- package/src/modules/agent-tasks/prompt.ts +57 -9
- package/src/modules/agent-tasks/service.ts +3 -2
- package/src/modules/agent-tasks/wire.ts +12 -0
- package/src/modules/auth/index.ts +11 -9
- package/src/modules/companies/events.ts +25 -0
- package/src/modules/companies/index.ts +2 -0
- package/src/modules/companies/service.ts +8 -11
- package/src/modules/core.ts +4 -0
- package/src/modules/deals/events.ts +34 -0
- package/src/modules/deals/index.ts +2 -0
- package/src/modules/deals/service.ts +17 -18
- package/src/modules/decisions/events.ts +24 -0
- package/src/modules/decisions/index.ts +2 -0
- package/src/modules/decisions/service.ts +8 -7
- package/src/modules/forms/events.ts +37 -0
- package/src/modules/forms/index.ts +2 -0
- package/src/modules/forms/service.ts +12 -11
- package/src/modules/forms/submission.ts +80 -29
- package/src/modules/handbook/events.ts +31 -0
- package/src/modules/handbook/index.ts +2 -0
- package/src/modules/handbook/service.ts +8 -15
- package/src/modules/hiring/candidates.ts +8 -11
- package/src/modules/hiring/events.ts +36 -0
- package/src/modules/hiring/index.ts +2 -0
- package/src/modules/hiring/roles.ts +8 -11
- package/src/modules/import-export/events.ts +25 -0
- package/src/modules/import-export/index.ts +2 -0
- package/src/modules/import-export/service.ts +83 -15
- package/src/modules/notes/events.ts +25 -0
- package/src/modules/notes/index.ts +2 -0
- package/src/modules/notes/service.ts +8 -7
- package/src/modules/opportunities/events.ts +34 -0
- package/src/modules/opportunities/index.ts +2 -0
- package/src/modules/opportunities/service.ts +17 -18
- package/src/modules/partnerships/events.ts +34 -0
- package/src/modules/partnerships/index.ts +2 -0
- package/src/modules/partnerships/service.ts +17 -18
- package/src/modules/people/events.ts +36 -0
- package/src/modules/people/index.ts +2 -0
- package/src/modules/people/service.ts +97 -87
- package/src/modules/pipelines/service.ts +47 -8
- package/src/modules/plans/events.ts +24 -0
- package/src/modules/plans/index.ts +2 -0
- package/src/modules/plans/service.ts +14 -14
- package/src/modules/positions/events.ts +25 -0
- package/src/modules/positions/index.ts +2 -0
- package/src/modules/positions/service.ts +8 -11
- package/src/modules/raises/events.ts +34 -0
- package/src/modules/raises/index.ts +2 -0
- package/src/modules/raises/service.ts +17 -18
- package/src/modules/reseal.ts +144 -1
- package/src/modules/smtp-email/index.ts +119 -0
- package/src/modules/webhooks/index.ts +5 -1
- package/src/modules/webhooks/payloads.ts +94 -48
- package/src/modules/workspace/events.ts +46 -0
- package/src/modules/workspace/index.ts +6 -2
- package/src/modules/workspace/service.ts +37 -15
- package/src/modules/workspace/workspaceAccessMiddleware.ts +12 -3
- package/src/runtime/events.ts +268 -107
- package/src/runtime/module.ts +55 -2
- package/src/runtime/registry.ts +169 -1
- package/src/runtime/transaction.ts +94 -17
- package/src/testing/app.ts +11 -3
- package/src/testing/database.ts +6 -3
- package/src/testing/services.ts +34 -5
package/src/runtime/registry.ts
CHANGED
|
@@ -4,6 +4,8 @@ import type { Context, Handler, MiddlewareHandler } from 'hono'
|
|
|
4
4
|
import type { Actor } from '../lib/actor.ts'
|
|
5
5
|
import { requireWorkspaceId } from '../lib/actor.ts'
|
|
6
6
|
import type { Environment } from '../lib/config.ts'
|
|
7
|
+
import { createLogEmailSender } from '../lib/email.ts'
|
|
8
|
+
import type { EmailMessage, EmailSender } from '../lib/email.ts'
|
|
7
9
|
import { AppError, describeThrown, describeValidationIssue, toErrorDetails } from '../lib/errors.ts'
|
|
8
10
|
import type { Logger } from '../lib/logger.ts'
|
|
9
11
|
import { createEntitlementRegistry, requireCapability } from './entitlements.ts'
|
|
@@ -103,6 +105,28 @@ export interface ModuleRuntimeOptions {
|
|
|
103
105
|
readonly entitlements?: EntitlementRegistry | undefined
|
|
104
106
|
/** The database, transaction scope, and collaborators every module builds on. */
|
|
105
107
|
readonly services: ModuleServices
|
|
108
|
+
/**
|
|
109
|
+
* Picks the transactional-mail provider. `provider` is a name a module
|
|
110
|
+
* registered via `context.provideEmailSender(name, sender)`. `'log'` is a
|
|
111
|
+
* built-in the runtime always registers using its own logger and `from`, so
|
|
112
|
+
* a bare install boots without a provider module.
|
|
113
|
+
*
|
|
114
|
+
* `from` is the address on every outgoing message. The log provider uses it
|
|
115
|
+
* verbatim; other providers may read the same value through their own
|
|
116
|
+
* `context.config` if they need to.
|
|
117
|
+
*/
|
|
118
|
+
readonly email: {
|
|
119
|
+
readonly provider: string
|
|
120
|
+
readonly from: string
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Extra named providers seeded into the registry before any module runs.
|
|
124
|
+
* Modules still register through `context.provideEmailSender`; these are for
|
|
125
|
+
* tests that want to inspect what was sent without building a whole module.
|
|
126
|
+
* A name collision with a module (or with `'log'`) fails boot exactly as if
|
|
127
|
+
* two modules had registered it.
|
|
128
|
+
*/
|
|
129
|
+
readonly additionalEmailProviders?: ReadonlyMap<string, EmailSender> | undefined
|
|
106
130
|
/**
|
|
107
131
|
* The deploy-time module override (`lib/moduleConfig.ts`), parsed. A locked
|
|
108
132
|
* module id wins over whatever a workspace's own settings say.
|
|
@@ -132,6 +156,45 @@ interface Accumulator {
|
|
|
132
156
|
readonly mcpTools: McpTool[]
|
|
133
157
|
}
|
|
134
158
|
|
|
159
|
+
/**
|
|
160
|
+
* The email sender every module writes through. `target` is set once, after
|
|
161
|
+
* all modules have registered, from the provider `email.provider` picked. Until
|
|
162
|
+
* then a `.send()` throws: consumer modules capture the proxy at register time
|
|
163
|
+
* but only reach it at request time, so this is unreachable in normal flow.
|
|
164
|
+
*/
|
|
165
|
+
class EmailSenderProxy implements EmailSender {
|
|
166
|
+
private target: EmailSender | undefined = undefined
|
|
167
|
+
|
|
168
|
+
setTarget(sender: EmailSender): void {
|
|
169
|
+
this.target = sender
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
send(message: EmailMessage): Promise<void> {
|
|
173
|
+
if (this.target === undefined) {
|
|
174
|
+
return Promise.reject(
|
|
175
|
+
new Error('email sender used before boot resolved a provider. Set email.provider in kelpie.config.ts.'),
|
|
176
|
+
)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return this.target.send(message)
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* The name the runtime uses for its built-in log sender. Reserved: a module
|
|
185
|
+
* that tries to register under this name fails boot.
|
|
186
|
+
*/
|
|
187
|
+
const LOG_PROVIDER_NAME = 'log'
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Records which module registered a given provider name, so a collision error
|
|
191
|
+
* can name both parties. `build` is invoked once, only for the chosen provider.
|
|
192
|
+
*/
|
|
193
|
+
interface RegisteredProvider {
|
|
194
|
+
readonly build: () => EmailSender
|
|
195
|
+
readonly registeredBy: string
|
|
196
|
+
}
|
|
197
|
+
|
|
135
198
|
function createModuleContext(
|
|
136
199
|
module: KelpieModule,
|
|
137
200
|
accumulator: Accumulator,
|
|
@@ -139,9 +202,30 @@ function createModuleContext(
|
|
|
139
202
|
events: EventBus,
|
|
140
203
|
entitlements: EntitlementRegistry,
|
|
141
204
|
moduleCatalog: readonly ModuleCatalogEntry[],
|
|
205
|
+
emailProxy: EmailSenderProxy,
|
|
206
|
+
providers: Map<string, RegisteredProvider>,
|
|
142
207
|
): ModuleContext {
|
|
143
208
|
return {
|
|
144
209
|
...options.services,
|
|
210
|
+
email: emailProxy,
|
|
211
|
+
|
|
212
|
+
provideEmailSender(name, build) {
|
|
213
|
+
if (name === LOG_PROVIDER_NAME) {
|
|
214
|
+
throw new ModuleBootError([
|
|
215
|
+
`module "${module.id}" registers email provider "${name}", which is reserved by the runtime`,
|
|
216
|
+
])
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const existing = providers.get(name)
|
|
220
|
+
|
|
221
|
+
if (existing !== undefined) {
|
|
222
|
+
throw new ModuleBootError([
|
|
223
|
+
`module "${module.id}" registers email provider "${name}", but module "${existing.registeredBy}" already did`,
|
|
224
|
+
])
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
providers.set(name, { build, registeredBy: module.id })
|
|
228
|
+
},
|
|
145
229
|
|
|
146
230
|
routes(mount) {
|
|
147
231
|
const router = new Hono()
|
|
@@ -262,6 +346,33 @@ export async function registerModules(options: ModuleRuntimeOptions): Promise<Mo
|
|
|
262
346
|
|
|
263
347
|
const events = options.events ?? createEventBus(options.logger)
|
|
264
348
|
const entitlements = options.entitlements ?? createEntitlementRegistry()
|
|
349
|
+
const emailProxy = new EmailSenderProxy()
|
|
350
|
+
const emailProviders = new Map<string, RegisteredProvider>()
|
|
351
|
+
|
|
352
|
+
// The built-in log provider is always available, no module required. Named
|
|
353
|
+
// 'log' in kelpie.config.ts's email.provider picks this one. Built eagerly
|
|
354
|
+
// because it has no env to parse and never fails.
|
|
355
|
+
const logSender = createLogEmailSender(options.logger, options.email.from)
|
|
356
|
+
|
|
357
|
+
emailProviders.set(LOG_PROVIDER_NAME, {
|
|
358
|
+
build: () => logSender,
|
|
359
|
+
registeredBy: '<runtime>',
|
|
360
|
+
})
|
|
361
|
+
|
|
362
|
+
// Seed test-supplied providers before any module registers, so a module
|
|
363
|
+
// collision with one of them fails boot the same way two modules would.
|
|
364
|
+
if (options.additionalEmailProviders !== undefined) {
|
|
365
|
+
for (const [name, sender] of options.additionalEmailProviders) {
|
|
366
|
+
if (emailProviders.has(name)) {
|
|
367
|
+
throw new ModuleBootError([
|
|
368
|
+
`additionalEmailProviders names "${name}", which is already registered by the runtime`,
|
|
369
|
+
])
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
emailProviders.set(name, { build: () => sender, registeredBy: '<additionalEmailProviders>' })
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
265
376
|
const accumulator: Accumulator = {
|
|
266
377
|
routers: [],
|
|
267
378
|
publicRouters: [],
|
|
@@ -295,8 +406,30 @@ export async function registerModules(options: ModuleRuntimeOptions): Promise<Mo
|
|
|
295
406
|
entitlements.provide(createModuleConfigProvider(options.moduleConfig))
|
|
296
407
|
}
|
|
297
408
|
|
|
409
|
+
// Register every module's event catalog before any `register` runs, so a
|
|
410
|
+
// module that subscribes to another module's event during its own registration
|
|
411
|
+
// finds a validated name.
|
|
298
412
|
for (const module of ordered) {
|
|
299
|
-
|
|
413
|
+
if (module.events !== undefined) {
|
|
414
|
+
try {
|
|
415
|
+
events.registerCatalog({ moduleId: module.id, events: module.events })
|
|
416
|
+
} catch (error: unknown) {
|
|
417
|
+
throw new ModuleBootError([describeThrown(error)], { cause: error })
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
for (const module of ordered) {
|
|
423
|
+
const context = createModuleContext(
|
|
424
|
+
module,
|
|
425
|
+
accumulator,
|
|
426
|
+
options,
|
|
427
|
+
events,
|
|
428
|
+
entitlements,
|
|
429
|
+
moduleCatalog,
|
|
430
|
+
emailProxy,
|
|
431
|
+
emailProviders,
|
|
432
|
+
)
|
|
300
433
|
|
|
301
434
|
try {
|
|
302
435
|
await module.register(context)
|
|
@@ -313,9 +446,44 @@ export async function registerModules(options: ModuleRuntimeOptions): Promise<Mo
|
|
|
313
446
|
context.log.debug('module registered')
|
|
314
447
|
}
|
|
315
448
|
|
|
449
|
+
// Resolve the configured provider. The lookup happens after every module has
|
|
450
|
+
// registered, so a module registering a provider late in the list still wins
|
|
451
|
+
// if the config names it. An unknown name is a boot error whose message lists
|
|
452
|
+
// every registered name, so a self-hoster sees what they had to pick from.
|
|
453
|
+
const chosen = emailProviders.get(options.email.provider)
|
|
454
|
+
|
|
455
|
+
if (chosen === undefined) {
|
|
456
|
+
const available = [...emailProviders.keys()].sort()
|
|
457
|
+
|
|
458
|
+
throw new ModuleBootError([
|
|
459
|
+
`email.provider is "${options.email.provider}", which no module registered. Available: ${available.join(', ')}`,
|
|
460
|
+
])
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// Only the chosen provider is built. A registered-but-unused provider never
|
|
464
|
+
// runs its factory, so a self-hoster who leaves a module in `modules:` but
|
|
465
|
+
// picks a different provider is not asked for the unused module's env.
|
|
466
|
+
let chosenSender: EmailSender
|
|
467
|
+
|
|
468
|
+
try {
|
|
469
|
+
chosenSender = chosen.build()
|
|
470
|
+
} catch (error: unknown) {
|
|
471
|
+
if (error instanceof ModuleBootError) {
|
|
472
|
+
throw error
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
throw new ModuleBootError(
|
|
476
|
+
[`email provider "${options.email.provider}" (${chosen.registeredBy}) failed to build: ${describeThrown(error)}`],
|
|
477
|
+
{ cause: error },
|
|
478
|
+
)
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
emailProxy.setTarget(chosenSender)
|
|
482
|
+
|
|
316
483
|
options.logger.info('modules registered', {
|
|
317
484
|
count: ordered.length,
|
|
318
485
|
ids: ordered.map((module) => module.id),
|
|
486
|
+
emailProvider: options.email.provider,
|
|
319
487
|
})
|
|
320
488
|
|
|
321
489
|
return {
|
|
@@ -1,6 +1,10 @@
|
|
|
1
|
+
import type { EventActor, EventTarget, KelpieEvent } from '@kelpie/schemas'
|
|
2
|
+
|
|
1
3
|
import type { Database } from '../lib/database.ts'
|
|
4
|
+
import type { IdFactory } from '../lib/ids.ts'
|
|
2
5
|
import type { Logger } from '../lib/logger.ts'
|
|
3
|
-
import
|
|
6
|
+
import { checkEventCycle, currentEventChain } from './events.ts'
|
|
7
|
+
import type { EventBus, EventName, KelpieEventMap } from './events.ts'
|
|
4
8
|
|
|
5
9
|
/**
|
|
6
10
|
* One service call, one transaction, per `architecture.md`.
|
|
@@ -19,9 +23,21 @@ export type Transaction = Parameters<Parameters<Database['transaction']>[0]>[0]
|
|
|
19
23
|
*/
|
|
20
24
|
export type Queryable = Database | Transaction
|
|
21
25
|
|
|
22
|
-
|
|
26
|
+
type EventMapFor<Name extends EventName> = keyof KelpieEventMap extends never
|
|
27
|
+
? unknown
|
|
28
|
+
: Name extends keyof KelpieEventMap
|
|
29
|
+
? KelpieEventMap[Name]
|
|
30
|
+
: unknown
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Collects events during the transaction. Nothing leaves until commit.
|
|
34
|
+
*
|
|
35
|
+
* `emit` stamps the envelope (id, workspaceId, actor, occurredAt, causedBy) so
|
|
36
|
+
* services pass only what they know: the name, the target the event refers to,
|
|
37
|
+
* and the module-defined payload.
|
|
38
|
+
*/
|
|
23
39
|
export interface BufferedEvents {
|
|
24
|
-
emit<Name extends
|
|
40
|
+
emit<Name extends EventName>(name: Name, target: EventTarget, data: EventMapFor<Name>): void
|
|
25
41
|
}
|
|
26
42
|
|
|
27
43
|
export interface TransactionContext {
|
|
@@ -29,48 +45,100 @@ export interface TransactionContext {
|
|
|
29
45
|
readonly events: BufferedEvents
|
|
30
46
|
}
|
|
31
47
|
|
|
48
|
+
export interface TransactionOptions {
|
|
49
|
+
/**
|
|
50
|
+
* The tenant this work belongs to. Every event emitted from the scope stamps
|
|
51
|
+
* it into the envelope. Required when any event is emitted; a scope that
|
|
52
|
+
* emits nothing may omit it.
|
|
53
|
+
*/
|
|
54
|
+
readonly workspaceId?: string
|
|
55
|
+
/** Who caused this work. Defaults to `system`. */
|
|
56
|
+
readonly actor?: EventActor
|
|
57
|
+
}
|
|
58
|
+
|
|
32
59
|
/** Runs `work` in one transaction and publishes its events after commit. */
|
|
33
60
|
export type TransactionScope = <Result>(
|
|
34
61
|
work: (context: TransactionContext) => Promise<Result>,
|
|
62
|
+
options?: TransactionOptions,
|
|
35
63
|
) => Promise<Result>
|
|
36
64
|
|
|
37
|
-
/**
|
|
38
|
-
* A buffered event keeps its publication as a closure. The name and payload were
|
|
39
|
-
* correlated at the `emit` call site; storing them as a pair would lose that and
|
|
40
|
-
* force a cast at publication.
|
|
41
|
-
*/
|
|
42
65
|
interface BufferedEvent {
|
|
43
|
-
readonly name:
|
|
44
|
-
readonly
|
|
66
|
+
readonly name: string
|
|
67
|
+
readonly envelope: KelpieEvent<string, unknown>
|
|
45
68
|
}
|
|
46
69
|
|
|
70
|
+
const SYSTEM_ACTOR: EventActor = { kind: 'system' }
|
|
71
|
+
|
|
47
72
|
export interface TransactionScopeDependencies {
|
|
48
73
|
readonly db: Database
|
|
49
74
|
readonly bus: EventBus
|
|
50
75
|
readonly logger: Logger
|
|
76
|
+
readonly createId: IdFactory
|
|
77
|
+
/** Injected so a test can pin timestamps. Defaults to `Date.now`. */
|
|
78
|
+
readonly now?: () => Date
|
|
79
|
+
/** Chain-depth cap. Reads `KELPIE_EVENT_MAX_DEPTH`; otherwise 8. */
|
|
80
|
+
readonly maxDepth?: number
|
|
51
81
|
}
|
|
52
82
|
|
|
53
83
|
export function createTransactionScope(dependencies: TransactionScopeDependencies): TransactionScope {
|
|
54
|
-
|
|
84
|
+
const now = dependencies.now ?? ((): Date => new Date())
|
|
85
|
+
const maxDepth = dependencies.maxDepth ?? readMaxDepthFromEnv() ?? 8
|
|
86
|
+
|
|
87
|
+
return async function runInTransaction(work, options) {
|
|
88
|
+
const actor = options?.actor ?? SYSTEM_ACTOR
|
|
89
|
+
const workspaceId = options?.workspaceId
|
|
55
90
|
const buffered: BufferedEvent[] = []
|
|
56
91
|
|
|
57
92
|
const result = await dependencies.db.transaction((tx) =>
|
|
58
93
|
work({
|
|
59
94
|
tx,
|
|
60
95
|
events: {
|
|
61
|
-
emit(name,
|
|
62
|
-
|
|
96
|
+
emit(name, target, data) {
|
|
97
|
+
if (workspaceId === undefined || workspaceId.length === 0) {
|
|
98
|
+
throw new Error(
|
|
99
|
+
`event "${name}" was emitted without a workspaceId: pass it via TransactionOptions`,
|
|
100
|
+
)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const chain = currentEventChain()
|
|
104
|
+
const outcome = checkEventCycle(chain, name, target, maxDepth)
|
|
105
|
+
|
|
106
|
+
if (outcome.kind !== 'ok') {
|
|
107
|
+
dependencies.logger.error('event cycle guard dropped emit', {
|
|
108
|
+
event: name,
|
|
109
|
+
target,
|
|
110
|
+
reason: outcome.kind,
|
|
111
|
+
chain: chain.map((entry) => ({
|
|
112
|
+
id: entry.id,
|
|
113
|
+
name: entry.name,
|
|
114
|
+
target: { type: entry.targetType, id: entry.targetId },
|
|
115
|
+
})),
|
|
116
|
+
})
|
|
117
|
+
return
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const envelope: KelpieEvent<string, unknown> = {
|
|
121
|
+
id: dependencies.createId('event'),
|
|
122
|
+
name,
|
|
123
|
+
workspaceId,
|
|
124
|
+
actor,
|
|
125
|
+
occurredAt: now().toISOString(),
|
|
126
|
+
target,
|
|
127
|
+
...(chain.length > 0 ? { causedBy: chain[chain.length - 1]!.id } : {}),
|
|
128
|
+
data: data as unknown,
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
buffered.push({ name, envelope })
|
|
63
132
|
},
|
|
64
133
|
},
|
|
65
134
|
}),
|
|
66
135
|
)
|
|
67
136
|
|
|
68
137
|
// Past this line the transaction has committed. Publishing is deliberately
|
|
69
|
-
// not awaited:
|
|
70
|
-
//
|
|
71
|
-
// graceful shutdown wait via `bus.drain()`.
|
|
138
|
+
// not awaited: an outbound HTTP delivery must not sit inside the request
|
|
139
|
+
// that triggered it. Tests and graceful shutdown wait via `bus.drain()`.
|
|
72
140
|
for (const event of buffered) {
|
|
73
|
-
void
|
|
141
|
+
void dependencies.bus.publish(event.envelope as never).catch((error: unknown) => {
|
|
74
142
|
dependencies.logger.error('event publication failed', { event: event.name, error })
|
|
75
143
|
})
|
|
76
144
|
}
|
|
@@ -78,3 +146,12 @@ export function createTransactionScope(dependencies: TransactionScopeDependencie
|
|
|
78
146
|
return result
|
|
79
147
|
}
|
|
80
148
|
}
|
|
149
|
+
|
|
150
|
+
function readMaxDepthFromEnv(): number | undefined {
|
|
151
|
+
const raw = process.env.KELPIE_EVENT_MAX_DEPTH
|
|
152
|
+
if (raw === undefined || raw === '') {
|
|
153
|
+
return undefined
|
|
154
|
+
}
|
|
155
|
+
const parsed = Number.parseInt(raw, 10)
|
|
156
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : undefined
|
|
157
|
+
}
|
package/src/testing/app.ts
CHANGED
|
@@ -5,7 +5,7 @@ import type { AppBindings } from '../app.ts'
|
|
|
5
5
|
import type { Actor } from '../lib/actor.ts'
|
|
6
6
|
import type { Environment } from '../lib/config.ts'
|
|
7
7
|
import type { DatabaseProbe } from '../lib/database.ts'
|
|
8
|
-
import { createLogger } from '../lib/logger.ts'
|
|
8
|
+
import { createCaptureTransport, createLogger } from '../lib/logger.ts'
|
|
9
9
|
import { rateLimitConfigFrom, rateLimitConfigSchema } from '../lib/rateLimit.ts'
|
|
10
10
|
import type { RateLimitConfig } from '../lib/rateLimit.ts'
|
|
11
11
|
import type { KelpieModule } from '../runtime/module.ts'
|
|
@@ -13,7 +13,7 @@ import type { ModuleContributions } from '../runtime/registry.ts'
|
|
|
13
13
|
import type { EntitlementRegistry } from '../runtime/entitlements.ts'
|
|
14
14
|
import { registerModules } from '../runtime/registry.ts'
|
|
15
15
|
import { TEST_ENVIRONMENT } from './environment.ts'
|
|
16
|
-
import { createTestServices } from './services.ts'
|
|
16
|
+
import { TEST_EMAIL_FROM, TEST_EMAIL_PROVIDER, createTestServices } from './services.ts'
|
|
17
17
|
import type { TestServices } from './services.ts'
|
|
18
18
|
|
|
19
19
|
/** The same defaults `loadConfig` produces from an empty environment: one source of numbers for both. */
|
|
@@ -71,14 +71,22 @@ const reachableDatabase = (): Promise<DatabaseProbe> => Promise.resolve({ reacha
|
|
|
71
71
|
|
|
72
72
|
export async function createTestApp(options: TestAppOptions = {}): Promise<TestApp> {
|
|
73
73
|
const logLines: string[] = []
|
|
74
|
-
const logger = createLogger(
|
|
74
|
+
const logger = createLogger({
|
|
75
|
+
level: 'debug',
|
|
76
|
+
transports: [createCaptureTransport((line) => logLines.push(line))],
|
|
77
|
+
})
|
|
75
78
|
|
|
76
79
|
const services = options.services ?? createTestServices()
|
|
80
|
+
// Seed the test collecting sender under a stable name and point the email
|
|
81
|
+
// config at it. A test that wants a real provider module in the mix registers
|
|
82
|
+
// it as usual; picking that name in `email.provider` overrides this default.
|
|
77
83
|
const contributions = await registerModules({
|
|
78
84
|
modules: options.modules ?? [],
|
|
79
85
|
environment: options.environment ?? TEST_ENVIRONMENT,
|
|
80
86
|
logger,
|
|
81
87
|
events: services.events,
|
|
88
|
+
email: { provider: TEST_EMAIL_PROVIDER, from: TEST_EMAIL_FROM },
|
|
89
|
+
additionalEmailProviders: new Map([[TEST_EMAIL_PROVIDER, services.emailSender]]),
|
|
82
90
|
...(options.entitlements === undefined ? {} : { entitlements: options.entitlements }),
|
|
83
91
|
...(options.moduleConfig === undefined ? {} : { moduleConfig: options.moduleConfig }),
|
|
84
92
|
...(options.resolveActor === undefined ? {} : { resolveActor: options.resolveActor }),
|
package/src/testing/database.ts
CHANGED
|
@@ -9,7 +9,7 @@ import { coreModules } from '../modules/core.ts'
|
|
|
9
9
|
import { runMigrations } from '../runtime/migrate.ts'
|
|
10
10
|
import { registerModules } from '../runtime/registry.ts'
|
|
11
11
|
import { TEST_ENVIRONMENT } from './environment.ts'
|
|
12
|
-
import { createTestServices } from './services.ts'
|
|
12
|
+
import { TEST_EMAIL_FROM, TEST_EMAIL_PROVIDER, createTestServices } from './services.ts'
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* A migrated database for integration tests.
|
|
@@ -20,7 +20,7 @@ import { createTestServices } from './services.ts'
|
|
|
20
20
|
* magnitude faster and just as isolated.
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
|
-
const silentLogger = createLogger('error',
|
|
23
|
+
const silentLogger = createLogger({ level: 'error', transports: [] })
|
|
24
24
|
|
|
25
25
|
export interface TestDatabase extends DatabaseConnection {
|
|
26
26
|
/** Empties every table. Cheaper than re-running migrations between tests. */
|
|
@@ -72,12 +72,15 @@ export async function connectTestDatabase(connectionString: string): Promise<Tes
|
|
|
72
72
|
await ensureDatabaseExists(connectionString)
|
|
73
73
|
|
|
74
74
|
const connection = connectDatabase(connectionString, silentLogger)
|
|
75
|
+
const services = createTestServices({ db: connection.db })
|
|
75
76
|
const contributions = await registerModules({
|
|
76
77
|
modules: coreModules,
|
|
77
78
|
// Enough for core modules to configure themselves; no test reads it further.
|
|
78
79
|
environment: TEST_ENVIRONMENT,
|
|
79
80
|
logger: silentLogger,
|
|
80
|
-
services
|
|
81
|
+
services,
|
|
82
|
+
email: { provider: TEST_EMAIL_PROVIDER, from: TEST_EMAIL_FROM },
|
|
83
|
+
additionalEmailProviders: new Map([[TEST_EMAIL_PROVIDER, services.emailSender]]),
|
|
81
84
|
})
|
|
82
85
|
|
|
83
86
|
await runMigrations(connection.db, contributions.schemas, silentLogger)
|
package/src/testing/services.ts
CHANGED
|
@@ -4,6 +4,7 @@ import type { EmailMessage, EmailSender } from '../lib/email.ts'
|
|
|
4
4
|
import { createIdFactory } from '../lib/ids.ts'
|
|
5
5
|
import type { IdFactory } from '../lib/ids.ts'
|
|
6
6
|
import { createLogger } from '../lib/logger.ts'
|
|
7
|
+
import type { SecretEncryptionConfig } from '../lib/secrets.ts'
|
|
7
8
|
import { createEventBus } from '../runtime/events.ts'
|
|
8
9
|
import type { EventBus } from '../runtime/events.ts'
|
|
9
10
|
import type { ModuleServices } from '../runtime/module.ts'
|
|
@@ -20,14 +21,35 @@ import { createTransactionScope } from '../runtime/transaction.ts'
|
|
|
20
21
|
/** Never queried. A unit test that reaches the database fails here, loudly. */
|
|
21
22
|
const UNUSED_DATABASE_URL = 'postgres://unused:unused@127.0.0.1:1/unused'
|
|
22
23
|
|
|
24
|
+
/**
|
|
25
|
+
* The provider name `createTestApp` seeds and points `email.provider` at.
|
|
26
|
+
* Exported so a test spawning `registerModules` directly can pass the same
|
|
27
|
+
* name and reach the same collecting sender.
|
|
28
|
+
*/
|
|
29
|
+
export const TEST_EMAIL_PROVIDER = 'test'
|
|
30
|
+
|
|
31
|
+
/** The from address the test email config uses. Arbitrary but real. */
|
|
32
|
+
export const TEST_EMAIL_FROM = 'kelpie-test@example.com'
|
|
33
|
+
|
|
23
34
|
export interface TestServicesOptions {
|
|
24
35
|
readonly db?: Database
|
|
25
36
|
readonly now?: () => Date
|
|
26
37
|
readonly createId?: IdFactory
|
|
27
38
|
readonly events?: EventBus
|
|
39
|
+
/** Threaded through to `ModuleServices.appBaseUrl`, so a test exercises the preferred path. */
|
|
40
|
+
readonly appBaseUrl?: string
|
|
41
|
+
/** Threaded through to `ModuleServices.secretEncryption`, so a test exercises the preferred path. */
|
|
42
|
+
readonly secretEncryption?: SecretEncryptionConfig
|
|
28
43
|
}
|
|
29
44
|
|
|
30
45
|
export interface TestServices extends ModuleServices {
|
|
46
|
+
/**
|
|
47
|
+
* The collecting sender `createTestApp` seeds under `TEST_EMAIL_PROVIDER`.
|
|
48
|
+
* `services.emailSender` (this field) is what actually receives calls; a
|
|
49
|
+
* consumer module reaches it through `context.email`, which the runtime's
|
|
50
|
+
* proxy points at whichever provider `email.provider` picked.
|
|
51
|
+
*/
|
|
52
|
+
readonly emailSender: EmailSender
|
|
31
53
|
/** Everything the email port was asked to send, in order. */
|
|
32
54
|
readonly sentEmails: readonly EmailMessage[]
|
|
33
55
|
readonly events: EventBus
|
|
@@ -50,18 +72,25 @@ function createCollectingEmailSender(): { sender: EmailSender; sent: EmailMessag
|
|
|
50
72
|
}
|
|
51
73
|
|
|
52
74
|
export function createTestServices(options: TestServicesOptions = {}): TestServices {
|
|
53
|
-
const logger = createLogger('error',
|
|
75
|
+
const logger = createLogger({ level: 'error', transports: [] })
|
|
54
76
|
const db = options.db ?? connectDatabase(UNUSED_DATABASE_URL, logger).db
|
|
55
77
|
const events = options.events ?? createEventBus(logger)
|
|
78
|
+
const createId = options.createId ?? createIdFactory()
|
|
79
|
+
const now = options.now ?? ((): Date => new Date())
|
|
56
80
|
const { sender, sent } = createCollectingEmailSender()
|
|
57
81
|
|
|
58
82
|
return {
|
|
59
83
|
db,
|
|
60
|
-
transaction: createTransactionScope({ db, bus: events, logger }),
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
84
|
+
transaction: createTransactionScope({ db, bus: events, logger, createId, now }),
|
|
85
|
+
createId,
|
|
86
|
+
now,
|
|
87
|
+
emailSender: sender,
|
|
64
88
|
sentEmails: sent,
|
|
65
89
|
events,
|
|
90
|
+
// Spread conditionally so the field is absent (not `undefined`) when the
|
|
91
|
+
// caller does not opt in, keeping the fallback path (context.config) live
|
|
92
|
+
// for the many suites that never touch these fields.
|
|
93
|
+
...(options.appBaseUrl === undefined ? {} : { appBaseUrl: options.appBaseUrl }),
|
|
94
|
+
...(options.secretEncryption === undefined ? {} : { secretEncryption: options.secretEncryption }),
|
|
66
95
|
}
|
|
67
96
|
}
|