@prisma-next/target-postgres 0.14.0-dev.11 → 0.14.0-dev.12
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/dist/canonicalize-Bn3BM1Cn.mjs +46 -0
- package/dist/canonicalize-Bn3BM1Cn.mjs.map +1 -0
- package/dist/canonicalize-CAbXtVtB.d.mts +37 -0
- package/dist/canonicalize-CAbXtVtB.d.mts.map +1 -0
- package/dist/contract-free.d.mts +1 -1
- package/dist/contract-free.d.mts.map +1 -1
- package/dist/contract-free.mjs +2 -2
- package/dist/control.d.mts +1 -1
- package/dist/control.mjs +4 -4
- package/dist/{data-transform-DDgWdB5o.d.mts → data-transform-qKy1U5V9.d.mts} +2 -2
- package/dist/{data-transform-DDgWdB5o.d.mts.map → data-transform-qKy1U5V9.d.mts.map} +1 -1
- package/dist/data-transform.d.mts +1 -1
- package/dist/{ddl-QDyOSeLc.mjs → ddl-BlsTIBeP.mjs} +54 -4
- package/dist/ddl-BlsTIBeP.mjs.map +1 -0
- package/dist/ddl.d.mts +3 -2
- package/dist/ddl.mjs +2 -2
- package/dist/descriptor-meta-b_SzXwen.mjs +239 -0
- package/dist/descriptor-meta-b_SzXwen.mjs.map +1 -0
- package/dist/{issue-planner-DL6g3CmE.mjs → issue-planner-CwoeupPM.mjs} +9 -6
- package/dist/{issue-planner-DL6g3CmE.mjs.map → issue-planner-CwoeupPM.mjs.map} +1 -1
- package/dist/issue-planner.d.mts +3 -3
- package/dist/issue-planner.d.mts.map +1 -1
- package/dist/issue-planner.mjs +1 -1
- package/dist/migration.d.mts +3 -3
- package/dist/migration.mjs +2 -2
- package/dist/{nodes-Bbhs2rwj.mjs → nodes-DhDIIhBT.mjs} +44 -2
- package/dist/nodes-DhDIIhBT.mjs.map +1 -0
- package/dist/{nodes-pLeLgdis.d.mts → nodes-YvdohJ2X.d.mts} +40 -3
- package/dist/nodes-YvdohJ2X.d.mts.map +1 -0
- package/dist/{op-factory-call-DmQEc3XV.d.mts → op-factory-call-DXHAIisl.d.mts} +37 -4
- package/dist/op-factory-call-DXHAIisl.d.mts.map +1 -0
- package/dist/{op-factory-call-D_p5vxwt.mjs → op-factory-call-tMksrGIq.mjs} +146 -4
- package/dist/op-factory-call-tMksrGIq.mjs.map +1 -0
- package/dist/op-factory-call.d.mts +1 -1
- package/dist/op-factory-call.mjs +1 -1
- package/dist/pack.d.mts +74 -1
- package/dist/pack.d.mts.map +1 -1
- package/dist/pack.mjs +1 -1
- package/dist/{planner-Bs_baQax.mjs → planner-BbSZJVW7.mjs} +114 -22
- package/dist/planner-BbSZJVW7.mjs.map +1 -0
- package/dist/{planner-produced-postgres-migration-Cji5vxUf.mjs → planner-produced-postgres-migration-C0FHnEsV.mjs} +2 -2
- package/dist/{planner-produced-postgres-migration-Cji5vxUf.mjs.map → planner-produced-postgres-migration-C0FHnEsV.mjs.map} +1 -1
- package/dist/{planner-produced-postgres-migration-QqHa2C2l.d.mts → planner-produced-postgres-migration-DfM5lBv1.d.mts} +3 -3
- package/dist/{planner-produced-postgres-migration-QqHa2C2l.d.mts.map → planner-produced-postgres-migration-DfM5lBv1.d.mts.map} +1 -1
- package/dist/planner-produced-postgres-migration.d.mts +1 -1
- package/dist/planner-produced-postgres-migration.mjs +1 -1
- package/dist/{planner-sql-checks-jqUUGyQR.mjs → planner-sql-checks-CI6Z21mm.mjs} +2 -2
- package/dist/{planner-sql-checks-jqUUGyQR.mjs.map → planner-sql-checks-CI6Z21mm.mjs.map} +1 -1
- package/dist/planner-sql-checks.mjs +1 -1
- package/dist/{planner-target-details-CIY6tLeo.d.mts → planner-target-details-HkP4RrRO.d.mts} +2 -2
- package/dist/planner-target-details-HkP4RrRO.d.mts.map +1 -0
- package/dist/planner-target-details.d.mts +1 -1
- package/dist/planner.d.mts +35 -3
- package/dist/planner.d.mts.map +1 -1
- package/dist/planner.mjs +2 -2
- package/dist/{postgres-contract-serializer-k3TAcPMY.mjs → postgres-contract-serializer-k5eg5ZUZ.mjs} +29 -16
- package/dist/postgres-contract-serializer-k5eg5ZUZ.mjs.map +1 -0
- package/dist/{postgres-migration-B5jKrXv3.mjs → postgres-migration-BJWxullf.mjs} +2 -2
- package/dist/{postgres-migration-B5jKrXv3.mjs.map → postgres-migration-BJWxullf.mjs.map} +1 -1
- package/dist/{postgres-migration-Y4YBJqkS.d.mts → postgres-migration-D22a2y0l.d.mts} +4 -4
- package/dist/{postgres-migration-Y4YBJqkS.d.mts.map → postgres-migration-D22a2y0l.d.mts.map} +1 -1
- package/dist/postgres-rls-policy-D_z1osEq.d.mts +60 -0
- package/dist/postgres-rls-policy-D_z1osEq.d.mts.map +1 -0
- package/dist/postgres-role-_CCuyPCQ.d.mts +33 -0
- package/dist/postgres-role-_CCuyPCQ.d.mts.map +1 -0
- package/dist/{postgres-schema-COGZ1ark.mjs → postgres-schema-BVDr_61a.mjs} +139 -3
- package/dist/postgres-schema-BVDr_61a.mjs.map +1 -0
- package/dist/postgres-schema-D0h4lsw2.d.mts +158 -0
- package/dist/postgres-schema-D0h4lsw2.d.mts.map +1 -0
- package/dist/postgres-schema-ir-BF0X8jit.mjs +66 -0
- package/dist/postgres-schema-ir-BF0X8jit.mjs.map +1 -0
- package/dist/postgres-schema-ir-BQ3RpZMz.d.mts +60 -0
- package/dist/postgres-schema-ir-BQ3RpZMz.d.mts.map +1 -0
- package/dist/postgres-schema-ir-annotations-El62Sywa.mjs +14 -0
- package/dist/postgres-schema-ir-annotations-El62Sywa.mjs.map +1 -0
- package/dist/render-ops.d.mts +1 -1
- package/dist/rls-canonicalize.d.mts +2 -0
- package/dist/rls-canonicalize.mjs +2 -0
- package/dist/runtime.d.mts +3 -2
- package/dist/runtime.d.mts.map +1 -1
- package/dist/runtime.mjs +2 -2
- package/dist/schema-ir-annotations.d.mts +8 -0
- package/dist/schema-ir-annotations.d.mts.map +1 -0
- package/dist/schema-ir-annotations.mjs +2 -0
- package/dist/types.d.mts +5 -140
- package/dist/types.mjs +3 -2
- package/package.json +21 -17
- package/src/contract-free/checks.ts +73 -0
- package/src/contract-free/ddl.ts +35 -0
- package/src/core/authoring.ts +115 -1
- package/src/core/ddl/nodes.ts +68 -1
- package/src/core/descriptor-meta.ts +4 -0
- package/src/core/entity-kinds.ts +16 -0
- package/src/core/migrations/control-policy.ts +3 -0
- package/src/core/migrations/issue-planner.ts +8 -0
- package/src/core/migrations/op-factory-call.ts +96 -0
- package/src/core/migrations/operations/rls.ts +106 -0
- package/src/core/migrations/planner-target-details.ts +3 -1
- package/src/core/migrations/planner.ts +99 -1
- package/src/core/migrations/verify-postgres-namespaces.ts +12 -15
- package/src/core/migrations/verify-postgres-rls-policies.ts +62 -0
- package/src/core/postgres-contract-serializer.ts +43 -19
- package/src/core/postgres-rls-policy.ts +103 -0
- package/src/core/postgres-role.ts +53 -0
- package/src/core/postgres-schema-ir-annotations.ts +22 -0
- package/src/core/postgres-schema-ir.ts +101 -0
- package/src/core/postgres-schema.ts +24 -4
- package/src/core/postgres-validators.ts +20 -0
- package/src/core/rls/canonicalize.ts +48 -0
- package/src/exports/ddl.ts +3 -0
- package/src/exports/planner.ts +1 -0
- package/src/exports/rls-canonicalize.ts +6 -0
- package/src/exports/schema-ir-annotations.ts +1 -0
- package/src/exports/types.ts +12 -0
- package/dist/ddl-QDyOSeLc.mjs.map +0 -1
- package/dist/descriptor-meta-CpGygXpI.mjs +0 -140
- package/dist/descriptor-meta-CpGygXpI.mjs.map +0 -1
- package/dist/nodes-Bbhs2rwj.mjs.map +0 -1
- package/dist/nodes-pLeLgdis.d.mts.map +0 -1
- package/dist/op-factory-call-D_p5vxwt.mjs.map +0 -1
- package/dist/op-factory-call-DmQEc3XV.d.mts.map +0 -1
- package/dist/planner-Bs_baQax.mjs.map +0 -1
- package/dist/planner-target-details-CIY6tLeo.d.mts.map +0 -1
- package/dist/postgres-contract-serializer-k3TAcPMY.mjs.map +0 -1
- package/dist/postgres-schema-COGZ1ark.mjs.map +0 -1
- package/dist/types.d.mts.map +0 -1
|
@@ -345,6 +345,79 @@ export function extensionExistsAst(extensionName: string): ExtensionExistsCheckB
|
|
|
345
345
|
};
|
|
346
346
|
}
|
|
347
347
|
|
|
348
|
+
export interface RlsPolicyExistsCheckBuilder {
|
|
349
|
+
policyPresent(): SelectAst;
|
|
350
|
+
policyAbsent(): SelectAst;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Typed RLS-policy existence check over `pg_policies`, with schema, table,
|
|
355
|
+
* and policy names bound as text parameters (never inlined). The `schema`
|
|
356
|
+
* coordinate is the resolved live-database schema name (e.g. `'public'`),
|
|
357
|
+
* compared against `pg_policies.schemaname` as a bound parameter.
|
|
358
|
+
*/
|
|
359
|
+
export function rlsPolicyExistsAst(options: {
|
|
360
|
+
readonly schema: string;
|
|
361
|
+
readonly table: string;
|
|
362
|
+
readonly policyName: string;
|
|
363
|
+
}): RlsPolicyExistsCheckBuilder {
|
|
364
|
+
const inner = () =>
|
|
365
|
+
exprSelect()
|
|
366
|
+
.from(cfTable('pg_policies'))
|
|
367
|
+
.project('one', cfExpr.lit(1))
|
|
368
|
+
.where(
|
|
369
|
+
cfExpr.allOf([
|
|
370
|
+
cfExpr.identifierRef('schemaname').eqParam(options.schema, PG_TEXT_CODEC_ID),
|
|
371
|
+
cfExpr.identifierRef('tablename').eqParam(options.table, PG_TEXT_CODEC_ID),
|
|
372
|
+
cfExpr.identifierRef('policyname').eqParam(options.policyName, PG_TEXT_CODEC_ID),
|
|
373
|
+
]),
|
|
374
|
+
);
|
|
375
|
+
return {
|
|
376
|
+
policyPresent: () => exprSelect().project('result', cfExpr.exists(inner())).build(),
|
|
377
|
+
policyAbsent: () => exprSelect().project('result', cfExpr.notExists(inner())).build(),
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
export interface RlsEnabledCheckBuilder {
|
|
382
|
+
rlsEnabled(): SelectAst;
|
|
383
|
+
rlsDisabled(): SelectAst;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Typed row-level-security enabled check over `pg_class` joined to
|
|
388
|
+
* `pg_namespace`, comparing `pg_class.relrowsecurity` against the expected
|
|
389
|
+
* boolean. Schema and table names are bound as text parameters.
|
|
390
|
+
*/
|
|
391
|
+
export function rlsEnabledAst(schema: string, table: string): RlsEnabledCheckBuilder {
|
|
392
|
+
const inner = (enabled: boolean) =>
|
|
393
|
+
exprSelect()
|
|
394
|
+
.from(cfTable('pg_class', 'c'))
|
|
395
|
+
.join(
|
|
396
|
+
cfTable('pg_namespace', 'n'),
|
|
397
|
+
cfExpr.columnRef('n', 'oid').eqExpr(cfExpr.columnRef('c', 'relnamespace')),
|
|
398
|
+
)
|
|
399
|
+
.project('one', cfExpr.lit(1))
|
|
400
|
+
.where(
|
|
401
|
+
cfExpr.allOf([
|
|
402
|
+
cfExpr.columnRef('n', 'nspname').eqParam(schema, PG_TEXT_CODEC_ID),
|
|
403
|
+
cfExpr.columnRef('c', 'relname').eqParam(table, PG_TEXT_CODEC_ID),
|
|
404
|
+
enabled
|
|
405
|
+
? cfExpr.columnRef('c', 'relrowsecurity')
|
|
406
|
+
: cfExpr.columnRef('c', 'relrowsecurity').not(),
|
|
407
|
+
]),
|
|
408
|
+
);
|
|
409
|
+
return {
|
|
410
|
+
rlsEnabled: () =>
|
|
411
|
+
exprSelect()
|
|
412
|
+
.project('result', cfExpr.exists(inner(true)))
|
|
413
|
+
.build(),
|
|
414
|
+
rlsDisabled: () =>
|
|
415
|
+
exprSelect()
|
|
416
|
+
.project('result', cfExpr.exists(inner(false)))
|
|
417
|
+
.build(),
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
|
|
348
421
|
export interface IndexExistsCheckBuilder {
|
|
349
422
|
indexPresent(): SelectAst;
|
|
350
423
|
indexAbsent(): SelectAst;
|
package/src/contract-free/ddl.ts
CHANGED
|
@@ -3,8 +3,11 @@ import {
|
|
|
3
3
|
AddColumnAction,
|
|
4
4
|
type AnyAlterTableAction,
|
|
5
5
|
PostgresAlterTable,
|
|
6
|
+
PostgresCreatePolicy,
|
|
6
7
|
PostgresCreateSchema,
|
|
7
8
|
PostgresCreateTable,
|
|
9
|
+
PostgresDropPolicy,
|
|
10
|
+
type RlsPolicyOperation,
|
|
8
11
|
} from '../core/ddl/nodes';
|
|
9
12
|
|
|
10
13
|
/**
|
|
@@ -62,3 +65,35 @@ export function alterTable(options: {
|
|
|
62
65
|
}): PostgresAlterTable {
|
|
63
66
|
return new PostgresAlterTable(options);
|
|
64
67
|
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Build a Postgres `CREATE POLICY` DDL node.
|
|
71
|
+
*
|
|
72
|
+
* Identifiers (`schema`, `table`, `name`) are quoted by the renderer.
|
|
73
|
+
* `using` and `withCheck` are emitted verbatim as predicate SQL — callers
|
|
74
|
+
* must supply safe, pre-validated SQL expressions.
|
|
75
|
+
*/
|
|
76
|
+
export function createPolicy(options: {
|
|
77
|
+
readonly schema: string;
|
|
78
|
+
readonly table: string;
|
|
79
|
+
readonly name: string;
|
|
80
|
+
readonly permissive: boolean;
|
|
81
|
+
readonly operation: RlsPolicyOperation;
|
|
82
|
+
readonly roles: readonly string[];
|
|
83
|
+
readonly using?: string;
|
|
84
|
+
readonly withCheck?: string;
|
|
85
|
+
}): PostgresCreatePolicy {
|
|
86
|
+
return new PostgresCreatePolicy(options);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Build a Postgres `DROP POLICY` DDL node.
|
|
91
|
+
* Identifiers (`schema`, `table`, `name`) are quoted by the renderer.
|
|
92
|
+
*/
|
|
93
|
+
export function dropPolicy(options: {
|
|
94
|
+
readonly schema: string;
|
|
95
|
+
readonly table: string;
|
|
96
|
+
readonly name: string;
|
|
97
|
+
}): PostgresDropPolicy {
|
|
98
|
+
return new PostgresDropPolicy(options);
|
|
99
|
+
}
|
package/src/core/authoring.ts
CHANGED
|
@@ -1,13 +1,94 @@
|
|
|
1
1
|
import { temporalAuthoringPresets } from '@prisma-next/family-sql/control';
|
|
2
2
|
import type {
|
|
3
|
+
AuthoringEntityContext,
|
|
3
4
|
AuthoringEntityTypeNamespace,
|
|
4
5
|
AuthoringFieldNamespace,
|
|
6
|
+
AuthoringPslBlockDescriptorNamespace,
|
|
5
7
|
AuthoringTypeNamespace,
|
|
8
|
+
PslExtensionBlock,
|
|
6
9
|
} from '@prisma-next/framework-components/authoring';
|
|
10
|
+
import { PostgresRlsPolicy } from './postgres-rls-policy';
|
|
11
|
+
import { PostgresRole, type PostgresRoleInput } from './postgres-role';
|
|
12
|
+
import { PostgresRlsPolicySchema, PostgresRoleSchema } from './postgres-validators';
|
|
13
|
+
import { computeContentHash, normalizePredicate } from './rls/canonicalize';
|
|
7
14
|
|
|
8
15
|
export const postgresAuthoringTypes = {} as const satisfies AuthoringTypeNamespace;
|
|
9
16
|
|
|
10
|
-
export
|
|
17
|
+
export interface RlsPolicyExtensionBlock extends PslExtensionBlock {
|
|
18
|
+
readonly namespaceId: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function readRefParam(block: PslExtensionBlock, key: string): string | undefined {
|
|
22
|
+
const param = block.parameters[key];
|
|
23
|
+
return param?.kind === 'ref' ? param.identifier : undefined;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function readValueParam(block: PslExtensionBlock, key: string): string | undefined {
|
|
27
|
+
const param = block.parameters[key];
|
|
28
|
+
return param?.kind === 'value' ? param.raw : undefined;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function readListRefParams(block: PslExtensionBlock, key: string): string[] {
|
|
32
|
+
const param = block.parameters[key];
|
|
33
|
+
if (param?.kind !== 'list') return [];
|
|
34
|
+
return param.items.flatMap((item) => (item.kind === 'ref' ? [item.identifier] : []));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function unwrapQuotedString(raw: string): string {
|
|
38
|
+
if (raw.startsWith('"') && raw.endsWith('"') && raw.length >= 2) {
|
|
39
|
+
return raw.slice(1, -1).replace(/\\"/g, '"');
|
|
40
|
+
}
|
|
41
|
+
return raw;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function lowerRlsPolicyFromBlock(
|
|
45
|
+
block: RlsPolicyExtensionBlock,
|
|
46
|
+
_ctx: AuthoringEntityContext,
|
|
47
|
+
): PostgresRlsPolicy {
|
|
48
|
+
const prefix = block.name;
|
|
49
|
+
const targetModelName = readRefParam(block, 'target') ?? '';
|
|
50
|
+
const tableName = targetModelName.charAt(0).toLowerCase() + targetModelName.slice(1);
|
|
51
|
+
const roles = [...readListRefParams(block, 'roles')].sort();
|
|
52
|
+
const using = unwrapQuotedString(readValueParam(block, 'using') ?? '');
|
|
53
|
+
|
|
54
|
+
const wireHash = computeContentHash({
|
|
55
|
+
using: normalizePredicate(using),
|
|
56
|
+
roles,
|
|
57
|
+
operation: 'select',
|
|
58
|
+
permissive: true,
|
|
59
|
+
});
|
|
60
|
+
const wireName = `${prefix}_${wireHash}`;
|
|
61
|
+
|
|
62
|
+
return new PostgresRlsPolicy({
|
|
63
|
+
name: wireName,
|
|
64
|
+
prefix,
|
|
65
|
+
tableName,
|
|
66
|
+
namespaceId: block.namespaceId,
|
|
67
|
+
operation: 'select',
|
|
68
|
+
roles,
|
|
69
|
+
using,
|
|
70
|
+
permissive: true,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export const postgresAuthoringEntityTypes = {
|
|
75
|
+
role: {
|
|
76
|
+
kind: 'entity',
|
|
77
|
+
discriminator: 'role',
|
|
78
|
+
validatorSchema: PostgresRoleSchema,
|
|
79
|
+
output: {
|
|
80
|
+
factory: (input: PostgresRoleInput): PostgresRole => new PostgresRole(input),
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
policy: {
|
|
84
|
+
kind: 'entity',
|
|
85
|
+
discriminator: 'policy',
|
|
86
|
+
validatorSchema: PostgresRlsPolicySchema,
|
|
87
|
+
output: {
|
|
88
|
+
factory: lowerRlsPolicyFromBlock,
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
} as const satisfies AuthoringEntityTypeNamespace;
|
|
11
92
|
|
|
12
93
|
/**
|
|
13
94
|
* Field presets contributed by the Postgres target pack.
|
|
@@ -22,6 +103,39 @@ export const postgresAuthoringEntityTypes = {} as const satisfies AuthoringEntit
|
|
|
22
103
|
* portability use `uuidString` / `id.uuidv4String` / `id.uuidv7String` from
|
|
23
104
|
* the family pack instead.
|
|
24
105
|
*/
|
|
106
|
+
/**
|
|
107
|
+
* PSL block descriptor for `policy_select`.
|
|
108
|
+
*
|
|
109
|
+
* The parser learns the block shape from this descriptor; lowering from
|
|
110
|
+
* `PslExtensionBlock` to `PostgresRlsPolicy` is wired in the PSL
|
|
111
|
+
* interpreter (a later dispatch). The `discriminator` matches
|
|
112
|
+
* `PostgresRlsPolicy.kind` so the parsed block node carries the same
|
|
113
|
+
* discriminant as the IR class it will lower to.
|
|
114
|
+
*
|
|
115
|
+
* The `roles` list uses `scope:'cross-space'` because same-namespace
|
|
116
|
+
* role ref resolution requires PSL namespace entries keyed by `refKind`
|
|
117
|
+
* (i.e. `'role'`), which in turn requires the role block discriminator to
|
|
118
|
+
* equal `'role'`. Aligning discriminator with refKind is tracked for
|
|
119
|
+
* slice 4 (cross-space roles). Until then cross-space passes validation
|
|
120
|
+
* unconditionally and the authored role names flow through unchanged.
|
|
121
|
+
*/
|
|
122
|
+
export const postgresAuthoringPslBlockDescriptors = {
|
|
123
|
+
policy_select: {
|
|
124
|
+
kind: 'pslBlock',
|
|
125
|
+
keyword: 'policy_select',
|
|
126
|
+
discriminator: 'policy',
|
|
127
|
+
name: { required: true },
|
|
128
|
+
parameters: {
|
|
129
|
+
target: { kind: 'ref', refKind: 'model', scope: 'same-namespace', required: true },
|
|
130
|
+
roles: {
|
|
131
|
+
kind: 'list',
|
|
132
|
+
of: { kind: 'ref', refKind: 'role', scope: 'cross-space' },
|
|
133
|
+
},
|
|
134
|
+
using: { kind: 'value', codecId: 'pg/text@1', required: true },
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
} as const satisfies AuthoringPslBlockDescriptorNamespace;
|
|
138
|
+
|
|
25
139
|
export const postgresAuthoringFieldPresets = {
|
|
26
140
|
text: {
|
|
27
141
|
kind: 'fieldPreset',
|
package/src/core/ddl/nodes.ts
CHANGED
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
DdlNode,
|
|
4
4
|
type DdlTableConstraint,
|
|
5
5
|
} from '@prisma-next/sql-relational-core/ast';
|
|
6
|
+
import type { RlsPolicyOperation } from '../rls/canonicalize';
|
|
6
7
|
|
|
7
8
|
// ---------------------------------------------------------------------------
|
|
8
9
|
// AlterTableAction — nested polymorphic hierarchy
|
|
@@ -42,6 +43,8 @@ export interface PostgresDdlVisitor<R> {
|
|
|
42
43
|
createTable(node: PostgresCreateTable): R;
|
|
43
44
|
createSchema(node: PostgresCreateSchema): R;
|
|
44
45
|
alterTable(node: PostgresAlterTable): R;
|
|
46
|
+
createPolicy(node: PostgresCreatePolicy): R;
|
|
47
|
+
dropPolicy(node: PostgresDropPolicy): R;
|
|
45
48
|
}
|
|
46
49
|
|
|
47
50
|
export abstract class PostgresDdlNode extends DdlNode {
|
|
@@ -127,4 +130,68 @@ export class PostgresAlterTable extends PostgresDdlNode {
|
|
|
127
130
|
}
|
|
128
131
|
}
|
|
129
132
|
|
|
130
|
-
export type
|
|
133
|
+
export type { RlsPolicyOperation };
|
|
134
|
+
|
|
135
|
+
export class PostgresCreatePolicy extends PostgresDdlNode {
|
|
136
|
+
readonly kind = 'create-policy' as const;
|
|
137
|
+
readonly schema: string;
|
|
138
|
+
readonly table: string;
|
|
139
|
+
readonly name: string;
|
|
140
|
+
readonly permissive: boolean;
|
|
141
|
+
readonly operation: RlsPolicyOperation;
|
|
142
|
+
readonly roles: ReadonlyArray<string>;
|
|
143
|
+
readonly using: string | undefined;
|
|
144
|
+
readonly withCheck: string | undefined;
|
|
145
|
+
|
|
146
|
+
constructor(options: {
|
|
147
|
+
readonly schema: string;
|
|
148
|
+
readonly table: string;
|
|
149
|
+
readonly name: string;
|
|
150
|
+
readonly permissive: boolean;
|
|
151
|
+
readonly operation: RlsPolicyOperation;
|
|
152
|
+
readonly roles: readonly string[];
|
|
153
|
+
readonly using?: string;
|
|
154
|
+
readonly withCheck?: string;
|
|
155
|
+
}) {
|
|
156
|
+
super();
|
|
157
|
+
this.schema = options.schema;
|
|
158
|
+
this.table = options.table;
|
|
159
|
+
this.name = options.name;
|
|
160
|
+
this.permissive = options.permissive;
|
|
161
|
+
this.operation = options.operation;
|
|
162
|
+
this.roles = Object.freeze([...options.roles]);
|
|
163
|
+
this.using = options.using;
|
|
164
|
+
this.withCheck = options.withCheck;
|
|
165
|
+
this.freeze();
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
override accept<R>(visitor: PostgresDdlVisitor<R>): R {
|
|
169
|
+
return visitor.createPolicy(this);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export class PostgresDropPolicy extends PostgresDdlNode {
|
|
174
|
+
readonly kind = 'drop-policy' as const;
|
|
175
|
+
readonly schema: string;
|
|
176
|
+
readonly table: string;
|
|
177
|
+
readonly name: string;
|
|
178
|
+
|
|
179
|
+
constructor(options: { readonly schema: string; readonly table: string; readonly name: string }) {
|
|
180
|
+
super();
|
|
181
|
+
this.schema = options.schema;
|
|
182
|
+
this.table = options.table;
|
|
183
|
+
this.name = options.name;
|
|
184
|
+
this.freeze();
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
override accept<R>(visitor: PostgresDdlVisitor<R>): R {
|
|
188
|
+
return visitor.dropPolicy(this);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export type AnyPostgresDdlNode =
|
|
193
|
+
| PostgresCreateTable
|
|
194
|
+
| PostgresCreateSchema
|
|
195
|
+
| PostgresAlterTable
|
|
196
|
+
| PostgresCreatePolicy
|
|
197
|
+
| PostgresDropPolicy;
|
|
@@ -2,9 +2,11 @@ import type { CodecTypes } from '../exports/codec-types';
|
|
|
2
2
|
import {
|
|
3
3
|
postgresAuthoringEntityTypes,
|
|
4
4
|
postgresAuthoringFieldPresets,
|
|
5
|
+
postgresAuthoringPslBlockDescriptors,
|
|
5
6
|
postgresAuthoringTypes,
|
|
6
7
|
} from './authoring';
|
|
7
8
|
import { postgresTargetDescriptorMetaRuntime } from './descriptor-meta-runtime';
|
|
9
|
+
import { postgresCreateNamespace } from './postgres-schema';
|
|
8
10
|
|
|
9
11
|
const postgresTargetDescriptorMetaBase = {
|
|
10
12
|
...postgresTargetDescriptorMetaRuntime,
|
|
@@ -13,6 +15,8 @@ const postgresTargetDescriptorMetaBase = {
|
|
|
13
15
|
type: postgresAuthoringTypes,
|
|
14
16
|
field: postgresAuthoringFieldPresets,
|
|
15
17
|
entityTypes: postgresAuthoringEntityTypes,
|
|
18
|
+
pslBlockDescriptors: postgresAuthoringPslBlockDescriptors,
|
|
19
|
+
createNamespace: postgresCreateNamespace,
|
|
16
20
|
},
|
|
17
21
|
} as const;
|
|
18
22
|
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { EntityKindDescriptor } from '@prisma-next/framework-components/ir';
|
|
2
|
+
import { PostgresRlsPolicy, type PostgresRlsPolicyInput } from './postgres-rls-policy';
|
|
3
|
+
import { PostgresRole, type PostgresRoleInput } from './postgres-role';
|
|
4
|
+
import { PostgresRlsPolicySchema, PostgresRoleSchema } from './postgres-validators';
|
|
5
|
+
|
|
6
|
+
export const policyEntityKind: EntityKindDescriptor<PostgresRlsPolicyInput, PostgresRlsPolicy> = {
|
|
7
|
+
kind: 'policy',
|
|
8
|
+
schema: PostgresRlsPolicySchema,
|
|
9
|
+
construct: (input) => new PostgresRlsPolicy(input),
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export const roleEntityKind: EntityKindDescriptor<PostgresRoleInput, PostgresRole> = {
|
|
13
|
+
kind: 'role',
|
|
14
|
+
schema: PostgresRoleSchema,
|
|
15
|
+
construct: (input) => new PostgresRole(input),
|
|
16
|
+
};
|
|
@@ -21,6 +21,8 @@ import type { PostgresOpFactoryCall } from './op-factory-call';
|
|
|
21
21
|
const OBJECT_CREATION_FACTORIES: ReadonlySet<string> = new Set<string>([
|
|
22
22
|
'createTable',
|
|
23
23
|
'createSchema',
|
|
24
|
+
'createRlsPolicy',
|
|
25
|
+
'enableRowLevelSecurity',
|
|
24
26
|
]);
|
|
25
27
|
|
|
26
28
|
function createsNewTopLevelObject(call: PostgresOpFactoryCall): boolean {
|
|
@@ -151,6 +153,7 @@ export function resolvePostgresCallControlPolicySubject(
|
|
|
151
153
|
const POSTGRES_ISSUE_CREATION_FACTORY: Readonly<Record<string, string>> = Object.freeze({
|
|
152
154
|
missing_schema: 'createSchema',
|
|
153
155
|
missing_table: 'createTable',
|
|
156
|
+
missing_rls_policy: 'createRlsPolicy',
|
|
154
157
|
});
|
|
155
158
|
|
|
156
159
|
export function resolvePostgresIssueCreationFactoryName(issue: SchemaIssue): string | undefined {
|
|
@@ -695,6 +695,8 @@ type CallCategory =
|
|
|
695
695
|
| 'dep'
|
|
696
696
|
| 'drop'
|
|
697
697
|
| 'table'
|
|
698
|
+
| 'rlsEnable'
|
|
699
|
+
| 'rlsPolicy'
|
|
698
700
|
| 'column'
|
|
699
701
|
| 'alter'
|
|
700
702
|
| 'primaryKey'
|
|
@@ -724,6 +726,12 @@ function classifyCall(call: PostgresOpFactoryCall): CallCategory {
|
|
|
724
726
|
return 'unique'; // after uniques, before indexes
|
|
725
727
|
case 'createTable':
|
|
726
728
|
return 'table';
|
|
729
|
+
case 'enableRowLevelSecurity':
|
|
730
|
+
return 'rlsEnable';
|
|
731
|
+
case 'createRlsPolicy':
|
|
732
|
+
return 'rlsPolicy';
|
|
733
|
+
case 'dropRlsPolicy':
|
|
734
|
+
return 'drop';
|
|
727
735
|
case 'addColumn':
|
|
728
736
|
return 'column';
|
|
729
737
|
case 'alterColumnType':
|
|
@@ -40,6 +40,7 @@ import { blindCast } from '@prisma-next/utils/casts';
|
|
|
40
40
|
import { ifDefined } from '@prisma-next/utils/defined';
|
|
41
41
|
import { columnExistsAst, tableExistsAst } from '../../contract-free/checks';
|
|
42
42
|
import * as contractFreeDdl from '../../contract-free/ddl';
|
|
43
|
+
import type { PostgresRlsPolicy } from '../postgres-rls-policy';
|
|
43
44
|
import { escapeLiteral, quoteIdentifier } from '../sql-utils';
|
|
44
45
|
import type { PostgresColumnDefault } from '../types';
|
|
45
46
|
import {
|
|
@@ -61,6 +62,7 @@ import {
|
|
|
61
62
|
} from './operations/constraints';
|
|
62
63
|
import { createExtension } from './operations/dependencies';
|
|
63
64
|
import { createIndex, dropIndex } from './operations/indexes';
|
|
65
|
+
import { createRlsPolicy, dropRlsPolicy, enableRowLevelSecurity } from './operations/rls';
|
|
64
66
|
import type { ForeignKeySpec } from './operations/shared';
|
|
65
67
|
import { step, targetDetails } from './operations/shared';
|
|
66
68
|
import { dropTable } from './operations/tables';
|
|
@@ -1376,6 +1378,97 @@ export class DataTransformCall extends PostgresOpFactoryCallNode {
|
|
|
1376
1378
|
}
|
|
1377
1379
|
}
|
|
1378
1380
|
|
|
1381
|
+
export class CreatePostgresRlsPolicyCall extends PostgresOpFactoryCallNode {
|
|
1382
|
+
readonly factoryName = 'createRlsPolicy' as const;
|
|
1383
|
+
readonly operationClass = 'additive' as const;
|
|
1384
|
+
readonly schemaName: string;
|
|
1385
|
+
readonly tableName: string;
|
|
1386
|
+
readonly policy: PostgresRlsPolicy;
|
|
1387
|
+
readonly label: string;
|
|
1388
|
+
|
|
1389
|
+
constructor(schemaName: string, tableName: string, policy: PostgresRlsPolicy) {
|
|
1390
|
+
super();
|
|
1391
|
+
this.schemaName = schemaName;
|
|
1392
|
+
this.tableName = tableName;
|
|
1393
|
+
this.policy = policy;
|
|
1394
|
+
this.label = `Create RLS policy "${policy.name}" on "${tableName}"`;
|
|
1395
|
+
this.freeze();
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
async toOp(lowerer?: ExecuteRequestLowerer): Promise<Op> {
|
|
1399
|
+
if (lowerer === undefined) {
|
|
1400
|
+
throw new Error(
|
|
1401
|
+
`CreatePostgresRlsPolicyCall.toOp: a lowerer is required on the Postgres planner path (policy "${this.policy.name}" on table "${this.tableName}"). Pass the control adapter to createPostgresMigrationPlanner.`,
|
|
1402
|
+
);
|
|
1403
|
+
}
|
|
1404
|
+
return createRlsPolicy(this.schemaName, this.tableName, this.policy, lowerer);
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
renderTypeScript(): string {
|
|
1408
|
+
return `createRlsPolicy(${jsonToTsSource(this.schemaName)}, ${jsonToTsSource(this.tableName)}, ${jsonToTsSource(this.policy)})`;
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
export class DropPostgresRlsPolicyCall extends PostgresOpFactoryCallNode {
|
|
1413
|
+
readonly factoryName = 'dropRlsPolicy' as const;
|
|
1414
|
+
readonly operationClass = 'destructive' as const;
|
|
1415
|
+
readonly schemaName: string;
|
|
1416
|
+
readonly tableName: string;
|
|
1417
|
+
readonly policyName: string;
|
|
1418
|
+
readonly label: string;
|
|
1419
|
+
|
|
1420
|
+
constructor(schemaName: string, tableName: string, policyName: string) {
|
|
1421
|
+
super();
|
|
1422
|
+
this.schemaName = schemaName;
|
|
1423
|
+
this.tableName = tableName;
|
|
1424
|
+
this.policyName = policyName;
|
|
1425
|
+
this.label = `Drop RLS policy "${policyName}" on "${tableName}"`;
|
|
1426
|
+
this.freeze();
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
async toOp(lowerer?: ExecuteRequestLowerer): Promise<Op> {
|
|
1430
|
+
if (lowerer === undefined) {
|
|
1431
|
+
throw new Error(
|
|
1432
|
+
`DropPostgresRlsPolicyCall.toOp: a lowerer is required on the Postgres planner path (policy "${this.policyName}" on table "${this.tableName}"). Pass the control adapter to createPostgresMigrationPlanner.`,
|
|
1433
|
+
);
|
|
1434
|
+
}
|
|
1435
|
+
return dropRlsPolicy(this.schemaName, this.tableName, this.policyName, lowerer);
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
renderTypeScript(): string {
|
|
1439
|
+
return `dropRlsPolicy(${jsonToTsSource(this.schemaName)}, ${jsonToTsSource(this.tableName)}, ${jsonToTsSource(this.policyName)})`;
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
export class EnableRowLevelSecurityCall extends PostgresOpFactoryCallNode {
|
|
1444
|
+
readonly factoryName = 'enableRowLevelSecurity' as const;
|
|
1445
|
+
readonly operationClass = 'additive' as const;
|
|
1446
|
+
readonly schemaName: string;
|
|
1447
|
+
readonly tableName: string;
|
|
1448
|
+
readonly label: string;
|
|
1449
|
+
|
|
1450
|
+
constructor(schemaName: string, tableName: string) {
|
|
1451
|
+
super();
|
|
1452
|
+
this.schemaName = schemaName;
|
|
1453
|
+
this.tableName = tableName;
|
|
1454
|
+
this.label = `Enable row-level security on "${tableName}"`;
|
|
1455
|
+
this.freeze();
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
async toOp(lowerer?: ExecuteRequestLowerer): Promise<Op> {
|
|
1459
|
+
if (lowerer === undefined) {
|
|
1460
|
+
throw new Error(
|
|
1461
|
+
`EnableRowLevelSecurityCall.toOp: a lowerer is required on the Postgres planner path (table "${this.tableName}"). Pass the control adapter to createPostgresMigrationPlanner.`,
|
|
1462
|
+
);
|
|
1463
|
+
}
|
|
1464
|
+
return enableRowLevelSecurity(this.schemaName, this.tableName, lowerer);
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
renderTypeScript(): string {
|
|
1468
|
+
return `enableRowLevelSecurity(${jsonToTsSource(this.schemaName)}, ${jsonToTsSource(this.tableName)})`;
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1379
1472
|
export type PostgresOpFactoryCall =
|
|
1380
1473
|
| CreateTableCall
|
|
1381
1474
|
| DropTableCall
|
|
@@ -1399,4 +1492,7 @@ export type PostgresOpFactoryCall =
|
|
|
1399
1492
|
| RawSqlCall
|
|
1400
1493
|
| CreateExtensionCall
|
|
1401
1494
|
| CreateSchemaCall
|
|
1495
|
+
| CreatePostgresRlsPolicyCall
|
|
1496
|
+
| DropPostgresRlsPolicyCall
|
|
1497
|
+
| EnableRowLevelSecurityCall
|
|
1402
1498
|
| DataTransformCall;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import type { ExecuteRequestLowerer } from '@prisma-next/family-sql/control-adapter';
|
|
2
|
+
import { ifDefined } from '@prisma-next/utils/defined';
|
|
3
|
+
import { rlsEnabledAst, rlsPolicyExistsAst } from '../../../contract-free/checks';
|
|
4
|
+
import { createPolicy, dropPolicy } from '../../../contract-free/ddl';
|
|
5
|
+
import type { PostgresRlsPolicy } from '../../postgres-rls-policy';
|
|
6
|
+
import { qualifyTableName } from '../planner-sql-checks';
|
|
7
|
+
import { type Op, step, targetDetails } from './shared';
|
|
8
|
+
|
|
9
|
+
const PLAIN_IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_$]*$/;
|
|
10
|
+
|
|
11
|
+
function validateRoleName(role: string): string {
|
|
12
|
+
if (!PLAIN_IDENTIFIER.test(role)) {
|
|
13
|
+
throw new Error(
|
|
14
|
+
`Invalid role name ${JSON.stringify(role)}: role names must be plain SQL identifiers matching ^[A-Za-z_][A-Za-z0-9_$]*$`,
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
return role;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function createRlsPolicy(
|
|
21
|
+
schemaName: string,
|
|
22
|
+
tableName: string,
|
|
23
|
+
policy: PostgresRlsPolicy,
|
|
24
|
+
lowerer: ExecuteRequestLowerer,
|
|
25
|
+
): Promise<Op> {
|
|
26
|
+
const validatedRoles = policy.roles.map(validateRoleName);
|
|
27
|
+
const ddlNode = createPolicy({
|
|
28
|
+
schema: schemaName,
|
|
29
|
+
table: tableName,
|
|
30
|
+
name: policy.name,
|
|
31
|
+
permissive: policy.permissive,
|
|
32
|
+
operation: policy.operation,
|
|
33
|
+
roles: validatedRoles,
|
|
34
|
+
...ifDefined('using', policy.using),
|
|
35
|
+
...ifDefined('withCheck', policy.withCheck),
|
|
36
|
+
});
|
|
37
|
+
const checks = rlsPolicyExistsAst({
|
|
38
|
+
schema: schemaName,
|
|
39
|
+
table: tableName,
|
|
40
|
+
policyName: policy.name,
|
|
41
|
+
});
|
|
42
|
+
const absent = await lowerer.lowerToExecuteRequest(checks.policyAbsent());
|
|
43
|
+
const execute = await lowerer.lowerToExecuteRequest(ddlNode);
|
|
44
|
+
const present = await lowerer.lowerToExecuteRequest(checks.policyPresent());
|
|
45
|
+
return {
|
|
46
|
+
id: `rlsPolicy.${schemaName}.${tableName}.${policy.name}`,
|
|
47
|
+
label: `Create RLS policy "${policy.name}" on "${tableName}"`,
|
|
48
|
+
operationClass: 'additive',
|
|
49
|
+
target: targetDetails('rlsPolicy', policy.name, schemaName, tableName),
|
|
50
|
+
precheck: [
|
|
51
|
+
step(`ensure RLS policy "${policy.name}" does not exist`, absent.sql, absent.params),
|
|
52
|
+
],
|
|
53
|
+
execute: [step(`create RLS policy "${policy.name}"`, execute.sql, execute.params)],
|
|
54
|
+
postcheck: [step(`verify RLS policy "${policy.name}" exists`, present.sql, present.params)],
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export async function dropRlsPolicy(
|
|
59
|
+
schemaName: string,
|
|
60
|
+
tableName: string,
|
|
61
|
+
policyName: string,
|
|
62
|
+
lowerer: ExecuteRequestLowerer,
|
|
63
|
+
): Promise<Op> {
|
|
64
|
+
const ddlNode = dropPolicy({ schema: schemaName, table: tableName, name: policyName });
|
|
65
|
+
const checks = rlsPolicyExistsAst({ schema: schemaName, table: tableName, policyName });
|
|
66
|
+
const present = await lowerer.lowerToExecuteRequest(checks.policyPresent());
|
|
67
|
+
const execute = await lowerer.lowerToExecuteRequest(ddlNode);
|
|
68
|
+
const absent = await lowerer.lowerToExecuteRequest(checks.policyAbsent());
|
|
69
|
+
return {
|
|
70
|
+
id: `rlsPolicy.${schemaName}.${tableName}.${policyName}.drop`,
|
|
71
|
+
label: `Drop RLS policy "${policyName}" on "${tableName}"`,
|
|
72
|
+
operationClass: 'destructive',
|
|
73
|
+
target: targetDetails('rlsPolicy', policyName, schemaName, tableName),
|
|
74
|
+
precheck: [step(`ensure RLS policy "${policyName}" exists`, present.sql, present.params)],
|
|
75
|
+
execute: [step(`drop RLS policy "${policyName}"`, execute.sql, execute.params)],
|
|
76
|
+
postcheck: [step(`verify RLS policy "${policyName}" is absent`, absent.sql, absent.params)],
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export async function enableRowLevelSecurity(
|
|
81
|
+
schemaName: string,
|
|
82
|
+
tableName: string,
|
|
83
|
+
lowerer: ExecuteRequestLowerer,
|
|
84
|
+
): Promise<Op> {
|
|
85
|
+
const checks = rlsEnabledAst(schemaName, tableName);
|
|
86
|
+
const disabled = await lowerer.lowerToExecuteRequest(checks.rlsDisabled());
|
|
87
|
+
const enabled = await lowerer.lowerToExecuteRequest(checks.rlsEnabled());
|
|
88
|
+
return {
|
|
89
|
+
id: `rowLevelSecurity.${schemaName}.${tableName}`,
|
|
90
|
+
label: `Enable row-level security on "${tableName}"`,
|
|
91
|
+
operationClass: 'additive',
|
|
92
|
+
target: targetDetails('rowLevelSecurity', tableName, schemaName),
|
|
93
|
+
precheck: [
|
|
94
|
+
step(`check RLS is not already enabled on "${tableName}"`, disabled.sql, disabled.params),
|
|
95
|
+
],
|
|
96
|
+
execute: [
|
|
97
|
+
step(
|
|
98
|
+
`enable row-level security on "${tableName}"`,
|
|
99
|
+
`ALTER TABLE ${qualifyTableName(schemaName, tableName)} ENABLE ROW LEVEL SECURITY`,
|
|
100
|
+
),
|
|
101
|
+
],
|
|
102
|
+
postcheck: [
|
|
103
|
+
step(`verify row-level security is enabled on "${tableName}"`, enabled.sql, enabled.params),
|
|
104
|
+
],
|
|
105
|
+
};
|
|
106
|
+
}
|