@prisma-next/target-postgres 0.14.0-dev.3 → 0.14.0-dev.31
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/{codec-types-DHCkwPKE.d.mts → codec-types-CueCoGD0.d.mts} +2 -2
- package/dist/{codec-types-DHCkwPKE.d.mts.map → codec-types-CueCoGD0.d.mts.map} +1 -1
- package/dist/codec-types.d.mts +1 -1
- package/dist/{codecs--0A5_4Bq.d.mts → codecs-Ca8upop6.d.mts} +12 -1
- package/dist/codecs-Ca8upop6.d.mts.map +1 -0
- package/dist/codecs.d.mts +1 -1
- package/dist/codecs.mjs +34 -1
- package/dist/codecs.mjs.map +1 -1
- package/dist/contract-free.d.mts +8 -3
- package/dist/contract-free.d.mts.map +1 -1
- package/dist/contract-free.mjs +3 -3
- package/dist/control.d.mts +1 -1
- package/dist/control.d.mts.map +1 -1
- package/dist/control.mjs +11 -10
- package/dist/control.mjs.map +1 -1
- package/dist/data-transform-BOWpliq8.mjs.map +1 -1
- 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-CGf6UABz.mjs} +62 -5
- package/dist/ddl-CGf6UABz.mjs.map +1 -0
- package/dist/ddl.d.mts +3 -2
- package/dist/ddl.mjs +2 -2
- package/dist/{default-normalizer-DyyCHQWs.mjs → default-normalizer-CzV8-QSu.mjs} +58 -2
- package/dist/default-normalizer-CzV8-QSu.mjs.map +1 -0
- package/dist/default-normalizer.d.mts +1 -1
- package/dist/default-normalizer.d.mts.map +1 -1
- package/dist/default-normalizer.mjs +1 -1
- package/dist/descriptor-meta-Dvvpu_jU.mjs +17 -0
- package/dist/descriptor-meta-Dvvpu_jU.mjs.map +1 -0
- package/dist/descriptor-meta-runtime-D5zHFKUz.mjs +228 -0
- package/dist/descriptor-meta-runtime-D5zHFKUz.mjs.map +1 -0
- package/dist/{issue-planner-DL6g3CmE.mjs → issue-planner-D5H1VuVP.mjs} +11 -9
- package/dist/issue-planner-D5H1VuVP.mjs.map +1 -0
- 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-CSkPX3rI.d.mts +120 -0
- package/dist/nodes-CSkPX3rI.d.mts.map +1 -0
- package/dist/{nodes-Bbhs2rwj.mjs → nodes-Dh__NS58.mjs} +56 -2
- package/dist/nodes-Dh__NS58.mjs.map +1 -0
- package/dist/{op-factory-call-D_p5vxwt.mjs → op-factory-call-DIdBDwmO.mjs} +199 -28
- package/dist/op-factory-call-DIdBDwmO.mjs.map +1 -0
- package/dist/{op-factory-call-DmQEc3XV.d.mts → op-factory-call-DPJR7tJc.d.mts} +44 -13
- package/dist/op-factory-call-DPJR7tJc.d.mts.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 -2
- package/dist/pack.d.mts.map +1 -1
- package/dist/pack.mjs +1 -1
- package/dist/{planner-Bs_baQax.mjs → planner-BnNEtGYM.mjs} +156 -24
- package/dist/planner-BnNEtGYM.mjs.map +1 -0
- package/dist/{planner-ddl-builders-B2wOwLqI.mjs → planner-ddl-builders-B8Nn6nHN.mjs} +12 -27
- package/dist/planner-ddl-builders-B8Nn6nHN.mjs.map +1 -0
- package/dist/planner-ddl-builders.d.mts +2 -4
- package/dist/planner-ddl-builders.d.mts.map +1 -1
- package/dist/planner-ddl-builders.mjs +2 -2
- package/dist/{planner-produced-postgres-migration-QqHa2C2l.d.mts → planner-produced-postgres-migration-BO-frvvN.d.mts} +3 -3
- package/dist/{planner-produced-postgres-migration-QqHa2C2l.d.mts.map → planner-produced-postgres-migration-BO-frvvN.d.mts.map} +1 -1
- package/dist/{planner-produced-postgres-migration-Cji5vxUf.mjs → planner-produced-postgres-migration-DzvdQ-7K.mjs} +2 -2
- package/dist/{planner-produced-postgres-migration-Cji5vxUf.mjs.map → planner-produced-postgres-migration-DzvdQ-7K.mjs.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-CyjaoKhm.mjs} +2 -2
- package/dist/{planner-sql-checks-jqUUGyQR.mjs.map → planner-sql-checks-CyjaoKhm.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 +31 -4
- package/dist/planner.d.mts.map +1 -1
- package/dist/planner.mjs +2 -2
- package/dist/{postgres-contract-serializer-k3TAcPMY.mjs → postgres-contract-serializer-tGMiJcl4.mjs} +30 -21
- package/dist/postgres-contract-serializer-tGMiJcl4.mjs.map +1 -0
- package/dist/{postgres-migration-B5jKrXv3.mjs → postgres-migration-C8JZyTSJ.mjs} +2 -2
- package/dist/{postgres-migration-B5jKrXv3.mjs.map → postgres-migration-C8JZyTSJ.mjs.map} +1 -1
- package/dist/{postgres-migration-Y4YBJqkS.d.mts → postgres-migration-Cs7zMOe1.d.mts} +5 -5
- package/dist/{postgres-migration-Y4YBJqkS.d.mts.map → postgres-migration-Cs7zMOe1.d.mts.map} +1 -1
- package/dist/postgres-rls-policy-12wAEQhs.d.mts +60 -0
- package/dist/postgres-rls-policy-12wAEQhs.d.mts.map +1 -0
- package/dist/postgres-schema-DJUEkWg-.d.mts +190 -0
- package/dist/postgres-schema-DJUEkWg-.d.mts.map +1 -0
- package/dist/{postgres-schema-COGZ1ark.mjs → postgres-schema-DLcc3W2-.mjs} +138 -5
- package/dist/postgres-schema-DLcc3W2-.mjs.map +1 -0
- package/dist/postgres-schema-ir-BJl_a3J7.d.mts +102 -0
- package/dist/postgres-schema-ir-BJl_a3J7.d.mts.map +1 -0
- package/dist/postgres-schema-ir-I_FiUndw.mjs +133 -0
- package/dist/postgres-schema-ir-I_FiUndw.mjs.map +1 -0
- package/dist/postgres-schema-ir-annotations-BbDSiwx2.mjs +14 -0
- package/dist/postgres-schema-ir-annotations-BbDSiwx2.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 +5 -5
- 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 +4 -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 +45 -1
- package/src/core/authoring.ts +115 -1
- package/src/core/codecs.ts +35 -1
- package/src/core/ddl/nodes.ts +94 -2
- package/src/core/default-normalizer.ts +60 -2
- package/src/core/descriptor-meta.ts +4 -0
- package/src/core/entity-kinds.ts +16 -0
- package/src/core/migrations/bound-schema.ts +10 -0
- package/src/core/migrations/contract-to-postgres-schema-ir.ts +91 -0
- package/src/core/migrations/control-policy.ts +3 -0
- package/src/core/migrations/diff-postgres-schema.ts +55 -0
- package/src/core/migrations/issue-planner.ts +8 -0
- package/src/core/migrations/op-factory-call.ts +106 -21
- package/src/core/migrations/operations/columns.ts +24 -12
- package/src/core/migrations/operations/data-transform.ts +6 -0
- package/src/core/migrations/operations/rls.ts +106 -0
- package/src/core/migrations/operations/shared.ts +0 -30
- package/src/core/migrations/planner-ddl-builders.ts +15 -56
- package/src/core/migrations/planner-recipes.ts +31 -13
- package/src/core/migrations/planner-strategies.ts +3 -11
- package/src/core/migrations/planner-target-details.ts +3 -1
- package/src/core/migrations/planner.ts +84 -1
- package/src/core/migrations/verify-postgres-namespaces.ts +12 -15
- package/src/core/postgres-contract-serializer.ts +48 -33
- package/src/core/postgres-schema.ts +37 -13
- package/src/core/postgres-validators.ts +20 -0
- package/src/core/rls/canonicalize.ts +48 -0
- package/src/core/schema-ir/postgres-rls-policy.ts +101 -0
- package/src/core/schema-ir/postgres-role.ts +52 -0
- package/src/core/schema-ir/postgres-schema-ir-annotations.ts +22 -0
- package/src/core/schema-ir/postgres-schema-ir.ts +138 -0
- package/src/core/schema-ir/postgres-table-ir.ts +96 -0
- package/src/exports/contract-free.ts +7 -1
- package/src/exports/control.ts +11 -12
- package/src/exports/ddl.ts +4 -0
- package/src/exports/planner-ddl-builders.ts +0 -2
- package/src/exports/planner.ts +5 -0
- package/src/exports/rls-canonicalize.ts +6 -0
- package/src/exports/schema-ir-annotations.ts +1 -0
- package/src/exports/types.ts +20 -0
- package/dist/codecs--0A5_4Bq.d.mts.map +0 -1
- package/dist/ddl-QDyOSeLc.mjs.map +0 -1
- package/dist/default-normalizer-DyyCHQWs.mjs.map +0 -1
- package/dist/descriptor-meta-CpGygXpI.mjs +0 -140
- package/dist/descriptor-meta-CpGygXpI.mjs.map +0 -1
- package/dist/issue-planner-DL6g3CmE.mjs.map +0 -1
- package/dist/nodes-Bbhs2rwj.mjs.map +0 -1
- package/dist/nodes-pLeLgdis.d.mts +0 -67
- 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-ddl-builders-B2wOwLqI.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
|
@@ -10,15 +10,15 @@ import {
|
|
|
10
10
|
isAuthoringEntityTypeDescriptor,
|
|
11
11
|
} from '@prisma-next/framework-components/authoring';
|
|
12
12
|
import {
|
|
13
|
+
type AnyEntityKindDescriptor,
|
|
13
14
|
type Namespace,
|
|
14
|
-
NamespaceBase,
|
|
15
15
|
UNBOUND_NAMESPACE_ID,
|
|
16
16
|
} from '@prisma-next/framework-components/ir';
|
|
17
|
-
import type {
|
|
17
|
+
import type { SqlNamespaceInput, SqlStorage } from '@prisma-next/sql-contract/types';
|
|
18
18
|
import { blindCast } from '@prisma-next/utils/casts';
|
|
19
|
-
import type { JsonObject } from '@prisma-next/utils/json';
|
|
19
|
+
import type { JsonObject, JsonValue } from '@prisma-next/utils/json';
|
|
20
20
|
import { postgresAuthoringEntityTypes } from './authoring';
|
|
21
|
-
import {
|
|
21
|
+
import { policyEntityKind, roleEntityKind } from './entity-kinds';
|
|
22
22
|
import { isPostgresSchema, PostgresSchema } from './postgres-schema';
|
|
23
23
|
|
|
24
24
|
const POSTGRES_AUTHORING_CTX: AuthoringEntityContext = {
|
|
@@ -32,7 +32,8 @@ function isAuthoringEntityTypeFactoryOutput(
|
|
|
32
32
|
return (
|
|
33
33
|
typeof output === 'object' &&
|
|
34
34
|
output !== null &&
|
|
35
|
-
|
|
35
|
+
'factory' in output &&
|
|
36
|
+
typeof output.factory === 'function'
|
|
36
37
|
);
|
|
37
38
|
}
|
|
38
39
|
|
|
@@ -65,34 +66,29 @@ function collectStorageTypesHydrators(
|
|
|
65
66
|
}
|
|
66
67
|
|
|
67
68
|
export class PostgresContractSerializer extends SqlContractSerializerBase<Contract<SqlStorage>> {
|
|
68
|
-
constructor() {
|
|
69
|
+
constructor(extraPackEntityKinds: readonly AnyEntityKindDescriptor[] = []) {
|
|
69
70
|
const storageTypesHydrators = collectStorageTypesHydrators(postgresAuthoringEntityTypes);
|
|
70
|
-
super(storageTypesHydrators);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
protected override get defaultNamespaceId(): string {
|
|
74
|
-
return postgresTargetDescriptorMeta.defaultNamespaceId;
|
|
71
|
+
super(storageTypesHydrators, [policyEntityKind, roleEntityKind, ...extraPackEntityKinds]);
|
|
75
72
|
}
|
|
76
73
|
|
|
77
74
|
protected override hydrateSqlNamespaceEntry(
|
|
78
75
|
nsId: string,
|
|
79
|
-
raw:
|
|
80
|
-
): Namespace |
|
|
81
|
-
if (raw instanceof NamespaceBase) {
|
|
82
|
-
return raw;
|
|
83
|
-
}
|
|
76
|
+
raw: Record<string, unknown>,
|
|
77
|
+
): Namespace | SqlNamespaceInput {
|
|
84
78
|
const hydrated = blindCast<
|
|
85
|
-
|
|
86
|
-
'
|
|
79
|
+
SqlNamespaceInput,
|
|
80
|
+
'raw is always plain JSON, so super.hydrateSqlNamespaceEntry returns SqlNamespaceInput'
|
|
87
81
|
>(super.hydrateSqlNamespaceEntry(nsId, raw));
|
|
88
82
|
const { id, entries } = hydrated;
|
|
89
83
|
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
if (id === UNBOUND_NAMESPACE_ID &&
|
|
84
|
+
const allSlotsEmpty = Object.values(entries).every(
|
|
85
|
+
(slot) => slot === undefined || Object.keys(slot).length === 0,
|
|
86
|
+
);
|
|
87
|
+
if (id === UNBOUND_NAMESPACE_ID && allSlotsEmpty) {
|
|
94
88
|
return PostgresSchema.unbound;
|
|
95
89
|
}
|
|
90
|
+
const valueSetSlot = entries['valueSet'];
|
|
91
|
+
const hasValueSets = valueSetSlot !== undefined && Object.keys(valueSetSlot).length > 0;
|
|
96
92
|
return new PostgresSchema({
|
|
97
93
|
id,
|
|
98
94
|
entries: {
|
|
@@ -118,7 +114,7 @@ export class PostgresContractSerializer extends SqlContractSerializerBase<Contra
|
|
|
118
114
|
table: Object.fromEntries(
|
|
119
115
|
Object.entries(ns.entries.table ?? {}).map(([tableName, table]) => [
|
|
120
116
|
tableName,
|
|
121
|
-
this.
|
|
117
|
+
this.serializeJsonObject(table),
|
|
122
118
|
]),
|
|
123
119
|
),
|
|
124
120
|
},
|
|
@@ -132,42 +128,61 @@ export class PostgresContractSerializer extends SqlContractSerializerBase<Contra
|
|
|
132
128
|
if (storage.types !== undefined) {
|
|
133
129
|
const typesOut: Record<string, JsonObject> = {};
|
|
134
130
|
for (const [name, entry] of Object.entries(storage.types)) {
|
|
135
|
-
typesOut[name] = this.
|
|
131
|
+
typesOut[name] = this.serializeJsonObject(entry);
|
|
136
132
|
}
|
|
137
133
|
storageOut['types'] = typesOut;
|
|
138
134
|
}
|
|
139
|
-
return
|
|
135
|
+
return blindCast<
|
|
136
|
+
JsonObject,
|
|
137
|
+
'contract minus storage plus a JSON-shaped storageOut is a JsonObject'
|
|
138
|
+
>({
|
|
140
139
|
...rest,
|
|
141
140
|
storage: storageOut,
|
|
142
|
-
}
|
|
141
|
+
});
|
|
143
142
|
}
|
|
144
143
|
|
|
145
144
|
private serializePostgresNamespace(ns: PostgresSchema, isUnboundSlot: boolean): JsonObject {
|
|
146
145
|
const tablesOut: Record<string, JsonObject> = {};
|
|
147
146
|
for (const [tableName, table] of Object.entries(ns.table)) {
|
|
148
|
-
tablesOut[tableName] = this.
|
|
147
|
+
tablesOut[tableName] = this.serializeJsonObject(table);
|
|
149
148
|
}
|
|
150
149
|
const valueSetEntries = ns.valueSet;
|
|
151
150
|
const valueSetOut: Record<string, JsonObject> = {};
|
|
152
151
|
if (valueSetEntries !== undefined) {
|
|
153
152
|
for (const [valueSetName, valueSet] of Object.entries(valueSetEntries)) {
|
|
154
|
-
valueSetOut[valueSetName] =
|
|
155
|
-
JsonObject,
|
|
156
|
-
'serializeJsonValue round-trips the value-set node through JSON, yielding a JsonObject'
|
|
157
|
-
>(this.serializeJsonValue(valueSet));
|
|
153
|
+
valueSetOut[valueSetName] = this.serializeJsonObject(valueSet);
|
|
158
154
|
}
|
|
159
155
|
}
|
|
156
|
+
const roleOut: Record<string, JsonObject> = {};
|
|
157
|
+
for (const [roleName, role] of Object.entries(ns.role)) {
|
|
158
|
+
roleOut[roleName] = this.serializeJsonObject(role);
|
|
159
|
+
}
|
|
160
|
+
const policyOut: Record<string, JsonObject> = {};
|
|
161
|
+
for (const [policyName, policy] of Object.entries(ns.policy)) {
|
|
162
|
+
policyOut[policyName] = this.serializeJsonObject(policy);
|
|
163
|
+
}
|
|
160
164
|
return {
|
|
161
165
|
id: ns.id,
|
|
162
166
|
kind: isUnboundSlot ? 'postgres-unbound-schema' : 'postgres-schema',
|
|
163
167
|
entries: {
|
|
164
168
|
table: tablesOut,
|
|
165
169
|
...(Object.keys(valueSetOut).length > 0 ? { valueSet: valueSetOut } : {}),
|
|
170
|
+
...(Object.keys(roleOut).length > 0 ? { role: roleOut } : {}),
|
|
171
|
+
...(Object.keys(policyOut).length > 0 ? { policy: policyOut } : {}),
|
|
166
172
|
},
|
|
167
173
|
};
|
|
168
174
|
}
|
|
169
175
|
|
|
170
|
-
private
|
|
171
|
-
return
|
|
176
|
+
private serializeJsonObject(value: unknown): JsonObject {
|
|
177
|
+
return blindCast<
|
|
178
|
+
JsonObject,
|
|
179
|
+
'serializeJsonValue round-trips an IR node through JSON, yielding a JsonObject'
|
|
180
|
+
>(this.serializeJsonValue(value));
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
private serializeJsonValue(value: unknown): JsonValue {
|
|
184
|
+
return blindCast<JsonValue, 'JSON.parse(JSON.stringify(x)) yields a JsonValue'>(
|
|
185
|
+
JSON.parse(JSON.stringify(value)),
|
|
186
|
+
);
|
|
172
187
|
}
|
|
173
188
|
}
|
|
@@ -1,24 +1,36 @@
|
|
|
1
|
+
import type { Contract } from '@prisma-next/contract/types';
|
|
1
2
|
import {
|
|
2
3
|
freezeNode,
|
|
3
4
|
hydrateNamespaceEntities,
|
|
4
|
-
NamespaceBase,
|
|
5
5
|
UNBOUND_NAMESPACE_ID,
|
|
6
6
|
} from '@prisma-next/framework-components/ir';
|
|
7
7
|
import { composeSqlEntityKinds } from '@prisma-next/sql-contract/entity-kinds';
|
|
8
|
-
import
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
8
|
+
import {
|
|
9
|
+
SqlNamespaceBase,
|
|
10
|
+
type SqlNamespaceEntries,
|
|
11
|
+
type SqlNamespaceInput,
|
|
12
|
+
type SqlStorage,
|
|
13
|
+
type StorageTable,
|
|
14
|
+
type StorageValueSet,
|
|
14
15
|
} from '@prisma-next/sql-contract/types';
|
|
16
|
+
|
|
15
17
|
import { type CfExpr, cfExpr } from '@prisma-next/sql-relational-core/contract-free';
|
|
16
18
|
import { blindCast } from '@prisma-next/utils/casts';
|
|
17
19
|
import { ifDefined } from '@prisma-next/utils/defined';
|
|
18
20
|
import { PostgresTableSource } from './ast/table-source';
|
|
19
21
|
import { PG_TEXT_CODEC_ID } from './codec-ids';
|
|
22
|
+
import { policyEntityKind, roleEntityKind } from './entity-kinds';
|
|
23
|
+
import type { PostgresRlsPolicy } from './schema-ir/postgres-rls-policy';
|
|
24
|
+
import type { PostgresRole } from './schema-ir/postgres-role';
|
|
20
25
|
import { escapeLiteral } from './sql-utils';
|
|
21
26
|
|
|
27
|
+
export type PostgresContract = Contract<SqlStorage> & { readonly target: 'postgres' };
|
|
28
|
+
|
|
29
|
+
export type PostgresNamespaceEntries = SqlNamespaceEntries & {
|
|
30
|
+
readonly policy?: Readonly<Record<string, PostgresRlsPolicy>>;
|
|
31
|
+
readonly role?: Readonly<Record<string, PostgresRole>>;
|
|
32
|
+
};
|
|
33
|
+
|
|
22
34
|
export interface PostgresSchemaInput {
|
|
23
35
|
readonly id: string;
|
|
24
36
|
readonly entries: Readonly<Record<string, Readonly<Record<string, unknown>>>>;
|
|
@@ -38,7 +50,7 @@ export interface PostgresSchemaInput {
|
|
|
38
50
|
* prefix entirely — call sites stay polymorphic and never branch on
|
|
39
51
|
* `id === UNBOUND_NAMESPACE_ID`.
|
|
40
52
|
*/
|
|
41
|
-
export class PostgresSchema extends
|
|
53
|
+
export class PostgresSchema extends SqlNamespaceBase {
|
|
42
54
|
/**
|
|
43
55
|
* Stable singleton reference for the late-bound slot. Materialised
|
|
44
56
|
* lazily below the singleton subclass declaration so the static
|
|
@@ -50,13 +62,17 @@ export class PostgresSchema extends NamespaceBase {
|
|
|
50
62
|
|
|
51
63
|
declare readonly kind: 'schema';
|
|
52
64
|
readonly id: string;
|
|
53
|
-
readonly entries:
|
|
65
|
+
readonly entries: PostgresNamespaceEntries;
|
|
54
66
|
|
|
55
67
|
constructor(input: PostgresSchemaInput) {
|
|
56
68
|
super();
|
|
57
69
|
this.id = input.id;
|
|
58
70
|
|
|
59
|
-
const dispatched = hydrateNamespaceEntities(
|
|
71
|
+
const dispatched = hydrateNamespaceEntities(
|
|
72
|
+
input.entries,
|
|
73
|
+
composeSqlEntityKinds([policyEntityKind, roleEntityKind]),
|
|
74
|
+
'carry',
|
|
75
|
+
);
|
|
60
76
|
|
|
61
77
|
// Drop an empty valueSet so presence signals non-emptiness.
|
|
62
78
|
const valueSetRaw = dispatched['valueSet'];
|
|
@@ -67,8 +83,8 @@ export class PostgresSchema extends NamespaceBase {
|
|
|
67
83
|
|
|
68
84
|
this.entries = Object.freeze(
|
|
69
85
|
blindCast<
|
|
70
|
-
|
|
71
|
-
'composeSqlEntityKinds() supplies table→StorageTable
|
|
86
|
+
PostgresNamespaceEntries,
|
|
87
|
+
'composeSqlEntityKinds([policyEntityKind, roleEntityKind]) supplies table→StorageTable, valueSet→StorageValueSet, policy→PostgresRlsPolicy, role→PostgresRole descriptors'
|
|
72
88
|
>(withPresence),
|
|
73
89
|
);
|
|
74
90
|
Object.defineProperty(this, 'kind', {
|
|
@@ -88,6 +104,14 @@ export class PostgresSchema extends NamespaceBase {
|
|
|
88
104
|
return this.entries.valueSet;
|
|
89
105
|
}
|
|
90
106
|
|
|
107
|
+
get policy(): Readonly<Record<string, PostgresRlsPolicy>> {
|
|
108
|
+
return this.entries.policy ?? Object.freeze({});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
get role(): Readonly<Record<string, PostgresRole>> {
|
|
112
|
+
return this.entries.role ?? Object.freeze({});
|
|
113
|
+
}
|
|
114
|
+
|
|
91
115
|
/**
|
|
92
116
|
* The bare schema qualifier as it would appear in a rendered SQL
|
|
93
117
|
* fragment (already quoted). The unbound-schema singleton overrides
|
|
@@ -247,7 +271,7 @@ export function isPostgresSchema(ns: unknown): ns is PostgresSchema {
|
|
|
247
271
|
* by reference and trust the resulting `SqlStorage.namespaces` map to
|
|
248
272
|
* be value-stable for a given input set.
|
|
249
273
|
*/
|
|
250
|
-
export function postgresCreateNamespace(input:
|
|
274
|
+
export function postgresCreateNamespace(input: SqlNamespaceInput): PostgresSchema {
|
|
251
275
|
const schemaInput: PostgresSchemaInput = {
|
|
252
276
|
id: input.id,
|
|
253
277
|
entries: {
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type } from 'arktype';
|
|
2
|
+
|
|
3
|
+
export const PostgresRoleSchema = type({
|
|
4
|
+
kind: "'role'",
|
|
5
|
+
name: 'string',
|
|
6
|
+
namespaceId: 'string',
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
export const PostgresRlsPolicySchema = type({
|
|
10
|
+
kind: "'policy'",
|
|
11
|
+
name: 'string',
|
|
12
|
+
prefix: 'string',
|
|
13
|
+
tableName: 'string',
|
|
14
|
+
namespaceId: 'string',
|
|
15
|
+
operation: "'select' | 'insert' | 'update' | 'delete' | 'all'",
|
|
16
|
+
roles: type.string.array().readonly(),
|
|
17
|
+
'using?': 'string',
|
|
18
|
+
'withCheck?': 'string',
|
|
19
|
+
permissive: 'boolean',
|
|
20
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
export type RlsPolicyOperation = 'select' | 'insert' | 'update' | 'delete' | 'all';
|
|
4
|
+
|
|
5
|
+
export interface ContentHashParts {
|
|
6
|
+
readonly using?: string;
|
|
7
|
+
readonly withCheck?: string;
|
|
8
|
+
readonly roles: readonly string[];
|
|
9
|
+
readonly operation: RlsPolicyOperation;
|
|
10
|
+
readonly permissive: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Stabilizes an authored predicate for hashing: trim, and collapse runs of
|
|
15
|
+
* internal whitespace to a single space.
|
|
16
|
+
*
|
|
17
|
+
* This is deliberately minimal. The content hash is the equivalence relation
|
|
18
|
+
* for a policy, and the wire name (prefix + hash) is the only thing ever
|
|
19
|
+
* compared — we never recompute the hash from an introspected policy body, so
|
|
20
|
+
* there is no need to match Postgres's reprinted form. Minimal normalization
|
|
21
|
+
* also protects the no-collision property: aggressive rewriting (lowercasing,
|
|
22
|
+
* paren-stripping, cast-alias folding) risks collapsing two distinct predicates
|
|
23
|
+
* onto one hash. Out-of-band alteration of a hashed policy is unsupported.
|
|
24
|
+
*
|
|
25
|
+
* The normalizer is a stability commitment: any change re-suffixes all wire names.
|
|
26
|
+
*/
|
|
27
|
+
export function normalizePredicate(sql: string): string {
|
|
28
|
+
return sql.replace(/\s+/g, ' ').trim();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Returns the first 8 lowercase hex characters of the SHA-256 digest over the
|
|
33
|
+
* canonical content tuple for an RLS policy:
|
|
34
|
+
*
|
|
35
|
+
* [normalize(using), normalize(withCheck), sortedRoles, operation, permissive]
|
|
36
|
+
*
|
|
37
|
+
* Schema and table are excluded (they are orthogonal to policy equivalence).
|
|
38
|
+
* Uses `JSON.stringify` for a deterministic encoding.
|
|
39
|
+
*/
|
|
40
|
+
export function computeContentHash(parts: ContentHashParts): string {
|
|
41
|
+
const using = normalizePredicate(parts.using ?? '');
|
|
42
|
+
const withCheck = normalizePredicate(parts.withCheck ?? '');
|
|
43
|
+
const roles = [...new Set(parts.roles)].sort();
|
|
44
|
+
const permissive = parts.permissive ? 'permissive' : 'restrictive';
|
|
45
|
+
|
|
46
|
+
const tuple = JSON.stringify([using, withCheck, roles, parts.operation, permissive]);
|
|
47
|
+
return createHash('sha256').update(tuple).digest('hex').slice(0, 8);
|
|
48
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import type { DiffableNode } from '@prisma-next/framework-components/control';
|
|
2
|
+
import { freezeNode } from '@prisma-next/framework-components/ir';
|
|
3
|
+
import { SqlNode } from '@prisma-next/sql-contract/types';
|
|
4
|
+
|
|
5
|
+
export type RlsPolicyOperation = 'select' | 'insert' | 'update' | 'delete' | 'all';
|
|
6
|
+
|
|
7
|
+
export interface PostgresRlsPolicyInput {
|
|
8
|
+
/** Full wire name: `<prefix>_<8hex>`. Stored as-is; hashing is not this class's job. */
|
|
9
|
+
readonly name: string;
|
|
10
|
+
/** User-supplied prefix (the part before the `_<8hex>` suffix). */
|
|
11
|
+
readonly prefix: string;
|
|
12
|
+
/** Name of the table this policy attaches to, by name within the same schema. */
|
|
13
|
+
readonly tableName: string;
|
|
14
|
+
/** Namespace coordinate (schema name). Policies are schema-scoped. */
|
|
15
|
+
readonly namespaceId: string;
|
|
16
|
+
readonly operation: RlsPolicyOperation;
|
|
17
|
+
/** Sorted role names rendered in `TO <roles>`. Plain strings in this slice. */
|
|
18
|
+
readonly roles: readonly string[];
|
|
19
|
+
/** USING predicate SQL string, if present. */
|
|
20
|
+
readonly using?: string;
|
|
21
|
+
/** WITH CHECK predicate SQL string, if present. */
|
|
22
|
+
readonly withCheck?: string;
|
|
23
|
+
/** `true` = `AS PERMISSIVE`, `false` = `AS RESTRICTIVE`. */
|
|
24
|
+
readonly permissive: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Postgres IR class for a row-level security policy (`CREATE POLICY … ON …`).
|
|
29
|
+
*
|
|
30
|
+
* Target-only concept — no SQL-family abstract. Extends `SqlNode` directly.
|
|
31
|
+
* Frozen at construction via `freezeNode(this)`. The `kind: 'policy'`
|
|
32
|
+
* discriminant is enumerable (overrides SqlNode's non-enumerable `'sql'`) so it
|
|
33
|
+
* survives JSON serialization and enables dispatch. The literal matches the
|
|
34
|
+
* entries key (one-string rule: node.kind === entries key === entity kind).
|
|
35
|
+
*/
|
|
36
|
+
export class PostgresRlsPolicy extends SqlNode implements DiffableNode {
|
|
37
|
+
override readonly kind = 'policy' as const;
|
|
38
|
+
readonly name: string;
|
|
39
|
+
readonly prefix: string;
|
|
40
|
+
readonly tableName: string;
|
|
41
|
+
readonly namespaceId: string;
|
|
42
|
+
readonly operation: RlsPolicyOperation;
|
|
43
|
+
readonly roles: readonly string[];
|
|
44
|
+
declare readonly using?: string;
|
|
45
|
+
declare readonly withCheck?: string;
|
|
46
|
+
readonly permissive: boolean;
|
|
47
|
+
|
|
48
|
+
constructor(input: PostgresRlsPolicyInput) {
|
|
49
|
+
super();
|
|
50
|
+
this.name = input.name;
|
|
51
|
+
this.prefix = input.prefix;
|
|
52
|
+
this.tableName = input.tableName;
|
|
53
|
+
this.namespaceId = input.namespaceId;
|
|
54
|
+
this.operation = input.operation;
|
|
55
|
+
this.roles = Object.freeze([...input.roles]);
|
|
56
|
+
if (input.using !== undefined) this.using = input.using;
|
|
57
|
+
if (input.withCheck !== undefined) this.withCheck = input.withCheck;
|
|
58
|
+
this.permissive = input.permissive;
|
|
59
|
+
freezeNode(this);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
get id(): string {
|
|
63
|
+
return this.name;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
children(): readonly DiffableNode[] {
|
|
67
|
+
return [];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Equality by wire name only. The wire name is `<prefix>_<sha256(body)[0..8]>`,
|
|
72
|
+
* so name-equality IS body-equality — two policies with different bodies
|
|
73
|
+
* have different hashes and therefore different names. We deliberately
|
|
74
|
+
* skip comparing bodies directly because Postgres reprints predicate
|
|
75
|
+
* expressions (e.g. strips outer parentheses), so a byte-compare against
|
|
76
|
+
* the authored body would produce false mismatches on a clean re-verify.
|
|
77
|
+
*/
|
|
78
|
+
isEqualTo(other: DiffableNode): boolean {
|
|
79
|
+
if (!isPostgresRlsPolicy(other)) {
|
|
80
|
+
throw new Error(
|
|
81
|
+
`PostgresRlsPolicy.isEqualTo: expected a PostgresRlsPolicy, got ${other.constructor?.name ?? typeof other}`,
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
return this.name === other.name;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function isPostgresRlsPolicy(node: DiffableNode | undefined): node is PostgresRlsPolicy {
|
|
89
|
+
return node !== undefined && 'kind' in node && node.kind === 'policy';
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function assertPostgresRlsPolicy(
|
|
93
|
+
node: DiffableNode | undefined,
|
|
94
|
+
): asserts node is PostgresRlsPolicy {
|
|
95
|
+
if (!isPostgresRlsPolicy(node)) {
|
|
96
|
+
const kind = node !== undefined && 'kind' in node ? String(node.kind) : typeof node;
|
|
97
|
+
throw new Error(
|
|
98
|
+
`planPostgresSchemaDiff: expected a PostgresRlsPolicy on the policy-diff path but got ${kind}`,
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { DiffableNode } from '@prisma-next/framework-components/control';
|
|
2
|
+
import { freezeNode } from '@prisma-next/framework-components/ir';
|
|
3
|
+
import { SqlNode } from '@prisma-next/sql-contract/types';
|
|
4
|
+
|
|
5
|
+
export interface PostgresRoleInput {
|
|
6
|
+
readonly name: string;
|
|
7
|
+
/**
|
|
8
|
+
* Namespace coordinate. Roles are cluster-scoped; pass `UNBOUND_NAMESPACE_ID`
|
|
9
|
+
* from `@prisma-next/framework-components/ir`.
|
|
10
|
+
*/
|
|
11
|
+
readonly namespaceId: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Postgres IR class for a database role (`CREATE ROLE …`).
|
|
16
|
+
*
|
|
17
|
+
* Roles are cluster-scoped, so their namespace coordinate is always
|
|
18
|
+
* `UNBOUND_NAMESPACE_ID`. Target-only concept — no SQL-family abstract.
|
|
19
|
+
* Extends `SqlNode` directly, frozen at construction via `freezeNode(this)`.
|
|
20
|
+
* The `kind: 'role'` discriminant is enumerable so it survives JSON.
|
|
21
|
+
* Matches the entries key (one-string rule).
|
|
22
|
+
*/
|
|
23
|
+
export class PostgresRole extends SqlNode implements DiffableNode {
|
|
24
|
+
override readonly kind = 'role' as const;
|
|
25
|
+
readonly name: string;
|
|
26
|
+
readonly namespaceId: string;
|
|
27
|
+
|
|
28
|
+
constructor(input: PostgresRoleInput) {
|
|
29
|
+
super();
|
|
30
|
+
this.name = input.name;
|
|
31
|
+
this.namespaceId = input.namespaceId;
|
|
32
|
+
freezeNode(this);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Roles are cluster-unique; the name alone is sufficient as the id. */
|
|
36
|
+
get id(): string {
|
|
37
|
+
return this.name;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
children(): readonly DiffableNode[] {
|
|
41
|
+
return [];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
isEqualTo(other: DiffableNode): boolean {
|
|
45
|
+
if (!(other instanceof PostgresRole)) {
|
|
46
|
+
throw new Error(
|
|
47
|
+
`PostgresRole.isEqualTo: expected a PostgresRole, got ${other.constructor?.name ?? typeof other}`,
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
return this.name === other.name;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { UNBOUND_NAMESPACE_ID } from '@prisma-next/framework-components/ir';
|
|
2
|
+
import type { SqlStorage } from '@prisma-next/sql-contract/types';
|
|
3
|
+
import type { SqlSchemaIR } from '@prisma-next/sql-schema-ir/types';
|
|
4
|
+
import { isPostgresSchema } from '../postgres-schema';
|
|
5
|
+
import { isPostgresSchemaIR } from './postgres-schema-ir';
|
|
6
|
+
|
|
7
|
+
export function resolveDdlSchemaForNamespaceStorage(
|
|
8
|
+
storage: SqlStorage,
|
|
9
|
+
namespaceId: string,
|
|
10
|
+
schemaIr?: SqlSchemaIR,
|
|
11
|
+
): string {
|
|
12
|
+
if (namespaceId === UNBOUND_NAMESPACE_ID) {
|
|
13
|
+
return (
|
|
14
|
+
(schemaIr && isPostgresSchemaIR(schemaIr) ? schemaIr.pgSchemaName : undefined) ?? 'public'
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
const namespace = storage.namespaces[namespaceId];
|
|
18
|
+
if (namespace && isPostgresSchema(namespace)) {
|
|
19
|
+
return namespace.ddlSchemaName(storage);
|
|
20
|
+
}
|
|
21
|
+
return namespaceId;
|
|
22
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import type { DiffableNode } from '@prisma-next/framework-components/control';
|
|
2
|
+
import { freezeNode } from '@prisma-next/framework-components/ir';
|
|
3
|
+
import {
|
|
4
|
+
type SqlAnnotations,
|
|
5
|
+
type SqlSchemaIR,
|
|
6
|
+
SqlSchemaIRNode,
|
|
7
|
+
type SqlSchemaTarget,
|
|
8
|
+
type SqlTableIRInput,
|
|
9
|
+
} from '@prisma-next/sql-schema-ir/types';
|
|
10
|
+
import { blindCast } from '@prisma-next/utils/casts';
|
|
11
|
+
import type { PostgresRlsPolicy } from './postgres-rls-policy';
|
|
12
|
+
import type { PostgresRole } from './postgres-role';
|
|
13
|
+
import { PostgresTableIR } from './postgres-table-ir';
|
|
14
|
+
|
|
15
|
+
export interface PostgresSchemaIRInput {
|
|
16
|
+
readonly tables: Record<
|
|
17
|
+
string,
|
|
18
|
+
PostgresTableIR | (SqlTableIRInput & { rlsPolicies?: readonly PostgresRlsPolicy[] })
|
|
19
|
+
>;
|
|
20
|
+
readonly pgSchemaName: string;
|
|
21
|
+
readonly pgVersion: string;
|
|
22
|
+
readonly roles: readonly PostgresRole[];
|
|
23
|
+
readonly existingSchemas: readonly string[];
|
|
24
|
+
readonly nativeEnumTypeNames: readonly string[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Postgres-specific schema IR. Mirrors the structure of `SqlSchemaIR`
|
|
29
|
+
* (same `tables` + optional `annotations` fields) and adds typed fields for
|
|
30
|
+
* data the postgres adapter collects during introspection.
|
|
31
|
+
*
|
|
32
|
+
* Extends `SqlSchemaIRNode` directly rather than `SqlSchemaIR` because
|
|
33
|
+
* `SqlSchemaIR` calls `freezeNode` in its constructor, which prevents
|
|
34
|
+
* subclass field initialisation. `PostgresSchemaIR` replicates the minimal
|
|
35
|
+
* `SqlSchemaIR` structure and freezes itself at the end of its own
|
|
36
|
+
* constructor.
|
|
37
|
+
*
|
|
38
|
+
* `tables` holds `PostgresTableIR` instances which carry their own RLS
|
|
39
|
+
* policies. `children()` returns the tables directly — the table instances
|
|
40
|
+
* ARE the diff-tree nodes.
|
|
41
|
+
*
|
|
42
|
+
* Nothing RLS-specific leaks into the sql-family layer.
|
|
43
|
+
*/
|
|
44
|
+
export class PostgresSchemaIR extends SqlSchemaIRNode implements DiffableNode {
|
|
45
|
+
readonly nodeTarget: SqlSchemaTarget = 'postgres';
|
|
46
|
+
readonly tables: Readonly<Record<string, PostgresTableIR>>;
|
|
47
|
+
declare readonly annotations?: SqlAnnotations;
|
|
48
|
+
readonly pgSchemaName: string;
|
|
49
|
+
readonly pgVersion: string;
|
|
50
|
+
readonly roles: readonly PostgresRole[];
|
|
51
|
+
readonly existingSchemas: readonly string[];
|
|
52
|
+
readonly nativeEnumTypeNames: readonly string[];
|
|
53
|
+
|
|
54
|
+
constructor(input: PostgresSchemaIRInput) {
|
|
55
|
+
super();
|
|
56
|
+
this.tables = Object.freeze(
|
|
57
|
+
Object.fromEntries(
|
|
58
|
+
Object.entries(input.tables).map(([key, t]) => [
|
|
59
|
+
key,
|
|
60
|
+
t instanceof PostgresTableIR ? t : new PostgresTableIR(t),
|
|
61
|
+
]),
|
|
62
|
+
),
|
|
63
|
+
);
|
|
64
|
+
this.pgSchemaName = input.pgSchemaName;
|
|
65
|
+
this.pgVersion = input.pgVersion;
|
|
66
|
+
this.roles = Object.freeze([...input.roles]);
|
|
67
|
+
this.existingSchemas = Object.freeze([...input.existingSchemas]);
|
|
68
|
+
this.nativeEnumTypeNames = Object.freeze([...input.nativeEnumTypeNames]);
|
|
69
|
+
// Populate the annotations.pg bag with only the subset the family layer
|
|
70
|
+
// reads (nativeEnumTypeNames for PSL inference, existingSchemas for
|
|
71
|
+
// namespace presence checks).
|
|
72
|
+
this.annotations = {
|
|
73
|
+
pg: {
|
|
74
|
+
schema: input.pgSchemaName,
|
|
75
|
+
...(input.nativeEnumTypeNames.length > 0 && {
|
|
76
|
+
nativeEnumTypeNames: input.nativeEnumTypeNames,
|
|
77
|
+
}),
|
|
78
|
+
...(input.existingSchemas.length > 0 && {
|
|
79
|
+
existingSchemas: input.existingSchemas,
|
|
80
|
+
}),
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
freezeNode(this);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
get id(): string {
|
|
87
|
+
return this.pgSchemaName;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
get rlsPolicies(): readonly PostgresRlsPolicy[] {
|
|
91
|
+
return Object.values(this.tables).flatMap((t) => t.rlsPolicies);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
isEqualTo(_other: DiffableNode): boolean {
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
children(): readonly DiffableNode[] {
|
|
99
|
+
return Object.values(this.tables);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Structural guard for `PostgresSchemaIR`, narrowing on the `nodeTarget`
|
|
105
|
+
* discriminant rather than `instanceof`. `nodeTarget` is an enumerable own field
|
|
106
|
+
* (a plain class-field initializer), so it survives the `{ ...schema, tables }`
|
|
107
|
+
* spread the multi-space verify path (`projectSchemaToSpace`) produces — that
|
|
108
|
+
* projected object is not a class instance but retains every enumerable own
|
|
109
|
+
* property. The family-level `kind = 'sql-schema-ir'` discriminator is unusable
|
|
110
|
+
* here: it is shared by every SQL schema-IR node and is non-enumerable (dropped
|
|
111
|
+
* by the spread).
|
|
112
|
+
*/
|
|
113
|
+
export function isPostgresSchemaIR(schema: SqlSchemaIR): schema is PostgresSchemaIR {
|
|
114
|
+
return schema.nodeTarget === 'postgres';
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function assertPostgresSchemaIR(schema: SqlSchemaIR): asserts schema is PostgresSchemaIR {
|
|
118
|
+
if (!isPostgresSchemaIR(schema)) {
|
|
119
|
+
throw new Error(
|
|
120
|
+
`planPostgresSchemaDiff: expected a PostgresSchemaIR but got nodeTarget=${String(schema.nodeTarget ?? typeof schema)}`,
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Returns `schema` as-is when it is a real `PostgresSchemaIR` instance, or
|
|
127
|
+
* reconstructs one when `projectSchemaToSpace` has spread the class into a
|
|
128
|
+
* plain object (losing prototype methods).
|
|
129
|
+
*/
|
|
130
|
+
export function ensurePostgresSchemaIR(schema: PostgresSchemaIR): PostgresSchemaIR {
|
|
131
|
+
if (schema instanceof PostgresSchemaIR) return schema;
|
|
132
|
+
return new PostgresSchemaIR(
|
|
133
|
+
blindCast<
|
|
134
|
+
PostgresSchemaIRInput,
|
|
135
|
+
'spread objects from projectSchemaToSpace preserve all own-enumerable fields'
|
|
136
|
+
>(schema),
|
|
137
|
+
);
|
|
138
|
+
}
|