@rebasepro/server-postgresql 0.4.0 → 0.6.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/README.md +69 -89
- package/dist/{server-postgresql/src/PostgresAdapter.d.ts → PostgresAdapter.d.ts} +1 -1
- package/dist/{server-postgresql/src/PostgresBackendDriver.d.ts → PostgresBackendDriver.d.ts} +2 -2
- package/dist/{server-postgresql/src/PostgresBootstrapper.d.ts → PostgresBootstrapper.d.ts} +11 -1
- package/dist/{server-postgresql/src/auth → auth}/services.d.ts +11 -11
- package/dist/{server-postgresql/src/collections → collections}/PostgresCollectionRegistry.d.ts +4 -0
- package/dist/{server-postgresql/src/data-transformer.d.ts → data-transformer.d.ts} +0 -3
- package/dist/{server-postgresql/src/databasePoolManager.d.ts → databasePoolManager.d.ts} +1 -1
- package/dist/index.es.js +10174 -11184
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +10735 -11462
- package/dist/index.umd.js.map +1 -1
- package/dist/{server-postgresql/src/services → services}/EntityPersistService.d.ts +0 -14
- package/dist/types.d.ts +3 -0
- package/dist/utils/pg-error-utils.d.ts +55 -0
- package/dist/{server-postgresql/src/websocket.d.ts → websocket.d.ts} +8 -3
- package/package.json +24 -21
- package/src/PostgresAdapter.ts +9 -10
- package/src/PostgresBackendDriver.ts +135 -122
- package/src/PostgresBootstrapper.ts +90 -16
- package/src/auth/ensure-tables.ts +28 -5
- package/src/auth/services.ts +56 -45
- package/src/cli.ts +140 -110
- package/src/collections/PostgresCollectionRegistry.ts +7 -0
- package/src/connection.ts +11 -6
- package/src/data-transformer.ts +73 -109
- package/src/databasePoolManager.ts +5 -3
- package/src/history/HistoryService.ts +3 -2
- package/src/history/ensure-history-table.ts +5 -4
- package/src/schema/auth-schema.ts +1 -2
- package/src/schema/doctor-cli.ts +2 -1
- package/src/schema/doctor.ts +40 -37
- package/src/schema/generate-drizzle-schema-logic.ts +56 -18
- package/src/schema/generate-drizzle-schema.ts +11 -11
- package/src/schema/introspect-db-inference.ts +25 -25
- package/src/schema/introspect-db-logic.ts +38 -38
- package/src/schema/introspect-db.ts +28 -27
- package/src/services/BranchService.ts +14 -0
- package/src/services/EntityFetchService.ts +28 -25
- package/src/services/EntityPersistService.ts +11 -124
- package/src/services/RelationService.ts +57 -37
- package/src/services/entity-helpers.ts +6 -2
- package/src/services/realtimeService.ts +45 -32
- package/src/types.ts +4 -0
- package/src/utils/drizzle-conditions.ts +31 -15
- package/src/utils/pg-error-utils.ts +211 -0
- package/src/websocket.ts +51 -33
- package/test/auth-services.test.ts +36 -19
- package/test/batch-many-to-many-regression.test.ts +119 -39
- package/test/data-transformer-hardening.test.ts +67 -33
- package/test/data-transformer.test.ts +4 -2
- package/test/doctor.test.ts +10 -5
- package/test/drizzle-conditions.test.ts +59 -6
- package/test/generate-drizzle-schema.test.ts +65 -40
- package/test/introspect-db-generation.test.ts +179 -81
- package/test/introspect-db-utils.test.ts +92 -37
- package/test/mocks/chalk.cjs +7 -0
- package/test/pg-error-utils.test.ts +221 -0
- package/test/postgresDataDriver.test.ts +14 -5
- package/test/property-ordering.test.ts +126 -79
- package/test/realtimeService.test.ts +6 -2
- package/test/relation-pipeline-gaps.test.ts +84 -36
- package/test/relations.test.ts +247 -0
- package/test/unmapped-tables-safety.test.ts +14 -6
- package/test/websocket.test.ts +1 -1
- package/tsconfig.json +5 -0
- package/tsconfig.prod.json +3 -0
- package/vite.config.ts +5 -5
- package/dist/common/src/collections/CollectionRegistry.d.ts +0 -56
- package/dist/common/src/collections/default-collections.d.ts +0 -9
- package/dist/common/src/collections/index.d.ts +0 -2
- package/dist/common/src/data/buildRebaseData.d.ts +0 -14
- package/dist/common/src/data/query_builder.d.ts +0 -55
- package/dist/common/src/index.d.ts +0 -4
- package/dist/common/src/util/builders.d.ts +0 -57
- package/dist/common/src/util/callbacks.d.ts +0 -6
- package/dist/common/src/util/collections.d.ts +0 -11
- package/dist/common/src/util/common.d.ts +0 -2
- package/dist/common/src/util/conditions.d.ts +0 -26
- package/dist/common/src/util/entities.d.ts +0 -58
- package/dist/common/src/util/enums.d.ts +0 -3
- package/dist/common/src/util/index.d.ts +0 -16
- package/dist/common/src/util/navigation_from_path.d.ts +0 -34
- package/dist/common/src/util/navigation_utils.d.ts +0 -20
- package/dist/common/src/util/parent_references_from_path.d.ts +0 -6
- package/dist/common/src/util/paths.d.ts +0 -14
- package/dist/common/src/util/permissions.d.ts +0 -6
- package/dist/common/src/util/references.d.ts +0 -2
- package/dist/common/src/util/relations.d.ts +0 -22
- package/dist/common/src/util/resolutions.d.ts +0 -72
- package/dist/common/src/util/storage.d.ts +0 -24
- package/dist/types/src/controllers/analytics_controller.d.ts +0 -7
- package/dist/types/src/controllers/auth.d.ts +0 -104
- package/dist/types/src/controllers/client.d.ts +0 -168
- package/dist/types/src/controllers/collection_registry.d.ts +0 -46
- package/dist/types/src/controllers/customization_controller.d.ts +0 -60
- package/dist/types/src/controllers/data.d.ts +0 -207
- package/dist/types/src/controllers/data_driver.d.ts +0 -218
- package/dist/types/src/controllers/database_admin.d.ts +0 -11
- package/dist/types/src/controllers/dialogs_controller.d.ts +0 -36
- package/dist/types/src/controllers/effective_role.d.ts +0 -4
- package/dist/types/src/controllers/email.d.ts +0 -36
- package/dist/types/src/controllers/index.d.ts +0 -18
- package/dist/types/src/controllers/local_config_persistence.d.ts +0 -20
- package/dist/types/src/controllers/navigation.d.ts +0 -225
- package/dist/types/src/controllers/registry.d.ts +0 -63
- package/dist/types/src/controllers/side_dialogs_controller.d.ts +0 -67
- package/dist/types/src/controllers/side_entity_controller.d.ts +0 -97
- package/dist/types/src/controllers/snackbar.d.ts +0 -24
- package/dist/types/src/controllers/storage.d.ts +0 -171
- package/dist/types/src/index.d.ts +0 -4
- package/dist/types/src/rebase_context.d.ts +0 -122
- package/dist/types/src/types/auth_adapter.d.ts +0 -301
- package/dist/types/src/types/backend.d.ts +0 -536
- package/dist/types/src/types/backend_hooks.d.ts +0 -172
- package/dist/types/src/types/builders.d.ts +0 -15
- package/dist/types/src/types/chips.d.ts +0 -5
- package/dist/types/src/types/collections.d.ts +0 -941
- package/dist/types/src/types/component_ref.d.ts +0 -47
- package/dist/types/src/types/cron.d.ts +0 -102
- package/dist/types/src/types/data_source.d.ts +0 -64
- package/dist/types/src/types/database_adapter.d.ts +0 -94
- package/dist/types/src/types/entities.d.ts +0 -145
- package/dist/types/src/types/entity_actions.d.ts +0 -104
- package/dist/types/src/types/entity_callbacks.d.ts +0 -173
- package/dist/types/src/types/entity_link_builder.d.ts +0 -7
- package/dist/types/src/types/entity_overrides.d.ts +0 -10
- package/dist/types/src/types/entity_views.d.ts +0 -87
- package/dist/types/src/types/export_import.d.ts +0 -21
- package/dist/types/src/types/formex.d.ts +0 -40
- package/dist/types/src/types/index.d.ts +0 -28
- package/dist/types/src/types/locales.d.ts +0 -4
- package/dist/types/src/types/modify_collections.d.ts +0 -5
- package/dist/types/src/types/plugins.d.ts +0 -282
- package/dist/types/src/types/properties.d.ts +0 -1181
- package/dist/types/src/types/property_config.d.ts +0 -74
- package/dist/types/src/types/relations.d.ts +0 -336
- package/dist/types/src/types/slots.d.ts +0 -262
- package/dist/types/src/types/translations.d.ts +0 -900
- package/dist/types/src/types/user_management_delegate.d.ts +0 -86
- package/dist/types/src/types/websockets.d.ts +0 -78
- package/dist/types/src/users/index.d.ts +0 -1
- package/dist/types/src/users/user.d.ts +0 -50
- package/drizzle.test.config.ts +0 -10
- /package/dist/{server-postgresql/src/auth → auth}/ensure-tables.d.ts +0 -0
- /package/dist/{server-postgresql/src/cli.d.ts → cli.d.ts} +0 -0
- /package/dist/{server-postgresql/src/connection.d.ts → connection.d.ts} +0 -0
- /package/dist/{server-postgresql/src/history → history}/HistoryService.d.ts +0 -0
- /package/dist/{server-postgresql/src/history → history}/ensure-history-table.d.ts +0 -0
- /package/dist/{server-postgresql/src/index.d.ts → index.d.ts} +0 -0
- /package/dist/{server-postgresql/src/interfaces.d.ts → interfaces.d.ts} +0 -0
- /package/dist/{server-postgresql/src/schema → schema}/auth-schema.d.ts +0 -0
- /package/dist/{server-postgresql/src/schema → schema}/doctor-cli.d.ts +0 -0
- /package/dist/{server-postgresql/src/schema → schema}/doctor.d.ts +0 -0
- /package/dist/{server-postgresql/src/schema → schema}/generate-drizzle-schema-logic.d.ts +0 -0
- /package/dist/{server-postgresql/src/schema → schema}/generate-drizzle-schema.d.ts +0 -0
- /package/dist/{server-postgresql/src/schema → schema}/introspect-db-inference.d.ts +0 -0
- /package/dist/{server-postgresql/src/schema → schema}/introspect-db-logic.d.ts +0 -0
- /package/dist/{server-postgresql/src/schema → schema}/introspect-db.d.ts +0 -0
- /package/dist/{server-postgresql/src/schema → schema}/test-schema.d.ts +0 -0
- /package/dist/{server-postgresql/src/services → services}/BranchService.d.ts +0 -0
- /package/dist/{server-postgresql/src/services → services}/EntityFetchService.d.ts +0 -0
- /package/dist/{server-postgresql/src/services → services}/RelationService.d.ts +0 -0
- /package/dist/{server-postgresql/src/services → services}/entity-helpers.d.ts +0 -0
- /package/dist/{server-postgresql/src/services → services}/entityService.d.ts +0 -0
- /package/dist/{server-postgresql/src/services → services}/index.d.ts +0 -0
- /package/dist/{server-postgresql/src/services → services}/realtimeService.d.ts +0 -0
- /package/dist/{server-postgresql/src/utils → utils}/drizzle-conditions.d.ts +0 -0
|
@@ -1,14 +1,10 @@
|
|
|
1
|
-
// import { NodePgDatabase } from "drizzle-orm/node-postgres";
|
|
2
1
|
import { EntityService } from "./services/entityService";
|
|
3
2
|
import { BranchService } from "./services/BranchService";
|
|
4
3
|
import { RealtimeService } from "./services/realtimeService";
|
|
5
4
|
import { DatabasePoolManager } from "./databasePoolManager";
|
|
6
5
|
import { DrizzleClient } from "./interfaces";
|
|
7
|
-
import { User, RebaseClient } from "@rebasepro/types";
|
|
8
|
-
import { sql as drizzleSql } from "drizzle-orm";
|
|
9
|
-
import { buildPropertyCallbacks, updateDateAutoValues } from "@rebasepro/common";
|
|
10
|
-
import { PostgresCollectionRegistry } from "./collections/PostgresCollectionRegistry";
|
|
11
6
|
import {
|
|
7
|
+
DatabaseAdmin,
|
|
12
8
|
DataDriver,
|
|
13
9
|
DeleteEntityProps,
|
|
14
10
|
Entity,
|
|
@@ -18,22 +14,23 @@ import {
|
|
|
18
14
|
ListenCollectionProps,
|
|
19
15
|
ListenEntityProps,
|
|
20
16
|
RebaseCallContext,
|
|
21
|
-
|
|
17
|
+
RebaseClient,
|
|
22
18
|
RebaseData,
|
|
23
|
-
|
|
19
|
+
RestFetchService,
|
|
20
|
+
SaveEntityProps,
|
|
24
21
|
TableColumnInfo,
|
|
25
22
|
TableForeignKeyInfo,
|
|
26
23
|
TableJunctionInfo,
|
|
24
|
+
TableMetadata,
|
|
27
25
|
TablePolicyInfo,
|
|
28
|
-
|
|
29
|
-
SchemaAdmin,
|
|
30
|
-
DatabaseAdmin,
|
|
31
|
-
RestFetchService
|
|
26
|
+
User
|
|
32
27
|
} from "@rebasepro/types";
|
|
33
|
-
import {
|
|
34
|
-
|
|
28
|
+
import { sql as drizzleSql } from "drizzle-orm";
|
|
29
|
+
import { buildPropertyCallbacks, buildRebaseData, updateDateAutoValues } from "@rebasepro/common";
|
|
30
|
+
import { PostgresCollectionRegistry } from "./collections/PostgresCollectionRegistry";
|
|
35
31
|
import { HistoryService } from "./history/HistoryService";
|
|
36
32
|
import { mergeDeep } from "@rebasepro/utils";
|
|
33
|
+
import { logger } from "@rebasepro/server-core";
|
|
37
34
|
|
|
38
35
|
export class PostgresBackendDriver implements DataDriver {
|
|
39
36
|
key = "postgres";
|
|
@@ -112,15 +109,28 @@ export class PostgresBackendDriver implements DataDriver {
|
|
|
112
109
|
return this.entityService.getFetchService();
|
|
113
110
|
}
|
|
114
111
|
|
|
112
|
+
private buildCallContext(): RebaseCallContext {
|
|
113
|
+
return {
|
|
114
|
+
user: this.user,
|
|
115
|
+
driver: this,
|
|
116
|
+
data: this.data,
|
|
117
|
+
client: this.client,
|
|
118
|
+
storageSource: this.client?.storage
|
|
119
|
+
} as unknown as RebaseCallContext;
|
|
120
|
+
}
|
|
115
121
|
|
|
116
122
|
private resolveCollectionCallbacks<M extends Record<string, unknown>>(collection: EntityCollection<M> | undefined, path: string) {
|
|
117
|
-
if (!collection && !path) return {
|
|
118
|
-
|
|
119
|
-
|
|
123
|
+
if (!collection && !path) return {
|
|
124
|
+
collection: undefined,
|
|
125
|
+
callbacks: undefined,
|
|
126
|
+
propertyCallbacks: undefined
|
|
127
|
+
};
|
|
120
128
|
const registryCollection = this.registry?.getCollectionByPath(path);
|
|
121
129
|
const resolvedCollection = registryCollection
|
|
122
|
-
? {
|
|
123
|
-
...
|
|
130
|
+
? {
|
|
131
|
+
...collection,
|
|
132
|
+
...registryCollection
|
|
133
|
+
} as EntityCollection<M>
|
|
124
134
|
: collection as EntityCollection<M>;
|
|
125
135
|
|
|
126
136
|
const callbacks = resolvedCollection?.callbacks;
|
|
@@ -137,17 +147,17 @@ propertyCallbacks: undefined };
|
|
|
137
147
|
}
|
|
138
148
|
|
|
139
149
|
async fetchCollection<M extends Record<string, unknown>>({
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
150
|
+
path,
|
|
151
|
+
collection,
|
|
152
|
+
filter,
|
|
153
|
+
limit,
|
|
154
|
+
offset,
|
|
155
|
+
startAfter,
|
|
156
|
+
orderBy,
|
|
157
|
+
searchString,
|
|
158
|
+
order,
|
|
159
|
+
vectorSearch
|
|
160
|
+
}: FetchCollectionProps<M>): Promise<Entity<M>[]> {
|
|
151
161
|
|
|
152
162
|
const entities = await this.entityService.fetchCollection<M>(path, {
|
|
153
163
|
filter,
|
|
@@ -161,16 +171,14 @@ propertyCallbacks: undefined };
|
|
|
161
171
|
vectorSearch
|
|
162
172
|
});
|
|
163
173
|
|
|
164
|
-
const {
|
|
174
|
+
const {
|
|
175
|
+
collection: resolvedCollection,
|
|
176
|
+
callbacks,
|
|
177
|
+
propertyCallbacks
|
|
178
|
+
} = this.resolveCollectionCallbacks(collection, path);
|
|
165
179
|
|
|
166
180
|
if (callbacks?.afterRead || propertyCallbacks?.afterRead) {
|
|
167
|
-
const contextForCallback =
|
|
168
|
-
user: this.user,
|
|
169
|
-
driver: this,
|
|
170
|
-
data: this.data,
|
|
171
|
-
client: this.client,
|
|
172
|
-
storageSource: this.client?.storage
|
|
173
|
-
} as unknown as RebaseCallContext; // Backend context
|
|
181
|
+
const contextForCallback = this.buildCallContext();
|
|
174
182
|
return Promise.all(entities.map(async (entity) => {
|
|
175
183
|
let fetched = entity;
|
|
176
184
|
if (callbacks?.afterRead) {
|
|
@@ -197,18 +205,18 @@ propertyCallbacks: undefined };
|
|
|
197
205
|
}
|
|
198
206
|
|
|
199
207
|
listenCollection<M extends Record<string, unknown>>({
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
208
|
+
path,
|
|
209
|
+
collection,
|
|
210
|
+
filter,
|
|
211
|
+
limit,
|
|
212
|
+
offset,
|
|
213
|
+
startAfter,
|
|
214
|
+
orderBy,
|
|
215
|
+
searchString,
|
|
216
|
+
order,
|
|
217
|
+
onUpdate,
|
|
218
|
+
onError
|
|
219
|
+
}: ListenCollectionProps<M>): () => void {
|
|
212
220
|
|
|
213
221
|
const subscriptionId = this.generateSubscriptionId();
|
|
214
222
|
|
|
@@ -261,27 +269,25 @@ propertyCallbacks: undefined };
|
|
|
261
269
|
}
|
|
262
270
|
|
|
263
271
|
async fetchEntity<M extends Record<string, unknown>>({
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
272
|
+
path,
|
|
273
|
+
entityId,
|
|
274
|
+
databaseId,
|
|
275
|
+
collection
|
|
276
|
+
}: FetchEntityProps<M>): Promise<Entity<M> | undefined> {
|
|
269
277
|
let entity = await this.entityService.fetchEntity<M>(
|
|
270
278
|
path,
|
|
271
279
|
entityId,
|
|
272
280
|
databaseId || collection?.databaseId
|
|
273
281
|
);
|
|
274
282
|
|
|
275
|
-
const {
|
|
283
|
+
const {
|
|
284
|
+
collection: resolvedCollection,
|
|
285
|
+
callbacks,
|
|
286
|
+
propertyCallbacks
|
|
287
|
+
} = this.resolveCollectionCallbacks(collection, path);
|
|
276
288
|
|
|
277
289
|
if (entity && (callbacks?.afterRead || propertyCallbacks?.afterRead)) {
|
|
278
|
-
const contextForCallback =
|
|
279
|
-
user: this.user,
|
|
280
|
-
driver: this,
|
|
281
|
-
data: this.data,
|
|
282
|
-
client: this.client,
|
|
283
|
-
storageSource: this.client?.storage
|
|
284
|
-
} as unknown as RebaseCallContext; // Backend context
|
|
290
|
+
const contextForCallback = this.buildCallContext();
|
|
285
291
|
if (callbacks?.afterRead) {
|
|
286
292
|
entity = await callbacks.afterRead({
|
|
287
293
|
collection: resolvedCollection as EntityCollection<M>,
|
|
@@ -304,12 +310,12 @@ propertyCallbacks: undefined };
|
|
|
304
310
|
}
|
|
305
311
|
|
|
306
312
|
listenEntity<M extends Record<string, unknown>>({
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
+
path,
|
|
314
|
+
entityId,
|
|
315
|
+
collection,
|
|
316
|
+
onUpdate,
|
|
317
|
+
onError
|
|
318
|
+
}: ListenEntityProps<M>): () => void {
|
|
313
319
|
|
|
314
320
|
const subscriptionId = this.generateSubscriptionId();
|
|
315
321
|
const callbackWrapper = (entity: Entity<M> | null) => {
|
|
@@ -349,23 +355,21 @@ propertyCallbacks: undefined };
|
|
|
349
355
|
}
|
|
350
356
|
|
|
351
357
|
async saveEntity<M extends Record<string, unknown>>({
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
const {
|
|
358
|
+
path,
|
|
359
|
+
entityId,
|
|
360
|
+
values,
|
|
361
|
+
collection,
|
|
362
|
+
status
|
|
363
|
+
}: SaveEntityProps<M>): Promise<Entity<M>> {
|
|
364
|
+
|
|
365
|
+
const {
|
|
366
|
+
collection: resolvedCollection,
|
|
367
|
+
callbacks,
|
|
368
|
+
propertyCallbacks
|
|
369
|
+
} = this.resolveCollectionCallbacks(collection, path);
|
|
360
370
|
|
|
361
371
|
let updatedValues = values;
|
|
362
|
-
const contextForCallback =
|
|
363
|
-
user: this.user,
|
|
364
|
-
driver: this,
|
|
365
|
-
data: this.data,
|
|
366
|
-
client: this.client,
|
|
367
|
-
storageSource: this.client?.storage
|
|
368
|
-
} as unknown as RebaseCallContext;
|
|
372
|
+
const contextForCallback = this.buildCallContext();
|
|
369
373
|
|
|
370
374
|
// Fetch previous values for callbacks AND history recording
|
|
371
375
|
let previousValuesForHistory: Partial<Entity<M>["values"]> | undefined;
|
|
@@ -528,20 +532,18 @@ propertyCallbacks: undefined };
|
|
|
528
532
|
}
|
|
529
533
|
|
|
530
534
|
async deleteEntity<M extends Record<string, unknown>>({
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
535
|
+
entity,
|
|
536
|
+
collection
|
|
537
|
+
}: DeleteEntityProps<M>): Promise<void> {
|
|
534
538
|
|
|
535
539
|
// Resolve from backend registry to restore callbacks lost during WebSocket serialization
|
|
536
|
-
const {
|
|
540
|
+
const {
|
|
541
|
+
collection: resolvedCollection,
|
|
542
|
+
callbacks,
|
|
543
|
+
propertyCallbacks
|
|
544
|
+
} = this.resolveCollectionCallbacks(collection, entity.path);
|
|
537
545
|
|
|
538
|
-
const contextForCallback =
|
|
539
|
-
user: this.user,
|
|
540
|
-
driver: this,
|
|
541
|
-
data: this.data,
|
|
542
|
-
client: this.client,
|
|
543
|
-
storageSource: this.client?.storage
|
|
544
|
-
} as unknown as RebaseCallContext;
|
|
546
|
+
const contextForCallback = this.buildCallContext();
|
|
545
547
|
|
|
546
548
|
if (callbacks?.beforeDelete || propertyCallbacks?.beforeDelete) {
|
|
547
549
|
let preventDefault = false;
|
|
@@ -653,17 +655,18 @@ propertyCallbacks: undefined };
|
|
|
653
655
|
);
|
|
654
656
|
}
|
|
655
657
|
|
|
656
|
-
|
|
657
658
|
async countEntities<M extends Record<string, unknown>>({
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
659
|
+
path,
|
|
660
|
+
collection,
|
|
661
|
+
filter,
|
|
662
|
+
searchString
|
|
663
|
+
}: FetchCollectionProps<M>): Promise<number> {
|
|
663
664
|
return this.entityService.countEntities(
|
|
664
665
|
path,
|
|
665
|
-
{
|
|
666
|
-
|
|
666
|
+
{
|
|
667
|
+
filter,
|
|
668
|
+
searchString
|
|
669
|
+
}
|
|
667
670
|
);
|
|
668
671
|
}
|
|
669
672
|
|
|
@@ -679,7 +682,10 @@ searchString }
|
|
|
679
682
|
return this.poolManager.getDrizzle(databaseName);
|
|
680
683
|
}
|
|
681
684
|
|
|
682
|
-
async executeSql(sqlText: string, options?: {
|
|
685
|
+
async executeSql(sqlText: string, options?: {
|
|
686
|
+
database?: string,
|
|
687
|
+
role?: string
|
|
688
|
+
}): Promise<Record<string, unknown>[]> {
|
|
683
689
|
if (!options?.database && !options?.role) {
|
|
684
690
|
return this.entityService.executeSql(sqlText);
|
|
685
691
|
}
|
|
@@ -704,7 +710,7 @@ searchString }
|
|
|
704
710
|
}
|
|
705
711
|
|
|
706
712
|
if (needsRoleSwitch && options?.role) {
|
|
707
|
-
const safeRole = options.role.replace(/"/g,
|
|
713
|
+
const safeRole = options.role.replace(/"/g, "\"\"");
|
|
708
714
|
return await targetDb.transaction(async (tx) => {
|
|
709
715
|
await tx.execute(drizzleSql.raw(`SET LOCAL ROLE "${safeRole}"`));
|
|
710
716
|
const result = await tx.execute(drizzleSql.raw(sqlText));
|
|
@@ -817,7 +823,7 @@ searchString }
|
|
|
817
823
|
`);
|
|
818
824
|
junctionTables = new Set(junctionResult.map((r: Record<string, unknown>) => r.table_name as string));
|
|
819
825
|
} catch (e) {
|
|
820
|
-
|
|
826
|
+
logger.warn("Could not detect junction tables", { error: e });
|
|
821
827
|
}
|
|
822
828
|
|
|
823
829
|
const filteredTables = allTables.filter(name => !junctionTables.has(name));
|
|
@@ -828,7 +834,6 @@ searchString }
|
|
|
828
834
|
return filteredTables.filter((name: string) => !mappedSet.has(name.toLowerCase()));
|
|
829
835
|
}
|
|
830
836
|
|
|
831
|
-
|
|
832
837
|
/**
|
|
833
838
|
* Fetch metadata for a given table from information_schema (columns, policies, constraints).
|
|
834
839
|
*/
|
|
@@ -972,31 +977,35 @@ export class AuthenticatedPostgresBackendDriver implements DataDriver {
|
|
|
972
977
|
fetchCollectionForRest: async (collectionPath, options, include) => {
|
|
973
978
|
return this.withTransaction(async (delegate) => {
|
|
974
979
|
return delegate.restFetchService.fetchCollectionForRest(collectionPath, options, include);
|
|
975
|
-
});
|
|
980
|
+
}, { accessMode: "read only" });
|
|
976
981
|
},
|
|
977
982
|
fetchEntityForRest: async (collectionPath, entityId, include, databaseId) => {
|
|
978
983
|
return this.withTransaction(async (delegate) => {
|
|
979
984
|
return delegate.restFetchService.fetchEntityForRest(collectionPath, entityId, include, databaseId);
|
|
980
|
-
});
|
|
985
|
+
}, { accessMode: "read only" });
|
|
981
986
|
}
|
|
982
987
|
};
|
|
983
988
|
}
|
|
984
989
|
|
|
985
990
|
private async withTransaction<T>(
|
|
986
|
-
operation: (delegate: PostgresBackendDriver) => Promise<T
|
|
991
|
+
operation: (delegate: PostgresBackendDriver) => Promise<T>,
|
|
992
|
+
options?: {
|
|
993
|
+
accessMode?: "read only" | "read write";
|
|
994
|
+
isolationLevel?: "read uncommitted" | "read committed" | "repeatable read" | "serializable"
|
|
995
|
+
}
|
|
987
996
|
): Promise<T> {
|
|
988
997
|
const pendingNotifications: PostgresBackendDriver["_pendingNotifications"] = [];
|
|
989
998
|
|
|
990
999
|
const result = await this.delegate.db.transaction(async (tx) => {
|
|
991
1000
|
let userId = this.user?.uid;
|
|
992
1001
|
if (!userId) {
|
|
993
|
-
|
|
1002
|
+
logger.warn("[DataDriver] User ID (uid) is missing for authenticated delegate. Using 'anonymous'. User object", { detail: this.user });
|
|
994
1003
|
userId = "anonymous";
|
|
995
1004
|
}
|
|
996
1005
|
|
|
997
1006
|
const userRoles = this.user?.roles ?? [];
|
|
998
1007
|
if (!this.user?.roles) {
|
|
999
|
-
|
|
1008
|
+
logger.warn("[DataDriver] User roles are missing for authenticated delegate. Using empty array. User object", { detail: this.user });
|
|
1000
1009
|
}
|
|
1001
1010
|
const normalizedRoles = userRoles.map((r: unknown) =>
|
|
1002
1011
|
typeof r === "string" ? r : (r as Record<string, unknown>)?.id ?? String(r)
|
|
@@ -1007,8 +1016,10 @@ export class AuthenticatedPostgresBackendDriver implements DataDriver {
|
|
|
1007
1016
|
SELECT
|
|
1008
1017
|
set_config('app.user_id', ${userId}, true),
|
|
1009
1018
|
set_config('app.user_roles', ${rolesString}, true),
|
|
1010
|
-
set_config('app.jwt', ${JSON.stringify({
|
|
1011
|
-
|
|
1019
|
+
set_config('app.jwt', ${JSON.stringify({
|
|
1020
|
+
sub: userId,
|
|
1021
|
+
roles: userRoles
|
|
1022
|
+
})}, true)
|
|
1012
1023
|
`);
|
|
1013
1024
|
|
|
1014
1025
|
const txEntityService = new EntityService(tx, this.delegate.registry);
|
|
@@ -1020,7 +1031,7 @@ roles: userRoles })}, true)
|
|
|
1020
1031
|
txDelegate.client = this.delegate.client;
|
|
1021
1032
|
|
|
1022
1033
|
return await operation(txDelegate);
|
|
1023
|
-
});
|
|
1034
|
+
}, options);
|
|
1024
1035
|
|
|
1025
1036
|
for (const notification of pendingNotifications) {
|
|
1026
1037
|
try {
|
|
@@ -1031,7 +1042,7 @@ roles: userRoles })}, true)
|
|
|
1031
1042
|
notification.databaseId
|
|
1032
1043
|
);
|
|
1033
1044
|
} catch (e) {
|
|
1034
|
-
|
|
1045
|
+
logger.error("[DataDriver] Error flushing deferred notification", { error: e });
|
|
1035
1046
|
}
|
|
1036
1047
|
}
|
|
1037
1048
|
|
|
@@ -1039,7 +1050,7 @@ roles: userRoles })}, true)
|
|
|
1039
1050
|
}
|
|
1040
1051
|
|
|
1041
1052
|
async fetchCollection<M extends Record<string, unknown>>(props: FetchCollectionProps<M>): Promise<Entity<M>[]> {
|
|
1042
|
-
return this.withTransaction((delegate) => delegate.fetchCollection(props));
|
|
1053
|
+
return this.withTransaction((delegate) => delegate.fetchCollection(props), { accessMode: "read only" });
|
|
1043
1054
|
}
|
|
1044
1055
|
|
|
1045
1056
|
/**
|
|
@@ -1047,8 +1058,10 @@ roles: userRoles })}, true)
|
|
|
1047
1058
|
* registered realtime subscription so RLS-aware polling can apply.
|
|
1048
1059
|
*/
|
|
1049
1060
|
private injectAuthContext(unsubscribe: () => void): () => void {
|
|
1050
|
-
const authContext = {
|
|
1051
|
-
|
|
1061
|
+
const authContext = {
|
|
1062
|
+
userId: this.user?.uid || "anonymous",
|
|
1063
|
+
roles: this.user?.roles ?? []
|
|
1064
|
+
};
|
|
1052
1065
|
const entries = Array.from(this.delegate.realtimeService.subscriptions.entries());
|
|
1053
1066
|
const lastEntry = entries[entries.length - 1];
|
|
1054
1067
|
const lastSub = lastEntry?.[1] as Record<string, unknown> | undefined;
|
|
@@ -1063,7 +1076,7 @@ roles: this.user?.roles ?? [] };
|
|
|
1063
1076
|
}
|
|
1064
1077
|
|
|
1065
1078
|
async fetchEntity<M extends Record<string, unknown>>(props: FetchEntityProps<M>): Promise<Entity<M> | undefined> {
|
|
1066
|
-
return this.withTransaction((delegate) => delegate.fetchEntity(props));
|
|
1079
|
+
return this.withTransaction((delegate) => delegate.fetchEntity(props), { accessMode: "read only" });
|
|
1067
1080
|
}
|
|
1068
1081
|
|
|
1069
1082
|
listenEntity<M extends Record<string, unknown>>(props: ListenEntityProps<M>): () => void {
|
|
@@ -1079,7 +1092,7 @@ roles: this.user?.roles ?? [] };
|
|
|
1079
1092
|
}
|
|
1080
1093
|
|
|
1081
1094
|
async deleteAll(path: string): Promise<void> {
|
|
1082
|
-
return this.delegate.deleteAll(path);
|
|
1095
|
+
return this.withTransaction((delegate) => delegate.deleteAll(path));
|
|
1083
1096
|
}
|
|
1084
1097
|
|
|
1085
1098
|
async checkUniqueField(
|
|
@@ -1089,11 +1102,11 @@ roles: this.user?.roles ?? [] };
|
|
|
1089
1102
|
entityId?: string,
|
|
1090
1103
|
collection?: EntityCollection
|
|
1091
1104
|
): Promise<boolean> {
|
|
1092
|
-
return this.withTransaction((delegate) => delegate.checkUniqueField(path, name, value, entityId, collection));
|
|
1105
|
+
return this.withTransaction((delegate) => delegate.checkUniqueField(path, name, value, entityId, collection), { accessMode: "read only" });
|
|
1093
1106
|
}
|
|
1094
1107
|
|
|
1095
1108
|
async countEntities<M extends Record<string, unknown>>(props: FetchCollectionProps<M>): Promise<number> {
|
|
1096
|
-
return this.withTransaction((delegate) => delegate.countEntities(props));
|
|
1109
|
+
return this.withTransaction((delegate) => delegate.countEntities(props), { accessMode: "read only" });
|
|
1097
1110
|
}
|
|
1098
1111
|
|
|
1099
1112
|
}
|
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
|
|
7
7
|
import { getTableName, isTable, Relations, sql, Table } from "drizzle-orm";
|
|
8
8
|
import { NodePgDatabase } from "drizzle-orm/node-postgres";
|
|
9
|
-
import { PgEnum, PgTable, getTableConfig
|
|
9
|
+
import { PgEnum, PgTable, getTableConfig } from "drizzle-orm/pg-core";
|
|
10
|
+
import type { RebasePgTable } from "./types";
|
|
10
11
|
import {
|
|
11
12
|
BackendBootstrapper,
|
|
12
13
|
InitializedDriver,
|
|
@@ -15,7 +16,8 @@ import {
|
|
|
15
16
|
RealtimeProvider,
|
|
16
17
|
type DataDriver,
|
|
17
18
|
type AuthAdapter,
|
|
18
|
-
EntityCollection
|
|
19
|
+
EntityCollection,
|
|
20
|
+
PostgresCollection
|
|
19
21
|
} from "@rebasepro/types";
|
|
20
22
|
import { PostgresBackendDriver } from "./PostgresBackendDriver";
|
|
21
23
|
import { RealtimeService } from "./services/realtimeService";
|
|
@@ -23,28 +25,33 @@ import { DatabasePoolManager } from "./databasePoolManager";
|
|
|
23
25
|
import { PostgresCollectionRegistry } from "./collections/PostgresCollectionRegistry";
|
|
24
26
|
import {
|
|
25
27
|
createAuthRoutes,
|
|
26
|
-
createAdminRoutes,
|
|
27
28
|
requireAuth,
|
|
28
29
|
requireAdmin,
|
|
29
30
|
logger
|
|
30
|
-
// @ts-ignore
|
|
31
31
|
} from "@rebasepro/server-core";
|
|
32
32
|
import { ensureAuthTablesExist } from "./auth/ensure-tables";
|
|
33
33
|
import { UserService, PostgresAuthRepository, AuthSchemaTables } from "./auth/services";
|
|
34
34
|
import { createAuthSchema } from "./schema/auth-schema";
|
|
35
35
|
|
|
36
|
-
// @ts-ignore
|
|
37
36
|
import { createEmailService, type EmailConfig, type EmailService } from "@rebasepro/server-core";
|
|
38
|
-
// @ts-ignore
|
|
39
37
|
import { createHistoryRoutes } from "@rebasepro/server-core";
|
|
40
38
|
import { HistoryService } from "./history/HistoryService";
|
|
41
39
|
import { ensureHistoryTableExists } from "./history/ensure-history-table";
|
|
42
|
-
// @ts-ignore
|
|
43
|
-
import type { AuthConfig, PostgresDriverConfig, HistoryConfig } from "@rebasepro/server-core";
|
|
44
40
|
import type { Hono } from "hono";
|
|
45
|
-
// @ts-ignore
|
|
46
41
|
import type { HonoEnv } from "@rebasepro/server-core";
|
|
47
42
|
|
|
43
|
+
export interface PostgresDriverConfig {
|
|
44
|
+
connectionString?: string;
|
|
45
|
+
adminConnectionString?: string;
|
|
46
|
+
readConnectionString?: string;
|
|
47
|
+
connection?: unknown;
|
|
48
|
+
schema?: {
|
|
49
|
+
tables?: Record<string, unknown>;
|
|
50
|
+
enums?: Record<string, unknown>;
|
|
51
|
+
relations?: Record<string, unknown>;
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
48
55
|
/**
|
|
49
56
|
* Opaque internals bag that PostgresBootstrapper stores during `initializeDriver()`
|
|
50
57
|
* and re-uses in subsequent lifecycle hooks.
|
|
@@ -107,9 +114,10 @@ export function createPostgresBootstrapper(pgConfig: PostgresDriverConfig): Back
|
|
|
107
114
|
...(pgConfig.schema?.relations || {})
|
|
108
115
|
};
|
|
109
116
|
const { drizzle: createDrizzle } = await import("drizzle-orm/node-postgres");
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
|
|
117
|
+
const connection = pgConfig.connection;
|
|
118
|
+
const rawClient = (connection && typeof connection === "object" && "$client" in connection
|
|
119
|
+
? (connection as Record<string, unknown>).$client
|
|
120
|
+
: connection) as import("pg").Pool;
|
|
113
121
|
const schemaAwareDb = createDrizzle(rawClient, { schema: mergedSchema });
|
|
114
122
|
|
|
115
123
|
// Verify connection
|
|
@@ -162,6 +170,72 @@ export function createPostgresBootstrapper(pgConfig: PostgresDriverConfig): Back
|
|
|
162
170
|
}
|
|
163
171
|
}
|
|
164
172
|
|
|
173
|
+
// ── Startup Schema Validation ────────────────────────────────────
|
|
174
|
+
// One-directional: only checks collections → DB (extra DB tables
|
|
175
|
+
// that aren't mapped to collections are perfectly fine).
|
|
176
|
+
try {
|
|
177
|
+
const registeredCollections = registry.getCollections();
|
|
178
|
+
if (registeredCollections.length > 0) {
|
|
179
|
+
const schemasToCheck = Array.from(new Set(
|
|
180
|
+
registeredCollections.map(c => "schema" in c && c.schema ? c.schema : "public")
|
|
181
|
+
));
|
|
182
|
+
const schemasList = schemasToCheck.map(s => `'${s}'`).join(",");
|
|
183
|
+
const result = await schemaAwareDb.execute(sql.raw(`
|
|
184
|
+
SELECT table_name, table_schema
|
|
185
|
+
FROM information_schema.tables
|
|
186
|
+
WHERE table_schema IN (${schemasList})
|
|
187
|
+
AND table_type = 'BASE TABLE'
|
|
188
|
+
`));
|
|
189
|
+
const dbTables = new Set(
|
|
190
|
+
(result.rows as Array<{ table_name: string; table_schema: string }>).map(r =>
|
|
191
|
+
r.table_schema === "public" ? r.table_name : `${r.table_schema}.${r.table_name}`
|
|
192
|
+
)
|
|
193
|
+
);
|
|
194
|
+
const missing: Array<{ slug: string; table: string }> = [];
|
|
195
|
+
for (const col of registeredCollections) {
|
|
196
|
+
const schemaName = "schema" in col && col.schema ? col.schema : "public";
|
|
197
|
+
const tableName = registry.hasTableForCollection(
|
|
198
|
+
col.table ?? col.slug
|
|
199
|
+
)
|
|
200
|
+
? (col.table ?? col.slug)
|
|
201
|
+
: col.slug;
|
|
202
|
+
// Resolve the actual table name the registry stored
|
|
203
|
+
const resolvedTable = registry.getTableNames().find((k) =>
|
|
204
|
+
k === tableName ||
|
|
205
|
+
k === col.slug
|
|
206
|
+
);
|
|
207
|
+
const checkName = resolvedTable ?? tableName;
|
|
208
|
+
const fullCheckName = schemaName === "public" ? checkName : `${schemaName}.${checkName}`;
|
|
209
|
+
if (!dbTables.has(fullCheckName)) {
|
|
210
|
+
missing.push({ slug: col.slug,
|
|
211
|
+
table: checkName });
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
if (missing.length > 0) {
|
|
215
|
+
const lines = missing.map(
|
|
216
|
+
m => ` • collection "${m.slug}" → table "${m.table}"`
|
|
217
|
+
);
|
|
218
|
+
logger.warn([
|
|
219
|
+
"",
|
|
220
|
+
"┌──────────────────────────────────────────────────────────────┐",
|
|
221
|
+
"│ ⚠️ SCHEMA DRIFT — Missing tables in database │",
|
|
222
|
+
"├──────────────────────────────────────────────────────────────┤",
|
|
223
|
+
...lines.map(l => `│ ${l.padEnd(60)}│`),
|
|
224
|
+
"├──────────────────────────────────────────────────────────────┤",
|
|
225
|
+
"│ Run one of: │",
|
|
226
|
+
"│ pnpm db:push (dev — fast, no migration files) │",
|
|
227
|
+
"│ pnpm db:migrate (prod — creates migration files) │",
|
|
228
|
+
"└──────────────────────────────────────────────────────────────┘",
|
|
229
|
+
""
|
|
230
|
+
].join("\n"));
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
} catch (err) {
|
|
234
|
+
logger.warn("⚠️ Startup schema validation could not run", {
|
|
235
|
+
error: err instanceof Error ? err.message : String(err)
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
|
|
165
239
|
const internals: PostgresDriverInternals = {
|
|
166
240
|
db: schemaAwareDb,
|
|
167
241
|
readDb,
|
|
@@ -207,7 +281,7 @@ export function createPostgresBootstrapper(pgConfig: PostgresDriverConfig): Back
|
|
|
207
281
|
: authCollection.slug)
|
|
208
282
|
: undefined;
|
|
209
283
|
const usersTable = tableName
|
|
210
|
-
? registry.getTable(tableName) as
|
|
284
|
+
? registry.getTable(tableName) as RebasePgTable | undefined
|
|
211
285
|
: undefined;
|
|
212
286
|
|
|
213
287
|
let usersSchemaName = "rebase";
|
|
@@ -217,7 +291,7 @@ export function createPostgresBootstrapper(pgConfig: PostgresDriverConfig): Back
|
|
|
217
291
|
|
|
218
292
|
const authTables = createAuthSchema(usersSchemaName) as unknown as AuthSchemaTables;
|
|
219
293
|
if (usersTable) {
|
|
220
|
-
authTables.users = usersTable as
|
|
294
|
+
authTables.users = usersTable as RebasePgTable;
|
|
221
295
|
}
|
|
222
296
|
|
|
223
297
|
const userService = new UserService(db, authTables);
|
|
@@ -230,7 +304,7 @@ authRepository };
|
|
|
230
304
|
},
|
|
231
305
|
|
|
232
306
|
async initializeHistory(config: unknown, driverResult: InitializedDriver): Promise<{ historyService: HistoryService } | undefined> {
|
|
233
|
-
const historyConfig = config as
|
|
307
|
+
const historyConfig = config as { retention?: number } | boolean | undefined;
|
|
234
308
|
if (!historyConfig) return undefined;
|
|
235
309
|
|
|
236
310
|
const internals = driverResult.internals as PostgresDriverInternals;
|
|
@@ -266,7 +340,7 @@ authRepository };
|
|
|
266
340
|
server as import("http").Server,
|
|
267
341
|
realtimeService as RealtimeService,
|
|
268
342
|
driver as PostgresBackendDriver,
|
|
269
|
-
config as
|
|
343
|
+
config as { requireAuth?: boolean },
|
|
270
344
|
adapter as AuthAdapter | undefined
|
|
271
345
|
);
|
|
272
346
|
}
|