@pikku/kysely 0.13.0 → 0.13.2
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 +117 -0
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.js +2 -0
- package/dist/src/kysely-ai-agent-run-service.d.ts +1 -0
- package/dist/src/kysely-ai-agent-run-service.js +15 -1
- package/dist/src/kysely-scope-service.d.ts +59 -0
- package/dist/src/kysely-scope-service.js +268 -0
- package/dist/src/kysely-tables.d.ts +67 -0
- package/dist/src/kysely-webhook-service.d.ts +27 -0
- package/dist/src/kysely-webhook-service.js +150 -0
- package/dist/src/kysely-workflow-service.js +1 -5
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/run-tests.sh +4 -4
- package/src/index.ts +2 -0
- package/src/kysely-ai-agent-run-service.ts +33 -1
- package/src/kysely-scope-service.test.ts +416 -0
- package/src/kysely-scope-service.ts +350 -0
- package/src/kysely-tables.ts +74 -0
- package/src/kysely-webhook-service.test.ts +120 -0
- package/src/kysely-webhook-service.ts +200 -0
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { PIKKU_OUTGOING_WEBHOOK_QUEUE_NAME, QueueWebhookService, } from '@pikku/core/services';
|
|
2
|
+
import { sql } from 'kysely';
|
|
3
|
+
/**
|
|
4
|
+
* Durable {@link QueueWebhookService}: still delivers through the
|
|
5
|
+
* `pikku-outgoing-webhooks` queue, but records a `webhook_delivery` row per
|
|
6
|
+
* `send()` and one `webhook_delivery_attempt` row per try. Register it as the
|
|
7
|
+
* `webhookService` singleton; the queue worker persists each attempt through
|
|
8
|
+
* {@link recordAttempt}, overriding the base's throwing default.
|
|
9
|
+
*/
|
|
10
|
+
export class KyselyWebhookService extends QueueWebhookService {
|
|
11
|
+
db;
|
|
12
|
+
initialized = false;
|
|
13
|
+
constructor(queueService, db) {
|
|
14
|
+
super(queueService);
|
|
15
|
+
this.db = db;
|
|
16
|
+
}
|
|
17
|
+
async init() {
|
|
18
|
+
if (this.initialized) {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
await this.db.schema
|
|
22
|
+
.createTable('webhook_delivery')
|
|
23
|
+
.ifNotExists()
|
|
24
|
+
.addColumn('delivery_id', 'text', (col) => col.primaryKey())
|
|
25
|
+
.addColumn('organization_id', 'text')
|
|
26
|
+
.addColumn('url', 'text', (col) => col.notNull())
|
|
27
|
+
.addColumn('event', 'text')
|
|
28
|
+
.addColumn('status', 'text', (col) => col.defaultTo('pending').notNull())
|
|
29
|
+
.addColumn('attempts', 'integer', (col) => col.defaultTo(0).notNull())
|
|
30
|
+
.addColumn('created_at', 'timestamp', (col) => col.defaultTo(sql `CURRENT_TIMESTAMP`).notNull())
|
|
31
|
+
.addColumn('updated_at', 'timestamp', (col) => col.defaultTo(sql `CURRENT_TIMESTAMP`).notNull())
|
|
32
|
+
.addColumn('delivered_at', 'timestamp')
|
|
33
|
+
.execute();
|
|
34
|
+
await this.db.schema
|
|
35
|
+
.createTable('webhook_delivery_attempt')
|
|
36
|
+
.ifNotExists()
|
|
37
|
+
.addColumn('attempt_id', 'text', (col) => col.primaryKey())
|
|
38
|
+
.addColumn('delivery_id', 'text', (col) => col.notNull().references('webhook_delivery.delivery_id').onDelete('cascade'))
|
|
39
|
+
.addColumn('attempt_number', 'integer', (col) => col.notNull())
|
|
40
|
+
.addColumn('status_code', 'integer')
|
|
41
|
+
.addColumn('response_body', 'text')
|
|
42
|
+
.addColumn('error', 'text')
|
|
43
|
+
.addColumn('created_at', 'timestamp', (col) => col.defaultTo(sql `CURRENT_TIMESTAMP`).notNull())
|
|
44
|
+
.execute();
|
|
45
|
+
await this.createIndexSafe(this.db.schema
|
|
46
|
+
.createIndex('idx_webhook_delivery_org')
|
|
47
|
+
.ifNotExists()
|
|
48
|
+
.on('webhook_delivery')
|
|
49
|
+
.column('organization_id'));
|
|
50
|
+
await this.createIndexSafe(this.db.schema
|
|
51
|
+
.createIndex('idx_webhook_delivery_attempt_delivery')
|
|
52
|
+
.ifNotExists()
|
|
53
|
+
.on('webhook_delivery_attempt')
|
|
54
|
+
.column('delivery_id'));
|
|
55
|
+
this.initialized = true;
|
|
56
|
+
}
|
|
57
|
+
async send(input) {
|
|
58
|
+
const deliveryId = globalThis.crypto.randomUUID();
|
|
59
|
+
const { jobData, options } = await this.prepareDelivery(input);
|
|
60
|
+
await this.db
|
|
61
|
+
.insertInto('webhookDelivery')
|
|
62
|
+
.values({
|
|
63
|
+
deliveryId,
|
|
64
|
+
organizationId: input.organizationId ?? null,
|
|
65
|
+
url: input.url,
|
|
66
|
+
event: input.event ?? null,
|
|
67
|
+
})
|
|
68
|
+
.execute();
|
|
69
|
+
// deliveryId doubles as the queue jobId (idempotency) and rides in the
|
|
70
|
+
// payload so the worker can record attempts against this row.
|
|
71
|
+
const jobId = await this.queueService.add(PIKKU_OUTGOING_WEBHOOK_QUEUE_NAME, { ...jobData, deliveryId }, { ...options, jobId: deliveryId });
|
|
72
|
+
return { jobId };
|
|
73
|
+
}
|
|
74
|
+
async recordAttempt(deliveryId, { statusCode, responseBody, error, delivered }) {
|
|
75
|
+
const now = new Date();
|
|
76
|
+
await this.db.transaction().execute(async (trx) => {
|
|
77
|
+
const existing = await trx
|
|
78
|
+
.selectFrom('webhookDeliveryAttempt')
|
|
79
|
+
.select((eb) => eb.fn.countAll().as('count'))
|
|
80
|
+
.where('deliveryId', '=', deliveryId)
|
|
81
|
+
.executeTakeFirst();
|
|
82
|
+
const attemptNumber = Number(existing?.count ?? 0) + 1;
|
|
83
|
+
await trx
|
|
84
|
+
.insertInto('webhookDeliveryAttempt')
|
|
85
|
+
.values({
|
|
86
|
+
attemptId: globalThis.crypto.randomUUID(),
|
|
87
|
+
deliveryId,
|
|
88
|
+
attemptNumber,
|
|
89
|
+
statusCode: statusCode ?? null,
|
|
90
|
+
responseBody: responseBody ?? null,
|
|
91
|
+
error: error ?? null,
|
|
92
|
+
})
|
|
93
|
+
.execute();
|
|
94
|
+
await trx
|
|
95
|
+
.updateTable('webhookDelivery')
|
|
96
|
+
.set({
|
|
97
|
+
status: delivered ? 'delivered' : 'failed',
|
|
98
|
+
attempts: attemptNumber,
|
|
99
|
+
updatedAt: now,
|
|
100
|
+
...(delivered ? { deliveredAt: now } : {}),
|
|
101
|
+
})
|
|
102
|
+
.where('deliveryId', '=', deliveryId)
|
|
103
|
+
.execute();
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
/** List deliveries, most recent first — for the console webhooks view. */
|
|
107
|
+
async listDeliveries(opts) {
|
|
108
|
+
let query = this.db
|
|
109
|
+
.selectFrom('webhookDelivery')
|
|
110
|
+
.selectAll()
|
|
111
|
+
.orderBy('createdAt', 'desc')
|
|
112
|
+
.limit(opts?.limit ?? 100);
|
|
113
|
+
if (opts?.organizationId) {
|
|
114
|
+
query = query.where('organizationId', '=', opts.organizationId);
|
|
115
|
+
}
|
|
116
|
+
return query.execute();
|
|
117
|
+
}
|
|
118
|
+
/** A single delivery with its full attempt history, or null if unknown. */
|
|
119
|
+
async getDelivery(deliveryId) {
|
|
120
|
+
const delivery = await this.db
|
|
121
|
+
.selectFrom('webhookDelivery')
|
|
122
|
+
.selectAll()
|
|
123
|
+
.where('deliveryId', '=', deliveryId)
|
|
124
|
+
.executeTakeFirst();
|
|
125
|
+
if (!delivery) {
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
const attempts = await this.db
|
|
129
|
+
.selectFrom('webhookDeliveryAttempt')
|
|
130
|
+
.selectAll()
|
|
131
|
+
.where('deliveryId', '=', deliveryId)
|
|
132
|
+
.orderBy('attemptNumber', 'asc')
|
|
133
|
+
.execute();
|
|
134
|
+
return { delivery, attempts };
|
|
135
|
+
}
|
|
136
|
+
async createIndexSafe(builder) {
|
|
137
|
+
try {
|
|
138
|
+
await builder.execute();
|
|
139
|
+
}
|
|
140
|
+
catch (e) {
|
|
141
|
+
if (e?.code === 'ER_DUP_KEYNAME' || e?.errno === 1061)
|
|
142
|
+
return;
|
|
143
|
+
if (e?.code === '42P07')
|
|
144
|
+
return;
|
|
145
|
+
if (e?.message?.includes('already exists'))
|
|
146
|
+
return;
|
|
147
|
+
throw e;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
@@ -308,11 +308,7 @@ export class KyselyWorkflowService extends PikkuWorkflowService {
|
|
|
308
308
|
if (latestHistory) {
|
|
309
309
|
await this.db
|
|
310
310
|
.updateTable('workflowStepHistory')
|
|
311
|
-
.set({
|
|
312
|
-
status: 'succeeded',
|
|
313
|
-
result: resultJson,
|
|
314
|
-
succeededAt: new Date(),
|
|
315
|
-
})
|
|
311
|
+
.set({ status: 'succeeded', result: resultJson, succeededAt: new Date() })
|
|
316
312
|
.where('historyId', '=', latestHistory.historyId)
|
|
317
313
|
.execute();
|
|
318
314
|
}
|
|
@@ -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-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"}
|
|
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-scope-service.ts","../src/kysely-secret-service.ts","../src/kysely-session-store.ts","../src/kysely-tables.ts","../src/kysely-webhook-service.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.13.
|
|
3
|
+
"version": "0.13.2",
|
|
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.66"
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
26
|
"kysely": "^0.29.0"
|
package/run-tests.sh
CHANGED
|
@@ -38,16 +38,16 @@ if [ ${#files[@]} -eq 0 ]; then
|
|
|
38
38
|
fi
|
|
39
39
|
|
|
40
40
|
# Construct the node command
|
|
41
|
-
node_cmd=
|
|
41
|
+
node_cmd=(node --import tsx --test)
|
|
42
42
|
|
|
43
43
|
# Append options based on flags
|
|
44
44
|
if [ "$watch_mode" = true ]; then
|
|
45
|
-
node_cmd
|
|
45
|
+
node_cmd+=(--watch)
|
|
46
46
|
fi
|
|
47
47
|
|
|
48
48
|
if [ "$coverage_mode" = true ]; then
|
|
49
|
-
node_cmd
|
|
49
|
+
node_cmd+=(--test-coverage-include="src/**/*.{ts,js}" --test-coverage-exclude="**/dist/**" --experimental-test-coverage --test-reporter=lcov --test-reporter-destination=lcov.info)
|
|
50
50
|
fi
|
|
51
51
|
|
|
52
52
|
# Execute the node command with the expanded list of files
|
|
53
|
-
$node_cmd "${files[@]}"
|
|
53
|
+
"${node_cmd[@]}" "${files[@]}"
|
package/src/index.ts
CHANGED
|
@@ -12,6 +12,8 @@ export type { KyselySecretServiceConfig } from './kysely-secret-service.js'
|
|
|
12
12
|
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
|
+
export { KyselyScopeService } from './kysely-scope-service.js'
|
|
16
|
+
export { KyselyWebhookService } from './kysely-webhook-service.js'
|
|
15
17
|
export {
|
|
16
18
|
createAuditedKysely,
|
|
17
19
|
type CreateAuditedKyselyOptions,
|
|
@@ -5,19 +5,36 @@ import type {
|
|
|
5
5
|
AgentRunService,
|
|
6
6
|
} from '@pikku/core/ai-agent'
|
|
7
7
|
import type { Kysely } from 'kysely'
|
|
8
|
+
import { sql } from 'kysely'
|
|
8
9
|
import type { KyselyPikkuDB } from './kysely-tables.js'
|
|
9
10
|
import { parseJson } from './kysely-json.js'
|
|
10
11
|
|
|
12
|
+
// Owner ids routinely contain `_` (e.g. `user_123`), which LIKE treats as a
|
|
13
|
+
// single-character wildcard — so a raw prefix would match a foreign owner.
|
|
14
|
+
const escapeLikePattern = (value: string) =>
|
|
15
|
+
value.replace(/[\\%_]/g, (char) => `\\${char}`)
|
|
16
|
+
|
|
11
17
|
export class KyselyAgentRunService implements AgentRunService {
|
|
12
18
|
constructor(private db: Kysely<KyselyPikkuDB>) {}
|
|
13
19
|
|
|
14
20
|
async listThreads(options?: {
|
|
15
21
|
agentName?: string
|
|
16
22
|
resourceId?: string
|
|
23
|
+
owners?: string[]
|
|
17
24
|
limit?: number
|
|
18
25
|
offset?: number
|
|
19
26
|
}): Promise<AIThread[]> {
|
|
20
|
-
const {
|
|
27
|
+
const {
|
|
28
|
+
agentName,
|
|
29
|
+
resourceId,
|
|
30
|
+
owners,
|
|
31
|
+
limit = 50,
|
|
32
|
+
offset = 0,
|
|
33
|
+
} = options ?? {}
|
|
34
|
+
|
|
35
|
+
// An owners constraint is an authorization boundary, so an empty list must
|
|
36
|
+
// match nothing rather than degrade to "no filter".
|
|
37
|
+
if (owners && owners.length === 0) return []
|
|
21
38
|
|
|
22
39
|
let query = this.db
|
|
23
40
|
.selectFrom('aiThreads as t')
|
|
@@ -46,6 +63,21 @@ export class KyselyAgentRunService implements AgentRunService {
|
|
|
46
63
|
query = query.where('t.resourceId', '=', resourceId)
|
|
47
64
|
}
|
|
48
65
|
|
|
66
|
+
if (owners) {
|
|
67
|
+
query = query.where((eb) =>
|
|
68
|
+
eb.or(
|
|
69
|
+
owners.flatMap((owner) => [
|
|
70
|
+
eb('t.resourceId', '=', owner),
|
|
71
|
+
eb(
|
|
72
|
+
't.resourceId',
|
|
73
|
+
'like',
|
|
74
|
+
sql<string>`${`${escapeLikePattern(owner)}:%`} escape '\\'`
|
|
75
|
+
),
|
|
76
|
+
])
|
|
77
|
+
)
|
|
78
|
+
)
|
|
79
|
+
}
|
|
80
|
+
|
|
49
81
|
const result = await query
|
|
50
82
|
.orderBy('t.updatedAt', 'desc')
|
|
51
83
|
.limit(limit)
|