@venturekit/data 0.0.31 → 0.0.33

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 (47) hide show
  1. package/dist/files/index.d.ts +9 -0
  2. package/dist/files/index.d.ts.map +1 -0
  3. package/dist/files/index.js +8 -0
  4. package/dist/files/index.js.map +1 -0
  5. package/dist/files/postgres.d.ts +150 -0
  6. package/dist/files/postgres.d.ts.map +1 -0
  7. package/dist/files/postgres.js +194 -0
  8. package/dist/files/postgres.js.map +1 -0
  9. package/dist/idempotency/index.d.ts +9 -0
  10. package/dist/idempotency/index.d.ts.map +1 -0
  11. package/dist/idempotency/index.js +8 -0
  12. package/dist/idempotency/index.js.map +1 -0
  13. package/dist/idempotency/postgres.d.ts +107 -0
  14. package/dist/idempotency/postgres.d.ts.map +1 -0
  15. package/dist/idempotency/postgres.js +145 -0
  16. package/dist/idempotency/postgres.js.map +1 -0
  17. package/dist/internal/identifier.d.ts +16 -0
  18. package/dist/internal/identifier.d.ts.map +1 -0
  19. package/dist/internal/identifier.js +23 -0
  20. package/dist/internal/identifier.js.map +1 -0
  21. package/dist/jobs/index.d.ts +9 -0
  22. package/dist/jobs/index.d.ts.map +1 -0
  23. package/dist/jobs/index.js +8 -0
  24. package/dist/jobs/index.js.map +1 -0
  25. package/dist/jobs/postgres.d.ts +197 -0
  26. package/dist/jobs/postgres.d.ts.map +1 -0
  27. package/dist/jobs/postgres.js +270 -0
  28. package/dist/jobs/postgres.js.map +1 -0
  29. package/dist/outbox/index.d.ts +9 -0
  30. package/dist/outbox/index.d.ts.map +1 -0
  31. package/dist/outbox/index.js +8 -0
  32. package/dist/outbox/index.js.map +1 -0
  33. package/dist/outbox/postgres.d.ts +124 -0
  34. package/dist/outbox/postgres.d.ts.map +1 -0
  35. package/dist/outbox/postgres.js +177 -0
  36. package/dist/outbox/postgres.js.map +1 -0
  37. package/dist/query/index.d.ts.map +1 -1
  38. package/dist/query/index.js.map +1 -1
  39. package/dist/query/secret.d.ts.map +1 -1
  40. package/dist/query/secret.js +1 -1
  41. package/dist/query/secret.js.map +1 -1
  42. package/package.json +23 -3
  43. package/src/sql/0000_vk_data_foundation.sql +305 -0
  44. package/src/sql/vk_data_001_idempotency.sql +48 -0
  45. package/src/sql/vk_data_002_outbox.sql +99 -0
  46. package/src/sql/vk_data_003_jobs.sql +114 -0
  47. package/src/sql/vk_data_004_file_object.sql +112 -0
@@ -131,7 +131,7 @@ async function fetchAndApply(arn) {
131
131
  `from Secrets Manager (secret: ${redactedArn}). If the Lambda runs in ` +
132
132
  `a VPC, verify it has a route to Secrets Manager: a working NAT gateway/instance, ` +
133
133
  `or a com.amazonaws.<region>.secretsmanager VPC interface endpoint. ` +
134
- `Override the timeout via DB_SECRET_FETCH_TIMEOUT_MS.`);
134
+ `Override the timeout via DB_SECRET_FETCH_TIMEOUT_MS.`, { cause: err });
135
135
  }
136
136
  throw err;
137
137
  }
@@ -1 +1 @@
1
- {"version":3,"file":"secret.js","sourceRoot":"","sources":["../../src/query/secret.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AAGH,OAAO,EAAE,KAAK,EAAE,MAAM,uBAAuB,CAAC;AAE9C;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,gCAAgC,GAAG,KAAK,CAAC;AAEtD,SAAS,4BAA4B;IACnC,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,0BAA0B,CAAC;IACnD,IAAI,CAAC,GAAG;QAAE,OAAO,gCAAgC,CAAC;IAClD,MAAM,CAAC,GAAG,MAAM,CAAC,QAAQ,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;IACnC,OAAO,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,gCAAgC,CAAC;AAC5E,CAAC;AAED;;;;;GAKG;AACH,SAAS,SAAS,CAAC,GAAW;IAC5B,MAAM,GAAG,GAAG,GAAG,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC;IACjC,OAAO,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC;AAC/C,CAAC;AAED,IAAI,UAAU,GAAyB,IAAI,CAAC;AAkB5C,MAAM,CAAC,KAAK,UAAU,kBAAkB;IACtC,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,aAAa,CAAC;IACtC,IAAI,CAAC,GAAG;QAAE,OAAO;IACjB,qEAAqE;IACrE,6CAA6C;IAC7C,IAAI,OAAO,CAAC,GAAG,CAAC,OAAO,IAAI,OAAO,CAAC,GAAG,CAAC,WAAW;QAAE,OAAO;IAE3D,IAAI,CAAC,UAAU,EAAE,CAAC;QAChB,UAAU,GAAG,aAAa,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;YAC5C,kEAAkE;YAClE,4DAA4D;YAC5D,uBAAuB;YACvB,UAAU,GAAG,IAAI,CAAC;YAClB,MAAM,GAAG,CAAC;QACZ,CAAC,CAAC,CAAC;IACL,CAAC;IAED,MAAM,UAAU,CAAC;AACnB,CAAC;AAED,KAAK,UAAU,aAAa,CAAC,GAAW;IACtC,qEAAqE;IACrE,gEAAgE;IAChE,2BAA2B;IAC3B,MAAM,EAAE,GAAG,MAAM,MAAM,CAAC,iCAAiC,CAAC,CAAC;IAC3D,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC;IACtC,MAAM,MAAM,GAAG,IAAI,EAAE,CAAC,oBAAoB,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;IAErE,MAAM,SAAS,GAAG,4BAA4B,EAAE,CAAC;IACjD,MAAM,UAAU,GAAG,IAAI,eAAe,EAAE,CAAC;IACzC,mEAAmE;IACnE,+DAA+D;IAC/D,sDAAsD;IACtD,MAAM,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,UAAU,CAAC,KAAK,EAAE,EAAE,SAAS,CAAC,CAAC;IAC9D,IAAI,OAAO,KAAK,CAAC,KAAK,KAAK,UAAU;QAAE,KAAK,CAAC,KAAK,EAAE,CAAC;IAErD,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;IAC7B,MAAM,WAAW,GAAG,SAAS,CAAC,GAAG,CAAC,CAAC;IACnC,KAAK,CAAC,IAAI,CAAC,8CAA8C,EAAE;QACzD,MAAM,EAAE,WAAW;QACnB,MAAM,EAAE,MAAM,IAAI,aAAa;QAC/B,SAAS;KACV,CAAC,CAAC;IAEH,IAAI,MAAmC,CAAC;IACxC,IAAI,CAAC;QACH,MAAM,GAAG,MAAM,MAAM,CAAC,IAAI,CACxB,IAAI,EAAE,CAAC,qBAAqB,CAAC,EAAE,QAAQ,EAAE,GAAG,EAAE,CAAC,EAC/C,EAAE,WAAW,EAAE,UAAU,CAAC,MAAM,EAAE,CACnC,CAAC;IACJ,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,UAAU,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,SAAS,CAAC;QAC1C,MAAM,OAAO,GAAG,UAAU,CAAC,MAAM,CAAC,OAAO,CAAC;QAC1C,MAAM,SAAS,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC;QAC9D,MAAM,YAAY,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QACtE,KAAK,CAAC,KAAK,CAAC,qDAAqD,EAAE;YACjE,MAAM,EAAE,WAAW;YACnB,UAAU;YACV,QAAQ,EAAE,OAAO;YACjB,SAAS;YACT,YAAY;SACb,CAAC,CAAC;QACH,IAAI,OAAO,EAAE,CAAC;YACZ,gEAAgE;YAChE,kEAAkE;YAClE,yDAAyD;YACzD,MAAM,IAAI,KAAK,CACb,gCAAgC,SAAS,+BAA+B;gBACxE,iCAAiC,WAAW,2BAA2B;gBACvE,mFAAmF;gBACnF,qEAAqE;gBACrE,sDAAsD,CACvD,CAAC;QACJ,CAAC;QACD,MAAM,GAAG,CAAC;IACZ,CAAC;YAAS,CAAC;QACT,YAAY,CAAC,KAAK,CAAC,CAAC;IACtB,CAAC;IAED,MAAM,eAAe,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,SAAS,CAAC;IAC/C,MAAM,GAAG,GAAG,MAAM,CAAC,YAAY,CAAC;IAChC,IAAI,CAAC,GAAG,EAAE,CAAC;QACT,MAAM,IAAI,KAAK,CACb,4EAA4E,GAAG,EAAE,CAClF,CAAC;IACJ,CAAC;IAED,IAAI,MAAqB,CAAC;IAC1B,IAAI,CAAC;QACH,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAkB,CAAC;IAC5C,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,IAAI,KAAK,CACb,qDAAqD;YACnD,2EAA2E,CAC9E,CAAC;IACJ,CAAC;IAED,iEAAiE;IACjE,sEAAsE;IACtE,mEAAmE;IACnE,yDAAyD;IACzD,MAAM,YAAY,GAAa,EAAE,CAAC;IAClC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,OAAO,IAAI,MAAM,CAAC,QAAQ,EAAE,CAAC;QAC5C,OAAO,CAAC,GAAG,CAAC,OAAO,GAAG,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;QAC9C,YAAY,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;IAC/B,CAAC;IACD,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,WAAW,IAAI,MAAM,CAAC,QAAQ,EAAE,CAAC;QAChD,OAAO,CAAC,GAAG,CAAC,WAAW,GAAG,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;QAClD,YAAY,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC;IACnC,CAAC;IACD,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,OAAO,IAAI,MAAM,CAAC,IAAI,EAAE,CAAC;QACxC,OAAO,CAAC,GAAG,CAAC,OAAO,GAAG,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;QAC1C,YAAY,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;IAC/B,CAAC;IACD,IACE,CAAC,OAAO,CAAC,GAAG,CAAC,OAAO;QACpB,MAAM,CAAC,IAAI,KAAK,SAAS;QACzB,MAAM,CAAC,IAAI,KAAK,IAAI,EACpB,CAAC;QACD,OAAO,CAAC,GAAG,CAAC,OAAO,GAAG,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;QAC1C,YAAY,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;IAC/B,CAAC;IACD,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,OAAO,IAAI,MAAM,CAAC,MAAM,EAAE,CAAC;QAC1C,OAAO,CAAC,GAAG,CAAC,OAAO,GAAG,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;QAC5C,YAAY,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;IAC/B,CAAC;IAED,KAAK,CAAC,IAAI,CAAC,+BAA+B,EAAE;QAC1C,MAAM,EAAE,WAAW;QACnB,UAAU,EAAE,eAAe;QAC3B,YAAY;KACb,CAAC,CAAC;AACL,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,wBAAwB;IACtC,UAAU,GAAG,IAAI,CAAC;AACpB,CAAC"}
1
+ {"version":3,"file":"secret.js","sourceRoot":"","sources":["../../src/query/secret.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,12 +1,13 @@
1
1
  {
2
2
  "name": "@venturekit/data",
3
- "version": "0.0.31",
3
+ "version": "0.0.33",
4
4
  "description": "Database and data layer for VentureKit",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
7
7
  "types": "./dist/index.d.ts",
8
8
  "files": [
9
- "dist"
9
+ "dist",
10
+ "src/sql/*.sql"
10
11
  ],
11
12
  "repository": {
12
13
  "type": "git",
@@ -22,15 +23,34 @@
22
23
  "access": "public"
23
24
  },
24
25
  "license": "Apache-2.0",
26
+ "vk": {
27
+ "migrations": "src/sql"
28
+ },
25
29
  "exports": {
26
30
  ".": {
27
31
  "types": "./dist/index.d.ts",
28
32
  "import": "./dist/index.js"
33
+ },
34
+ "./idempotency": {
35
+ "types": "./dist/idempotency/index.d.ts",
36
+ "import": "./dist/idempotency/index.js"
37
+ },
38
+ "./outbox": {
39
+ "types": "./dist/outbox/index.d.ts",
40
+ "import": "./dist/outbox/index.js"
41
+ },
42
+ "./jobs": {
43
+ "types": "./dist/jobs/index.d.ts",
44
+ "import": "./dist/jobs/index.js"
45
+ },
46
+ "./files": {
47
+ "types": "./dist/files/index.d.ts",
48
+ "import": "./dist/files/index.js"
29
49
  }
30
50
  },
31
51
  "dependencies": {
32
52
  "@aws-sdk/client-secrets-manager": "^3.1126.0",
33
- "@venturekit/core": "0.0.31",
53
+ "@venturekit/core": "0.0.33",
34
54
  "pg": "^8.12.0"
35
55
  },
36
56
  "devDependencies": {
@@ -0,0 +1,305 @@
1
+ -- @venturekit/data — multi-tenant Postgres foundation.
2
+ --
3
+ -- Functions created by this migration:
4
+ -- vk_uuid_generate_v7() — time-ordered primary keys
5
+ -- vk_tenant_scope() — every tenant this request may READ
6
+ -- vk_acting_tenant() — the single tenant it may WRITE as
7
+ -- vk_install_tenant_guards(schema, table, role) — forced RLS + policy + grants
8
+ -- vk_install_catalog_guards(schema, table, role) — read-only grants for shared catalogs
9
+ --
10
+ -- # Why this is the framework's job
11
+ --
12
+ -- `runWithTenantScope()` in this package publishes `app.tenant_id` and
13
+ -- `app.tenant_ids` as Postgres GUCs. That is only half a tenancy feature: the
14
+ -- GUCs do nothing until a policy reads them, and until now every consumer had
15
+ -- to write that policy — and the `ENABLE`/`FORCE`/`GRANT` around it — by hand,
16
+ -- once per table. A project with sixty tenant-scoped tables hand-writes ~300
17
+ -- lines in which a single omission is invisible: the table still works, queries
18
+ -- still return rows, and the only symptom is that one table has no isolation at
19
+ -- all. Shipping the reader and the installer next to the writer of the GUCs
20
+ -- makes that omission impossible to write by accident.
21
+ --
22
+ -- Row-level security is also the one guarantee that CANNOT move into
23
+ -- application code: Postgres enforces it against the connection, which is
24
+ -- exactly what makes it hold when a query forgets its WHERE clause.
25
+ --
26
+ -- # The role split RLS depends on
27
+ --
28
+ -- RLS is skipped for superusers, for roles with BYPASSRLS, and for a table's
29
+ -- owner unless FORCE ROW LEVEL SECURITY is set. So:
30
+ --
31
+ -- * migrations run as the OWNER (they must create and alter tables);
32
+ -- * the application connects as the tenant role — `DatabaseIntent.tenantRole`
33
+ -- in `vk.config.ts` — which owns nothing and has neither attribute, and is
34
+ -- granted only DML.
35
+ --
36
+ -- FORCE is set anyway, so even a mistaken connection as the owner is filtered.
37
+ -- Local development against a superuser bypasses all of it, which is why
38
+ -- isolation tests must connect as the tenant role explicitly or they pass while
39
+ -- proving nothing.
40
+ --
41
+ -- Names are `vk_`-prefixed and unqualified, matching `vk_tenants` and
42
+ -- `vk_notifications`: a package-owned object must not collide with a consumer's
43
+ -- own, and creating a schema in someone else's database is not this package's
44
+ -- call to make. A project that prefers its own namespace can wrap these —
45
+ -- `CREATE OR REPLACE FUNCTION sys.tenant_scope() ... SELECT <schema>.vk_tenant_scope()`
46
+ -- — which keeps one implementation while leaving existing call sites alone.
47
+ --
48
+ -- # Why the `0000_` filename prefix
49
+ --
50
+ -- The migration runner merges the project's own `.sql` files with every
51
+ -- installed package's and applies them in ALPHABETIC order, which puts the
52
+ -- conventional `vk_<pkg>_<NNN>_*` names *after* a project's `0xx_*` files. That
53
+ -- is right for tables a project only reads, and wrong for these: a consumer's
54
+ -- own migration calls `vk_install_tenant_guards(...)` as it creates each table,
55
+ -- so the functions must already exist. Worse, a `LANGUAGE sql` wrapper around
56
+ -- one of them is validated when it is CREATEd, so a project delegating to these
57
+ -- would fail at migrate time rather than at call time.
58
+ --
59
+ -- So this file takes the same one-character departure, for the same reason, as
60
+ -- `@venturekit-pro/tenancy`'s `0000_vk_tenancy_tenants.sql`: primordial-parent
61
+ -- status. Sorting before `0000_vk_tenancy_*` is incidental - neither depends on
62
+ -- the other.
63
+ --
64
+ -- # Effect on projects already in production
65
+ --
66
+ -- Additive, and safe to pick up late. The runner tracks applied files by name
67
+ -- and applies any it has not seen, so an existing database simply gains these
68
+ -- five functions on its next migrate; the `0000_` name does not imply
69
+ -- re-running anything, and nothing here depends on schema that a later
70
+ -- migration created. No existing file is touched, which is the thing that would
71
+ -- break - `vk migrate` hashes every applied file and raises
72
+ -- `MigrationHashMismatchError` if one changes. `pgcrypto` is already created by
73
+ -- `@venturekit-pro/tenancy`.
74
+ --
75
+ -- Idempotent throughout (`CREATE OR REPLACE`, `IF NOT EXISTS`), so re-applying
76
+ -- after a partial failure never breaks a deploy.
77
+
78
+ -- ─── Extensions ─────────────────────────────────────────────────────────
79
+ -- `pgcrypto` for gen_random_bytes, which the v7 generator below needs.
80
+ CREATE EXTENSION IF NOT EXISTS pgcrypto;
81
+
82
+ -- ─── Identifiers ────────────────────────────────────────────────────────
83
+
84
+ /**
85
+ * Time-ordered uuid (RFC 9562 v7).
86
+ *
87
+ * v4 keys scatter inserts across the whole index, so a busy table's hot page
88
+ * set is the entire index rather than its right edge — measurable write
89
+ * amplification once a table is taking millions of rows a day. v7 puts a
90
+ * millisecond timestamp in the leading 48 bits, restoring insert locality while
91
+ * still not exposing a guessable sequence.
92
+ *
93
+ * `clock_timestamp()` rather than `now()`: `now()` is fixed for the whole
94
+ * transaction, so a bulk insert of 5 000 rows would emit 5 000 keys sharing one
95
+ * timestamp prefix and lose the ordering this exists to provide.
96
+ */
97
+ CREATE OR REPLACE FUNCTION vk_uuid_generate_v7()
98
+ RETURNS uuid
99
+ LANGUAGE plpgsql
100
+ VOLATILE
101
+ PARALLEL SAFE
102
+ AS $$
103
+ DECLARE
104
+ ts_ms bigint := (extract(epoch FROM clock_timestamp()) * 1000)::bigint;
105
+ raw bytea;
106
+ BEGIN
107
+ -- 48-bit big-endian millisecond timestamp, then 80 random bits.
108
+ raw := substring(int8send(ts_ms) FROM 3 FOR 6) || gen_random_bytes(10);
109
+ -- Version 7 in the high nibble of octet 6.
110
+ raw := set_byte(raw, 6, (get_byte(raw, 6) & 15) | 112);
111
+ -- RFC 4122 variant (10xx) in the high bits of octet 8.
112
+ raw := set_byte(raw, 8, (get_byte(raw, 8) & 63) | 128);
113
+ RETURN encode(raw, 'hex')::uuid;
114
+ END;
115
+ $$;
116
+
117
+ COMMENT ON FUNCTION vk_uuid_generate_v7() IS
118
+ 'Time-ordered uuid v7 (RFC 9562). Insert-local alternative to gen_random_uuid().';
119
+
120
+ -- ─── Tenancy ────────────────────────────────────────────────────────────
121
+
122
+ /**
123
+ * Every tenant the current request may READ, or NULL when un-scoped.
124
+ *
125
+ * Reads `app.tenant_ids`, published by this package's `runWithTenantScope()`.
126
+ * A parent tenant's request carries itself plus its descendants, so "a group
127
+ * owner sees their child tenants" is satisfied by this one policy instead of a
128
+ * branch in every query.
129
+ *
130
+ * NULL is the fail-closed sentinel: `x = ANY (NULL)` is NULL, so an un-scoped
131
+ * request sees nothing rather than everything. The `true` argument to
132
+ * `current_setting` makes an unset GUC return NULL instead of raising, which is
133
+ * what lets a migration or an admin connection run outside any request scope.
134
+ *
135
+ * STABLE, so the planner evaluates it once per statement rather than per row.
136
+ */
137
+ CREATE OR REPLACE FUNCTION vk_tenant_scope()
138
+ RETURNS uuid[]
139
+ LANGUAGE sql
140
+ STABLE
141
+ PARALLEL SAFE
142
+ AS $$
143
+ SELECT CASE
144
+ WHEN coalesce(current_setting('app.tenant_ids', true), '') = '' THEN NULL::uuid[]
145
+ ELSE string_to_array(current_setting('app.tenant_ids', true), ',')::uuid[]
146
+ END;
147
+ $$;
148
+
149
+ COMMENT ON FUNCTION vk_tenant_scope() IS
150
+ 'Tenants readable by this request (app.tenant_ids). NULL = un-scoped = no rows.';
151
+
152
+ /**
153
+ * The single tenant this request ACTS as — `app.tenant_id`, the acting tenant
154
+ * of the scope.
155
+ *
156
+ * Writes are checked against this, not against the whole scope: a parent-tenant
157
+ * administrator reading twelve child tenants should not be able to create a row
158
+ * in one of them by accident. Widening a write to another tenant has to be a
159
+ * deliberate re-scope, which is visible in the code that does it.
160
+ */
161
+ CREATE OR REPLACE FUNCTION vk_acting_tenant()
162
+ RETURNS uuid
163
+ LANGUAGE sql
164
+ STABLE
165
+ PARALLEL SAFE
166
+ AS $$
167
+ SELECT nullif(current_setting('app.tenant_id', true), '')::uuid;
168
+ $$;
169
+
170
+ COMMENT ON FUNCTION vk_acting_tenant() IS
171
+ 'The tenant this request may WRITE as (app.tenant_id). NULL = un-scoped.';
172
+
173
+ /**
174
+ * Attach the standard guards to a tenant-scoped table:
175
+ *
176
+ * 1. RLS, ENABLEd and FORCEd;
177
+ * 2. the isolation policy — read across the scope, write only as the acting
178
+ * tenant;
179
+ * 3. DML grants for `p_role`.
180
+ *
181
+ * This is DDL run at migration time, not runtime behaviour: a macro that
182
+ * expands to the statements every tenant-scoped table needs.
183
+ *
184
+ * `p_role` is a parameter rather than a constant because the application role is
185
+ * the consumer's (`DatabaseIntent.tenantRole`), and a framework that hard-coded
186
+ * one would work for exactly one project. It is validated against `pg_roles`
187
+ * and interpolated with `%I`, so it cannot carry SQL.
188
+ *
189
+ * Idempotent, so re-running a migration during development is safe. The table
190
+ * must have a `tenant_id` column, asserted rather than assumed because a table
191
+ * that silently skipped its policy is exactly the failure this prevents.
192
+ */
193
+ CREATE OR REPLACE FUNCTION vk_install_tenant_guards(
194
+ p_schema text,
195
+ p_table text,
196
+ p_role text
197
+ )
198
+ RETURNS void
199
+ LANGUAGE plpgsql
200
+ AS $$
201
+ DECLARE
202
+ qualified text := format('%I.%I', p_schema, p_table);
203
+ fn_schema text;
204
+ BEGIN
205
+ IF NOT EXISTS (
206
+ SELECT 1 FROM information_schema.columns
207
+ WHERE table_schema = p_schema AND table_name = p_table AND column_name = 'tenant_id'
208
+ ) THEN
209
+ RAISE EXCEPTION 'vk_install_tenant_guards: %.% has no tenant_id column', p_schema, p_table;
210
+ END IF;
211
+
212
+ -- A missing role is a misconfiguration, not something to paper over: the
213
+ -- grant would fail anyway, and failing here names the cause.
214
+ IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = p_role) THEN
215
+ RAISE EXCEPTION 'vk_install_tenant_guards: role % does not exist', p_role;
216
+ END IF;
217
+
218
+ /* The policy must name the scope functions SCHEMA-QUALIFIED.
219
+ A policy expression is re-resolved against the *querying* session's
220
+ `search_path`, not the one in effect when the policy was created. Left
221
+ bare, `vk_tenant_scope()` would fail — or, far worse, resolve to a
222
+ same-named function in a schema earlier on some connection's path, which
223
+ is a tenant-isolation bypass that no test connecting normally would see.
224
+ So resolve where this migration actually put them, once, here. */
225
+ SELECT n.nspname INTO fn_schema
226
+ FROM pg_proc p
227
+ JOIN pg_namespace n ON n.oid = p.pronamespace
228
+ WHERE p.proname = 'vk_tenant_scope'
229
+ LIMIT 1;
230
+
231
+ IF fn_schema IS NULL THEN
232
+ RAISE EXCEPTION 'vk_install_tenant_guards: vk_tenant_scope() not found — is @venturekit/data''s migration applied?';
233
+ END IF;
234
+
235
+ EXECUTE format('ALTER TABLE %s ENABLE ROW LEVEL SECURITY', qualified);
236
+ EXECUTE format('ALTER TABLE %s FORCE ROW LEVEL SECURITY', qualified);
237
+
238
+ EXECUTE format('DROP POLICY IF EXISTS tenant_isolation ON %s', qualified);
239
+ EXECUTE format(
240
+ 'CREATE POLICY tenant_isolation ON %s
241
+ USING (tenant_id = ANY (%I.vk_tenant_scope()))
242
+ WITH CHECK (tenant_id = %I.vk_acting_tenant())',
243
+ qualified, fn_schema, fn_schema);
244
+
245
+ EXECUTE format('GRANT SELECT, INSERT, UPDATE, DELETE ON %s TO %I', qualified, p_role);
246
+ END;
247
+ $$;
248
+
249
+ COMMENT ON FUNCTION vk_install_tenant_guards(text, text, text) IS
250
+ 'Attach forced RLS, the isolation policy and DML grants. Call once per tenant-scoped table.';
251
+
252
+ /**
253
+ * Grants for a table that is deliberately NOT tenant-scoped — the shared
254
+ * catalogs every tenant reads and none may edit.
255
+ *
256
+ * Read-only to the application on purpose: these rows are curated centrally,
257
+ * and a tenant-facing service editing a shared catalog is how one tenant's
258
+ * data-entry mistake becomes everyone's.
259
+ */
260
+ CREATE OR REPLACE FUNCTION vk_install_catalog_guards(
261
+ p_schema text,
262
+ p_table text,
263
+ p_role text
264
+ )
265
+ RETURNS void
266
+ LANGUAGE plpgsql
267
+ AS $$
268
+ DECLARE
269
+ qualified text := format('%I.%I', p_schema, p_table);
270
+ BEGIN
271
+ IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = p_role) THEN
272
+ RAISE EXCEPTION 'vk_install_catalog_guards: role % does not exist', p_role;
273
+ END IF;
274
+
275
+ EXECUTE format('GRANT SELECT ON %s TO %I', qualified, p_role);
276
+ END;
277
+ $$;
278
+
279
+ COMMENT ON FUNCTION vk_install_catalog_guards(text, text, text) IS
280
+ 'Read-only grants for a shared, non-tenant-scoped catalog table.';
281
+
282
+ -- ─── Auditing the result ────────────────────────────────────────────────
283
+ --
284
+ -- "Which tenant-scoped tables are missing RLS?" is a query, not a view, so it
285
+ -- can live in the test that asserts it rather than in a schema object nobody
286
+ -- reads:
287
+ --
288
+ -- SELECT c.relname
289
+ -- FROM pg_class c
290
+ -- JOIN pg_namespace n ON n.oid = c.relnamespace
291
+ -- WHERE n.nspname = ANY ($1::text[])
292
+ -- AND c.relkind = 'r'
293
+ -- AND EXISTS (SELECT 1 FROM information_schema.columns
294
+ -- WHERE table_schema = n.nspname AND table_name = c.relname
295
+ -- AND column_name = 'tenant_id')
296
+ -- AND NOT (c.relrowsecurity AND EXISTS (
297
+ -- SELECT 1 FROM pg_policy p WHERE p.polrelid = c.oid));
298
+ --
299
+ -- A table counts as protected when RLS is ENABLEd and at least one policy
300
+ -- exists. FORCE is deliberately not required by that check: it applies the
301
+ -- policy to the table owner too, and trusted processes that legitimately write
302
+ -- across tenants (an event projector, an ingest listener, catalog seeds) run as
303
+ -- the owner. What matters is that the tenant role — the only one serving user
304
+ -- requests, and which owns nothing — is filtered. ENABLE plus a policy
305
+ -- guarantees that.
@@ -0,0 +1,48 @@
1
+ -- @venturekit/data — idempotency records.
2
+ --
3
+ -- Table created by this migration:
4
+ -- vk_idempotency_record — stored responses for replayed unsafe requests
5
+ --
6
+ -- Backs `createPostgresIdempotencyStore()` (`src/idempotency/postgres.ts`),
7
+ -- which plugs into `@venturekit/runtime`'s `idempotencyMiddleware`. That
8
+ -- middleware has always taken a pluggable store and shipped two — in-memory
9
+ -- (per-process, so wrong for Lambda) and DynamoDB — leaving a Postgres-only
10
+ -- project to stand up a DynamoDB table for four columns.
11
+ --
12
+ -- Unlike `0000_vk_data_foundation.sql` this file keeps the
13
+ -- conventional `vk_data_001_` name: nothing in a consumer's own migration
14
+ -- references the table, so sorting after the project's `0xx_*` files is fine.
15
+ --
16
+ -- Additive for existing projects: one new table, no existing file edited, and
17
+ -- nothing writes to it until a project wires the middleware.
18
+
19
+ CREATE TABLE IF NOT EXISTS vk_idempotency_record (
20
+ -- The idempotency key, optionally namespaced by the store's `scope` as
21
+ -- `<scope>:<key>`. Scoping matters: the middleware's default extractor reads
22
+ -- a CLIENT-supplied header, so without it two callers sending the same key
23
+ -- collide and the second is served the first's cached response — across
24
+ -- tenants, a cross-tenant read. See the `scope` option.
25
+ key text PRIMARY KEY,
26
+ -- The serialized handler response, replayed verbatim on a duplicate.
27
+ -- Empty while status is 'pending'.
28
+ response text NOT NULL DEFAULT '',
29
+ -- 'pending' is written before the handler runs and is what makes a
30
+ -- concurrent duplicate detectable; 'completed' once the response is stored.
31
+ -- A CHECK rather than an enum: enums are a migration to extend, and these
32
+ -- two values are the middleware's whole vocabulary.
33
+ status text NOT NULL CHECK (status IN ('pending', 'completed')),
34
+ -- Absolute expiry. Reads filter on this, so an expired row is already
35
+ -- invisible whether or not anything has deleted it.
36
+ expires_at timestamptz NOT NULL,
37
+ created_at timestamptz NOT NULL DEFAULT now(),
38
+ updated_at timestamptz NOT NULL DEFAULT now()
39
+ );
40
+
41
+ -- Postgres has no TTL, so expired rows are deleted by
42
+ -- `purgeExpiredIdempotencyRecords()` on a schedule. This index is what keeps
43
+ -- that sweep from scanning the table.
44
+ CREATE INDEX IF NOT EXISTS vk_idempotency_record_expires_at_idx
45
+ ON vk_idempotency_record (expires_at);
46
+
47
+ COMMENT ON TABLE vk_idempotency_record IS
48
+ 'Replay protection for unsafe requests. Backs @venturekit/data''s createPostgresIdempotencyStore().';
@@ -0,0 +1,99 @@
1
+ -- @venturekit/data — transactional outbox.
2
+ --
3
+ -- Table created by this migration:
4
+ -- vk_outbox — domain events awaiting publication
5
+ --
6
+ -- Backs `createPostgresOutboxStore()` (`src/outbox/postgres.ts`), the storage
7
+ -- half of `@venturekit/runtime/patterns`' outbox relay. `publishEvent()` puts an
8
+ -- event on EventBridge now; this table is how an event becomes part of the
9
+ -- transaction that caused it, so the two can never disagree about whether the
10
+ -- state change happened.
11
+ --
12
+ -- Additive for existing projects: one new table, no existing file edited, and
13
+ -- nothing writes to it until a project calls `appendToOutbox()`.
14
+
15
+ CREATE TABLE IF NOT EXISTS vk_outbox (
16
+ -- Supplied by the writer, not defaulted, and expected to be a time-ordered
17
+ -- uuid v7 (`newEventId()` in the runtime, `vk_uuid_generate_v7()` in SQL).
18
+ -- This is the primary key of an append-only table AND every consumer's dedupe
19
+ -- key: a v4 scatters the index it is clustered on as the table grows, and an
20
+ -- append-only table only grows. No DEFAULT because an id the writer already
21
+ -- put on the wire must be the id stored here.
22
+ id uuid PRIMARY KEY,
23
+
24
+ -- The ordering key. A sequence, so ordering is assigned at insert by the
25
+ -- database rather than by a clock — two events a millisecond apart on
26
+ -- different connections still get a total order, which `occurred_at` cannot
27
+ -- promise. The relay sorts by this and the consumer keeps it as the source
28
+ -- version.
29
+ seq bigserial NOT NULL UNIQUE,
30
+
31
+ type text NOT NULL,
32
+
33
+ -- Ordering is guaranteed PER AGGREGATE, so these two columns are what the
34
+ -- relay's blocking is keyed on, not decoration.
35
+ aggregate_type text NOT NULL,
36
+ aggregate_id text NOT NULL,
37
+
38
+ -- NULL for platform-global facts that belong to no tenant.
39
+ tenant_id uuid,
40
+
41
+ payload jsonb NOT NULL DEFAULT '{}'::jsonb,
42
+ -- Payload schema version. The envelope calls it `version`; the column is
43
+ -- `event_version` because `version` is a word Postgres tooling likes to use.
44
+ event_version integer NOT NULL DEFAULT 1,
45
+ -- When the business fact occurred, which is not when the row was inserted.
46
+ occurred_at timestamptz NOT NULL DEFAULT now(),
47
+ -- `{ type: 'user' | 'system', id }`. jsonb rather than two columns so the
48
+ -- envelope round-trips without the adapter reassembling it.
49
+ actor jsonb NOT NULL,
50
+
51
+ correlation_id text,
52
+ causation_id text,
53
+
54
+ -- Publication state. NULL means pending; the relay sets it AFTER the bus has
55
+ -- accepted the event, never before — marking first turns a crash into an
56
+ -- event that no longer exists anywhere.
57
+ published_at timestamptz,
58
+
59
+ -- Retry accounting. `attempts >= maxAttempts` is the quarantine, and the
60
+ -- relay's claim excludes both the quarantined row and anything queued behind
61
+ -- it for the same aggregate.
62
+ attempts integer NOT NULL DEFAULT 0,
63
+ last_error text,
64
+ failed_at timestamptz,
65
+
66
+ -- An operator deciding the event will never publish. Separate from `attempts`
67
+ -- on purpose: collapsing the two means raising the quarantine bound later
68
+ -- drags retired events back onto the bus. A discard also RELEASES the
69
+ -- successors held behind it, which is what makes quarantine a state an
70
+ -- aggregate can leave.
71
+ discarded_at timestamptz,
72
+ discard_reason text,
73
+
74
+ created_at timestamptz NOT NULL DEFAULT now()
75
+ );
76
+
77
+ -- The relay's claim: pending, not discarded, not quarantined, oldest first.
78
+ -- Partial, so it indexes only the rows that are actually claimable — which in a
79
+ -- healthy system is a handful out of however many million have been published.
80
+ CREATE INDEX IF NOT EXISTS vk_outbox_pending_seq_idx
81
+ ON vk_outbox (seq)
82
+ WHERE published_at IS NULL AND discarded_at IS NULL;
83
+
84
+ -- The `blocked` CTE in `claim()`: which aggregates have an abandoned event.
85
+ CREATE INDEX IF NOT EXISTS vk_outbox_blocked_idx
86
+ ON vk_outbox (aggregate_type, aggregate_id)
87
+ WHERE published_at IS NULL AND discarded_at IS NULL;
88
+
89
+ -- "What is stuck, and why" — the operator's query, and the one a dashboard runs.
90
+ CREATE INDEX IF NOT EXISTS vk_outbox_quarantined_idx
91
+ ON vk_outbox (failed_at DESC)
92
+ WHERE published_at IS NULL AND discarded_at IS NULL AND attempts > 0;
93
+
94
+ COMMENT ON TABLE vk_outbox IS
95
+ 'Transactional outbox. Events are appended in the caller''s transaction and relayed by @venturekit/runtime''s relayOnce().';
96
+ COMMENT ON COLUMN vk_outbox.seq IS
97
+ 'Ordering key assigned at insert. The relay guarantees per-aggregate order by this, and never publishes an event while an earlier one for the same aggregate is unpublished.';
98
+ COMMENT ON COLUMN vk_outbox.discarded_at IS
99
+ 'Operator retired this event. Leaves attempts untouched and releases successors of the same aggregate.';
@@ -0,0 +1,114 @@
1
+ -- @venturekit/data — durable background jobs.
2
+ --
3
+ -- Table created by this migration:
4
+ -- vk_job — queued work, in Postgres rather than SQS
5
+ --
6
+ -- Backs `@venturekit/data/jobs`.
7
+ --
8
+ -- # Why this exists next to the SQS `queues` intent rather than instead of it
9
+ --
10
+ -- SQS is the right tool for fire-and-forget fan-out and it stays the default.
11
+ -- What it cannot do is answer a question about work in flight. "Which imports
12
+ -- are still geocoding for this tenant" is a screen in the product, and a queue
13
+ -- has no query surface: messages are invisible until received, and received
14
+ -- messages are invisible to everyone else. Rebuilding that visibility on top of
15
+ -- SQS means a second table tracking what you put in the queue — at which point
16
+ -- the queue is the redundant half, because the table can be claimed from
17
+ -- directly.
18
+ --
19
+ -- So: reach for `queues` when the work is opaque and throughput matters, and for
20
+ -- this when the work is a thing the product talks about — an import, a export, a
21
+ -- recompute someone is waiting on.
22
+ --
23
+ -- Claiming uses FOR UPDATE SKIP LOCKED, which lets several workers share the
24
+ -- table without a lock convoy: each transaction takes rows nobody else holds and
25
+ -- skips the rest rather than queueing behind them.
26
+ --
27
+ -- Additive for existing projects: one new table, no existing file edited, and
28
+ -- nothing writes to it until a project calls `enqueueJob()`.
29
+
30
+ CREATE TABLE IF NOT EXISTS vk_job (
31
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
32
+
33
+ -- NULL for platform-wide work (partition maintenance, a global recompute).
34
+ -- Deliberately no foreign key: the table a tenant lives in is the consumer's
35
+ -- (`vk_tenants` only exists if @venturekit-pro/tenancy is installed), and a
36
+ -- framework-owned FK into an optional package's table would make this
37
+ -- migration fail for everyone who does not use it.
38
+ tenant_id uuid,
39
+
40
+ -- Dispatch key. `runJobsOnce()` maps this to a handler, so it is the contract
41
+ -- between whoever enqueues and whoever runs.
42
+ kind text NOT NULL,
43
+ payload jsonb NOT NULL DEFAULT '{}'::jsonb,
44
+
45
+ -- text + CHECK rather than an enum: an enum is a new type in the consumer's
46
+ -- schema and a migration to extend, and this vocabulary is closed anyway.
47
+ status text NOT NULL DEFAULT 'queued'
48
+ CHECK (status IN ('queued', 'running', 'succeeded', 'failed', 'cancelled')),
49
+
50
+ -- Scheduling and retry are the SAME mechanism: a failed job is re-queued with
51
+ -- a later `run_after`, and a job scheduled for tomorrow is just one with a
52
+ -- distant `run_after`. One column, so there is no way for the two to disagree.
53
+ run_after timestamptz NOT NULL DEFAULT now(),
54
+
55
+ attempts integer NOT NULL DEFAULT 0,
56
+ max_attempts integer NOT NULL DEFAULT 5,
57
+
58
+ -- Held while a worker owns the row. A worker that crashes leaves these set
59
+ -- and the row in 'running' forever, which is what `reclaimStuckJobs()` looks
60
+ -- for: 'running' plus an old `locked_at` is indistinguishable from a dead
61
+ -- worker, and treating it as one is the only way the work resumes.
62
+ locked_at timestamptz,
63
+ locked_by text,
64
+
65
+ last_error text,
66
+ created_at timestamptz NOT NULL DEFAULT now(),
67
+ updated_at timestamptz NOT NULL DEFAULT now(),
68
+ finished_at timestamptz,
69
+
70
+ CONSTRAINT vk_job_attempts_within_max CHECK (attempts <= max_attempts),
71
+ -- At least one, because the claim increments `attempts` before the handler
72
+ -- runs: with a bound of zero the first claim would violate the check above
73
+ -- and the job could never be taken at all.
74
+ CONSTRAINT vk_job_max_attempts_positive CHECK (max_attempts >= 1)
75
+ );
76
+
77
+ -- The claim: due, runnable, oldest first. Partial, so it indexes only the rows
78
+ -- a worker can take rather than every job ever run.
79
+ CREATE INDEX IF NOT EXISTS vk_job_runnable_idx
80
+ ON vk_job (run_after)
81
+ WHERE status = 'queued';
82
+
83
+ -- The product's per-tenant job list — the query this table exists to make
84
+ -- possible.
85
+ CREATE INDEX IF NOT EXISTS vk_job_tenant_status_idx
86
+ ON vk_job (tenant_id, status, created_at DESC);
87
+
88
+ -- Finding work abandoned by a dead worker.
89
+ CREATE INDEX IF NOT EXISTS vk_job_stuck_idx
90
+ ON vk_job (locked_at)
91
+ WHERE status = 'running';
92
+
93
+ -- # On row-level security
94
+ --
95
+ -- `vk_install_tenant_guards()` is deliberately NOT applied here, for two
96
+ -- reasons that are both about the worker rather than the reader:
97
+ --
98
+ -- 1. `tenant_id` is nullable, and RLS would make platform-wide rows invisible
99
+ -- to the very process that has to run them;
100
+ -- 2. a worker runs outside any request, so there is no acting tenant to scope
101
+ -- to — it processes every tenant's work by design.
102
+ --
103
+ -- Reads that serve a REQUEST must therefore carry the scope themselves, which
104
+ -- `listJobs()` does (`tenant_id = ANY (vk_tenant_scope())` when a scope is
105
+ -- engaged). That is the same predicate the policy would have applied, so a
106
+ -- caller who goes through the helper cannot see another tenant's work; a caller
107
+ -- who writes their own SQL against this table is responsible for the clause.
108
+
109
+ COMMENT ON TABLE vk_job IS
110
+ 'Durable background work, claimable with FOR UPDATE SKIP LOCKED. In Postgres so that work in flight is queryable; see @venturekit/data/jobs.';
111
+ COMMENT ON COLUMN vk_job.run_after IS
112
+ 'Earliest time this job may run. Retry backoff and future scheduling are the same mechanism.';
113
+ COMMENT ON COLUMN vk_job.locked_at IS
114
+ 'Set while a worker holds the row. Old + status running = a dead worker; reclaimStuckJobs() re-queues it.';