@rebasepro/server-postgresql 0.0.1-canary.892f711 → 0.0.1-canary.a6becfb

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 (33) hide show
  1. package/dist/index.es.js +381 -1074
  2. package/dist/index.es.js.map +1 -1
  3. package/dist/index.umd.js +312 -1005
  4. package/dist/index.umd.js.map +1 -1
  5. package/dist/server-postgresql/src/PostgresBackendDriver.d.ts +2 -1
  6. package/dist/server-postgresql/src/schema/introspect-db-inference.d.ts +5 -0
  7. package/dist/server-postgresql/src/schema/introspect-db-logic.d.ts +44 -9
  8. package/dist/server-postgresql/src/services/EntityPersistService.d.ts +9 -0
  9. package/dist/types/src/controllers/auth.d.ts +8 -2
  10. package/dist/types/src/controllers/client.d.ts +13 -0
  11. package/dist/types/src/controllers/navigation.d.ts +18 -6
  12. package/dist/types/src/controllers/registry.d.ts +9 -1
  13. package/dist/types/src/controllers/side_entity_controller.d.ts +7 -0
  14. package/dist/types/src/rebase_context.d.ts +17 -0
  15. package/dist/types/src/types/collections.d.ts +64 -1
  16. package/dist/types/src/types/component_ref.d.ts +47 -0
  17. package/dist/types/src/types/entity_views.d.ts +2 -1
  18. package/dist/types/src/types/index.d.ts +1 -0
  19. package/dist/types/src/types/properties.d.ts +3 -3
  20. package/dist/types/src/types/translations.d.ts +8 -0
  21. package/package.json +5 -5
  22. package/src/PostgresBackendDriver.ts +23 -6
  23. package/src/cli.ts +9 -1
  24. package/src/data-transformer.ts +84 -1
  25. package/src/schema/generate-drizzle-schema-logic.ts +35 -0
  26. package/src/schema/introspect-db-inference.ts +238 -0
  27. package/src/schema/introspect-db-logic.ts +337 -36
  28. package/src/schema/introspect-db.ts +66 -23
  29. package/src/services/EntityFetchService.ts +16 -0
  30. package/src/services/EntityPersistService.ts +88 -12
  31. package/test/generate-drizzle-schema.test.ts +128 -0
  32. package/test/introspect-db-generation.test.ts +5 -5
  33. package/test/property-ordering.test.ts +395 -0
@@ -258,7 +258,26 @@ export function serializePropertyToServer(value: unknown, property: Property): u
258
258
  }
259
259
  return value;
260
260
 
261
+ case "string":
262
+ if (typeof value === "string") {
263
+ if (value.startsWith("data:application/octet-stream;base64,")) {
264
+ const base64Data = value.split(",")[1];
265
+ if (base64Data) {
266
+ return Buffer.from(base64Data, "base64");
267
+ }
268
+ }
269
+ }
270
+ return value;
271
+
261
272
  default:
273
+ if (typeof value === "string") {
274
+ if (value.startsWith("data:application/octet-stream;base64,")) {
275
+ const base64Data = value.split(",")[1];
276
+ if (base64Data) {
277
+ return Buffer.from(base64Data, "base64");
278
+ }
279
+ }
280
+ }
262
281
  return value;
263
282
  }
264
283
  }
@@ -447,6 +466,45 @@ export function parsePropertyFromServer(value: unknown, property: Property, coll
447
466
  }
448
467
 
449
468
  switch (property.type) {
469
+ case "string": {
470
+ if (typeof value === "string") return value;
471
+
472
+ // Handle Buffer objects (e.g. from PostgreSQL bytea columns)
473
+ let isBuffer = false;
474
+ let buf: Buffer | null = null;
475
+
476
+ if (Buffer.isBuffer(value)) {
477
+ isBuffer = true;
478
+ buf = value;
479
+ } else if (typeof value === "object" && value !== null && (value as any).type === "Buffer" && Array.isArray((value as any).data)) {
480
+ isBuffer = true;
481
+ buf = Buffer.from((value as any).data);
482
+ }
483
+
484
+ if (isBuffer && buf) {
485
+ // Heuristic: if all bytes are printable ASCII, return utf8, else base64
486
+ let isPrintable = true;
487
+ for (let i = 0; i < buf.length; i++) {
488
+ const b = buf[i];
489
+ // Allow standard printable ASCII + common whitespace (\r, \n, \t)
490
+ if ((b < 32 || b > 126) && b !== 9 && b !== 10 && b !== 13) {
491
+ isPrintable = false;
492
+ break;
493
+ }
494
+ }
495
+ return isPrintable ? buf.toString("utf8") : `data:application/octet-stream;base64,${buf.toString("base64")}`;
496
+ }
497
+
498
+ if (typeof value === "object" && value !== null) {
499
+ try {
500
+ return JSON.stringify(value);
501
+ } catch {
502
+ return String(value);
503
+ }
504
+ }
505
+ return String(value);
506
+ }
507
+
450
508
  case "relation":
451
509
  // Transform ID back to relation object with type information
452
510
  if (typeof value === "string" || typeof value === "number") {
@@ -538,8 +596,33 @@ export function parsePropertyFromServer(value: unknown, property: Property, coll
538
596
  return null;
539
597
  }
540
598
 
541
- default:
599
+ default: {
600
+ // Fallback for buffers in case they are mapped to something other than string
601
+ let isBuffer = false;
602
+ let buf: Buffer | null = null;
603
+
604
+ if (Buffer.isBuffer(value)) {
605
+ isBuffer = true;
606
+ buf = value;
607
+ } else if (typeof value === "object" && value !== null && (value as any).type === "Buffer" && Array.isArray((value as any).data)) {
608
+ isBuffer = true;
609
+ buf = Buffer.from((value as any).data);
610
+ }
611
+
612
+ if (isBuffer && buf) {
613
+ let isPrintable = true;
614
+ for (let i = 0; i < buf.length; i++) {
615
+ const b = buf[i];
616
+ if ((b < 32 || b > 126) && b !== 9 && b !== 10 && b !== 13) {
617
+ isPrintable = false;
618
+ break;
619
+ }
620
+ }
621
+ return isPrintable ? buf.toString("utf8") : `data:application/octet-stream;base64,${buf.toString("base64")}`;
622
+ }
623
+
542
624
  return value;
625
+ }
543
626
  }
544
627
  }
545
628
 
@@ -138,6 +138,10 @@ const getDrizzleColumn = (propName: string, prop: Property, collection: EntityCo
138
138
  } else {
139
139
  columnDefinition = `timestamp("${colName}", { withTimezone: true, mode: 'string' })`;
140
140
  }
141
+ // autoValue: database-level default for initial value on INSERT
142
+ if (dateProp.autoValue === "on_create" || dateProp.autoValue === "on_update") {
143
+ columnDefinition += `.default(sql\`now()\`)`;
144
+ }
141
145
  break;
142
146
  }
143
147
  case "map":
@@ -473,6 +477,8 @@ const computeSharedRelationName = (
473
477
  export const generateSchema = async (collections: EntityCollection[], stripPolicies = false): Promise<string> => {
474
478
  let schemaContent = "// This file is auto-generated by the Rebase Drizzle generator. Do not edit manually.\n\n";
475
479
 
480
+
481
+
476
482
  const hasUuid = collections.some(c =>
477
483
  c.properties && Object.values(c.properties).some(
478
484
  (p: Property) => p.type === "string" && ((p as unknown as Record<string, unknown>).autoValue === "uuid" || (p as unknown as Record<string, unknown>).isId === "uuid")
@@ -583,6 +589,7 @@ export const generateSchema = async (collections: EntityCollection[], stripPolic
583
589
  Object.entries(collection.properties ?? {}).forEach(([propName, prop]) => {
584
590
  const columnString = getDrizzleColumn(propName, prop as Property, collection, collections);
585
591
  if (columnString) columns.add(columnString);
592
+
586
593
  });
587
594
 
588
595
  // Backwards compatibility: if no id/primary key column is found in properties, but `id` wasn't explicitly provided
@@ -740,6 +747,33 @@ export const generateSchema = async (collections: EntityCollection[], stripPolic
740
747
  console.warn(`Could not generate relation ${relationKey} for ${collection.name}:`, e);
741
748
  }
742
749
  }
750
+
751
+ // Synthesize missing reciprocal relations
752
+ for (const otherCollection of collections) {
753
+ if (otherCollection.slug === collection.slug) continue;
754
+
755
+ const otherRelations = resolveCollectionRelations(otherCollection);
756
+ for (const [otherKey, otherRel] of Object.entries(otherRelations)) {
757
+ if (otherRel.direction === "inverse" && otherRel.foreignKeyOnTarget) {
758
+ try {
759
+ const otherTarget = otherRel.target();
760
+ if (otherTarget.slug === collection.slug) {
761
+ const drizzleRelationName = computeSharedRelationName(otherRel, otherCollection, collections);
762
+ const deduplicationKey = `${drizzleRelationName}::owning`;
763
+
764
+ if (!emittedRelationNames.has(deduplicationKey)) {
765
+ const otherTableVar = getTableVarName(getTableName(otherCollection));
766
+ const synthKey = `_synth_${otherTableVar}_${otherRel.foreignKeyOnTarget}`;
767
+ tableRelations.push(` "${synthKey}": one(${otherTableVar}, {\n fields: [${tableVarName}.${otherRel.foreignKeyOnTarget}],\n references: [${otherTableVar}.${getPrimaryKeyName(otherCollection)}],\n relationName: \"${drizzleRelationName}\"\n })`);
768
+ emittedRelationNames.add(deduplicationKey);
769
+ }
770
+ }
771
+ } catch (e) {
772
+ // ignore
773
+ }
774
+ }
775
+ }
776
+ }
743
777
  }
744
778
 
745
779
  if (tableRelations.length > 0) {
@@ -757,3 +791,4 @@ export const generateSchema = async (collections: EntityCollection[], stripPolic
757
791
 
758
792
  return schemaContent;
759
793
  };
794
+
@@ -0,0 +1,238 @@
1
+ import { humanize } from "./introspect-db-logic";
2
+
3
+ export interface InferenceResult {
4
+ propType?: string; // If the inference changes the base type
5
+ extra?: string; // String to append to the property definition
6
+ }
7
+
8
+ const ISO_8601_REGEX = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/;
9
+ const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
10
+ const CUID_REGEX = /^c[^\s-]{7,}$/i; // Basic CUID check
11
+ const COLOR_HEX_REGEX = /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/;
12
+
13
+ export function inferPropertyFromData(
14
+ columnName: string,
15
+ pgDataType: string,
16
+ currentPropType: string,
17
+ sampleValues: unknown[],
18
+ isPk: boolean
19
+ ): InferenceResult {
20
+ let result: InferenceResult = {};
21
+ let extraLines: string[] = [];
22
+
23
+ // Filter out null/undefined for analysis
24
+ const validValues = sampleValues.filter(v => v !== null && v !== undefined && v !== "");
25
+
26
+ if (validValues.length === 0) {
27
+ return result; // Not enough data
28
+ }
29
+
30
+ const colNameLower = columnName.toLowerCase();
31
+
32
+ // ── Number Analysis ──────────────────────────────────────────────────
33
+ if (currentPropType === "number") {
34
+ // Boolean stored as int
35
+ const allZeroOrOne = validValues.every(v => v === 0 || v === 1 || v === "0" || v === "1");
36
+ if (allZeroOrOne) {
37
+ result.propType = "boolean";
38
+ return result; // Don't do min/max if boolean
39
+ }
40
+
41
+ // Min / Max constraints
42
+ const numValues = validValues.map(v => Number(v)).filter(v => !isNaN(v));
43
+ if (numValues.length === validValues.length) {
44
+ const min = Math.min(...numValues);
45
+ const max = Math.max(...numValues);
46
+ // Example heuristic: percentages
47
+ if (min >= 0 && max <= 100 && (colNameLower.includes("percent") || colNameLower.includes("rate") || colNameLower.includes("score"))) {
48
+ extraLines.push(` validation: {\n min: 0,\n max: 100\n }`);
49
+ } else if (min >= 0 && (colNameLower.includes("count") || colNameLower.includes("total") || colNameLower.includes("amount"))) {
50
+ extraLines.push(` validation: {\n min: 0\n }`);
51
+ }
52
+ }
53
+
54
+ // Currency
55
+ if (colNameLower.includes("price") || colNameLower.includes("cost") || colNameLower.includes("amount") || colNameLower.includes("fee") || pgDataType === "money") {
56
+ extraLines.push(` ui: {\n currency: true\n }`);
57
+ }
58
+ }
59
+
60
+ // ── JSON / JSONB Analysis ────────────────────────────────────────────
61
+ if (currentPropType === "map" || pgDataType.includes("json")) {
62
+ // PostGres jsonb can be object or array
63
+ let allArrays = true;
64
+ let allObjects = true;
65
+
66
+ for (const v of validValues) {
67
+ let parsed = v;
68
+ if (typeof v === "string") {
69
+ try { parsed = JSON.parse(v); } catch (e) { allArrays = false; allObjects = false; break; }
70
+ }
71
+ if (Array.isArray(parsed)) {
72
+ allObjects = false;
73
+ } else if (typeof parsed === "object" && parsed !== null) {
74
+ allArrays = false;
75
+ } else {
76
+ allArrays = false;
77
+ allObjects = false;
78
+ }
79
+ }
80
+
81
+ if (allArrays && !allObjects) {
82
+ result.propType = "array";
83
+ // Infer inner type
84
+ let allNumbers = true;
85
+ let allStrings = true;
86
+ for (const v of validValues) {
87
+ let parsed = typeof v === "string" ? JSON.parse(v) : v;
88
+ for (const item of parsed) {
89
+ if (typeof item !== "number") allNumbers = false;
90
+ if (typeof item !== "string") allStrings = false;
91
+ }
92
+ }
93
+ const innerType = allNumbers ? "number" : allStrings ? "string" : "map";
94
+ extraLines.push(` of: { name: "${humanize(columnName)} Item", type: "${innerType}" }`);
95
+ } else {
96
+ result.propType = "map";
97
+
98
+ // Infer inner schema
99
+ if (allObjects && validValues.length > 0) {
100
+ const schema: Record<string, string> = {};
101
+ for (const v of validValues) {
102
+ let parsed = typeof v === "string" ? JSON.parse(v) : v;
103
+ for (const [k, val] of Object.entries(parsed)) {
104
+ if (val === null || val === undefined) continue;
105
+ const type = typeof val;
106
+ if (type === "string" || type === "number" || type === "boolean") {
107
+ if (!schema[k]) schema[k] = type;
108
+ else if (schema[k] !== type) schema[k] = "mixed";
109
+ } else if (Array.isArray(val)) {
110
+ if (!schema[k]) schema[k] = "array";
111
+ else if (schema[k] !== "array") schema[k] = "mixed";
112
+ } else if (type === "object") {
113
+ if (!schema[k]) schema[k] = "map";
114
+ else if (schema[k] !== "map") schema[k] = "mixed";
115
+ }
116
+ }
117
+ }
118
+
119
+ const keys = Object.keys(schema).filter(k => schema[k] !== "mixed");
120
+ if (keys.length > 0) {
121
+ const props = keys.map(k => {
122
+ return `\n ${k}: { name: "${humanize(k)}", type: "${schema[k]}" }`;
123
+ }).join(",");
124
+ extraLines.push(` properties: {${props}\n }`);
125
+ } else {
126
+ extraLines.push(` keyValue: true`);
127
+ }
128
+ } else {
129
+ extraLines.push(` keyValue: true`);
130
+ }
131
+ }
132
+ }
133
+
134
+ // ── String Analysis ──────────────────────────────────────────────────
135
+ if (currentPropType === "string") {
136
+ // Date/Time Strings
137
+ if (validValues.every(v => typeof v === 'string' && ISO_8601_REGEX.test(v))) {
138
+ result.propType = "date";
139
+ return result;
140
+ }
141
+
142
+ // Implicit Enums
143
+ const uniqueValues = new Set(validValues);
144
+ // Determine the maximum length among unique values
145
+ let maxEnumLength = 0;
146
+ for (const v of uniqueValues) {
147
+ if (typeof v === "string" && v.length > maxEnumLength) {
148
+ maxEnumLength = v.length;
149
+ }
150
+ }
151
+
152
+ // Ensure no empty string, max length makes sense, and fewer unique values than total values (unless small total)
153
+ if (uniqueValues.size > 0 && uniqueValues.size <= 5 && maxEnumLength <= 50 && validValues.length > uniqueValues.size && !uniqueValues.has("")) {
154
+ const isLikelyId = isPk || colNameLower.endsWith("_id");
155
+ if (!isLikelyId) {
156
+ const enumEntries = Array.from(uniqueValues).map(v => `{ id: ${JSON.stringify(v)}, label: ${JSON.stringify(humanize(v as string))} }`).join(", ");
157
+ extraLines.push(` enum: [${enumEntries}]`);
158
+ result.extra = extraLines.length > 0 ? "\n" + extraLines.join(",\n") + "," : "";
159
+ return result; // Skip other string checks if it's an enum
160
+ }
161
+ }
162
+
163
+ // UUID / CUID Detection
164
+ const allUuid = validValues.every(v => typeof v === 'string' && UUID_REGEX.test(v));
165
+ const allCuid = validValues.every(v => typeof v === 'string' && CUID_REGEX.test(v));
166
+ if (allUuid) {
167
+ if (isPk) extraLines.push(` isId: "uuid"`);
168
+ } else if (allCuid) {
169
+ if (isPk) extraLines.push(` isId: "cuid"`);
170
+ }
171
+
172
+ // Color Codes
173
+ const allColors = validValues.every(v => typeof v === 'string' && COLOR_HEX_REGEX.test(v));
174
+ if (allColors) {
175
+ extraLines.push(` ui: {\n color: true\n }`);
176
+ }
177
+
178
+ // Text Lengths, Multiline & Markdown
179
+ let maxLength = 0;
180
+ let hasNewlines = false;
181
+ let hasMarkdown = false;
182
+ for (const v of validValues) {
183
+ if (typeof v === "string") {
184
+ if (v.length > maxLength) maxLength = v.length;
185
+ if (v.includes("\n")) hasNewlines = true;
186
+ if (/^#{1,6}\s+.+/m.test(v) || /\*\*.+\*\*/.test(v) || /\[.+\]\(.+\)/.test(v) || /^\s*[-*]\s+.+/m.test(v)) {
187
+ hasMarkdown = true;
188
+ }
189
+ }
190
+ }
191
+
192
+ if (hasMarkdown) {
193
+ extraLines.push(` multiline: true,\n markdown: true`);
194
+ } else if (hasNewlines || maxLength > 100) {
195
+ extraLines.push(` multiline: true`);
196
+ }
197
+
198
+ if (maxLength > 0 && maxLength < 10000) { // arbitrary cap to avoid huge limits
199
+ // Pad the max length slightly for the constraint
200
+ const paddedMax = Math.ceil((maxLength * 1.5) / 10) * 10;
201
+ // Only add length constraint if it seems like a bounded string, not a long text
202
+ if (paddedMax < 255) {
203
+ extraLines.push(` validation: {\n max: ${paddedMax}\n }`);
204
+ }
205
+ }
206
+
207
+ // Storage & Media (Extracted from old logic)
208
+ const isUrl = colNameLower.endsWith("_url") || colNameLower.endsWith("_uri") || colNameLower.endsWith("_link");
209
+ const isMedia = colNameLower.includes("image") || colNameLower.includes("avatar") || colNameLower.includes("photo") || colNameLower.includes("logo") || colNameLower.includes("cover");
210
+
211
+ const allAbsoluteUrls = validValues.every(v => typeof v === 'string' && (v.startsWith("http://") || v.startsWith("https://")));
212
+ if (allAbsoluteUrls) {
213
+ const isImage = validValues.some(v => typeof v === 'string' && v.match(/\.(jpeg|jpg|gif|png|webp|svg)/i));
214
+ if (isImage || isMedia) {
215
+ extraLines.push(` ui: {\n url: "image"\n }`);
216
+ } else {
217
+ extraLines.push(` ui: {\n url: true\n }`);
218
+ }
219
+ } else {
220
+ const hasFileExtension = validValues.some(v => typeof v === 'string' && v.match(/\.[a-zA-Z0-9]+$/));
221
+ if (hasFileExtension) {
222
+ const firstVal = validValues[0] as string;
223
+ const lastSlash = firstVal.lastIndexOf('/');
224
+ let inferredStoragePath = lastSlash > 0 ? firstVal.substring(0, lastSlash) : "files";
225
+ extraLines.push(` storage: {\n storagePath: "${inferredStoragePath}"\n }`);
226
+ } else if (isUrl) {
227
+ if (isMedia) {
228
+ extraLines.push(` ui: {\n url: "image"\n }`);
229
+ } else {
230
+ extraLines.push(` ui: {\n url: true\n }`);
231
+ }
232
+ }
233
+ }
234
+ }
235
+
236
+ result.extra = extraLines.length > 0 ? "\n" + extraLines.join(",\n") + "," : "";
237
+ return result;
238
+ }