@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
package/src/data-transformer.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { getTableName, resolveCollectionRelations, findRelation, createRelationR
|
|
|
6
6
|
import { PostgresCollectionRegistry } from "./collections/PostgresCollectionRegistry";
|
|
7
7
|
import { DrizzleConditionBuilder } from "./utils/drizzle-conditions";
|
|
8
8
|
import { getPrimaryKeys, buildCompositeId } from "./services/entity-helpers";
|
|
9
|
+
import { logger } from "@rebasepro/server-core";
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* Data transformation utilities for converting between frontend and database formats.
|
|
@@ -90,7 +91,9 @@ export function serializeDataToServer<M extends Record<string, unknown>>(
|
|
|
90
91
|
collection?: EntityCollection,
|
|
91
92
|
registry?: PostgresCollectionRegistry
|
|
92
93
|
): SerializedEntityData {
|
|
93
|
-
if (!entity || !properties) return { scalarData: entity ?? {},
|
|
94
|
+
if (!entity || !properties) return { scalarData: entity ?? {},
|
|
95
|
+
inverseRelationUpdates: [],
|
|
96
|
+
joinPathRelationUpdates: [] };
|
|
94
97
|
|
|
95
98
|
const result: Record<string, unknown> = {};
|
|
96
99
|
|
|
@@ -128,7 +131,6 @@ export function serializeDataToServer<M extends Record<string, unknown>>(
|
|
|
128
131
|
}
|
|
129
132
|
|
|
130
133
|
|
|
131
|
-
|
|
132
134
|
// Handle relation properties specially
|
|
133
135
|
if (property.type === "relation" && collection) {
|
|
134
136
|
const relation = findRelation(resolvedRelations, key);
|
|
@@ -272,41 +274,19 @@ export function serializePropertyToServer(value: unknown, property: Property): u
|
|
|
272
274
|
return value;
|
|
273
275
|
}
|
|
274
276
|
|
|
275
|
-
case "binary":
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
if (base64Data) {
|
|
280
|
-
return Buffer.from(base64Data, "base64");
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
if (Buffer.isBuffer(value)) {
|
|
285
|
-
return value;
|
|
286
|
-
}
|
|
277
|
+
case "binary": {
|
|
278
|
+
const decoded = tryDecodeBase64DataUrl(value);
|
|
279
|
+
if (decoded) return decoded;
|
|
280
|
+
if (Buffer.isBuffer(value)) return value;
|
|
287
281
|
return value;
|
|
282
|
+
}
|
|
288
283
|
|
|
289
284
|
case "string":
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
if (base64Data) {
|
|
294
|
-
return Buffer.from(base64Data, "base64");
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
return value;
|
|
299
|
-
|
|
300
|
-
default:
|
|
301
|
-
if (typeof value === "string") {
|
|
302
|
-
if (value.startsWith("data:application/octet-stream;base64,")) {
|
|
303
|
-
const base64Data = value.split(",")[1];
|
|
304
|
-
if (base64Data) {
|
|
305
|
-
return Buffer.from(base64Data, "base64");
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
}
|
|
285
|
+
default: {
|
|
286
|
+
const decoded = tryDecodeBase64DataUrl(value);
|
|
287
|
+
if (decoded) return decoded;
|
|
309
288
|
return value;
|
|
289
|
+
}
|
|
310
290
|
}
|
|
311
291
|
}
|
|
312
292
|
|
|
@@ -342,7 +322,7 @@ export async function parseDataFromServer<M extends Record<string, unknown>>(
|
|
|
342
322
|
const targetCollection = relation.target();
|
|
343
323
|
result[propKey] = createRelationRef(fkValue.toString(), targetCollection.slug);
|
|
344
324
|
} catch (e) {
|
|
345
|
-
|
|
325
|
+
logger.warn(`Could not resolve target collection for relation property: ${propKey}`, { error: e });
|
|
346
326
|
}
|
|
347
327
|
}
|
|
348
328
|
} else if (relation.direction === "inverse" && relation.foreignKeyOnTarget && db && registry) {
|
|
@@ -372,7 +352,7 @@ export async function parseDataFromServer<M extends Record<string, unknown>>(
|
|
|
372
352
|
} else {
|
|
373
353
|
// One-to-many: return array of relation objects
|
|
374
354
|
const targetPks = getPrimaryKeys(targetCollection, registry!);
|
|
375
|
-
result[propKey] = relatedEntities.map((entity: Record<string, unknown>) =>
|
|
355
|
+
result[propKey] = relatedEntities.map((entity: Record<string, unknown>) =>
|
|
376
356
|
createRelationRef(buildCompositeId(entity, targetPks), targetCollection.slug)
|
|
377
357
|
);
|
|
378
358
|
}
|
|
@@ -380,7 +360,7 @@ export async function parseDataFromServer<M extends Record<string, unknown>>(
|
|
|
380
360
|
}
|
|
381
361
|
}
|
|
382
362
|
} catch (e) {
|
|
383
|
-
|
|
363
|
+
logger.warn(`Could not resolve inverse relation property: ${propKey}`, { error: e });
|
|
384
364
|
}
|
|
385
365
|
} else if (relation.direction === "inverse" && relation.joinPath && db && registry) {
|
|
386
366
|
// Join path relation: Multi-hop relation using joins
|
|
@@ -393,7 +373,7 @@ export async function parseDataFromServer<M extends Record<string, unknown>>(
|
|
|
393
373
|
// Build the join query following the join path
|
|
394
374
|
const sourceTable = registry.getTable(getTableName(collection));
|
|
395
375
|
if (!sourceTable) {
|
|
396
|
-
|
|
376
|
+
logger.warn(`Source table not found for collection: ${collection.slug}`);
|
|
397
377
|
continue;
|
|
398
378
|
}
|
|
399
379
|
|
|
@@ -404,7 +384,7 @@ export async function parseDataFromServer<M extends Record<string, unknown>>(
|
|
|
404
384
|
for (const join of relation.joinPath) {
|
|
405
385
|
const joinTable = registry.getTable(join.table);
|
|
406
386
|
if (!joinTable) {
|
|
407
|
-
|
|
387
|
+
logger.warn(`Join table not found: ${join.table}`);
|
|
408
388
|
break;
|
|
409
389
|
}
|
|
410
390
|
|
|
@@ -422,7 +402,7 @@ export async function parseDataFromServer<M extends Record<string, unknown>>(
|
|
|
422
402
|
const toCol = joinTable[toColName as keyof typeof joinTable] as AnyPgColumn;
|
|
423
403
|
|
|
424
404
|
if (!fromCol || !toCol) {
|
|
425
|
-
|
|
405
|
+
logger.warn(`Join columns not found: ${fromColumn} -> ${toColumn}`);
|
|
426
406
|
break;
|
|
427
407
|
}
|
|
428
408
|
|
|
@@ -436,7 +416,7 @@ export async function parseDataFromServer<M extends Record<string, unknown>>(
|
|
|
436
416
|
query = query.where(eq(sourceIdField, currentEntityId)) as typeof query;
|
|
437
417
|
} else {
|
|
438
418
|
// For composite keys, we would need to map the split parts. For now log a warning.
|
|
439
|
-
|
|
419
|
+
logger.warn(`Join path resolution for composite primary keys is not yet fully supported: ${collection.slug}`);
|
|
440
420
|
}
|
|
441
421
|
|
|
442
422
|
// Build additional conditions array
|
|
@@ -475,7 +455,7 @@ export async function parseDataFromServer<M extends Record<string, unknown>>(
|
|
|
475
455
|
}
|
|
476
456
|
}
|
|
477
457
|
} catch (e) {
|
|
478
|
-
|
|
458
|
+
logger.warn(`Could not resolve join path relation property: ${propKey}`, { error: e });
|
|
479
459
|
}
|
|
480
460
|
}
|
|
481
461
|
}
|
|
@@ -486,24 +466,53 @@ export async function parseDataFromServer<M extends Record<string, unknown>>(
|
|
|
486
466
|
}
|
|
487
467
|
|
|
488
468
|
/**
|
|
489
|
-
*
|
|
469
|
+
* Try to decode a `data:application/octet-stream;base64,...` data URL string
|
|
470
|
+
* into a Buffer. Returns null if the value is not a matching data URL.
|
|
490
471
|
*/
|
|
491
|
-
|
|
492
|
-
if (
|
|
493
|
-
|
|
472
|
+
function tryDecodeBase64DataUrl(value: unknown): Buffer | null {
|
|
473
|
+
if (typeof value !== "string") return null;
|
|
474
|
+
if (!value.startsWith("data:application/octet-stream;base64,")) return null;
|
|
475
|
+
const base64Data = value.split(",")[1];
|
|
476
|
+
return base64Data ? Buffer.from(base64Data, "base64") : null;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* Try to resolve an unknown value into a Buffer.
|
|
481
|
+
* Handles native Buffers and `{ type: "Buffer", data: number[] }` objects (from JSON deserialization).
|
|
482
|
+
* Returns null if the value is not a buffer.
|
|
483
|
+
*/
|
|
484
|
+
function tryResolveBuffer(value: unknown): Buffer | null {
|
|
485
|
+
if (Buffer.isBuffer(value)) return value;
|
|
486
|
+
if (typeof value === "object" && value !== null) {
|
|
487
|
+
const rawVal = value as Record<string, unknown>;
|
|
488
|
+
if (rawVal.type === "Buffer" && Array.isArray(rawVal.data)) {
|
|
489
|
+
return Buffer.from(rawVal.data as number[]);
|
|
490
|
+
}
|
|
494
491
|
}
|
|
492
|
+
return null;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/**
|
|
496
|
+
* Convert a Buffer to a UTF-8 string if all bytes are printable ASCII,
|
|
497
|
+
* otherwise return a base64 data URL.
|
|
498
|
+
*/
|
|
499
|
+
function bufferToStringOrBase64(buf: Buffer): string {
|
|
500
|
+
for (let i = 0; i < buf.length; i++) {
|
|
501
|
+
const b = buf[i];
|
|
502
|
+
// Allow standard printable ASCII + common whitespace (\r, \n, \t)
|
|
503
|
+
if ((b < 32 || b > 126) && b !== 9 && b !== 10 && b !== 13) {
|
|
504
|
+
return `data:application/octet-stream;base64,${buf.toString("base64")}`;
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
return buf.toString("utf8");
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
export function parsePropertyFromServer(value: unknown, property: Property, collection: EntityCollection, propertyKey?: string): unknown {
|
|
511
|
+
if (value === null || value === undefined) return value;
|
|
495
512
|
|
|
496
513
|
switch (property.type) {
|
|
497
514
|
case "binary": {
|
|
498
|
-
|
|
499
|
-
if (Buffer.isBuffer(value)) {
|
|
500
|
-
buf = value;
|
|
501
|
-
} else if (typeof value === "object" && value !== null) {
|
|
502
|
-
const rawVal = value as Record<string, unknown>;
|
|
503
|
-
if (rawVal.type === "Buffer" && Array.isArray(rawVal.data)) {
|
|
504
|
-
buf = Buffer.from(rawVal.data as number[]);
|
|
505
|
-
}
|
|
506
|
-
}
|
|
515
|
+
const buf = tryResolveBuffer(value);
|
|
507
516
|
if (buf) {
|
|
508
517
|
return `data:application/octet-stream;base64,${buf.toString("base64")}`;
|
|
509
518
|
}
|
|
@@ -512,36 +521,13 @@ export function parsePropertyFromServer(value: unknown, property: Property, coll
|
|
|
512
521
|
|
|
513
522
|
case "string": {
|
|
514
523
|
if (typeof value === "string") return value;
|
|
515
|
-
|
|
524
|
+
|
|
516
525
|
// Handle Buffer objects (e.g. from PostgreSQL bytea columns)
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
if (Buffer.isBuffer(value)) {
|
|
521
|
-
isBuffer = true;
|
|
522
|
-
buf = value;
|
|
523
|
-
} else if (typeof value === "object" && value !== null) {
|
|
524
|
-
const rawVal = value as Record<string, unknown>;
|
|
525
|
-
if (rawVal.type === "Buffer" && Array.isArray(rawVal.data)) {
|
|
526
|
-
isBuffer = true;
|
|
527
|
-
buf = Buffer.from(rawVal.data as number[]);
|
|
528
|
-
}
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
if (isBuffer && buf) {
|
|
532
|
-
// Heuristic: if all bytes are printable ASCII, return utf8, else base64
|
|
533
|
-
let isPrintable = true;
|
|
534
|
-
for (let i = 0; i < buf.length; i++) {
|
|
535
|
-
const b = buf[i];
|
|
536
|
-
// Allow standard printable ASCII + common whitespace (\r, \n, \t)
|
|
537
|
-
if ((b < 32 || b > 126) && b !== 9 && b !== 10 && b !== 13) {
|
|
538
|
-
isPrintable = false;
|
|
539
|
-
break;
|
|
540
|
-
}
|
|
541
|
-
}
|
|
542
|
-
return isPrintable ? buf.toString("utf8") : `data:application/octet-stream;base64,${buf.toString("base64")}`;
|
|
526
|
+
const buf = tryResolveBuffer(value);
|
|
527
|
+
if (buf) {
|
|
528
|
+
return bufferToStringOrBase64(buf);
|
|
543
529
|
}
|
|
544
|
-
|
|
530
|
+
|
|
545
531
|
if (typeof value === "object" && value !== null) {
|
|
546
532
|
try {
|
|
547
533
|
return JSON.stringify(value);
|
|
@@ -565,7 +551,7 @@ export function parsePropertyFromServer(value: unknown, property: Property, coll
|
|
|
565
551
|
}
|
|
566
552
|
|
|
567
553
|
if (!relationDef) {
|
|
568
|
-
|
|
554
|
+
logger.warn(`Relation not defined in property for key: ${propertyKey || "unknown"}`);
|
|
569
555
|
return value;
|
|
570
556
|
}
|
|
571
557
|
|
|
@@ -573,7 +559,7 @@ export function parsePropertyFromServer(value: unknown, property: Property, coll
|
|
|
573
559
|
const targetCollection = relationDef.target();
|
|
574
560
|
return createRelationRef(value.toString(), targetCollection.slug);
|
|
575
561
|
} catch (e) {
|
|
576
|
-
|
|
562
|
+
logger.warn(`Could not resolve target collection for relation property: ${propertyKey || "unknown"}`, { error: e });
|
|
577
563
|
return value;
|
|
578
564
|
}
|
|
579
565
|
}
|
|
@@ -665,32 +651,10 @@ export function parsePropertyFromServer(value: unknown, property: Property, coll
|
|
|
665
651
|
|
|
666
652
|
default: {
|
|
667
653
|
// Fallback for buffers in case they are mapped to something other than string
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
if (Buffer.isBuffer(value)) {
|
|
672
|
-
isBuffer = true;
|
|
673
|
-
buf = value;
|
|
674
|
-
} else if (typeof value === "object" && value !== null) {
|
|
675
|
-
const rawVal = value as Record<string, unknown>;
|
|
676
|
-
if (rawVal.type === "Buffer" && Array.isArray(rawVal.data)) {
|
|
677
|
-
isBuffer = true;
|
|
678
|
-
buf = Buffer.from(rawVal.data as number[]);
|
|
679
|
-
}
|
|
680
|
-
}
|
|
681
|
-
|
|
682
|
-
if (isBuffer && buf) {
|
|
683
|
-
let isPrintable = true;
|
|
684
|
-
for (let i = 0; i < buf.length; i++) {
|
|
685
|
-
const b = buf[i];
|
|
686
|
-
if ((b < 32 || b > 126) && b !== 9 && b !== 10 && b !== 13) {
|
|
687
|
-
isPrintable = false;
|
|
688
|
-
break;
|
|
689
|
-
}
|
|
690
|
-
}
|
|
691
|
-
return isPrintable ? buf.toString("utf8") : `data:application/octet-stream;base64,${buf.toString("base64")}`;
|
|
654
|
+
const buf = tryResolveBuffer(value);
|
|
655
|
+
if (buf) {
|
|
656
|
+
return bufferToStringOrBase64(buf);
|
|
692
657
|
}
|
|
693
|
-
|
|
694
658
|
return value;
|
|
695
659
|
}
|
|
696
660
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Pool } from "pg";
|
|
2
2
|
import { drizzle } from "drizzle-orm/node-postgres";
|
|
3
3
|
import { NodePgDatabase } from "drizzle-orm/node-postgres";
|
|
4
|
+
import { logger } from "@rebasepro/server-core";
|
|
4
5
|
|
|
5
6
|
export class DatabasePoolManager {
|
|
6
7
|
private pools: Map<string, Pool> = new Map();
|
|
@@ -18,7 +19,7 @@ export class DatabasePoolManager {
|
|
|
18
19
|
}
|
|
19
20
|
}
|
|
20
21
|
|
|
21
|
-
public getDrizzle(databaseName: string): NodePgDatabase<
|
|
22
|
+
public getDrizzle(databaseName: string): NodePgDatabase<Record<string, never>> {
|
|
22
23
|
const existing = this.drizzleInstances.get(databaseName);
|
|
23
24
|
if (existing) {
|
|
24
25
|
return existing;
|
|
@@ -47,7 +48,7 @@ export class DatabasePoolManager {
|
|
|
47
48
|
|
|
48
49
|
// Prevent idle client errors from crashing the Node.js process
|
|
49
50
|
pool.on("error", (err) => {
|
|
50
|
-
|
|
51
|
+
logger.error(`[DatabasePoolManager] Unexpected error on idle client for db ${databaseName}`, { error: err });
|
|
51
52
|
});
|
|
52
53
|
|
|
53
54
|
this.pools.set(databaseName, pool);
|
|
@@ -76,10 +77,11 @@ export class DatabasePoolManager {
|
|
|
76
77
|
public async shutdown(): Promise<void> {
|
|
77
78
|
const promises = [];
|
|
78
79
|
for (const [dbName, pool] of this.pools.entries()) {
|
|
79
|
-
|
|
80
|
+
logger.info(`[DatabasePoolManager] Shutting down pool for ${dbName}`);
|
|
80
81
|
promises.push(pool.end());
|
|
81
82
|
}
|
|
82
83
|
await Promise.all(promises);
|
|
83
84
|
this.pools.clear();
|
|
85
|
+
this.drizzleInstances.clear();
|
|
84
86
|
}
|
|
85
87
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { sql } from "drizzle-orm";
|
|
2
2
|
import { NodePgDatabase } from "drizzle-orm/node-postgres";
|
|
3
|
+
import { logger } from "@rebasepro/server-core";
|
|
3
4
|
|
|
4
5
|
export interface HistoryEntry {
|
|
5
6
|
id: string;
|
|
@@ -99,10 +100,10 @@ export class HistoryService {
|
|
|
99
100
|
|
|
100
101
|
// Non-blocking prune for this specific entity
|
|
101
102
|
this.pruneEntity(tableName, entityId).catch(err =>
|
|
102
|
-
|
|
103
|
+
logger.error("History prune failed", { error: err })
|
|
103
104
|
);
|
|
104
105
|
} catch (error) {
|
|
105
|
-
|
|
106
|
+
logger.error("Failed to record entity history", { error: error });
|
|
106
107
|
}
|
|
107
108
|
}
|
|
108
109
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { sql } from "drizzle-orm";
|
|
2
2
|
import { NodePgDatabase } from "drizzle-orm/node-postgres";
|
|
3
|
+
import { logger } from "@rebasepro/server-core";
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Auto-create the entity history table if it doesn't exist.
|
|
@@ -7,7 +8,7 @@ import { NodePgDatabase } from "drizzle-orm/node-postgres";
|
|
|
7
8
|
* pattern as `ensureAuthTablesExist`.
|
|
8
9
|
*/
|
|
9
10
|
export async function ensureHistoryTableExists(db: NodePgDatabase): Promise<void> {
|
|
10
|
-
|
|
11
|
+
logger.info("🔍 Checking entity history table...");
|
|
11
12
|
|
|
12
13
|
try {
|
|
13
14
|
// Create the rebase schema (idempotent — may already exist from auth init)
|
|
@@ -37,9 +38,9 @@ export async function ensureHistoryTableExists(db: NodePgDatabase): Promise<void
|
|
|
37
38
|
ON rebase.entity_history(table_name, entity_id, updated_at DESC)
|
|
38
39
|
`);
|
|
39
40
|
|
|
40
|
-
|
|
41
|
+
logger.info("✅ Entity history table ready");
|
|
41
42
|
} catch (error) {
|
|
42
|
-
|
|
43
|
-
|
|
43
|
+
logger.error("❌ Failed to create entity history table", { error: error });
|
|
44
|
+
logger.warn("⚠️ Continuing without creating history table.");
|
|
44
45
|
}
|
|
45
46
|
}
|
|
@@ -4,7 +4,7 @@ import { relations } from "drizzle-orm";
|
|
|
4
4
|
/**
|
|
5
5
|
* Factory function to dynamically create the auth tables bound to the specified schema names.
|
|
6
6
|
*/
|
|
7
|
-
export function createAuthSchema(usersSchemaName
|
|
7
|
+
export function createAuthSchema(usersSchemaName = "rebase") {
|
|
8
8
|
const usersSchema = usersSchemaName === "public" ? null : pgSchema(usersSchemaName);
|
|
9
9
|
|
|
10
10
|
const tableCreator = (usersSchema ? usersSchema.table.bind(usersSchema) : pgTable) as typeof pgTable;
|
|
@@ -30,7 +30,6 @@ export function createAuthSchema(usersSchemaName: string = "rebase") {
|
|
|
30
30
|
});
|
|
31
31
|
|
|
32
32
|
|
|
33
|
-
|
|
34
33
|
/**
|
|
35
34
|
* Refresh tokens for long-lived sessions
|
|
36
35
|
*/
|
package/src/schema/doctor-cli.ts
CHANGED
|
@@ -7,6 +7,7 @@ import path from "path";
|
|
|
7
7
|
import chalk from "chalk";
|
|
8
8
|
import fs from "fs";
|
|
9
9
|
import { runDoctor } from "./doctor";
|
|
10
|
+
import { logger } from "@rebasepro/server-core";
|
|
10
11
|
|
|
11
12
|
async function main() {
|
|
12
13
|
const collectionsArg = process.argv.find((a) => a.startsWith("--collections="));
|
|
@@ -46,6 +47,6 @@ async function main() {
|
|
|
46
47
|
}
|
|
47
48
|
|
|
48
49
|
main().catch((err) => {
|
|
49
|
-
|
|
50
|
+
logger.error(chalk.red(" ✗ Doctor failed"), { error: err });
|
|
50
51
|
process.exit(1);
|
|
51
52
|
});
|
package/src/schema/doctor.ts
CHANGED
|
@@ -17,6 +17,7 @@ import { generateSchema } from "./generate-drizzle-schema-logic";
|
|
|
17
17
|
import { generateTypedefs } from "@rebasepro/sdk-generator";
|
|
18
18
|
import { getTableName, resolveCollectionRelations, findRelation } from "@rebasepro/common";
|
|
19
19
|
import { toSnakeCase } from "@rebasepro/utils";
|
|
20
|
+
import { logger } from "@rebasepro/server-core";
|
|
20
21
|
|
|
21
22
|
/**
|
|
22
23
|
* Resolve the SQL column name for a property.
|
|
@@ -149,7 +150,7 @@ export async function loadCollections(collectionsPath: string): Promise<EntityCo
|
|
|
149
150
|
}
|
|
150
151
|
} catch (err: unknown) {
|
|
151
152
|
const message = err instanceof Error ? err.message : String(err);
|
|
152
|
-
|
|
153
|
+
logger.error(chalk.yellow(` ⚠ Could not load ${file}: ${message}`));
|
|
153
154
|
}
|
|
154
155
|
}
|
|
155
156
|
}
|
|
@@ -244,7 +245,8 @@ export async function checkCollectionsVsSdk(
|
|
|
244
245
|
message: `Generated SDK typedefs file does not exist at "${sdkFilePath}".`,
|
|
245
246
|
fix: "Run `rebase generate-sdk`"
|
|
246
247
|
});
|
|
247
|
-
return { passed: false,
|
|
248
|
+
return { passed: false,
|
|
249
|
+
issues };
|
|
248
250
|
}
|
|
249
251
|
|
|
250
252
|
try {
|
|
@@ -277,7 +279,8 @@ export async function checkCollectionsVsSdk(
|
|
|
277
279
|
});
|
|
278
280
|
}
|
|
279
281
|
|
|
280
|
-
return { passed: issues.length === 0,
|
|
282
|
+
return { passed: issues.length === 0,
|
|
283
|
+
issues };
|
|
281
284
|
}
|
|
282
285
|
|
|
283
286
|
// ── Phase 2: Collections ↔ Database ──────────────────────────────────────
|
|
@@ -326,7 +329,7 @@ export async function checkCollectionsVsDatabase(
|
|
|
326
329
|
WHERE table_schema = ANY($1) AND table_type = 'BASE TABLE'`,
|
|
327
330
|
[schemas]
|
|
328
331
|
);
|
|
329
|
-
const existingTables = new Set(tablesResult.rows.map((r) =>
|
|
332
|
+
const existingTables = new Set(tablesResult.rows.map((r) =>
|
|
330
333
|
r.table_schema === "public" ? r.table_name : `${r.table_schema}.${r.table_name}`
|
|
331
334
|
));
|
|
332
335
|
|
|
@@ -457,8 +460,8 @@ export async function checkCollectionsVsDatabase(
|
|
|
457
460
|
targetSchemaName = targetColl.schema || "public";
|
|
458
461
|
} catch { /* ignore */ }
|
|
459
462
|
|
|
460
|
-
const hasFk = tableFks.some((fk) =>
|
|
461
|
-
fk.column_name === fkColName &&
|
|
463
|
+
const hasFk = tableFks.some((fk) =>
|
|
464
|
+
fk.column_name === fkColName &&
|
|
462
465
|
fk.foreign_table_name === targetTableName &&
|
|
463
466
|
fk.foreign_table_schema === targetSchemaName
|
|
464
467
|
);
|
|
@@ -615,10 +618,10 @@ issues };
|
|
|
615
618
|
// ── Report Rendering ─────────────────────────────────────────────────────
|
|
616
619
|
|
|
617
620
|
export function renderReport(report: DoctorReport): void {
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
621
|
+
logger.info("");
|
|
622
|
+
logger.info(chalk.bold(" 🩺 Rebase Schema Doctor"));
|
|
623
|
+
logger.info(chalk.gray(" ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"));
|
|
624
|
+
logger.info("");
|
|
622
625
|
|
|
623
626
|
// Phase 1
|
|
624
627
|
renderPhase(
|
|
@@ -642,7 +645,7 @@ export function renderReport(report: DoctorReport): void {
|
|
|
642
645
|
);
|
|
643
646
|
|
|
644
647
|
// Summary
|
|
645
|
-
|
|
648
|
+
logger.info(chalk.gray(" ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"));
|
|
646
649
|
const { passed, warnings, errors } = report.summary;
|
|
647
650
|
|
|
648
651
|
const parts: string[] = [];
|
|
@@ -650,50 +653,50 @@ export function renderReport(report: DoctorReport): void {
|
|
|
650
653
|
if (warnings > 0) parts.push(chalk.yellow(`${warnings} warnings`));
|
|
651
654
|
if (errors > 0) parts.push(chalk.red(`${errors} errors`));
|
|
652
655
|
|
|
653
|
-
|
|
654
|
-
|
|
656
|
+
logger.info(` Summary: ${parts.join(", ")}`);
|
|
657
|
+
logger.info("");
|
|
655
658
|
|
|
656
659
|
if (errors > 0) {
|
|
657
|
-
|
|
660
|
+
logger.info(chalk.red.bold(" ✗ Schema drift detected. Run the suggested fixes above."));
|
|
658
661
|
} else if (warnings > 0) {
|
|
659
|
-
|
|
662
|
+
logger.info(chalk.yellow.bold(" ⚠ Minor issues detected. Consider running the suggested fixes."));
|
|
660
663
|
} else {
|
|
661
|
-
|
|
664
|
+
logger.info(chalk.green.bold(" ✓ All schemas are in sync!"));
|
|
662
665
|
}
|
|
663
|
-
|
|
666
|
+
logger.info("");
|
|
664
667
|
}
|
|
665
668
|
|
|
666
669
|
function renderPhase(label: string, passed: boolean, issues: DoctorIssue[]): void {
|
|
667
670
|
if (passed) {
|
|
668
|
-
|
|
671
|
+
logger.info(` ${chalk.green("✅")} ${label}: ${chalk.green("In sync")}`);
|
|
669
672
|
} else {
|
|
670
673
|
const errorCount = issues.filter((i) => i.severity === "error").length;
|
|
671
674
|
const warnCount = issues.filter((i) => i.severity === "warning").length;
|
|
672
675
|
const parts: string[] = [];
|
|
673
676
|
if (errorCount > 0) parts.push(`${errorCount} error${errorCount > 1 ? "s" : ""}`);
|
|
674
677
|
if (warnCount > 0) parts.push(`${warnCount} warning${warnCount > 1 ? "s" : ""}`);
|
|
675
|
-
|
|
678
|
+
logger.info(` ${chalk.yellow("⚠️")} ${label}: ${chalk.yellow(parts.join(", "))}`);
|
|
676
679
|
}
|
|
677
|
-
|
|
680
|
+
logger.info("");
|
|
678
681
|
|
|
679
682
|
for (const issue of issues) {
|
|
680
683
|
const severityIcon = issue.severity === "error" ? chalk.red("✗") : chalk.yellow("⚠");
|
|
681
684
|
const categoryLabel = formatCategory(issue.category);
|
|
682
|
-
|
|
685
|
+
logger.info(` ${chalk.gray("┌─")} ${severityIcon} ${chalk.bold(categoryLabel)} ${chalk.gray("─".repeat(Math.max(0, 42 - categoryLabel.length)))}`);
|
|
683
686
|
|
|
684
687
|
if (issue.table) {
|
|
685
688
|
const colPart = issue.column ? ` │ Column: ${chalk.cyan(issue.column)}` : "";
|
|
686
|
-
|
|
689
|
+
logger.info(` ${chalk.gray("│")} Table: ${chalk.cyan(issue.table)}${colPart}`);
|
|
687
690
|
}
|
|
688
691
|
|
|
689
692
|
if (issue.expected && issue.actual) {
|
|
690
|
-
|
|
693
|
+
logger.info(` ${chalk.gray("│")} Expected: ${chalk.green(issue.expected)} │ Actual: ${chalk.red(issue.actual)}`);
|
|
691
694
|
}
|
|
692
695
|
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
696
|
+
logger.info(` ${chalk.gray("│")} ${issue.message}`);
|
|
697
|
+
logger.info(` ${chalk.gray("│")} Fix: ${chalk.blue(issue.fix)}`);
|
|
698
|
+
logger.info(` ${chalk.gray("└" + "─".repeat(48))}`);
|
|
699
|
+
logger.info("");
|
|
697
700
|
}
|
|
698
701
|
}
|
|
699
702
|
|
|
@@ -720,33 +723,33 @@ export async function runDoctor(options: {
|
|
|
720
723
|
sdkPath: string;
|
|
721
724
|
databaseUrl?: string;
|
|
722
725
|
}): Promise<DoctorReport> {
|
|
723
|
-
|
|
724
|
-
|
|
726
|
+
logger.info("");
|
|
727
|
+
logger.info(chalk.bold(" 🩺 Loading collections..."));
|
|
725
728
|
const collections = await loadCollections(options.collectionsPath);
|
|
726
729
|
if (collections.length === 0) {
|
|
727
|
-
|
|
730
|
+
logger.error(chalk.red(" ✗ No collections found."));
|
|
728
731
|
process.exit(1);
|
|
729
732
|
}
|
|
730
|
-
|
|
731
|
-
|
|
733
|
+
logger.info(chalk.gray(` Found ${collections.length} collection(s)`));
|
|
734
|
+
logger.info("");
|
|
732
735
|
|
|
733
736
|
// Phase 1: Collections ↔ Generated Schema
|
|
734
|
-
|
|
737
|
+
logger.info(chalk.gray(" Checking Collections → Generated Schema..."));
|
|
735
738
|
const collectionsToSchema = await checkCollectionsVsSchema(collections, options.schemaPath);
|
|
736
739
|
|
|
737
740
|
// Phase 2: Collections ↔ Database (only if we have a DATABASE_URL)
|
|
738
741
|
let schemaToDatabase: { passed: boolean; issues: DoctorIssue[] } = { passed: true,
|
|
739
742
|
issues: [] };
|
|
740
743
|
if (options.databaseUrl) {
|
|
741
|
-
|
|
744
|
+
logger.info(chalk.gray(" Checking Collections → Database..."));
|
|
742
745
|
schemaToDatabase = await checkCollectionsVsDatabase(collections, options.databaseUrl);
|
|
743
746
|
} else {
|
|
744
|
-
|
|
745
|
-
|
|
747
|
+
logger.info(chalk.yellow(" ⚠ DATABASE_URL not set — skipping database comparison."));
|
|
748
|
+
logger.info(chalk.gray(" Set DATABASE_URL in your .env to enable full drift detection."));
|
|
746
749
|
}
|
|
747
750
|
|
|
748
751
|
// Phase 3: Collections ↔ SDK Types
|
|
749
|
-
|
|
752
|
+
logger.info(chalk.gray(" Checking Collections → SDK Types..."));
|
|
750
753
|
const collectionsToSdk = await checkCollectionsVsSdk(collections, options.sdkPath);
|
|
751
754
|
|
|
752
755
|
const allIssues = [...collectionsToSchema.issues, ...schemaToDatabase.issues, ...collectionsToSdk.issues];
|