@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.
- package/CHANGELOG.md +505 -0
- package/dist/src/index.d.ts +4 -1
- package/dist/src/index.js +3 -1
- package/dist/src/kysely-audit-service.d.ts +34 -2
- package/dist/src/kysely-audit-service.js +163 -3
- package/dist/src/kysely-credential-service.d.ts +3 -0
- package/dist/src/kysely-credential-service.js +50 -11
- package/dist/src/kysely-scope-service.d.ts +38 -0
- package/dist/src/kysely-scope-service.js +125 -2
- package/dist/src/kysely-secret-service.d.ts +7 -3
- package/dist/src/kysely-secret-service.js +55 -13
- package/dist/src/kysely-tables.d.ts +49 -2
- package/dist/src/kysely-virtual-user-run-store.d.ts +43 -0
- package/dist/src/kysely-virtual-user-run-store.js +131 -0
- package/dist/src/kysely-workflow-service.d.ts +0 -1
- package/dist/src/kysely-workflow-service.js +0 -12
- package/dist/src/schema/audit.schema.d.ts +16 -0
- package/dist/src/schema/audit.schema.js +47 -0
- package/dist/src/schema/credential.schema.d.ts +3 -2
- package/dist/src/schema/credential.schema.js +9 -3
- package/dist/src/schema/index.d.ts +17 -4
- package/dist/src/schema/index.js +40 -6
- package/dist/src/schema/pikku-schema.types.d.ts +41 -5
- package/dist/src/schema/pikku-schema.types.js +27 -12
- package/dist/src/schema/scope.schema.js +12 -0
- package/dist/src/schema/secret.schema.js +5 -0
- package/dist/src/schema/virtual-user.schema.d.ts +19 -0
- package/dist/src/schema/virtual-user.schema.js +54 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/src/create-audited-kysely.test.ts +10 -10
- package/src/index.ts +7 -0
- package/src/kysely-audit-service.test.ts +360 -0
- package/src/kysely-audit-service.ts +181 -4
- package/src/kysely-credential-service.ts +57 -12
- package/src/kysely-scope-service.ts +151 -2
- package/src/kysely-secret-service.ts +70 -21
- package/src/kysely-services.test.ts +123 -1
- package/src/kysely-tables.test.ts +12 -1
- package/src/kysely-tables.ts +53 -2
- package/src/kysely-virtual-user-run-store.test.ts +212 -0
- package/src/kysely-virtual-user-run-store.ts +149 -0
- package/src/kysely-workflow-service.ts +0 -17
- package/src/schema/audit.schema.ts +55 -0
- package/src/schema/credential.schema.ts +14 -3
- package/src/schema/index.ts +59 -10
- package/src/schema/pikku-schema.types.ts +65 -10
- package/src/schema/schema.test.ts +82 -4
- package/src/schema/scope.schema.ts +14 -0
- package/src/schema/secret.schema.ts +9 -0
- package/src/schema/virtual-user.schema.ts +65 -0
|
@@ -398,10 +398,9 @@ describe('ensurePikkuSchema — a connection bound to a schema', () => {
|
|
|
398
398
|
// raise a syntax error of its own on this DDL — a column type resolved from
|
|
399
399
|
// `requires` goes in verbatim. Answering for it in sqlite's name would be
|
|
400
400
|
// the misleading-error bug, reintroduced.
|
|
401
|
-
const pgError = Object.assign(
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
)
|
|
401
|
+
const pgError = Object.assign(new Error('syntax error at or near "not"'), {
|
|
402
|
+
code: '42601',
|
|
403
|
+
})
|
|
405
404
|
const db = new Kysely<any>({
|
|
406
405
|
dialect: {
|
|
407
406
|
...dialect('postgres'),
|
|
@@ -454,3 +453,82 @@ describe('ensurePikkuSchema — a connection bound to a schema', () => {
|
|
|
454
453
|
)
|
|
455
454
|
})
|
|
456
455
|
})
|
|
456
|
+
|
|
457
|
+
describe('compiling into a schema', () => {
|
|
458
|
+
const compilePg = (schemaName?: string) =>
|
|
459
|
+
compilePikkuSchemas(
|
|
460
|
+
new Kysely<any>({ dialect: dialect('postgres') }),
|
|
461
|
+
pikkuSchemas,
|
|
462
|
+
{},
|
|
463
|
+
schemaName
|
|
464
|
+
)
|
|
465
|
+
|
|
466
|
+
test('qualifies every table it creates', () => {
|
|
467
|
+
const sql = compilePg('app')
|
|
468
|
+
const creates = sql.match(/create table[^(]*/gi) ?? []
|
|
469
|
+
|
|
470
|
+
assert.ok(creates.length > 0, 'nothing was compiled')
|
|
471
|
+
for (const create of creates) {
|
|
472
|
+
assert.match(
|
|
473
|
+
create,
|
|
474
|
+
/"app"\./,
|
|
475
|
+
`unqualified create table in a schema-bound compile: ${create.trim()}`
|
|
476
|
+
)
|
|
477
|
+
}
|
|
478
|
+
})
|
|
479
|
+
|
|
480
|
+
test('qualifies foreign key targets, so they do not resolve by search path', () => {
|
|
481
|
+
const sql = compilePg('app')
|
|
482
|
+
|
|
483
|
+
assert.match(sql, /references "app"\./)
|
|
484
|
+
assert.doesNotMatch(
|
|
485
|
+
sql,
|
|
486
|
+
/references "(?!app")[^"]+" \(/,
|
|
487
|
+
'a foreign key onto an unqualified table would find whatever search_path points at'
|
|
488
|
+
)
|
|
489
|
+
})
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* The regression this option exists for.
|
|
493
|
+
*
|
|
494
|
+
* Raw SQL is passed through verbatim, so the expression index on `credentials`
|
|
495
|
+
* is the one statement `withSchema` cannot reach. Left unqualified it creates
|
|
496
|
+
* the uniqueness rule on a different table than the `create table` above it.
|
|
497
|
+
*/
|
|
498
|
+
test('qualifies the raw expression index too', () => {
|
|
499
|
+
assert.match(
|
|
500
|
+
compilePg('app'),
|
|
501
|
+
/CREATE UNIQUE INDEX credentials_name_user_id_unique ON "app"\."credentials"/
|
|
502
|
+
)
|
|
503
|
+
})
|
|
504
|
+
|
|
505
|
+
test('leaves the index name itself unqualified', () => {
|
|
506
|
+
// An index belongs to the schema of the table it is on; postgres rejects a
|
|
507
|
+
// CREATE INDEX that says otherwise.
|
|
508
|
+
assert.doesNotMatch(compilePg('app'), /INDEX "?app"?\."?credentials_name/i)
|
|
509
|
+
})
|
|
510
|
+
|
|
511
|
+
test('compiles unqualified when no schema is asked for', () => {
|
|
512
|
+
const sql = compilePg()
|
|
513
|
+
|
|
514
|
+
assert.doesNotMatch(sql, /"app"\./)
|
|
515
|
+
assert.match(sql, /create table "credentials"/i)
|
|
516
|
+
assert.match(
|
|
517
|
+
sql,
|
|
518
|
+
/CREATE UNIQUE INDEX credentials_name_user_id_unique ON "credentials"/
|
|
519
|
+
)
|
|
520
|
+
})
|
|
521
|
+
|
|
522
|
+
test('does not qualify the connection it was handed', () => {
|
|
523
|
+
// The caller's connection is a scratch database the declaration was just
|
|
524
|
+
// applied to. Binding it would create tables in a schema that database has
|
|
525
|
+
// never heard of — so the schema must reach the SQL and nothing else.
|
|
526
|
+
const db = new Kysely<any>({ dialect: dialect('postgres') })
|
|
527
|
+
compilePikkuSchemas(db, pikkuSchemas, {}, 'app')
|
|
528
|
+
|
|
529
|
+
assert.match(
|
|
530
|
+
db.schema.createTable('after').addColumn('id', 'text').compile().sql,
|
|
531
|
+
/create table "after"/
|
|
532
|
+
)
|
|
533
|
+
})
|
|
534
|
+
})
|
|
@@ -35,6 +35,20 @@ export const scopeSchema: PikkuSchema = {
|
|
|
35
35
|
.createTable('pikkuRoles')
|
|
36
36
|
.addColumn('name', 'text', (col) => col.primaryKey())
|
|
37
37
|
.addColumn('description', 'text')
|
|
38
|
+
// Declared in code with `defineSystemRole` rather than composed by an
|
|
39
|
+
// admin. What it buys is the refusals: a system role cannot be renamed,
|
|
40
|
+
// re-scoped or deleted from the console, and a console role cannot be
|
|
41
|
+
// created with its name — two rows answering to one name would make
|
|
42
|
+
// "does Susan hold `buyer`?" depend on which one the store returned.
|
|
43
|
+
.addColumn('system', 'boolean', (col) => col.defaultTo(false).notNull())
|
|
44
|
+
// A system role whose declaration has gone: still held by whoever holds
|
|
45
|
+
// it, no longer offered for new grants, awaiting `pikku roles prune`.
|
|
46
|
+
// The same additive contract as `pikkuScopes.declared`, and for the same
|
|
47
|
+
// reason — a mid-deploy revocation is not something a code edit should
|
|
48
|
+
// be able to cause.
|
|
49
|
+
.addColumn('declared', 'boolean', (col) =>
|
|
50
|
+
col.defaultTo(true).notNull()
|
|
51
|
+
)
|
|
38
52
|
.addColumn('createdAt', 'timestamp', (col) =>
|
|
39
53
|
col.defaultTo(sql`CURRENT_TIMESTAMP`).notNull()
|
|
40
54
|
),
|
|
@@ -27,6 +27,15 @@ export const secretSchema: PikkuSchema = {
|
|
|
27
27
|
col.defaultTo(sql`CURRENT_TIMESTAMP`).notNull()
|
|
28
28
|
),
|
|
29
29
|
|
|
30
|
+
(db) =>
|
|
31
|
+
db.schema
|
|
32
|
+
.createTable('secretKekSalts')
|
|
33
|
+
.addColumn('keyVersion', 'integer', (col) => col.primaryKey())
|
|
34
|
+
.addColumn('salt', 'varchar(64)', (col) => col.notNull())
|
|
35
|
+
.addColumn('createdAt', 'timestamp', (col) =>
|
|
36
|
+
col.defaultTo(sql`CURRENT_TIMESTAMP`).notNull()
|
|
37
|
+
),
|
|
38
|
+
|
|
30
39
|
(db) =>
|
|
31
40
|
db.schema
|
|
32
41
|
.createTable('secretsAudit')
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { sql } from 'kysely'
|
|
2
|
+
import type { PikkuSchema } from './pikku-schema.types.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The `virtualUserRun` table {@link KyselyVirtualUserRunStore} writes to.
|
|
6
|
+
*
|
|
7
|
+
* Deliberately not in `pikkuSchemas`: the runtime never needs this table, only
|
|
8
|
+
* a project that runs virtual users does, and creating it for everyone would
|
|
9
|
+
* put an empty table in every database. `KyselyVirtualUserRunStore.init()`
|
|
10
|
+
* applies it, so it arrives exactly when the thing that fills it does.
|
|
11
|
+
*
|
|
12
|
+
* `persona` is text rather than a foreign key: personas are declared in code
|
|
13
|
+
* with `definePersonas()`, not rows, and a run has to outlive the deletion of
|
|
14
|
+
* the persona it ran as.
|
|
15
|
+
*
|
|
16
|
+
* The JSON columns are text on every engine, like the audit table's, because
|
|
17
|
+
* the store serialises them itself — a project on SQLite and one on Postgres
|
|
18
|
+
* then hold the same bytes, and comparing this week's findings against last
|
|
19
|
+
* week's does not depend on which database they were found in.
|
|
20
|
+
*/
|
|
21
|
+
export const virtualUserSchema: PikkuSchema = {
|
|
22
|
+
name: 'virtualUser',
|
|
23
|
+
statements: [
|
|
24
|
+
(db) =>
|
|
25
|
+
db.schema
|
|
26
|
+
.createTable('virtualUserRun')
|
|
27
|
+
.addColumn('runId', 'varchar(36)', (col) => col.primaryKey())
|
|
28
|
+
.addColumn('persona', 'varchar(255)', (col) => col.notNull())
|
|
29
|
+
.addColumn('disposition', 'varchar(50)', (col) => col.notNull())
|
|
30
|
+
.addColumn('seed', 'bigint', (col) => col.notNull())
|
|
31
|
+
// running | completed | failed. Not derived from `finishedAt` being
|
|
32
|
+
// null: a crashed run has no finish time either, and "crashed" and
|
|
33
|
+
// "still going" are not the same answer.
|
|
34
|
+
.addColumn('status', 'varchar(50)', (col) =>
|
|
35
|
+
col.defaultTo('running').notNull()
|
|
36
|
+
)
|
|
37
|
+
.addColumn('goals', 'text', (col) => col.defaultTo('[]').notNull())
|
|
38
|
+
.addColumn('memory', 'text', (col) => col.defaultTo('{}').notNull())
|
|
39
|
+
.addColumn('findings', 'text', (col) => col.defaultTo('[]').notNull())
|
|
40
|
+
// Counts the engine kept: steps, calls, mutations, tokens, elapsed.
|
|
41
|
+
.addColumn('tally', 'text')
|
|
42
|
+
.addColumn('stoppedBy', 'varchar(255)')
|
|
43
|
+
// Why the run itself failed, as opposed to what it found.
|
|
44
|
+
.addColumn('error', 'text')
|
|
45
|
+
.addColumn('startedBy', 'varchar(255)')
|
|
46
|
+
.addColumn('createdAt', 'timestamp', (col) =>
|
|
47
|
+
col.defaultTo(sql`CURRENT_TIMESTAMP`).notNull()
|
|
48
|
+
)
|
|
49
|
+
.addColumn('finishedAt', 'timestamp'),
|
|
50
|
+
|
|
51
|
+
// The two reads: the run list newest-first, and one persona's history when
|
|
52
|
+
// a finding needs comparing against what that persona found before.
|
|
53
|
+
(db) =>
|
|
54
|
+
db.schema
|
|
55
|
+
.createIndex('idx_virtual_user_run_created')
|
|
56
|
+
.on('virtualUserRun')
|
|
57
|
+
.column('createdAt'),
|
|
58
|
+
|
|
59
|
+
(db) =>
|
|
60
|
+
db.schema
|
|
61
|
+
.createIndex('idx_virtual_user_run_persona')
|
|
62
|
+
.on('virtualUserRun')
|
|
63
|
+
.columns(['persona', 'createdAt']),
|
|
64
|
+
],
|
|
65
|
+
}
|