@pikku/kysely 0.13.6 → 0.13.10

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.
Files changed (51) hide show
  1. package/CHANGELOG.md +505 -0
  2. package/dist/src/index.d.ts +4 -1
  3. package/dist/src/index.js +3 -1
  4. package/dist/src/kysely-audit-service.d.ts +34 -2
  5. package/dist/src/kysely-audit-service.js +163 -3
  6. package/dist/src/kysely-credential-service.d.ts +3 -0
  7. package/dist/src/kysely-credential-service.js +50 -11
  8. package/dist/src/kysely-scope-service.d.ts +38 -0
  9. package/dist/src/kysely-scope-service.js +125 -2
  10. package/dist/src/kysely-secret-service.d.ts +7 -3
  11. package/dist/src/kysely-secret-service.js +55 -13
  12. package/dist/src/kysely-tables.d.ts +49 -2
  13. package/dist/src/kysely-virtual-user-run-store.d.ts +43 -0
  14. package/dist/src/kysely-virtual-user-run-store.js +131 -0
  15. package/dist/src/kysely-workflow-service.d.ts +0 -1
  16. package/dist/src/kysely-workflow-service.js +0 -12
  17. package/dist/src/schema/audit.schema.d.ts +16 -0
  18. package/dist/src/schema/audit.schema.js +47 -0
  19. package/dist/src/schema/credential.schema.d.ts +3 -2
  20. package/dist/src/schema/credential.schema.js +9 -3
  21. package/dist/src/schema/index.d.ts +17 -4
  22. package/dist/src/schema/index.js +40 -6
  23. package/dist/src/schema/pikku-schema.types.d.ts +41 -5
  24. package/dist/src/schema/pikku-schema.types.js +27 -12
  25. package/dist/src/schema/scope.schema.js +12 -0
  26. package/dist/src/schema/secret.schema.js +5 -0
  27. package/dist/src/schema/virtual-user.schema.d.ts +19 -0
  28. package/dist/src/schema/virtual-user.schema.js +54 -0
  29. package/dist/tsconfig.tsbuildinfo +1 -1
  30. package/package.json +2 -2
  31. package/src/create-audited-kysely.test.ts +10 -10
  32. package/src/index.ts +7 -0
  33. package/src/kysely-audit-service.test.ts +360 -0
  34. package/src/kysely-audit-service.ts +181 -4
  35. package/src/kysely-credential-service.ts +57 -12
  36. package/src/kysely-scope-service.ts +151 -2
  37. package/src/kysely-secret-service.ts +70 -21
  38. package/src/kysely-services.test.ts +123 -1
  39. package/src/kysely-tables.test.ts +12 -1
  40. package/src/kysely-tables.ts +53 -2
  41. package/src/kysely-virtual-user-run-store.test.ts +212 -0
  42. package/src/kysely-virtual-user-run-store.ts +149 -0
  43. package/src/kysely-workflow-service.ts +0 -17
  44. package/src/schema/audit.schema.ts +55 -0
  45. package/src/schema/credential.schema.ts +14 -3
  46. package/src/schema/index.ts +59 -10
  47. package/src/schema/pikku-schema.types.ts +65 -10
  48. package/src/schema/schema.test.ts +82 -4
  49. package/src/schema/scope.schema.ts +14 -0
  50. package/src/schema/secret.schema.ts +9 -0
  51. package/src/schema/virtual-user.schema.ts +65 -0
@@ -11,7 +11,40 @@ export interface SchemaStatement {
11
11
  execute(): Promise<unknown>;
12
12
  compile(): CompiledQuery;
13
13
  }
14
- export type SchemaStatementFactory = (db: Kysely<any>, types: RequiredTypes) => SchemaStatement;
14
+ export type SchemaStatementFactory = (db: Kysely<any>, types: RequiredTypes, ctx: SchemaContext) => SchemaStatement;
15
+ /**
16
+ * What a statement needs to know about the schema its DDL lands in.
17
+ *
18
+ * Only raw SQL needs this. The schema builder learns the schema from the
19
+ * connection — `withSchema('app')` qualifies every table it names, foreign key
20
+ * targets included — but a raw fragment is passed through verbatim, so a
21
+ * statement that spells its own SQL has to spell the qualifier too.
22
+ */
23
+ export interface SchemaContext {
24
+ /**
25
+ * A table as the surrounding DDL addresses it: `"app"."credentials"` on a
26
+ * schema-bound connection, `"credentials"` otherwise.
27
+ *
28
+ * Takes the physical name, not the camelCase one the schema builder accepts.
29
+ * Raw SQL does not go through `CamelCasePlugin`, so it is already written in
30
+ * physical names throughout and this is no exception.
31
+ */
32
+ table: (name: string) => RawBuilder<unknown>;
33
+ }
34
+ /**
35
+ * The `SchemaContext` for DDL that names no schema.
36
+ *
37
+ * The default everywhere a schema has not been asked for, which is every engine
38
+ * but postgres and most postgres projects too.
39
+ */
40
+ export declare const unqualifiedContext: SchemaContext;
41
+ /**
42
+ * A `SchemaContext` qualifying every table into `schema`.
43
+ *
44
+ * Identifiers are quoted rather than interpolated bare so a schema named after
45
+ * a reserved word still compiles.
46
+ */
47
+ export declare const schemaContext: (schema: string | undefined) => SchemaContext;
15
48
  /**
16
49
  * The physical types of the columns named in `requires`, keyed `table.column`.
17
50
  *
@@ -68,8 +101,11 @@ export interface SchemaRequirement {
68
101
  * Wrap a raw SQL fragment as a statement.
69
102
  *
70
103
  * For the DDL kysely's schema builder cannot express — a unique index over an
71
- * expression, say. Note that raw SQL is *not* rewritten by `withSchema`, so a
72
- * fragment naming a table unqualified will resolve against the connection's
73
- * search path rather than the schema the rest of the declaration lands in.
104
+ * expression, say. Raw SQL is *not* rewritten by `withSchema`, so a fragment
105
+ * naming a table unqualified resolves against the connection's search path
106
+ * rather than the schema the rest of the declaration lands in. Take the
107
+ * function form and name tables through `ctx.table` to stay with them; the
108
+ * bare-fragment form is for SQL that names no table at all.
74
109
  */
75
- export declare const rawStatement: (fragment: RawBuilder<unknown>) => SchemaStatementFactory;
110
+ export declare function rawStatement(fragment: RawBuilder<unknown>): SchemaStatementFactory;
111
+ export declare function rawStatement(build: (ctx: SchemaContext) => RawBuilder<unknown>): SchemaStatementFactory;
@@ -1,4 +1,22 @@
1
1
  import { sql } from 'kysely';
2
+ /**
3
+ * The `SchemaContext` for DDL that names no schema.
4
+ *
5
+ * The default everywhere a schema has not been asked for, which is every engine
6
+ * but postgres and most postgres projects too.
7
+ */
8
+ export const unqualifiedContext = {
9
+ table: (name) => sql.raw(`"${name}"`),
10
+ };
11
+ /**
12
+ * A `SchemaContext` qualifying every table into `schema`.
13
+ *
14
+ * Identifiers are quoted rather than interpolated bare so a schema named after
15
+ * a reserved word still compiles.
16
+ */
17
+ export const schemaContext = (schema) => schema
18
+ ? { table: (name) => sql.raw(`"${schema}"."${name}"`) }
19
+ : unqualifiedContext;
2
20
  /**
3
21
  * The declared type for a foreign key onto a column another source owns.
4
22
  *
@@ -7,15 +25,12 @@ import { sql } from 'kysely';
7
25
  * and only the shape of the statement is being inspected.
8
26
  */
9
27
  export const requiredType = (types, table, column) => sql.raw(types[`${table}.${column}`] ?? 'text');
10
- /**
11
- * Wrap a raw SQL fragment as a statement.
12
- *
13
- * For the DDL kysely's schema builder cannot express — a unique index over an
14
- * expression, say. Note that raw SQL is *not* rewritten by `withSchema`, so a
15
- * fragment naming a table unqualified will resolve against the connection's
16
- * search path rather than the schema the rest of the declaration lands in.
17
- */
18
- export const rawStatement = (fragment) => (db) => ({
19
- execute: () => fragment.execute(db),
20
- compile: () => fragment.compile(db),
21
- });
28
+ export function rawStatement(source) {
29
+ return (db, _types, ctx = unqualifiedContext) => {
30
+ const fragment = typeof source === 'function' ? source(ctx) : source;
31
+ return {
32
+ execute: () => fragment.execute(db),
33
+ compile: () => fragment.compile(db),
34
+ };
35
+ };
36
+ }
@@ -29,6 +29,18 @@ export const scopeSchema = {
29
29
  .createTable('pikkuRoles')
30
30
  .addColumn('name', 'text', (col) => col.primaryKey())
31
31
  .addColumn('description', 'text')
32
+ // Declared in code with `defineSystemRole` rather than composed by an
33
+ // admin. What it buys is the refusals: a system role cannot be renamed,
34
+ // re-scoped or deleted from the console, and a console role cannot be
35
+ // created with its name — two rows answering to one name would make
36
+ // "does Susan hold `buyer`?" depend on which one the store returned.
37
+ .addColumn('system', 'boolean', (col) => col.defaultTo(false).notNull())
38
+ // A system role whose declaration has gone: still held by whoever holds
39
+ // it, no longer offered for new grants, awaiting `pikku roles prune`.
40
+ // The same additive contract as `pikkuScopes.declared`, and for the same
41
+ // reason — a mid-deploy revocation is not something a code edit should
42
+ // be able to cause.
43
+ .addColumn('declared', 'boolean', (col) => col.defaultTo(true).notNull())
32
44
  .addColumn('createdAt', 'timestamp', (col) => col.defaultTo(sql `CURRENT_TIMESTAMP`).notNull()),
33
45
  (db) => db.schema
34
46
  .createTable('pikkuRoleScopes')
@@ -19,6 +19,11 @@ export const secretSchema = {
19
19
  .addColumn('keyVersion', 'integer', (col) => col.notNull())
20
20
  .addColumn('createdAt', 'timestamp', (col) => col.defaultTo(sql `CURRENT_TIMESTAMP`).notNull())
21
21
  .addColumn('updatedAt', 'timestamp', (col) => col.defaultTo(sql `CURRENT_TIMESTAMP`).notNull()),
22
+ (db) => db.schema
23
+ .createTable('secretKekSalts')
24
+ .addColumn('keyVersion', 'integer', (col) => col.primaryKey())
25
+ .addColumn('salt', 'varchar(64)', (col) => col.notNull())
26
+ .addColumn('createdAt', 'timestamp', (col) => col.defaultTo(sql `CURRENT_TIMESTAMP`).notNull()),
22
27
  (db) => db.schema
23
28
  .createTable('secretsAudit')
24
29
  .addColumn('id', 'varchar(36)', (col) => col.primaryKey())
@@ -0,0 +1,19 @@
1
+ import type { PikkuSchema } from './pikku-schema.types.js';
2
+ /**
3
+ * The `virtualUserRun` table {@link KyselyVirtualUserRunStore} writes to.
4
+ *
5
+ * Deliberately not in `pikkuSchemas`: the runtime never needs this table, only
6
+ * a project that runs virtual users does, and creating it for everyone would
7
+ * put an empty table in every database. `KyselyVirtualUserRunStore.init()`
8
+ * applies it, so it arrives exactly when the thing that fills it does.
9
+ *
10
+ * `persona` is text rather than a foreign key: personas are declared in code
11
+ * with `definePersonas()`, not rows, and a run has to outlive the deletion of
12
+ * the persona it ran as.
13
+ *
14
+ * The JSON columns are text on every engine, like the audit table's, because
15
+ * the store serialises them itself — a project on SQLite and one on Postgres
16
+ * then hold the same bytes, and comparing this week's findings against last
17
+ * week's does not depend on which database they were found in.
18
+ */
19
+ export declare const virtualUserSchema: PikkuSchema;
@@ -0,0 +1,54 @@
1
+ import { sql } from 'kysely';
2
+ /**
3
+ * The `virtualUserRun` table {@link KyselyVirtualUserRunStore} writes to.
4
+ *
5
+ * Deliberately not in `pikkuSchemas`: the runtime never needs this table, only
6
+ * a project that runs virtual users does, and creating it for everyone would
7
+ * put an empty table in every database. `KyselyVirtualUserRunStore.init()`
8
+ * applies it, so it arrives exactly when the thing that fills it does.
9
+ *
10
+ * `persona` is text rather than a foreign key: personas are declared in code
11
+ * with `definePersonas()`, not rows, and a run has to outlive the deletion of
12
+ * the persona it ran as.
13
+ *
14
+ * The JSON columns are text on every engine, like the audit table's, because
15
+ * the store serialises them itself — a project on SQLite and one on Postgres
16
+ * then hold the same bytes, and comparing this week's findings against last
17
+ * week's does not depend on which database they were found in.
18
+ */
19
+ export const virtualUserSchema = {
20
+ name: 'virtualUser',
21
+ statements: [
22
+ (db) => db.schema
23
+ .createTable('virtualUserRun')
24
+ .addColumn('runId', 'varchar(36)', (col) => col.primaryKey())
25
+ .addColumn('persona', 'varchar(255)', (col) => col.notNull())
26
+ .addColumn('disposition', 'varchar(50)', (col) => col.notNull())
27
+ .addColumn('seed', 'bigint', (col) => col.notNull())
28
+ // running | completed | failed. Not derived from `finishedAt` being
29
+ // null: a crashed run has no finish time either, and "crashed" and
30
+ // "still going" are not the same answer.
31
+ .addColumn('status', 'varchar(50)', (col) => col.defaultTo('running').notNull())
32
+ .addColumn('goals', 'text', (col) => col.defaultTo('[]').notNull())
33
+ .addColumn('memory', 'text', (col) => col.defaultTo('{}').notNull())
34
+ .addColumn('findings', 'text', (col) => col.defaultTo('[]').notNull())
35
+ // Counts the engine kept: steps, calls, mutations, tokens, elapsed.
36
+ .addColumn('tally', 'text')
37
+ .addColumn('stoppedBy', 'varchar(255)')
38
+ // Why the run itself failed, as opposed to what it found.
39
+ .addColumn('error', 'text')
40
+ .addColumn('startedBy', 'varchar(255)')
41
+ .addColumn('createdAt', 'timestamp', (col) => col.defaultTo(sql `CURRENT_TIMESTAMP`).notNull())
42
+ .addColumn('finishedAt', 'timestamp'),
43
+ // The two reads: the run list newest-first, and one persona's history when
44
+ // a finding needs comparing against what that persona found before.
45
+ (db) => db.schema
46
+ .createIndex('idx_virtual_user_run_created')
47
+ .on('virtualUserRun')
48
+ .column('createdAt'),
49
+ (db) => db.schema
50
+ .createIndex('idx_virtual_user_run_persona')
51
+ .on('virtualUserRun')
52
+ .columns(['persona', 'createdAt']),
53
+ ],
54
+ };
@@ -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-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","../src/schema/ai.schema.ts","../src/schema/channel.schema.ts","../src/schema/credential.schema.ts","../src/schema/deployment.schema.ts","../src/schema/index.ts","../src/schema/pikku-schema.types.ts","../src/schema/scope.schema.ts","../src/schema/secret.schema.ts","../src/schema/session.schema.ts","../src/schema/webhook.schema.ts","../src/schema/workflow.schema.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-virtual-user-run-store.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","../src/schema/ai.schema.ts","../src/schema/audit.schema.ts","../src/schema/channel.schema.ts","../src/schema/credential.schema.ts","../src/schema/deployment.schema.ts","../src/schema/index.ts","../src/schema/pikku-schema.types.ts","../src/schema/scope.schema.ts","../src/schema/secret.schema.ts","../src/schema/session.schema.ts","../src/schema/virtual-user.schema.ts","../src/schema/webhook.schema.ts","../src/schema/workflow.schema.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.6",
3
+ "version": "0.13.10",
4
4
  "description": "Kysely-backed Pikku services — workflow state, secrets, sessions, scopes and audit",
5
5
  "author": "yasser.fadl@gmail.com",
6
6
  "license": "MIT",
@@ -21,7 +21,7 @@
21
21
  "prepublishOnly": "yarn build"
22
22
  },
23
23
  "peerDependencies": {
24
- "@pikku/core": "^0.12.71"
24
+ "@pikku/core": "^0.12.77"
25
25
  },
26
26
  "dependencies": {
27
27
  "kysely": "^0.29.0"
@@ -33,8 +33,8 @@ interface AuditEventTable {
33
33
  traceId: string | null
34
34
  transactionId: string | null
35
35
  queryId: string | null
36
- actorUserId: string | null
37
- actorOrgId: string | null
36
+ userId: string | null
37
+ orgId: string | null
38
38
  actorPikkuUserId: string | null
39
39
  metadataJson: string | null
40
40
  }
@@ -76,8 +76,8 @@ async function createAuditEventsTable(
76
76
  .addColumn('traceId', 'text')
77
77
  .addColumn('transactionId', 'text')
78
78
  .addColumn('queryId', 'text')
79
- .addColumn('actorUserId', 'text')
80
- .addColumn('actorOrgId', 'text')
79
+ .addColumn('userId', 'text')
80
+ .addColumn('orgId', 'text')
81
81
  .addColumn('actorPikkuUserId', 'text')
82
82
  .addColumn('metadataJson', 'text')
83
83
  .execute()
@@ -113,9 +113,9 @@ class ImmediateAuditService implements AuditService {
113
113
  traceId: event.traceId ?? null,
114
114
  transactionId: event.transactionId ?? null,
115
115
  queryId: event.queryId ?? null,
116
- actorUserId: event.actor?.userId ?? null,
117
- actorOrgId: event.actor?.orgId ?? null,
118
- actorPikkuUserId: event.actor?.pikkuUserId ?? null,
116
+ userId: event.userIdentity?.userId ?? null,
117
+ orgId: event.userIdentity?.orgId ?? null,
118
+ actorPikkuUserId: event.userIdentity?.pikkuUserId ?? null,
119
119
  metadataJson: event.metadata ? JSON.stringify(event.metadata) : null,
120
120
  })
121
121
  .execute()
@@ -207,7 +207,7 @@ describe('createAuditedKysely', () => {
207
207
  changes: { title: 'write tests', done: 0 },
208
208
  rowCount: 1,
209
209
  })
210
- assert.deepEqual(events[0]?.actor, {
210
+ assert.deepEqual(events[0]?.userIdentity, {
211
211
  userId: 'user-1',
212
212
  orgId: 'org-1',
213
213
  pikkuUserId: 'user-1',
@@ -257,7 +257,7 @@ describe('createAuditedKysely', () => {
257
257
  title: 'after',
258
258
  done: 1,
259
259
  })
260
- assert.deepEqual(events[0]?.actor, {
260
+ assert.deepEqual(events[0]?.userIdentity, {
261
261
  userId: 'user-2',
262
262
  orgId: undefined,
263
263
  pikkuUserId: 'user-2',
@@ -455,7 +455,7 @@ describe('createAuditedKysely', () => {
455
455
  ['todos:best-effort', 'todos:best-effort']
456
456
  )
457
457
  assert.deepEqual(
458
- persisted.map((event) => event.actorUserId),
458
+ persisted.map((event) => event.userId),
459
459
  ['user-best-effort', 'user-best-effort']
460
460
  )
461
461
  assert.equal(persisted[0]?.queryId, 'q-1')
package/src/index.ts CHANGED
@@ -19,6 +19,7 @@ export {
19
19
  type CreateAuditedKyselyOptions,
20
20
  } from './create-audited-kysely.js'
21
21
  export { KyselyAuditService } from './kysely-audit-service.js'
22
+ export { KyselyVirtualUserRunStore } from './kysely-virtual-user-run-store.js'
22
23
 
23
24
  export {
24
25
  SerializePlugin,
@@ -34,7 +35,9 @@ export {
34
35
  type SerializePluginOptions,
35
36
  } from './serialize-plugin.js'
36
37
 
38
+ export { virtualUserSchema } from './schema/virtual-user.schema.js'
37
39
  export {
40
+ auditSchema,
38
41
  pikkuSchemas,
39
42
  applyPikkuSchemas,
40
43
  compilePikkuSchemas,
@@ -55,3 +58,7 @@ export {
55
58
  export type { KyselyPikkuDB } from './kysely-tables.js'
56
59
  export type { WorkflowRunService } from '@pikku/core/workflow'
57
60
  export type { AgentRunService, AgentRunRow } from '@pikku/core/ai-agent'
61
+ export type {
62
+ VirtualUserRunStore,
63
+ VirtualUserRunRecord,
64
+ } from '@pikku/core/virtual-user'