@cosmicdrift/kumiko-bundled-features 0.338.0 → 0.340.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/auth-email-password/handlers/invite-accept-with-login.write.js +2 -2
- package/dist/auth-email-password/handlers/invite-accept.write.js +2 -4
- package/dist/auth-email-password/handlers/invite-info.query.js +3 -1
- package/dist/auth-email-password/handlers/invite-signup-complete.write.js +2 -4
- package/dist/auth-email-password/handlers/signup-confirm.write.js +1 -1
- package/dist/auth-email-password/handlers/signup-request.write.js +1 -1
- package/dist/auth-email-password/web/auth-gate.d.ts +12 -2
- package/dist/auth-email-password/web/auth-gate.js +25 -1
- package/dist/auth-email-password/web/auth-redirect.d.ts +5 -0
- package/dist/auth-email-password/web/auth-redirect.js +58 -0
- package/dist/auth-email-password/web/client-plugin.d.ts +8 -1
- package/dist/auth-email-password/web/client-plugin.js +2 -0
- package/dist/auth-email-password/web/index.d.ts +2 -1
- package/dist/auth-email-password/web/index.js +1 -0
- package/dist/auth-email-password/web/session.d.ts +3 -1
- package/dist/auth-email-password/web/session.js +8 -2
- package/dist/auth-mfa/feature.js +1 -1
- package/dist/auth-mfa/handlers/enable-confirm-preauth.write.js +1 -1
- package/dist/auth-mfa/handlers/enable-start-preauth.write.js +1 -1
- package/dist/auth-mfa/handlers/verify.write.js +1 -1
- package/dist/auth-mfa/mfa-code-verifier.js +1 -1
- package/dist/auth-mfa/mfa-status-checker.js +1 -1
- package/dist/billing-foundation/constants.d.ts +7 -0
- package/dist/billing-foundation/constants.js +8 -0
- package/dist/billing-foundation/consumer-protection/confirmation-mail.js +3 -3
- package/dist/billing-foundation/consumer-protection/consent-gate.d.ts +3 -3
- package/dist/billing-foundation/consumer-protection/consent-gate.js +10 -2
- package/dist/billing-foundation/consumer-protection/consent-text.js +2 -2
- package/dist/billing-foundation/consumer-protection/register-termination.d.ts +1 -1
- package/dist/billing-foundation/consumer-protection/register-termination.js +31 -3
- package/dist/billing-foundation/consumer-protection/termination-mail.d.ts +3 -1
- package/dist/billing-foundation/consumer-protection/termination-mail.js +1 -1
- package/dist/billing-foundation/consumer-protection/termination-notify.d.ts +18 -0
- package/dist/billing-foundation/consumer-protection/termination-notify.js +21 -0
- package/dist/billing-foundation/consumer-protection/termination-pages.js +31 -2
- package/dist/billing-foundation/consumer-protection/termination-record.d.ts +24 -2
- package/dist/billing-foundation/consumer-protection/termination-record.js +102 -34
- package/dist/billing-foundation/consumer-protection/termination-request.js +10 -42
- package/dist/billing-foundation/consumer-protection/termination-texts.js +20 -20
- package/dist/billing-foundation/events.d.ts +19 -0
- package/dist/billing-foundation/events.js +12 -0
- package/dist/billing-foundation/handlers/create-checkout-session.write.js +2 -2
- package/dist/billing-foundation/handlers/start-plan-checkout.write.js +4 -2
- package/dist/billing-foundation/i18n.js +3 -0
- package/dist/billing-foundation/projection.js +23 -40
- package/dist/billing-foundation/tenant-destroy-hook.js +4 -4
- package/dist/billing-foundation/types.d.ts +6 -1
- package/dist/billing-foundation/validate-options.js +4 -0
- package/dist/channel-discord/discord-channel.d.ts +6 -0
- package/dist/channel-discord/discord-channel.js +19 -0
- package/dist/channel-discord/feature.d.ts +3 -0
- package/dist/channel-discord/feature.js +30 -0
- package/dist/channel-discord/index.d.ts +2 -0
- package/dist/channel-discord/index.js +2 -0
- package/dist/channel-slack/feature.d.ts +3 -0
- package/dist/channel-slack/feature.js +30 -0
- package/dist/channel-slack/index.d.ts +2 -0
- package/dist/channel-slack/index.js +2 -0
- package/dist/channel-slack/slack-channel.d.ts +5 -0
- package/dist/channel-slack/slack-channel.js +16 -0
- package/dist/channel-teams/feature.d.ts +3 -0
- package/dist/channel-teams/feature.js +34 -0
- package/dist/channel-teams/index.d.ts +2 -0
- package/dist/channel-teams/index.js +2 -0
- package/dist/channel-teams/teams-channel.d.ts +5 -0
- package/dist/channel-teams/teams-channel.js +38 -0
- package/dist/channel-telegram/feature.d.ts +3 -0
- package/dist/channel-telegram/feature.js +28 -0
- package/dist/channel-telegram/index.d.ts +2 -0
- package/dist/channel-telegram/index.js +2 -0
- package/dist/channel-telegram/telegram-channel.d.ts +8 -0
- package/dist/channel-telegram/telegram-channel.js +54 -0
- package/dist/crypto-shredding/handlers/forget-subject.write.js +6 -6
- package/dist/custom-fields/handlers/clear-custom-field.write.js +1 -1
- package/dist/custom-fields/handlers/define-tenant-field.write.js +1 -1
- package/dist/custom-fields/handlers/set-custom-field.write.js +1 -1
- package/dist/custom-fields/wire-user-data-rights.js +1 -1
- package/dist/data-retention/feature.js +1 -1
- package/dist/delivery/channel-context.d.ts +2 -1
- package/dist/delivery/channel-context.js +2 -2
- package/dist/delivery/chat-webhook-channel.d.ts +19 -0
- package/dist/delivery/chat-webhook-channel.js +48 -0
- package/dist/delivery/chat-webhook-sender.d.ts +21 -0
- package/dist/delivery/chat-webhook-sender.js +102 -0
- package/dist/delivery/delivery-service.js +3 -0
- package/dist/delivery/index.d.ts +2 -0
- package/dist/delivery/index.js +2 -0
- package/dist/delivery/jobs.js +1 -1
- package/dist/delivery/testing.d.ts +2 -0
- package/dist/delivery/types.d.ts +3 -1
- package/dist/delivery/types.js +1 -2
- package/dist/file-derivatives/handlers/public-variant-by-file-ref.query.js +1 -1
- package/dist/files-tenant-data/index.js +1 -1
- package/dist/form-draft/feature.js +1 -1
- package/dist/form-draft/handlers/discard.write.js +1 -1
- package/dist/form-draft/handlers/save.write.js +1 -1
- package/dist/inbound-mail-foundation/connect-routes.d.ts +7 -0
- package/dist/inbound-mail-foundation/connect-routes.js +13 -8
- package/dist/inbound-mail-foundation/feature.js +1 -1
- package/dist/inbound-mail-foundation/handlers/ingest-message.write.js +1 -1
- package/dist/inbound-mail-foundation/index.d.ts +1 -0
- package/dist/inbound-mail-foundation/index.js +1 -0
- package/dist/inbound-mail-foundation/oauth-access-token.d.ts +28 -0
- package/dist/inbound-mail-foundation/oauth-access-token.js +131 -0
- package/dist/inbound-mail-foundation/projection.js +37 -45
- package/dist/inbound-mail-foundation/tenant-destroy-hook.js +1 -1
- package/dist/inbound-mail-foundation/types.d.ts +18 -1
- package/dist/inbound-mail-foundation/watch-supervisor.d.ts +5 -0
- package/dist/inbound-mail-foundation/watch-supervisor.js +42 -14
- package/dist/managed-pages/handlers/set.write.js +1 -1
- package/dist/notes-history/handlers/add-note.write.js +1 -1
- package/dist/renderer-simple/feature.d.ts +8 -2
- package/dist/renderer-simple/feature.js +8 -5
- package/dist/renderer-simple/index.d.ts +2 -2
- package/dist/renderer-simple/index.js +1 -1
- package/dist/renderer-simple/simple-renderer.d.ts +15 -0
- package/dist/renderer-simple/simple-renderer.js +74 -27
- package/dist/secrets/feature.js +1 -1
- package/dist/sessions/feature.js +1 -1
- package/dist/sessions/handlers/revoke-all-for-user.write.js +1 -1
- package/dist/tenant-handover/handlers/claim.write.js +1 -2
- package/dist/tenant-lifecycle/feature.js +1 -1
- package/dist/tenant-lifecycle/handlers/cancel-destruction.write.js +3 -1
- package/dist/tenant-lifecycle/handlers/request-destruction.write.js +1 -1
- package/dist/tier-engine/feature.js +1 -1
- package/dist/tier-engine/handlers/get-tenant-tier.query.js +1 -1
- package/dist/tier-engine/handlers/set-tenant-tier.write.js +1 -1
- package/dist/user/handlers/update-roles.js +7 -18
- package/dist/user-data-rights/feature.js +2 -2
- package/dist/user-data-rights/handlers/cancel-deletion.write.js +1 -1
- package/dist/user-data-rights/handlers/confirm-deletion-by-token.write.js +1 -1
- package/dist/user-data-rights/handlers/download-by-job.query.js +1 -1
- package/dist/user-data-rights/handlers/download-by-token.query.js +1 -1
- package/dist/user-data-rights/handlers/export-status.query.js +1 -1
- package/dist/user-data-rights/handlers/lift-restriction.write.js +2 -4
- package/dist/user-data-rights/handlers/my-audit-log.query.js +1 -1
- package/dist/user-data-rights/handlers/request-deletion-by-email.write.js +1 -1
- package/dist/user-data-rights/handlers/request-deletion.write.js +1 -1
- package/dist/user-data-rights/handlers/request-export.write.js +1 -1
- package/dist/user-data-rights/handlers/restrict-account.write.js +2 -2
- package/dist/user-data-rights/handlers/run-forget-cleanup.write.js +1 -1
- package/dist/user-data-rights/lib/update-user-lifecycle.js +1 -1
- package/dist/workflow-runner/db/queries/due-runs.d.ts +4 -5
- package/dist/workflow-runner/db/queries/due-runs.js +5 -4
- package/dist/workflow-runner/event-subscriber.js +6 -2
- package/dist/workflow-runner/event-trigger.js +5 -0
- package/dist/workflow-runner/feature.js +4 -6
- package/dist/workflow-runner/handlers/resume-run.write.js +147 -59
- package/dist/workflow-runner/pending-projection.js +1 -0
- package/dist/workflow-runner/runner.d.ts +7 -1
- package/dist/workflow-runner/runner.js +1 -1
- package/dist/workflow-runner/tables.js +8 -1
- package/package.json +41 -9
- package/src/auth-email-password/changes.json +7 -0
- package/src/billing-foundation/changes.json +41 -0
- package/src/delivery/changes.json +7 -0
- package/src/inbound-mail-foundation/changes.json +14 -0
- package/src/renderer-simple/changes.json +9 -1
- package/src/user/changes.json +13 -6
- package/src/workflow-runner/changes.json +14 -0
- package/dist/billing-foundation/db/queries/payment-projection.d.ts +0 -2
- package/dist/billing-foundation/db/queries/payment-projection.js +0 -13
- package/dist/billing-foundation/db/queries/subscription-projection.d.ts +0 -2
- package/dist/billing-foundation/db/queries/subscription-projection.js +0 -7
- package/dist/inbound-mail-foundation/db/queries/inbound-projections.d.ts +0 -7
- package/dist/inbound-mail-foundation/db/queries/inbound-projections.js +0 -24
|
@@ -16,16 +16,19 @@
|
|
|
16
16
|
// otherwise (wait already wrote its effect; resume past it).
|
|
17
17
|
//
|
|
18
18
|
// The run's ORIGINAL trigger event (the one that started the whole run) is
|
|
19
|
-
// always
|
|
20
|
-
// never from the pending row
|
|
21
|
-
// WorkflowRunStartedPayload in ./runner).
|
|
22
|
-
//
|
|
23
|
-
//
|
|
19
|
+
// always re-read from the event store via the triggerEventRef on the run's
|
|
20
|
+
// own WORKFLOW_RUN_STARTED_TYPE event, never from the pending row (see
|
|
21
|
+
// WorkflowRunStartedPayload in ./runner). Neither the run stream nor the
|
|
22
|
+
// pending row holds a copy of a foreign payload, so an erased source event
|
|
23
|
+
// shows up here in its shredded form, and an unloadable one (archived
|
|
24
|
+
// stream) fails the run. The pending row's own
|
|
25
|
+
// triggerEventType/triggerEventRef columns are a DIFFERENT thing: for a
|
|
26
|
+
// waitForEvent suspension they point at the AWAITED event the Phase 3b
|
|
24
27
|
// event-subscriber matched (NULL until then, and NULL forever on a
|
|
25
|
-
// timeout-without-a-match). That payload becomes the resumed
|
|
26
|
-
// result for the skipped waitForEvent step itself (see
|
|
27
|
-
// steps/wait-for-event.ts) — pre-seeded into stepsAcc below so
|
|
28
|
-
// subsequent step's resolver can read `ctx.steps[awaits.someKey]`.
|
|
28
|
+
// timeout-without-a-match). That event's payload becomes the resumed
|
|
29
|
+
// pipeline's result for the skipped waitForEvent step itself (see
|
|
30
|
+
// resultKey on steps/wait-for-event.ts) — pre-seeded into stepsAcc below so
|
|
31
|
+
// a subsequent step's resolver can read `ctx.steps[awaits.someKey]`.
|
|
29
32
|
import { fetchOne } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
30
33
|
import { buildPipelineSteps, computeDefinitionFingerprint, describeWorkflowStepError, getStep, runStepList, SYSTEM_ROLE, WORKFLOW_AGGREGATE_TYPE, WORKFLOW_RESUMED_TYPE, WORKFLOW_RETRY_SCHEDULED_TYPE, WORKFLOW_RUN_COMPLETED_TYPE, WORKFLOW_RUN_FAILED_TYPE, WORKFLOW_RUN_STARTED_TYPE, WORKFLOW_WAITING_FOR_EVENT_TYPE, } from "@cosmicdrift/kumiko-framework/engine";
|
|
31
34
|
import { InternalError } from "@cosmicdrift/kumiko-framework/errors";
|
|
@@ -113,40 +116,134 @@ function isRunAlreadySettled(runEvents, stepIndex, expectedRetryAttempt) {
|
|
|
113
116
|
return resumed.stepIndex === stepIndex && resumed.retryAttempt === expectedRetryAttempt;
|
|
114
117
|
});
|
|
115
118
|
}
|
|
116
|
-
function
|
|
119
|
+
function isRecord(value) {
|
|
120
|
+
return typeof value === "object" && value !== null;
|
|
121
|
+
}
|
|
122
|
+
function isTriggerEventRef(value) {
|
|
123
|
+
return (isRecord(value) &&
|
|
124
|
+
typeof value["eventId"] === "string" &&
|
|
125
|
+
typeof value["aggregateId"] === "string" &&
|
|
126
|
+
typeof value["version"] === "number");
|
|
127
|
+
}
|
|
128
|
+
function readTriggerSource(startedPayload) {
|
|
129
|
+
if (!isRecord(startedPayload) || typeof startedPayload["triggerEventType"] !== "string") {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
const eventType = startedPayload["triggerEventType"];
|
|
133
|
+
const ref = startedPayload["triggerEventRef"];
|
|
134
|
+
if (isTriggerEventRef(ref)) {
|
|
135
|
+
return { kind: "ref", eventType, ref };
|
|
136
|
+
}
|
|
137
|
+
if ("triggerPayload" in startedPayload) {
|
|
138
|
+
return { kind: "legacy-copy", eventType, payload: startedPayload["triggerPayload"] };
|
|
139
|
+
}
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
async function loadReferencedEvent(ctx, ref) {
|
|
143
|
+
const events = await ctx.loadAggregate(ref.aggregateId);
|
|
144
|
+
return events.find((e) => e.id === ref.eventId);
|
|
145
|
+
}
|
|
146
|
+
async function recoverTriggerEvent(ctx, runEvents, runId, user) {
|
|
117
147
|
const started = runEvents.find((e) => e.type === WORKFLOW_RUN_STARTED_TYPE);
|
|
118
148
|
if (!started) {
|
|
119
149
|
throw new InternalError({
|
|
120
150
|
message: `workflow-runner:write:resume-run: run ${runId} has no ${WORKFLOW_RUN_STARTED_TYPE} event — cannot recover its trigger event.`,
|
|
121
151
|
});
|
|
122
152
|
}
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
153
|
+
const source = readTriggerSource(started.payload);
|
|
154
|
+
if (!source) {
|
|
155
|
+
throw new InternalError({
|
|
156
|
+
message: `workflow-runner:write:resume-run: run ${runId} has a malformed ${WORKFLOW_RUN_STARTED_TYPE} event — neither triggerEventRef nor a legacy triggerPayload.`,
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
if (source.kind === "legacy-copy") {
|
|
160
|
+
return {
|
|
161
|
+
event: { type: source.eventType, payload: source.payload, user },
|
|
162
|
+
unavailable: false,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
const triggerEvent = await loadReferencedEvent(ctx, source.ref);
|
|
166
|
+
if (!triggerEvent) {
|
|
167
|
+
return { event: null, unavailable: true };
|
|
168
|
+
}
|
|
169
|
+
return {
|
|
170
|
+
event: { type: source.eventType, payload: triggerEvent.payload, user },
|
|
171
|
+
unavailable: false,
|
|
128
172
|
};
|
|
129
|
-
|
|
173
|
+
}
|
|
174
|
+
// NULL ref + NULL payload = timeout without a match: the step result stays
|
|
175
|
+
// undefined, same as before. A matched row whose event can no longer be loaded
|
|
176
|
+
// is NOT folded into that case: a later step could not tell "nothing arrived"
|
|
177
|
+
// from "arrived but gone" and would run on missing data, so the run fails
|
|
178
|
+
// instead. Rows matched before the reference existed still carry the copy.
|
|
179
|
+
async function loadAwaitedPayload(ctx, pending) {
|
|
180
|
+
if (!isTriggerEventRef(pending.triggerEventRef)) {
|
|
181
|
+
return { unavailable: false, payload: pending.triggerPayload ?? undefined };
|
|
182
|
+
}
|
|
183
|
+
const awaited = await loadReferencedEvent(ctx, pending.triggerEventRef);
|
|
184
|
+
return awaited ? { unavailable: false, payload: awaited.payload } : { unavailable: true };
|
|
130
185
|
}
|
|
131
186
|
// The suspended waitForEvent step itself is skipped on resume
|
|
132
187
|
// (resumeFrom = stepIndex + 1) — its run() never re-executes, so its
|
|
133
188
|
// resultKey (args.event, see steps/wait-for-event.ts) never gets
|
|
134
|
-
// populated by the normal runStepList loop. Seed it here from the
|
|
135
|
-
//
|
|
136
|
-
//
|
|
137
|
-
|
|
138
|
-
// resolver reading it just sees `undefined`, no special-casing needed.
|
|
139
|
-
function seedResumedStepResults(pending, steps, stepIndex) {
|
|
189
|
+
// populated by the normal runStepList loop. Seed it here from the awaited
|
|
190
|
+
// event's payload so a subsequent step's resolver sees the matched event via
|
|
191
|
+
// `ctx.steps[awaits.someKey]`, same as any other step result.
|
|
192
|
+
function seedResumedStepResults(suspensionEventType, awaitedPayload, steps, stepIndex) {
|
|
140
193
|
const stepsAcc = {};
|
|
141
|
-
if (
|
|
194
|
+
if (suspensionEventType === WORKFLOW_WAITING_FOR_EVENT_TYPE) {
|
|
142
195
|
const suspendedStep = steps[stepIndex];
|
|
143
196
|
const key = suspendedStep && getStep(suspendedStep.kind)?.resultKey?.(suspendedStep.args);
|
|
144
197
|
if (key !== undefined) {
|
|
145
|
-
stepsAcc[key] =
|
|
198
|
+
stepsAcc[key] = awaitedPayload;
|
|
146
199
|
}
|
|
147
200
|
}
|
|
148
201
|
return stepsAcc;
|
|
149
202
|
}
|
|
203
|
+
async function runResumedPipeline(ctx, input) {
|
|
204
|
+
const { workflow, workflowName, runId, stepIndex, resumeFrom, triggerEvent, awaitedPayload, pending, } = input;
|
|
205
|
+
try {
|
|
206
|
+
const steps = buildPipelineSteps(workflow.pipelineDef, triggerEvent);
|
|
207
|
+
const workflowCtx = {
|
|
208
|
+
runId,
|
|
209
|
+
workflowName,
|
|
210
|
+
stepIndex,
|
|
211
|
+
definitionFingerprint: pending.definitionFingerprint ?? undefined,
|
|
212
|
+
...(pending.retryAttempt !== null && { retryAttempt: pending.retryAttempt + 1 }),
|
|
213
|
+
};
|
|
214
|
+
const stepsAcc = seedResumedStepResults(pending.suspensionEventType, awaitedPayload, steps, stepIndex);
|
|
215
|
+
const outcome = await runStepList(steps, triggerEvent, ctx, stepsAcc, {}, workflowCtx, resumeFrom);
|
|
216
|
+
if (outcome.kind === "suspended") {
|
|
217
|
+
if (!isResumableSuspension(steps, outcome.stepIndex)) {
|
|
218
|
+
throw new WorkflowSuspensionUnsupportedError(workflowName, outcome.stepIndex);
|
|
219
|
+
}
|
|
220
|
+
// Another suspension further down the pipeline — pending-projection.ts
|
|
221
|
+
// already materialised the new row; nothing more to do this pass.
|
|
222
|
+
return { isSuccess: true, data: { outcome: "suspended" } };
|
|
223
|
+
}
|
|
224
|
+
const completedPayload = {
|
|
225
|
+
workflowName,
|
|
226
|
+
stepIndex: steps.length,
|
|
227
|
+
};
|
|
228
|
+
await ctx.unsafeAppendEvent({
|
|
229
|
+
aggregateId: runId,
|
|
230
|
+
aggregateType: WORKFLOW_AGGREGATE_TYPE,
|
|
231
|
+
type: WORKFLOW_RUN_COMPLETED_TYPE,
|
|
232
|
+
payload: completedPayload,
|
|
233
|
+
});
|
|
234
|
+
return { isSuccess: true, data: { outcome: "completed" } };
|
|
235
|
+
}
|
|
236
|
+
catch (error) {
|
|
237
|
+
log.warn("workflow run failed", { runId, workflowName, stepIndex, error: String(error) });
|
|
238
|
+
const failedPayload = {
|
|
239
|
+
workflowName,
|
|
240
|
+
stepIndex,
|
|
241
|
+
error: describeWorkflowStepError(error),
|
|
242
|
+
};
|
|
243
|
+
await appendRunFailed(ctx, runId, failedPayload);
|
|
244
|
+
return { isSuccess: true, data: { outcome: "failed" } };
|
|
245
|
+
}
|
|
246
|
+
}
|
|
150
247
|
export const resumeRunHandler = {
|
|
151
248
|
name: "resume-run",
|
|
152
249
|
agent: { expose: false },
|
|
@@ -198,48 +295,39 @@ export const resumeRunHandler = {
|
|
|
198
295
|
// pipeline; nothing left for this call to do.
|
|
199
296
|
return { isSuccess: true, data: { outcome: "already-resumed" } };
|
|
200
297
|
}
|
|
201
|
-
const
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
const steps = buildPipelineSteps(workflow.pipelineDef, triggerEvent);
|
|
205
|
-
const workflowCtx = {
|
|
206
|
-
runId,
|
|
298
|
+
const recovered = await recoverTriggerEvent(ctx, runEvents, runId, event.user);
|
|
299
|
+
if (recovered.unavailable) {
|
|
300
|
+
await appendRunFailed(ctx, runId, {
|
|
207
301
|
workflowName,
|
|
208
302
|
stepIndex,
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
};
|
|
212
|
-
const stepsAcc = seedResumedStepResults(pending, steps, stepIndex);
|
|
213
|
-
const outcome = await runStepList(steps, triggerEvent, ctx, stepsAcc, {}, workflowCtx, resumeFrom);
|
|
214
|
-
if (outcome.kind === "suspended") {
|
|
215
|
-
if (!isResumableSuspension(steps, outcome.stepIndex)) {
|
|
216
|
-
throw new WorkflowSuspensionUnsupportedError(workflowName, outcome.stepIndex);
|
|
217
|
-
}
|
|
218
|
-
// Another suspension further down the pipeline — pending-projection.ts
|
|
219
|
-
// already materialised the new row; nothing more to do this pass.
|
|
220
|
-
return { isSuccess: true, data: { outcome: "suspended" } };
|
|
221
|
-
}
|
|
222
|
-
const completedPayload = {
|
|
223
|
-
workflowName,
|
|
224
|
-
stepIndex: steps.length,
|
|
225
|
-
};
|
|
226
|
-
await ctx.unsafeAppendEvent({
|
|
227
|
-
aggregateId: runId,
|
|
228
|
-
aggregateType: WORKFLOW_AGGREGATE_TYPE,
|
|
229
|
-
type: WORKFLOW_RUN_COMPLETED_TYPE,
|
|
230
|
-
payload: completedPayload,
|
|
303
|
+
error: `Trigger event of run ${runId} is no longer available — cannot resume.`,
|
|
304
|
+
reason: "trigger_event_unavailable",
|
|
231
305
|
});
|
|
232
|
-
return { isSuccess: true, data: { outcome: "
|
|
306
|
+
return { isSuccess: true, data: { outcome: "failed" } };
|
|
233
307
|
}
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
308
|
+
const triggerEvent = recovered.event;
|
|
309
|
+
const awaited = pending.suspensionEventType === WORKFLOW_WAITING_FOR_EVENT_TYPE
|
|
310
|
+
? await loadAwaitedPayload(ctx, pending)
|
|
311
|
+
: { unavailable: false, payload: undefined };
|
|
312
|
+
if (awaited.unavailable) {
|
|
313
|
+
await appendRunFailed(ctx, runId, {
|
|
237
314
|
workflowName,
|
|
238
315
|
stepIndex,
|
|
239
|
-
error:
|
|
240
|
-
|
|
241
|
-
|
|
316
|
+
error: `Awaited event of run ${runId} is no longer available — cannot resume.`,
|
|
317
|
+
reason: "awaited_event_unavailable",
|
|
318
|
+
});
|
|
242
319
|
return { isSuccess: true, data: { outcome: "failed" } };
|
|
243
320
|
}
|
|
321
|
+
const resumeFrom = pending.suspensionEventType === WORKFLOW_RETRY_SCHEDULED_TYPE ? stepIndex : stepIndex + 1;
|
|
322
|
+
return runResumedPipeline(ctx, {
|
|
323
|
+
workflow,
|
|
324
|
+
workflowName,
|
|
325
|
+
runId,
|
|
326
|
+
stepIndex,
|
|
327
|
+
resumeFrom,
|
|
328
|
+
triggerEvent,
|
|
329
|
+
awaitedPayload: awaited.payload,
|
|
330
|
+
pending,
|
|
331
|
+
});
|
|
244
332
|
},
|
|
245
333
|
};
|
|
@@ -27,6 +27,7 @@ async function upsertPending(tx, row) {
|
|
|
27
27
|
// Phase 3's event-subscriber writes these when the awaited event
|
|
28
28
|
// arrives — Phase 1 never touches them, insert or update.
|
|
29
29
|
triggerEventType: null,
|
|
30
|
+
triggerEventRef: null,
|
|
30
31
|
triggerPayload: null,
|
|
31
32
|
}, { conflictKeys: ["tenantId", "runId", "stepIndex"] });
|
|
32
33
|
}
|
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
import type { HandlerContext, StepInstance, WorkflowDefinition, WriteEvent } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
2
|
export declare function isResumableSuspension(steps: readonly StepInstance[], stepIndex: number): boolean;
|
|
3
|
+
export type TriggerEventRef = {
|
|
4
|
+
readonly eventId: string;
|
|
5
|
+
readonly aggregateId: string;
|
|
6
|
+
readonly version: number;
|
|
7
|
+
};
|
|
3
8
|
export type WorkflowRunStartedPayload = {
|
|
4
9
|
readonly workflowName: string;
|
|
5
10
|
readonly triggerEventType: string;
|
|
6
|
-
readonly
|
|
11
|
+
readonly triggerEventRef: TriggerEventRef;
|
|
7
12
|
readonly definitionFingerprint: string;
|
|
8
13
|
readonly idempotencyKey?: string;
|
|
9
14
|
};
|
|
@@ -35,6 +40,7 @@ export declare function startAndRunWorkflow(args: {
|
|
|
35
40
|
readonly runId: string;
|
|
36
41
|
readonly workflow: WorkflowDefinition;
|
|
37
42
|
readonly triggerEvent: WriteEvent;
|
|
43
|
+
readonly triggerEventRef: TriggerEventRef;
|
|
38
44
|
readonly idempotencyKey?: string;
|
|
39
45
|
readonly handlerCtx: HandlerContext;
|
|
40
46
|
}): Promise<{
|
|
@@ -42,7 +42,7 @@ export async function startAndRunWorkflow(args) {
|
|
|
42
42
|
const startedPayload = {
|
|
43
43
|
workflowName: args.workflow.name,
|
|
44
44
|
triggerEventType: args.triggerEvent.type,
|
|
45
|
-
|
|
45
|
+
triggerEventRef: args.triggerEventRef,
|
|
46
46
|
definitionFingerprint: fingerprint,
|
|
47
47
|
...(args.idempotencyKey && { idempotencyKey: args.idempotencyKey }),
|
|
48
48
|
};
|
|
@@ -38,8 +38,14 @@ export const workflowRunPendingTable = pgTable("workflow_run_pending", {
|
|
|
38
38
|
waitEventType: text("wait_event_type"),
|
|
39
39
|
matchExpr: jsonb("match_expr"),
|
|
40
40
|
// Written by the Phase 3 event-subscriber when the awaited event
|
|
41
|
-
// arrives — always NULL in Phase 1/2.
|
|
41
|
+
// arrives — always NULL in Phase 1/2. The row points at the awaited event
|
|
42
|
+
// ({ eventId, aggregateId, version }); resume-run re-reads it from the
|
|
43
|
+
// event store, so erasing the event also covers the workflow.
|
|
42
44
|
triggerEventType: text("trigger_event_type"),
|
|
45
|
+
triggerEventRef: jsonb("trigger_event_ref"),
|
|
46
|
+
// Legacy copy of the awaited payload. No longer written; only rows
|
|
47
|
+
// matched before the reference existed still carry it, and resume-run
|
|
48
|
+
// falls back to it for those.
|
|
43
49
|
triggerPayload: jsonb("trigger_payload"),
|
|
44
50
|
}, (t) => [
|
|
45
51
|
primaryKey({
|
|
@@ -65,6 +71,7 @@ export const workflowRunPendingTableMeta = defineUnmanagedTable({
|
|
|
65
71
|
{ name: "wait_event_type", pgType: "text", notNull: false },
|
|
66
72
|
{ name: "match_expr", pgType: "jsonb", notNull: false },
|
|
67
73
|
{ name: "trigger_event_type", pgType: "text", notNull: false },
|
|
74
|
+
{ name: "trigger_event_ref", pgType: "jsonb", notNull: false },
|
|
68
75
|
{ name: "trigger_payload", pgType: "jsonb", notNull: false },
|
|
69
76
|
],
|
|
70
77
|
indexes: [
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-bundled-features",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.340.0",
|
|
4
4
|
"description": "Built-in features — tenant, user, auth, delivery. The stuff you'd rewrite anyway, already typed.",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
@@ -365,6 +365,22 @@
|
|
|
365
365
|
"types": "./dist/channel-push/index.d.ts",
|
|
366
366
|
"default": "./dist/channel-push/index.js"
|
|
367
367
|
},
|
|
368
|
+
"./channel-slack": {
|
|
369
|
+
"types": "./dist/channel-slack/index.d.ts",
|
|
370
|
+
"default": "./dist/channel-slack/index.js"
|
|
371
|
+
},
|
|
372
|
+
"./channel-discord": {
|
|
373
|
+
"types": "./dist/channel-discord/index.d.ts",
|
|
374
|
+
"default": "./dist/channel-discord/index.js"
|
|
375
|
+
},
|
|
376
|
+
"./channel-teams": {
|
|
377
|
+
"types": "./dist/channel-teams/index.d.ts",
|
|
378
|
+
"default": "./dist/channel-teams/index.js"
|
|
379
|
+
},
|
|
380
|
+
"./channel-telegram": {
|
|
381
|
+
"types": "./dist/channel-telegram/index.d.ts",
|
|
382
|
+
"default": "./dist/channel-telegram/index.js"
|
|
383
|
+
},
|
|
368
384
|
"./renderer-simple": {
|
|
369
385
|
"types": "./dist/renderer-simple/index.d.ts",
|
|
370
386
|
"default": "./dist/renderer-simple/index.js"
|
|
@@ -483,12 +499,12 @@
|
|
|
483
499
|
}
|
|
484
500
|
},
|
|
485
501
|
"dependencies": {
|
|
486
|
-
"@cosmicdrift/kumiko-dispatcher-live": "0.
|
|
487
|
-
"@cosmicdrift/kumiko-framework": "0.
|
|
488
|
-
"@cosmicdrift/kumiko-headless": "0.
|
|
489
|
-
"@cosmicdrift/kumiko-renderer": "0.
|
|
490
|
-
"@cosmicdrift/kumiko-renderer-web": "0.
|
|
491
|
-
"@cosmicdrift/kumiko-types": "0.
|
|
502
|
+
"@cosmicdrift/kumiko-dispatcher-live": "0.340.0",
|
|
503
|
+
"@cosmicdrift/kumiko-framework": "0.340.0",
|
|
504
|
+
"@cosmicdrift/kumiko-headless": "0.340.0",
|
|
505
|
+
"@cosmicdrift/kumiko-renderer": "0.340.0",
|
|
506
|
+
"@cosmicdrift/kumiko-renderer-web": "0.340.0",
|
|
507
|
+
"@cosmicdrift/kumiko-types": "0.340.0",
|
|
492
508
|
"@mollie/api-client": "^4.5.0",
|
|
493
509
|
"@node-rs/argon2": "^2.0.2",
|
|
494
510
|
"@types/mailparser": "^3.4.6",
|
|
@@ -859,6 +875,22 @@
|
|
|
859
875
|
"types": "./dist/channel-push/index.d.ts",
|
|
860
876
|
"default": "./dist/channel-push/index.js"
|
|
861
877
|
},
|
|
878
|
+
"./channel-slack": {
|
|
879
|
+
"types": "./dist/channel-slack/index.d.ts",
|
|
880
|
+
"default": "./dist/channel-slack/index.js"
|
|
881
|
+
},
|
|
882
|
+
"./channel-discord": {
|
|
883
|
+
"types": "./dist/channel-discord/index.d.ts",
|
|
884
|
+
"default": "./dist/channel-discord/index.js"
|
|
885
|
+
},
|
|
886
|
+
"./channel-teams": {
|
|
887
|
+
"types": "./dist/channel-teams/index.d.ts",
|
|
888
|
+
"default": "./dist/channel-teams/index.js"
|
|
889
|
+
},
|
|
890
|
+
"./channel-telegram": {
|
|
891
|
+
"types": "./dist/channel-telegram/index.d.ts",
|
|
892
|
+
"default": "./dist/channel-telegram/index.js"
|
|
893
|
+
},
|
|
862
894
|
"./renderer-simple": {
|
|
863
895
|
"types": "./dist/renderer-simple/index.d.ts",
|
|
864
896
|
"default": "./dist/renderer-simple/index.js"
|
|
@@ -985,8 +1017,8 @@
|
|
|
985
1017
|
],
|
|
986
1018
|
"devDependencies": {
|
|
987
1019
|
"@testing-library/user-event": "^14.6.1",
|
|
988
|
-
"@cosmicdrift/kumiko-locale-de": "0.
|
|
989
|
-
"@cosmicdrift/kumiko-locale-es": "0.
|
|
1020
|
+
"@cosmicdrift/kumiko-locale-de": "0.340.0",
|
|
1021
|
+
"@cosmicdrift/kumiko-locale-es": "0.340.0",
|
|
990
1022
|
"jsqr": "^1.4.0"
|
|
991
1023
|
}
|
|
992
1024
|
}
|
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.339.0",
|
|
4
|
+
"type": "improvement",
|
|
5
|
+
"title": "emailPasswordClient({ loginUrl, postLogoutUrl }) sends unauthenticated visitors to an external login page with a same-origin next path and navigates there on logout",
|
|
6
|
+
"detail": "emailPasswordClient accepts loginUrl and postLogoutUrl for external login pages",
|
|
7
|
+
"migration": "No code change needed."
|
|
8
|
+
},
|
|
2
9
|
{
|
|
3
10
|
"version": "0.335.0",
|
|
4
11
|
"type": "fix",
|
|
@@ -1,4 +1,45 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.340.0",
|
|
4
|
+
"type": "improvement",
|
|
5
|
+
"title": "Payment and subscription projections use typed bun-db helpers",
|
|
6
|
+
"detail": "`payment-received` inserts through `insertOnConflictDoNothing`, subscription events upsert through `upsertOnConflict`. The raw `db/queries/payment-projection.ts` and `subscription-projection.ts` modules are gone.",
|
|
7
|
+
"migration": "keine"
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
"version": "0.339.0",
|
|
11
|
+
"type": "improvement",
|
|
12
|
+
"title": "Missing or unchecked checkout consent returns 422 consent_required",
|
|
13
|
+
"detail": "The consent payload is optional in the `start-plan-checkout` and `create-checkout-session` input schemas and its two flags are plain booleans. `prepareConsent` throws `UnprocessableError(\"consent_required\")` with `billing-foundation.errors.consentRequired` when consumer protection is on and the consent is missing or either flag is not true.",
|
|
14
|
+
"migration": "Clients that treated the previous `400` validation error for a missing consent as the signal must check for `422` with `reason: \"consent_required\"`."
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"version": "0.339.0",
|
|
18
|
+
"type": "fix",
|
|
19
|
+
"title": "Contract confirmation, termination receipt and cancellation pages use \"du\"",
|
|
20
|
+
"detail": "The German contract confirmation mail, the termination and withdrawal receipt mail, the public cancellation pages and the subscription and payment submit messages address the customer with \"du\". The recorded consent statements are first-person and unchanged, so `consentTextVersion` stays the same."
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"version": "0.339.0",
|
|
24
|
+
"type": "fix",
|
|
25
|
+
"title": "The § 312k confirm POST forwards the visitor's host to /api/write",
|
|
26
|
+
"detail": "The `/legal/kuendigen` and `/legal/cancel` pages re-enter `/api/write` through the app. The re-entry now carries `Host`, `X-Forwarded-Host`, `X-Forwarded-Proto`, `X-Tenant` and the `kumiko_tenant` cookie as the visitor sent them, so a host-based `tenantResolver` resolves the same tenant as for a direct call; before, it saw no host and every confirm failed with `400 tenant_required`. The session cookie, `Authorization` and `X-Forwarded-For` are not forwarded.",
|
|
27
|
+
"migration": "keine"
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"version": "0.339.0",
|
|
31
|
+
"type": "improvement",
|
|
32
|
+
"title": "consumerProtection.terminationScope \"platform\" for hosts without a tenant",
|
|
33
|
+
"detail": "`consumerProtection.terminationScope` is `\"tenant-host\"` (default, unchanged) or `\"platform\"`. With `\"platform\"`, `request-contract-termination` is flagged `tenantlessAnonymous` and also runs on a host whose resolver returns no tenant: it finds the contract by the declarant's email as before, records on the matched tenant's subscription stream, and answers every outcome identically. An unknown option value fails at `createBillingFoundationFeature`.",
|
|
34
|
+
"migration": "Apps that serve the pages on a platform apex (publicstatus) set `consumerProtection.terminationScope: \"platform\"`. Other apps change nothing."
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"version": "0.339.0",
|
|
38
|
+
"type": "improvement",
|
|
39
|
+
"title": "Public termination declarations cancel at the provider in a job",
|
|
40
|
+
"detail": "For the public channel, `request-contract-termination` no longer calls the provider in the request. A matched request appends the PII-free `contract-termination-declared` event (`requestId`, `declarationType`, `terminationKind`, `receivedAtIso`, `locale`) via the system-only `declare-contract-termination`; the job `cancel-on-public-termination-declared` then runs `record-contract-termination`, which asks the provider for `cancel_at_period_end` and appends `contract-termination-requested` with the outcome. Matched and unmatched requests do the same work in the request, so the response time does not reveal whether the email is a customer. The job is idempotent per `requestId`. A provider without `cancelSubscription`, a provider error or a missing subscription now sends a separate operator notice with request id and tenant id but no name or email. A public withdrawal still makes no provider call. The account path (`terminate-contract`) stays synchronous.",
|
|
41
|
+
"migration": "Code that expected `contract-termination-requested` right after the public request must wait for the job (in tests: `drainJobs`)."
|
|
42
|
+
},
|
|
2
43
|
{
|
|
3
44
|
"version": "0.338.0",
|
|
4
45
|
"type": "improvement",
|
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.339.0",
|
|
4
|
+
"type": "improvement",
|
|
5
|
+
"title": "Slack, Discord, Microsoft Teams and Telegram delivery channels",
|
|
6
|
+
"detail": "`channel-slack`, `channel-discord`, `channel-teams` and `channel-telegram` register route-only delivery channels. Webhook URLs and the Telegram bot token live in the tenant's secrets; the send job reads them with an audit entry `<feature>:send`. `DeliveryChannel.resolve` is optional and `ChannelContext.secrets` is available on the send job path.",
|
|
7
|
+
"migration": "keine"
|
|
8
|
+
},
|
|
2
9
|
{
|
|
3
10
|
"version": "0.338.0",
|
|
4
11
|
"type": "improvement",
|
|
@@ -1,4 +1,18 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.340.0",
|
|
4
|
+
"type": "improvement",
|
|
5
|
+
"title": "Inbound mail projections use typed bun-db helpers",
|
|
6
|
+
"detail": "Mail-account and mail-thread events upsert through `upsertOnConflict` (`connectedAt` is only written on insert), inbound messages insert through `insertOnConflictDoNothing`. The raw `db/queries/inbound-projections.ts` module is gone.",
|
|
7
|
+
"migration": "keine"
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
"version": "0.339.0",
|
|
11
|
+
"type": "improvement",
|
|
12
|
+
"title": "Inbound mail OAuth accounts refresh their access token",
|
|
13
|
+
"detail": "`createInboundMailSupervisor` obtains a valid access token via the new `createOAuthAccessTokenManager` before `plugin.fetch`/`plugin.watch` for accounts whose provider declares `oauth` and whose authMethod is `oauth`/`xoauth2`, and passes it as `opts.accessToken` (`fetch`) or `{ accessToken }` (`watch`, 4th argument). `refreshAccessToken` receives an optional 4th argument `{ signal }` (aborted after 30 s); providers must pass it to their token-endpoint fetch, since the foundation awaits the provider and never drops a late result. The foundation has no single construction point for routes and supervisor, so apps that want connect to prime the cache and write under the refresh lock create one manager with `createOAuthAccessTokenManager({ db, secrets })` and pass it to both `createInboundMailSupervisor({ oauthTokens })` and `createInboundMailConnectRoutes({ oauthTokens })`; without it the supervisor builds its own manager and the callback writes the secret without the lock. `InboundAuthError` from `refreshAccessToken` moves the account to `auth_error`; the generic sync-error log and the `token_exchange_failed` response body no longer contain provider error messages.",
|
|
14
|
+
"migration": "Apps with OAuth inbound accounts create one `createOAuthAccessTokenManager({ db, secrets })` and pass it as `oauthTokens` to both `createInboundMailSupervisor` and `createInboundMailConnectRoutes`. Provider plugins with a `refreshAccessToken` pass the `signal` from its fourth argument to their token-endpoint fetch."
|
|
15
|
+
},
|
|
2
16
|
{
|
|
3
17
|
"version": "0.335.0",
|
|
4
18
|
"type": "fix",
|
|
@@ -1 +1,9 @@
|
|
|
1
|
-
[
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.339.0",
|
|
4
|
+
"type": "improvement",
|
|
5
|
+
"title": "createRendererSimpleFeature({ mailBranding }) brands every notification mail, including the auth mails, with product name or logo, primary color and footer text and links",
|
|
6
|
+
"detail": "renderer-simple accepts mailBranding for all framework notification mails",
|
|
7
|
+
"migration": "No code change needed."
|
|
8
|
+
}
|
|
9
|
+
]
|
package/src/user/changes.json
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.340.0",
|
|
4
|
+
"type": "improvement",
|
|
5
|
+
"title": "The last-SystemAdmin check uses jsonb containment through selectMany",
|
|
6
|
+
"detail": "Removing the SystemAdmin role still refuses to drop the last active SystemAdmin; the count now comes from `selectMany` with a roles containment filter instead of raw SQL.",
|
|
7
|
+
"migration": "keine"
|
|
8
|
+
},
|
|
2
9
|
{
|
|
3
10
|
"version": "0.271.0",
|
|
4
11
|
"type": "improvement",
|
|
@@ -8,7 +15,7 @@
|
|
|
8
15
|
{
|
|
9
16
|
"version": "0.218.1",
|
|
10
17
|
"type": "improvement",
|
|
11
|
-
"title": "user.email searchable
|
|
18
|
+
"title": "user.email searchable — reindex required after upgrade",
|
|
12
19
|
"detail": "email gained searchable for roster lookup. buildSearchDocument only runs on write; existing rows need reindexEntity(\"user\") (or the reindex job) plus Meili configure for the new attribute.",
|
|
13
20
|
"migration": "After deploying, run reindexEntity(\"user\") (or the existing reindex job) so pre-upgrade user rows appear in roster search."
|
|
14
21
|
},
|
|
@@ -16,28 +23,28 @@
|
|
|
16
23
|
"version": "0.218.0",
|
|
17
24
|
"type": "fix",
|
|
18
25
|
"title": "user-list: system-scope search; membership roles on tenants; emailVerified sort",
|
|
19
|
-
"detail": "systemScope entityList search uses SYSTEM_TENANT_ID (was session tenant
|
|
26
|
+
"detail": "systemScope entityList search uses SYSTEM_TENANT_ID (was session tenant → Meili miss/500). Tenants column shows membership roles (Offlot Demo (TenantAdmin)). emailVerified sortable; email also searchable for roster lookup. Global roles stay SystemAdmin-only — TenantAdmin/Admin live on membership (Team → Members)."
|
|
20
27
|
},
|
|
21
28
|
{
|
|
22
29
|
"version": "0.217.0",
|
|
23
30
|
"type": "improvement",
|
|
24
31
|
"title": "user-list: roles multiSelect + tenants column; roles editable as SystemAdmin (fw#2384 follow-up)",
|
|
25
|
-
"detail": "user.roles is now createMultiSelectField (jsonb string[], options=[SystemAdmin]) so entityEdit shows a real multi-select instead of a raw JSON text box
|
|
26
|
-
"migration": "Run `kumiko-schema generate` and apply
|
|
32
|
+
"detail": "user.roles is now createMultiSelectField (jsonb string[], options=[SystemAdmin]) so entityEdit shows a real multi-select instead of a raw JSON text box — SystemAdmins can promote another SystemAdmin from the UI. user-list columns gain roles + tenants; tenants is a derived field filled by the list handler from memberships (platform roster was unusable without tenant context). switch-tenant + auth-client parseRoles accept jsonb arrays (string-only deserialize dropped SystemAdmin on tenant switch). Soft-deleted users stay hidden by default.",
|
|
33
|
+
"migration": "Run `kumiko-schema generate` and apply — migrates read_users.roles from text to jsonb USING roles::jsonb. Existing JSON array strings (`[]`, `[\"SystemAdmin\"]`) convert cleanly. Consumers that wrote roles as a JSON-encoded string via user:create/update should pass string[] instead (parseRoles still accepts both on read)."
|
|
27
34
|
},
|
|
28
35
|
{
|
|
29
36
|
"version": "0.167.1",
|
|
30
37
|
"type": "breaking",
|
|
31
38
|
"title": "user.locale no longer defaults to \"de\" (fw#1637).",
|
|
32
39
|
"detail": "The entity-level default contradicted tenant-settings' own \"en\" default and silently overrode any app or tenant locale configuration for every new user. locale now stays unset until the client or a resolution chain provides one. Consumers that already fall back with `user.locale ?? \"en\"` are unaffected in shape but now see null instead of \"de\".",
|
|
33
|
-
"migration": "Run `kumiko-schema generate` and apply
|
|
40
|
+
"migration": "Run `kumiko-schema generate` and apply — the migration emits `ALTER TABLE read_users ALTER COLUMN locale DROP DEFAULT`. Note that DROP DEFAULT only affects future inserts: existing rows keep the \"de\" the old default wrote, which no user ever chose. Your app therefore splits into pre-bump users on \"de\" and post-bump users on null, and each group resolves to a different language. Decide deliberately: either keep the old values (and state that pre-bump users stay German), or run `UPDATE read_users SET locale = NULL WHERE locale = 'de'` once so every user follows the same chain — the latter only if no UI ever let users edit the field, otherwise it erases real choices. If your app relied on the implicit German default, set an explicit fallback instead (fw#1653)."
|
|
34
41
|
},
|
|
35
42
|
{
|
|
36
43
|
"version": "0.167.1",
|
|
37
44
|
"type": "improvement",
|
|
38
45
|
"title": "user.timezone field, wired into ctx.tz (fw#1636).",
|
|
39
46
|
"detail": "The user entity gains a `timezone` field (createTzField, no default). buildHandlerContext passes it to ctx.tz.user; the fallback chain stays tenant.timezone, then \"UTC\". Before this, ctx.tz.tenant and ctx.tz.user both hardcoded \"UTC\" while the plumbing already accepted per-request values.",
|
|
40
|
-
"migration": "Run `kumiko-schema generate` and apply
|
|
47
|
+
"migration": "Run `kumiko-schema generate` and apply — the migration adds `read_users.timezone`. No backfill: an unset timezone falls through to the tenant's, exactly as before the field existed."
|
|
41
48
|
},
|
|
42
49
|
{
|
|
43
50
|
"version": "0.165.3",
|
|
@@ -1,4 +1,18 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.340.0",
|
|
4
|
+
"type": "improvement",
|
|
5
|
+
"title": "The resume job reads due runs through the job's TenantDb",
|
|
6
|
+
"detail": "`selectDueWorkflowRunPending` takes a `TenantDb` and returns `{ runId, stepIndex }` rows, so the tenant filter comes from the TenantDb instead of a hand-written `WHERE tenant_id`.",
|
|
7
|
+
"migration": "keine"
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
"version": "0.339.0",
|
|
11
|
+
"type": "breaking",
|
|
12
|
+
"title": "workflow-run stream stores a reference to the trigger event instead of its payload",
|
|
13
|
+
"detail": "`workflow.run-started` now holds `triggerEventRef: { eventId, aggregateId, version }` and no `triggerPayload`; the wait, waitForEvent and retry step events no longer embed it either. `workflow_run_pending` gets a `trigger_event_ref` column and the event-subscriber stops writing `trigger_payload`. `resume-run` loads the trigger and the awaited event through `ctx.loadAggregate` and fails the run with `trigger_event_unavailable` or `awaited_event_unavailable` when one is gone.",
|
|
14
|
+
"migration": "Own code that reads `triggerPayload` from `workflow.run-started` must read `triggerEventRef` and load the event with `ctx.loadAggregate(ref.aggregateId)`. Apps generate the migration for the new `workflow_run_pending.trigger_event_ref` column with `kumiko schema generate`. The step events `workflow.step.waiting`, `workflow.step.waiting-for-event` and `workflow.retry.scheduled` no longer carry `triggerPayload`; a custom resume loop that read it must load the trigger event through the `triggerEventRef` on its own run-started event (see `samples/recipes/workflow-engine/src/postgres-resume-loop.ts`). Stored run-started events and pending rows keep their old copy; this release rewrites no events, and runs started before it resume from that copy."
|
|
15
|
+
},
|
|
2
16
|
{
|
|
3
17
|
"version": "0.337.0",
|
|
4
18
|
"type": "fix",
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import { asRawClient } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
2
|
-
// INSERT-once, not UPSERT: unlike subscriptionsProjectionTable (one row per
|
|
3
|
-
// tenant, state overwritten on every event), a payment row is an immutable
|
|
4
|
-
// fact keyed by paymentRowId(tenantId, providerName, providerEventId) — ON
|
|
5
|
-
// CONFLICT DO NOTHING makes a projection rebuild/replay idempotent without
|
|
6
|
-
// ever mutating an existing row.
|
|
7
|
-
export async function insertPaymentProjectionRow(tx, tableName, insertCols) {
|
|
8
|
-
const insertKeys = Object.keys(insertCols);
|
|
9
|
-
const insertPlaceholders = insertKeys.map((_, i) => `$${i + 1}`);
|
|
10
|
-
const sqlText = `INSERT INTO "${tableName}" (${insertKeys.map((k) => `"${k}"`).join(", ")}) VALUES (${insertPlaceholders.join(", ")}) ON CONFLICT ("id") DO NOTHING`;
|
|
11
|
-
// kumiko-lint-ignore raw-sql tableName + column keys are code-controlled, not user input: tableName is buildEntityTable()'s static table name and insertKeys come from applyPaymentReceived's hardcoded object-literal columns in projection.ts — only values flow through $1..$n
|
|
12
|
-
await asRawClient(tx).unsafe(sqlText, Object.values(insertCols));
|
|
13
|
-
}
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
import { asRawClient } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
2
|
-
export async function upsertSubscriptionProjectionRow(tx, tableName, insertCols, setClauses, params) {
|
|
3
|
-
const insertKeys = Object.keys(insertCols);
|
|
4
|
-
const insertPlaceholders = insertKeys.map((_, i) => `$${i + 1}`);
|
|
5
|
-
const sqlText = `INSERT INTO "${tableName}" (${insertKeys.map((k) => `"${k}"`).join(", ")}) VALUES (${insertPlaceholders.join(", ")}) ON CONFLICT ("id") DO UPDATE SET ${setClauses.join(", ")}`;
|
|
6
|
-
await asRawClient(tx).unsafe(sqlText, params);
|
|
7
|
-
}
|