@nodii/db-rls 0.0.2 → 0.2.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/bin/migrate-gen.mjs +12 -0
- package/dist/auth-schema.d.ts +45 -0
- package/dist/auth-schema.d.ts.map +1 -0
- package/dist/auth-schema.js +101 -0
- package/dist/auth-schema.js.map +1 -0
- package/dist/configure.d.ts +45 -0
- package/dist/configure.d.ts.map +1 -0
- package/dist/configure.js +85 -0
- package/dist/configure.js.map +1 -0
- package/dist/context.d.ts +43 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +98 -0
- package/dist/context.js.map +1 -0
- package/dist/drizzle-client.d.ts +22 -0
- package/dist/drizzle-client.d.ts.map +1 -0
- package/dist/drizzle-client.js +109 -0
- package/dist/drizzle-client.js.map +1 -0
- package/dist/in-memory-pool.d.ts +31 -0
- package/dist/in-memory-pool.d.ts.map +1 -0
- package/dist/in-memory-pool.js +53 -0
- package/dist/in-memory-pool.js.map +1 -0
- package/dist/index.d.ts +21 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +35 -4
- package/dist/index.js.map +1 -1
- package/dist/init.d.ts +18 -0
- package/dist/init.d.ts.map +1 -0
- package/dist/init.js +40 -0
- package/dist/init.js.map +1 -0
- package/dist/migrate-gen.d.ts +41 -0
- package/dist/migrate-gen.d.ts.map +1 -0
- package/dist/migrate-gen.js +123 -0
- package/dist/migrate-gen.js.map +1 -0
- package/dist/pg-pool-adapter.d.ts +44 -0
- package/dist/pg-pool-adapter.d.ts.map +1 -0
- package/dist/pg-pool-adapter.js +111 -0
- package/dist/pg-pool-adapter.js.map +1 -0
- package/dist/rls-policy-gen.d.ts +26 -0
- package/dist/rls-policy-gen.d.ts.map +1 -0
- package/dist/rls-policy-gen.js +81 -0
- package/dist/rls-policy-gen.js.map +1 -0
- package/dist/rls-policy.d.ts +30 -0
- package/dist/rls-policy.d.ts.map +1 -0
- package/dist/rls-policy.js +81 -0
- package/dist/rls-policy.js.map +1 -0
- package/dist/types.d.ts +71 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +56 -0
- package/dist/types.js.map +1 -0
- package/package.json +31 -7
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// In-memory PgPool / PgTx test doubles.
|
|
2
|
+
//
|
|
3
|
+
// Records every `set_config` call and every `exec` invocation so tests
|
|
4
|
+
// can assert the exact transcript without needing a real Postgres.
|
|
5
|
+
// Production wiring (pg / Drizzle / psycopg / pgx) lands in v0.1.x —
|
|
6
|
+
// `// TODO db-rls-v0.1.x: real driver adapters`.
|
|
7
|
+
/** In-memory PgPool. Each instance owns its own transcript so tests can
|
|
8
|
+
* inspect what each pool was asked to do. */
|
|
9
|
+
export class InMemoryPgPool {
|
|
10
|
+
kind;
|
|
11
|
+
transcript = [];
|
|
12
|
+
constructor(kind) {
|
|
13
|
+
this.kind = kind;
|
|
14
|
+
}
|
|
15
|
+
async withTransaction(fn) {
|
|
16
|
+
this.transcript.push({ kind: "begin", pool: this.kind });
|
|
17
|
+
const tx = new InMemoryPgTx(this);
|
|
18
|
+
try {
|
|
19
|
+
const result = await fn(tx);
|
|
20
|
+
this.transcript.push({ kind: "commit", pool: this.kind });
|
|
21
|
+
return result;
|
|
22
|
+
}
|
|
23
|
+
catch (err) {
|
|
24
|
+
this.transcript.push({ kind: "rollback", pool: this.kind });
|
|
25
|
+
throw err;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
class InMemoryPgTx {
|
|
30
|
+
poolKind;
|
|
31
|
+
pool;
|
|
32
|
+
constructor(pool) {
|
|
33
|
+
this.pool = pool;
|
|
34
|
+
this.poolKind = pool.kind;
|
|
35
|
+
}
|
|
36
|
+
async exec(sql, params = []) {
|
|
37
|
+
this.pool.transcript.push({
|
|
38
|
+
kind: "exec",
|
|
39
|
+
pool: this.poolKind,
|
|
40
|
+
sql,
|
|
41
|
+
params,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
async setConfig(key, value) {
|
|
45
|
+
this.pool.transcript.push({
|
|
46
|
+
kind: "set_config",
|
|
47
|
+
pool: this.poolKind,
|
|
48
|
+
key,
|
|
49
|
+
value,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
//# sourceMappingURL=in-memory-pool.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"in-memory-pool.js","sourceRoot":"","sources":["../src/in-memory-pool.ts"],"names":[],"mappings":"AAAA,wCAAwC;AACxC,EAAE;AACF,uEAAuE;AACvE,mEAAmE;AACnE,qEAAqE;AACrE,iDAAiD;AAYjD;8CAC8C;AAC9C,MAAM,OAAO,cAAc;IAChB,IAAI,CAAW;IACf,UAAU,GAAsB,EAAE,CAAC;IAE5C,YAAY,IAAc;QACxB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;IACnB,CAAC;IAED,KAAK,CAAC,eAAe,CAAI,EAA4B;QACnD,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC;QACzD,MAAM,EAAE,GAAG,IAAI,YAAY,CAAC,IAAI,CAAC,CAAC;QAClC,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,MAAM,EAAE,CAAC,EAAE,CAAC,CAAC;YAC5B,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC;YAC1D,OAAO,MAAM,CAAC;QAChB,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC;YAC5D,MAAM,GAAG,CAAC;QACZ,CAAC;IACH,CAAC;CACF;AAED,MAAM,YAAY;IACP,QAAQ,CAAW;IACX,IAAI,CAAiB;IAEtC,YAAY,IAAoB;QAC9B,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC;IAC5B,CAAC;IAED,KAAK,CAAC,IAAI,CAAC,GAAW,EAAE,SAA6B,EAAE;QACrD,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC;YACxB,IAAI,EAAE,MAAM;YACZ,IAAI,EAAE,IAAI,CAAC,QAAQ;YACnB,GAAG;YACH,MAAM;SACP,CAAC,CAAC;IACL,CAAC;IAED,KAAK,CAAC,SAAS,CAAC,GAAW,EAAE,KAAa;QACxC,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC;YACxB,IAAI,EAAE,YAAY;YAClB,IAAI,EAAE,IAAI,CAAC,QAAQ;YACnB,GAAG;YACH,KAAK;SACN,CAAC,CAAC;IACL,CAAC;CACF"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,23 @@
|
|
|
1
1
|
export declare const LIB_NAME = "db-rls";
|
|
2
|
-
export declare const VERSION = "0.
|
|
2
|
+
export declare const VERSION = "0.1.1";
|
|
3
|
+
export { initDbRls, requireDbRls, getDbRlsOrNull, requirePool, _resetForTests, } from "./init";
|
|
4
|
+
export type { InitDbRlsConfig, ResolvedDbRls } from "./init";
|
|
5
|
+
export { configureDbRls, PoolCredentialMismatch, _resetConfigureForTests, } from "./configure";
|
|
6
|
+
export type { ConfigureDbRlsOpts } from "./configure";
|
|
7
|
+
export { PgClientPool } from "./pg-pool-adapter";
|
|
8
|
+
export type { PgPoolAdapterOpts } from "./pg-pool-adapter";
|
|
9
|
+
export { withUserContext, withSystemContext, withOwnerContext, autoDispatch, } from "./context";
|
|
10
|
+
export { withUserContextDb, withSystemContextDb, withOwnerContextDb, } from "./drizzle-client";
|
|
11
|
+
export type { DrizzleClient, NodePgDatabase } from "./drizzle-client";
|
|
12
|
+
export { AUTH_SCHEMA_SQL, AUTH_SCHEMA_GRANTS_SQL, applyAuthSchemaMigrations, authSchemaFullSql, } from "./auth-schema";
|
|
13
|
+
export type { SqlExecutor } from "./auth-schema";
|
|
14
|
+
export { withRls, generateTableGrants } from "./rls-policy-gen";
|
|
15
|
+
export type { RlsVerb, WithRlsOpts } from "./rls-policy-gen";
|
|
16
|
+
export { migrateGen, runMigrateGenCli, userTenantIdsRealSql, userTenantIdsStubSql, } from "./migrate-gen";
|
|
17
|
+
export type { MigrateGenInput, MigrateGenResult } from "./migrate-gen";
|
|
18
|
+
export { generateRlsPolicy, authSchemaHelpers } from "./rls-policy";
|
|
19
|
+
export { InMemoryPgPool } from "./in-memory-pool";
|
|
20
|
+
export type { TranscriptEntry } from "./in-memory-pool";
|
|
21
|
+
export { DbRlsError, DbRlsNotInitialized, PoolUnavailable, MissingTenantContext, TransactionRolledBack, } from "./types";
|
|
22
|
+
export type { AuthContext, AutoDispatchOpts, GenerateRlsPolicyOpts, PgPool, PgTx, PoolKind, SystemContextOpts, UserContextOpts, } from "./types";
|
|
3
23
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAuBA,eAAO,MAAM,QAAQ,WAAW,CAAC;AACjC,eAAO,MAAM,OAAO,UAAU,CAAC;AAE/B,OAAO,EACL,SAAS,EACT,YAAY,EACZ,cAAc,EACd,WAAW,EACX,cAAc,GACf,MAAM,QAAQ,CAAC;AAChB,YAAY,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,QAAQ,CAAC;AAE7D,OAAO,EACL,cAAc,EACd,sBAAsB,EACtB,uBAAuB,GACxB,MAAM,aAAa,CAAC;AACrB,YAAY,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAEtD,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACjD,YAAY,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAE3D,OAAO,EACL,eAAe,EACf,iBAAiB,EACjB,gBAAgB,EAChB,YAAY,GACb,MAAM,WAAW,CAAC;AAEnB,OAAO,EACL,iBAAiB,EACjB,mBAAmB,EACnB,kBAAkB,GACnB,MAAM,kBAAkB,CAAC;AAC1B,YAAY,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAEtE,OAAO,EACL,eAAe,EACf,sBAAsB,EACtB,yBAAyB,EACzB,iBAAiB,GAClB,MAAM,eAAe,CAAC;AACvB,YAAY,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAEjD,OAAO,EAAE,OAAO,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AAChE,YAAY,EAAE,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAE7D,OAAO,EACL,UAAU,EACV,gBAAgB,EAChB,oBAAoB,EACpB,oBAAoB,GACrB,MAAM,eAAe,CAAC;AACvB,YAAY,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAIvE,OAAO,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AAEpE,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAClD,YAAY,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAExD,OAAO,EACL,UAAU,EACV,mBAAmB,EACnB,eAAe,EACf,oBAAoB,EACpB,qBAAqB,GACtB,MAAM,SAAS,CAAC;AAEjB,YAAY,EACV,WAAW,EACX,gBAAgB,EAChB,qBAAqB,EACrB,MAAM,EACN,IAAI,EACJ,QAAQ,EACR,iBAAiB,EACjB,eAAe,GAChB,MAAM,SAAS,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,38 @@
|
|
|
1
|
-
// @nodii/db-rls —
|
|
2
|
-
// Implementation lands at v0.1.0 per the locked feature_doc.
|
|
1
|
+
// @nodii/db-rls — v0.1.1 public barrel.
|
|
3
2
|
//
|
|
4
|
-
// Spec:
|
|
3
|
+
// Spec: planning hub feature_doc serviceId=nodii-libs docKey=db-rls
|
|
4
|
+
// (A1 posture — RLS on every tenant-scoped table — locked 2026-05-25).
|
|
5
|
+
// Polyglot ship: TS + Python + Go in parity.
|
|
6
|
+
//
|
|
7
|
+
// v0.1.1 surface:
|
|
8
|
+
// - configureDbRls (real pg.Pool-aware boot helper) + initDbRls (raw)
|
|
9
|
+
// - PgClientPool (wraps pg.Pool → PgPool interface)
|
|
10
|
+
// - InMemoryPgPool (still exported — used by upstream tests)
|
|
11
|
+
// - withUserContext / withSystemContext / withOwnerContext / autoDispatch
|
|
12
|
+
// (operates on the substrate-agnostic PgTx)
|
|
13
|
+
// - withUserContextDb / withSystemContextDb / withOwnerContextDb
|
|
14
|
+
// (Drizzle-bound; require a real PgClientPool)
|
|
15
|
+
// - applyAuthSchemaMigrations + AUTH_SCHEMA_SQL + AUTH_SCHEMA_GRANTS_SQL
|
|
16
|
+
// - withRls() (RLS-policy DDL generator per § 5.5)
|
|
17
|
+
// - generateRlsPolicy / authSchemaHelpers (deprecated aliases, kept for
|
|
18
|
+
// parity-fence + parity with the v0.1.0 surface; no NotImplemented paths)
|
|
19
|
+
// - migrateGen + runMigrateGenCli + userTenantIdsRealSql + userTenantIdsStubSql
|
|
20
|
+
// - Typed errors: DbRlsError + DbRlsNotInitialized + PoolUnavailable +
|
|
21
|
+
// MissingTenantContext + TransactionRolledBack +
|
|
22
|
+
// PoolCredentialMismatch
|
|
5
23
|
export const LIB_NAME = "db-rls";
|
|
6
|
-
export const VERSION = "0.
|
|
24
|
+
export const VERSION = "0.1.1";
|
|
25
|
+
export { initDbRls, requireDbRls, getDbRlsOrNull, requirePool, _resetForTests, } from "./init";
|
|
26
|
+
export { configureDbRls, PoolCredentialMismatch, _resetConfigureForTests, } from "./configure";
|
|
27
|
+
export { PgClientPool } from "./pg-pool-adapter";
|
|
28
|
+
export { withUserContext, withSystemContext, withOwnerContext, autoDispatch, } from "./context";
|
|
29
|
+
export { withUserContextDb, withSystemContextDb, withOwnerContextDb, } from "./drizzle-client";
|
|
30
|
+
export { AUTH_SCHEMA_SQL, AUTH_SCHEMA_GRANTS_SQL, applyAuthSchemaMigrations, authSchemaFullSql, } from "./auth-schema";
|
|
31
|
+
export { withRls, generateTableGrants } from "./rls-policy-gen";
|
|
32
|
+
export { migrateGen, runMigrateGenCli, userTenantIdsRealSql, userTenantIdsStubSql, } from "./migrate-gen";
|
|
33
|
+
// Legacy v0.1.0 surface kept for callers that already imported these
|
|
34
|
+
// names. Both are byte-equal SQL helpers — no NotImplemented paths.
|
|
35
|
+
export { generateRlsPolicy, authSchemaHelpers } from "./rls-policy";
|
|
36
|
+
export { InMemoryPgPool } from "./in-memory-pool";
|
|
37
|
+
export { DbRlsError, DbRlsNotInitialized, PoolUnavailable, MissingTenantContext, TransactionRolledBack, } from "./types";
|
|
7
38
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,wCAAwC;AACxC,EAAE;AACF,oEAAoE;AACpE,uEAAuE;AACvE,6CAA6C;AAC7C,EAAE;AACF,kBAAkB;AAClB,wEAAwE;AACxE,sDAAsD;AACtD,+DAA+D;AAC/D,4EAA4E;AAC5E,gDAAgD;AAChD,mEAAmE;AACnE,mDAAmD;AACnD,2EAA2E;AAC3E,qDAAqD;AACrD,0EAA0E;AAC1E,8EAA8E;AAC9E,kFAAkF;AAClF,yEAAyE;AACzE,mEAAmE;AACnE,2CAA2C;AAE3C,MAAM,CAAC,MAAM,QAAQ,GAAG,QAAQ,CAAC;AACjC,MAAM,CAAC,MAAM,OAAO,GAAG,OAAO,CAAC;AAE/B,OAAO,EACL,SAAS,EACT,YAAY,EACZ,cAAc,EACd,WAAW,EACX,cAAc,GACf,MAAM,QAAQ,CAAC;AAGhB,OAAO,EACL,cAAc,EACd,sBAAsB,EACtB,uBAAuB,GACxB,MAAM,aAAa,CAAC;AAGrB,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAGjD,OAAO,EACL,eAAe,EACf,iBAAiB,EACjB,gBAAgB,EAChB,YAAY,GACb,MAAM,WAAW,CAAC;AAEnB,OAAO,EACL,iBAAiB,EACjB,mBAAmB,EACnB,kBAAkB,GACnB,MAAM,kBAAkB,CAAC;AAG1B,OAAO,EACL,eAAe,EACf,sBAAsB,EACtB,yBAAyB,EACzB,iBAAiB,GAClB,MAAM,eAAe,CAAC;AAGvB,OAAO,EAAE,OAAO,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AAGhE,OAAO,EACL,UAAU,EACV,gBAAgB,EAChB,oBAAoB,EACpB,oBAAoB,GACrB,MAAM,eAAe,CAAC;AAGvB,qEAAqE;AACrE,oEAAoE;AACpE,OAAO,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AAEpE,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAGlD,OAAO,EACL,UAAU,EACV,mBAAmB,EACnB,eAAe,EACf,oBAAoB,EACpB,qBAAqB,GACtB,MAAM,SAAS,CAAC"}
|
package/dist/init.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type PgPool, type PoolKind } from "./types";
|
|
2
|
+
export interface InitDbRlsConfig {
|
|
3
|
+
owner: PgPool;
|
|
4
|
+
services: PgPool;
|
|
5
|
+
authenticated: PgPool;
|
|
6
|
+
}
|
|
7
|
+
export interface ResolvedDbRls {
|
|
8
|
+
owner: PgPool;
|
|
9
|
+
services: PgPool;
|
|
10
|
+
authenticated: PgPool;
|
|
11
|
+
}
|
|
12
|
+
export declare function initDbRls(config: InitDbRlsConfig): void;
|
|
13
|
+
export declare function requireDbRls(): ResolvedDbRls;
|
|
14
|
+
export declare function getDbRlsOrNull(): ResolvedDbRls | null;
|
|
15
|
+
export declare function requirePool(kind: PoolKind): PgPool;
|
|
16
|
+
/** Test-only reset; NOT exported through the public barrel. */
|
|
17
|
+
export declare function _resetForTests(): void;
|
|
18
|
+
//# sourceMappingURL=init.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../src/init.ts"],"names":[],"mappings":"AAKA,OAAO,EAEL,KAAK,MAAM,EAEX,KAAK,QAAQ,EACd,MAAM,SAAS,CAAC;AAEjB,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,MAAM,CAAC;CACvB;AAID,wBAAgB,SAAS,CAAC,MAAM,EAAE,eAAe,GAAG,IAAI,CAcvD;AAED,wBAAgB,YAAY,IAAI,aAAa,CAK5C;AAED,wBAAgB,cAAc,IAAI,aAAa,GAAG,IAAI,CAErD;AAED,wBAAgB,WAAW,CAAC,IAAI,EAAE,QAAQ,GAAG,MAAM,CAOlD;AAED,+DAA+D;AAC/D,wBAAgB,cAAc,IAAI,IAAI,CAErC"}
|
package/dist/init.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// initDbRls — process-local bootstrap; mirrors initSaga / initTelemetry.
|
|
2
|
+
//
|
|
3
|
+
// Spec § 5.2. Caller supplies one PgPool per role. Subsequent calls warn +
|
|
4
|
+
// no-op (first-init wins). Public APIs throw `DbRlsNotInitialized` before init.
|
|
5
|
+
import { DbRlsNotInitialized, PoolUnavailable, } from "./types";
|
|
6
|
+
let resolved = null;
|
|
7
|
+
export function initDbRls(config) {
|
|
8
|
+
if (resolved) {
|
|
9
|
+
// eslint-disable-next-line no-console
|
|
10
|
+
console.warn("[@nodii/db-rls] initDbRls() called more than once; ignoring subsequent call.");
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
resolved = {
|
|
14
|
+
owner: config.owner,
|
|
15
|
+
services: config.services,
|
|
16
|
+
authenticated: config.authenticated,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
export function requireDbRls() {
|
|
20
|
+
if (!resolved) {
|
|
21
|
+
throw new DbRlsNotInitialized();
|
|
22
|
+
}
|
|
23
|
+
return resolved;
|
|
24
|
+
}
|
|
25
|
+
export function getDbRlsOrNull() {
|
|
26
|
+
return resolved;
|
|
27
|
+
}
|
|
28
|
+
export function requirePool(kind) {
|
|
29
|
+
const cfg = requireDbRls();
|
|
30
|
+
const pool = cfg[kind];
|
|
31
|
+
if (!pool) {
|
|
32
|
+
throw new PoolUnavailable(kind);
|
|
33
|
+
}
|
|
34
|
+
return pool;
|
|
35
|
+
}
|
|
36
|
+
/** Test-only reset; NOT exported through the public barrel. */
|
|
37
|
+
export function _resetForTests() {
|
|
38
|
+
resolved = null;
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=init.js.map
|
package/dist/init.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"init.js","sourceRoot":"","sources":["../src/init.ts"],"names":[],"mappings":"AAAA,yEAAyE;AACzE,EAAE;AACF,2EAA2E;AAC3E,gFAAgF;AAEhF,OAAO,EACL,mBAAmB,EAEnB,eAAe,GAEhB,MAAM,SAAS,CAAC;AAcjB,IAAI,QAAQ,GAAyB,IAAI,CAAC;AAE1C,MAAM,UAAU,SAAS,CAAC,MAAuB;IAC/C,IAAI,QAAQ,EAAE,CAAC;QACb,sCAAsC;QACtC,OAAO,CAAC,IAAI,CACV,8EAA8E,CAC/E,CAAC;QACF,OAAO;IACT,CAAC;IAED,QAAQ,GAAG;QACT,KAAK,EAAE,MAAM,CAAC,KAAK;QACnB,QAAQ,EAAE,MAAM,CAAC,QAAQ;QACzB,aAAa,EAAE,MAAM,CAAC,aAAa;KACpC,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,YAAY;IAC1B,IAAI,CAAC,QAAQ,EAAE,CAAC;QACd,MAAM,IAAI,mBAAmB,EAAE,CAAC;IAClC,CAAC;IACD,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED,MAAM,UAAU,cAAc;IAC5B,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,IAAc;IACxC,MAAM,GAAG,GAAG,YAAY,EAAE,CAAC;IAC3B,MAAM,IAAI,GAAG,GAAG,CAAC,IAAI,CAAC,CAAC;IACvB,IAAI,CAAC,IAAI,EAAE,CAAC;QACV,MAAM,IAAI,eAAe,CAAC,IAAI,CAAC,CAAC;IAClC,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,+DAA+D;AAC/D,MAAM,UAAU,cAAc;IAC5B,QAAQ,GAAG,IAAI,CAAC;AAClB,CAAC"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export interface MigrateGenInput {
|
|
2
|
+
/** Pattern A service name, e.g. "nodii-billing-service". Used only for
|
|
3
|
+
* the file header comment; not in the SQL body. */
|
|
4
|
+
serviceName: string;
|
|
5
|
+
/** Defaults to `./infra/postgres`. */
|
|
6
|
+
outDir?: string;
|
|
7
|
+
/** When `true`, emit ONLY the stub variant (returns empty set). Use
|
|
8
|
+
* when running in the chicken-and-egg case where Drizzle migrate has
|
|
9
|
+
* not yet created the mirror tables. */
|
|
10
|
+
stub?: boolean;
|
|
11
|
+
}
|
|
12
|
+
export interface MigrateGenResult {
|
|
13
|
+
/** Absolute path of the file that was written. */
|
|
14
|
+
path: string;
|
|
15
|
+
/** Contents written (also returned for parity-fence assertions). */
|
|
16
|
+
contents: string;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* SQL text for the real (joins mirror tables) helper. Module-level
|
|
20
|
+
* constant so parity-fence can byte-compare.
|
|
21
|
+
*/
|
|
22
|
+
export declare function userTenantIdsRealSql(serviceName: string): string;
|
|
23
|
+
/**
|
|
24
|
+
* SQL text for the chicken-and-egg stub variant (returns empty set).
|
|
25
|
+
*/
|
|
26
|
+
export declare function userTenantIdsStubSql(serviceName: string): string;
|
|
27
|
+
/**
|
|
28
|
+
* Generates the residual migration phase file and returns its contents.
|
|
29
|
+
* If `outDir` is set, writes the file to disk; otherwise just returns
|
|
30
|
+
* the contents (used by tests + parity-fence).
|
|
31
|
+
*/
|
|
32
|
+
export declare function migrateGen(input: MigrateGenInput): MigrateGenResult;
|
|
33
|
+
/**
|
|
34
|
+
* CLI entrypoint. Invoked via `npx @nodii/db-rls migrate-gen ...` or
|
|
35
|
+
* `bun ts/db-rls/src/migrate-gen.ts ...`.
|
|
36
|
+
*
|
|
37
|
+
* Returns exit code (0 on success, non-zero on usage error). Caller
|
|
38
|
+
* (a small bin/migrate-gen.js shim) translates to process.exit.
|
|
39
|
+
*/
|
|
40
|
+
export declare function runMigrateGenCli(args: readonly string[]): number;
|
|
41
|
+
//# sourceMappingURL=migrate-gen.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"migrate-gen.d.ts","sourceRoot":"","sources":["../src/migrate-gen.ts"],"names":[],"mappings":"AAsBA,MAAM,WAAW,eAAe;IAC9B;wDACoD;IACpD,WAAW,EAAE,MAAM,CAAC;IACpB,sCAAsC;IACtC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;6CAEyC;IACzC,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AAED,MAAM,WAAW,gBAAgB;IAC/B,kDAAkD;IAClD,IAAI,EAAE,MAAM,CAAC;IACb,oEAAoE;IACpE,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAehE;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAWhE;AAED;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,eAAe,GAAG,gBAAgB,CAanE;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,CA6BhE"}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
// migrate-gen — generates the per-service residual migration phase
|
|
2
|
+
// per § 5.7.
|
|
3
|
+
//
|
|
4
|
+
// Output:
|
|
5
|
+
// <outDir>/004-helpers-and-grants/0001_user_tenant_ids.sql
|
|
6
|
+
//
|
|
7
|
+
// The generated file installs `auth.user_tenant_ids(uuid)` joining the
|
|
8
|
+
// service's `employees_mirror` + `customers_mirror` tables (per PII
|
|
9
|
+
// doctrine § Q5 + db-rls Q1 lock). Caller services apply it AFTER
|
|
10
|
+
// Drizzle migrations create the mirror tables (or wire the stub
|
|
11
|
+
// 0000_user_tenant_ids_stub.sql per the chicken-and-egg path).
|
|
12
|
+
//
|
|
13
|
+
// CLI shape:
|
|
14
|
+
// npx @nodii/db-rls migrate-gen <serviceName> [--out <path>]
|
|
15
|
+
// bun ts/db-rls/src/migrate-gen.ts <serviceName> [--out <path>]
|
|
16
|
+
//
|
|
17
|
+
// Output is byte-equal across TS / Python / Go ports — parity-fence test
|
|
18
|
+
// asserts directly.
|
|
19
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
20
|
+
import { dirname, join, resolve } from "node:path";
|
|
21
|
+
/**
|
|
22
|
+
* SQL text for the real (joins mirror tables) helper. Module-level
|
|
23
|
+
* constant so parity-fence can byte-compare.
|
|
24
|
+
*/
|
|
25
|
+
export function userTenantIdsRealSql(serviceName) {
|
|
26
|
+
return `-- Service: ${serviceName}
|
|
27
|
+
-- Generated by @nodii/db-rls migrate-gen
|
|
28
|
+
-- Phase: 004-helpers-and-grants — service-owned auth.user_tenant_ids
|
|
29
|
+
-- Spec: § 5.7 (terraform-bootstrap split)
|
|
30
|
+
|
|
31
|
+
CREATE OR REPLACE FUNCTION auth.user_tenant_ids(p_user_id uuid) RETURNS SETOF uuid
|
|
32
|
+
LANGUAGE sql STABLE AS $$
|
|
33
|
+
SELECT tenant_id FROM employees_mirror WHERE id = p_user_id
|
|
34
|
+
UNION
|
|
35
|
+
SELECT tenant_id FROM customers_mirror WHERE id = p_user_id
|
|
36
|
+
$$;
|
|
37
|
+
|
|
38
|
+
GRANT EXECUTE ON FUNCTION auth.user_tenant_ids(uuid) TO nodii_services, authenticated;
|
|
39
|
+
`;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* SQL text for the chicken-and-egg stub variant (returns empty set).
|
|
43
|
+
*/
|
|
44
|
+
export function userTenantIdsStubSql(serviceName) {
|
|
45
|
+
return `-- Service: ${serviceName}
|
|
46
|
+
-- Generated by @nodii/db-rls migrate-gen --stub
|
|
47
|
+
-- Phase: 004-helpers-and-grants — stub variant (returns empty set)
|
|
48
|
+
-- Spec: § 5.7 (chicken-and-egg)
|
|
49
|
+
|
|
50
|
+
CREATE OR REPLACE FUNCTION auth.user_tenant_ids(p_user_id uuid) RETURNS SETOF uuid
|
|
51
|
+
LANGUAGE sql STABLE AS $$ SELECT NULL::uuid WHERE false $$;
|
|
52
|
+
|
|
53
|
+
GRANT EXECUTE ON FUNCTION auth.user_tenant_ids(uuid) TO nodii_services, authenticated;
|
|
54
|
+
`;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Generates the residual migration phase file and returns its contents.
|
|
58
|
+
* If `outDir` is set, writes the file to disk; otherwise just returns
|
|
59
|
+
* the contents (used by tests + parity-fence).
|
|
60
|
+
*/
|
|
61
|
+
export function migrateGen(input) {
|
|
62
|
+
const contents = input.stub
|
|
63
|
+
? userTenantIdsStubSql(input.serviceName)
|
|
64
|
+
: userTenantIdsRealSql(input.serviceName);
|
|
65
|
+
const fileName = input.stub
|
|
66
|
+
? "0000_user_tenant_ids_stub.sql"
|
|
67
|
+
: "0001_user_tenant_ids.sql";
|
|
68
|
+
const phase = "004-helpers-and-grants";
|
|
69
|
+
const outDir = resolve(input.outDir ?? "./infra/postgres");
|
|
70
|
+
const path = join(outDir, phase, fileName);
|
|
71
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
72
|
+
writeFileSync(path, contents);
|
|
73
|
+
return { path, contents };
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* CLI entrypoint. Invoked via `npx @nodii/db-rls migrate-gen ...` or
|
|
77
|
+
* `bun ts/db-rls/src/migrate-gen.ts ...`.
|
|
78
|
+
*
|
|
79
|
+
* Returns exit code (0 on success, non-zero on usage error). Caller
|
|
80
|
+
* (a small bin/migrate-gen.js shim) translates to process.exit.
|
|
81
|
+
*/
|
|
82
|
+
export function runMigrateGenCli(args) {
|
|
83
|
+
const positional = [];
|
|
84
|
+
let outDir;
|
|
85
|
+
let stub = false;
|
|
86
|
+
for (let i = 0; i < args.length; i++) {
|
|
87
|
+
const a = args[i];
|
|
88
|
+
if (a === "--out" && i + 1 < args.length) {
|
|
89
|
+
outDir = args[++i];
|
|
90
|
+
}
|
|
91
|
+
else if (a === "--stub") {
|
|
92
|
+
stub = true;
|
|
93
|
+
}
|
|
94
|
+
else if (a === "--help" || a === "-h") {
|
|
95
|
+
printUsage();
|
|
96
|
+
return 0;
|
|
97
|
+
}
|
|
98
|
+
else if (a && !a.startsWith("--")) {
|
|
99
|
+
positional.push(a);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
// CLI form: `migrate-gen <serviceName>` — the first positional after the
|
|
103
|
+
// optional "migrate-gen" leading token (when invoked via `npx`).
|
|
104
|
+
const serviceName = positional[0] === "migrate-gen" ? positional[1] : positional[0];
|
|
105
|
+
if (!serviceName) {
|
|
106
|
+
printUsage();
|
|
107
|
+
return 2;
|
|
108
|
+
}
|
|
109
|
+
const result = migrateGen({ serviceName, outDir, stub });
|
|
110
|
+
// eslint-disable-next-line no-console
|
|
111
|
+
console.log(result.path);
|
|
112
|
+
return 0;
|
|
113
|
+
}
|
|
114
|
+
function printUsage() {
|
|
115
|
+
// eslint-disable-next-line no-console
|
|
116
|
+
console.log(`usage: migrate-gen <serviceName> [--out <dir>] [--stub]
|
|
117
|
+
|
|
118
|
+
Emits infra/postgres/004-helpers-and-grants/0001_user_tenant_ids.sql
|
|
119
|
+
(or the stub variant 0000_user_tenant_ids_stub.sql with --stub).
|
|
120
|
+
|
|
121
|
+
Spec: @nodii/db-rls § 5.7 (terraform-bootstrap split).`);
|
|
122
|
+
}
|
|
123
|
+
//# sourceMappingURL=migrate-gen.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"migrate-gen.js","sourceRoot":"","sources":["../src/migrate-gen.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { Pool } from "pg";
|
|
2
|
+
import type { PgPool, PgTx, PoolKind } from "./types";
|
|
3
|
+
export interface PgPoolAdapterOpts {
|
|
4
|
+
/** Underlying pg.Pool. Caller owns lifecycle (creation + .end()). */
|
|
5
|
+
pool: Pool;
|
|
6
|
+
/** Logical pool role — drives error/metric labels. */
|
|
7
|
+
kind: PoolKind;
|
|
8
|
+
/** Acquire-time threshold (ms) above which we surface a slow-acquire
|
|
9
|
+
* telemetry hook. Defaults to 50ms per § 5.10. */
|
|
10
|
+
slowAcquireMs?: number;
|
|
11
|
+
/** Optional hook fired when acquisition exceeds `slowAcquireMs`. Tests
|
|
12
|
+
* hook this to assert the metric path fires. */
|
|
13
|
+
onSlowAcquire?: (info: {
|
|
14
|
+
pool: PoolKind;
|
|
15
|
+
durationMs: number;
|
|
16
|
+
}) => void;
|
|
17
|
+
/** Optional hook fired every time a tx finishes (commit OR rollback).
|
|
18
|
+
* Tests use this to assert lifecycle parity. */
|
|
19
|
+
onTxFinish?: (info: {
|
|
20
|
+
pool: PoolKind;
|
|
21
|
+
outcome: "committed" | "rolled_back" | "error";
|
|
22
|
+
durationMs: number;
|
|
23
|
+
}) => void;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Real PgPool adapter wrapping `pg.Pool`. Caller supplies the underlying
|
|
27
|
+
* pool with the right role credentials (boot validation in
|
|
28
|
+
* `configureDbRls` enforces this).
|
|
29
|
+
*/
|
|
30
|
+
export declare class PgClientPool implements PgPool {
|
|
31
|
+
readonly kind: PoolKind;
|
|
32
|
+
private readonly pool;
|
|
33
|
+
private readonly slowAcquireMs;
|
|
34
|
+
private readonly onSlowAcquire?;
|
|
35
|
+
private readonly onTxFinish?;
|
|
36
|
+
constructor(opts: PgPoolAdapterOpts);
|
|
37
|
+
/** Direct access to the underlying pg.Pool (for ORM clients that need
|
|
38
|
+
* the raw handle — Drizzle's `drizzle(pool)` constructor for instance).
|
|
39
|
+
* Library code does NOT use this for tx work; that path goes through
|
|
40
|
+
* withTransaction so the GUC + release lifecycle is enforced. */
|
|
41
|
+
get rawPool(): Pool;
|
|
42
|
+
withTransaction<T>(fn: (tx: PgTx) => Promise<T>): Promise<T>;
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=pg-pool-adapter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pg-pool-adapter.d.ts","sourceRoot":"","sources":["../src/pg-pool-adapter.ts"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAE,IAAI,EAAc,MAAM,IAAI,CAAC;AAC3C,OAAO,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAEtD,MAAM,WAAW,iBAAiB;IAChC,qEAAqE;IACrE,IAAI,EAAE,IAAI,CAAC;IACX,sDAAsD;IACtD,IAAI,EAAE,QAAQ,CAAC;IACf;uDACmD;IACnD,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;qDACiD;IACjD,aAAa,CAAC,EAAE,CAAC,IAAI,EAAE;QAAE,IAAI,EAAE,QAAQ,CAAC;QAAC,UAAU,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;IACvE;qDACiD;IACjD,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE;QAClB,IAAI,EAAE,QAAQ,CAAC;QACf,OAAO,EAAE,WAAW,GAAG,aAAa,GAAG,OAAO,CAAC;QAC/C,UAAU,EAAE,MAAM,CAAC;KACpB,KAAK,IAAI,CAAC;CACZ;AAsBD;;;;GAIG;AACH,qBAAa,YAAa,YAAW,MAAM;IACzC,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;IACxB,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAO;IAC5B,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAS;IACvC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAC,CAAqC;IACpE,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAkC;gBAElD,IAAI,EAAE,iBAAiB;IAQnC;;;sEAGkE;IAClE,IAAI,OAAO,IAAI,IAAI,CAElB;IAEK,eAAe,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,EAAE,IAAI,KAAK,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;CAgDnE"}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// Real PgPool adapter wrapping a `pg.Pool` instance — implements the
|
|
2
|
+
// PgPool/PgTx surface from src/types.ts so the existing with*Context
|
|
3
|
+
// helpers, in-memory test doubles, and parity-fence asserts all keep
|
|
4
|
+
// working unchanged.
|
|
5
|
+
//
|
|
6
|
+
// Lifecycle per § 5.3:
|
|
7
|
+
// 1. Acquire client from pg.Pool (timed; metric emitted on slow acquire)
|
|
8
|
+
// 2. BEGIN
|
|
9
|
+
// 3. set_config(GUC, value, true) ← caller-driven via PgTx.setConfig
|
|
10
|
+
// 4. yield client to caller
|
|
11
|
+
// 5. COMMIT (or ROLLBACK on throw) + release
|
|
12
|
+
//
|
|
13
|
+
// Pool-leak guard: client.release() ALWAYS runs in the finally block,
|
|
14
|
+
// regardless of commit/rollback outcome. The acquire-timer fires a
|
|
15
|
+
// telemetry span when acquisition exceeds 50ms (§ 5.10).
|
|
16
|
+
class PgClientTx {
|
|
17
|
+
poolKind;
|
|
18
|
+
client;
|
|
19
|
+
constructor(kind, client) {
|
|
20
|
+
this.poolKind = kind;
|
|
21
|
+
this.client = client;
|
|
22
|
+
}
|
|
23
|
+
async exec(sql, params = []) {
|
|
24
|
+
await this.client.query(sql, params);
|
|
25
|
+
}
|
|
26
|
+
async setConfig(key, value) {
|
|
27
|
+
// set_config(name, value, is_local=true) — tx-local per § 5.3.
|
|
28
|
+
// Never expose the false variant; library refuses session-scoped GUCs.
|
|
29
|
+
await this.client.query("SELECT set_config($1, $2, true)", [key, value]);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Real PgPool adapter wrapping `pg.Pool`. Caller supplies the underlying
|
|
34
|
+
* pool with the right role credentials (boot validation in
|
|
35
|
+
* `configureDbRls` enforces this).
|
|
36
|
+
*/
|
|
37
|
+
export class PgClientPool {
|
|
38
|
+
kind;
|
|
39
|
+
pool;
|
|
40
|
+
slowAcquireMs;
|
|
41
|
+
onSlowAcquire;
|
|
42
|
+
onTxFinish;
|
|
43
|
+
constructor(opts) {
|
|
44
|
+
this.kind = opts.kind;
|
|
45
|
+
this.pool = opts.pool;
|
|
46
|
+
this.slowAcquireMs = opts.slowAcquireMs ?? 50;
|
|
47
|
+
this.onSlowAcquire = opts.onSlowAcquire;
|
|
48
|
+
this.onTxFinish = opts.onTxFinish;
|
|
49
|
+
}
|
|
50
|
+
/** Direct access to the underlying pg.Pool (for ORM clients that need
|
|
51
|
+
* the raw handle — Drizzle's `drizzle(pool)` constructor for instance).
|
|
52
|
+
* Library code does NOT use this for tx work; that path goes through
|
|
53
|
+
* withTransaction so the GUC + release lifecycle is enforced. */
|
|
54
|
+
get rawPool() {
|
|
55
|
+
return this.pool;
|
|
56
|
+
}
|
|
57
|
+
async withTransaction(fn) {
|
|
58
|
+
const acquireStart = Date.now();
|
|
59
|
+
const client = await this.pool.connect();
|
|
60
|
+
const acquireMs = Date.now() - acquireStart;
|
|
61
|
+
if (acquireMs >= this.slowAcquireMs && this.onSlowAcquire) {
|
|
62
|
+
try {
|
|
63
|
+
this.onSlowAcquire({ pool: this.kind, durationMs: acquireMs });
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
/* hook errors are non-fatal */
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
const txStart = Date.now();
|
|
70
|
+
let outcome = "error";
|
|
71
|
+
try {
|
|
72
|
+
await client.query("BEGIN");
|
|
73
|
+
const tx = new PgClientTx(this.kind, client);
|
|
74
|
+
let result;
|
|
75
|
+
try {
|
|
76
|
+
result = await fn(tx);
|
|
77
|
+
}
|
|
78
|
+
catch (err) {
|
|
79
|
+
// Rollback then re-throw. Swallow rollback failure (don't double-throw).
|
|
80
|
+
try {
|
|
81
|
+
await client.query("ROLLBACK");
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
/* swallow per § 5.9 */
|
|
85
|
+
}
|
|
86
|
+
outcome = "rolled_back";
|
|
87
|
+
throw err;
|
|
88
|
+
}
|
|
89
|
+
await client.query("COMMIT");
|
|
90
|
+
outcome = "committed";
|
|
91
|
+
return result;
|
|
92
|
+
}
|
|
93
|
+
finally {
|
|
94
|
+
const durationMs = Date.now() - txStart;
|
|
95
|
+
try {
|
|
96
|
+
this.onTxFinish?.({ pool: this.kind, outcome, durationMs });
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
/* hook errors are non-fatal */
|
|
100
|
+
}
|
|
101
|
+
// Pool-leak guard — release ALWAYS runs, even on commit/rollback failure.
|
|
102
|
+
try {
|
|
103
|
+
client.release();
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
/* swallow release errors */
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
//# sourceMappingURL=pg-pool-adapter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pg-pool-adapter.js","sourceRoot":"","sources":["../src/pg-pool-adapter.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export type RlsVerb = "SELECT" | "INSERT" | "UPDATE" | "DELETE";
|
|
2
|
+
export interface WithRlsOpts {
|
|
3
|
+
/** Defaults to `["SELECT", "INSERT"]` (the audit-flavor floor). */
|
|
4
|
+
allow?: readonly RlsVerb[];
|
|
5
|
+
/** Tenant column name; defaults to "tenant_id". */
|
|
6
|
+
tenantColumn?: string;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Emits the policy DDL block for `table`.
|
|
10
|
+
*
|
|
11
|
+
* Returned string ends with `\n` for file-friendliness. Single
|
|
12
|
+
* deterministic output (no whitespace drift, stable ordering) so the
|
|
13
|
+
* parity-fence test is byte-equal.
|
|
14
|
+
*
|
|
15
|
+
* If `table` is schema-qualified (`schema.tbl`), the ALTER + ON clauses
|
|
16
|
+
* use the qualified form; the policy NAME uses only the unqualified
|
|
17
|
+
* tail (Postgres policy names live in the table's namespace and must
|
|
18
|
+
* be a plain identifier, not a dotted path).
|
|
19
|
+
*/
|
|
20
|
+
export declare function withRls(table: string, opts?: WithRlsOpts): string;
|
|
21
|
+
/**
|
|
22
|
+
* Per-table grants — non-owner roles need GRANT on the verbs in `allow`.
|
|
23
|
+
* Owner doesn't need a grant (BYPASSRLS + table-owner).
|
|
24
|
+
*/
|
|
25
|
+
export declare function generateTableGrants(table: string, opts?: WithRlsOpts): string;
|
|
26
|
+
//# sourceMappingURL=rls-policy-gen.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rls-policy-gen.d.ts","sourceRoot":"","sources":["../src/rls-policy-gen.ts"],"names":[],"mappings":"AASA,MAAM,MAAM,OAAO,GAAG,QAAQ,GAAG,QAAQ,GAAG,QAAQ,GAAG,QAAQ,CAAC;AAEhE,MAAM,WAAW,WAAW;IAC1B,mEAAmE;IACnE,KAAK,CAAC,EAAE,SAAS,OAAO,EAAE,CAAC;IAC3B,mDAAmD;IACnD,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAID;;;;;;;;;;;GAWG;AACH,wBAAgB,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,GAAE,WAAgB,GAAG,MAAM,CA0BrE;AAQD;;;GAGG;AACH,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,MAAM,EACb,IAAI,GAAE,WAAgB,GACrB,MAAM,CASR"}
|