@rebasepro/server-postgres 0.13.1-canary.gf57a27e → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/PostgresBootstrapper.d.ts +26 -0
- package/dist/auth/services.d.ts +21 -0
- package/dist/{auth-users-columns-Dt9g712t.js → auth-users-columns-BfQHf9JE.js} +525 -63
- package/dist/auth-users-columns-BfQHf9JE.js.map +1 -0
- package/dist/{backup-service-Bww-Lg0s.js → backup-service-BH0Dzo_h.js} +2 -3
- package/dist/{backup-service-Bww-Lg0s.js.map → backup-service-BH0Dzo_h.js.map} +1 -1
- package/dist/cli-output.d.ts +34 -0
- package/dist/data-transformer.d.ts +7 -2
- package/dist/data_driver-ULAyJEi9.js +193 -0
- package/dist/data_driver-ULAyJEi9.js.map +1 -0
- package/dist/ensure-collection-policies-8vuu-n4r.js +124 -0
- package/dist/ensure-collection-policies-8vuu-n4r.js.map +1 -0
- package/dist/{ensure-collection-tables-DRxaUG96.js → ensure-collection-tables-CbvaGuVn.js} +89 -10
- package/dist/ensure-collection-tables-CbvaGuVn.js.map +1 -0
- package/dist/index.es.js +1310 -1060
- package/dist/index.es.js.map +1 -1
- package/dist/{rls-bootstrap-sql-Bpv3nUZo.js → rls-bootstrap-sql-69hYT8nr.js} +2 -2
- package/dist/{rls-bootstrap-sql-Bpv3nUZo.js.map → rls-bootstrap-sql-69hYT8nr.js.map} +1 -1
- package/dist/rls-enforcement-BJ_3wxwg.js +425 -0
- package/dist/rls-enforcement-BJ_3wxwg.js.map +1 -0
- package/dist/schema/auth-schema.d.ts +102 -0
- package/dist/schema/doctor-policy-checks.d.ts +28 -0
- package/dist/schema/doctor.d.ts +41 -25
- package/dist/schema/ensure-collection-policies.d.ts +33 -9
- package/dist/schema/ensure-collection-tables.d.ts +60 -6
- package/dist/schema/generate-drizzle-schema-logic.d.ts +9 -1
- package/dist/schema/introspect-db-inference.d.ts +8 -1
- package/dist/schema/introspect-db-logic.d.ts +49 -0
- package/dist/schema/introspect-db-project.d.ts +21 -0
- package/dist/schema/search-column.d.ts +49 -0
- package/dist/security/policy-drift.d.ts +34 -0
- package/dist/security/rls-enforcement.d.ts +8 -3
- package/dist/services/FetchService.d.ts +9 -0
- package/dist/services/PersistService.d.ts +21 -17
- package/dist/services/RelationService.d.ts +9 -57
- package/dist/services/RelationWriteService.d.ts +82 -0
- package/dist/services/collection-helpers.d.ts +42 -0
- package/dist/services/dataService.d.ts +2 -0
- package/dist/services/junction-writes.d.ts +82 -0
- package/dist/services/realtimeService.d.ts +137 -2
- package/dist/services/write-denial.d.ts +36 -0
- package/dist/{src-C_wvdMnl.js → src-DCdn3Val.js} +35 -3
- package/dist/src-DCdn3Val.js.map +1 -0
- package/dist/utils/drizzle-conditions.d.ts +54 -1
- package/dist/{websocket-D0TBU3ia.js → websocket-C8ZqVBiV.js} +75 -18
- package/dist/websocket-C8ZqVBiV.js.map +1 -0
- package/package.json +6 -6
- package/src/PostgresBackendDriver.ts +7 -3
- package/src/PostgresBootstrapper.ts +95 -9
- package/src/auth/ensure-tables.ts +27 -5
- package/src/auth/services.ts +82 -5
- package/src/backup/backup-cli.ts +59 -57
- package/src/cli-errors.ts +6 -6
- package/src/cli-helpers.ts +4 -4
- package/src/cli-output.ts +43 -0
- package/src/cli.ts +155 -147
- package/src/collections/buildRegistry.ts +3 -1
- package/src/data-transformer.ts +111 -25
- package/src/history/ensure-history-table.ts +2 -2
- package/src/schema/auth-schema.ts +17 -1
- package/src/schema/doctor-cli.ts +14 -65
- package/src/schema/doctor-policy-checks.ts +105 -0
- package/src/schema/doctor.ts +149 -72
- package/src/schema/ensure-collection-policies.ts +99 -6
- package/src/schema/ensure-collection-tables.ts +214 -17
- package/src/schema/generate-drizzle-schema-logic.ts +121 -65
- package/src/schema/generate-drizzle-schema.ts +11 -10
- package/src/schema/generate-postgres-ddl-logic.ts +28 -1
- package/src/schema/generate-postgres-ddl.ts +14 -13
- package/src/schema/generated-schema-staleness.ts +7 -5
- package/src/schema/introspect-db-inference.ts +9 -2
- package/src/schema/introspect-db-logic.ts +251 -75
- package/src/schema/introspect-db-project.ts +78 -0
- package/src/schema/introspect-db.ts +42 -25
- package/src/schema/introspect-runtime.ts +14 -2
- package/src/schema/search-column.ts +85 -0
- package/src/security/policy-drift.test.ts +104 -3
- package/src/security/policy-drift.ts +129 -7
- package/src/security/rls-enforcement.ts +9 -4
- package/src/services/FetchService.ts +105 -7
- package/src/services/PersistService.ts +68 -42
- package/src/services/RelationService.ts +35 -695
- package/src/services/RelationWriteService.ts +653 -0
- package/src/services/cdc/trigger-cdc.ts +5 -1
- package/src/services/channel-history.ts +9 -3
- package/src/services/channel-presence.ts +10 -3
- package/src/services/collection-helpers.ts +89 -4
- package/src/services/dataService.ts +2 -0
- package/src/services/junction-writes.ts +295 -0
- package/src/services/pg-notify-listener.ts +1 -1
- package/src/services/realtimeService.ts +337 -82
- package/src/services/write-denial.ts +55 -0
- package/src/utils/drizzle-conditions.ts +211 -34
- package/src/utils/pg-error-utils.ts +8 -3
- package/src/websocket.ts +113 -16
- package/dist/auth-users-columns-Dt9g712t.js.map +0 -1
- package/dist/ensure-collection-policies-CwYUliAa.js +0 -57
- package/dist/ensure-collection-policies-CwYUliAa.js.map +0 -1
- package/dist/ensure-collection-tables-DRxaUG96.js.map +0 -1
- package/dist/policy-CPkCqVTz.js +0 -105
- package/dist/policy-CPkCqVTz.js.map +0 -1
- package/dist/src-C_wvdMnl.js.map +0 -1
- package/dist/websocket-D0TBU3ia.js.map +0 -1
|
@@ -427,6 +427,23 @@ export declare function createAuthSchema(usersSchemaName?: string): {
|
|
|
427
427
|
identity: undefined;
|
|
428
428
|
generated: undefined;
|
|
429
429
|
}, {}, {}>;
|
|
430
|
+
aal: import("drizzle-orm/pg-core").PgColumn<{
|
|
431
|
+
name: "aal";
|
|
432
|
+
tableName: "refresh_tokens";
|
|
433
|
+
dataType: "string";
|
|
434
|
+
columnType: "PgText";
|
|
435
|
+
data: string;
|
|
436
|
+
driverParam: string;
|
|
437
|
+
notNull: false;
|
|
438
|
+
hasDefault: false;
|
|
439
|
+
isPrimaryKey: false;
|
|
440
|
+
isAutoincrement: false;
|
|
441
|
+
hasRuntimeDefault: false;
|
|
442
|
+
enumValues: [string, ...string[]];
|
|
443
|
+
baseColumn: never;
|
|
444
|
+
identity: undefined;
|
|
445
|
+
generated: undefined;
|
|
446
|
+
}, {}, {}>;
|
|
430
447
|
userAgent: import("drizzle-orm/pg-core").PgColumn<{
|
|
431
448
|
name: "user_agent";
|
|
432
449
|
tableName: "refresh_tokens";
|
|
@@ -880,6 +897,23 @@ export declare function createAuthSchema(usersSchemaName?: string): {
|
|
|
880
897
|
identity: undefined;
|
|
881
898
|
generated: undefined;
|
|
882
899
|
}, {}, {}>;
|
|
900
|
+
lastUsedCounter: import("drizzle-orm/pg-core").PgColumn<{
|
|
901
|
+
name: "last_used_counter";
|
|
902
|
+
tableName: "mfa_factors";
|
|
903
|
+
dataType: "number";
|
|
904
|
+
columnType: "PgBigInt53";
|
|
905
|
+
data: number;
|
|
906
|
+
driverParam: string | number;
|
|
907
|
+
notNull: false;
|
|
908
|
+
hasDefault: false;
|
|
909
|
+
isPrimaryKey: false;
|
|
910
|
+
isAutoincrement: false;
|
|
911
|
+
hasRuntimeDefault: false;
|
|
912
|
+
enumValues: undefined;
|
|
913
|
+
baseColumn: never;
|
|
914
|
+
identity: undefined;
|
|
915
|
+
generated: undefined;
|
|
916
|
+
}, {}, {}>;
|
|
883
917
|
createdAt: import("drizzle-orm/pg-core").PgColumn<{
|
|
884
918
|
name: "created_at";
|
|
885
919
|
tableName: "mfa_factors";
|
|
@@ -1006,6 +1040,23 @@ export declare function createAuthSchema(usersSchemaName?: string): {
|
|
|
1006
1040
|
identity: undefined;
|
|
1007
1041
|
generated: undefined;
|
|
1008
1042
|
}, {}, {}>;
|
|
1043
|
+
attempts: import("drizzle-orm/pg-core").PgColumn<{
|
|
1044
|
+
name: "attempts";
|
|
1045
|
+
tableName: "mfa_challenges";
|
|
1046
|
+
dataType: "number";
|
|
1047
|
+
columnType: "PgInteger";
|
|
1048
|
+
data: number;
|
|
1049
|
+
driverParam: string | number;
|
|
1050
|
+
notNull: true;
|
|
1051
|
+
hasDefault: true;
|
|
1052
|
+
isPrimaryKey: false;
|
|
1053
|
+
isAutoincrement: false;
|
|
1054
|
+
hasRuntimeDefault: false;
|
|
1055
|
+
enumValues: undefined;
|
|
1056
|
+
baseColumn: never;
|
|
1057
|
+
identity: undefined;
|
|
1058
|
+
generated: undefined;
|
|
1059
|
+
}, {}, {}>;
|
|
1009
1060
|
expiresAt: import("drizzle-orm/pg-core").PgColumn<{
|
|
1010
1061
|
name: "expires_at";
|
|
1011
1062
|
tableName: "mfa_challenges";
|
|
@@ -1642,6 +1693,23 @@ export declare const refreshTokens: import("drizzle-orm/pg-core").PgTableWithCol
|
|
|
1642
1693
|
identity: undefined;
|
|
1643
1694
|
generated: undefined;
|
|
1644
1695
|
}, {}, {}>;
|
|
1696
|
+
aal: import("drizzle-orm/pg-core").PgColumn<{
|
|
1697
|
+
name: "aal";
|
|
1698
|
+
tableName: "refresh_tokens";
|
|
1699
|
+
dataType: "string";
|
|
1700
|
+
columnType: "PgText";
|
|
1701
|
+
data: string;
|
|
1702
|
+
driverParam: string;
|
|
1703
|
+
notNull: false;
|
|
1704
|
+
hasDefault: false;
|
|
1705
|
+
isPrimaryKey: false;
|
|
1706
|
+
isAutoincrement: false;
|
|
1707
|
+
hasRuntimeDefault: false;
|
|
1708
|
+
enumValues: [string, ...string[]];
|
|
1709
|
+
baseColumn: never;
|
|
1710
|
+
identity: undefined;
|
|
1711
|
+
generated: undefined;
|
|
1712
|
+
}, {}, {}>;
|
|
1645
1713
|
userAgent: import("drizzle-orm/pg-core").PgColumn<{
|
|
1646
1714
|
name: "user_agent";
|
|
1647
1715
|
tableName: "refresh_tokens";
|
|
@@ -2095,6 +2163,23 @@ export declare const mfaFactors: import("drizzle-orm/pg-core").PgTableWithColumn
|
|
|
2095
2163
|
identity: undefined;
|
|
2096
2164
|
generated: undefined;
|
|
2097
2165
|
}, {}, {}>;
|
|
2166
|
+
lastUsedCounter: import("drizzle-orm/pg-core").PgColumn<{
|
|
2167
|
+
name: "last_used_counter";
|
|
2168
|
+
tableName: "mfa_factors";
|
|
2169
|
+
dataType: "number";
|
|
2170
|
+
columnType: "PgBigInt53";
|
|
2171
|
+
data: number;
|
|
2172
|
+
driverParam: string | number;
|
|
2173
|
+
notNull: false;
|
|
2174
|
+
hasDefault: false;
|
|
2175
|
+
isPrimaryKey: false;
|
|
2176
|
+
isAutoincrement: false;
|
|
2177
|
+
hasRuntimeDefault: false;
|
|
2178
|
+
enumValues: undefined;
|
|
2179
|
+
baseColumn: never;
|
|
2180
|
+
identity: undefined;
|
|
2181
|
+
generated: undefined;
|
|
2182
|
+
}, {}, {}>;
|
|
2098
2183
|
createdAt: import("drizzle-orm/pg-core").PgColumn<{
|
|
2099
2184
|
name: "created_at";
|
|
2100
2185
|
tableName: "mfa_factors";
|
|
@@ -2221,6 +2306,23 @@ export declare const mfaChallenges: import("drizzle-orm/pg-core").PgTableWithCol
|
|
|
2221
2306
|
identity: undefined;
|
|
2222
2307
|
generated: undefined;
|
|
2223
2308
|
}, {}, {}>;
|
|
2309
|
+
attempts: import("drizzle-orm/pg-core").PgColumn<{
|
|
2310
|
+
name: "attempts";
|
|
2311
|
+
tableName: "mfa_challenges";
|
|
2312
|
+
dataType: "number";
|
|
2313
|
+
columnType: "PgInteger";
|
|
2314
|
+
data: number;
|
|
2315
|
+
driverParam: string | number;
|
|
2316
|
+
notNull: true;
|
|
2317
|
+
hasDefault: true;
|
|
2318
|
+
isPrimaryKey: false;
|
|
2319
|
+
isAutoincrement: false;
|
|
2320
|
+
hasRuntimeDefault: false;
|
|
2321
|
+
enumValues: undefined;
|
|
2322
|
+
baseColumn: never;
|
|
2323
|
+
identity: undefined;
|
|
2324
|
+
generated: undefined;
|
|
2325
|
+
}, {}, {}>;
|
|
2224
2326
|
expiresAt: import("drizzle-orm/pg-core").PgColumn<{
|
|
2225
2327
|
name: "expires_at";
|
|
2226
2328
|
tableName: "mfa_challenges";
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the RLS checks concluded.
|
|
3
|
+
*
|
|
4
|
+
* `unchecked` exists because "we could not look" and "we looked and it is fine"
|
|
5
|
+
* used to be reported identically: a collections path that did not resolve made
|
|
6
|
+
* the loader return `[]` (it warns, it does not throw), `checkPolicyDrift`
|
|
7
|
+
* early-returned an empty diff, and the gate printed
|
|
8
|
+
* `✓ RLS policies match your collections` having compared zero policies against
|
|
9
|
+
* zero collections. Any exception at all — a collection file that throws on
|
|
10
|
+
* import, a `pg_policies` read the CI role is not granted, a connection reset —
|
|
11
|
+
* did the same thing through a `warn`, and exited 0.
|
|
12
|
+
*/
|
|
13
|
+
export type PolicyCheckStatus = "ok" | "problems" | "unchecked";
|
|
14
|
+
/**
|
|
15
|
+
* The exit code for `rebase doctor --policies`.
|
|
16
|
+
*
|
|
17
|
+
* A gate that could not run has not passed. Only a completed, clean check
|
|
18
|
+
* exits 0 — anything else, including "we never opened a connection", is a
|
|
19
|
+
* failure, or the flag certifies a database nobody looked at.
|
|
20
|
+
*/
|
|
21
|
+
export declare function exitCodeForPolicyGate(status: PolicyCheckStatus): 0 | 1;
|
|
22
|
+
/**
|
|
23
|
+
* Policies actually deployed vs the ones the collections describe, plus policy
|
|
24
|
+
* roles this server could never satisfy.
|
|
25
|
+
*
|
|
26
|
+
* Never reports `ok` for work it did not do — see {@link PolicyCheckStatus}.
|
|
27
|
+
*/
|
|
28
|
+
export declare function runPolicyChecks(collectionsPath: string, databaseUrl?: string): Promise<PolicyCheckStatus>;
|
package/dist/schema/doctor.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { CollectionConfig, Property } from "@rebasepro/types";
|
|
|
2
2
|
export type IssueSeverity = "error" | "warning" | "info";
|
|
3
3
|
export interface DoctorIssue {
|
|
4
4
|
severity: IssueSeverity;
|
|
5
|
-
category: "missing_table" | "missing_column" | "type_mismatch" | "missing_constraint" | "schema_stale" | "missing_enum" | "enum_value_mismatch" | "missing_foreign_key" | "sdk_stale" | "sdk_not_generated";
|
|
5
|
+
category: "missing_table" | "missing_column" | "type_mismatch" | "missing_constraint" | "schema_stale" | "missing_enum" | "enum_value_mismatch" | "missing_foreign_key" | "sdk_stale" | "sdk_not_generated" | "sdk_ungeneratable";
|
|
6
6
|
table?: string;
|
|
7
7
|
column?: string;
|
|
8
8
|
expected?: string;
|
|
@@ -10,21 +10,46 @@ export interface DoctorIssue {
|
|
|
10
10
|
message: string;
|
|
11
11
|
fix: string;
|
|
12
12
|
}
|
|
13
|
+
export interface DoctorPhase {
|
|
14
|
+
passed: boolean;
|
|
15
|
+
issues: DoctorIssue[];
|
|
16
|
+
/**
|
|
17
|
+
* Why this phase never ran, when it did not.
|
|
18
|
+
*
|
|
19
|
+
* A check that did not happen is a third state, not a passing one. While a
|
|
20
|
+
* skipped phase initialised to `{ passed: true, issues: [] }` it rendered as
|
|
21
|
+
* `✅ Collections → Database: In sync` and counted towards
|
|
22
|
+
* `✓ All schemas are in sync!` — so a project whose connection string was
|
|
23
|
+
* spelled `POSTGRES_URL`, or a CI job that never exported one, got two green
|
|
24
|
+
* ticks and exit 0 against a database with no tables in it.
|
|
25
|
+
*/
|
|
26
|
+
skipped?: string;
|
|
27
|
+
/**
|
|
28
|
+
* Why this phase had nothing to compare against, when it had nothing.
|
|
29
|
+
*
|
|
30
|
+
* Distinct from `skipped`, which means "the check could not run, and that
|
|
31
|
+
* is probably worth fixing". This one means "the artifact is optional and
|
|
32
|
+
* you have not asked for it": no drift is possible, so the run is still a
|
|
33
|
+
* clean bill of health.
|
|
34
|
+
*
|
|
35
|
+
* It exists because the alternative was a contradiction. The typed-SDK
|
|
36
|
+
* phase returned `{ passed: true }` when `generated/sdk/database.types.ts`
|
|
37
|
+
* did not exist, so a fresh project's report read
|
|
38
|
+
* `✅ Collections → SDK Types: In sync` directly above
|
|
39
|
+
* `ℹ Typed SDK not generated (optional).` — one line calling a file
|
|
40
|
+
* synchronised and the next saying it is absent. "In sync" is a claim about
|
|
41
|
+
* a comparison, and no comparison happened.
|
|
42
|
+
*/
|
|
43
|
+
notApplicable?: string;
|
|
44
|
+
}
|
|
13
45
|
export interface DoctorReport {
|
|
14
|
-
collectionsToSchema:
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
};
|
|
18
|
-
collectionsToSdk: {
|
|
19
|
-
passed: boolean;
|
|
20
|
-
issues: DoctorIssue[];
|
|
21
|
-
};
|
|
22
|
-
schemaToDatabase: {
|
|
23
|
-
passed: boolean;
|
|
24
|
-
issues: DoctorIssue[];
|
|
25
|
-
};
|
|
46
|
+
collectionsToSchema: DoctorPhase;
|
|
47
|
+
collectionsToSdk: DoctorPhase;
|
|
48
|
+
schemaToDatabase: DoctorPhase;
|
|
26
49
|
summary: {
|
|
27
50
|
passed: number;
|
|
51
|
+
skipped: number;
|
|
52
|
+
notApplicable: number;
|
|
28
53
|
warnings: number;
|
|
29
54
|
errors: number;
|
|
30
55
|
};
|
|
@@ -37,18 +62,9 @@ export declare function getExpectedColumnType(prop: Property): string | null;
|
|
|
37
62
|
* wrong thing.
|
|
38
63
|
*/
|
|
39
64
|
export declare function loadCollections(collectionsPath: string): Promise<CollectionConfig[]>;
|
|
40
|
-
export declare function checkCollectionsVsSchema(collections: CollectionConfig[], schemaFilePath: string): Promise<
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
}>;
|
|
44
|
-
export declare function checkCollectionsVsSdk(collections: CollectionConfig[], sdkFilePath: string): Promise<{
|
|
45
|
-
passed: boolean;
|
|
46
|
-
issues: DoctorIssue[];
|
|
47
|
-
}>;
|
|
48
|
-
export declare function checkCollectionsVsDatabase(collections: CollectionConfig[], databaseUrl: string): Promise<{
|
|
49
|
-
passed: boolean;
|
|
50
|
-
issues: DoctorIssue[];
|
|
51
|
-
}>;
|
|
65
|
+
export declare function checkCollectionsVsSchema(collections: CollectionConfig[], schemaFilePath: string): Promise<DoctorPhase>;
|
|
66
|
+
export declare function checkCollectionsVsSdk(collections: CollectionConfig[], sdkFilePath: string): Promise<DoctorPhase>;
|
|
67
|
+
export declare function checkCollectionsVsDatabase(collections: CollectionConfig[], databaseUrl: string): Promise<DoctorPhase>;
|
|
52
68
|
export declare function renderReport(report: DoctorReport): void;
|
|
53
69
|
export declare function runDoctor(options: {
|
|
54
70
|
collectionsPath: string;
|
|
@@ -43,18 +43,42 @@ export interface PolicyEnsureResult {
|
|
|
43
43
|
table: string;
|
|
44
44
|
reason: string;
|
|
45
45
|
}[];
|
|
46
|
-
/**
|
|
46
|
+
/**
|
|
47
|
+
* Tables that have RLS on but did not get every policy. They deny — RLS
|
|
48
|
+
* with no matching policy is deny-all — so they are safe but not servable.
|
|
49
|
+
*/
|
|
47
50
|
failures: {
|
|
48
51
|
table: string;
|
|
49
52
|
error: string;
|
|
50
53
|
}[];
|
|
54
|
+
/**
|
|
55
|
+
* Tables RLS could not be enabled on, whose DML grant was withdrawn instead.
|
|
56
|
+
*
|
|
57
|
+
* This state had no name, and that was the bug: `ENABLE ROW LEVEL SECURITY`
|
|
58
|
+
* failing was recorded as a `failure` and reported with the same "it stays
|
|
59
|
+
* locked (denies)" wording as a failed policy — but the two are opposites.
|
|
60
|
+
* A policy statement failing leaves RLS on and the table denying. `enableRls`
|
|
61
|
+
* failing leaves RLS *off*, and the schema-wide grant to the user role has
|
|
62
|
+
* already been made by `ensureRlsEnforcement`, so the table is readable and
|
|
63
|
+
* writable by every authenticated request with no row filtering at all.
|
|
64
|
+
*/
|
|
65
|
+
unsecured: {
|
|
66
|
+
table: string;
|
|
67
|
+
error: string;
|
|
68
|
+
grantWithdrawn: boolean;
|
|
69
|
+
}[];
|
|
70
|
+
/**
|
|
71
|
+
* Generated policies removed because no current rule produces them.
|
|
72
|
+
*
|
|
73
|
+
* A policy's name embeds a hash of the rule's semantics, so editing a rule
|
|
74
|
+
* does not update a policy — it creates a new one and abandons the old.
|
|
75
|
+
* Postgres ORs permissive policies, so the abandoned one keeps granting:
|
|
76
|
+
* a `USING (true)` tightened to an owner check went on admitting everyone,
|
|
77
|
+
* forever, while the deploy logged success.
|
|
78
|
+
*
|
|
79
|
+
* `db push` reconciles this, and cannot reach a managed tenant's in-cluster
|
|
80
|
+
* database — which is the reason this module exists. So boot has to do it.
|
|
81
|
+
*/
|
|
82
|
+
orphansDropped: number;
|
|
51
83
|
}
|
|
52
|
-
/**
|
|
53
|
-
* Bring the declared collections' RLS policies up to date. Returns what it did.
|
|
54
|
-
*
|
|
55
|
-
* Only tables that already exist are touched: the boot-time table creator runs
|
|
56
|
-
* first, so anything still missing is a table this additive path is not allowed
|
|
57
|
-
* to create (a junction, or a relation left to a migration). Enabling RLS on a
|
|
58
|
-
* non-existent table would error, so those are recorded as skipped, not failed.
|
|
59
|
-
*/
|
|
60
84
|
export declare function ensureCollectionPolicies(client: Queryable, collections: CollectionConfig[], log?: (message: string) => void): Promise<PolicyEnsureResult>;
|
|
@@ -54,9 +54,17 @@ export interface ExistingSchema {
|
|
|
54
54
|
* constraint that then fails harmlessly as a duplicate.
|
|
55
55
|
*/
|
|
56
56
|
constraints?: Set<string>;
|
|
57
|
+
/**
|
|
58
|
+
* `schema.table.column` → that column's comment, for the columns that have
|
|
59
|
+
* one. This is where a generated search column's fingerprint lives, so it
|
|
60
|
+
* is the only evidence that a `search` block has changed since the column
|
|
61
|
+
* was built. Absent is read as "no column is stamped", which plans a stamp
|
|
62
|
+
* and reports nothing as drifted.
|
|
63
|
+
*/
|
|
64
|
+
columnComments?: Map<string, string>;
|
|
57
65
|
}
|
|
58
66
|
export interface EnsureAction {
|
|
59
|
-
kind: "create-enum" | "create-table" | "add-column" | "add-constraint" | "rename-column" | "create-extension" | "create-function" | "create-index";
|
|
67
|
+
kind: "create-enum" | "create-table" | "add-column" | "add-constraint" | "rename-column" | "create-extension" | "create-function" | "create-index" | "comment-column";
|
|
60
68
|
/** Qualified target, for logging: `public.posts` or `public.posts.title`. */
|
|
61
69
|
target: string;
|
|
62
70
|
sql: string;
|
|
@@ -76,6 +84,49 @@ export interface EnsurePlan {
|
|
|
76
84
|
* resolving to nothing — which is indistinguishable from having no data.
|
|
77
85
|
*/
|
|
78
86
|
legacyForeignKeys: LegacyForeignKey[];
|
|
87
|
+
/**
|
|
88
|
+
* Generated search columns whose `search` block has changed since they were
|
|
89
|
+
* built. Reported, never planned into `actions` — see
|
|
90
|
+
* {@link SearchColumnDrift} for why applying it is not this path's call.
|
|
91
|
+
*/
|
|
92
|
+
searchDrift: SearchColumnDrift[];
|
|
93
|
+
/**
|
|
94
|
+
* Generated search columns that exist but carry no fingerprint — created
|
|
95
|
+
* before this check existed, or by `search.sql` on an older CLI. The plan
|
|
96
|
+
* stamps them so the *next* change is detectable; whether they match the
|
|
97
|
+
* current block cannot be known, which is what the caller reports.
|
|
98
|
+
*/
|
|
99
|
+
searchAdopted: {
|
|
100
|
+
table: string;
|
|
101
|
+
column: string;
|
|
102
|
+
}[];
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* A generated search column built from a `search` block that has since changed.
|
|
106
|
+
*
|
|
107
|
+
* Reported instead of applied because the two ways to apply it are both worse
|
|
108
|
+
* than stopping. `ALTER COLUMN … SET EXPRESSION` exists only on PG17+ and
|
|
109
|
+
* rewrites the table either way; `DROP COLUMN` + `ADD COLUMN` rewrites it under
|
|
110
|
+
* an ACCESS EXCLUSIVE lock and rebuilds the GIN index. This module runs
|
|
111
|
+
* unattended against live customer data with nobody reading a diff — the same
|
|
112
|
+
* reason it withholds `SET NOT NULL` from an adopted table — so a multi-minute
|
|
113
|
+
* outage is not a decision it may take on its own.
|
|
114
|
+
*
|
|
115
|
+
* Not applying it silently is not an option either: that is the bug this
|
|
116
|
+
* detection exists for. A collection that added a field, flipped `unaccent` or
|
|
117
|
+
* raised a weight kept indexing the *old* set forever, and the only symptom was
|
|
118
|
+
* searches returning nothing for content plainly in the row.
|
|
119
|
+
*/
|
|
120
|
+
export interface SearchColumnDrift {
|
|
121
|
+
/** `schema.table`. */
|
|
122
|
+
table: string;
|
|
123
|
+
column: string;
|
|
124
|
+
/** The fingerprint recorded on the column. */
|
|
125
|
+
found: string;
|
|
126
|
+
/** The fingerprint the current `search` block computes. */
|
|
127
|
+
expected: string;
|
|
128
|
+
/** The statements that would rebuild the column, for the operator to run. */
|
|
129
|
+
rebuild: string[];
|
|
79
130
|
}
|
|
80
131
|
/** A relation column whose old and new spellings both plausibly apply. */
|
|
81
132
|
export interface LegacyForeignKey {
|
|
@@ -88,14 +139,17 @@ export interface LegacyForeignKey {
|
|
|
88
139
|
}
|
|
89
140
|
export interface EnsureOutcome extends EnsurePlan {
|
|
90
141
|
/**
|
|
91
|
-
*
|
|
142
|
+
* Actions that could not be applied and are non-fatal by nature.
|
|
92
143
|
*
|
|
93
|
-
* A foreign key can only fail on data that already
|
|
94
|
-
* column it would police exists either way, so the
|
|
95
|
-
*
|
|
96
|
-
* outage.
|
|
144
|
+
* Two kinds qualify. A foreign key can only fail on data that already
|
|
145
|
+
* violates it, and the column it would police exists either way, so the
|
|
146
|
+
* collection still serves; refusing to boot over one would turn a
|
|
147
|
+
* pre-existing data problem into an outage. A column comment is the search
|
|
148
|
+
* fingerprint, which needs table ownership — losing it costs drift
|
|
149
|
+
* detection on the next boot, not the deployment. Both are reported loudly.
|
|
97
150
|
*/
|
|
98
151
|
failures: {
|
|
152
|
+
kind: EnsureAction["kind"];
|
|
99
153
|
target: string;
|
|
100
154
|
error: string;
|
|
101
155
|
}[];
|
|
@@ -1,2 +1,10 @@
|
|
|
1
|
-
import { CollectionConfig } from "@rebasepro/types";
|
|
1
|
+
import { CollectionConfig, Property } from "@rebasepro/types";
|
|
2
|
+
/**
|
|
3
|
+
* The Drizzle column declaration a property compiles to, or `null` when the
|
|
4
|
+
* property puts no column on *this* table (an inverse relation, whose column
|
|
5
|
+
* lives on the target). Exported so it can be checked against its DDL twin
|
|
6
|
+
* `getSqlColumnType` directly — the two disagreeing is what left `geopoint`
|
|
7
|
+
* with a database column and no Drizzle key.
|
|
8
|
+
*/
|
|
9
|
+
export declare const getDrizzleColumn: (propName: string, prop: Property, collection: CollectionConfig, collections: CollectionConfig[]) => string | null;
|
|
2
10
|
export declare const generateSchema: (allCollections: CollectionConfig[], stripPolicies?: boolean) => Promise<string>;
|
|
@@ -2,4 +2,11 @@ export interface InferenceResult {
|
|
|
2
2
|
propType?: string;
|
|
3
3
|
extra?: string;
|
|
4
4
|
}
|
|
5
|
-
export declare function inferPropertyFromData(columnName: string, pgDataType: string, currentPropType: string, sampleValues: unknown[], isPk: boolean
|
|
5
|
+
export declare function inferPropertyFromData(columnName: string, pgDataType: string, currentPropType: string, sampleValues: unknown[], isPk: boolean,
|
|
6
|
+
/**
|
|
7
|
+
* False when generating for a project without `@rebasepro/admin-types`, where
|
|
8
|
+
* `BaseProperty` declares no `admin` field and the block below would not
|
|
9
|
+
* compile. The type-level inferences (`propType`, `url`, `storage`) are
|
|
10
|
+
* unaffected — only the form-widget hints are dropped.
|
|
11
|
+
*/
|
|
12
|
+
emitAdmin?: boolean): InferenceResult;
|
|
@@ -177,10 +177,59 @@ export interface GeneratedFile {
|
|
|
177
177
|
* have no database to read constraints or row counts from, and they must keep
|
|
178
178
|
* producing a valid collection.
|
|
179
179
|
*/
|
|
180
|
+
/**
|
|
181
|
+
* Which `defineCollection` — if any — the project being generated into can import.
|
|
182
|
+
*
|
|
183
|
+
* A bare `const x: PostgresCollectionConfig = { … }` annotation widens `properties`
|
|
184
|
+
* to `Record<string, …>`, and every key-shaped field in the admin block —
|
|
185
|
+
* `titleProperty`, `sort`, `propertiesOrder`, `listProperties`, `fixedFilter` — is
|
|
186
|
+
* derived from those keys. Annotated, they accept any string: introspection was
|
|
187
|
+
* emitting a `propertiesOrder` array that nothing checked, so renaming a column and
|
|
188
|
+
* re-introspecting left a stale key that compiled silently. `defineCollection` is
|
|
189
|
+
* the identity function whose `const P` type parameter keeps the keys literal, which
|
|
190
|
+
* is what turns that checking on.
|
|
191
|
+
*
|
|
192
|
+
* There are two of them and they are not interchangeable:
|
|
193
|
+
*
|
|
194
|
+
* - `admin-types` — `@rebasepro/admin-types`. Its index side-effect-imports
|
|
195
|
+
* `augment.ts`, so importing it is also what *declares* the `admin` block. Only a
|
|
196
|
+
* project that depends on the package can resolve it.
|
|
197
|
+
* - `common` — `@rebasepro/common`. Same key inference, no admin surface, no React
|
|
198
|
+
* anywhere in its graph (`scripts/headless-guard` lists it as core). This is the
|
|
199
|
+
* headless flavour.
|
|
200
|
+
* - `annotation` — neither package is declared, so neither import would resolve and
|
|
201
|
+
* the old annotation is the only honest thing to emit. Projects scaffolded before
|
|
202
|
+
* `@rebasepro/common` joined the headless config package land here.
|
|
203
|
+
*
|
|
204
|
+
* The last two emit **no admin block, on the collection or on any property**. That is
|
|
205
|
+
* not a downgrade: `@rebasepro/types` declares no `admin` field at all, so the block
|
|
206
|
+
* introspection used to emit was a type error in every headless project it was
|
|
207
|
+
* written into. See `packages/admin-types/src/augment.ts`.
|
|
208
|
+
*/
|
|
209
|
+
export type CollectionBuilder = "admin-types" | "common" | "annotation";
|
|
210
|
+
/**
|
|
211
|
+
* The package specifiers the generated files name, spelled once.
|
|
212
|
+
*
|
|
213
|
+
* Written as constants rather than inline in the import templates below because
|
|
214
|
+
* `scripts/headless-guard/check-types.mjs` scans core sources for `from
|
|
215
|
+
* "@rebasepro/admin-types"` and cannot tell a real import from one this module
|
|
216
|
+
* *writes*. It is right to be that blunt — the guard's whole value is that it
|
|
217
|
+
* cannot be reasoned around — so the string simply never appears in that shape
|
|
218
|
+
* here. Inlining them back into the templates re-breaks `check:types-headless`.
|
|
219
|
+
*/
|
|
220
|
+
export declare const ADMIN_TYPES_PACKAGE = "@rebasepro/admin-types";
|
|
221
|
+
export declare const COMMON_PACKAGE = "@rebasepro/common";
|
|
222
|
+
export declare const TYPES_PACKAGE = "@rebasepro/types";
|
|
180
223
|
export interface GenerationContext {
|
|
181
224
|
metadata?: SchemaMetadata;
|
|
182
225
|
classifications?: Map<string, TableClassification>;
|
|
183
226
|
checkFacts?: CheckFactsByTable;
|
|
227
|
+
/**
|
|
228
|
+
* Defaults to `admin-types`, which is what the generator has always emitted.
|
|
229
|
+
* The CLI never relies on the default — `introspect-db.ts` detects the flavour
|
|
230
|
+
* from the target project and passes it. See `detectCollectionBuilder`.
|
|
231
|
+
*/
|
|
232
|
+
builder?: CollectionBuilder;
|
|
184
233
|
}
|
|
185
234
|
/**
|
|
186
235
|
* Generate the full TypeScript file content for a single collection.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type CollectionBuilder } from "./introspect-db-logic";
|
|
2
|
+
/**
|
|
3
|
+
* Which `defineCollection` to generate against, for collections written to `outDir`.
|
|
4
|
+
*
|
|
5
|
+
* **The detection is the package manifests above the output directory**, unioned from
|
|
6
|
+
* `outDir` up to and including the project root — the first ancestor holding a
|
|
7
|
+
* `rebase.json`, or `MAX_LEVELS` up if there is none.
|
|
8
|
+
*
|
|
9
|
+
* That is the rule Node itself applies: a file in `config/collections` resolves a bare
|
|
10
|
+
* specifier through `config/node_modules`, then `<project>/node_modules`, and so on up.
|
|
11
|
+
* Reading the manifests along that same path answers the only question that matters —
|
|
12
|
+
* *will this import resolve in the project I am writing into* — from the state on disk
|
|
13
|
+
* at the moment of generation. The alternatives are all proxies for it: `rebase.json`'s
|
|
14
|
+
* `apps` block says a CMS scaffold declared an admin app, and a `frontend/` directory
|
|
15
|
+
* says one was scaffolded, but neither is what the compiler consults, and either can be
|
|
16
|
+
* true of a project whose `config` package does not depend on `@rebasepro/admin-types`.
|
|
17
|
+
*
|
|
18
|
+
* Ambiguity resolves towards the admin panel: a project that declares both packages has
|
|
19
|
+
* a panel, and `@rebasepro/admin-types` is the flavour that keeps the `admin` block.
|
|
20
|
+
*/
|
|
21
|
+
export declare function detectCollectionBuilder(outDir: string): CollectionBuilder;
|
|
@@ -154,6 +154,55 @@ export declare const fuzzyColumnDefinition: (spec: SearchColumnSpec) => string |
|
|
|
154
154
|
* concurrent form instead; see `ensureSearchColumns`.
|
|
155
155
|
*/
|
|
156
156
|
export declare const searchIndexStatements: (spec: SearchColumnSpec) => string[];
|
|
157
|
+
/**
|
|
158
|
+
* Marker on the comment of every generated search column this module creates.
|
|
159
|
+
*
|
|
160
|
+
* Versioned because the fingerprint below is only comparable against itself: a
|
|
161
|
+
* future change to how it is computed has to read as "not stamped by this
|
|
162
|
+
* version" rather than as drift on every existing column.
|
|
163
|
+
*/
|
|
164
|
+
export declare const SEARCH_STAMP_PREFIX = "rebase:search:v1:";
|
|
165
|
+
/**
|
|
166
|
+
* A stable fingerprint of one generated column's expression.
|
|
167
|
+
*
|
|
168
|
+
* Why a stamp rather than reading the expression back: Postgres stores a
|
|
169
|
+
* generated column's expression *parsed*, and hands it back deparsed — casts
|
|
170
|
+
* made explicit, identifiers requoted, schema qualifications added or dropped
|
|
171
|
+
* according to `search_path`. Comparing that text to the text we generated
|
|
172
|
+
* would report drift on wording, and this comparison decides whether a boot
|
|
173
|
+
* refuses, so a false positive is an outage. The stamp is written by the same
|
|
174
|
+
* code that writes the column, so equality means what it says.
|
|
175
|
+
*/
|
|
176
|
+
export declare const searchExpressionFingerprint: (expression: string) => string;
|
|
177
|
+
/** One generated column, with the fingerprint that identifies its expression. */
|
|
178
|
+
export interface SearchColumnStamp {
|
|
179
|
+
column: string;
|
|
180
|
+
/** The expression the column is generated from. */
|
|
181
|
+
expression: string;
|
|
182
|
+
fingerprint: string;
|
|
183
|
+
/** `COMMENT ON COLUMN …`, which is where the fingerprint is recorded. */
|
|
184
|
+
sql: string;
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* The stamps for a spec's generated columns — one per column, never shared.
|
|
188
|
+
*
|
|
189
|
+
* Per column on purpose: turning `fuzzy` on adds a second column and changes
|
|
190
|
+
* nothing about the first, and a spec-wide fingerprint would report the
|
|
191
|
+
* untouched `tsvector` column as drifted and refuse a boot over a change that
|
|
192
|
+
* is purely additive.
|
|
193
|
+
*/
|
|
194
|
+
export declare const searchColumnStamps: (spec: SearchColumnSpec) => SearchColumnStamp[];
|
|
195
|
+
/**
|
|
196
|
+
* The same drift check as the boot ensure, for the SQL file.
|
|
197
|
+
*
|
|
198
|
+
* Needed because {@link searchColumnStamps} would otherwise *launder* drift on
|
|
199
|
+
* the migration path: `ADD COLUMN IF NOT EXISTS` does nothing to a column that
|
|
200
|
+
* exists, so a re-generated `search.sql` would stamp a stale column with the
|
|
201
|
+
* new block's fingerprint and the next boot would find them in agreement.
|
|
202
|
+
* Guarding first means the file refuses instead — `rebase db push` is attended,
|
|
203
|
+
* and the operator reading the failure is the person who changed the block.
|
|
204
|
+
*/
|
|
205
|
+
export declare const searchStampGuards: (spec: SearchColumnSpec) => string[];
|
|
157
206
|
/**
|
|
158
207
|
* The index names the spec creates.
|
|
159
208
|
*
|
|
@@ -26,6 +26,20 @@ export interface PolicyRef {
|
|
|
26
26
|
hasUsing: boolean;
|
|
27
27
|
/** Whether a WITH CHECK clause is present at all (not what it says). */
|
|
28
28
|
hasWithCheck: boolean;
|
|
29
|
+
/**
|
|
30
|
+
* PERMISSIVE or RESTRICTIVE — the `AS` clause.
|
|
31
|
+
*
|
|
32
|
+
* An exact catalogue value on both sides, so it belongs with roles and
|
|
33
|
+
* command rather than with the expression text. It matters more than either:
|
|
34
|
+
* permissive policies are ORed together and restrictive ones ANDed, so a rule
|
|
35
|
+
* declared `mode: "restrictive"` whose live policy is PERMISSIVE has had its
|
|
36
|
+
* gate turned from a requirement into an alternative — the maximally
|
|
37
|
+
* permissive way for this to be wrong.
|
|
38
|
+
*
|
|
39
|
+
* The DDL regex captured this from the start and the destructuring threw it
|
|
40
|
+
* away; `pg_policies.permissive` was never selected.
|
|
41
|
+
*/
|
|
42
|
+
mode?: "PERMISSIVE" | "RESTRICTIVE";
|
|
29
43
|
/**
|
|
30
44
|
* The live clause text, when read from `pg_policies`. Present only for live
|
|
31
45
|
* policies (the expected side is parsed from DDL and does not carry it).
|
|
@@ -62,6 +76,26 @@ export interface PolicyDrift {
|
|
|
62
76
|
policy: PolicyRef;
|
|
63
77
|
reason: string;
|
|
64
78
|
}[];
|
|
79
|
+
/**
|
|
80
|
+
* A table the collections describe whose RLS switch is off.
|
|
81
|
+
*
|
|
82
|
+
* `ALTER TABLE posts DISABLE ROW LEVEL SECURITY` leaves every row in
|
|
83
|
+
* `pg_policies` untouched, so before this category every expected policy
|
|
84
|
+
* still matched on name, roles, command and clause presence and the checker
|
|
85
|
+
* reported clean — on a table Postgres was applying no filter to at all.
|
|
86
|
+
* Requests run as `rebase_user`, which holds full DML, so the table is wide
|
|
87
|
+
* open while `doctor` certifies it.
|
|
88
|
+
*
|
|
89
|
+
* `forced` reports `relforcerowsecurity`, which is what also subjects the
|
|
90
|
+
* table's *owner* to its policies. Its absence is not drift on its own —
|
|
91
|
+
* Rebase does not connect as the owner in the request path — so it is
|
|
92
|
+
* reported for context rather than raised as a failure.
|
|
93
|
+
*/
|
|
94
|
+
rlsDisabled: {
|
|
95
|
+
schema: string;
|
|
96
|
+
table: string;
|
|
97
|
+
forced: boolean;
|
|
98
|
+
}[];
|
|
65
99
|
}
|
|
66
100
|
export interface Queryable {
|
|
67
101
|
query<R>(text: string, values?: unknown[]): Promise<{
|
|
@@ -14,9 +14,14 @@ import { REBASE_USER_ROLE } from "@rebasepro/common";
|
|
|
14
14
|
* are validation/side-effects, not a security boundary.
|
|
15
15
|
*
|
|
16
16
|
* - **Server context** — the base (owner) connection: auth flows, migrations,
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
17
|
+
* and raw `rebase.sql`. As table owner it bypasses RLS. This is the trusted
|
|
18
|
+
* plane, equivalent to Supabase's `service_role`.
|
|
19
|
+
*
|
|
20
|
+
* `rebase.dataAsAdmin` is **not** in it, despite the name. `init.ts` scopes
|
|
21
|
+
* that driver with `withAuth(SERVICE_IDENTITY)`, so it arrives as user
|
|
22
|
+
* context above — `rebase_user`, `app.uid = 'service'`, policies evaluated —
|
|
23
|
+
* and clears the default policies through their admin arm rather than the
|
|
24
|
+
* `auth.uid() IS NULL` one.
|
|
20
25
|
*
|
|
21
26
|
* This module provides the three pieces:
|
|
22
27
|
*
|
|
@@ -190,6 +190,15 @@ export declare class FetchService {
|
|
|
190
190
|
*/
|
|
191
191
|
searchRows<M extends Record<string, unknown>>(collectionPath: string, searchString: string, options?: {
|
|
192
192
|
filter?: FilterValues<Extract<keyof M, string>>;
|
|
193
|
+
/**
|
|
194
|
+
* An `or(...)`/`and(...)` group, applied alongside `filter`.
|
|
195
|
+
*
|
|
196
|
+
* `fetchRowsWithConditions` has always applied one; it was missing
|
|
197
|
+
* from this signature, so a realtime search subscription carrying a
|
|
198
|
+
* group could not pass it on and served every row matching the text
|
|
199
|
+
* that RLS allowed.
|
|
200
|
+
*/
|
|
201
|
+
logical?: LogicalCondition;
|
|
193
202
|
orderBy?: string;
|
|
194
203
|
order?: "desc" | "asc";
|
|
195
204
|
limit?: number;
|