@rebasepro/server-postgres 0.13.1-canary.gef9608c → 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/PostgresBackendDriver.d.ts +48 -1
- package/dist/PostgresBootstrapper.d.ts +26 -0
- package/dist/auth/services.d.ts +21 -0
- package/dist/{src-CU6WZGYV.js → auth-users-columns-BfQHf9JE.js} +1111 -92
- package/dist/auth-users-columns-BfQHf9JE.js.map +1 -0
- package/dist/{backup-service-CD8o_1Sl.js → backup-service-BH0Dzo_h.js} +2 -3
- package/dist/{backup-service-CD8o_1Sl.js.map → backup-service-BH0Dzo_h.js.map} +1 -1
- package/dist/cli-helpers.d.ts +56 -0
- 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-BLIIACla.js → ensure-collection-tables-CbvaGuVn.js} +162 -16
- package/dist/ensure-collection-tables-CbvaGuVn.js.map +1 -0
- package/dist/index.es.js +1720 -946
- package/dist/index.es.js.map +1 -1
- package/dist/rls-bootstrap-sql-69hYT8nr.js +244 -0
- package/dist/rls-bootstrap-sql-69hYT8nr.js.map +1 -0
- 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/auth-users-columns.d.ts +97 -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/generate-postgres-ddl-logic.d.ts +48 -0
- 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/rls-bootstrap-sql.d.ts +135 -0
- package/dist/schema/search-column.d.ts +248 -0
- package/dist/security/policy-drift.d.ts +34 -0
- package/dist/security/rls-enforcement.d.ts +61 -5
- package/dist/services/FetchService.d.ts +24 -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 +3 -0
- package/dist/services/junction-writes.d.ts +82 -0
- package/dist/services/realtimeService.d.ts +139 -2
- package/dist/services/write-denial.d.ts +36 -0
- package/dist/{src-DoU9yPqq.js → src-DCdn3Val.js} +124 -3
- package/dist/src-DCdn3Val.js.map +1 -0
- package/dist/utils/drizzle-conditions.d.ts +124 -2
- package/dist/{websocket-B2LsrINK.js → websocket-C8ZqVBiV.js} +75 -18
- package/dist/websocket-C8ZqVBiV.js.map +1 -0
- package/package.json +8 -7
- package/src/PostgresBackendDriver.ts +172 -6
- package/src/PostgresBootstrapper.ts +136 -11
- package/src/auth/ensure-tables.ts +212 -91
- 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 +124 -11
- package/src/cli-output.ts +43 -0
- package/src/cli.ts +299 -168
- package/src/collections/buildRegistry.ts +3 -1
- package/src/data-transformer.ts +129 -25
- package/src/history/ensure-history-table.ts +9 -2
- package/src/schema/auth-schema.ts +17 -1
- package/src/schema/auth-users-columns.ts +131 -0
- 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 +366 -30
- package/src/schema/generate-drizzle-schema-logic.ts +146 -66
- package/src/schema/generate-drizzle-schema.ts +11 -10
- package/src/schema/generate-postgres-ddl-logic.ts +277 -10
- package/src/schema/generate-postgres-ddl.ts +38 -14
- package/src/schema/generated-schema-staleness.ts +14 -7
- 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/rls-bootstrap-sql.ts +288 -0
- package/src/schema/search-column.ts +643 -0
- package/src/security/anonymous-grants.test.ts +4 -2
- package/src/security/policy-drift.test.ts +104 -3
- package/src/security/policy-drift.ts +129 -7
- package/src/security/rls-enforcement.ts +150 -7
- package/src/services/BranchService.ts +5 -0
- package/src/services/FetchService.ts +243 -22
- package/src/services/PersistService.ts +68 -42
- package/src/services/RelationService.ts +37 -696
- package/src/services/RelationWriteService.ts +653 -0
- package/src/services/cdc/trigger-cdc.ts +5 -1
- package/src/services/channel-history.ts +14 -0
- package/src/services/channel-presence.ts +13 -0
- package/src/services/collection-helpers.ts +89 -4
- package/src/services/dataService.ts +3 -0
- package/src/services/junction-writes.ts +295 -0
- package/src/services/pg-notify-listener.ts +1 -1
- package/src/services/realtimeService.ts +347 -86
- package/src/services/write-denial.ts +55 -0
- package/src/utils/drizzle-conditions.ts +433 -35
- package/src/utils/pg-error-utils.ts +8 -3
- package/src/websocket.ts +113 -16
- package/dist/ensure-collection-policies-Bck0ky4u.js +0 -57
- package/dist/ensure-collection-policies-Bck0ky4u.js.map +0 -1
- package/dist/ensure-collection-tables-BLIIACla.js.map +0 -1
- package/dist/policy-CeA1JcxP.js +0 -105
- package/dist/policy-CeA1JcxP.js.map +0 -1
- package/dist/schema/auth-bootstrap-sql.d.ts +0 -24
- package/dist/src-CU6WZGYV.js.map +0 -1
- package/dist/src-DoU9yPqq.js.map +0 -1
- package/dist/websocket-B2LsrINK.js.map +0 -1
- package/src/schema/auth-bootstrap-sql.ts +0 -47
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one description of what an auth user table's columns must be.
|
|
3
|
+
*
|
|
4
|
+
* ## Why this exists
|
|
5
|
+
*
|
|
6
|
+
* Three different code paths create `rebase.users`, and before this module they
|
|
7
|
+
* disagreed about it:
|
|
8
|
+
*
|
|
9
|
+
* | column | `db push` (generator) | `ensureAuthTablesExist` | `ensureCollectionTables` |
|
|
10
|
+
* |------------------|-----------------------|-------------------------|--------------------------|
|
|
11
|
+
* | `email` | `TEXT UNIQUE NOT NULL`| `TEXT NOT NULL` + CHECK | `TEXT` (nullable) |
|
|
12
|
+
* | `roles` | `TEXT[]` | `NOT NULL DEFAULT '{}'` | nullable, no default |
|
|
13
|
+
* | `email_verified` | `BOOLEAN` | `NOT NULL DEFAULT FALSE`| nullable |
|
|
14
|
+
* | `created_at` | `DEFAULT now()` | `NOT NULL DEFAULT NOW()`| no default |
|
|
15
|
+
*
|
|
16
|
+
* They are all `CREATE TABLE IF NOT EXISTS` / `ADD COLUMN IF NOT EXISTS`, so
|
|
17
|
+
* whichever ran first decided the table and the other two silently no-op'd.
|
|
18
|
+
* Boot order therefore chose the constraints: a managed deploy (collection
|
|
19
|
+
* tables first) got a users table whose `email` was nullable and whose
|
|
20
|
+
* `email_verified` had no default, while the same project pushed from a
|
|
21
|
+
* checkout got the strict one. In the same table, `is_anonymous` came out
|
|
22
|
+
* `NOT NULL DEFAULT false` — because *that* column existed in only one of the
|
|
23
|
+
* three lists, so its owner's definition won by default.
|
|
24
|
+
*
|
|
25
|
+
* A second consequence was drift in the other direction. The scaffold's
|
|
26
|
+
* `users.ts` describes 12 columns; auth needs 14. `db push` is declarative and
|
|
27
|
+
* builds its desired state from the collection alone, so the two columns only
|
|
28
|
+
* auth knows about (`is_anonymous`, `tokens_valid_after`) read as unmanaged
|
|
29
|
+
* drift, and a push run after the server had booted once planned to DROP them.
|
|
30
|
+
*
|
|
31
|
+
* Both problems are the same problem: no single place said what this table is.
|
|
32
|
+
* This is that place. Every creator asks here first and only then falls back to
|
|
33
|
+
* the collection's own property definitions, so a column auth owns has one
|
|
34
|
+
* definition regardless of who gets there first, and a column the developer
|
|
35
|
+
* added to their users collection still behaves like any other field.
|
|
36
|
+
*
|
|
37
|
+
* ## What belongs here
|
|
38
|
+
*
|
|
39
|
+
* Only columns the auth services read or write. A developer's own additions to
|
|
40
|
+
* their users collection (`bio`, `stripe_customer_id`) are ordinary columns and
|
|
41
|
+
* must NOT be listed — they are generated from the collection like every other
|
|
42
|
+
* field, and listing them here would freeze a user's schema.
|
|
43
|
+
*
|
|
44
|
+
* `id` is deliberately absent: its type comes from the collection's id property
|
|
45
|
+
* (uuid / increment / text) and each creator already derives it.
|
|
46
|
+
*
|
|
47
|
+
* Keep in step with {@link AUTH_SCHEMA_VERSION} when a change here makes an
|
|
48
|
+
* older runtime unable to work against a migrated table.
|
|
49
|
+
*/
|
|
50
|
+
/**
|
|
51
|
+
* A column auth owns.
|
|
52
|
+
*
|
|
53
|
+
* Structured rather than one SQL string because the same facts are needed in
|
|
54
|
+
* three grammars — a `CREATE TABLE` column list, an `ADD COLUMN IF NOT EXISTS`,
|
|
55
|
+
* and a pair of `ALTER COLUMN … SET DEFAULT` / `SET NOT NULL` reconciles for a
|
|
56
|
+
* table that already exists with the wrong shape. Parsing a string back apart
|
|
57
|
+
* for the third of those is how the copies drifted in the first place.
|
|
58
|
+
*/
|
|
59
|
+
export interface AuthUsersColumn {
|
|
60
|
+
/** Physical column name. */
|
|
61
|
+
column: string;
|
|
62
|
+
/** Postgres type. */
|
|
63
|
+
type: string;
|
|
64
|
+
/** Default expression, verbatim, or absent for no default. */
|
|
65
|
+
default?: string;
|
|
66
|
+
/** Whether the column is NOT NULL. */
|
|
67
|
+
notNull?: boolean;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* `email` is NOT NULL on purpose, and the anonymous sign-in route depends on it
|
|
71
|
+
* — it synthesizes `anon_<32 hex>@anonymous.local` rather than inserting NULL.
|
|
72
|
+
* The 320-char bound (RFC 5321) is a CHECK rather than a `VARCHAR(n)`, added
|
|
73
|
+
* separately by `ensureAuthTablesExist` so it can be `NOT VALID` on an adopted
|
|
74
|
+
* table that already holds a longer row.
|
|
75
|
+
*/
|
|
76
|
+
export declare const AUTH_USERS_COLUMNS: readonly AuthUsersColumn[];
|
|
77
|
+
/** Type + inline constraints, as they appear after the column name. */
|
|
78
|
+
export declare function authUsersColumnSql(spec: AuthUsersColumn): string;
|
|
79
|
+
/**
|
|
80
|
+
* The auth-owned definition for a physical column name, or `undefined` when
|
|
81
|
+
* auth does not own it.
|
|
82
|
+
*
|
|
83
|
+
* Callers pass the RESOLVED column name (after `columnName` mapping), because
|
|
84
|
+
* that is the only name the three creators agree on: the scaffold's users
|
|
85
|
+
* collection spells the property `displayName` and the column `display_name`.
|
|
86
|
+
*/
|
|
87
|
+
export declare function authUsersColumnDefinition(column: string): string | undefined;
|
|
88
|
+
/**
|
|
89
|
+
* Whether a collection is an auth collection, i.e. whether the definitions in
|
|
90
|
+
* this module apply to its table at all.
|
|
91
|
+
*
|
|
92
|
+
* Duplicated in shape from `@rebasepro/common`'s policy defaults on purpose:
|
|
93
|
+
* that one takes a `CollectionConfig`, this one is called from DDL code paths
|
|
94
|
+
* that hold looser objects, and both spellings must accept `auth: true` as well
|
|
95
|
+
* as `auth: { enabled: true }`.
|
|
96
|
+
*/
|
|
97
|
+
export declare function isAuthCollection(collection: unknown): boolean;
|
|
@@ -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";
|
|
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>;
|
|
@@ -29,8 +29,56 @@ export declare const generatePolicyStatements: (collection: CollectionConfig, ru
|
|
|
29
29
|
*/
|
|
30
30
|
export declare const quoteSqlLiteral: (value: string) => string;
|
|
31
31
|
export declare const getSqlColumnType: (propName: string, prop: Property, collection: CollectionConfig, collections: CollectionConfig[]) => string;
|
|
32
|
+
/**
|
|
33
|
+
* Everything a `search` block needs, as a file Rebase applies itself.
|
|
34
|
+
*
|
|
35
|
+
* Search is the one part of the schema Atlas does not own. Two independent
|
|
36
|
+
* reasons, and either alone would be enough:
|
|
37
|
+
*
|
|
38
|
+
* 1. Its free tier refuses to *parse* a desired-state file that so much as
|
|
39
|
+
* contains a function — "functions and procedures are available to
|
|
40
|
+
* logged-in users only". A generated `tsvector` column cannot avoid one:
|
|
41
|
+
* `unaccent` is STABLE and jsonb flattening needs a set-returning function,
|
|
42
|
+
* so both have to be wrapped in an IMMUTABLE helper to be legal in a
|
|
43
|
+
* generated column at all.
|
|
44
|
+
* 2. Even with the file accepted, Atlas *wipes* the dev database it diffs
|
|
45
|
+
* against, so a helper seeded there beforehand is gone by the time the plan
|
|
46
|
+
* is analysed. There is no hook to reinstate it.
|
|
47
|
+
*
|
|
48
|
+
* So the column, its index and its helpers are excluded from Atlas's view
|
|
49
|
+
* (`searchExcludePatterns`) and applied from here — the same arrangement the
|
|
50
|
+
* RLS policies already use, and for the same underlying reason.
|
|
51
|
+
*
|
|
52
|
+
* Ordered as it must run: extensions, then helpers, then the column whose
|
|
53
|
+
* expression calls them, then the index over that column. Every statement is
|
|
54
|
+
* `IF NOT EXISTS` / `OR REPLACE`, because this is replayed on every push and
|
|
55
|
+
* appended to migrations that run against databases at any stage of their
|
|
56
|
+
* life. Empty when nothing opted in — the caller writes no file then.
|
|
57
|
+
*
|
|
58
|
+
* The leading half — extensions and helpers, without the table-shaped
|
|
59
|
+
* statements — is available on its own as {@link searchPrerequisiteStatements},
|
|
60
|
+
* for the dev database Atlas analyses plans against. That database has none of
|
|
61
|
+
* the project's tables, so it wants the functions and nothing else.
|
|
62
|
+
*/
|
|
63
|
+
export declare const searchPrerequisiteStatements: (allCollections: CollectionConfig[]) => string[];
|
|
64
|
+
export declare const generatePostgresSearchDdl: (allCollections: CollectionConfig[]) => string;
|
|
65
|
+
/**
|
|
66
|
+
* Glob patterns telling Atlas to leave the search column and its index alone.
|
|
67
|
+
*
|
|
68
|
+
* Without these, a desired state that omits search reads to Atlas as an
|
|
69
|
+
* instruction to drop the column — taking the index and the whole search
|
|
70
|
+
* feature with it on the next push.
|
|
71
|
+
*
|
|
72
|
+
* Fully qualified, `schema.table.object`, matching the include list. The
|
|
73
|
+
* two-part form is what Atlas wants when the connection URL scopes it to one
|
|
74
|
+
* schema and is *silently ignored* otherwise: it reads `posts.search_vector`
|
|
75
|
+
* as a table named `search_vector` in a schema named `posts`, matches nothing,
|
|
76
|
+
* and reports no error for the pattern that never fired.
|
|
77
|
+
*/
|
|
78
|
+
export declare const searchExcludePatterns: (allCollections: CollectionConfig[]) => string[];
|
|
32
79
|
export declare const generatePostgresDdl: (allCollections: CollectionConfig[], options?: {
|
|
33
80
|
includePolicies?: boolean;
|
|
81
|
+
includeSearch?: boolean;
|
|
34
82
|
}) => Promise<string>;
|
|
35
83
|
/** The RLS statements one declared collection's table needs, ready to run. */
|
|
36
84
|
/**
|
|
@@ -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;
|