@rebasepro/server-postgres 0.17.3 → 0.18.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/LICENSE +0 -1
- package/README.md +31 -0
- package/dist/backup-argv.d.ts +19 -0
- package/dist/branch-argv.d.ts +28 -0
- package/dist/branch-prune.d.ts +77 -0
- package/dist/cli-collections-path.d.ts +31 -0
- package/dist/cli-errors.d.ts +123 -0
- package/dist/cli-flags.d.ts +101 -0
- package/dist/cli-helpers.d.ts +94 -10
- package/dist/collection-index-C7LopMyX.js +247 -0
- package/dist/collection-index-C7LopMyX.js.map +1 -0
- package/dist/databasePoolManager.d.ts +27 -0
- package/dist/{ensure-collection-policies-UCqgv_8c.js → ensure-collection-policies-CagOzEPS.js} +6 -6
- package/dist/{ensure-collection-policies-UCqgv_8c.js.map → ensure-collection-policies-CagOzEPS.js.map} +1 -1
- package/dist/{ensure-collection-tables-DgVixhX3.js → ensure-collection-tables-DbAhrjnG.js} +614 -52
- package/dist/ensure-collection-tables-DbAhrjnG.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.es.js +19307 -3059
- package/dist/index.es.js.map +1 -1
- package/dist/{rls-enforcement-DvAbL9YJ.js → rls-enforcement-ClJoTboT.js} +53 -189
- package/dist/rls-enforcement-ClJoTboT.js.map +1 -0
- package/dist/schema/column-type-drift.d.ts +79 -0
- package/dist/schema/doctor.d.ts +42 -0
- package/dist/schema/ensure-collection-tables.d.ts +90 -0
- package/dist/schema/generate-next-step.d.ts +30 -0
- package/dist/schema/generate-postgres-ddl-logic.d.ts +28 -0
- package/dist/schema/generated-schema-staleness.d.ts +37 -0
- package/dist/schema/introspect-db-queries.d.ts +34 -0
- package/dist/security/rls-enforcement.d.ts +63 -2
- package/dist/services/BranchService.d.ts +86 -1
- package/dist/services/collection-helpers.d.ts +1 -1
- package/dist/services/realtimeService.d.ts +30 -2
- package/dist/services/row-pipeline.d.ts +12 -1
- package/dist/utils/pg-error-utils.d.ts +3 -1
- package/dist/utils/pg-numeric-number-patch.d.ts +31 -0
- package/package.json +32 -27
- package/dist/backup-service-DCk7KhhL.js +0 -9044
- package/dist/backup-service-DCk7KhhL.js.map +0 -1
- package/dist/collection-index-BRUg10H5.js +0 -5690
- package/dist/collection-index-BRUg10H5.js.map +0 -1
- package/dist/connection-GOKU3Hu5.js +0 -277
- package/dist/connection-GOKU3Hu5.js.map +0 -1
- package/dist/ensure-collection-tables-DgVixhX3.js.map +0 -1
- package/dist/rls-bootstrap-sql-B5C9LoJ6.js +0 -244
- package/dist/rls-bootstrap-sql-B5C9LoJ6.js.map +0 -1
- package/dist/rls-enforcement-DvAbL9YJ.js.map +0 -1
- package/dist/src-DiB5RP2Z.js +0 -541
- package/dist/src-DiB5RP2Z.js.map +0 -1
- package/dist/websocket-BZ4H5wUz.js +0 -8198
- package/dist/websocket-BZ4H5wUz.js.map +0 -1
- package/src/PostgresAdapter.ts +0 -91
- package/src/PostgresBackendDriver.ts +0 -1857
- package/src/PostgresBootstrapper.ts +0 -1157
- package/src/auth/ensure-tables.ts +0 -896
- package/src/auth/schema-version.ts +0 -260
- package/src/auth/services.ts +0 -1610
- package/src/backup/backup-cli.ts +0 -520
- package/src/backup/backup-cron.ts +0 -212
- package/src/backup/backup-logic.ts +0 -93
- package/src/backup/backup-service.ts +0 -485
- package/src/backup/index.ts +0 -12
- package/src/backup/pg-tools.ts +0 -403
- package/src/backup/retention.ts +0 -86
- package/src/cli-errors.ts +0 -265
- package/src/cli-helpers.ts +0 -569
- package/src/cli-output.ts +0 -43
- package/src/cli.ts +0 -1339
- package/src/collections/PostgresCollectionRegistry.ts +0 -108
- package/src/collections/buildRegistry.ts +0 -68
- package/src/collections/validate-relations.ts +0 -402
- package/src/connection.ts +0 -333
- package/src/data-transformer.ts +0 -845
- package/src/databasePoolManager.ts +0 -95
- package/src/history/HistoryService.ts +0 -254
- package/src/history/ensure-history-table.ts +0 -53
- package/src/index.ts +0 -19
- package/src/interfaces.ts +0 -60
- package/src/module-dir.ts +0 -7
- package/src/schema/atlas-argv.ts +0 -94
- package/src/schema/auth-schema.ts +0 -298
- package/src/schema/auth-users-columns.ts +0 -131
- package/src/schema/carved-out-migration.ts +0 -404
- package/src/schema/classify-change.ts +0 -436
- package/src/schema/collection-index.ts +0 -427
- package/src/schema/collections-schema-version.ts +0 -103
- package/src/schema/destructive-sql.ts +0 -94
- package/src/schema/doctor-cli.ts +0 -66
- package/src/schema/doctor-policy-checks.ts +0 -105
- package/src/schema/doctor.ts +0 -837
- package/src/schema/drizzle-ddl.ts +0 -33
- package/src/schema/dynamic-tables.test.ts +0 -302
- package/src/schema/dynamic-tables.ts +0 -293
- package/src/schema/ensure-collection-policies.ts +0 -198
- package/src/schema/ensure-collection-tables.test.ts +0 -518
- package/src/schema/ensure-collection-tables.ts +0 -1267
- package/src/schema/generate-drizzle-schema-logic.ts +0 -957
- package/src/schema/generate-drizzle-schema.ts +0 -143
- package/src/schema/generate-postgres-ddl-logic.ts +0 -1339
- package/src/schema/generate-postgres-ddl.ts +0 -180
- package/src/schema/generate-schema-commit.ts +0 -267
- package/src/schema/generated-schema-staleness.ts +0 -284
- package/src/schema/introspect-db-constraints.ts +0 -385
- package/src/schema/introspect-db-inference.ts +0 -255
- package/src/schema/introspect-db-logic.ts +0 -1387
- package/src/schema/introspect-db-naming.ts +0 -15
- package/src/schema/introspect-db-project.ts +0 -78
- package/src/schema/introspect-db-queries.ts +0 -326
- package/src/schema/introspect-db-structure.ts +0 -670
- package/src/schema/introspect-db-types.ts +0 -56
- package/src/schema/introspect-db.ts +0 -257
- package/src/schema/introspect-runtime.test.ts +0 -260
- package/src/schema/introspect-runtime.ts +0 -327
- package/src/schema/non-sql-collections.test.ts +0 -131
- package/src/schema/rls-bootstrap-sql.ts +0 -288
- package/src/schema/search-column.ts +0 -643
- package/src/schema/test-schema.ts +0 -11
- package/src/schema/vector-index.ts +0 -491
- package/src/security/anonymous-grants.test.ts +0 -73
- package/src/security/policy-drift.test.ts +0 -435
- package/src/security/policy-drift.ts +0 -473
- package/src/security/rls-enforcement.ts +0 -505
- package/src/services/BranchService.ts +0 -326
- package/src/services/FetchService.ts +0 -2115
- package/src/services/PersistService.ts +0 -557
- package/src/services/RelationService.ts +0 -978
- package/src/services/RelationWriteService.ts +0 -653
- package/src/services/cdc/CdcListener.ts +0 -103
- package/src/services/cdc/junction-tables.ts +0 -91
- package/src/services/cdc/trigger-cdc.ts +0 -173
- package/src/services/channel-bus/ChannelBus.ts +0 -44
- package/src/services/channel-bus/PostgresChannelBus.ts +0 -299
- package/src/services/channel-bus/index.ts +0 -114
- package/src/services/channel-history.ts +0 -424
- package/src/services/channel-presence.ts +0 -185
- package/src/services/collection-helpers.ts +0 -420
- package/src/services/dataService.ts +0 -256
- package/src/services/index.ts +0 -14
- package/src/services/junction-writes.ts +0 -295
- package/src/services/nested-path.ts +0 -145
- package/src/services/pg-notify-listener.ts +0 -151
- package/src/services/realtimeService.ts +0 -2545
- package/src/services/row-pipeline.ts +0 -240
- package/src/services/write-denial.ts +0 -55
- package/src/types.ts +0 -4
- package/src/utils/connection-string.ts +0 -58
- package/src/utils/drizzle-conditions.ts +0 -2743
- package/src/utils/pg-array-null-patch.ts +0 -42
- package/src/utils/pg-error-utils.ts +0 -391
- package/src/websocket.ts +0 -855
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
import { Pool } from "pg";
|
|
2
|
-
import { drizzle } from "drizzle-orm/node-postgres";
|
|
3
|
-
import { NodePgDatabase } from "drizzle-orm/node-postgres";
|
|
4
|
-
import { logger } from "@rebasepro/server";
|
|
5
|
-
import { guardPoolAgainstDirtyRelease, pinSearchPath, cappedPoolMax } from "./connection";
|
|
6
|
-
|
|
7
|
-
export class DatabasePoolManager {
|
|
8
|
-
private pools: Map<string, Pool> = new Map();
|
|
9
|
-
private drizzleInstances: Map<string, NodePgDatabase> = new Map();
|
|
10
|
-
public readonly defaultDatabaseName: string;
|
|
11
|
-
private readonly rootConnectionString: string;
|
|
12
|
-
|
|
13
|
-
constructor(adminConnectionString: string) {
|
|
14
|
-
this.rootConnectionString = adminConnectionString;
|
|
15
|
-
try {
|
|
16
|
-
const url = new URL(adminConnectionString);
|
|
17
|
-
this.defaultDatabaseName = url.pathname.slice(1);
|
|
18
|
-
} catch (e) {
|
|
19
|
-
throw new Error(`Invalid adminConnectionString provided: ${e}`);
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
public getDrizzle(databaseName: string): NodePgDatabase<Record<string, never>> {
|
|
24
|
-
const existing = this.drizzleInstances.get(databaseName);
|
|
25
|
-
if (existing) {
|
|
26
|
-
return existing;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
const pool = this.getPool(databaseName);
|
|
30
|
-
const db = drizzle(pool);
|
|
31
|
-
this.drizzleInstances.set(databaseName, db);
|
|
32
|
-
return db;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
public getPool(databaseName: string): Pool {
|
|
36
|
-
if (this.pools.has(databaseName)) {
|
|
37
|
-
return this.pools.get(databaseName)!;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
const url = new URL(this.rootConnectionString);
|
|
41
|
-
url.pathname = `/${databaseName}`;
|
|
42
|
-
|
|
43
|
-
const pool = new Pool({
|
|
44
|
-
// Same pin as the primary pool: these are branch/multi-database
|
|
45
|
-
// connections to the *same* server, so they inherit the same
|
|
46
|
-
// `"$user"` hazard. See `pinSearchPath`.
|
|
47
|
-
connectionString: pinSearchPath(url.toString()),
|
|
48
|
-
// Capped by REBASE_DB_POOL_MAX, which the managed development
|
|
49
|
-
// database sets to 1: PGlite multiplexes onto a single session and
|
|
50
|
-
// overlapping transactions deadlock there.
|
|
51
|
-
max: cappedPoolMax(10),
|
|
52
|
-
idleTimeoutMillis: 10000, // Reduced from 30000 for aggressive cleanup
|
|
53
|
-
allowExitOnIdle: true // Prevent idle clients from hanging the Node.js process
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
// Prevent idle client errors from crashing the Node.js process
|
|
57
|
-
pool.on("error", (err) => {
|
|
58
|
-
logger.error(`[DatabasePoolManager] Unexpected error on idle client for db ${databaseName}`, { error: err });
|
|
59
|
-
});
|
|
60
|
-
guardPoolAgainstDirtyRelease(pool, `pg-pool:${databaseName}`);
|
|
61
|
-
|
|
62
|
-
this.pools.set(databaseName, pool);
|
|
63
|
-
return pool;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Disconnect and remove the pool for a specific database.
|
|
68
|
-
* Required before `CREATE DATABASE ... TEMPLATE` or `DROP DATABASE`,
|
|
69
|
-
* which need exclusive access to the target database.
|
|
70
|
-
*/
|
|
71
|
-
public async disconnectDatabase(databaseName: string): Promise<void> {
|
|
72
|
-
const pool = this.pools.get(databaseName);
|
|
73
|
-
if (pool) {
|
|
74
|
-
await pool.end();
|
|
75
|
-
this.pools.delete(databaseName);
|
|
76
|
-
this.drizzleInstances.delete(databaseName);
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/** Check if a pool exists for a given database name. */
|
|
81
|
-
public hasPool(databaseName: string): boolean {
|
|
82
|
-
return this.pools.has(databaseName);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
public async shutdown(): Promise<void> {
|
|
86
|
-
const promises = [];
|
|
87
|
-
for (const [dbName, pool] of this.pools.entries()) {
|
|
88
|
-
logger.info(`[DatabasePoolManager] Shutting down pool for ${dbName}`);
|
|
89
|
-
promises.push(pool.end());
|
|
90
|
-
}
|
|
91
|
-
await Promise.all(promises);
|
|
92
|
-
this.pools.clear();
|
|
93
|
-
this.drizzleInstances.clear();
|
|
94
|
-
}
|
|
95
|
-
}
|
|
@@ -1,254 +0,0 @@
|
|
|
1
|
-
import { sql } from "drizzle-orm";
|
|
2
|
-
import { NodePgDatabase } from "drizzle-orm/node-postgres";
|
|
3
|
-
import { logger } from "@rebasepro/server";
|
|
4
|
-
import type { EntityHistoryEntry } from "@rebasepro/types";
|
|
5
|
-
|
|
6
|
-
export type {
|
|
7
|
-
RecordHistoryParams,
|
|
8
|
-
FetchHistoryOptions,
|
|
9
|
-
HistoryRetentionConfig
|
|
10
|
-
} from "@rebasepro/types";
|
|
11
|
-
import type { RecordHistoryParams, FetchHistoryOptions, HistoryRetentionConfig } from "@rebasepro/types";
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* A Postgres history row is already the wire shape — `updated_at` comes back
|
|
15
|
-
* from the driver as a string. Kept as an alias because the name is used
|
|
16
|
-
* throughout this package and in `PostgresBackendDriver`.
|
|
17
|
-
*/
|
|
18
|
-
export type HistoryEntry = EntityHistoryEntry;
|
|
19
|
-
|
|
20
|
-
const DEFAULT_RETENTION: HistoryRetentionConfig = {
|
|
21
|
-
maxEntries: 200,
|
|
22
|
-
ttlDays: 90
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Service for recording and querying row change history.
|
|
27
|
-
* Stores history entries in the `rebase.entity_history` table.
|
|
28
|
-
*/
|
|
29
|
-
export class HistoryService {
|
|
30
|
-
public retention: HistoryRetentionConfig;
|
|
31
|
-
|
|
32
|
-
constructor(
|
|
33
|
-
private db: NodePgDatabase,
|
|
34
|
-
retention?: Partial<HistoryRetentionConfig>
|
|
35
|
-
) {
|
|
36
|
-
this.retention = { ...DEFAULT_RETENTION,
|
|
37
|
-
...retention };
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Record a history entry for an row change.
|
|
42
|
-
* This is intentionally fire-and-forget safe — errors are logged but never
|
|
43
|
-
* bubble up to block the main save/delete operation.
|
|
44
|
-
*
|
|
45
|
-
* After inserting, kicks off a non-blocking pruning pass for this row.
|
|
46
|
-
*/
|
|
47
|
-
async recordHistory(params: RecordHistoryParams): Promise<void> {
|
|
48
|
-
const {
|
|
49
|
-
tableName,
|
|
50
|
-
id,
|
|
51
|
-
action,
|
|
52
|
-
values,
|
|
53
|
-
previousValues,
|
|
54
|
-
updatedBy
|
|
55
|
-
} = params;
|
|
56
|
-
|
|
57
|
-
const changedFields = previousValues && values
|
|
58
|
-
? findChangedFields(previousValues, values)
|
|
59
|
-
: null;
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
// Skip recording if this is an update with zero actual changes
|
|
63
|
-
|
|
64
|
-
if (action === "update" && (!changedFields || changedFields.length === 0)) {
|
|
65
|
-
return;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
try {
|
|
69
|
-
await this.db.execute(sql`
|
|
70
|
-
INSERT INTO rebase.entity_history
|
|
71
|
-
(table_name, entity_id, action, changed_fields, "values", previous_values, updated_by)
|
|
72
|
-
VALUES (
|
|
73
|
-
${tableName},
|
|
74
|
-
${String(id)},
|
|
75
|
-
${action},
|
|
76
|
-
${changedFields ? sql`ARRAY[${sql.join(changedFields.map(f => sql`${f}`), sql`, `)}]::text[]` : sql`NULL`},
|
|
77
|
-
${values ? sql`${JSON.stringify(values)}::jsonb` : sql`NULL`},
|
|
78
|
-
${previousValues ? sql`${JSON.stringify(previousValues)}::jsonb` : sql`NULL`},
|
|
79
|
-
${updatedBy ?? null}
|
|
80
|
-
)
|
|
81
|
-
`);
|
|
82
|
-
|
|
83
|
-
// Non-blocking prune for this specific row
|
|
84
|
-
this.pruneEntity(tableName, id).catch(err =>
|
|
85
|
-
logger.error("History prune failed", { error: err })
|
|
86
|
-
);
|
|
87
|
-
} catch (error) {
|
|
88
|
-
logger.error("Failed to record row history", { error: error });
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* Fetch history entries for an row, ordered by most recent first.
|
|
94
|
-
*/
|
|
95
|
-
async fetchHistory(
|
|
96
|
-
tableName: string,
|
|
97
|
-
id: string,
|
|
98
|
-
options: FetchHistoryOptions = {}
|
|
99
|
-
): Promise<{ data: HistoryEntry[]; total: number }> {
|
|
100
|
-
const limit = options.limit ?? 20;
|
|
101
|
-
const offset = options.offset ?? 0;
|
|
102
|
-
|
|
103
|
-
const [countResult, dataResult] = await Promise.all([
|
|
104
|
-
this.db.execute(sql`
|
|
105
|
-
SELECT COUNT(*) as count
|
|
106
|
-
FROM rebase.entity_history
|
|
107
|
-
WHERE table_name = ${tableName}
|
|
108
|
-
AND entity_id = ${String(id)}
|
|
109
|
-
`),
|
|
110
|
-
this.db.execute(sql`
|
|
111
|
-
SELECT id, table_name, entity_id, action, changed_fields,
|
|
112
|
-
"values", previous_values, updated_by, updated_at
|
|
113
|
-
FROM rebase.entity_history
|
|
114
|
-
WHERE table_name = ${tableName}
|
|
115
|
-
AND entity_id = ${String(id)}
|
|
116
|
-
ORDER BY updated_at DESC
|
|
117
|
-
LIMIT ${limit}
|
|
118
|
-
OFFSET ${offset}
|
|
119
|
-
`)
|
|
120
|
-
]);
|
|
121
|
-
|
|
122
|
-
const total = parseInt(
|
|
123
|
-
(countResult.rows[0] as Record<string, string>)?.count ?? "0",
|
|
124
|
-
10
|
|
125
|
-
);
|
|
126
|
-
|
|
127
|
-
return {
|
|
128
|
-
data: dataResult.rows as unknown as HistoryEntry[],
|
|
129
|
-
total
|
|
130
|
-
};
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
/**
|
|
134
|
-
* Fetch a single history entry by ID.
|
|
135
|
-
*/
|
|
136
|
-
async fetchHistoryEntry(historyId: string): Promise<HistoryEntry | null> {
|
|
137
|
-
const result = await this.db.execute(sql`
|
|
138
|
-
SELECT id, table_name, entity_id, action, changed_fields,
|
|
139
|
-
"values", previous_values, updated_by, updated_at
|
|
140
|
-
FROM rebase.entity_history
|
|
141
|
-
WHERE id = ${historyId}
|
|
142
|
-
`);
|
|
143
|
-
|
|
144
|
-
if (result.rows.length === 0) return null;
|
|
145
|
-
return result.rows[0] as unknown as HistoryEntry;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
// ───────── Retention / Pruning ─────────
|
|
149
|
-
|
|
150
|
-
/**
|
|
151
|
-
* Prune history for a single row: enforce maxEntries and TTL.
|
|
152
|
-
*/
|
|
153
|
-
async pruneEntity(tableName: string, id: string): Promise<number> {
|
|
154
|
-
let deleted = 0;
|
|
155
|
-
|
|
156
|
-
// 1. TTL — delete entries older than ttlDays
|
|
157
|
-
const ttlResult = await this.db.execute(sql`
|
|
158
|
-
DELETE FROM rebase.entity_history
|
|
159
|
-
WHERE table_name = ${tableName}
|
|
160
|
-
AND entity_id = ${String(id)}
|
|
161
|
-
AND updated_at < NOW() - MAKE_INTERVAL(days => ${this.retention.ttlDays})
|
|
162
|
-
`);
|
|
163
|
-
deleted += ttlResult.rowCount ?? 0;
|
|
164
|
-
|
|
165
|
-
// 2. Max entries — keep the newest maxEntries, delete the rest
|
|
166
|
-
const maxResult = await this.db.execute(sql`
|
|
167
|
-
DELETE FROM rebase.entity_history
|
|
168
|
-
WHERE id IN (
|
|
169
|
-
SELECT id FROM rebase.entity_history
|
|
170
|
-
WHERE table_name = ${tableName}
|
|
171
|
-
AND entity_id = ${String(id)}
|
|
172
|
-
ORDER BY updated_at DESC
|
|
173
|
-
OFFSET ${this.retention.maxEntries}
|
|
174
|
-
)
|
|
175
|
-
`);
|
|
176
|
-
deleted += maxResult.rowCount ?? 0;
|
|
177
|
-
|
|
178
|
-
return deleted;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
/**
|
|
182
|
-
* Global prune: enforce TTL across ALL rows in a single sweep.
|
|
183
|
-
* Intended to be called periodically (e.g. once per hour or daily).
|
|
184
|
-
*/
|
|
185
|
-
async pruneExpired(): Promise<number> {
|
|
186
|
-
const result = await this.db.execute(sql`
|
|
187
|
-
DELETE FROM rebase.entity_history
|
|
188
|
-
WHERE updated_at < NOW() - MAKE_INTERVAL(days => ${this.retention.ttlDays})
|
|
189
|
-
`);
|
|
190
|
-
return result.rowCount ?? 0;
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
/**
|
|
196
|
-
* Deep equality without JSON.stringify.
|
|
197
|
-
* Handles primitives, arrays, Dates, and plain objects recursively.
|
|
198
|
-
*/
|
|
199
|
-
function deepEqual(a: unknown, b: unknown): boolean {
|
|
200
|
-
if (a === b) return true;
|
|
201
|
-
if (a == null || b == null) return false;
|
|
202
|
-
if (a instanceof Date && b instanceof Date) return a.getTime() === b.getTime();
|
|
203
|
-
if (Array.isArray(a) && Array.isArray(b)) {
|
|
204
|
-
if (a.length !== b.length) return false;
|
|
205
|
-
return a.every((v, i) => deepEqual(v, b[i]));
|
|
206
|
-
}
|
|
207
|
-
if (typeof a === "object" && typeof b === "object") {
|
|
208
|
-
const aObj = a as Record<string, unknown>;
|
|
209
|
-
const bObj = b as Record<string, unknown>;
|
|
210
|
-
const aKeys = Object.keys(aObj);
|
|
211
|
-
const bKeys = Object.keys(bObj);
|
|
212
|
-
if (aKeys.length !== bKeys.length) return false;
|
|
213
|
-
return aKeys.every(k => deepEqual(aObj[k], bObj[k]));
|
|
214
|
-
}
|
|
215
|
-
return false;
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
/**
|
|
219
|
-
* Shallow comparison to find top-level keys that changed between two objects.
|
|
220
|
-
*/
|
|
221
|
-
export function findChangedFields(
|
|
222
|
-
oldValues: Record<string, unknown>,
|
|
223
|
-
newValues: Record<string, unknown>
|
|
224
|
-
): string[] | null {
|
|
225
|
-
const changed: string[] = [];
|
|
226
|
-
const allKeys = new Set([
|
|
227
|
-
...Object.keys(oldValues),
|
|
228
|
-
...Object.keys(newValues)
|
|
229
|
-
]);
|
|
230
|
-
|
|
231
|
-
for (const key of allKeys) {
|
|
232
|
-
const oldVal = oldValues[key];
|
|
233
|
-
const newVal = newValues[key];
|
|
234
|
-
|
|
235
|
-
// Skip internal metadata
|
|
236
|
-
if (key.startsWith("__")) continue;
|
|
237
|
-
|
|
238
|
-
if (oldVal !== newVal) {
|
|
239
|
-
// For objects/arrays, use structural comparison
|
|
240
|
-
if (
|
|
241
|
-
typeof oldVal === "object" && oldVal !== null &&
|
|
242
|
-
typeof newVal === "object" && newVal !== null
|
|
243
|
-
) {
|
|
244
|
-
if (!deepEqual(oldVal, newVal)) {
|
|
245
|
-
changed.push(key);
|
|
246
|
-
}
|
|
247
|
-
} else {
|
|
248
|
-
changed.push(key);
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
return changed.length > 0 ? changed : null;
|
|
254
|
-
}
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
import { sql } from "drizzle-orm";
|
|
2
|
-
import { NodePgDatabase } from "drizzle-orm/node-postgres";
|
|
3
|
-
import { logger } from "@rebasepro/server";
|
|
4
|
-
import { revokeInternalTableSql } from "@rebasepro/common";
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Auto-create the row history table if it doesn't exist.
|
|
8
|
-
* This runs on startup when history is enabled, following the same
|
|
9
|
-
* pattern as `ensureAuthTablesExist`.
|
|
10
|
-
*/
|
|
11
|
-
export async function ensureHistoryTableExists(db: NodePgDatabase): Promise<void> {
|
|
12
|
-
logger.debug("🔍 Checking row history table...");
|
|
13
|
-
|
|
14
|
-
try {
|
|
15
|
-
// Create the rebase schema (idempotent — may already exist from auth init)
|
|
16
|
-
await db.execute(sql`CREATE SCHEMA IF NOT EXISTS rebase`);
|
|
17
|
-
|
|
18
|
-
await db.execute(sql`
|
|
19
|
-
CREATE TABLE IF NOT EXISTS rebase.entity_history (
|
|
20
|
-
id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
|
|
21
|
-
table_name TEXT NOT NULL,
|
|
22
|
-
entity_id TEXT NOT NULL,
|
|
23
|
-
action TEXT NOT NULL,
|
|
24
|
-
changed_fields TEXT[],
|
|
25
|
-
"values" JSONB,
|
|
26
|
-
previous_values JSONB,
|
|
27
|
-
updated_by TEXT,
|
|
28
|
-
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
29
|
-
)
|
|
30
|
-
`);
|
|
31
|
-
|
|
32
|
-
await db.execute(sql`
|
|
33
|
-
CREATE INDEX IF NOT EXISTS idx_history_entity
|
|
34
|
-
ON rebase.entity_history(table_name, entity_id)
|
|
35
|
-
`);
|
|
36
|
-
|
|
37
|
-
await db.execute(sql`
|
|
38
|
-
CREATE INDEX IF NOT EXISTS idx_history_time
|
|
39
|
-
ON rebase.entity_history(table_name, entity_id, updated_at DESC)
|
|
40
|
-
`);
|
|
41
|
-
|
|
42
|
-
// Every previous value of every audited row, in one table with no RLS
|
|
43
|
-
// and no tenant scoping — so a readable copy defeats the row policies on
|
|
44
|
-
// the tables it shadows. The driver's schema-wide grant reaches it
|
|
45
|
-
// (created here, after that grant ran), so take it back.
|
|
46
|
-
await db.execute(sql.raw(revokeInternalTableSql("rebase", "entity_history")));
|
|
47
|
-
|
|
48
|
-
logger.debug("✅ Entity history table ready");
|
|
49
|
-
} catch (error) {
|
|
50
|
-
logger.error("❌ Failed to create row history table", { error: error });
|
|
51
|
-
logger.warn("⚠️ Continuing without creating history table.");
|
|
52
|
-
}
|
|
53
|
-
}
|
package/src/index.ts
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
export * from "./connection";
|
|
2
|
-
export * from "./interfaces";
|
|
3
|
-
export * from "./PostgresBackendDriver";
|
|
4
|
-
export * from "./databasePoolManager";
|
|
5
|
-
export * from "./schema/auth-schema";
|
|
6
|
-
export * from "./schema/generate-drizzle-schema-logic";
|
|
7
|
-
export * from "./schema/generate-drizzle-schema";
|
|
8
|
-
export * from "./utils/drizzle-conditions";
|
|
9
|
-
export * from "./services/realtimeService";
|
|
10
|
-
// The channel bus is a public extension point: a transport published as its own
|
|
11
|
-
// package implements `ChannelBus` (declared in @rebasepro/types and re-exported
|
|
12
|
-
// here) and is passed to `realtime.bus`. Without this export it would be one.
|
|
13
|
-
export * from "./services/channel-bus";
|
|
14
|
-
export * from "./websocket";
|
|
15
|
-
export * from "./collections/PostgresCollectionRegistry";
|
|
16
|
-
export * from "./services/BranchService";
|
|
17
|
-
export * from "./backup";
|
|
18
|
-
export * from "./PostgresBootstrapper";
|
|
19
|
-
export * from "./PostgresAdapter";
|
package/src/interfaces.ts
DELETED
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Database Abstraction Interfaces
|
|
3
|
-
*
|
|
4
|
-
* These interfaces define the contracts that any database backend must implement
|
|
5
|
-
* to be used with Rebase. This allows for pluggable database backends like
|
|
6
|
-
* PostgreSQL, MongoDB, MySQL, etc.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import {
|
|
10
|
-
Entity,
|
|
11
|
-
CollectionConfig,
|
|
12
|
-
FilterValues,
|
|
13
|
-
WhereFilterOp,
|
|
14
|
-
DatabaseConnection,
|
|
15
|
-
QueryFilter,
|
|
16
|
-
FetchCollectionOptions,
|
|
17
|
-
SearchOptions,
|
|
18
|
-
CountOptions,
|
|
19
|
-
ConditionBuilder,
|
|
20
|
-
ConditionBuilderStatic,
|
|
21
|
-
DataRepository,
|
|
22
|
-
CollectionSubscriptionConfig,
|
|
23
|
-
SingleSubscriptionConfig,
|
|
24
|
-
RealtimeProvider,
|
|
25
|
-
CollectionRegistryInterface,
|
|
26
|
-
DataTransformer,
|
|
27
|
-
BackendConfig,
|
|
28
|
-
BackendInstance,
|
|
29
|
-
BackendFactory
|
|
30
|
-
} from "@rebasepro/types";
|
|
31
|
-
import { NodePgDatabase } from "drizzle-orm/node-postgres";
|
|
32
|
-
import { PgTransaction } from "drizzle-orm/pg-core";
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Type representing either a direct database connection or a transaction.
|
|
36
|
-
* Used to allow services to operate within a transaction context.
|
|
37
|
-
* Note: `any` is intentional here — it represents a Drizzle client with
|
|
38
|
-
* a dynamic schema, enabling `db.query[tableName]` access without casts.
|
|
39
|
-
*/
|
|
40
|
-
|
|
41
|
-
export type DrizzleClient = NodePgDatabase<any> | PgTransaction<any, any, any>;
|
|
42
|
-
|
|
43
|
-
export type {
|
|
44
|
-
DatabaseConnection,
|
|
45
|
-
QueryFilter,
|
|
46
|
-
FetchCollectionOptions,
|
|
47
|
-
SearchOptions,
|
|
48
|
-
CountOptions,
|
|
49
|
-
ConditionBuilder,
|
|
50
|
-
ConditionBuilderStatic,
|
|
51
|
-
DataRepository,
|
|
52
|
-
CollectionSubscriptionConfig,
|
|
53
|
-
SingleSubscriptionConfig,
|
|
54
|
-
RealtimeProvider,
|
|
55
|
-
CollectionRegistryInterface,
|
|
56
|
-
DataTransformer,
|
|
57
|
-
BackendConfig,
|
|
58
|
-
BackendInstance,
|
|
59
|
-
BackendFactory
|
|
60
|
-
};
|
package/src/module-dir.ts
DELETED
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
import path from "path";
|
|
2
|
-
import { fileURLToPath } from "url";
|
|
3
|
-
|
|
4
|
-
// Isolated in its own module so the jest CommonJS transform can swap it for a
|
|
5
|
-
// __dirname shim via moduleNameMapper — `import.meta` is a syntax error once
|
|
6
|
-
// ts-jest transpiles to CJS.
|
|
7
|
-
export const moduleDir = path.dirname(fileURLToPath(import.meta.url));
|
package/src/schema/atlas-argv.ts
DELETED
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The argv `runAtlas` hands the Atlas binary, as a pure function.
|
|
3
|
-
*
|
|
4
|
-
* Split out of `cli.ts` because the flags an Atlas subcommand accepts are not
|
|
5
|
-
* uniform and getting one wrong is fatal rather than degraded: Atlas rejects an
|
|
6
|
-
* unknown flag before doing any work, so a flag on the wrong subcommand takes
|
|
7
|
-
* the whole command down.
|
|
8
|
-
*
|
|
9
|
-
* `--exclude` is the one that bit. Of the six invocations here, exactly one
|
|
10
|
-
* accepts it. The guard this replaces was `args.includes("apply") ||
|
|
11
|
-
* args.includes("diff")`, and reading it as a *subcommand* test is the trap:
|
|
12
|
-
* `migrate apply` passes `args.includes("apply")` just as `schema apply` does.
|
|
13
|
-
* So the flag went onto three invocations, two of which reject it, and both
|
|
14
|
-
* `rebase db generate` and `rebase db migrate` exited 1 with `unknown flag:
|
|
15
|
-
* --exclude` for every project declaring a `search` block.
|
|
16
|
-
*
|
|
17
|
-
* The domain is half the identity of an Atlas subcommand, and a guard that
|
|
18
|
-
* looks only at `args` cannot tell `schema apply` from `migrate apply`.
|
|
19
|
-
*
|
|
20
|
-
* Nothing here touches a database or the filesystem, so the whole matrix is
|
|
21
|
-
* unit-tested in `atlas-argv.test.ts`.
|
|
22
|
-
*/
|
|
23
|
-
|
|
24
|
-
export interface AtlasInvocation {
|
|
25
|
-
/** `schema` or `migrate`. */
|
|
26
|
-
domain: string;
|
|
27
|
-
/** The subcommand and its own flags, e.g. `["diff", "--dir", "file://…"]`. */
|
|
28
|
-
args: string[];
|
|
29
|
-
/** The target database, already rewritten for libpq. */
|
|
30
|
-
url: string;
|
|
31
|
-
/** The database Atlas plans against, already rewritten for libpq. */
|
|
32
|
-
devUrl: string;
|
|
33
|
-
/**
|
|
34
|
-
* Everything the caller resolved for `--exclude`, in the order it should
|
|
35
|
-
* appear. Dropped entirely when the subcommand does not accept the flag —
|
|
36
|
-
* see {@link acceptsExcludeFlag}.
|
|
37
|
-
*/
|
|
38
|
-
excludes?: string[];
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Does this Atlas invocation accept `--exclude`?
|
|
43
|
-
*
|
|
44
|
-
* Only `atlas schema apply` does, on the pinned 1.2.3. Measured against the
|
|
45
|
-
* binary: `migrate diff` and `migrate apply` both answer `unknown flag:
|
|
46
|
-
* --exclude`, and neither lists it in `--help` (`migrate apply` offers
|
|
47
|
-
* `--url/--dir/--format/--revisions-schema/--dry-run/--lock-name/--lock-timeout/`
|
|
48
|
-
* `--skip-lock/--baseline/--to-version/--tx-mode/--exec-order/--allow-dirty`).
|
|
49
|
-
* Hence `domain === "schema"` and not just the subcommand.
|
|
50
|
-
*
|
|
51
|
-
* The `exclude` attribute of an `atlas.hcl` `env` block is not a way round it
|
|
52
|
-
* either: measured, it is accepted and then ignored on the diff path, which is
|
|
53
|
-
* the silent-no-match shape that costs the most time to notice.
|
|
54
|
-
*
|
|
55
|
-
* So the diff keeps the carved-out objects out of the migration afterwards
|
|
56
|
-
* instead, with `stripCarvedOutStatements`.
|
|
57
|
-
*/
|
|
58
|
-
export function acceptsExcludeFlag(domain: string, args: string[]): boolean {
|
|
59
|
-
return domain === "schema" && args.includes("apply");
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/** Assemble the full argv, connection flags and all. */
|
|
63
|
-
export function buildAtlasArgs(invocation: AtlasInvocation): string[] {
|
|
64
|
-
const { domain, args, url, devUrl, excludes = [] } = invocation;
|
|
65
|
-
const argv = [domain, ...args];
|
|
66
|
-
|
|
67
|
-
if (domain === "schema") {
|
|
68
|
-
if (args.includes("apply")) {
|
|
69
|
-
argv.push("--url", url, "--dev-url", devUrl);
|
|
70
|
-
} else if (args.includes("clean") || args.includes("inspect")) {
|
|
71
|
-
argv.push("--url", url);
|
|
72
|
-
}
|
|
73
|
-
} else if (domain === "migrate") {
|
|
74
|
-
if (args.includes("diff")) {
|
|
75
|
-
argv.push("--dev-url", devUrl);
|
|
76
|
-
} else if (args.includes("apply") || args.includes("status")) {
|
|
77
|
-
argv.push("--url", url, "--revisions-schema", "rebase");
|
|
78
|
-
if (args.includes("apply")) {
|
|
79
|
-
argv.push("--allow-dirty");
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// Second line of defence, deliberately not a caller's responsibility: a
|
|
85
|
-
// future caller that resolves excludes for the diff path gets them dropped
|
|
86
|
-
// here rather than a command that will not run.
|
|
87
|
-
if (acceptsExcludeFlag(domain, args)) {
|
|
88
|
-
for (const exclude of excludes) {
|
|
89
|
-
argv.push("--exclude", exclude);
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
return argv;
|
|
94
|
-
}
|