@rebasepro/server-postgres 0.0.1-canary.4829d6e
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/LICENSE +22 -0
- package/README.md +86 -0
- package/dist/PostgresAdapter.d.ts +6 -0
- package/dist/PostgresBackendDriver.d.ts +150 -0
- package/dist/PostgresBootstrapper.d.ts +51 -0
- package/dist/auth/ensure-tables.d.ts +10 -0
- package/dist/auth/services.d.ts +250 -0
- package/dist/backup/backup-cli.d.ts +3 -0
- package/dist/backup/backup-cron.d.ts +53 -0
- package/dist/backup/backup-service.d.ts +85 -0
- package/dist/backup/index.d.ts +12 -0
- package/dist/backup/pg-tools.d.ts +110 -0
- package/dist/backup/retention.d.ts +35 -0
- package/dist/chunk-DSJWtz9O.js +40 -0
- package/dist/cli-errors.d.ts +42 -0
- package/dist/cli-helpers.d.ts +7 -0
- package/dist/cli.d.ts +1 -0
- package/dist/collections/PostgresCollectionRegistry.d.ts +47 -0
- package/dist/connection.d.ts +65 -0
- package/dist/data-transformer.d.ts +55 -0
- package/dist/databasePoolManager.d.ts +20 -0
- package/dist/history/HistoryService.d.ts +71 -0
- package/dist/history/ensure-history-table.d.ts +7 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.es.js +23535 -0
- package/dist/index.es.js.map +1 -0
- package/dist/interfaces.d.ts +18 -0
- package/dist/schema/auth-bootstrap-sql.d.ts +24 -0
- package/dist/schema/auth-default-policies.d.ts +10 -0
- package/dist/schema/auth-schema.d.ts +2376 -0
- package/dist/schema/doctor-cli.d.ts +2 -0
- package/dist/schema/doctor.d.ts +58 -0
- package/dist/schema/dynamic-tables.d.ts +31 -0
- package/dist/schema/generate-drizzle-schema-logic.d.ts +2 -0
- package/dist/schema/generate-drizzle-schema.d.ts +1 -0
- package/dist/schema/generate-postgres-ddl-logic.d.ts +5 -0
- package/dist/schema/generate-postgres-ddl.d.ts +1 -0
- package/dist/schema/introspect-db-inference.d.ts +5 -0
- package/dist/schema/introspect-db-logic.d.ts +118 -0
- package/dist/schema/introspect-db.d.ts +1 -0
- package/dist/schema/introspect-runtime.d.ts +57 -0
- package/dist/schema/test-schema.d.ts +24 -0
- package/dist/security/policy-drift.d.ts +57 -0
- package/dist/security/rls-enforcement.d.ts +122 -0
- package/dist/services/BranchService.d.ts +47 -0
- package/dist/services/FetchService.d.ts +214 -0
- package/dist/services/PersistService.d.ts +39 -0
- package/dist/services/RelationService.d.ts +109 -0
- package/dist/services/cdc/CdcListener.d.ts +54 -0
- package/dist/services/cdc/trigger-cdc.d.ts +64 -0
- package/dist/services/collection-helpers.d.ts +38 -0
- package/dist/services/dataService.d.ts +110 -0
- package/dist/services/index.d.ts +4 -0
- package/dist/services/realtimeService.d.ts +298 -0
- package/dist/src-Eh-CZosp.js +595 -0
- package/dist/src-Eh-CZosp.js.map +1 -0
- package/dist/types.d.ts +3 -0
- package/dist/utils/drizzle-conditions.d.ts +138 -0
- package/dist/utils/pg-array-null-patch.d.ts +16 -0
- package/dist/utils/pg-error-utils.d.ts +65 -0
- package/dist/utils/table-classification.d.ts +8 -0
- package/dist/websocket.d.ts +18 -0
- package/package.json +113 -0
- package/src/PostgresAdapter.ts +58 -0
- package/src/PostgresBackendDriver.ts +1387 -0
- package/src/PostgresBootstrapper.ts +581 -0
- package/src/auth/ensure-tables.ts +367 -0
- package/src/auth/services.ts +1321 -0
- package/src/backup/backup-cli.ts +383 -0
- package/src/backup/backup-cron.ts +189 -0
- package/src/backup/backup-service.ts +299 -0
- package/src/backup/index.ts +12 -0
- package/src/backup/pg-tools.ts +231 -0
- package/src/backup/retention.ts +75 -0
- package/src/cli-errors.ts +265 -0
- package/src/cli-helpers.ts +196 -0
- package/src/cli.ts +786 -0
- package/src/collections/PostgresCollectionRegistry.ts +103 -0
- package/src/connection.ts +166 -0
- package/src/data-transformer.ts +733 -0
- package/src/databasePoolManager.ts +87 -0
- package/src/history/HistoryService.ts +272 -0
- package/src/history/ensure-history-table.ts +46 -0
- package/src/index.ts +15 -0
- package/src/interfaces.ts +60 -0
- package/src/schema/auth-bootstrap-sql.ts +41 -0
- package/src/schema/auth-default-policies.ts +125 -0
- package/src/schema/auth-schema.ts +233 -0
- package/src/schema/doctor-cli.ts +113 -0
- package/src/schema/doctor.ts +733 -0
- package/src/schema/dynamic-tables.test.ts +302 -0
- package/src/schema/dynamic-tables.ts +293 -0
- package/src/schema/generate-drizzle-schema-logic.ts +850 -0
- package/src/schema/generate-drizzle-schema.ts +131 -0
- package/src/schema/generate-postgres-ddl-logic.ts +490 -0
- package/src/schema/generate-postgres-ddl.ts +92 -0
- package/src/schema/introspect-db-inference.ts +238 -0
- package/src/schema/introspect-db-logic.ts +910 -0
- package/src/schema/introspect-db.ts +266 -0
- package/src/schema/introspect-runtime.test.ts +212 -0
- package/src/schema/introspect-runtime.ts +293 -0
- package/src/schema/test-schema.ts +11 -0
- package/src/security/policy-drift.test.ts +122 -0
- package/src/security/policy-drift.ts +159 -0
- package/src/security/rls-enforcement.ts +295 -0
- package/src/services/BranchService.ts +251 -0
- package/src/services/FetchService.ts +1661 -0
- package/src/services/PersistService.ts +329 -0
- package/src/services/RelationService.ts +1306 -0
- package/src/services/cdc/CdcListener.ts +167 -0
- package/src/services/cdc/trigger-cdc.ts +169 -0
- package/src/services/collection-helpers.ts +151 -0
- package/src/services/dataService.ts +246 -0
- package/src/services/index.ts +13 -0
- package/src/services/realtimeService.ts +1502 -0
- package/src/types.ts +4 -0
- package/src/utils/drizzle-conditions.ts +1162 -0
- package/src/utils/pg-array-null-patch.ts +42 -0
- package/src/utils/pg-error-utils.ts +227 -0
- package/src/utils/table-classification.ts +16 -0
- package/src/websocket.ts +640 -0
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { CollectionConfig, Property } from "@rebasepro/types";
|
|
2
|
+
export type IssueSeverity = "error" | "warning" | "info";
|
|
3
|
+
export interface DoctorIssue {
|
|
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";
|
|
6
|
+
table?: string;
|
|
7
|
+
column?: string;
|
|
8
|
+
expected?: string;
|
|
9
|
+
actual?: string;
|
|
10
|
+
message: string;
|
|
11
|
+
fix: string;
|
|
12
|
+
}
|
|
13
|
+
export interface DoctorReport {
|
|
14
|
+
collectionsToSchema: {
|
|
15
|
+
passed: boolean;
|
|
16
|
+
issues: DoctorIssue[];
|
|
17
|
+
};
|
|
18
|
+
collectionsToSdk: {
|
|
19
|
+
passed: boolean;
|
|
20
|
+
issues: DoctorIssue[];
|
|
21
|
+
};
|
|
22
|
+
schemaToDatabase: {
|
|
23
|
+
passed: boolean;
|
|
24
|
+
issues: DoctorIssue[];
|
|
25
|
+
};
|
|
26
|
+
summary: {
|
|
27
|
+
passed: number;
|
|
28
|
+
warnings: number;
|
|
29
|
+
errors: number;
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
export declare function getExpectedColumnType(prop: Property): string | null;
|
|
33
|
+
/**
|
|
34
|
+
* Re-exported so callers keep importing it from here, but the implementation is
|
|
35
|
+
* now shared with the runtime and the generators — a doctor that disagreed with
|
|
36
|
+
* the policy generator about which files are collections would compare the
|
|
37
|
+
* wrong thing.
|
|
38
|
+
*/
|
|
39
|
+
export declare function loadCollections(collectionsPath: string): Promise<CollectionConfig[]>;
|
|
40
|
+
export declare function checkCollectionsVsSchema(collections: CollectionConfig[], schemaFilePath: string): Promise<{
|
|
41
|
+
passed: boolean;
|
|
42
|
+
issues: DoctorIssue[];
|
|
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
|
+
}>;
|
|
52
|
+
export declare function renderReport(report: DoctorReport): void;
|
|
53
|
+
export declare function runDoctor(options: {
|
|
54
|
+
collectionsPath: string;
|
|
55
|
+
schemaPath: string;
|
|
56
|
+
sdkPath: string;
|
|
57
|
+
databaseUrl?: string;
|
|
58
|
+
}): Promise<DoctorReport>;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build drizzle tables at runtime from an introspected schema.
|
|
3
|
+
*
|
|
4
|
+
* CMS mode gets its drizzle tables from a generated `schema.generated.ts` that
|
|
5
|
+
* the developer commits. BaaS mode has no such file — it points at a database
|
|
6
|
+
* and serves it — so the equivalent table objects are constructed here from
|
|
7
|
+
* `information_schema` metadata.
|
|
8
|
+
*
|
|
9
|
+
* These are handed to drizzle as its schema, which keeps the relational query
|
|
10
|
+
* path (`db.query.*`) working; without them FetchService would fall back to
|
|
11
|
+
* plain selects and lose relation loading.
|
|
12
|
+
*/
|
|
13
|
+
import { type PgTable } from "drizzle-orm/pg-core";
|
|
14
|
+
import { type Relations } from "drizzle-orm";
|
|
15
|
+
import type { TableMeta } from "./introspect-db-logic";
|
|
16
|
+
/**
|
|
17
|
+
* Build one drizzle table per introspected table, keyed by table name.
|
|
18
|
+
*/
|
|
19
|
+
export declare function buildDrizzleTablesFromSchema(tablesMap: Map<string, TableMeta>, pgSchemaName?: string): Record<string, PgTable>;
|
|
20
|
+
/**
|
|
21
|
+
* Build drizzle `relations()` for the foreign keys, so the relational query
|
|
22
|
+
* path can actually load them.
|
|
23
|
+
*
|
|
24
|
+
* FetchService asks drizzle for `with: { <key>: true }`, keyed by the relation
|
|
25
|
+
* property on the collection. Tables alone don't satisfy that — without these,
|
|
26
|
+
* `?include=author` silently returns the raw `author_id` and no author.
|
|
27
|
+
*
|
|
28
|
+
* The keys here must match `buildRelations` in introspect-runtime.ts, which is
|
|
29
|
+
* what names the collection's relation properties.
|
|
30
|
+
*/
|
|
31
|
+
export declare function buildDrizzleRelationsFromSchema(tablesMap: Map<string, TableMeta>, tables: Record<string, PgTable>): Record<string, Relations>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { CollectionConfig } from "@rebasepro/types";
|
|
2
|
+
export declare const generatePostgresDdl: (collections: CollectionConfig[], options?: {
|
|
3
|
+
includePolicies?: boolean;
|
|
4
|
+
}) => Promise<string>;
|
|
5
|
+
export declare const generatePostgresPoliciesDdl: (collections: CollectionConfig[]) => string;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
export interface TableRow {
|
|
2
|
+
table_name: string;
|
|
3
|
+
}
|
|
4
|
+
export interface TableColumn {
|
|
5
|
+
table_name: string;
|
|
6
|
+
column_name: string;
|
|
7
|
+
data_type: string;
|
|
8
|
+
udt_name: string;
|
|
9
|
+
is_nullable: string;
|
|
10
|
+
column_default: string | null;
|
|
11
|
+
atttypmod: number | null;
|
|
12
|
+
}
|
|
13
|
+
export interface EnumValue {
|
|
14
|
+
enum_name: string;
|
|
15
|
+
enum_value: string;
|
|
16
|
+
sort_order: number;
|
|
17
|
+
}
|
|
18
|
+
export interface PrimaryKeyRow {
|
|
19
|
+
table_name: string;
|
|
20
|
+
column_name: string;
|
|
21
|
+
}
|
|
22
|
+
export interface ForeignKeyRow {
|
|
23
|
+
table_name: string;
|
|
24
|
+
column_name: string;
|
|
25
|
+
foreign_table_name: string;
|
|
26
|
+
foreign_column_name: string;
|
|
27
|
+
}
|
|
28
|
+
export interface TableMeta {
|
|
29
|
+
name: string;
|
|
30
|
+
columns: TableColumn[];
|
|
31
|
+
pks: string[];
|
|
32
|
+
fks: ForeignKeyRow[];
|
|
33
|
+
}
|
|
34
|
+
export declare function singularize(word: string): string;
|
|
35
|
+
/**
|
|
36
|
+
* Convert a snake_case name to a human-readable Title Case label.
|
|
37
|
+
* e.g. "created_at" -> "Created At", "customer_id" -> "Customer Id"
|
|
38
|
+
*/
|
|
39
|
+
export declare function humanize(snakeName: string): string;
|
|
40
|
+
/**
|
|
41
|
+
* Convert a snake_case table name to a camelCase + "Collection" variable name.
|
|
42
|
+
* e.g. "company_token" -> "companyTokenCollection"
|
|
43
|
+
*/
|
|
44
|
+
export declare function toCollectionVarName(tableName: string): string;
|
|
45
|
+
export declare function getIconForTable(tableName: string): string;
|
|
46
|
+
/**
|
|
47
|
+
* Map a PostgreSQL data type to a Rebase property type.
|
|
48
|
+
*/
|
|
49
|
+
export declare function mapPgType(dataType: string): string;
|
|
50
|
+
export declare function buildEnumMap(enumValues: EnumValue[]): Map<string, string[]>;
|
|
51
|
+
export declare function buildTablesMap(tables: TableRow[], columns: TableColumn[], pks: PrimaryKeyRow[], fks: ForeignKeyRow[]): Map<string, TableMeta>;
|
|
52
|
+
export declare function identifyJoinTables(tablesMap: Map<string, TableMeta>): Set<string>;
|
|
53
|
+
/**
|
|
54
|
+
* Property metadata used to compute display priority.
|
|
55
|
+
* Keeps computePropertyPriority free of any TableMeta coupling.
|
|
56
|
+
*/
|
|
57
|
+
export interface PropertyOrderingContext {
|
|
58
|
+
/** The resolved Rebase property type (e.g. "string", "number", "date", "relation"). */
|
|
59
|
+
propType: string;
|
|
60
|
+
/** Whether this column is a primary key. */
|
|
61
|
+
isPk: boolean;
|
|
62
|
+
/** Whether this column is an enum (USER-DEFINED with matching values). */
|
|
63
|
+
isEnum: boolean;
|
|
64
|
+
/** Whether this is a storage/file-upload field (detected from column name). */
|
|
65
|
+
isStorage: boolean;
|
|
66
|
+
/** The PostgreSQL data_type (e.g. "text", "character varying", "jsonb"). */
|
|
67
|
+
pgDataType: string;
|
|
68
|
+
/** The original column index in PostgreSQL (for stable tiebreaking). */
|
|
69
|
+
originalIndex: number;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Compute a numeric priority score for a property.
|
|
73
|
+
* Lower scores appear first in the generated `propertiesOrder` array.
|
|
74
|
+
*
|
|
75
|
+
* The system uses 14 tiers (0–139), with the original column index
|
|
76
|
+
* added as a fractional tiebreaker (originalIndex / 10000) to
|
|
77
|
+
* guarantee stable ordering within the same tier.
|
|
78
|
+
*
|
|
79
|
+
* Pure function — no side effects.
|
|
80
|
+
*/
|
|
81
|
+
export declare function computePropertyPriority(columnName: string, ctx: PropertyOrderingContext): number;
|
|
82
|
+
/**
|
|
83
|
+
* Sort a `propertiesOrder` array using the priority heuristic.
|
|
84
|
+
* Returns a new sorted array; does not mutate the input.
|
|
85
|
+
*
|
|
86
|
+
* @param entries - Array of { key, columnName, propType, ... } objects
|
|
87
|
+
* carrying the information needed to compute priority.
|
|
88
|
+
*/
|
|
89
|
+
export interface PropertyOrderEntry {
|
|
90
|
+
/** The property key in the generated collection (may differ from columnName for relations). */
|
|
91
|
+
key: string;
|
|
92
|
+
/** The ordering context for this property. */
|
|
93
|
+
ctx: PropertyOrderingContext;
|
|
94
|
+
}
|
|
95
|
+
export declare function sortPropertiesOrder(entries: PropertyOrderEntry[]): string[];
|
|
96
|
+
export interface GeneratedFile {
|
|
97
|
+
tableName: string;
|
|
98
|
+
fileName: string;
|
|
99
|
+
content: string;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Generate the full TypeScript file content for a single collection.
|
|
103
|
+
* Pure function — no I/O.
|
|
104
|
+
*/
|
|
105
|
+
export declare function generateCollectionFile(tableName: string, meta: TableMeta, allFks: ForeignKeyRow[], joinTables: Set<string>, tablesMap: Map<string, TableMeta>, enumMap: Map<string, string[]>, sampleData?: Record<string, unknown>[]): string;
|
|
106
|
+
/**
|
|
107
|
+
* Generate the content for an index.ts file that re-exports all collections.
|
|
108
|
+
*/
|
|
109
|
+
export declare function generateIndexContent(fileNames: string[]): string;
|
|
110
|
+
/**
|
|
111
|
+
* Merge new exports into existing index.ts content.
|
|
112
|
+
* Returns the merged content string.
|
|
113
|
+
*/
|
|
114
|
+
export declare function mergeIndexContent(existingContent: string, newFileNames: string[]): string;
|
|
115
|
+
/**
|
|
116
|
+
* Safely extract the host portion of a database URL for logging.
|
|
117
|
+
*/
|
|
118
|
+
export declare function safeHostFromUrl(url: string): string;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime introspection — builds collections in memory from the live database.
|
|
3
|
+
*
|
|
4
|
+
* This is what makes BaaS mode work with zero configuration: instead of loading
|
|
5
|
+
* collection files from disk, the server reads `information_schema` at boot and
|
|
6
|
+
* derives a collection per table, so any database is served over REST without a
|
|
7
|
+
* single config file.
|
|
8
|
+
*
|
|
9
|
+
* Distinct from `introspect-db.ts`, which runs the same queries but emits
|
|
10
|
+
* TypeScript *source* for a developer to edit and commit (CMS mode). The two
|
|
11
|
+
* share the mapping helpers in `introspect-db-logic.ts` so a table is described
|
|
12
|
+
* the same way whether it was generated or introspected.
|
|
13
|
+
*/
|
|
14
|
+
import type { PostgresCollectionConfig } from "@rebasepro/types";
|
|
15
|
+
import { TableMeta } from "./introspect-db-logic";
|
|
16
|
+
export interface IntrospectedSchema {
|
|
17
|
+
tablesMap: Map<string, TableMeta>;
|
|
18
|
+
enumMap: Map<string, string[]>;
|
|
19
|
+
joinTables: Set<string>;
|
|
20
|
+
}
|
|
21
|
+
/** Whether a table carries an authorization model of its own. */
|
|
22
|
+
export interface TableRlsStatus {
|
|
23
|
+
table: string;
|
|
24
|
+
/** ALTER TABLE … ENABLE ROW LEVEL SECURITY has been run. */
|
|
25
|
+
rlsEnabled: boolean;
|
|
26
|
+
/** Policies attached to it. RLS enabled with none = nothing is visible. */
|
|
27
|
+
policyCount: number;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Read the RLS posture of each table in a schema.
|
|
31
|
+
*
|
|
32
|
+
* This is what decides whether baas mode may serve a table. A table with RLS
|
|
33
|
+
* disabled has no authorization model: since every authenticated request runs
|
|
34
|
+
* as `rebase_user`, and that role is granted DML on the schema, serving such a
|
|
35
|
+
* table hands every row to every logged-in user.
|
|
36
|
+
*/
|
|
37
|
+
export declare function readRlsStatus(client: Queryable, pgSchema: string): Promise<Map<string, TableRlsStatus>>;
|
|
38
|
+
/** Minimal query surface — satisfied by pg.Client and pg.Pool alike. */
|
|
39
|
+
export interface Queryable {
|
|
40
|
+
query<R>(text: string, values?: unknown[]): Promise<{
|
|
41
|
+
rows: R[];
|
|
42
|
+
}>;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Read tables, columns, enums, primary keys and foreign keys for a schema.
|
|
46
|
+
* Mirrors the queries in introspect-db.ts.
|
|
47
|
+
*/
|
|
48
|
+
export declare function introspectSchema(client: Queryable, pgSchema: string): Promise<IntrospectedSchema>;
|
|
49
|
+
/**
|
|
50
|
+
* Turn an introspected schema into collections.
|
|
51
|
+
*
|
|
52
|
+
* Join tables are skipped: they carry no identity of their own and exist to
|
|
53
|
+
* express a many-to-many edge between two other tables.
|
|
54
|
+
*/
|
|
55
|
+
export declare function buildCollectionsFromSchema({ tablesMap, enumMap, joinTables }: IntrospectedSchema, pgSchema: string): PostgresCollectionConfig[];
|
|
56
|
+
/** Introspect the database and return ready-to-serve collections. */
|
|
57
|
+
export declare function introspectCollections(client: Queryable, pgSchema: string): Promise<PostgresCollectionConfig[]>;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export declare const testTable: import("drizzle-orm/pg-core").PgTableWithColumns<{
|
|
2
|
+
name: "test";
|
|
3
|
+
schema: undefined;
|
|
4
|
+
columns: {
|
|
5
|
+
id: import("drizzle-orm/pg-core").PgColumn<{
|
|
6
|
+
name: "id";
|
|
7
|
+
tableName: "test";
|
|
8
|
+
dataType: "string";
|
|
9
|
+
columnType: "PgText";
|
|
10
|
+
data: string;
|
|
11
|
+
driverParam: string;
|
|
12
|
+
notNull: true;
|
|
13
|
+
hasDefault: false;
|
|
14
|
+
isPrimaryKey: true;
|
|
15
|
+
isAutoincrement: false;
|
|
16
|
+
hasRuntimeDefault: false;
|
|
17
|
+
enumValues: [string, ...string[]];
|
|
18
|
+
baseColumn: never;
|
|
19
|
+
identity: undefined;
|
|
20
|
+
generated: undefined;
|
|
21
|
+
}, {}, {}>;
|
|
22
|
+
};
|
|
23
|
+
dialect: "pg";
|
|
24
|
+
}>;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compare the RLS policies a database actually has against the ones the
|
|
3
|
+
* collections describe.
|
|
4
|
+
*
|
|
5
|
+
* Policies live in Postgres; the collection config is only their *source*.
|
|
6
|
+
* Nothing reconciled the two, which is how the demo database served empty
|
|
7
|
+
* collections indefinitely: its policies granted `TO authenticated` (a Supabase
|
|
8
|
+
* role name) while requests run as `rebase_user`, so RLS filtered every row.
|
|
9
|
+
* The config was later corrected and the database never noticed — an empty
|
|
10
|
+
* table is indistinguishable from a table with no data.
|
|
11
|
+
*
|
|
12
|
+
* Expected policies are parsed from `generatePostgresPoliciesDdl`, the same
|
|
13
|
+
* function `db push` uses to write `drizzle/policies.sql`, so this compares
|
|
14
|
+
* against exactly what would be applied rather than a reimplementation.
|
|
15
|
+
*/
|
|
16
|
+
import type { CollectionConfig } from "@rebasepro/types";
|
|
17
|
+
export interface PolicyRef {
|
|
18
|
+
schema: string;
|
|
19
|
+
table: string;
|
|
20
|
+
name: string;
|
|
21
|
+
/** Roles in the TO clause. */
|
|
22
|
+
roles: string[];
|
|
23
|
+
/** SELECT / INSERT / UPDATE / DELETE / ALL. */
|
|
24
|
+
command: string;
|
|
25
|
+
}
|
|
26
|
+
export interface PolicyDrift {
|
|
27
|
+
/** Described by the collections, absent from the database. */
|
|
28
|
+
missing: PolicyRef[];
|
|
29
|
+
/** In the database, described by no collection — stale pushes live here. */
|
|
30
|
+
orphaned: PolicyRef[];
|
|
31
|
+
/** Same policy name, different roles or command. */
|
|
32
|
+
diverged: {
|
|
33
|
+
expected: PolicyRef;
|
|
34
|
+
actual: PolicyRef;
|
|
35
|
+
differences: string[];
|
|
36
|
+
}[];
|
|
37
|
+
}
|
|
38
|
+
export interface Queryable {
|
|
39
|
+
query<R>(text: string, values?: unknown[]): Promise<{
|
|
40
|
+
rows: R[];
|
|
41
|
+
}>;
|
|
42
|
+
}
|
|
43
|
+
/** Parse the generated DDL rather than rebuilding the shape by hand. */
|
|
44
|
+
export declare function parseExpectedPolicies(ddl: string): PolicyRef[];
|
|
45
|
+
/**
|
|
46
|
+
* Diff expected against live.
|
|
47
|
+
*
|
|
48
|
+
* Compares names, roles and command only — all exact values. Policy
|
|
49
|
+
* *expressions* are deliberately not compared: Postgres rewrites `qual`/
|
|
50
|
+
* `with_check` when storing them (parenthesising, casting, schema-qualifying),
|
|
51
|
+
* so text comparison reports drift that does not exist, and a check that cries
|
|
52
|
+
* wolf gets ignored. Roles alone catch the failure this exists for.
|
|
53
|
+
*/
|
|
54
|
+
export declare function checkPolicyDrift(client: Queryable, collections: CollectionConfig[]): Promise<PolicyDrift>;
|
|
55
|
+
export declare const hasDrift: (d: PolicyDrift) => boolean;
|
|
56
|
+
/** Human-readable report; empty string when the database matches the config. */
|
|
57
|
+
export declare function formatPolicyDrift(drift: PolicyDrift): string;
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { SQL } from "drizzle-orm";
|
|
2
|
+
/**
|
|
3
|
+
* Unified RLS enforcement — the "user context vs server context" model.
|
|
4
|
+
*
|
|
5
|
+
* Every operation runs in one of two contexts:
|
|
6
|
+
*
|
|
7
|
+
* - **User context** — a request authenticated (or anonymous) via
|
|
8
|
+
* `driver.withAuth(user)`. Runs as the restricted `rebase_user` role: a
|
|
9
|
+
* non-owner, NOSUPERUSER, NOBYPASSRLS role, so Postgres RLS binds *every*
|
|
10
|
+
* statement (SELECT, INSERT, UPDATE, DELETE). The collection's
|
|
11
|
+
* `securityRules` are the whole authorization model; app-layer callbacks
|
|
12
|
+
* are validation/side-effects, not a security boundary.
|
|
13
|
+
*
|
|
14
|
+
* - **Server context** — the base (owner) connection: auth flows, migrations,
|
|
15
|
+
* background jobs, and the explicit `rebase.dataAsAdmin` accessor. As table
|
|
16
|
+
* owner it bypasses RLS. This is the trusted plane, equivalent to
|
|
17
|
+
* Supabase's `service_role`.
|
|
18
|
+
*
|
|
19
|
+
* This module provides the three pieces:
|
|
20
|
+
*
|
|
21
|
+
* 1. {@link detectConnectionPosture} — is the connection subject to RLS at
|
|
22
|
+
* all? (superuser / BYPASSRLS / table owner ⇒ no)
|
|
23
|
+
* 2. {@link ensureAppRole} — idempotently provision `rebase_user` with
|
|
24
|
+
* SELECT/INSERT/UPDATE/DELETE grants (+ default privileges so future
|
|
25
|
+
* tables stay covered).
|
|
26
|
+
* 3. {@link applyAuthContext} — per-transaction: set the `app.*` GUCs the
|
|
27
|
+
* policies read (`auth.uid()` etc.) and `SET LOCAL ROLE rebase_user` so
|
|
28
|
+
* RLS binds. Transaction-scoped, so it composes with poolers.
|
|
29
|
+
*
|
|
30
|
+
* Provisioning runs from the framework's own bootstrap/migrate (which already
|
|
31
|
+
* self-creates the `auth` schema and functions) — enforcement is default-on,
|
|
32
|
+
* not an operator opt-in.
|
|
33
|
+
*/
|
|
34
|
+
/** The restricted role every authenticated (user-context) request runs as. */
|
|
35
|
+
export declare const REBASE_USER_ROLE = "rebase_user";
|
|
36
|
+
/** Minimal SQL runner so callers can adapt drizzle or pg.Client. */
|
|
37
|
+
export type RawSqlRunner = (sqlText: string) => Promise<Record<string, unknown>[]>;
|
|
38
|
+
/** Minimal transaction surface needed by {@link applyAuthContext}. */
|
|
39
|
+
export interface SqlTx {
|
|
40
|
+
execute(query: SQL): Promise<unknown>;
|
|
41
|
+
}
|
|
42
|
+
export interface ConnectionPosture {
|
|
43
|
+
/** The connection's `current_user`. */
|
|
44
|
+
role: string;
|
|
45
|
+
superuser: boolean;
|
|
46
|
+
bypassRLS: boolean;
|
|
47
|
+
/** Owns at least one user table — owners bypass non-FORCE RLS. */
|
|
48
|
+
ownsTables: boolean;
|
|
49
|
+
/** True when RLS would NOT constrain this connection. */
|
|
50
|
+
privileged: boolean;
|
|
51
|
+
}
|
|
52
|
+
export interface AuthContext {
|
|
53
|
+
userId: string;
|
|
54
|
+
/** Raw roles as carried on the user (strings or `{ id }` objects). */
|
|
55
|
+
roles: unknown[];
|
|
56
|
+
}
|
|
57
|
+
export declare function detectConnectionPosture(run: RawSqlRunner): Promise<ConnectionPosture>;
|
|
58
|
+
/**
|
|
59
|
+
* Human-actionable instructions for when the connection cannot provision the
|
|
60
|
+
* user role itself (no CREATEROLE and role not pre-created by the platform).
|
|
61
|
+
*/
|
|
62
|
+
export declare function appRoleSetupInstructions(connectionRole: string, schemas: string[]): string;
|
|
63
|
+
/**
|
|
64
|
+
* Idempotently provision the `rebase_user` role, membership for the current
|
|
65
|
+
* connection role, and DML grants (+ default privileges for future tables)
|
|
66
|
+
* on every existing schema in `schemas`.
|
|
67
|
+
*
|
|
68
|
+
* Split into privilege tiers so it works both when the connection is a
|
|
69
|
+
* superuser (creates everything) and when the platform pre-created the role
|
|
70
|
+
* and membership (e.g. CNPG `postInitApplicationSQL`) and the connection is
|
|
71
|
+
* merely the table owner — owners can always run the grant tier themselves.
|
|
72
|
+
*
|
|
73
|
+
* RLS still filters every row: these grants only make the tables *reachable*
|
|
74
|
+
* by the role; the policies decide which rows/commands actually pass.
|
|
75
|
+
*
|
|
76
|
+
* Throws with precise setup instructions when the role is missing and the
|
|
77
|
+
* connection cannot create it.
|
|
78
|
+
*/
|
|
79
|
+
export declare function ensureAppRole(run: RawSqlRunner, schemas: string[]): Promise<void>;
|
|
80
|
+
/**
|
|
81
|
+
* Apply the authenticated context to a transaction: the `app.*` GUCs that RLS
|
|
82
|
+
* policies read via `auth.uid()` / `auth.roles()` / `auth.jwt()`, and — when
|
|
83
|
+
* `userRole` is set — `SET LOCAL ROLE` so RLS binds every statement in this
|
|
84
|
+
* transaction (reads *and* writes).
|
|
85
|
+
*
|
|
86
|
+
* GUCs are set with `is_local = true` and the role switch is `LOCAL`: both
|
|
87
|
+
* reset at commit/rollback, so pooled connections are never polluted.
|
|
88
|
+
*
|
|
89
|
+
* Fails closed by construction: if the role switch errors, the transaction
|
|
90
|
+
* aborts instead of proceeding privileged.
|
|
91
|
+
*
|
|
92
|
+
* SECURITY: this function is only ever called on the **user** path (the server
|
|
93
|
+
* context uses the base/owner driver and never calls it). The default policies
|
|
94
|
+
* treat `auth.uid() IS NULL` as the trusted server context, and `auth.uid()`
|
|
95
|
+
* is `NULLIF(current_setting('app.user_id'), '')` — so an EMPTY user id would
|
|
96
|
+
* be read as NULL and silently escalate a user request to server privileges.
|
|
97
|
+
* Coerce empty/blank ids to a sentinel here, at the single chokepoint, rather
|
|
98
|
+
* than trusting every caller (e.g. realtime subscription auth) to do it.
|
|
99
|
+
*/
|
|
100
|
+
export declare function applyAuthContext(tx: SqlTx, auth: AuthContext, userRole?: string): Promise<void>;
|
|
101
|
+
/**
|
|
102
|
+
* Reject `pgRoles` that this server can never satisfy.
|
|
103
|
+
*
|
|
104
|
+
* `pgRoles` sets the `TO` clause of a generated policy, so a policy naming a
|
|
105
|
+
* role the request never runs as simply never applies — and RLS then filters
|
|
106
|
+
* every row. The table reads as empty, which is indistinguishable from having
|
|
107
|
+
* no data, so the mistake survives review and ships.
|
|
108
|
+
*
|
|
109
|
+
* Requests run as `rebase_user`, so a policy is only reachable if it targets
|
|
110
|
+
* `public` or a role `rebase_user` holds. Anything else is a configuration
|
|
111
|
+
* error worth failing the boot for.
|
|
112
|
+
*/
|
|
113
|
+
export declare function validatePolicyPgRoles(run: RawSqlRunner, collections: {
|
|
114
|
+
slug?: string;
|
|
115
|
+
securityRules?: readonly {
|
|
116
|
+
name?: string;
|
|
117
|
+
pgRoles?: readonly string[];
|
|
118
|
+
}[];
|
|
119
|
+
}[],
|
|
120
|
+
/** The role requests actually run as: `rebase_user` when the connection is
|
|
121
|
+
* privileged enough to switch, otherwise the connection role itself. */
|
|
122
|
+
requestRole?: string): Promise<void>;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BranchService
|
|
3
|
+
*
|
|
4
|
+
* Manages database branching by creating/deleting PostgreSQL databases
|
|
5
|
+
* using `CREATE DATABASE ... TEMPLATE`. Branch metadata is stored in the
|
|
6
|
+
* `rebase.branches` table in the default (main) database, following the
|
|
7
|
+
* same `rebase` schema convention used by entity_history, auth, etc.
|
|
8
|
+
*/
|
|
9
|
+
import { BranchInfo } from "@rebasepro/types";
|
|
10
|
+
import { DrizzleClient } from "../interfaces";
|
|
11
|
+
import { DatabasePoolManager } from "../databasePoolManager";
|
|
12
|
+
export declare class BranchService {
|
|
13
|
+
private db;
|
|
14
|
+
private poolManager;
|
|
15
|
+
constructor(db: DrizzleClient, poolManager: DatabasePoolManager);
|
|
16
|
+
/**
|
|
17
|
+
* Ensure the `rebase.branches` metadata table exists in the default database.
|
|
18
|
+
* Idempotent — safe to call on every startup.
|
|
19
|
+
*/
|
|
20
|
+
ensureBranchMetadataTable(): Promise<void>;
|
|
21
|
+
/**
|
|
22
|
+
* Create a new branch database by templating the source database.
|
|
23
|
+
*
|
|
24
|
+
* Uses `CREATE DATABASE ... TEMPLATE` for an instant, full-fidelity copy
|
|
25
|
+
* of both schema and data.
|
|
26
|
+
*
|
|
27
|
+
* @param name User-facing branch name (e.g., "feature_auth")
|
|
28
|
+
* @param options.source Source database to clone; defaults to the main database.
|
|
29
|
+
*/
|
|
30
|
+
createBranch(name: string, options?: {
|
|
31
|
+
source?: string;
|
|
32
|
+
}): Promise<BranchInfo>;
|
|
33
|
+
/**
|
|
34
|
+
* Delete a branch database and remove its metadata.
|
|
35
|
+
* Cannot delete the main/default database.
|
|
36
|
+
*/
|
|
37
|
+
deleteBranch(name: string): Promise<void>;
|
|
38
|
+
/**
|
|
39
|
+
* List all branches recorded in the metadata table.
|
|
40
|
+
* Optionally fetches database sizes from pg_database.
|
|
41
|
+
*/
|
|
42
|
+
listBranches(): Promise<BranchInfo[]>;
|
|
43
|
+
/**
|
|
44
|
+
* Get info about a specific branch.
|
|
45
|
+
*/
|
|
46
|
+
getBranchInfo(name: string): Promise<BranchInfo | undefined>;
|
|
47
|
+
}
|