@defrex/autobuild-postgres-store 0.12.0 → 0.14.0

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/README.md CHANGED
@@ -119,9 +119,9 @@ Only an object-store 404 is treated as absent; authorization and service errors
119
119
  are propagated. Vercel supports either a Blob read-write token or a Vercel OIDC
120
120
  token paired with its Blob store ID.
121
121
 
122
- `PostgresTicketDatabase.source({ teamKey, claimedState?, createState? })` creates
123
- a request-bound TicketSource view. The deployment supplies four distinct
124
- lifecycle names; the defaults are Triage, Ready, Doing, and Done. Ticket bodies
122
+ `PostgresTicketDatabase.source({ repo, teamKey, lifecycle, claimedState?, createState? })`
123
+ creates a request-bound TicketSource view. The deployment supplies the project
124
+ identity, readable-id prefix, and lifecycle names. Ticket bodies
125
125
  are stored as PostgreSQL `text` and returned unchanged.
126
126
 
127
127
  For an authenticated HTTP deployment of this adapter, see the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@defrex/autobuild-postgres-store",
3
- "version": "0.12.0",
3
+ "version": "0.14.0",
4
4
  "type": "module",
5
5
  "license": "Apache-2.0",
6
6
  "engines": {
@@ -34,6 +34,14 @@
34
34
  "./connections": {
35
35
  "types": "./src/connections.ts",
36
36
  "import": "./src/connections.ts"
37
+ },
38
+ "./ticket-policy": {
39
+ "types": "./src/ticket-policy.ts",
40
+ "import": "./src/ticket-policy.ts"
41
+ },
42
+ "./ticket-grants": {
43
+ "types": "./src/ticket-grants.ts",
44
+ "import": "./src/ticket-grants.ts"
37
45
  }
38
46
  },
39
47
  "bin": {
package/src/schema.ts CHANGED
@@ -1,7 +1,11 @@
1
1
  import { PostgresProjectRegistry } from './projects'
2
2
  import type { PostgresEnv } from './env'
3
3
  import type { BlobStore } from '@defrex/autobuild/plugin-sdk'
4
- import { migrateLegacyTickets } from './ticket-migration'
4
+ import {
5
+ migrateLegacyTickets,
6
+ migrateReadableTickets,
7
+ READABLE_TICKET_CONSTRAINTS,
8
+ } from './ticket-migration'
5
9
  import { SQL } from 'bun'
6
10
  import {
7
11
  AUTH_SCHEMA_CHECKSUM,
@@ -12,7 +16,7 @@ import {
12
16
  assertAuthSchema,
13
17
  } from './auth-schema'
14
18
 
15
- export const SCHEMA_VERSION = 12
19
+ export const SCHEMA_VERSION = 14
16
20
  export const MIGRATE_COMMAND = 'bun run postgres:migrate (from a pinned Autobuild release checkout)'
17
21
 
18
22
  /** The frozen v3 DDL, kept verbatim so a deployed v3 marker's checksum can be
@@ -817,12 +821,31 @@ CREATE TABLE IF NOT EXISTS project_connections (
817
821
  CREATE TABLE IF NOT EXISTS connection_lookup_limits (
818
822
  actor text PRIMARY KEY, started_at timestamptz NOT NULL, attempts integer NOT NULL
819
823
  );`.trim()
820
- export const SCHEMA_DDL = `${SCHEMA_V11_DDL}\n${CONNECTION_SCHEMA_DDL}`
824
+ export const SCHEMA_V12_DDL = `${SCHEMA_V11_DDL}\n${CONNECTION_SCHEMA_DDL}`
825
+ export const SCHEMA_V12_CHECKSUM = new Bun.CryptoHasher('sha256')
826
+ .update(SCHEMA_V12_DDL)
827
+ .digest('hex')
828
+ export const TICKET_POLICY_SCHEMA_DDL = `
829
+ CREATE TABLE IF NOT EXISTS project_ticket_policy (
830
+ repo text PRIMARY KEY, registration bigint NOT NULL, policy jsonb NOT NULL,
831
+ writer text NOT NULL, updated_at timestamptz NOT NULL
832
+ );`
833
+ export const TICKET_GRANT_SCHEMA_DDL = `
834
+ CREATE TABLE IF NOT EXISTS ticket_grants (
835
+ id text PRIMARY KEY, digest text NOT NULL UNIQUE, repo text NOT NULL, registration bigint NOT NULL,
836
+ execution text NOT NULL, kind text NOT NULL CHECK (kind IN ('runner','phase')),
837
+ parent text REFERENCES ticket_grants(id), expires_at timestamptz NOT NULL, revoked_at timestamptz
838
+ );`
839
+ export const SCHEMA_V13_DDL = `${SCHEMA_V12_DDL}\n${TICKET_POLICY_SCHEMA_DDL}\n${TICKET_GRANT_SCHEMA_DDL}`
840
+ export const SCHEMA_V13_CHECKSUM = new Bun.CryptoHasher('sha256')
841
+ .update(SCHEMA_V13_DDL)
842
+ .digest('hex')
843
+ export const SCHEMA_DDL = `${SCHEMA_V13_DDL}\nALTER TABLE repo_streams ADD COLUMN IF NOT EXISTS tick jsonb;`
821
844
  export const SCHEMA_CHECKSUM = new Bun.CryptoHasher('sha256').update(SCHEMA_DDL).digest('hex')
822
845
 
823
846
  /** Ticket persistence evolves independently so deployed v1 BuildStore markers
824
847
  * remain valid while the hosted ticket tables are added. */
825
- export const TICKET_SCHEMA_VERSION = 2
848
+ export const TICKET_SCHEMA_VERSION = 5
826
849
  export const TICKET_SCHEMA_V1_DDL = `
827
850
  CREATE TABLE IF NOT EXISTS ab_ticket_schema_migrations (
828
851
  singleton boolean PRIMARY KEY DEFAULT true CHECK (singleton),
@@ -851,7 +874,35 @@ CREATE TABLE IF NOT EXISTS ab_ticket_blockers (
851
874
  export const TICKET_SCHEMA_V1_CHECKSUM = new Bun.CryptoHasher('sha256')
852
875
  .update(TICKET_SCHEMA_V1_DDL)
853
876
  .digest('hex')
854
- export const TICKET_SCHEMA_DDL = TICKET_SCHEMA_V1_DDL.replaceAll('team', 'repo')
877
+ export const TICKET_SCHEMA_V2_DDL = TICKET_SCHEMA_V1_DDL.replaceAll('team', 'repo')
878
+ export const TICKET_SCHEMA_V2_CHECKSUM = new Bun.CryptoHasher('sha256')
879
+ .update(TICKET_SCHEMA_V2_DDL)
880
+ .digest('hex')
881
+ export const TICKET_SCHEMA_V3_DDL = TICKET_SCHEMA_V2_DDL.replace(
882
+ 'updated_at timestamptz NOT NULL,',
883
+ 'updated_at timestamptz NOT NULL, revision bigint NOT NULL DEFAULT 1,',
884
+ )
885
+ export const TICKET_SCHEMA_V3_CHECKSUM = new Bun.CryptoHasher('sha256')
886
+ .update(TICKET_SCHEMA_V3_DDL)
887
+ .digest('hex')
888
+ export const TICKET_STATES_DDL = `CREATE TABLE IF NOT EXISTS ab_ticket_states (
889
+ repo text NOT NULL, name text NOT NULL, about text,
890
+ PRIMARY KEY (repo, name),
891
+ CHECK (length(btrim(name)) > 0),
892
+ CHECK (about IS NULL OR (length(btrim(about)) > 0 AND position(chr(10) in about) = 0 AND position(chr(13) in about) = 0))
893
+ );`
894
+ export const TICKET_SCHEMA_V4_DDL = `${TICKET_SCHEMA_V3_DDL}\n${TICKET_STATES_DDL}`
895
+ export const TICKET_SCHEMA_V4_CHECKSUM = new Bun.CryptoHasher('sha256')
896
+ .update(TICKET_SCHEMA_V4_DDL)
897
+ .digest('hex')
898
+ export const TICKET_SCHEMA_DDL =
899
+ TICKET_SCHEMA_V4_DDL +
900
+ `
901
+ ALTER TABLE ab_tickets ADD COLUMN IF NOT EXISTS readable_id text;
902
+ ALTER TABLE ab_tickets ADD COLUMN IF NOT EXISTS ticket_number bigint;
903
+ CREATE TABLE IF NOT EXISTS ab_ticket_counters (repo text PRIMARY KEY, last_number bigint NOT NULL CHECK (last_number >= 0));
904
+ ` +
905
+ READABLE_TICKET_CONSTRAINTS.trim()
855
906
  export const TICKET_SCHEMA_CHECKSUM = new Bun.CryptoHasher('sha256')
856
907
  .update(TICKET_SCHEMA_DDL)
857
908
  .digest('hex')
@@ -893,6 +944,24 @@ const EXPECTED_COLUMNS: Record<string, readonly ExpectedColumn[]> = {
893
944
  ['started_at', 'timestamp with time zone', true],
894
945
  ['attempts', 'integer', true],
895
946
  ],
947
+ project_ticket_policy: [
948
+ ['repo', 'text', true],
949
+ ['registration', 'bigint', true],
950
+ ['policy', 'jsonb', true],
951
+ ['writer', 'text', true],
952
+ ['updated_at', 'timestamp with time zone', true],
953
+ ],
954
+ ticket_grants: [
955
+ ['id', 'text', true],
956
+ ['digest', 'text', true],
957
+ ['repo', 'text', true],
958
+ ['registration', 'bigint', true],
959
+ ['execution', 'text', true],
960
+ ['kind', 'text', true],
961
+ ['parent', 'text', false],
962
+ ['expires_at', 'timestamp with time zone', true],
963
+ ['revoked_at', 'timestamp with time zone', false],
964
+ ],
896
965
  project_credentials: [
897
966
  ['id', 'text', true],
898
967
  ['digest', 'text', true],
@@ -973,6 +1042,7 @@ const EXPECTED_COLUMNS: Record<string, readonly ExpectedColumn[]> = {
973
1042
  ['lease_expires_at', 'timestamp with time zone', false],
974
1043
  ['lease_ttl_ms', 'bigint', false],
975
1044
  ['heartbeat_at', 'timestamp with time zone', false],
1045
+ ['tick', 'jsonb', false],
976
1046
  ],
977
1047
  repo_events: [
978
1048
  ['repo', 'text', true],
@@ -1048,6 +1118,10 @@ const EXPECTED_COLUMNS: Record<string, readonly ExpectedColumn[]> = {
1048
1118
  }
1049
1119
 
1050
1120
  const EXPECTED_CONSTRAINTS = [
1121
+ 'project_ticket_policy|p|repo||||',
1122
+ 'ticket_grants|p|id||||',
1123
+ 'ticket_grants|c|||||',
1124
+ 'ticket_grants|f|parent|ticket_grants|id|a|',
1051
1125
  'project_connections|p|id||||',
1052
1126
  'project_connections|c|||||',
1053
1127
  'connection_lookup_limits|p|actor||||',
@@ -1279,6 +1353,18 @@ export async function assertTicketSchema(sql: SQL): Promise<void> {
1279
1353
  ['labels', 'text[]', true],
1280
1354
  ['created_at', 'timestamp with time zone', true],
1281
1355
  ['updated_at', 'timestamp with time zone', true],
1356
+ ['revision', 'bigint', true],
1357
+ ['readable_id', 'text', true],
1358
+ ['ticket_number', 'bigint', true],
1359
+ ],
1360
+ ab_ticket_counters: [
1361
+ ['repo', 'text', true],
1362
+ ['last_number', 'bigint', true],
1363
+ ],
1364
+ ab_ticket_states: [
1365
+ ['repo', 'text', true],
1366
+ ['name', 'text', true],
1367
+ ['about', 'text', false],
1282
1368
  ],
1283
1369
  ab_ticket_comments: [
1284
1370
  ['repo', 'text', true],
@@ -1303,7 +1389,7 @@ export async function assertTicketSchema(sql: SQL): Promise<void> {
1303
1389
  WHERE n.nspname = current_schema() AND c.relkind IN ('r', 'p')
1304
1390
  AND a.attnum > 0 AND NOT a.attisdropped
1305
1391
  AND c.relname = ANY(ARRAY['ab_ticket_schema_migrations', 'ab_tickets',
1306
- 'ab_ticket_comments', 'ab_ticket_blockers'])
1392
+ 'ab_ticket_comments', 'ab_ticket_blockers', 'ab_ticket_counters', 'ab_ticket_states'])
1307
1393
  ORDER BY c.relname, a.attnum`
1308
1394
  for (const [table, shape] of Object.entries(expected)) {
1309
1395
  const actual = columns
@@ -1313,13 +1399,32 @@ export async function assertTicketSchema(sql: SQL): Promise<void> {
1313
1399
  throw schemaError(`ticket table ${table} is missing or mismatched`)
1314
1400
  }
1315
1401
  }
1402
+ const indexes =
1403
+ await sql`SELECT indexdef FROM pg_indexes WHERE schemaname = current_schema() AND tablename = 'ab_tickets' AND indexname = 'ab_tickets_readable_id_key'`
1404
+ if (
1405
+ !indexes[0] ||
1406
+ !String(indexes[0].indexdef).includes('UNIQUE INDEX') ||
1407
+ !String(indexes[0].indexdef).endsWith('(repo, lower(readable_id))')
1408
+ )
1409
+ throw schemaError('ticket readable id unique index is missing or mismatched')
1316
1410
  const constraints: { table_name: string; definition: string }[] = await sql`
1317
1411
  SELECT c.relname AS table_name, pg_get_constraintdef(k.oid) AS definition
1318
1412
  FROM pg_constraint k JOIN pg_class c ON c.oid = k.conrelid
1319
1413
  JOIN pg_namespace n ON n.oid = c.relnamespace
1320
- WHERE n.nspname = current_schema() AND k.contype IN ('p', 'u', 'f', 'c') AND c.relname IN ('ab_tickets', 'ab_ticket_comments', 'ab_ticket_blockers')`
1414
+ WHERE n.nspname = current_schema() AND k.contype IN ('p', 'u', 'f', 'c') AND c.relname IN ('ab_tickets', 'ab_ticket_comments', 'ab_ticket_blockers', 'ab_ticket_counters', 'ab_ticket_states')`
1321
1415
  const expectedConstraints: Record<string, string[]> = {
1322
- ab_tickets: ['PRIMARY KEY (repo, id)', 'UNIQUE (repo, creation_key)'],
1416
+ ab_ticket_states: [
1417
+ 'PRIMARY KEY (repo, name)',
1418
+ 'CHECK ((length(btrim(name)) > 0))',
1419
+ 'CHECK (((about IS NULL) OR ((length(btrim(about)) > 0) AND (POSITION((chr(10)) IN (about)) = 0) AND (POSITION((chr(13)) IN (about)) = 0))))',
1420
+ ],
1421
+ ab_tickets: [
1422
+ 'PRIMARY KEY (repo, id)',
1423
+ 'UNIQUE (repo, creation_key)',
1424
+ 'UNIQUE (repo, ticket_number)',
1425
+ 'CHECK ((ticket_number > 0))',
1426
+ ],
1427
+ ab_ticket_counters: ['PRIMARY KEY (repo)', 'CHECK ((last_number >= 0))'],
1323
1428
  ab_ticket_comments: [
1324
1429
  'PRIMARY KEY (repo, ticket_id, seq)',
1325
1430
  'FOREIGN KEY (repo, ticket_id) REFERENCES ab_tickets(repo, id) ON DELETE CASCADE',
@@ -1381,7 +1486,8 @@ export async function migratePostgres(
1381
1486
  await sql.begin(async (tx) => {
1382
1487
  await tx`SELECT pg_advisory_xact_lock(470281941)`
1383
1488
  await tx.unsafe(SCHEMA_DDL)
1384
- await tx.unsafe(TICKET_SCHEMA_DDL)
1489
+ await tx.unsafe(TICKET_SCHEMA_V2_DDL)
1490
+ await tx.unsafe(TICKET_STATES_DDL)
1385
1491
  await tx.unsafe(SNAPSHOT_SCHEMA_DDL)
1386
1492
  await tx.unsafe(AUTH_SCHEMA_DDL)
1387
1493
  const rows: { version: number; checksum: string }[] =
@@ -1559,6 +1665,11 @@ export async function migratePostgres(
1559
1665
  SET version = ${SCHEMA_VERSION}, checksum = ${SCHEMA_CHECKSUM},
1560
1666
  applied_at = ${new Date().toISOString()}
1561
1667
  WHERE singleton = true`
1668
+ } else if (version === 13 && marker.checksum === SCHEMA_V13_CHECKSUM) {
1669
+ await tx`UPDATE ab_schema_migrations SET version = ${SCHEMA_VERSION}, checksum = ${SCHEMA_CHECKSUM}, applied_at = ${new Date().toISOString()} WHERE singleton = true`
1670
+ } else if (version === 12 && marker.checksum === SCHEMA_V12_CHECKSUM) {
1671
+ await tx`UPDATE ab_schema_migrations SET version = ${SCHEMA_VERSION}, checksum = ${SCHEMA_CHECKSUM},
1672
+ applied_at = ${new Date().toISOString()} WHERE singleton = true`
1562
1673
  } else if (version === 11 && marker.checksum === SCHEMA_V11_CHECKSUM) {
1563
1674
  await tx`UPDATE ab_schema_migrations SET version = ${SCHEMA_VERSION}, checksum = ${SCHEMA_CHECKSUM},
1564
1675
  applied_at = ${new Date().toISOString()} WHERE singleton = true`
@@ -1600,8 +1711,32 @@ export async function migratePostgres(
1600
1711
  ticketMarker.checksum === TICKET_SCHEMA_V1_CHECKSUM
1601
1712
  ) {
1602
1713
  await migrateLegacyTickets(tx as SQL, options.openBlobs)
1714
+ await tx`ALTER TABLE ab_tickets ADD COLUMN IF NOT EXISTS revision bigint NOT NULL DEFAULT 1`
1715
+ await migrateReadableTickets(tx as SQL, options.openBlobs)
1603
1716
  await tx`UPDATE ab_ticket_schema_migrations SET version = ${TICKET_SCHEMA_VERSION},
1604
1717
  checksum = ${TICKET_SCHEMA_CHECKSUM}, applied_at = ${new Date().toISOString()} WHERE singleton = true`
1718
+ } else if (
1719
+ ticketMarker &&
1720
+ Number(ticketMarker.version) === 2 &&
1721
+ ticketMarker.checksum === TICKET_SCHEMA_V2_CHECKSUM
1722
+ ) {
1723
+ await tx`ALTER TABLE ab_tickets ADD COLUMN revision bigint NOT NULL DEFAULT 1`
1724
+ await migrateReadableTickets(tx as SQL, options.openBlobs)
1725
+ await tx`UPDATE ab_ticket_schema_migrations SET version = ${TICKET_SCHEMA_VERSION}, checksum = ${TICKET_SCHEMA_CHECKSUM}, applied_at = ${new Date().toISOString()} WHERE singleton = true`
1726
+ } else if (
1727
+ ticketMarker &&
1728
+ Number(ticketMarker.version) === 3 &&
1729
+ ticketMarker.checksum === TICKET_SCHEMA_V3_CHECKSUM
1730
+ ) {
1731
+ await migrateReadableTickets(tx as SQL, options.openBlobs)
1732
+ await tx`UPDATE ab_ticket_schema_migrations SET version = ${TICKET_SCHEMA_VERSION}, checksum = ${TICKET_SCHEMA_CHECKSUM}, applied_at = ${new Date().toISOString()} WHERE singleton = true`
1733
+ } else if (
1734
+ ticketMarker &&
1735
+ Number(ticketMarker.version) === 4 &&
1736
+ ticketMarker.checksum === TICKET_SCHEMA_V4_CHECKSUM
1737
+ ) {
1738
+ await migrateReadableTickets(tx as SQL, options.openBlobs)
1739
+ await tx`UPDATE ab_ticket_schema_migrations SET version = ${TICKET_SCHEMA_VERSION}, checksum = ${TICKET_SCHEMA_CHECKSUM}, applied_at = ${new Date().toISOString()} WHERE singleton = true`
1605
1740
  } else if (ticketMarker) {
1606
1741
  if (Number(ticketMarker.version) !== TICKET_SCHEMA_VERSION) {
1607
1742
  throw schemaError(
@@ -1615,10 +1750,19 @@ export async function migratePostgres(
1615
1750
  )
1616
1751
  }
1617
1752
  } else {
1753
+ await tx`ALTER TABLE ab_tickets ADD COLUMN IF NOT EXISTS revision bigint NOT NULL DEFAULT 1`
1754
+ await migrateReadableTickets(tx as SQL, options.openBlobs)
1618
1755
  await tx`INSERT INTO ab_ticket_schema_migrations
1619
1756
  (singleton, version, checksum, applied_at)
1620
1757
  VALUES (true, ${TICKET_SCHEMA_VERSION}, ${TICKET_SCHEMA_CHECKSUM}, ${new Date().toISOString()})`
1621
1758
  }
1759
+ await tx`INSERT INTO ab_ticket_states (repo, name)
1760
+ SELECT DISTINCT repo, state FROM ab_tickets ON CONFLICT DO NOTHING`
1761
+ await tx`INSERT INTO ab_ticket_states (repo, name)
1762
+ SELECT p.repo, value FROM project_ticket_policy p,
1763
+ LATERAL jsonb_each_text(p.policy->'tickets') AS field(key, value)
1764
+ WHERE key IN ('triageState', 'readyState', 'claimedState', 'doneState', 'createState', 'proposalState')
1765
+ ON CONFLICT DO NOTHING`
1622
1766
  const authRows: { version: number; checksum: string }[] =
1623
1767
  await tx`SELECT version, checksum FROM ab_auth_schema_migrations WHERE singleton = true FOR UPDATE`
1624
1768
  const authMarker = authRows[0]