@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.
Files changed (168) hide show
  1. package/README.md +69 -89
  2. package/dist/{server-postgresql/src/PostgresAdapter.d.ts → PostgresAdapter.d.ts} +1 -1
  3. package/dist/{server-postgresql/src/PostgresBackendDriver.d.ts → PostgresBackendDriver.d.ts} +2 -2
  4. package/dist/{server-postgresql/src/PostgresBootstrapper.d.ts → PostgresBootstrapper.d.ts} +11 -1
  5. package/dist/{server-postgresql/src/auth → auth}/services.d.ts +11 -11
  6. package/dist/{server-postgresql/src/collections → collections}/PostgresCollectionRegistry.d.ts +4 -0
  7. package/dist/{server-postgresql/src/data-transformer.d.ts → data-transformer.d.ts} +0 -3
  8. package/dist/{server-postgresql/src/databasePoolManager.d.ts → databasePoolManager.d.ts} +1 -1
  9. package/dist/index.es.js +10174 -11184
  10. package/dist/index.es.js.map +1 -1
  11. package/dist/index.umd.js +10735 -11462
  12. package/dist/index.umd.js.map +1 -1
  13. package/dist/{server-postgresql/src/services → services}/EntityPersistService.d.ts +0 -14
  14. package/dist/types.d.ts +3 -0
  15. package/dist/utils/pg-error-utils.d.ts +55 -0
  16. package/dist/{server-postgresql/src/websocket.d.ts → websocket.d.ts} +8 -3
  17. package/package.json +24 -21
  18. package/src/PostgresAdapter.ts +9 -10
  19. package/src/PostgresBackendDriver.ts +135 -122
  20. package/src/PostgresBootstrapper.ts +90 -16
  21. package/src/auth/ensure-tables.ts +28 -5
  22. package/src/auth/services.ts +56 -45
  23. package/src/cli.ts +140 -110
  24. package/src/collections/PostgresCollectionRegistry.ts +7 -0
  25. package/src/connection.ts +11 -6
  26. package/src/data-transformer.ts +73 -109
  27. package/src/databasePoolManager.ts +5 -3
  28. package/src/history/HistoryService.ts +3 -2
  29. package/src/history/ensure-history-table.ts +5 -4
  30. package/src/schema/auth-schema.ts +1 -2
  31. package/src/schema/doctor-cli.ts +2 -1
  32. package/src/schema/doctor.ts +40 -37
  33. package/src/schema/generate-drizzle-schema-logic.ts +56 -18
  34. package/src/schema/generate-drizzle-schema.ts +11 -11
  35. package/src/schema/introspect-db-inference.ts +25 -25
  36. package/src/schema/introspect-db-logic.ts +38 -38
  37. package/src/schema/introspect-db.ts +28 -27
  38. package/src/services/BranchService.ts +14 -0
  39. package/src/services/EntityFetchService.ts +28 -25
  40. package/src/services/EntityPersistService.ts +11 -124
  41. package/src/services/RelationService.ts +57 -37
  42. package/src/services/entity-helpers.ts +6 -2
  43. package/src/services/realtimeService.ts +45 -32
  44. package/src/types.ts +4 -0
  45. package/src/utils/drizzle-conditions.ts +31 -15
  46. package/src/utils/pg-error-utils.ts +211 -0
  47. package/src/websocket.ts +51 -33
  48. package/test/auth-services.test.ts +36 -19
  49. package/test/batch-many-to-many-regression.test.ts +119 -39
  50. package/test/data-transformer-hardening.test.ts +67 -33
  51. package/test/data-transformer.test.ts +4 -2
  52. package/test/doctor.test.ts +10 -5
  53. package/test/drizzle-conditions.test.ts +59 -6
  54. package/test/generate-drizzle-schema.test.ts +65 -40
  55. package/test/introspect-db-generation.test.ts +179 -81
  56. package/test/introspect-db-utils.test.ts +92 -37
  57. package/test/mocks/chalk.cjs +7 -0
  58. package/test/pg-error-utils.test.ts +221 -0
  59. package/test/postgresDataDriver.test.ts +14 -5
  60. package/test/property-ordering.test.ts +126 -79
  61. package/test/realtimeService.test.ts +6 -2
  62. package/test/relation-pipeline-gaps.test.ts +84 -36
  63. package/test/relations.test.ts +247 -0
  64. package/test/unmapped-tables-safety.test.ts +14 -6
  65. package/test/websocket.test.ts +1 -1
  66. package/tsconfig.json +5 -0
  67. package/tsconfig.prod.json +3 -0
  68. package/vite.config.ts +5 -5
  69. package/dist/common/src/collections/CollectionRegistry.d.ts +0 -56
  70. package/dist/common/src/collections/default-collections.d.ts +0 -9
  71. package/dist/common/src/collections/index.d.ts +0 -2
  72. package/dist/common/src/data/buildRebaseData.d.ts +0 -14
  73. package/dist/common/src/data/query_builder.d.ts +0 -55
  74. package/dist/common/src/index.d.ts +0 -4
  75. package/dist/common/src/util/builders.d.ts +0 -57
  76. package/dist/common/src/util/callbacks.d.ts +0 -6
  77. package/dist/common/src/util/collections.d.ts +0 -11
  78. package/dist/common/src/util/common.d.ts +0 -2
  79. package/dist/common/src/util/conditions.d.ts +0 -26
  80. package/dist/common/src/util/entities.d.ts +0 -58
  81. package/dist/common/src/util/enums.d.ts +0 -3
  82. package/dist/common/src/util/index.d.ts +0 -16
  83. package/dist/common/src/util/navigation_from_path.d.ts +0 -34
  84. package/dist/common/src/util/navigation_utils.d.ts +0 -20
  85. package/dist/common/src/util/parent_references_from_path.d.ts +0 -6
  86. package/dist/common/src/util/paths.d.ts +0 -14
  87. package/dist/common/src/util/permissions.d.ts +0 -6
  88. package/dist/common/src/util/references.d.ts +0 -2
  89. package/dist/common/src/util/relations.d.ts +0 -22
  90. package/dist/common/src/util/resolutions.d.ts +0 -72
  91. package/dist/common/src/util/storage.d.ts +0 -24
  92. package/dist/types/src/controllers/analytics_controller.d.ts +0 -7
  93. package/dist/types/src/controllers/auth.d.ts +0 -104
  94. package/dist/types/src/controllers/client.d.ts +0 -168
  95. package/dist/types/src/controllers/collection_registry.d.ts +0 -46
  96. package/dist/types/src/controllers/customization_controller.d.ts +0 -60
  97. package/dist/types/src/controllers/data.d.ts +0 -207
  98. package/dist/types/src/controllers/data_driver.d.ts +0 -218
  99. package/dist/types/src/controllers/database_admin.d.ts +0 -11
  100. package/dist/types/src/controllers/dialogs_controller.d.ts +0 -36
  101. package/dist/types/src/controllers/effective_role.d.ts +0 -4
  102. package/dist/types/src/controllers/email.d.ts +0 -36
  103. package/dist/types/src/controllers/index.d.ts +0 -18
  104. package/dist/types/src/controllers/local_config_persistence.d.ts +0 -20
  105. package/dist/types/src/controllers/navigation.d.ts +0 -225
  106. package/dist/types/src/controllers/registry.d.ts +0 -63
  107. package/dist/types/src/controllers/side_dialogs_controller.d.ts +0 -67
  108. package/dist/types/src/controllers/side_entity_controller.d.ts +0 -97
  109. package/dist/types/src/controllers/snackbar.d.ts +0 -24
  110. package/dist/types/src/controllers/storage.d.ts +0 -171
  111. package/dist/types/src/index.d.ts +0 -4
  112. package/dist/types/src/rebase_context.d.ts +0 -122
  113. package/dist/types/src/types/auth_adapter.d.ts +0 -301
  114. package/dist/types/src/types/backend.d.ts +0 -536
  115. package/dist/types/src/types/backend_hooks.d.ts +0 -172
  116. package/dist/types/src/types/builders.d.ts +0 -15
  117. package/dist/types/src/types/chips.d.ts +0 -5
  118. package/dist/types/src/types/collections.d.ts +0 -941
  119. package/dist/types/src/types/component_ref.d.ts +0 -47
  120. package/dist/types/src/types/cron.d.ts +0 -102
  121. package/dist/types/src/types/data_source.d.ts +0 -64
  122. package/dist/types/src/types/database_adapter.d.ts +0 -94
  123. package/dist/types/src/types/entities.d.ts +0 -145
  124. package/dist/types/src/types/entity_actions.d.ts +0 -104
  125. package/dist/types/src/types/entity_callbacks.d.ts +0 -173
  126. package/dist/types/src/types/entity_link_builder.d.ts +0 -7
  127. package/dist/types/src/types/entity_overrides.d.ts +0 -10
  128. package/dist/types/src/types/entity_views.d.ts +0 -87
  129. package/dist/types/src/types/export_import.d.ts +0 -21
  130. package/dist/types/src/types/formex.d.ts +0 -40
  131. package/dist/types/src/types/index.d.ts +0 -28
  132. package/dist/types/src/types/locales.d.ts +0 -4
  133. package/dist/types/src/types/modify_collections.d.ts +0 -5
  134. package/dist/types/src/types/plugins.d.ts +0 -282
  135. package/dist/types/src/types/properties.d.ts +0 -1181
  136. package/dist/types/src/types/property_config.d.ts +0 -74
  137. package/dist/types/src/types/relations.d.ts +0 -336
  138. package/dist/types/src/types/slots.d.ts +0 -262
  139. package/dist/types/src/types/translations.d.ts +0 -900
  140. package/dist/types/src/types/user_management_delegate.d.ts +0 -86
  141. package/dist/types/src/types/websockets.d.ts +0 -78
  142. package/dist/types/src/users/index.d.ts +0 -1
  143. package/dist/types/src/users/user.d.ts +0 -50
  144. package/drizzle.test.config.ts +0 -10
  145. /package/dist/{server-postgresql/src/auth → auth}/ensure-tables.d.ts +0 -0
  146. /package/dist/{server-postgresql/src/cli.d.ts → cli.d.ts} +0 -0
  147. /package/dist/{server-postgresql/src/connection.d.ts → connection.d.ts} +0 -0
  148. /package/dist/{server-postgresql/src/history → history}/HistoryService.d.ts +0 -0
  149. /package/dist/{server-postgresql/src/history → history}/ensure-history-table.d.ts +0 -0
  150. /package/dist/{server-postgresql/src/index.d.ts → index.d.ts} +0 -0
  151. /package/dist/{server-postgresql/src/interfaces.d.ts → interfaces.d.ts} +0 -0
  152. /package/dist/{server-postgresql/src/schema → schema}/auth-schema.d.ts +0 -0
  153. /package/dist/{server-postgresql/src/schema → schema}/doctor-cli.d.ts +0 -0
  154. /package/dist/{server-postgresql/src/schema → schema}/doctor.d.ts +0 -0
  155. /package/dist/{server-postgresql/src/schema → schema}/generate-drizzle-schema-logic.d.ts +0 -0
  156. /package/dist/{server-postgresql/src/schema → schema}/generate-drizzle-schema.d.ts +0 -0
  157. /package/dist/{server-postgresql/src/schema → schema}/introspect-db-inference.d.ts +0 -0
  158. /package/dist/{server-postgresql/src/schema → schema}/introspect-db-logic.d.ts +0 -0
  159. /package/dist/{server-postgresql/src/schema → schema}/introspect-db.d.ts +0 -0
  160. /package/dist/{server-postgresql/src/schema → schema}/test-schema.d.ts +0 -0
  161. /package/dist/{server-postgresql/src/services → services}/BranchService.d.ts +0 -0
  162. /package/dist/{server-postgresql/src/services → services}/EntityFetchService.d.ts +0 -0
  163. /package/dist/{server-postgresql/src/services → services}/RelationService.d.ts +0 -0
  164. /package/dist/{server-postgresql/src/services → services}/entity-helpers.d.ts +0 -0
  165. /package/dist/{server-postgresql/src/services → services}/entityService.d.ts +0 -0
  166. /package/dist/{server-postgresql/src/services → services}/index.d.ts +0 -0
  167. /package/dist/{server-postgresql/src/services → services}/realtimeService.d.ts +0 -0
  168. /package/dist/{server-postgresql/src/utils → utils}/drizzle-conditions.d.ts +0 -0
@@ -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 ?? {}, inverseRelationUpdates: [], joinPathRelationUpdates: [] };
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
- if (typeof value === "string") {
277
- if (value.startsWith("data:application/octet-stream;base64,")) {
278
- const base64Data = value.split(",")[1];
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
- if (typeof value === "string") {
291
- if (value.startsWith("data:application/octet-stream;base64,")) {
292
- const base64Data = value.split(",")[1];
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
- console.warn(`Could not resolve target collection for relation property: ${propKey}`, e);
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
- console.warn(`Could not resolve inverse relation property: ${propKey}`, e);
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
- console.warn(`Source table not found for collection: ${collection.slug}`);
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
- console.warn(`Join table not found: ${join.table}`);
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
- console.warn(`Join columns not found: ${fromColumn} -> ${toColumn}`);
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
- console.warn(`Join path resolution for composite primary keys is not yet fully supported: ${collection.slug}`);
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
- console.warn(`Could not resolve join path relation property: ${propKey}`, e);
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
- * Parse a single property value from database format to frontend format
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
- export function parsePropertyFromServer(value: unknown, property: Property, collection: EntityCollection, propertyKey?: string): unknown {
492
- if (value === null || value === undefined) {
493
- return value;
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
- let buf: Buffer | null = null;
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
- let isBuffer = false;
518
- let buf: Buffer | null = null;
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
- console.warn(`Relation not defined in property for key: ${propertyKey || "unknown"}`);
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
- console.warn(`Could not resolve target collection for relation property: ${propertyKey || "unknown"}`, e);
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
- let isBuffer = false;
669
- let buf: Buffer | null = null;
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<any> {
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
- console.error(`[DatabasePoolManager] Unexpected error on idle client for db ${databaseName}`, err);
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
- console.log(`[DatabasePoolManager] Shutting down pool for ${dbName}`);
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
- console.error("History prune failed:", err)
103
+ logger.error("History prune failed", { error: err })
103
104
  );
104
105
  } catch (error) {
105
- console.error("Failed to record entity history:", error);
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
- console.log("🔍 Checking entity history table...");
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
- console.log("✅ Entity history table ready");
41
+ logger.info("✅ Entity history table ready");
41
42
  } catch (error) {
42
- console.error("❌ Failed to create entity history table:", error);
43
- console.warn("⚠️ Continuing without creating history table.");
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: string = "rebase") {
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
  */
@@ -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
- console.error(chalk.red(" ✗ Doctor failed:"), err instanceof Error ? err.message : String(err));
50
+ logger.error(chalk.red(" ✗ Doctor failed"), { error: err });
50
51
  process.exit(1);
51
52
  });
@@ -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
- console.error(chalk.yellow(` ⚠ Could not load ${file}: ${message}`));
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, issues };
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, issues };
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
- console.log("");
619
- console.log(chalk.bold(" 🩺 Rebase Schema Doctor"));
620
- console.log(chalk.gray(" ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"));
621
- console.log("");
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
- console.log(chalk.gray(" ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"));
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
- console.log(` Summary: ${parts.join(", ")}`);
654
- console.log("");
656
+ logger.info(` Summary: ${parts.join(", ")}`);
657
+ logger.info("");
655
658
 
656
659
  if (errors > 0) {
657
- console.log(chalk.red.bold(" ✗ Schema drift detected. Run the suggested fixes above."));
660
+ logger.info(chalk.red.bold(" ✗ Schema drift detected. Run the suggested fixes above."));
658
661
  } else if (warnings > 0) {
659
- console.log(chalk.yellow.bold(" ⚠ Minor issues detected. Consider running the suggested fixes."));
662
+ logger.info(chalk.yellow.bold(" ⚠ Minor issues detected. Consider running the suggested fixes."));
660
663
  } else {
661
- console.log(chalk.green.bold(" ✓ All schemas are in sync!"));
664
+ logger.info(chalk.green.bold(" ✓ All schemas are in sync!"));
662
665
  }
663
- console.log("");
666
+ logger.info("");
664
667
  }
665
668
 
666
669
  function renderPhase(label: string, passed: boolean, issues: DoctorIssue[]): void {
667
670
  if (passed) {
668
- console.log(` ${chalk.green("✅")} ${label}: ${chalk.green("In sync")}`);
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
- console.log(` ${chalk.yellow("⚠️")} ${label}: ${chalk.yellow(parts.join(", "))}`);
678
+ logger.info(` ${chalk.yellow("⚠️")} ${label}: ${chalk.yellow(parts.join(", "))}`);
676
679
  }
677
- console.log("");
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
- console.log(` ${chalk.gray("┌─")} ${severityIcon} ${chalk.bold(categoryLabel)} ${chalk.gray("─".repeat(Math.max(0, 42 - categoryLabel.length)))}`);
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
- console.log(` ${chalk.gray("│")} Table: ${chalk.cyan(issue.table)}${colPart}`);
689
+ logger.info(` ${chalk.gray("│")} Table: ${chalk.cyan(issue.table)}${colPart}`);
687
690
  }
688
691
 
689
692
  if (issue.expected && issue.actual) {
690
- console.log(` ${chalk.gray("│")} Expected: ${chalk.green(issue.expected)} │ Actual: ${chalk.red(issue.actual)}`);
693
+ logger.info(` ${chalk.gray("│")} Expected: ${chalk.green(issue.expected)} │ Actual: ${chalk.red(issue.actual)}`);
691
694
  }
692
695
 
693
- console.log(` ${chalk.gray("│")} ${issue.message}`);
694
- console.log(` ${chalk.gray("│")} Fix: ${chalk.blue(issue.fix)}`);
695
- console.log(` ${chalk.gray("└" + "─".repeat(48))}`);
696
- console.log("");
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
- console.log("");
724
- console.log(chalk.bold(" 🩺 Loading collections..."));
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
- console.error(chalk.red(" ✗ No collections found."));
730
+ logger.error(chalk.red(" ✗ No collections found."));
728
731
  process.exit(1);
729
732
  }
730
- console.log(chalk.gray(` Found ${collections.length} collection(s)`));
731
- console.log("");
733
+ logger.info(chalk.gray(` Found ${collections.length} collection(s)`));
734
+ logger.info("");
732
735
 
733
736
  // Phase 1: Collections ↔ Generated Schema
734
- console.log(chalk.gray(" Checking Collections → Generated Schema..."));
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
- console.log(chalk.gray(" Checking Collections → Database..."));
744
+ logger.info(chalk.gray(" Checking Collections → Database..."));
742
745
  schemaToDatabase = await checkCollectionsVsDatabase(collections, options.databaseUrl);
743
746
  } else {
744
- console.log(chalk.yellow(" ⚠ DATABASE_URL not set — skipping database comparison."));
745
- console.log(chalk.gray(" Set DATABASE_URL in your .env to enable full drift detection."));
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
- console.log(chalk.gray(" Checking Collections → SDK Types..."));
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];