@pikku/kysely 0.12.10 → 0.12.12

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 CHANGED
@@ -1,3 +1,25 @@
1
+ ## 0.12.12
2
+
3
+ ### Patch Changes
4
+
5
+ - 55ba75a: Update the OSS runtime dependency set for newer Node support by bumping `uWebSockets.js` to `v20.68.0` and `better-sqlite3` to `v12.10.0`.
6
+ - Updated dependencies [c02275f]
7
+ - Updated dependencies [0bd0433]
8
+ - @pikku/core@0.12.24
9
+
10
+ ## 0.12.11
11
+
12
+ ### Patch Changes
13
+
14
+ - b9ed73e: Add deterministic workflow planned-step metadata support and SSE init stream payload generation.
15
+ - Persist `deterministic` and `plannedSteps` on workflow runs in core and service adapters.
16
+ - Expose planned-step metadata on workflow run status responses.
17
+ - Emit an initial `type: 'init'` SSE event for deterministic workflow streams before incremental updates.
18
+ - Add CLI tests covering serialized stream route output for init/update/done event behavior.
19
+
20
+ - Updated dependencies [b9ed73e]
21
+ - @pikku/core@0.12.19
22
+
1
23
  ## 0.12.0
2
24
 
3
25
  ## 0.12.10
@@ -5,7 +27,6 @@
5
27
  ### Patch Changes
6
28
 
7
29
  - 311c0c4: Unify session persistence through SessionStore, remove session blob from ChannelStore
8
-
9
30
  - PikkuSessionService now persists sessions via SessionStore on set()/clear() instead of every function call
10
31
  - ChannelStore no longer stores session data — maps channelId to pikkuUserId only
11
32
  - ChannelStore API: setUserSession/getChannelAndSession replaced with setPikkuUserId/getChannel
@@ -19,7 +40,6 @@
19
40
  ### Patch Changes
20
41
 
21
42
  - 624097e: Add deploy pipeline with provider-agnostic architecture
22
-
23
43
  - Add MetaService with explicit typed API, absorb WiringService reads
24
44
  - Add deployment service, traceId propagation, scoped logger
25
45
  - Rewrite analyzer: one function = one worker, gateways dispatch via RPC
@@ -39,7 +59,6 @@
39
59
  ### Patch Changes
40
60
 
41
61
  - f85c234: Add unified credential system with per-user OAuth and AI agent pre-flight checks
42
-
43
62
  - Unified CredentialService with lazy loading per user via pikkuUserId
44
63
  - wire.getCredential() for typed single credential lookup
45
64
  - MissingCredentialError with structured payload for client-side connect flows
@@ -144,7 +163,6 @@
144
163
  - Updated dependencies [62a8725]
145
164
  - Updated dependencies [62a8725]
146
165
  - Updated dependencies [62a8725]
147
-
148
166
  - @pikku/core@0.12.1
149
167
 
150
168
  - Updated dependencies
@@ -0,0 +1,10 @@
1
+ import type { AuditLog } from '@pikku/core';
2
+ import type { Kysely } from 'kysely';
3
+ export interface CreateAuditedKyselyOptions {
4
+ audit: AuditLog;
5
+ auditReads?: boolean;
6
+ eventType?: string;
7
+ transactionId?: string | null;
8
+ queryIdPrefix?: string;
9
+ }
10
+ export declare function createAuditedKysely<DB>(db: Kysely<DB>, options: CreateAuditedKyselyOptions): Kysely<DB>;
@@ -0,0 +1,167 @@
1
+ const QUERY_KIND_BY_NODE = {
2
+ SelectQueryNode: 'select',
3
+ InsertQueryNode: 'insert',
4
+ UpdateQueryNode: 'update',
5
+ DeleteQueryNode: 'delete',
6
+ };
7
+ function getIdentifierName(node) {
8
+ if (!node || typeof node !== 'object')
9
+ return undefined;
10
+ const candidate = node;
11
+ return (candidate.name ??
12
+ candidate.column?.name ??
13
+ candidate.identifier?.name ??
14
+ candidate.table?.identifier?.name);
15
+ }
16
+ function collectTableNames(node, out) {
17
+ if (!node || typeof node !== 'object')
18
+ return;
19
+ const candidate = node;
20
+ if (candidate.kind === 'TableNode' &&
21
+ typeof candidate.table?.identifier?.name === 'string') {
22
+ out.add(candidate.table.identifier.name);
23
+ }
24
+ for (const value of Object.values(node)) {
25
+ if (Array.isArray(value)) {
26
+ for (const item of value)
27
+ collectTableNames(item, out);
28
+ continue;
29
+ }
30
+ collectTableNames(value, out);
31
+ }
32
+ }
33
+ function extractValue(node) {
34
+ if (!node || typeof node !== 'object')
35
+ return node;
36
+ const candidate = node;
37
+ switch (candidate.kind) {
38
+ case 'ValueNode':
39
+ return candidate.value;
40
+ case 'PrimitiveValueListNode':
41
+ case 'ValueListNode':
42
+ return (candidate.values ?? []).map(extractValue);
43
+ case 'DefaultInsertValueNode':
44
+ return '[default]';
45
+ case 'ColumnNode':
46
+ case 'ReferenceNode':
47
+ return `[${candidate.kind}]`;
48
+ case 'RawNode':
49
+ return '[raw]';
50
+ case 'SelectQueryNode':
51
+ return '[subquery]';
52
+ case 'ParensNode':
53
+ return extractValue(candidate.node);
54
+ case 'BinaryOperationNode':
55
+ return {
56
+ left: extractValue(candidate.left),
57
+ right: extractValue(candidate.right),
58
+ };
59
+ default:
60
+ return `[${candidate.kind ?? 'expression'}]`;
61
+ }
62
+ }
63
+ function extractInsertChanges(node) {
64
+ const columns = Array.isArray(node.columns)
65
+ ? node.columns
66
+ .map((column) => getIdentifierName(column))
67
+ .filter(Boolean)
68
+ : [];
69
+ const values = Array.isArray(node.values?.values) ? node.values.values : [];
70
+ if (columns.length === 0 || values.length === 0) {
71
+ return {
72
+ changedColumns: columns.length > 0 ? columns : undefined,
73
+ };
74
+ }
75
+ const rows = values.map((row) => {
76
+ const rowValues = Array.isArray(row?.values)
77
+ ? row.values.map(extractValue)
78
+ : [];
79
+ return Object.fromEntries(columns.map((column, index) => [column, rowValues[index]]));
80
+ });
81
+ return {
82
+ changedColumns: columns,
83
+ changes: rows.length === 1 ? rows[0] : rows,
84
+ };
85
+ }
86
+ function extractUpdateChanges(node) {
87
+ const updates = Array.isArray(node.updates) ? node.updates : [];
88
+ const entries = updates
89
+ .map((update) => {
90
+ const column = getIdentifierName(update.column);
91
+ if (!column)
92
+ return null;
93
+ return [column, extractValue(update.value)];
94
+ })
95
+ .filter(Boolean);
96
+ if (entries.length === 0) {
97
+ return {};
98
+ }
99
+ return {
100
+ changedColumns: entries.map(([column]) => column),
101
+ changes: Object.fromEntries(entries),
102
+ };
103
+ }
104
+ function extractChangeSet(node, queryKind) {
105
+ if (queryKind === 'insert') {
106
+ return extractInsertChanges(node);
107
+ }
108
+ if (queryKind === 'update') {
109
+ return extractUpdateChanges(node);
110
+ }
111
+ return {};
112
+ }
113
+ class AuditedKyselyPlugin {
114
+ options;
115
+ pending = new Map();
116
+ queryCounter = 0;
117
+ constructor(options) {
118
+ this.options = options;
119
+ }
120
+ transformQuery(args) {
121
+ const queryKind = QUERY_KIND_BY_NODE[args.node.kind ?? ''];
122
+ if (!queryKind) {
123
+ return args.node;
124
+ }
125
+ if (queryKind === 'select' && this.options.auditReads === false) {
126
+ return args.node;
127
+ }
128
+ const tables = new Set();
129
+ collectTableNames(args.node, tables);
130
+ const queryId = `${this.options.queryIdPrefix ?? 'q'}-${++this.queryCounter}`;
131
+ this.pending.set(args.queryId, {
132
+ queryKind,
133
+ queryId,
134
+ tables: [...tables].sort(),
135
+ ...extractChangeSet(args.node, queryKind),
136
+ });
137
+ return args.node;
138
+ }
139
+ async transformResult(args) {
140
+ const pending = this.pending.get(args.queryId);
141
+ if (!pending) {
142
+ return args.result;
143
+ }
144
+ this.pending.delete(args.queryId);
145
+ const rawResult = args.result;
146
+ const rowCount = rawResult.rows.length > 0
147
+ ? rawResult.rows.length
148
+ : Number(rawResult.numUpdatedOrDeletedRows ?? rawResult.numAffectedRows ?? 0n);
149
+ await this.options.audit.write({
150
+ type: this.options.eventType ?? 'db.query',
151
+ source: 'auto',
152
+ transactionId: this.options.transactionId ?? null,
153
+ queryId: pending.queryId,
154
+ metadata: {
155
+ queryKind: pending.queryKind,
156
+ tables: pending.tables,
157
+ changedColumns: pending.changedColumns,
158
+ changes: pending.changes,
159
+ rowCount,
160
+ },
161
+ });
162
+ return rawResult;
163
+ }
164
+ }
165
+ export function createAuditedKysely(db, options) {
166
+ return db.withPlugin(new AuditedKyselyPlugin(options));
167
+ }
@@ -2,6 +2,7 @@ export { KyselyChannelStore } from './kysely-channel-store.js';
2
2
  export { KyselyEventHubStore } from './kysely-eventhub-store.js';
3
3
  export { KyselyWorkflowService } from './kysely-workflow-service.js';
4
4
  export { KyselyWorkflowRunService } from './kysely-workflow-run-service.js';
5
+ export { KyselyWorkflowMirror } from './kysely-workflow-mirror.js';
5
6
  export { KyselyDeploymentService } from './kysely-deployment-service.js';
6
7
  export { KyselyAIStorageService } from './kysely-ai-storage-service.js';
7
8
  export { KyselyAgentRunService } from './kysely-ai-agent-run-service.js';
@@ -11,6 +12,7 @@ export type { KyselySecretServiceConfig } from './kysely-secret-service.js';
11
12
  export { KyselyCredentialService } from './kysely-credential-service.js';
12
13
  export type { KyselyCredentialServiceConfig } from './kysely-credential-service.js';
13
14
  export { KyselySessionStore } from './kysely-session-store.js';
15
+ export { createAuditedKysely, type CreateAuditedKyselyOptions, } from './create-audited-kysely.js';
14
16
  export type { KyselyPikkuDB } from './kysely-tables.js';
15
17
  export type { WorkflowRunService } from '@pikku/core/workflow';
16
18
  export type { AgentRunService, AgentRunRow } from '@pikku/core/ai-agent';
package/dist/src/index.js CHANGED
@@ -2,6 +2,7 @@ export { KyselyChannelStore } from './kysely-channel-store.js';
2
2
  export { KyselyEventHubStore } from './kysely-eventhub-store.js';
3
3
  export { KyselyWorkflowService } from './kysely-workflow-service.js';
4
4
  export { KyselyWorkflowRunService } from './kysely-workflow-run-service.js';
5
+ export { KyselyWorkflowMirror } from './kysely-workflow-mirror.js';
5
6
  export { KyselyDeploymentService } from './kysely-deployment-service.js';
6
7
  export { KyselyAIStorageService } from './kysely-ai-storage-service.js';
7
8
  export { KyselyAgentRunService } from './kysely-ai-agent-run-service.js';
@@ -9,3 +10,4 @@ export { KyselyAIRunStateService } from './kysely-ai-run-state-service.js';
9
10
  export { KyselySecretService } from './kysely-secret-service.js';
10
11
  export { KyselyCredentialService } from './kysely-credential-service.js';
11
12
  export { KyselySessionStore } from './kysely-session-store.js';
13
+ export { createAuditedKysely, } from './create-audited-kysely.js';
@@ -6,6 +6,7 @@ export declare class KyselyAgentRunService implements AgentRunService {
6
6
  constructor(db: Kysely<KyselyPikkuDB>);
7
7
  listThreads(options?: {
8
8
  agentName?: string;
9
+ resourceId?: string;
9
10
  limit?: number;
10
11
  offset?: number;
11
12
  }): Promise<AIThread[]>;
@@ -5,7 +5,7 @@ export class KyselyAgentRunService {
5
5
  this.db = db;
6
6
  }
7
7
  async listThreads(options) {
8
- const { agentName, limit = 50, offset = 0 } = options ?? {};
8
+ const { agentName, resourceId, limit = 50, offset = 0 } = options ?? {};
9
9
  let query = this.db
10
10
  .selectFrom('aiThreads as t')
11
11
  .select([
@@ -23,6 +23,9 @@ export class KyselyAgentRunService {
23
23
  .where('agentName', '=', agentName)
24
24
  .distinct());
25
25
  }
26
+ if (resourceId) {
27
+ query = query.where('t.resourceId', '=', resourceId);
28
+ }
26
29
  const result = await query
27
30
  .orderBy('t.updatedAt', 'desc')
28
31
  .limit(limit)
@@ -13,5 +13,8 @@ export declare class KyselyChannelStore extends ChannelStore {
13
13
  getChannel(channelId: string): Promise<Channel & {
14
14
  pikkuUserId?: string;
15
15
  }>;
16
+ setState(channelId: string, state: unknown): Promise<void>;
17
+ getState(channelId: string): Promise<unknown | undefined>;
18
+ clearState(channelId: string): Promise<void>;
16
19
  close(): Promise<void>;
17
20
  }
@@ -20,6 +20,7 @@ export class KyselyChannelStore extends ChannelStore {
20
20
  .addColumn('created_at', 'timestamp', (col) => col.defaultTo(sql `CURRENT_TIMESTAMP`).notNull())
21
21
  .addColumn('opening_data', 'text', (col) => col.notNull().defaultTo('{}'))
22
22
  .addColumn('pikku_user_id', 'text')
23
+ .addColumn('state', 'text')
23
24
  .addColumn('last_wire', 'timestamp', (col) => col.defaultTo(sql `CURRENT_TIMESTAMP`).notNull())
24
25
  .execute();
25
26
  await this.db.schema
@@ -76,5 +77,29 @@ export class KyselyChannelStore extends ChannelStore {
76
77
  pikkuUserId: row.pikkuUserId ?? undefined,
77
78
  };
78
79
  }
80
+ async setState(channelId, state) {
81
+ await this.db
82
+ .updateTable('channels')
83
+ .set({ state: JSON.stringify(state ?? null) })
84
+ .where('channelId', '=', channelId)
85
+ .execute();
86
+ }
87
+ async getState(channelId) {
88
+ const row = await this.db
89
+ .selectFrom('channels')
90
+ .select(['state'])
91
+ .where('channelId', '=', channelId)
92
+ .executeTakeFirst();
93
+ if (!row || !row.state)
94
+ return undefined;
95
+ return parseJson(row.state) ?? undefined;
96
+ }
97
+ async clearState(channelId) {
98
+ await this.db
99
+ .updateTable('channels')
100
+ .set({ state: null })
101
+ .where('channelId', '=', channelId)
102
+ .execute();
103
+ }
79
104
  async close() { }
80
105
  }
@@ -6,6 +6,8 @@ export interface ChannelsTable {
6
6
  createdAt: Generated<Date>;
7
7
  openingData: string;
8
8
  pikkuUserId: string | null;
9
+ /** JSON-serialized per-socket channel state, keyed by channelId. */
10
+ state: string | null;
9
11
  lastWire: Generated<Date>;
10
12
  }
11
13
  export interface ChannelSubscriptionsTable {
@@ -22,6 +24,8 @@ export interface WorkflowRunsTable {
22
24
  state: Generated<string>;
23
25
  inline: Generated<boolean>;
24
26
  graphHash: string | null;
27
+ deterministic: Generated<boolean>;
28
+ plannedSteps: string | null;
25
29
  wire: string | null;
26
30
  createdAt: Generated<Date>;
27
31
  updatedAt: Generated<Date>;
@@ -0,0 +1,60 @@
1
+ import type { SerializedError } from '@pikku/core';
2
+ import type { StepState, WorkflowRunMirror, WorkflowRunWire, WorkflowStatus, WorkflowPlannedStep, WorkflowVersionStatus } from '@pikku/core/workflow';
3
+ import type { Kysely } from 'kysely';
4
+ import type { KyselyPikkuDB } from './kysely-tables.js';
5
+ /**
6
+ * Kysely-backed `WorkflowRunMirror`.
7
+ *
8
+ * Forwards executor writes to the same `workflow_runs` / `workflow_step` /
9
+ * `workflow_step_history` / `workflow_versions` tables that
10
+ * `KyselyWorkflowService` uses, so a `KyselyWorkflowRunService` reading
11
+ * those tables will see runs driven by any executor (Cloudflare Durable
12
+ * Object, Redis, MongoDB, in-memory) — not just kysely-driven runs.
13
+ *
14
+ * Errors thrown here are caught by the executor's `safeMirror` wrapper
15
+ * and logged, so a mirror outage cannot break a running workflow.
16
+ */
17
+ export declare class KyselyWorkflowMirror implements WorkflowRunMirror {
18
+ protected db: Kysely<KyselyPikkuDB>;
19
+ private initialized;
20
+ constructor(db: Kysely<KyselyPikkuDB>);
21
+ /**
22
+ * Create the underlying tables if they don't exist. Safe to call from
23
+ * either the mirror or `KyselyWorkflowService.init()` — schema is the
24
+ * same and uses `ifNotExists`.
25
+ */
26
+ init(): Promise<void>;
27
+ createRun(runId: string, workflowName: string, input: any, inline: boolean, graphHash: string, wire: WorkflowRunWire, options?: {
28
+ deterministic?: boolean;
29
+ plannedSteps?: WorkflowPlannedStep[];
30
+ }): Promise<void>;
31
+ updateRunStatus(id: string, status: WorkflowStatus, output?: any, error?: SerializedError): Promise<void>;
32
+ insertStepState(runId: string, step: StepState & {
33
+ stepName: string;
34
+ rpcName: string | null;
35
+ data: any;
36
+ }): Promise<void>;
37
+ setStepRunning(stepId: string): Promise<void>;
38
+ setStepScheduled(stepId: string): Promise<void>;
39
+ setStepResult(stepId: string, result: any): Promise<void>;
40
+ setStepChildRunId(stepId: string, childRunId: string): Promise<void>;
41
+ setStepError(stepId: string, error: SerializedError): Promise<void>;
42
+ createRetryAttempt(failedStepId: string, newStep: StepState & {
43
+ stepName: string;
44
+ }): Promise<void>;
45
+ setBranchTaken(stepId: string, branchKey: string): Promise<void>;
46
+ updateRunState(runId: string, name: string, value: unknown): Promise<void>;
47
+ upsertWorkflowVersion(name: string, graphHash: string, graph: any, source: string, status?: WorkflowVersionStatus): Promise<void>;
48
+ updateWorkflowVersionStatus(name: string, graphHash: string, status: WorkflowVersionStatus): Promise<void>;
49
+ private updateStepStatus;
50
+ private insertHistoryRecord;
51
+ /**
52
+ * The executor pattern is: `insertStepState` writes a 'pending' history
53
+ * row, then later transitions UPDATE that latest row in place rather
54
+ * than appending a new one — except for retry attempts which append.
55
+ *
56
+ * We mirror the same shape so `KyselyWorkflowRunService.getRunHistory`
57
+ * returns the same sequence the executor would have produced.
58
+ */
59
+ private appendOrUpdateLatestHistory;
60
+ }