@supabase/pg-delta 1.0.0-alpha.10 → 1.0.0-alpha.11
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/cli/commands/declarative-export.js +12 -17
- package/dist/cli/commands/plan.js +10 -13
- package/dist/cli/commands/sync.js +8 -12
- package/dist/cli/utils/integrations.d.ts +30 -6
- package/dist/cli/utils/integrations.js +98 -6
- package/dist/core/change-utils.d.ts +9 -0
- package/dist/core/change-utils.js +71 -0
- package/dist/core/change.types.d.ts +22 -0
- package/dist/core/change.types.js +37 -1
- package/dist/core/depend.js +25 -0
- package/dist/core/export/file-mapper.d.ts +2 -2
- package/dist/core/integrations/filter/dsl.d.ts +78 -74
- package/dist/core/integrations/filter/dsl.js +127 -79
- package/dist/core/integrations/filter/flatten.d.ts +51 -0
- package/dist/core/integrations/filter/flatten.js +116 -0
- package/dist/core/integrations/integration-dsl.d.ts +17 -1
- package/dist/core/integrations/merge.d.ts +20 -0
- package/dist/core/integrations/merge.js +60 -0
- package/dist/core/integrations/serialize/dsl.d.ts +7 -4
- package/dist/core/integrations/serialize/dsl.js +2 -2
- package/dist/core/integrations/supabase.js +23 -8
- package/dist/core/objects/aggregate/changes/aggregate.types.d.ts +1 -0
- package/dist/core/objects/base.change.d.ts +10 -0
- package/dist/core/objects/base.change.js +10 -0
- package/dist/core/objects/base.model.d.ts +4 -1
- package/dist/core/objects/base.model.js +5 -2
- package/dist/core/objects/collation/changes/collation.types.d.ts +1 -0
- package/dist/core/objects/domain/changes/domain.create.d.ts +1 -1
- package/dist/core/objects/domain/changes/domain.create.js +7 -1
- package/dist/core/objects/domain/changes/domain.types.d.ts +1 -0
- package/dist/core/objects/event-trigger/changes/event-trigger.types.d.ts +1 -0
- package/dist/core/objects/extension/changes/extension.types.d.ts +1 -0
- package/dist/core/objects/foreign-data-wrapper/foreign-data-wrapper/changes/foreign-data-wrapper.types.d.ts +1 -0
- package/dist/core/objects/foreign-data-wrapper/foreign-data-wrapper.types.d.ts +1 -0
- package/dist/core/objects/foreign-data-wrapper/foreign-table/changes/foreign-table.types.d.ts +1 -0
- package/dist/core/objects/foreign-data-wrapper/server/changes/server.types.d.ts +1 -0
- package/dist/core/objects/foreign-data-wrapper/user-mapping/changes/user-mapping.types.d.ts +1 -0
- package/dist/core/objects/index/changes/index.types.d.ts +1 -0
- package/dist/core/objects/language/changes/language.types.d.ts +1 -0
- package/dist/core/objects/materialized-view/changes/materialized-view.types.d.ts +1 -0
- package/dist/core/objects/procedure/changes/procedure.types.d.ts +1 -0
- package/dist/core/objects/publication/changes/publication.types.d.ts +1 -0
- package/dist/core/objects/rls-policy/changes/rls-policy.types.d.ts +1 -0
- package/dist/core/objects/role/changes/role.types.d.ts +1 -0
- package/dist/core/objects/rule/changes/rule.types.d.ts +1 -0
- package/dist/core/objects/schema/changes/schema.types.d.ts +1 -0
- package/dist/core/objects/sequence/changes/sequence.types.d.ts +1 -0
- package/dist/core/objects/subscription/changes/subscription.types.d.ts +1 -0
- package/dist/core/objects/table/changes/table.types.d.ts +1 -0
- package/dist/core/objects/trigger/changes/trigger.types.d.ts +1 -0
- package/dist/core/objects/type/composite-type/changes/composite-type.types.d.ts +1 -0
- package/dist/core/objects/type/enum/changes/enum.types.d.ts +1 -0
- package/dist/core/objects/type/range/changes/range.types.d.ts +1 -0
- package/dist/core/objects/type/type.types.d.ts +1 -0
- package/dist/core/objects/view/changes/view.types.d.ts +1 -0
- package/dist/core/objects/view/view.diff.js +24 -13
- package/dist/core/postgres-config.d.ts +2 -2
- package/dist/core/sort/custom-constraints.js +1 -1
- package/dist/core/sort/logical-sort.js +3 -24
- package/package.json +5 -1
- package/src/cli/commands/declarative-export.ts +19 -27
- package/src/cli/commands/plan.ts +14 -20
- package/src/cli/commands/sync.ts +8 -15
- package/src/cli/utils/integrations.test.ts +210 -3
- package/src/cli/utils/integrations.ts +134 -6
- package/src/core/catalog.snapshot.test.ts +11 -2
- package/src/core/change-utils.test.ts +61 -0
- package/src/core/change-utils.ts +73 -0
- package/src/core/change.types.ts +50 -0
- package/src/core/depend.ts +25 -0
- package/src/core/export/file-mapper.ts +7 -2
- package/src/core/integrations/filter/dsl.test.ts +299 -60
- package/src/core/integrations/filter/dsl.ts +208 -169
- package/src/core/integrations/filter/flatten.test.ts +282 -0
- package/src/core/integrations/filter/flatten.ts +150 -0
- package/src/core/integrations/integration-dsl.ts +17 -1
- package/src/core/integrations/merge.test.ts +128 -0
- package/src/core/integrations/merge.ts +72 -0
- package/src/core/integrations/serialize/dsl.test.ts +6 -6
- package/src/core/integrations/serialize/dsl.ts +7 -4
- package/src/core/integrations/supabase.ts +23 -8
- package/src/core/objects/aggregate/changes/aggregate.types.ts +1 -0
- package/src/core/objects/base.change.ts +10 -0
- package/src/core/objects/base.model.test.ts +43 -0
- package/src/core/objects/base.model.ts +5 -2
- package/src/core/objects/collation/changes/collation.types.ts +1 -0
- package/src/core/objects/domain/changes/domain.create.ts +17 -1
- package/src/core/objects/domain/changes/domain.types.ts +1 -0
- package/src/core/objects/event-trigger/changes/event-trigger.types.ts +1 -0
- package/src/core/objects/extension/changes/extension.types.ts +1 -0
- package/src/core/objects/foreign-data-wrapper/foreign-data-wrapper/changes/foreign-data-wrapper.types.ts +1 -0
- package/src/core/objects/foreign-data-wrapper/foreign-data-wrapper.types.ts +1 -0
- package/src/core/objects/foreign-data-wrapper/foreign-table/changes/foreign-table.types.ts +1 -0
- package/src/core/objects/foreign-data-wrapper/server/changes/server.types.ts +1 -0
- package/src/core/objects/foreign-data-wrapper/user-mapping/changes/user-mapping.types.ts +1 -0
- package/src/core/objects/index/changes/index.types.ts +1 -0
- package/src/core/objects/language/changes/language.types.ts +1 -0
- package/src/core/objects/materialized-view/changes/materialized-view.types.ts +1 -0
- package/src/core/objects/procedure/changes/procedure.types.ts +1 -0
- package/src/core/objects/publication/changes/publication.types.ts +1 -0
- package/src/core/objects/rls-policy/changes/rls-policy.types.ts +1 -0
- package/src/core/objects/role/changes/role.types.ts +1 -0
- package/src/core/objects/rule/changes/rule.types.ts +1 -0
- package/src/core/objects/schema/changes/schema.types.ts +1 -0
- package/src/core/objects/sequence/changes/sequence.types.ts +1 -0
- package/src/core/objects/subscription/changes/subscription.types.ts +1 -0
- package/src/core/objects/table/changes/table.types.ts +1 -0
- package/src/core/objects/trigger/changes/trigger.types.ts +1 -0
- package/src/core/objects/type/composite-type/changes/composite-type.types.ts +1 -0
- package/src/core/objects/type/enum/changes/enum.types.ts +1 -0
- package/src/core/objects/type/range/changes/range.types.ts +1 -0
- package/src/core/objects/type/type.types.ts +1 -0
- package/src/core/objects/view/changes/view.types.ts +1 -0
- package/src/core/objects/view/view.diff.test.ts +96 -0
- package/src/core/objects/view/view.diff.ts +30 -15
- package/src/core/postgres-config.ts +2 -2
- package/src/core/sort/custom-constraints.ts +1 -1
- package/src/core/sort/logical-sort.ts +3 -27
- package/src/typedoc.ts +248 -0
- package/dist/core/integrations/filter/extractors.d.ts +0 -12
- package/dist/core/integrations/filter/extractors.js +0 -178
- package/src/core/integrations/filter/extractors.test.ts +0 -244
- package/src/core/integrations/filter/extractors.ts +0 -187
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@supabase/pg-delta",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.11",
|
|
4
4
|
"description": "PostgreSQL migrations made easy",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -68,6 +68,7 @@
|
|
|
68
68
|
"scripts": {
|
|
69
69
|
"build": "tsc --project tsconfig.build.json",
|
|
70
70
|
"check-types": "tsc --noEmit",
|
|
71
|
+
"docs": "typedoc",
|
|
71
72
|
"format-and-lint": "biome check . --error-on-warnings",
|
|
72
73
|
"knip": "knip",
|
|
73
74
|
"pgdelta": "bun src/cli/bin/cli.ts",
|
|
@@ -84,6 +85,7 @@
|
|
|
84
85
|
"chalk": "^5.6.2",
|
|
85
86
|
"debug": "^4.3.7",
|
|
86
87
|
"pg": "^8.17.2",
|
|
88
|
+
"picomatch": "^4.0.3",
|
|
87
89
|
"zod": "^4.2.1"
|
|
88
90
|
},
|
|
89
91
|
"devDependencies": {
|
|
@@ -94,9 +96,11 @@
|
|
|
94
96
|
"@types/debug": "^4.1.12",
|
|
95
97
|
"@types/node": "^24.10.4",
|
|
96
98
|
"@types/pg": "^8.11.10",
|
|
99
|
+
"@types/picomatch": "^4.0.2",
|
|
97
100
|
"dedent": "^1.7.1",
|
|
98
101
|
"knip": "^5.75.2",
|
|
99
102
|
"testcontainers": "^11.10.0",
|
|
103
|
+
"typedoc": "^0.28.17",
|
|
100
104
|
"typescript": "^5.9.3"
|
|
101
105
|
}
|
|
102
106
|
}
|
|
@@ -6,13 +6,14 @@ import { mkdir, rm, writeFile } from "node:fs/promises";
|
|
|
6
6
|
import path from "node:path";
|
|
7
7
|
import { buildCommand, type CommandContext } from "@stricli/core";
|
|
8
8
|
import chalk from "chalk";
|
|
9
|
-
import
|
|
9
|
+
import { deserializeCatalog } from "../../core/catalog.snapshot.ts";
|
|
10
10
|
import { exportDeclarativeSchema } from "../../core/export/index.ts";
|
|
11
11
|
import type { Grouping, GroupingPattern } from "../../core/export/types.ts";
|
|
12
12
|
import type { FilterDSL } from "../../core/integrations/filter/dsl.ts";
|
|
13
|
-
import
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
import {
|
|
14
|
+
compileSerializeDSL,
|
|
15
|
+
type SerializeDSL,
|
|
16
|
+
} from "../../core/integrations/serialize/dsl.ts";
|
|
16
17
|
import { createPlan } from "../../core/plan/index.ts";
|
|
17
18
|
import type { SqlFormatOptions } from "../../core/plan/sql-format.ts";
|
|
18
19
|
import {
|
|
@@ -21,7 +22,7 @@ import {
|
|
|
21
22
|
computeFileDiff,
|
|
22
23
|
formatExportSummary,
|
|
23
24
|
} from "../utils/export-display.ts";
|
|
24
|
-
import {
|
|
25
|
+
import { resolveIntegrationOptions } from "../utils/integrations.ts";
|
|
25
26
|
import { isPostgresUrl, loadCatalogFromFile } from "../utils/resolve-input.ts";
|
|
26
27
|
|
|
27
28
|
function parseJsonFlag<T>(label: string, value: string): T {
|
|
@@ -185,29 +186,22 @@ After export, a tip is printed with the command to apply the schema to an empty
|
|
|
185
186
|
verbose?: boolean;
|
|
186
187
|
},
|
|
187
188
|
) {
|
|
188
|
-
const {
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
flags.serialize
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
const integrationDSL = await loadIntegrationDSL(flags.integration);
|
|
198
|
-
filterOption = filterOption ?? integrationDSL.filter;
|
|
199
|
-
serializeOption = serializeOption ?? integrationDSL.serialize;
|
|
200
|
-
integrationEmptyCatalog = integrationDSL.emptyCatalog;
|
|
201
|
-
}
|
|
189
|
+
const {
|
|
190
|
+
filter,
|
|
191
|
+
serialize,
|
|
192
|
+
emptyCatalog: integrationEmptyCatalog,
|
|
193
|
+
} = await resolveIntegrationOptions({
|
|
194
|
+
filter: flags.filter,
|
|
195
|
+
serialize: flags.serialize,
|
|
196
|
+
integration: flags.integration,
|
|
197
|
+
});
|
|
202
198
|
|
|
203
199
|
const resolvedSource = flags.source
|
|
204
200
|
? isPostgresUrl(flags.source)
|
|
205
201
|
? flags.source
|
|
206
202
|
: await loadCatalogFromFile(flags.source)
|
|
207
203
|
: integrationEmptyCatalog
|
|
208
|
-
? (
|
|
209
|
-
integrationEmptyCatalog,
|
|
210
|
-
)
|
|
204
|
+
? deserializeCatalog(integrationEmptyCatalog)
|
|
211
205
|
: null;
|
|
212
206
|
|
|
213
207
|
const resolvedTarget = isPostgresUrl(flags.target)
|
|
@@ -221,8 +215,8 @@ After export, a tip is printed with the command to apply the schema to an empty
|
|
|
221
215
|
// changes that depend on filtered objects (e.g. RLS policies that
|
|
222
216
|
// reference auth.uid() when the auth schema is filtered out).
|
|
223
217
|
const planResult = await createPlan(resolvedSource, resolvedTarget, {
|
|
224
|
-
filter
|
|
225
|
-
serialize
|
|
218
|
+
filter,
|
|
219
|
+
serialize,
|
|
226
220
|
skipDefaultPrivilegeSubtraction: true,
|
|
227
221
|
});
|
|
228
222
|
|
|
@@ -254,9 +248,7 @@ After export, a tip is printed with the command to apply the schema to an empty
|
|
|
254
248
|
}
|
|
255
249
|
|
|
256
250
|
const serializeFn =
|
|
257
|
-
|
|
258
|
-
? compileSerializeDSL(serializeOption)
|
|
259
|
-
: undefined;
|
|
251
|
+
serialize !== undefined ? compileSerializeDSL(serialize) : undefined;
|
|
260
252
|
|
|
261
253
|
const output = exportDeclarativeSchema(planResult, {
|
|
262
254
|
integration:
|
package/src/cli/commands/plan.ts
CHANGED
|
@@ -4,14 +4,13 @@
|
|
|
4
4
|
|
|
5
5
|
import { writeFile } from "node:fs/promises";
|
|
6
6
|
import { buildCommand, type CommandContext } from "@stricli/core";
|
|
7
|
+
import { deserializeCatalog } from "../../core/catalog.snapshot.ts";
|
|
7
8
|
import type { FilterDSL } from "../../core/integrations/filter/dsl.ts";
|
|
8
|
-
import type { ChangeFilter } from "../../core/integrations/filter/filter.types.ts";
|
|
9
9
|
import type { SerializeDSL } from "../../core/integrations/serialize/dsl.ts";
|
|
10
|
-
import type { ChangeSerializer } from "../../core/integrations/serialize/serialize.types.ts";
|
|
11
10
|
import { createPlan } from "../../core/plan/index.ts";
|
|
12
11
|
import type { SqlFormatOptions } from "../../core/plan/sql-format.ts";
|
|
13
12
|
import { setCommandExitCode } from "../exit-code.ts";
|
|
14
|
-
import {
|
|
13
|
+
import { resolveIntegrationOptions } from "../utils/integrations.ts";
|
|
15
14
|
import { isPostgresUrl, loadCatalogFromFile } from "../utils/resolve-input.ts";
|
|
16
15
|
import { formatPlanForDisplay } from "../utils.ts";
|
|
17
16
|
|
|
@@ -138,27 +137,22 @@ json/sql outputs are available for artifacts or piping.
|
|
|
138
137
|
"sql-format-options"?: SqlFormatOptions;
|
|
139
138
|
},
|
|
140
139
|
) {
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
serializeOption = serializeOption ?? integrationDSL.serialize;
|
|
151
|
-
integrationEmptyCatalog = integrationDSL.emptyCatalog;
|
|
152
|
-
}
|
|
140
|
+
const {
|
|
141
|
+
filter,
|
|
142
|
+
serialize,
|
|
143
|
+
emptyCatalog: integrationEmptyCatalog,
|
|
144
|
+
} = await resolveIntegrationOptions({
|
|
145
|
+
filter: flags.filter,
|
|
146
|
+
serialize: flags.serialize,
|
|
147
|
+
integration: flags.integration,
|
|
148
|
+
});
|
|
153
149
|
|
|
154
150
|
const resolvedSource = flags.source
|
|
155
151
|
? isPostgresUrl(flags.source)
|
|
156
152
|
? flags.source
|
|
157
153
|
: await loadCatalogFromFile(flags.source)
|
|
158
154
|
: integrationEmptyCatalog
|
|
159
|
-
? (
|
|
160
|
-
integrationEmptyCatalog,
|
|
161
|
-
)
|
|
155
|
+
? deserializeCatalog(integrationEmptyCatalog)
|
|
162
156
|
: null;
|
|
163
157
|
|
|
164
158
|
const resolvedTarget = isPostgresUrl(flags.target)
|
|
@@ -167,8 +161,8 @@ json/sql outputs are available for artifacts or piping.
|
|
|
167
161
|
|
|
168
162
|
const planResult = await createPlan(resolvedSource, resolvedTarget, {
|
|
169
163
|
role: flags.role,
|
|
170
|
-
filter
|
|
171
|
-
serialize
|
|
164
|
+
filter,
|
|
165
|
+
serialize,
|
|
172
166
|
});
|
|
173
167
|
if (!planResult) {
|
|
174
168
|
this.process.stdout.write("No changes detected.\n");
|
package/src/cli/commands/sync.ts
CHANGED
|
@@ -4,12 +4,10 @@
|
|
|
4
4
|
|
|
5
5
|
import { buildCommand, type CommandContext } from "@stricli/core";
|
|
6
6
|
import type { FilterDSL } from "../../core/integrations/filter/dsl.ts";
|
|
7
|
-
import type { ChangeFilter } from "../../core/integrations/filter/filter.types.ts";
|
|
8
7
|
import type { SerializeDSL } from "../../core/integrations/serialize/dsl.ts";
|
|
9
|
-
import type { ChangeSerializer } from "../../core/integrations/serialize/serialize.types.ts";
|
|
10
8
|
import { applyPlan } from "../../core/plan/apply.ts";
|
|
11
9
|
import { createPlan } from "../../core/plan/index.ts";
|
|
12
|
-
import {
|
|
10
|
+
import { resolveIntegrationOptions } from "../utils/integrations.ts";
|
|
13
11
|
import {
|
|
14
12
|
formatPlanForDisplay,
|
|
15
13
|
handleApplyResult,
|
|
@@ -120,22 +118,17 @@ Exit codes:
|
|
|
120
118
|
integration?: string;
|
|
121
119
|
},
|
|
122
120
|
) {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
flags.
|
|
127
|
-
|
|
128
|
-
const integrationDSL = await loadIntegrationDSL(flags.integration);
|
|
129
|
-
// Use integration DSL if explicit flags not provided
|
|
130
|
-
filterOption = filterOption ?? integrationDSL.filter;
|
|
131
|
-
serializeOption = serializeOption ?? integrationDSL.serialize;
|
|
132
|
-
}
|
|
121
|
+
const { filter, serialize } = await resolveIntegrationOptions({
|
|
122
|
+
filter: flags.filter,
|
|
123
|
+
serialize: flags.serialize,
|
|
124
|
+
integration: flags.integration,
|
|
125
|
+
});
|
|
133
126
|
|
|
134
127
|
// 1. Create the plan
|
|
135
128
|
const planResult = await createPlan(flags.source, flags.target, {
|
|
136
129
|
role: flags.role,
|
|
137
|
-
filter
|
|
138
|
-
serialize
|
|
130
|
+
filter,
|
|
131
|
+
serialize,
|
|
139
132
|
});
|
|
140
133
|
if (!planResult) {
|
|
141
134
|
this.process.stdout.write("No changes detected.\n");
|
|
@@ -2,7 +2,12 @@ import { describe, expect, test } from "bun:test";
|
|
|
2
2
|
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
|
-
import {
|
|
5
|
+
import type { FilterDSL } from "../../core/integrations/filter/dsl.ts";
|
|
6
|
+
import type { SerializeDSL } from "../../core/integrations/serialize/dsl.ts";
|
|
7
|
+
import {
|
|
8
|
+
loadIntegrationDSL,
|
|
9
|
+
resolveIntegrationOptions,
|
|
10
|
+
} from "./integrations.ts";
|
|
6
11
|
|
|
7
12
|
describe("loadIntegrationDSL", () => {
|
|
8
13
|
test("loads from .json file path", async () => {
|
|
@@ -12,12 +17,12 @@ describe("loadIntegrationDSL", () => {
|
|
|
12
17
|
await writeFile(
|
|
13
18
|
jsonPath,
|
|
14
19
|
JSON.stringify({
|
|
15
|
-
filter: { schema: "app" },
|
|
20
|
+
filter: { "*/schema": "app" },
|
|
16
21
|
}),
|
|
17
22
|
);
|
|
18
23
|
const dsl = await loadIntegrationDSL(jsonPath);
|
|
19
24
|
expect(dsl).toBeDefined();
|
|
20
|
-
expect(dsl.filter).toEqual({ schema: "app" });
|
|
25
|
+
expect(dsl.filter).toEqual({ "*/schema": "app" });
|
|
21
26
|
} finally {
|
|
22
27
|
await rm(dir, { recursive: true, force: true });
|
|
23
28
|
}
|
|
@@ -42,3 +47,205 @@ describe("loadIntegrationDSL", () => {
|
|
|
42
47
|
}
|
|
43
48
|
});
|
|
44
49
|
});
|
|
50
|
+
|
|
51
|
+
describe("extends resolution", () => {
|
|
52
|
+
test('extends: "supabase" → resolves and merges successfully', async () => {
|
|
53
|
+
const dir = await mkdtemp(path.join(tmpdir(), "pgd-extends-"));
|
|
54
|
+
const jsonPath = path.join(dir, "custom.json");
|
|
55
|
+
try {
|
|
56
|
+
await writeFile(
|
|
57
|
+
jsonPath,
|
|
58
|
+
JSON.stringify({
|
|
59
|
+
extends: "supabase",
|
|
60
|
+
serialize: [
|
|
61
|
+
{
|
|
62
|
+
when: { objectType: "table" },
|
|
63
|
+
options: { skipAuthorization: true },
|
|
64
|
+
},
|
|
65
|
+
],
|
|
66
|
+
}),
|
|
67
|
+
);
|
|
68
|
+
const dsl = await loadIntegrationDSL(jsonPath);
|
|
69
|
+
expect(dsl).toBeDefined();
|
|
70
|
+
// Should have the supabase filter merged in
|
|
71
|
+
expect(dsl.filter).toBeDefined();
|
|
72
|
+
// Should have both supabase serialize rules and our custom one
|
|
73
|
+
expect(dsl.serialize).toBeDefined();
|
|
74
|
+
expect(dsl.serialize?.length).toBeGreaterThan(1);
|
|
75
|
+
} finally {
|
|
76
|
+
await rm(dir, { recursive: true, force: true });
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test('extends: "./some-file.json" → throws error about core integrations only', async () => {
|
|
81
|
+
const dir = await mkdtemp(path.join(tmpdir(), "pgd-extends-"));
|
|
82
|
+
const filePath = path.join(dir, "child.json");
|
|
83
|
+
const parentPath = path.join(dir, "parent.json");
|
|
84
|
+
try {
|
|
85
|
+
await writeFile(
|
|
86
|
+
parentPath,
|
|
87
|
+
JSON.stringify({ filter: { "*/schema": "app" } }),
|
|
88
|
+
);
|
|
89
|
+
await writeFile(
|
|
90
|
+
filePath,
|
|
91
|
+
JSON.stringify({
|
|
92
|
+
extends: parentPath,
|
|
93
|
+
filter: { "*/schema": "public" },
|
|
94
|
+
}),
|
|
95
|
+
);
|
|
96
|
+
expect(loadIntegrationDSL(filePath)).rejects.toThrow(
|
|
97
|
+
/extends only supports core integration names/,
|
|
98
|
+
);
|
|
99
|
+
} finally {
|
|
100
|
+
await rm(dir, { recursive: true, force: true });
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test('extends: "nonexistent" → throws error about unknown core integration', async () => {
|
|
105
|
+
const dir = await mkdtemp(path.join(tmpdir(), "pgd-extends-"));
|
|
106
|
+
const jsonPath = path.join(dir, "custom.json");
|
|
107
|
+
try {
|
|
108
|
+
await writeFile(jsonPath, JSON.stringify({ extends: "nonexistent" }));
|
|
109
|
+
expect(loadIntegrationDSL(jsonPath)).rejects.toThrow(
|
|
110
|
+
/Unknown core integration: "nonexistent"/,
|
|
111
|
+
);
|
|
112
|
+
} finally {
|
|
113
|
+
await rm(dir, { recursive: true, force: true });
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
describe("resolveIntegrationOptions", () => {
|
|
119
|
+
test("no integration, no CLI flags → all undefined", async () => {
|
|
120
|
+
const result = await resolveIntegrationOptions({});
|
|
121
|
+
expect(result).toEqual({
|
|
122
|
+
filter: undefined,
|
|
123
|
+
serialize: undefined,
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test("CLI flags only → passed through unchanged", async () => {
|
|
128
|
+
const filter: FilterDSL = { "*/schema": "public" };
|
|
129
|
+
const serialize: SerializeDSL = [
|
|
130
|
+
{ when: { objectType: "schema" }, options: { skipAuthorization: true } },
|
|
131
|
+
];
|
|
132
|
+
const result = await resolveIntegrationOptions({ filter, serialize });
|
|
133
|
+
expect(result.filter).toEqual(filter);
|
|
134
|
+
expect(result.serialize).toEqual(serialize);
|
|
135
|
+
expect(result.emptyCatalog).toBeUndefined();
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test("integration only → integration values returned", async () => {
|
|
139
|
+
const dir = await mkdtemp(path.join(tmpdir(), "pgd-resolve-"));
|
|
140
|
+
const jsonPath = path.join(dir, "int.json");
|
|
141
|
+
try {
|
|
142
|
+
await writeFile(
|
|
143
|
+
jsonPath,
|
|
144
|
+
JSON.stringify({
|
|
145
|
+
filter: { "*/schema": "app" },
|
|
146
|
+
serialize: [{ when: { objectType: "table" }, options: {} }],
|
|
147
|
+
}),
|
|
148
|
+
);
|
|
149
|
+
const result = await resolveIntegrationOptions({
|
|
150
|
+
integration: jsonPath,
|
|
151
|
+
});
|
|
152
|
+
expect(result.filter).toEqual({ "*/schema": "app" });
|
|
153
|
+
expect(result.serialize).toEqual([
|
|
154
|
+
{ when: { objectType: "table" }, options: {} },
|
|
155
|
+
]);
|
|
156
|
+
} finally {
|
|
157
|
+
await rm(dir, { recursive: true, force: true });
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test("both filter + integration filter → AND-combined", async () => {
|
|
162
|
+
const dir = await mkdtemp(path.join(tmpdir(), "pgd-resolve-"));
|
|
163
|
+
const jsonPath = path.join(dir, "int.json");
|
|
164
|
+
try {
|
|
165
|
+
await writeFile(
|
|
166
|
+
jsonPath,
|
|
167
|
+
JSON.stringify({
|
|
168
|
+
filter: { "*/schema": "app" },
|
|
169
|
+
}),
|
|
170
|
+
);
|
|
171
|
+
const cliFilter: FilterDSL = { objectType: "table" };
|
|
172
|
+
const result = await resolveIntegrationOptions({
|
|
173
|
+
filter: cliFilter,
|
|
174
|
+
integration: jsonPath,
|
|
175
|
+
});
|
|
176
|
+
expect(result.filter).toEqual({
|
|
177
|
+
and: [{ "*/schema": "app" }, { objectType: "table" }],
|
|
178
|
+
});
|
|
179
|
+
} finally {
|
|
180
|
+
await rm(dir, { recursive: true, force: true });
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test("both serialize + integration serialize → concatenated (integration first)", async () => {
|
|
185
|
+
const dir = await mkdtemp(path.join(tmpdir(), "pgd-resolve-"));
|
|
186
|
+
const jsonPath = path.join(dir, "int.json");
|
|
187
|
+
try {
|
|
188
|
+
const intSerialize: SerializeDSL = [
|
|
189
|
+
{
|
|
190
|
+
when: { objectType: "schema" },
|
|
191
|
+
options: { skipAuthorization: true },
|
|
192
|
+
},
|
|
193
|
+
];
|
|
194
|
+
const cliSerialize: SerializeDSL = [
|
|
195
|
+
{ when: { objectType: "table" }, options: {} },
|
|
196
|
+
];
|
|
197
|
+
await writeFile(jsonPath, JSON.stringify({ serialize: intSerialize }));
|
|
198
|
+
const result = await resolveIntegrationOptions({
|
|
199
|
+
serialize: cliSerialize,
|
|
200
|
+
integration: jsonPath,
|
|
201
|
+
});
|
|
202
|
+
expect(result.serialize).toEqual([...intSerialize, ...cliSerialize]);
|
|
203
|
+
} finally {
|
|
204
|
+
await rm(dir, { recursive: true, force: true });
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
test("emptyCatalog returned from integration", async () => {
|
|
209
|
+
const dir = await mkdtemp(path.join(tmpdir(), "pgd-resolve-"));
|
|
210
|
+
const jsonPath = path.join(dir, "int.json");
|
|
211
|
+
try {
|
|
212
|
+
const emptyCatalog = {
|
|
213
|
+
version: 1,
|
|
214
|
+
currentUser: "postgres",
|
|
215
|
+
aggregates: {},
|
|
216
|
+
collations: {},
|
|
217
|
+
compositeTypes: {},
|
|
218
|
+
domains: {},
|
|
219
|
+
enums: {},
|
|
220
|
+
extensions: {},
|
|
221
|
+
procedures: {},
|
|
222
|
+
indexes: {},
|
|
223
|
+
materializedViews: {},
|
|
224
|
+
subscriptions: {},
|
|
225
|
+
publications: {},
|
|
226
|
+
rlsPolicies: {},
|
|
227
|
+
roles: {},
|
|
228
|
+
schemas: {},
|
|
229
|
+
sequences: {},
|
|
230
|
+
tables: {},
|
|
231
|
+
triggers: {},
|
|
232
|
+
eventTriggers: {},
|
|
233
|
+
rules: {},
|
|
234
|
+
ranges: {},
|
|
235
|
+
views: {},
|
|
236
|
+
foreignDataWrappers: {},
|
|
237
|
+
servers: {},
|
|
238
|
+
userMappings: {},
|
|
239
|
+
foreignTables: {},
|
|
240
|
+
depends: [],
|
|
241
|
+
};
|
|
242
|
+
await writeFile(jsonPath, JSON.stringify({ emptyCatalog }));
|
|
243
|
+
const result = await resolveIntegrationOptions({
|
|
244
|
+
integration: jsonPath,
|
|
245
|
+
});
|
|
246
|
+
expect(result.emptyCatalog).toEqual(emptyCatalog);
|
|
247
|
+
} finally {
|
|
248
|
+
await rm(dir, { recursive: true, force: true });
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
});
|
|
@@ -3,18 +3,19 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { readFile } from "node:fs/promises";
|
|
6
|
+
import type { CatalogSnapshot } from "../../core/catalog.snapshot.ts";
|
|
7
|
+
import type { FilterDSL } from "../../core/integrations/filter/dsl.ts";
|
|
6
8
|
import type { IntegrationDSL } from "../../core/integrations/integration-dsl.ts";
|
|
9
|
+
import { mergeIntegrations } from "../../core/integrations/merge.ts";
|
|
10
|
+
import type { SerializeDSL } from "../../core/integrations/serialize/dsl.ts";
|
|
7
11
|
|
|
8
12
|
/**
|
|
9
|
-
* Load
|
|
10
|
-
* If the path ends with .json, treats it as a JSON file path directly.
|
|
11
|
-
* Otherwise, tries to load from core integrations (TypeScript) first,
|
|
12
|
-
* then falls back to treating as a JSON file path.
|
|
13
|
+
* Load a raw integration DSL from a file or core integration (without resolving extends).
|
|
13
14
|
*
|
|
14
15
|
* @param nameOrPath - Integration name (e.g., "supabase") or file path (e.g., "./my-integration.json")
|
|
15
|
-
* @returns The loaded IntegrationDSL
|
|
16
|
+
* @returns The loaded IntegrationDSL (unresolved)
|
|
16
17
|
*/
|
|
17
|
-
|
|
18
|
+
async function loadRawIntegrationDSL(
|
|
18
19
|
nameOrPath: string,
|
|
19
20
|
): Promise<IntegrationDSL> {
|
|
20
21
|
// If path ends with .json, treat it as a JSON file path directly
|
|
@@ -40,3 +41,130 @@ export async function loadIntegrationDSL(
|
|
|
40
41
|
const content = await readFile(nameOrPath, "utf-8");
|
|
41
42
|
return JSON.parse(content) as IntegrationDSL;
|
|
42
43
|
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Load a core integration DSL by name only (no file path fallback).
|
|
47
|
+
*
|
|
48
|
+
* Used for resolving `extends` chains, which only support core integration names
|
|
49
|
+
* (e.g., "supabase"), not file paths.
|
|
50
|
+
*
|
|
51
|
+
* @param name - Core integration name (e.g., "supabase")
|
|
52
|
+
* @returns The loaded IntegrationDSL (unresolved)
|
|
53
|
+
*/
|
|
54
|
+
async function loadCoreIntegrationDSL(name: string): Promise<IntegrationDSL> {
|
|
55
|
+
try {
|
|
56
|
+
const module = await import(`../../core/integrations/${name}.ts`);
|
|
57
|
+
if (name in module) {
|
|
58
|
+
return module[name] as IntegrationDSL;
|
|
59
|
+
}
|
|
60
|
+
} catch {
|
|
61
|
+
// Module not found
|
|
62
|
+
}
|
|
63
|
+
throw new Error(
|
|
64
|
+
`Unknown core integration: "${name}". extends only supports core integration names (e.g., "supabase").`,
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Load an integration DSL, recursively resolving `extends` chains.
|
|
70
|
+
*
|
|
71
|
+
* When an integration has `extends`, the referenced integration(s) are loaded
|
|
72
|
+
* and merged: filters are AND-combined, serialize rules concatenated (base first),
|
|
73
|
+
* and emptyCatalog uses the most-specific value.
|
|
74
|
+
*
|
|
75
|
+
* Circular extends are detected and rejected with a descriptive error.
|
|
76
|
+
*
|
|
77
|
+
* @param nameOrPath - Integration name (e.g., "supabase") or file path
|
|
78
|
+
* @returns The fully resolved IntegrationDSL
|
|
79
|
+
*/
|
|
80
|
+
export async function loadIntegrationDSL(
|
|
81
|
+
nameOrPath: string,
|
|
82
|
+
): Promise<IntegrationDSL> {
|
|
83
|
+
return resolveIntegration(nameOrPath, new Set());
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function resolveIntegration(
|
|
87
|
+
nameOrPath: string,
|
|
88
|
+
visited: Set<string>,
|
|
89
|
+
preloadedRaw?: IntegrationDSL,
|
|
90
|
+
): Promise<IntegrationDSL> {
|
|
91
|
+
if (visited.has(nameOrPath)) {
|
|
92
|
+
throw new Error(
|
|
93
|
+
`Circular extends detected: ${[...visited, nameOrPath].join(" → ")}`,
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
visited.add(nameOrPath);
|
|
97
|
+
|
|
98
|
+
const raw = preloadedRaw ?? (await loadRawIntegrationDSL(nameOrPath));
|
|
99
|
+
|
|
100
|
+
if (!raw.extends) {
|
|
101
|
+
return raw;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Resolve base integrations (extends only supports core integration names)
|
|
105
|
+
const extendsArray = Array.isArray(raw.extends) ? raw.extends : [raw.extends];
|
|
106
|
+
|
|
107
|
+
const baseIntegrations: IntegrationDSL[] = [];
|
|
108
|
+
for (const baseName of extendsArray) {
|
|
109
|
+
const baseRaw = await loadCoreIntegrationDSL(baseName);
|
|
110
|
+
baseIntegrations.push(
|
|
111
|
+
await resolveIntegration(baseName, new Set(visited), baseRaw),
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Remove extends from the current integration before merging
|
|
116
|
+
const { extends: _, ...current } = raw;
|
|
117
|
+
|
|
118
|
+
// Merge: bases first (higher priority serialize), then current (most-specific)
|
|
119
|
+
return mergeIntegrations([...baseIntegrations, current]);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
interface ResolvedIntegrationOptions {
|
|
123
|
+
filter?: FilterDSL;
|
|
124
|
+
serialize?: SerializeDSL;
|
|
125
|
+
emptyCatalog?: CatalogSnapshot;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Load an integration (if provided) and merge its filter/serialize with CLI flags.
|
|
130
|
+
*
|
|
131
|
+
* - Filters are AND-combined (integration ∧ CLI flag)
|
|
132
|
+
* - Serialize rules are concatenated (integration first = higher priority)
|
|
133
|
+
* - emptyCatalog is extracted from the integration
|
|
134
|
+
*/
|
|
135
|
+
export async function resolveIntegrationOptions(options: {
|
|
136
|
+
filter?: FilterDSL;
|
|
137
|
+
serialize?: SerializeDSL;
|
|
138
|
+
integration?: string;
|
|
139
|
+
}): Promise<ResolvedIntegrationOptions> {
|
|
140
|
+
if (!options.integration) {
|
|
141
|
+
return {
|
|
142
|
+
filter: options.filter,
|
|
143
|
+
serialize: options.serialize,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const integrationDSL = await loadIntegrationDSL(options.integration);
|
|
148
|
+
|
|
149
|
+
// AND-combine integration filter with CLI --filter
|
|
150
|
+
let filter: FilterDSL | undefined;
|
|
151
|
+
if (integrationDSL.filter && options.filter) {
|
|
152
|
+
filter = { and: [integrationDSL.filter, options.filter] };
|
|
153
|
+
} else {
|
|
154
|
+
filter = options.filter ?? integrationDSL.filter;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Concatenate serialize rules (integration first = higher priority)
|
|
158
|
+
let serialize: SerializeDSL | undefined;
|
|
159
|
+
if (integrationDSL.serialize && options.serialize) {
|
|
160
|
+
serialize = [...integrationDSL.serialize, ...options.serialize];
|
|
161
|
+
} else {
|
|
162
|
+
serialize = options.serialize ?? integrationDSL.serialize;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return {
|
|
166
|
+
filter,
|
|
167
|
+
serialize,
|
|
168
|
+
emptyCatalog: integrationDSL.emptyCatalog,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
@@ -452,7 +452,11 @@ describe("catalog snapshot serde", () => {
|
|
|
452
452
|
|
|
453
453
|
// Filter out auth schema (no cascade): procedure is excluded but policy stays
|
|
454
454
|
const resultNoCascade = await createPlan(null, target, {
|
|
455
|
-
filter: {
|
|
455
|
+
filter: {
|
|
456
|
+
not: {
|
|
457
|
+
or: [{ "*/schema": ["auth"] }, { "schema/name": ["auth"] }],
|
|
458
|
+
},
|
|
459
|
+
},
|
|
456
460
|
});
|
|
457
461
|
expect(resultNoCascade).not.toBeNull();
|
|
458
462
|
if (resultNoCascade) {
|
|
@@ -464,7 +468,12 @@ describe("catalog snapshot serde", () => {
|
|
|
464
468
|
|
|
465
469
|
// Filter out auth schema with cascade: true: policy is also excluded (depends on auth.uid())
|
|
466
470
|
const resultCascade = await createPlan(null, target, {
|
|
467
|
-
filter: {
|
|
471
|
+
filter: {
|
|
472
|
+
not: {
|
|
473
|
+
or: [{ "*/schema": ["auth"] }, { "schema/name": ["auth"] }],
|
|
474
|
+
},
|
|
475
|
+
cascade: true,
|
|
476
|
+
},
|
|
468
477
|
});
|
|
469
478
|
expect(resultCascade).not.toBeNull();
|
|
470
479
|
if (resultCascade) {
|