@pikku/kysely 0.12.15 → 0.12.16

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,24 @@
1
+ ## 0.12.16
2
+
3
+ ### Patch Changes
4
+
5
+ - 6565b97: Fix `KyselyWorkflowRunService.getRunSteps`: build the `runningAt` /
6
+ `succeededAt` / `failedAt` correlated subqueries through the query builder
7
+ instead of raw `sql` fragments, so the active schema (`withSchema(...)`)
8
+ qualifies `workflow_step_history`. The raw fragments hardcoded an unqualified
9
+ table name and failed with `relation "workflow_step_history" does not exist`
10
+ against a connection whose `search_path` did not include the schema.
11
+ - 34f254e: Bump the `kysely` dependency range to `^0.29.0` so it dedupes onto a single
12
+ copy alongside Better Auth (which bundles kysely 0.29.x), avoiding two
13
+ incompatible `Kysely` classes (the `#private` brand mismatch) when both pikku's
14
+ adapters and Better Auth share a database connection.
15
+
16
+ kysely 0.29 is ESM-only, which the unmaintained `kysely-plugin-serialize`
17
+ (no `exports` map, CommonJS build) cannot import. Its `SerializePlugin` is now
18
+ maintained directly in `@pikku/kysely` and re-exported, and the external
19
+ dependency is dropped from `@pikku/kysely`, `@pikku/kysely-sqlite`, and
20
+ `@pikku/cloudflare`.
21
+
1
22
  ## 0.12.15
2
23
 
3
24
  ### Patch Changes
@@ -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 { SerializePlugin, SqliteSerializePlugin, BaseSerializePlugin, defaultSerializer, defaultDeserializer, maybeJson, skipTransform, dateRegex, type Serializer, type Deserializer, type SerializePluginOptions, } from './serialize-plugin.js';
16
17
  export type { KyselyPikkuDB } from './kysely-tables.js';
17
18
  export type { WorkflowRunService } from '@pikku/core/workflow';
18
19
  export type { AgentRunService, AgentRunRow } from '@pikku/core/ai-agent';
package/dist/src/index.js CHANGED
@@ -11,3 +11,4 @@ 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 { SerializePlugin, SqliteSerializePlugin, BaseSerializePlugin, defaultSerializer, defaultDeserializer, maybeJson, skipTransform, dateRegex, } from './serialize-plugin.js';
@@ -24,6 +24,6 @@ export declare class KyselySecretService implements SecretService {
24
24
  hasSecret(key: string): Promise<boolean>;
25
25
  setSecret(key: string, value: unknown): Promise<void>;
26
26
  deleteSecret(key: string): Promise<void>;
27
- getSecrets(keys: string[]): Promise<Record<string, unknown>>;
27
+ getSecrets<T extends Record<string, unknown> = Record<string, unknown>>(keys: (keyof T & string)[]): Promise<T>;
28
28
  rotateKEK(): Promise<number>;
29
29
  }
@@ -122,8 +122,15 @@ export class KyselySecretService {
122
122
  const kek = this.getKEK(row.keyVersion);
123
123
  out[row.key] = await envelopeDecrypt(kek, row.ciphertext, row.wrappedDek);
124
124
  }
125
- catch {
126
- // skip secrets that fail to decrypt
125
+ catch (cause) {
126
+ // A stored secret that fails to decrypt is never expected — it means the
127
+ // KEK does not match what the value was wrapped under (mismatched
128
+ // PIKKU_SECRET_KEK, or a missing previousKey for an older key_version).
129
+ // Swallowing it produces an undefined secret and an opaque downstream
130
+ // failure (e.g. an auth "server configuration" 500). Fail loud, naming
131
+ // the key and key_version so the misconfiguration is diagnosable.
132
+ throw new Error(`Failed to decrypt secret "${row.key}" (key_version ${row.keyVersion}): ` +
133
+ `the configured KEK does not match the key it was wrapped under`, { cause });
127
134
  }
128
135
  }
129
136
  return out;
@@ -1,4 +1,3 @@
1
- import { sql } from 'kysely';
2
1
  import { parseJson } from './kysely-json.js';
3
2
  export class KyselyWorkflowRunService {
4
3
  db;
@@ -83,9 +82,26 @@ export class KyselyWorkflowRunService {
83
82
  .select(eb.fn.countAll().as('cnt'))
84
83
  .whereRef('workflowStepHistory.workflowStepId', '=', 's.workflowStepId')
85
84
  .as('attemptCount'))
86
- .select(sql `(SELECT MAX(h.running_at) FROM workflow_step_history h WHERE h.workflow_step_id = s.workflow_step_id)`.as('runningAt'))
87
- .select(sql `(SELECT MAX(h.succeeded_at) FROM workflow_step_history h WHERE h.workflow_step_id = s.workflow_step_id)`.as('succeededAt'))
88
- .select(sql `(SELECT MAX(h.failed_at) FROM workflow_step_history h WHERE h.workflow_step_id = s.workflow_step_id)`.as('failedAt'))
85
+ // Build these correlated subqueries through the query builder rather
86
+ // than a raw `sql` fragment so the active schema (e.g.
87
+ // `withSchema('app')`) is applied to `workflow_step_history`. A raw
88
+ // fragment hardcodes an unqualified table name and dies against a
89
+ // connection whose search_path does not include the schema.
90
+ .select((eb) => eb
91
+ .selectFrom('workflowStepHistory')
92
+ .select((eb2) => eb2.fn.max('workflowStepHistory.runningAt').as('m'))
93
+ .whereRef('workflowStepHistory.workflowStepId', '=', 's.workflowStepId')
94
+ .as('runningAt'))
95
+ .select((eb) => eb
96
+ .selectFrom('workflowStepHistory')
97
+ .select((eb2) => eb2.fn.max('workflowStepHistory.succeededAt').as('m'))
98
+ .whereRef('workflowStepHistory.workflowStepId', '=', 's.workflowStepId')
99
+ .as('succeededAt'))
100
+ .select((eb) => eb
101
+ .selectFrom('workflowStepHistory')
102
+ .select((eb2) => eb2.fn.max('workflowStepHistory.failedAt').as('m'))
103
+ .whereRef('workflowStepHistory.workflowStepId', '=', 's.workflowStepId')
104
+ .as('failedAt'))
89
105
  .where('s.workflowRunId', '=', runId)
90
106
  .orderBy('s.createdAt', 'asc')
91
107
  .execute();
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Serialize / deserialize plugin for Kysely.
3
+ *
4
+ * Adapted from `kysely-plugin-serialize` (MIT, by subframe7536 —
5
+ * https://github.com/subframe7536/kysely-plugin-serialize), which is
6
+ * unmaintained and ships no `exports` map, so modern Node16/ESM resolution
7
+ * falls back to its CommonJS build and cannot import ESM-only kysely (>= 0.29).
8
+ * Maintained here directly as part of @pikku/kysely so we publish it ourselves.
9
+ *
10
+ * Serializes object/array/Date/boolean parameters to strings on the way into
11
+ * SQLite-style dialects, and deserializes them back on the way out.
12
+ */
13
+ import { type KyselyPlugin, type PluginTransformQueryArgs, type PluginTransformResultArgs, type QueryResult, type RootOperationNode, type UnknownRow } from 'kysely';
14
+ export type Serializer = (parameter: unknown) => unknown;
15
+ export type Deserializer = (parameter: unknown) => unknown;
16
+ export declare const defaultSerializer: Serializer;
17
+ export declare const dateRegex: RegExp;
18
+ export declare const defaultDeserializer: Deserializer;
19
+ /**
20
+ * Checks if a given string parameter is a JSON-like string.
21
+ *
22
+ * Determines whether the input starts and ends with curly braces `{}` or
23
+ * square brackets `[]`, the typical indicators of JSON objects and arrays.
24
+ */
25
+ export declare function maybeJson(parameter: string): boolean;
26
+ /**
27
+ * Determines whether a given parameter should be skipped during transformation.
28
+ *
29
+ * Skipped when it is `undefined`, `null`, a `bigint`, a `number`, or an object
30
+ * that exposes a `buffer` property (e.g. a typed array / Buffer).
31
+ */
32
+ export declare function skipTransform(parameter: unknown): boolean;
33
+ export declare class BaseSerializePlugin implements KyselyPlugin {
34
+ private readonly transformer;
35
+ private readonly deserializer;
36
+ private readonly skipNodeSet?;
37
+ private readonly ctx?;
38
+ /**
39
+ * Base class for {@link SerializePlugin}, without default options.
40
+ */
41
+ constructor(serializer: Serializer, deserializer: Deserializer, skipNodeKind: Array<RootOperationNode['kind']>);
42
+ transformQuery({ node, queryId, }: PluginTransformQueryArgs): RootOperationNode;
43
+ transformResult({ result, queryId, }: PluginTransformResultArgs): Promise<QueryResult<UnknownRow>>;
44
+ private parseRows;
45
+ }
46
+ export interface SerializePluginOptions {
47
+ /**
48
+ * serialize params
49
+ */
50
+ serializer?: Serializer;
51
+ /**
52
+ * deserialize params
53
+ */
54
+ deserializer?: Deserializer;
55
+ /**
56
+ * node kind to skip transform
57
+ */
58
+ skipNodeKind?: Array<RootOperationNode['kind']>;
59
+ }
60
+ export declare class SerializePlugin extends BaseSerializePlugin {
61
+ /**
62
+ * _**THIS PLUGIN SHOULD BE PLACED AT THE END OF PLUGINS ARRAY !!!**_
63
+ *
64
+ * reference from https://github.com/koskimas/kysely/pull/138
65
+ *
66
+ * Serializes object/array/Date/boolean column values to strings for
67
+ * SQLite-style dialects and deserializes them on the way back out. Pass a
68
+ * custom `serializer`/`deserializer` to override the default behaviour.
69
+ */
70
+ constructor(options?: SerializePluginOptions);
71
+ }
72
+ /**
73
+ * @deprecated use {@link SerializePlugin} instead
74
+ */
75
+ export declare const SqliteSerializePlugin: typeof SerializePlugin;
@@ -0,0 +1,177 @@
1
+ /**
2
+ * Serialize / deserialize plugin for Kysely.
3
+ *
4
+ * Adapted from `kysely-plugin-serialize` (MIT, by subframe7536 —
5
+ * https://github.com/subframe7536/kysely-plugin-serialize), which is
6
+ * unmaintained and ships no `exports` map, so modern Node16/ESM resolution
7
+ * falls back to its CommonJS build and cannot import ESM-only kysely (>= 0.29).
8
+ * Maintained here directly as part of @pikku/kysely so we publish it ourselves.
9
+ *
10
+ * Serializes object/array/Date/boolean parameters to strings on the way into
11
+ * SQLite-style dialects, and deserializes them back on the way out.
12
+ */
13
+ import { OperationNodeTransformer, } from 'kysely';
14
+ class SerializeParametersTransformer extends OperationNodeTransformer {
15
+ serializer;
16
+ constructor(serializer) {
17
+ super();
18
+ this.serializer = serializer;
19
+ }
20
+ transformPrimitiveValueList(node) {
21
+ return {
22
+ ...node,
23
+ values: node.values.map(this.serializer),
24
+ };
25
+ }
26
+ // https://www.npmjs.com/package/zodsql
27
+ transformColumnUpdate(node) {
28
+ const { value: valueNode } = node;
29
+ if (valueNode.kind !== 'ValueNode') {
30
+ return super.transformColumnUpdate(node);
31
+ }
32
+ const { value, ...item } = valueNode;
33
+ const serializedValue = this.serializer(value);
34
+ if (value === serializedValue) {
35
+ return super.transformColumnUpdate(node);
36
+ }
37
+ const serializedNode = { ...item, value: serializedValue };
38
+ return super.transformColumnUpdate({ ...node, value: serializedNode });
39
+ }
40
+ transformValue(node) {
41
+ return {
42
+ ...node,
43
+ value: this.serializer(node.value),
44
+ };
45
+ }
46
+ }
47
+ export const defaultSerializer = (parameter) => {
48
+ if (skipTransform(parameter) || typeof parameter === 'string') {
49
+ return parameter;
50
+ }
51
+ else if (typeof parameter === 'boolean') {
52
+ return '' + parameter;
53
+ }
54
+ else if (parameter instanceof Date) {
55
+ return parameter.toISOString();
56
+ }
57
+ else {
58
+ try {
59
+ return JSON.stringify(parameter);
60
+ }
61
+ catch {
62
+ return parameter;
63
+ }
64
+ }
65
+ };
66
+ export const dateRegex = /^\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}(?:\.\d+)?Z?$/;
67
+ export const defaultDeserializer = (parameter) => {
68
+ if (skipTransform(parameter)) {
69
+ return parameter;
70
+ }
71
+ if (typeof parameter === 'string') {
72
+ if (parameter === 'true') {
73
+ return true;
74
+ }
75
+ else if (parameter === 'false') {
76
+ return false;
77
+ }
78
+ else if (dateRegex.test(parameter)) {
79
+ return new Date(parameter);
80
+ }
81
+ else if (maybeJson(parameter)) {
82
+ try {
83
+ return JSON.parse(parameter);
84
+ }
85
+ catch {
86
+ // not valid JSON — fall through and return the raw string
87
+ }
88
+ }
89
+ return parameter;
90
+ }
91
+ return parameter;
92
+ };
93
+ /**
94
+ * Checks if a given string parameter is a JSON-like string.
95
+ *
96
+ * Determines whether the input starts and ends with curly braces `{}` or
97
+ * square brackets `[]`, the typical indicators of JSON objects and arrays.
98
+ */
99
+ export function maybeJson(parameter) {
100
+ return ((parameter.startsWith('{') && parameter.endsWith('}')) ||
101
+ (parameter.startsWith('[') && parameter.endsWith(']')));
102
+ }
103
+ /**
104
+ * Determines whether a given parameter should be skipped during transformation.
105
+ *
106
+ * Skipped when it is `undefined`, `null`, a `bigint`, a `number`, or an object
107
+ * that exposes a `buffer` property (e.g. a typed array / Buffer).
108
+ */
109
+ export function skipTransform(parameter) {
110
+ return (parameter === undefined ||
111
+ parameter === null ||
112
+ typeof parameter === 'bigint' ||
113
+ typeof parameter === 'number' ||
114
+ (typeof parameter === 'object' && 'buffer' in parameter));
115
+ }
116
+ export class BaseSerializePlugin {
117
+ transformer;
118
+ deserializer;
119
+ skipNodeSet;
120
+ ctx;
121
+ /**
122
+ * Base class for {@link SerializePlugin}, without default options.
123
+ */
124
+ constructor(serializer, deserializer, skipNodeKind) {
125
+ this.transformer = new SerializeParametersTransformer(serializer);
126
+ this.deserializer = deserializer;
127
+ if (skipNodeKind.length) {
128
+ this.skipNodeSet = new Set(skipNodeKind);
129
+ this.ctx = new WeakSet();
130
+ }
131
+ }
132
+ transformQuery({ node, queryId, }) {
133
+ if (this.skipNodeSet?.has(node.kind)) {
134
+ this.ctx?.add(queryId);
135
+ return node;
136
+ }
137
+ return this.transformer.transformNode(node);
138
+ }
139
+ async transformResult({ result, queryId, }) {
140
+ return this.ctx?.has(queryId)
141
+ ? result
142
+ : { ...result, rows: this.parseRows(result.rows) };
143
+ }
144
+ parseRows(rows) {
145
+ const result = [];
146
+ for (const row of rows) {
147
+ if (!row) {
148
+ continue;
149
+ }
150
+ const parsedRow = {};
151
+ for (const [key, value] of Object.entries(row)) {
152
+ parsedRow[key] = this.deserializer(value);
153
+ }
154
+ result.push(parsedRow);
155
+ }
156
+ return result;
157
+ }
158
+ }
159
+ export class SerializePlugin extends BaseSerializePlugin {
160
+ /**
161
+ * _**THIS PLUGIN SHOULD BE PLACED AT THE END OF PLUGINS ARRAY !!!**_
162
+ *
163
+ * reference from https://github.com/koskimas/kysely/pull/138
164
+ *
165
+ * Serializes object/array/Date/boolean column values to strings for
166
+ * SQLite-style dialects and deserializes them on the way back out. Pass a
167
+ * custom `serializer`/`deserializer` to override the default behaviour.
168
+ */
169
+ constructor(options = {}) {
170
+ const { deserializer = defaultDeserializer, serializer = defaultSerializer, skipNodeKind = [], } = options;
171
+ super(serializer, deserializer, skipNodeKind);
172
+ }
173
+ }
174
+ /**
175
+ * @deprecated use {@link SerializePlugin} instead
176
+ */
177
+ export const SqliteSerializePlugin = SerializePlugin;
@@ -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","../bin/pikku-kysely-pure.ts"],"version":"5.9.3"}
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":"5.9.3"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pikku/kysely",
3
- "version": "0.12.15",
3
+ "version": "0.12.16",
4
4
  "author": "yasser.fadl@gmail.com",
5
5
  "license": "MIT",
6
6
  "module": "dist/src/index.js",
@@ -23,13 +23,12 @@
23
23
  "@pikku/core": "^0.12.28"
24
24
  },
25
25
  "dependencies": {
26
- "kysely": "^0.28.12"
26
+ "kysely": "^0.29.0"
27
27
  },
28
28
  "devDependencies": {
29
29
  "@types/better-sqlite3": "^7.6.13",
30
30
  "better-sqlite3": "^12.10.0",
31
31
  "kysely-codegen": "^0.20.0",
32
- "kysely-plugin-serialize": "^0.8.2",
33
32
  "kysely-postgres-js": "^3.0.0",
34
33
  "postgres": "^3.4.8",
35
34
  "typescript": "^5.9"
package/src/index.ts CHANGED
@@ -17,6 +17,20 @@ export {
17
17
  type CreateAuditedKyselyOptions,
18
18
  } from './create-audited-kysely.js'
19
19
 
20
+ export {
21
+ SerializePlugin,
22
+ SqliteSerializePlugin,
23
+ BaseSerializePlugin,
24
+ defaultSerializer,
25
+ defaultDeserializer,
26
+ maybeJson,
27
+ skipTransform,
28
+ dateRegex,
29
+ type Serializer,
30
+ type Deserializer,
31
+ type SerializePluginOptions,
32
+ } from './serialize-plugin.js'
33
+
20
34
  export type { KyselyPikkuDB } from './kysely-tables.js'
21
35
  export type { WorkflowRunService } from '@pikku/core/workflow'
22
36
  export type { AgentRunService, AgentRunRow } from '@pikku/core/ai-agent'
@@ -149,7 +149,9 @@ export class KyselySecretService implements SecretService {
149
149
  await this.logAudit(key, 'delete')
150
150
  }
151
151
 
152
- async getSecrets(keys: string[]): Promise<Record<string, unknown>> {
152
+ async getSecrets<
153
+ T extends Record<string, unknown> = Record<string, unknown>,
154
+ >(keys: (keyof T & string)[]): Promise<T> {
153
155
  const rows = await this.db
154
156
  .selectFrom('secrets')
155
157
  .select(['key', 'ciphertext', 'wrappedDek', 'keyVersion'])
@@ -165,11 +167,21 @@ export class KyselySecretService implements SecretService {
165
167
  row.ciphertext,
166
168
  row.wrappedDek
167
169
  )
168
- } catch {
169
- // skip secrets that fail to decrypt
170
+ } catch (cause) {
171
+ // A stored secret that fails to decrypt is never expected — it means the
172
+ // KEK does not match what the value was wrapped under (mismatched
173
+ // PIKKU_SECRET_KEK, or a missing previousKey for an older key_version).
174
+ // Swallowing it produces an undefined secret and an opaque downstream
175
+ // failure (e.g. an auth "server configuration" 500). Fail loud, naming
176
+ // the key and key_version so the misconfiguration is diagnosable.
177
+ throw new Error(
178
+ `Failed to decrypt secret "${row.key}" (key_version ${row.keyVersion}): ` +
179
+ `the configured KEK does not match the key it was wrapped under`,
180
+ { cause }
181
+ )
170
182
  }
171
183
  }
172
- return out
184
+ return out as T
173
185
  }
174
186
 
175
187
  async rotateKEK(): Promise<number> {
@@ -1,7 +1,7 @@
1
1
  import { describe, test, before, after } from 'node:test'
2
2
  import assert from 'node:assert/strict'
3
3
  import { CamelCasePlugin, Kysely, SqliteDialect } from 'kysely'
4
- import { SerializePlugin } from 'kysely-plugin-serialize'
4
+ import { SerializePlugin } from './serialize-plugin.js'
5
5
  import Database from 'better-sqlite3'
6
6
  import { PostgresJSDialect } from 'kysely-postgres-js'
7
7
  import postgres from 'postgres'
@@ -1,7 +1,7 @@
1
1
  import { describe, test, beforeEach, afterEach } from 'node:test'
2
2
  import assert from 'node:assert/strict'
3
3
  import { CamelCasePlugin, Kysely, SqliteDialect } from 'kysely'
4
- import { SerializePlugin } from 'kysely-plugin-serialize'
4
+ import { SerializePlugin } from './serialize-plugin.js'
5
5
  import Database from 'better-sqlite3'
6
6
 
7
7
  import type { KyselyPikkuDB } from './kysely-tables.js'
@@ -5,7 +5,6 @@ import type {
5
5
  WorkflowRunService,
6
6
  } from '@pikku/core/workflow'
7
7
  import type { Kysely } from 'kysely'
8
- import { sql } from 'kysely'
9
8
  import type { KyselyPikkuDB } from './kysely-tables.js'
10
9
  import { parseJson } from './kysely-json.js'
11
10
 
@@ -112,14 +111,43 @@ export class KyselyWorkflowRunService implements WorkflowRunService {
112
111
  )
113
112
  .as('attemptCount')
114
113
  )
115
- .select(
116
- sql<string | null>`(SELECT MAX(h.running_at) FROM workflow_step_history h WHERE h.workflow_step_id = s.workflow_step_id)`.as('runningAt')
114
+ // Build these correlated subqueries through the query builder rather
115
+ // than a raw `sql` fragment so the active schema (e.g.
116
+ // `withSchema('app')`) is applied to `workflow_step_history`. A raw
117
+ // fragment hardcodes an unqualified table name and dies against a
118
+ // connection whose search_path does not include the schema.
119
+ .select((eb) =>
120
+ eb
121
+ .selectFrom('workflowStepHistory')
122
+ .select((eb2) => eb2.fn.max('workflowStepHistory.runningAt').as('m'))
123
+ .whereRef(
124
+ 'workflowStepHistory.workflowStepId',
125
+ '=',
126
+ 's.workflowStepId'
127
+ )
128
+ .as('runningAt')
117
129
  )
118
- .select(
119
- sql<string | null>`(SELECT MAX(h.succeeded_at) FROM workflow_step_history h WHERE h.workflow_step_id = s.workflow_step_id)`.as('succeededAt')
130
+ .select((eb) =>
131
+ eb
132
+ .selectFrom('workflowStepHistory')
133
+ .select((eb2) => eb2.fn.max('workflowStepHistory.succeededAt').as('m'))
134
+ .whereRef(
135
+ 'workflowStepHistory.workflowStepId',
136
+ '=',
137
+ 's.workflowStepId'
138
+ )
139
+ .as('succeededAt')
120
140
  )
121
- .select(
122
- sql<string | null>`(SELECT MAX(h.failed_at) FROM workflow_step_history h WHERE h.workflow_step_id = s.workflow_step_id)`.as('failedAt')
141
+ .select((eb) =>
142
+ eb
143
+ .selectFrom('workflowStepHistory')
144
+ .select((eb2) => eb2.fn.max('workflowStepHistory.failedAt').as('m'))
145
+ .whereRef(
146
+ 'workflowStepHistory.workflowStepId',
147
+ '=',
148
+ 's.workflowStepId'
149
+ )
150
+ .as('failedAt')
123
151
  )
124
152
  .where('s.workflowRunId', '=', runId)
125
153
  .orderBy('s.createdAt', 'asc')
@@ -0,0 +1,238 @@
1
+ /**
2
+ * Serialize / deserialize plugin for Kysely.
3
+ *
4
+ * Adapted from `kysely-plugin-serialize` (MIT, by subframe7536 —
5
+ * https://github.com/subframe7536/kysely-plugin-serialize), which is
6
+ * unmaintained and ships no `exports` map, so modern Node16/ESM resolution
7
+ * falls back to its CommonJS build and cannot import ESM-only kysely (>= 0.29).
8
+ * Maintained here directly as part of @pikku/kysely so we publish it ourselves.
9
+ *
10
+ * Serializes object/array/Date/boolean parameters to strings on the way into
11
+ * SQLite-style dialects, and deserializes them back on the way out.
12
+ */
13
+ import {
14
+ OperationNodeTransformer,
15
+ type ColumnUpdateNode,
16
+ type KyselyPlugin,
17
+ type PluginTransformQueryArgs,
18
+ type PluginTransformResultArgs,
19
+ type PrimitiveValueListNode,
20
+ type QueryId,
21
+ type QueryResult,
22
+ type RootOperationNode,
23
+ type UnknownRow,
24
+ type ValueNode,
25
+ } from 'kysely'
26
+
27
+ export type Serializer = (parameter: unknown) => unknown
28
+ export type Deserializer = (parameter: unknown) => unknown
29
+
30
+ class SerializeParametersTransformer extends OperationNodeTransformer {
31
+ private readonly serializer: Serializer
32
+
33
+ constructor(serializer: Serializer) {
34
+ super()
35
+ this.serializer = serializer
36
+ }
37
+
38
+ protected override transformPrimitiveValueList(
39
+ node: PrimitiveValueListNode
40
+ ): PrimitiveValueListNode {
41
+ return {
42
+ ...node,
43
+ values: node.values.map(this.serializer),
44
+ }
45
+ }
46
+
47
+ // https://www.npmjs.com/package/zodsql
48
+ protected override transformColumnUpdate(
49
+ node: ColumnUpdateNode
50
+ ): ColumnUpdateNode {
51
+ const { value: valueNode } = node
52
+ if (valueNode.kind !== 'ValueNode') {
53
+ return super.transformColumnUpdate(node)
54
+ }
55
+ const { value, ...item } = valueNode as ValueNode
56
+ const serializedValue = this.serializer(value)
57
+ if (value === serializedValue) {
58
+ return super.transformColumnUpdate(node)
59
+ }
60
+ const serializedNode: ValueNode = { ...item, value: serializedValue }
61
+ return super.transformColumnUpdate({ ...node, value: serializedNode })
62
+ }
63
+
64
+ protected override transformValue(node: ValueNode): ValueNode {
65
+ return {
66
+ ...node,
67
+ value: this.serializer(node.value),
68
+ }
69
+ }
70
+ }
71
+
72
+ export const defaultSerializer: Serializer = (parameter) => {
73
+ if (skipTransform(parameter) || typeof parameter === 'string') {
74
+ return parameter
75
+ } else if (typeof parameter === 'boolean') {
76
+ return '' + parameter
77
+ } else if (parameter instanceof Date) {
78
+ return parameter.toISOString()
79
+ } else {
80
+ try {
81
+ return JSON.stringify(parameter)
82
+ } catch {
83
+ return parameter
84
+ }
85
+ }
86
+ }
87
+
88
+ export const dateRegex = /^\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}(?:\.\d+)?Z?$/
89
+
90
+ export const defaultDeserializer: Deserializer = (parameter) => {
91
+ if (skipTransform(parameter)) {
92
+ return parameter
93
+ }
94
+ if (typeof parameter === 'string') {
95
+ if (parameter === 'true') {
96
+ return true
97
+ } else if (parameter === 'false') {
98
+ return false
99
+ } else if (dateRegex.test(parameter)) {
100
+ return new Date(parameter)
101
+ } else if (maybeJson(parameter)) {
102
+ try {
103
+ return JSON.parse(parameter)
104
+ } catch {
105
+ // not valid JSON — fall through and return the raw string
106
+ }
107
+ }
108
+ return parameter
109
+ }
110
+ return parameter
111
+ }
112
+
113
+ /**
114
+ * Checks if a given string parameter is a JSON-like string.
115
+ *
116
+ * Determines whether the input starts and ends with curly braces `{}` or
117
+ * square brackets `[]`, the typical indicators of JSON objects and arrays.
118
+ */
119
+ export function maybeJson(parameter: string): boolean {
120
+ return (
121
+ (parameter.startsWith('{') && parameter.endsWith('}')) ||
122
+ (parameter.startsWith('[') && parameter.endsWith(']'))
123
+ )
124
+ }
125
+
126
+ /**
127
+ * Determines whether a given parameter should be skipped during transformation.
128
+ *
129
+ * Skipped when it is `undefined`, `null`, a `bigint`, a `number`, or an object
130
+ * that exposes a `buffer` property (e.g. a typed array / Buffer).
131
+ */
132
+ export function skipTransform(parameter: unknown): boolean {
133
+ return (
134
+ parameter === undefined ||
135
+ parameter === null ||
136
+ typeof parameter === 'bigint' ||
137
+ typeof parameter === 'number' ||
138
+ (typeof parameter === 'object' && 'buffer' in parameter)
139
+ )
140
+ }
141
+
142
+ export class BaseSerializePlugin implements KyselyPlugin {
143
+ private readonly transformer: SerializeParametersTransformer
144
+ private readonly deserializer: Deserializer
145
+ private readonly skipNodeSet?: Set<RootOperationNode['kind']>
146
+ private readonly ctx?: WeakSet<QueryId>
147
+
148
+ /**
149
+ * Base class for {@link SerializePlugin}, without default options.
150
+ */
151
+ constructor(
152
+ serializer: Serializer,
153
+ deserializer: Deserializer,
154
+ skipNodeKind: Array<RootOperationNode['kind']>
155
+ ) {
156
+ this.transformer = new SerializeParametersTransformer(serializer)
157
+ this.deserializer = deserializer
158
+ if (skipNodeKind.length) {
159
+ this.skipNodeSet = new Set(skipNodeKind)
160
+ this.ctx = new WeakSet()
161
+ }
162
+ }
163
+
164
+ transformQuery({
165
+ node,
166
+ queryId,
167
+ }: PluginTransformQueryArgs): RootOperationNode {
168
+ if (this.skipNodeSet?.has(node.kind)) {
169
+ this.ctx?.add(queryId)
170
+ return node
171
+ }
172
+ return this.transformer.transformNode(node)
173
+ }
174
+
175
+ async transformResult({
176
+ result,
177
+ queryId,
178
+ }: PluginTransformResultArgs): Promise<QueryResult<UnknownRow>> {
179
+ return this.ctx?.has(queryId)
180
+ ? result
181
+ : { ...result, rows: this.parseRows(result.rows) }
182
+ }
183
+
184
+ private parseRows(rows: UnknownRow[]): UnknownRow[] {
185
+ const result: UnknownRow[] = []
186
+ for (const row of rows) {
187
+ if (!row) {
188
+ continue
189
+ }
190
+ const parsedRow: UnknownRow = {}
191
+ for (const [key, value] of Object.entries(row)) {
192
+ parsedRow[key] = this.deserializer(value)
193
+ }
194
+ result.push(parsedRow)
195
+ }
196
+ return result
197
+ }
198
+ }
199
+
200
+ export interface SerializePluginOptions {
201
+ /**
202
+ * serialize params
203
+ */
204
+ serializer?: Serializer
205
+ /**
206
+ * deserialize params
207
+ */
208
+ deserializer?: Deserializer
209
+ /**
210
+ * node kind to skip transform
211
+ */
212
+ skipNodeKind?: Array<RootOperationNode['kind']>
213
+ }
214
+
215
+ export class SerializePlugin extends BaseSerializePlugin {
216
+ /**
217
+ * _**THIS PLUGIN SHOULD BE PLACED AT THE END OF PLUGINS ARRAY !!!**_
218
+ *
219
+ * reference from https://github.com/koskimas/kysely/pull/138
220
+ *
221
+ * Serializes object/array/Date/boolean column values to strings for
222
+ * SQLite-style dialects and deserializes them on the way back out. Pass a
223
+ * custom `serializer`/`deserializer` to override the default behaviour.
224
+ */
225
+ constructor(options: SerializePluginOptions = {}) {
226
+ const {
227
+ deserializer = defaultDeserializer,
228
+ serializer = defaultSerializer,
229
+ skipNodeKind = [],
230
+ } = options
231
+ super(serializer, deserializer, skipNodeKind)
232
+ }
233
+ }
234
+
235
+ /**
236
+ * @deprecated use {@link SerializePlugin} instead
237
+ */
238
+ export const SqliteSerializePlugin = SerializePlugin