@pikku/kysely 0.12.17 → 0.13.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/CHANGELOG.md +40 -0
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.js +1 -0
- package/dist/src/kysely-audit-service.d.ts +19 -0
- package/dist/src/kysely-audit-service.js +55 -0
- package/dist/src/kysely-workflow-mirror.js +10 -0
- package/dist/src/kysely-workflow-run-service.js +2 -0
- package/dist/src/kysely-workflow-service.js +7 -3
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +3 -3
- package/src/index.ts +1 -0
- package/src/kysely-audit-service.ts +62 -0
- package/src/kysely-workflow-mirror.test.ts +23 -0
- package/src/kysely-workflow-mirror.ts +11 -0
- package/src/kysely-workflow-run-service.ts +2 -0
- package/src/kysely-workflow-service.ts +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,43 @@
|
|
|
1
|
+
## 0.13.0
|
|
2
|
+
|
|
3
|
+
### Minor Changes
|
|
4
|
+
|
|
5
|
+
- 241e6cf: Add `KyselyAuditService` — a durable `AuditService` that persists AuditEvents to an `audit` table via Kysely (the companion sink to `createAuditedKysely`). Its column mapping matches Fabric's platform audit-queue consumer, so a locally-run project and a deployed stage produce identical rows. Use it as the local/dev audit sink so audit events persist and are queryable without the platform queue.
|
|
6
|
+
|
|
7
|
+
### Patch Changes
|
|
8
|
+
|
|
9
|
+
- 41ce2cb: Upgrade to TypeScript 6 and raise the minimum Node.js version to 22.
|
|
10
|
+
|
|
11
|
+
All packages now build against `typescript@^6.0.3` and declare `engines.node >= 22`. Internal tooling (`ts-json-schema-generator`, `zod-to-ts`) was bumped to TypeScript 6-compatible releases.
|
|
12
|
+
|
|
13
|
+
- Updated dependencies [41ce2cb]
|
|
14
|
+
- @pikku/core@0.12.44
|
|
15
|
+
|
|
16
|
+
## 0.12.18
|
|
17
|
+
|
|
18
|
+
### Patch Changes
|
|
19
|
+
|
|
20
|
+
- 5c0ff0f: Fix `getRunHistory` dropping step provenance (`fromStepName`). The value was persisted on the step row and used by the graph planner, but `getRunHistory` built its rows from the per-attempt history and never carried `fromStepName` through — so run history (and any timeline reconstructed from it) reported no predecessors. Redis and Kysely `getRunHistory` now return `fromStepName`. Also adds the missing `from_step_name` column (+ backfill) to the Kysely workflow mirror's `workflow_step` schema and persists it on mirror inserts, so a mirror-side history has identical provenance.
|
|
21
|
+
- 72694f6: feat(workflow): expose per-step attempt count + record running/succeeded/failed timestamps
|
|
22
|
+
|
|
23
|
+
`getRunStatus` now returns `attempts` (the latest attempt count) per step, so
|
|
24
|
+
consumers can show retry counts without a second history query. It already
|
|
25
|
+
computed `duration` from `runningAt`/`succeededAt`, but the kysely and mongodb
|
|
26
|
+
workflow stores only stamped those timestamps on the _insert_ path — the
|
|
27
|
+
`running` / `succeeded` / `failed` status transitions updated the history row's
|
|
28
|
+
status without setting `runningAt` / `succeededAt` / `failedAt`, so `duration`
|
|
29
|
+
was always undefined. The transitions now stamp the matching timestamp, so step
|
|
30
|
+
duration is populated for kysely- and mongodb-backed runs. (Redis already
|
|
31
|
+
stamped on transition.) A shared service-suite test guards both behaviours.
|
|
32
|
+
|
|
33
|
+
- Updated dependencies [4be205f]
|
|
34
|
+
- Updated dependencies [061c717]
|
|
35
|
+
- Updated dependencies [2c55e13]
|
|
36
|
+
- Updated dependencies [c745c26]
|
|
37
|
+
- Updated dependencies [57900b5]
|
|
38
|
+
- Updated dependencies [72694f6]
|
|
39
|
+
- @pikku/core@0.12.39
|
|
40
|
+
|
|
1
41
|
## 0.12.17
|
|
2
42
|
|
|
3
43
|
### Patch Changes
|
package/dist/src/index.d.ts
CHANGED
|
@@ -13,6 +13,7 @@ export { KyselyCredentialService } from './kysely-credential-service.js';
|
|
|
13
13
|
export type { KyselyCredentialServiceConfig } from './kysely-credential-service.js';
|
|
14
14
|
export { KyselySessionStore } from './kysely-session-store.js';
|
|
15
15
|
export { createAuditedKysely, type CreateAuditedKyselyOptions, } from './create-audited-kysely.js';
|
|
16
|
+
export { KyselyAuditService } from './kysely-audit-service.js';
|
|
16
17
|
export { SerializePlugin, SqliteSerializePlugin, BaseSerializePlugin, defaultSerializer, defaultDeserializer, maybeJson, skipTransform, dateRegex, type Serializer, type Deserializer, type SerializePluginOptions, } from './serialize-plugin.js';
|
|
17
18
|
export type { KyselyPikkuDB } from './kysely-tables.js';
|
|
18
19
|
export type { WorkflowRunService } from '@pikku/core/workflow';
|
package/dist/src/index.js
CHANGED
|
@@ -11,4 +11,5 @@ export { KyselySecretService } from './kysely-secret-service.js';
|
|
|
11
11
|
export { KyselyCredentialService } from './kysely-credential-service.js';
|
|
12
12
|
export { KyselySessionStore } from './kysely-session-store.js';
|
|
13
13
|
export { createAuditedKysely, } from './create-audited-kysely.js';
|
|
14
|
+
export { KyselyAuditService } from './kysely-audit-service.js';
|
|
14
15
|
export { SerializePlugin, SqliteSerializePlugin, BaseSerializePlugin, defaultSerializer, defaultDeserializer, maybeJson, skipTransform, dateRegex, } from './serialize-plugin.js';
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { AuditEvent, AuditEventBatch, AuditService } from '@pikku/core';
|
|
2
|
+
import type { Kysely } from 'kysely';
|
|
3
|
+
/**
|
|
4
|
+
* Durable {@link AuditService} that persists AuditEvents to an `audit` table
|
|
5
|
+
* via Kysely — the companion sink to {@link createAuditedKysely}.
|
|
6
|
+
*
|
|
7
|
+
* The column mapping matches a platform audit-queue consumer, so a locally-run
|
|
8
|
+
* project and a deployed stage write identical rows (the read side relies only
|
|
9
|
+
* on `type`, `occurred_at`, `actor_user_id`, and the `data` JSON that holds
|
|
10
|
+
* `metadata`). The `audit` table is an optional per-project migration (not in
|
|
11
|
+
* the generated schema), so rows are inserted untyped; all columns are TEXT on
|
|
12
|
+
* every engine and ON CONFLICT DO NOTHING keeps writes idempotent on retries.
|
|
13
|
+
*/
|
|
14
|
+
export declare class KyselyAuditService implements AuditService {
|
|
15
|
+
private db;
|
|
16
|
+
constructor(db: Kysely<any>);
|
|
17
|
+
audit(event: AuditEvent): Promise<void>;
|
|
18
|
+
write(batch: AuditEventBatch): Promise<void>;
|
|
19
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
const jsonOrNull = (v) => v != null ? JSON.stringify(v) : null;
|
|
2
|
+
// No global `crypto` is guaranteed across every runtime, and audit_id is the
|
|
3
|
+
// PK — any collision is dropped by ON CONFLICT DO NOTHING.
|
|
4
|
+
const fallbackId = () => `evt_${Date.now().toString(36)}${Math.random().toString(36).slice(2, 10)}`;
|
|
5
|
+
/**
|
|
6
|
+
* Durable {@link AuditService} that persists AuditEvents to an `audit` table
|
|
7
|
+
* via Kysely — the companion sink to {@link createAuditedKysely}.
|
|
8
|
+
*
|
|
9
|
+
* The column mapping matches a platform audit-queue consumer, so a locally-run
|
|
10
|
+
* project and a deployed stage write identical rows (the read side relies only
|
|
11
|
+
* on `type`, `occurred_at`, `actor_user_id`, and the `data` JSON that holds
|
|
12
|
+
* `metadata`). The `audit` table is an optional per-project migration (not in
|
|
13
|
+
* the generated schema), so rows are inserted untyped; all columns are TEXT on
|
|
14
|
+
* every engine and ON CONFLICT DO NOTHING keeps writes idempotent on retries.
|
|
15
|
+
*/
|
|
16
|
+
export class KyselyAuditService {
|
|
17
|
+
db;
|
|
18
|
+
constructor(db) {
|
|
19
|
+
this.db = db;
|
|
20
|
+
}
|
|
21
|
+
async audit(event) {
|
|
22
|
+
await this.write([event]);
|
|
23
|
+
}
|
|
24
|
+
async write(batch) {
|
|
25
|
+
if (!batch.length)
|
|
26
|
+
return;
|
|
27
|
+
const rows = batch.map((e) => {
|
|
28
|
+
const metadata = e.metadata;
|
|
29
|
+
return {
|
|
30
|
+
audit_id: e.eventId ?? e.queryId ?? fallbackId(),
|
|
31
|
+
occurred_at: e.occurredAt ?? new Date().toISOString(),
|
|
32
|
+
type: e.type ?? 'unknown',
|
|
33
|
+
source: e.source ?? 'auto',
|
|
34
|
+
outcome: e.outcome ?? null,
|
|
35
|
+
function_id: e.functionId ?? null,
|
|
36
|
+
wire_type: e.wireType ?? null,
|
|
37
|
+
trace_id: e.traceId ?? null,
|
|
38
|
+
transaction_id: e.transactionId ?? null,
|
|
39
|
+
query_id: e.queryId ?? null,
|
|
40
|
+
actor_user_id: e.actor?.userId ?? null,
|
|
41
|
+
actor_org_id: e.actor?.orgId ?? null,
|
|
42
|
+
tables: jsonOrNull(metadata?.tables),
|
|
43
|
+
changed_cols: jsonOrNull(metadata?.changedColumns),
|
|
44
|
+
event: e.eventId ?? null,
|
|
45
|
+
old: null,
|
|
46
|
+
data: jsonOrNull(e.metadata),
|
|
47
|
+
};
|
|
48
|
+
});
|
|
49
|
+
await this.db
|
|
50
|
+
.insertInto('audit')
|
|
51
|
+
.values(rows)
|
|
52
|
+
.onConflict((oc) => oc.doNothing())
|
|
53
|
+
.execute();
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -61,6 +61,7 @@ export class KyselyWorkflowMirror {
|
|
|
61
61
|
.addColumn('branch_taken', 'text')
|
|
62
62
|
.addColumn('retries', 'integer')
|
|
63
63
|
.addColumn('retry_delay', 'text')
|
|
64
|
+
.addColumn('from_step_name', 'text')
|
|
64
65
|
.addColumn('created_at', 'timestamp', (col) => col.defaultTo(sql `CURRENT_TIMESTAMP`).notNull())
|
|
65
66
|
.addColumn('updated_at', 'timestamp', (col) => col.defaultTo(sql `CURRENT_TIMESTAMP`).notNull())
|
|
66
67
|
.addUniqueConstraint('workflow_step_run_name_unique', [
|
|
@@ -68,6 +69,14 @@ export class KyselyWorkflowMirror {
|
|
|
68
69
|
'step_name',
|
|
69
70
|
])
|
|
70
71
|
.execute();
|
|
72
|
+
// Additive: backfill the column on mirror tables created before from_step_name.
|
|
73
|
+
await this.db.schema
|
|
74
|
+
.alterTable('workflow_step')
|
|
75
|
+
.addColumn('from_step_name', 'text')
|
|
76
|
+
.execute()
|
|
77
|
+
.catch(() => {
|
|
78
|
+
// Column already exists (fresh tables include it above) — nothing to do.
|
|
79
|
+
});
|
|
71
80
|
await this.db.schema
|
|
72
81
|
.createTable('workflow_step_history')
|
|
73
82
|
.ifNotExists()
|
|
@@ -144,6 +153,7 @@ export class KyselyWorkflowMirror {
|
|
|
144
153
|
status: step.status,
|
|
145
154
|
retries: step.retries ?? null,
|
|
146
155
|
retryDelay: step.retryDelay?.toString() ?? null,
|
|
156
|
+
fromStepName: step.fromStepName ?? null,
|
|
147
157
|
createdAt: now,
|
|
148
158
|
updatedAt: now,
|
|
149
159
|
})
|
|
@@ -137,6 +137,7 @@ export class KyselyWorkflowRunService {
|
|
|
137
137
|
.select([
|
|
138
138
|
's.workflowStepId',
|
|
139
139
|
's.stepName',
|
|
140
|
+
's.fromStepName',
|
|
140
141
|
's.retries',
|
|
141
142
|
's.retryDelay',
|
|
142
143
|
'h.status',
|
|
@@ -158,6 +159,7 @@ export class KyselyWorkflowRunService {
|
|
|
158
159
|
return {
|
|
159
160
|
stepId,
|
|
160
161
|
stepName: row.stepName,
|
|
162
|
+
fromStepName: row.fromStepName ?? undefined,
|
|
161
163
|
status: row.status,
|
|
162
164
|
result: parseJson(row.result),
|
|
163
165
|
error: parseJson(row.error),
|
|
@@ -231,7 +231,7 @@ export class KyselyWorkflowService extends PikkuWorkflowService {
|
|
|
231
231
|
if (latestHistory) {
|
|
232
232
|
await this.db
|
|
233
233
|
.updateTable('workflowStepHistory')
|
|
234
|
-
.set({ status: 'running' })
|
|
234
|
+
.set({ status: 'running', runningAt: new Date() })
|
|
235
235
|
.where('historyId', '=', latestHistory.historyId)
|
|
236
236
|
.execute();
|
|
237
237
|
}
|
|
@@ -308,7 +308,11 @@ export class KyselyWorkflowService extends PikkuWorkflowService {
|
|
|
308
308
|
if (latestHistory) {
|
|
309
309
|
await this.db
|
|
310
310
|
.updateTable('workflowStepHistory')
|
|
311
|
-
.set({
|
|
311
|
+
.set({
|
|
312
|
+
status: 'succeeded',
|
|
313
|
+
result: resultJson,
|
|
314
|
+
succeededAt: new Date(),
|
|
315
|
+
})
|
|
312
316
|
.where('historyId', '=', latestHistory.historyId)
|
|
313
317
|
.execute();
|
|
314
318
|
}
|
|
@@ -340,7 +344,7 @@ export class KyselyWorkflowService extends PikkuWorkflowService {
|
|
|
340
344
|
if (latestHistory) {
|
|
341
345
|
await this.db
|
|
342
346
|
.updateTable('workflowStepHistory')
|
|
343
|
-
.set({ status: 'failed', error: errorJson })
|
|
347
|
+
.set({ status: 'failed', error: errorJson, failedAt: new Date() })
|
|
344
348
|
.where('historyId', '=', latestHistory.historyId)
|
|
345
349
|
.execute();
|
|
346
350
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"root":["../src/create-audited-kysely.ts","../src/index.ts","../src/kysely-ai-agent-run-service.ts","../src/kysely-ai-run-state-service.ts","../src/kysely-ai-storage-service.ts","../src/kysely-channel-store.ts","../src/kysely-credential-service.ts","../src/kysely-deployment-service.ts","../src/kysely-eventhub-store.ts","../src/kysely-json.ts","../src/kysely-secret-service.ts","../src/kysely-session-store.ts","../src/kysely-tables.ts","../src/kysely-workflow-mirror.ts","../src/kysely-workflow-run-service.ts","../src/kysely-workflow-service.ts","../src/serialize-plugin.ts","../bin/pikku-kysely-pure.ts"],"version":"
|
|
1
|
+
{"root":["../src/create-audited-kysely.ts","../src/index.ts","../src/kysely-ai-agent-run-service.ts","../src/kysely-ai-run-state-service.ts","../src/kysely-ai-storage-service.ts","../src/kysely-audit-service.ts","../src/kysely-channel-store.ts","../src/kysely-credential-service.ts","../src/kysely-deployment-service.ts","../src/kysely-eventhub-store.ts","../src/kysely-json.ts","../src/kysely-secret-service.ts","../src/kysely-session-store.ts","../src/kysely-tables.ts","../src/kysely-workflow-mirror.ts","../src/kysely-workflow-run-service.ts","../src/kysely-workflow-service.ts","../src/serialize-plugin.ts","../bin/pikku-kysely-pure.ts"],"version":"6.0.3"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pikku/kysely",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"author": "yasser.fadl@gmail.com",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"module": "dist/src/index.js",
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"prepublishOnly": "yarn build"
|
|
21
21
|
},
|
|
22
22
|
"peerDependencies": {
|
|
23
|
-
"@pikku/core": "^0.12.
|
|
23
|
+
"@pikku/core": "^0.12.44"
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
26
|
"kysely": "^0.29.0"
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"kysely-codegen": "^0.20.0",
|
|
32
32
|
"kysely-postgres-js": "^3.0.0",
|
|
33
33
|
"postgres": "^3.4.8",
|
|
34
|
-
"typescript": "^
|
|
34
|
+
"typescript": "^6.0.3"
|
|
35
35
|
},
|
|
36
36
|
"exports": {
|
|
37
37
|
".": "./dist/src/index.js"
|
package/src/index.ts
CHANGED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { AuditEvent, AuditEventBatch, AuditService } from '@pikku/core'
|
|
2
|
+
import type { Kysely } from 'kysely'
|
|
3
|
+
|
|
4
|
+
const jsonOrNull = (v: unknown): string | null =>
|
|
5
|
+
v != null ? JSON.stringify(v) : null
|
|
6
|
+
|
|
7
|
+
// No global `crypto` is guaranteed across every runtime, and audit_id is the
|
|
8
|
+
// PK — any collision is dropped by ON CONFLICT DO NOTHING.
|
|
9
|
+
const fallbackId = (): string =>
|
|
10
|
+
`evt_${Date.now().toString(36)}${Math.random().toString(36).slice(2, 10)}`
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Durable {@link AuditService} that persists AuditEvents to an `audit` table
|
|
14
|
+
* via Kysely — the companion sink to {@link createAuditedKysely}.
|
|
15
|
+
*
|
|
16
|
+
* The column mapping matches a platform audit-queue consumer, so a locally-run
|
|
17
|
+
* project and a deployed stage write identical rows (the read side relies only
|
|
18
|
+
* on `type`, `occurred_at`, `actor_user_id`, and the `data` JSON that holds
|
|
19
|
+
* `metadata`). The `audit` table is an optional per-project migration (not in
|
|
20
|
+
* the generated schema), so rows are inserted untyped; all columns are TEXT on
|
|
21
|
+
* every engine and ON CONFLICT DO NOTHING keeps writes idempotent on retries.
|
|
22
|
+
*/
|
|
23
|
+
export class KyselyAuditService implements AuditService {
|
|
24
|
+
constructor(private db: Kysely<any>) {}
|
|
25
|
+
|
|
26
|
+
async audit(event: AuditEvent): Promise<void> {
|
|
27
|
+
await this.write([event])
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async write(batch: AuditEventBatch): Promise<void> {
|
|
31
|
+
if (!batch.length) return
|
|
32
|
+
const rows = batch.map((e) => {
|
|
33
|
+
const metadata = e.metadata as
|
|
34
|
+
| { tables?: unknown; changedColumns?: unknown }
|
|
35
|
+
| undefined
|
|
36
|
+
return {
|
|
37
|
+
audit_id: e.eventId ?? e.queryId ?? fallbackId(),
|
|
38
|
+
occurred_at: e.occurredAt ?? new Date().toISOString(),
|
|
39
|
+
type: e.type ?? 'unknown',
|
|
40
|
+
source: e.source ?? 'auto',
|
|
41
|
+
outcome: e.outcome ?? null,
|
|
42
|
+
function_id: e.functionId ?? null,
|
|
43
|
+
wire_type: e.wireType ?? null,
|
|
44
|
+
trace_id: e.traceId ?? null,
|
|
45
|
+
transaction_id: e.transactionId ?? null,
|
|
46
|
+
query_id: e.queryId ?? null,
|
|
47
|
+
actor_user_id: e.actor?.userId ?? null,
|
|
48
|
+
actor_org_id: e.actor?.orgId ?? null,
|
|
49
|
+
tables: jsonOrNull(metadata?.tables),
|
|
50
|
+
changed_cols: jsonOrNull(metadata?.changedColumns),
|
|
51
|
+
event: e.eventId ?? null,
|
|
52
|
+
old: null,
|
|
53
|
+
data: jsonOrNull(e.metadata),
|
|
54
|
+
}
|
|
55
|
+
})
|
|
56
|
+
await (this.db as any)
|
|
57
|
+
.insertInto('audit')
|
|
58
|
+
.values(rows)
|
|
59
|
+
.onConflict((oc: any) => oc.doNothing())
|
|
60
|
+
.execute()
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -103,6 +103,29 @@ describe('KyselyWorkflowMirror — step lifecycle', () => {
|
|
|
103
103
|
assert.equal(history[0]!.status, 'pending')
|
|
104
104
|
})
|
|
105
105
|
|
|
106
|
+
test('getRunHistory surfaces step provenance (fromStepName)', async () => {
|
|
107
|
+
// The mirror persists fromStepName and getRunHistory reads it back, so the
|
|
108
|
+
// run history reconstructs the walked path (entry has none, the next step
|
|
109
|
+
// records its predecessor).
|
|
110
|
+
await seedStep(STEP_ID, 'begin')
|
|
111
|
+
await mirror.insertStepState(RUN_ID, {
|
|
112
|
+
stepId: `${STEP_ID}-2`,
|
|
113
|
+
stepName: 'next',
|
|
114
|
+
rpcName: 'rpc.fn',
|
|
115
|
+
data: { x: 2 },
|
|
116
|
+
status: 'pending',
|
|
117
|
+
attemptCount: 1,
|
|
118
|
+
fromStepName: 'begin',
|
|
119
|
+
createdAt: new Date(),
|
|
120
|
+
updatedAt: new Date(),
|
|
121
|
+
} as any)
|
|
122
|
+
|
|
123
|
+
const history = await runService.getRunHistory(RUN_ID)
|
|
124
|
+
const from = new Map(history.map((h) => [h.stepName, h.fromStepName]))
|
|
125
|
+
assert.equal(from.get('begin'), undefined, 'entry step has no predecessor')
|
|
126
|
+
assert.equal(from.get('next'), 'begin', 'next records its predecessor')
|
|
127
|
+
})
|
|
128
|
+
|
|
106
129
|
test('running → succeeded updates the in-place history row', async () => {
|
|
107
130
|
await seedStep()
|
|
108
131
|
await mirror.setStepRunning(STEP_ID)
|
|
@@ -79,6 +79,7 @@ export class KyselyWorkflowMirror implements WorkflowRunMirror {
|
|
|
79
79
|
.addColumn('branch_taken', 'text')
|
|
80
80
|
.addColumn('retries', 'integer')
|
|
81
81
|
.addColumn('retry_delay', 'text')
|
|
82
|
+
.addColumn('from_step_name', 'text')
|
|
82
83
|
.addColumn('created_at', 'timestamp', (col) =>
|
|
83
84
|
col.defaultTo(sql`CURRENT_TIMESTAMP`).notNull()
|
|
84
85
|
)
|
|
@@ -91,6 +92,15 @@ export class KyselyWorkflowMirror implements WorkflowRunMirror {
|
|
|
91
92
|
])
|
|
92
93
|
.execute()
|
|
93
94
|
|
|
95
|
+
// Additive: backfill the column on mirror tables created before from_step_name.
|
|
96
|
+
await this.db.schema
|
|
97
|
+
.alterTable('workflow_step')
|
|
98
|
+
.addColumn('from_step_name', 'text')
|
|
99
|
+
.execute()
|
|
100
|
+
.catch(() => {
|
|
101
|
+
// Column already exists (fresh tables include it above) — nothing to do.
|
|
102
|
+
})
|
|
103
|
+
|
|
94
104
|
await this.db.schema
|
|
95
105
|
.createTable('workflow_step_history')
|
|
96
106
|
.ifNotExists()
|
|
@@ -202,6 +212,7 @@ export class KyselyWorkflowMirror implements WorkflowRunMirror {
|
|
|
202
212
|
status: step.status,
|
|
203
213
|
retries: step.retries ?? null,
|
|
204
214
|
retryDelay: step.retryDelay?.toString() ?? null,
|
|
215
|
+
fromStepName: step.fromStepName ?? null,
|
|
205
216
|
createdAt: now,
|
|
206
217
|
updatedAt: now,
|
|
207
218
|
})
|
|
@@ -192,6 +192,7 @@ export class KyselyWorkflowRunService implements WorkflowRunService {
|
|
|
192
192
|
.select([
|
|
193
193
|
's.workflowStepId',
|
|
194
194
|
's.stepName',
|
|
195
|
+
's.fromStepName',
|
|
195
196
|
's.retries',
|
|
196
197
|
's.retryDelay',
|
|
197
198
|
'h.status',
|
|
@@ -215,6 +216,7 @@ export class KyselyWorkflowRunService implements WorkflowRunService {
|
|
|
215
216
|
return {
|
|
216
217
|
stepId,
|
|
217
218
|
stepName: row.stepName,
|
|
219
|
+
fromStepName: row.fromStepName ?? undefined,
|
|
218
220
|
status: row.status as StepState['status'],
|
|
219
221
|
result: parseJson(row.result),
|
|
220
222
|
error: parseJson(row.error),
|
|
@@ -319,7 +319,7 @@ export class KyselyWorkflowService extends PikkuWorkflowService {
|
|
|
319
319
|
if (latestHistory) {
|
|
320
320
|
await this.db
|
|
321
321
|
.updateTable('workflowStepHistory')
|
|
322
|
-
.set({ status: 'running' })
|
|
322
|
+
.set({ status: 'running', runningAt: new Date() })
|
|
323
323
|
.where('historyId', '=', latestHistory.historyId)
|
|
324
324
|
.execute()
|
|
325
325
|
}
|
|
@@ -417,7 +417,7 @@ export class KyselyWorkflowService extends PikkuWorkflowService {
|
|
|
417
417
|
if (latestHistory) {
|
|
418
418
|
await this.db
|
|
419
419
|
.updateTable('workflowStepHistory')
|
|
420
|
-
.set({ status: 'succeeded', result: resultJson })
|
|
420
|
+
.set({ status: 'succeeded', result: resultJson, succeededAt: new Date() })
|
|
421
421
|
.where('historyId', '=', latestHistory.historyId)
|
|
422
422
|
.execute()
|
|
423
423
|
}
|
|
@@ -456,7 +456,7 @@ export class KyselyWorkflowService extends PikkuWorkflowService {
|
|
|
456
456
|
if (latestHistory) {
|
|
457
457
|
await this.db
|
|
458
458
|
.updateTable('workflowStepHistory')
|
|
459
|
-
.set({ status: 'failed', error: errorJson })
|
|
459
|
+
.set({ status: 'failed', error: errorJson, failedAt: new Date() })
|
|
460
460
|
.where('historyId', '=', latestHistory.historyId)
|
|
461
461
|
.execute()
|
|
462
462
|
}
|