@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 +21 -0
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.js +1 -0
- package/dist/src/kysely-secret-service.d.ts +1 -1
- package/dist/src/kysely-secret-service.js +9 -2
- package/dist/src/kysely-workflow-run-service.js +20 -4
- package/dist/src/serialize-plugin.d.ts +75 -0
- package/dist/src/serialize-plugin.js +177 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -3
- package/src/index.ts +14 -0
- package/src/kysely-secret-service.ts +16 -4
- package/src/kysely-services.test.ts +1 -1
- package/src/kysely-workflow-mirror.test.ts +1 -1
- package/src/kysely-workflow-run-service.ts +35 -7
- package/src/serialize-plugin.ts +238 -0
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
|
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 { 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<
|
|
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
|
-
//
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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.
|
|
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.
|
|
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
|
|
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
|
-
//
|
|
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 '
|
|
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 '
|
|
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
|
-
|
|
116
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|