@rebasepro/server-postgresql 0.0.1-canary.c53f5db → 0.0.1-canary.cbdd980
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/dist/index.es.js +384 -1100
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +315 -1031
- package/dist/index.umd.js.map +1 -1
- package/dist/server-postgresql/src/PostgresBackendDriver.d.ts +2 -1
- package/dist/server-postgresql/src/schema/introspect-db-inference.d.ts +5 -0
- package/dist/server-postgresql/src/schema/introspect-db-logic.d.ts +44 -9
- package/dist/server-postgresql/src/services/EntityPersistService.d.ts +9 -0
- package/dist/types/src/controllers/auth.d.ts +8 -2
- package/dist/types/src/controllers/client.d.ts +13 -0
- package/dist/types/src/controllers/navigation.d.ts +18 -6
- package/dist/types/src/controllers/registry.d.ts +9 -1
- package/dist/types/src/controllers/side_entity_controller.d.ts +7 -0
- package/dist/types/src/rebase_context.d.ts +17 -0
- package/dist/types/src/types/collections.d.ts +21 -1
- package/dist/types/src/types/component_ref.d.ts +47 -0
- package/dist/types/src/types/cron.d.ts +1 -1
- package/dist/types/src/types/entity_views.d.ts +2 -1
- package/dist/types/src/types/index.d.ts +1 -0
- package/dist/types/src/types/properties.d.ts +68 -84
- package/dist/types/src/types/translations.d.ts +2 -0
- package/package.json +5 -5
- package/src/PostgresBackendDriver.ts +23 -6
- package/src/cli.ts +10 -2
- package/src/data-transformer.ts +84 -1
- package/src/schema/doctor.ts +14 -2
- package/src/schema/generate-drizzle-schema-logic.ts +59 -30
- package/src/schema/introspect-db-inference.ts +238 -0
- package/src/schema/introspect-db-logic.ts +365 -61
- package/src/schema/introspect-db.ts +66 -23
- package/src/services/EntityFetchService.ts +16 -0
- package/src/services/EntityPersistService.ts +88 -12
- package/test/generate-drizzle-schema.test.ts +295 -0
- package/test/introspect-db-generation.test.ts +32 -10
- package/test/property-ordering.test.ts +395 -0
- package/test/relations.test.ts +4 -4
- package/jest-all.log +0 -3128
- package/jest.log +0 -49
- package/scratch.ts +0 -41
- package/test-drizzle-bug.ts +0 -18
- package/test-drizzle-out/0000_cultured_freak.sql +0 -7
- package/test-drizzle-out/0001_tiresome_professor_monster.sql +0 -1
- package/test-drizzle-out/meta/0000_snapshot.json +0 -55
- package/test-drizzle-out/meta/0001_snapshot.json +0 -63
- package/test-drizzle-out/meta/_journal.json +0 -20
- package/test-drizzle-prompt.sh +0 -2
- package/test-policy-prompt.sh +0 -3
- package/test-programmatic.ts +0 -30
- package/test-programmatic2.ts +0 -59
- package/test-schema-no-policies.ts +0 -12
- package/test_drizzle_mock.js +0 -3
- package/test_find_changed.mjs +0 -32
- package/test_hash.js +0 -14
- package/test_output.txt +0 -3145
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* no process.exit. It is imported by introspect-db.ts (the CLI entry-point)
|
|
7
7
|
* and consumed directly by tests.
|
|
8
8
|
*/
|
|
9
|
+
import { inferPropertyFromData } from "./introspect-db-inference";
|
|
9
10
|
|
|
10
11
|
// ── Typed interfaces for SQL query results ────────────────────────────
|
|
11
12
|
|
|
@@ -254,6 +255,252 @@ export function identifyJoinTables(tablesMap: Map<string, TableMeta>): Set<strin
|
|
|
254
255
|
return joinTables;
|
|
255
256
|
}
|
|
256
257
|
|
|
258
|
+
// ── Property ordering heuristics ──────────────────────────────────────
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Property metadata used to compute display priority.
|
|
262
|
+
* Keeps computePropertyPriority free of any TableMeta coupling.
|
|
263
|
+
*/
|
|
264
|
+
export interface PropertyOrderingContext {
|
|
265
|
+
/** The resolved Rebase property type (e.g. "string", "number", "date", "relation"). */
|
|
266
|
+
propType: string;
|
|
267
|
+
/** Whether this column is a primary key. */
|
|
268
|
+
isPk: boolean;
|
|
269
|
+
/** Whether this column is an enum (USER-DEFINED with matching values). */
|
|
270
|
+
isEnum: boolean;
|
|
271
|
+
/** Whether this is a storage/file-upload field (detected from column name). */
|
|
272
|
+
isStorage: boolean;
|
|
273
|
+
/** The PostgreSQL data_type (e.g. "text", "character varying", "jsonb"). */
|
|
274
|
+
pgDataType: string;
|
|
275
|
+
/** The original column index in PostgreSQL (for stable tiebreaking). */
|
|
276
|
+
originalIndex: number;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// — Tier 0: Identity (0–9) ————————————————————————————————————————————
|
|
280
|
+
const IDENTITY_EXACT: Record<string, number> = {
|
|
281
|
+
id: 0,
|
|
282
|
+
uuid: 1,
|
|
283
|
+
_id: 2,
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
// — Tier 1: Title / Name — the "display column" (10–19) ———————————————
|
|
287
|
+
const TITLE_EXACT: Record<string, number> = {
|
|
288
|
+
name: 10,
|
|
289
|
+
title: 11,
|
|
290
|
+
label: 12,
|
|
291
|
+
display_name: 13,
|
|
292
|
+
displayname: 13,
|
|
293
|
+
headline: 14,
|
|
294
|
+
subject: 15,
|
|
295
|
+
heading: 16,
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
// — Tier 2: Human identity fields (20–29) —————————————————————————————
|
|
299
|
+
const HUMAN_IDENTITY_EXACT: Record<string, number> = {
|
|
300
|
+
first_name: 20,
|
|
301
|
+
firstname: 20,
|
|
302
|
+
last_name: 21,
|
|
303
|
+
lastname: 21,
|
|
304
|
+
full_name: 22,
|
|
305
|
+
fullname: 22,
|
|
306
|
+
given_name: 22,
|
|
307
|
+
family_name: 23,
|
|
308
|
+
middle_name: 24,
|
|
309
|
+
username: 25,
|
|
310
|
+
user_name: 25,
|
|
311
|
+
email: 26,
|
|
312
|
+
email_address: 26,
|
|
313
|
+
phone: 27,
|
|
314
|
+
phone_number: 27,
|
|
315
|
+
mobile: 27,
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
// — Tier 3: Core descriptors (30–39) ——————————————————————————————————
|
|
319
|
+
const DESCRIPTOR_EXACT: Record<string, number> = {
|
|
320
|
+
slug: 30,
|
|
321
|
+
code: 31,
|
|
322
|
+
sku: 32,
|
|
323
|
+
reference: 33,
|
|
324
|
+
ref: 33,
|
|
325
|
+
type: 34,
|
|
326
|
+
kind: 34,
|
|
327
|
+
status: 35,
|
|
328
|
+
state: 35,
|
|
329
|
+
role: 36,
|
|
330
|
+
category: 37,
|
|
331
|
+
group: 38,
|
|
332
|
+
priority: 39,
|
|
333
|
+
order: 39,
|
|
334
|
+
sort_order: 39,
|
|
335
|
+
position: 39,
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
// — Tier 12: System timestamps (120–129) ——————————————————————————————
|
|
339
|
+
const SYSTEM_TIMESTAMP_EXACT: Record<string, number> = {
|
|
340
|
+
created_at: 120,
|
|
341
|
+
createdat: 120,
|
|
342
|
+
creation_date: 120,
|
|
343
|
+
inserted_at: 121,
|
|
344
|
+
updated_at: 122,
|
|
345
|
+
updatedat: 122,
|
|
346
|
+
modified_at: 122,
|
|
347
|
+
last_modified: 122,
|
|
348
|
+
deleted_at: 123,
|
|
349
|
+
deletedat: 123,
|
|
350
|
+
archived_at: 124,
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
// — Pattern-based rules for partial matches ———————————————————————————
|
|
354
|
+
const TITLE_PATTERNS = ["name", "title", "label"];
|
|
355
|
+
const LONG_TEXT_NAMES = new Set(["description", "summary", "excerpt", "abstract", "overview", "bio", "biography", "about"]);
|
|
356
|
+
const RICH_CONTENT_NAMES = new Set(["content", "body", "html", "markup", "text", "article_body", "post_body"]);
|
|
357
|
+
const MEDIA_PATTERNS = ["image", "avatar", "photo", "logo", "cover", "thumbnail", "banner", "icon", "picture", "poster"];
|
|
358
|
+
const JSON_MAP_NAMES = new Set(["metadata", "meta", "config", "configuration", "settings", "options", "preferences", "data", "payload", "attributes", "extra", "additional_info"]);
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Compute a numeric priority score for a property.
|
|
362
|
+
* Lower scores appear first in the generated `propertiesOrder` array.
|
|
363
|
+
*
|
|
364
|
+
* The system uses 14 tiers (0–139), with the original column index
|
|
365
|
+
* added as a fractional tiebreaker (originalIndex / 10000) to
|
|
366
|
+
* guarantee stable ordering within the same tier.
|
|
367
|
+
*
|
|
368
|
+
* Pure function — no side effects.
|
|
369
|
+
*/
|
|
370
|
+
export function computePropertyPriority(
|
|
371
|
+
columnName: string,
|
|
372
|
+
ctx: PropertyOrderingContext,
|
|
373
|
+
): number {
|
|
374
|
+
// Normalize camelCase/PascalCase to snake_case, then lowercase
|
|
375
|
+
const col = columnName.replace(/([a-z0-9])([A-Z])/g, "$1_$2").toLowerCase();
|
|
376
|
+
const tiebreaker = ctx.originalIndex / 10000;
|
|
377
|
+
|
|
378
|
+
// ── Tier 0: Primary key identity fields
|
|
379
|
+
if (ctx.isPk) {
|
|
380
|
+
const exactScore = IDENTITY_EXACT[col];
|
|
381
|
+
return (exactScore ?? 5) + tiebreaker;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// ── Tier 12: System timestamps (check early to prevent false matches)
|
|
385
|
+
const systemTs = SYSTEM_TIMESTAMP_EXACT[col];
|
|
386
|
+
if (systemTs !== undefined) {
|
|
387
|
+
return systemTs + tiebreaker;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// ── Tier 1: Title / Name exact matches
|
|
391
|
+
const titleExact = TITLE_EXACT[col];
|
|
392
|
+
if (titleExact !== undefined) {
|
|
393
|
+
return titleExact + tiebreaker;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// ── Tier 2: Human identity exact matches
|
|
397
|
+
const humanExact = HUMAN_IDENTITY_EXACT[col];
|
|
398
|
+
if (humanExact !== undefined) {
|
|
399
|
+
return humanExact + tiebreaker;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// ── Tier 3: Core descriptor exact matches
|
|
403
|
+
const descriptorExact = DESCRIPTOR_EXACT[col];
|
|
404
|
+
if (descriptorExact !== undefined) {
|
|
405
|
+
return descriptorExact + tiebreaker;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// ── Tier 1b: Title-like partial matches (e.g. "product_name", "page_title")
|
|
409
|
+
// Score 17–19 so they rank after exact matches but still in tier 1.
|
|
410
|
+
for (const pattern of TITLE_PATTERNS) {
|
|
411
|
+
if (col.includes(pattern) && col !== pattern) {
|
|
412
|
+
return 17 + tiebreaker;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// ── Tier 9: Media / file upload fields (check before general strings)
|
|
417
|
+
if (ctx.isStorage) {
|
|
418
|
+
return 90 + tiebreaker;
|
|
419
|
+
}
|
|
420
|
+
for (const pattern of MEDIA_PATTERNS) {
|
|
421
|
+
if (col.includes(pattern)) {
|
|
422
|
+
return 91 + tiebreaker;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
if (col.endsWith("_url") || col.endsWith("_uri") || col.endsWith("_link")) {
|
|
426
|
+
return 92 + tiebreaker;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// ── Tier 7: Long text fields
|
|
430
|
+
if (LONG_TEXT_NAMES.has(col)) {
|
|
431
|
+
return 70 + tiebreaker;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// ── Tier 8: Rich content fields
|
|
435
|
+
if (RICH_CONTENT_NAMES.has(col)) {
|
|
436
|
+
return 80 + tiebreaker;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// ── Tier 10: JSON / Map types
|
|
440
|
+
if (ctx.propType === "map") {
|
|
441
|
+
return JSON_MAP_NAMES.has(col) ? 100 + tiebreaker : 105 + tiebreaker;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// ── Tier 11: Array types
|
|
445
|
+
if (ctx.propType === "array") {
|
|
446
|
+
return 110 + tiebreaker;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// ── Tier 6: Owning relations
|
|
450
|
+
if (ctx.propType === "relation") {
|
|
451
|
+
return 60 + tiebreaker;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// ── Tier 4: Short text, enums, booleans — "quick glance" fields
|
|
455
|
+
if (ctx.isEnum) {
|
|
456
|
+
return 40 + tiebreaker;
|
|
457
|
+
}
|
|
458
|
+
if (ctx.propType === "boolean") {
|
|
459
|
+
return 45 + tiebreaker;
|
|
460
|
+
}
|
|
461
|
+
if (ctx.propType === "string" && ctx.pgDataType !== "text") {
|
|
462
|
+
// Short string (varchar, char, uuid that's not a PK)
|
|
463
|
+
return 42 + tiebreaker;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// ── Tier 5: Numbers & user-facing dates
|
|
467
|
+
if (ctx.propType === "number") {
|
|
468
|
+
return 50 + tiebreaker;
|
|
469
|
+
}
|
|
470
|
+
if (ctx.propType === "date") {
|
|
471
|
+
// A date that isn't a system timestamp (already handled above)
|
|
472
|
+
return 55 + tiebreaker;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// ── Tier 7b: text data_type that didn't match long-text names
|
|
476
|
+
if (ctx.propType === "string" && ctx.pgDataType === "text") {
|
|
477
|
+
return 75 + tiebreaker;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// ── Tier 13: Fallback / unknown
|
|
481
|
+
return 130 + tiebreaker;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* Sort a `propertiesOrder` array using the priority heuristic.
|
|
486
|
+
* Returns a new sorted array; does not mutate the input.
|
|
487
|
+
*
|
|
488
|
+
* @param entries - Array of { key, columnName, propType, ... } objects
|
|
489
|
+
* carrying the information needed to compute priority.
|
|
490
|
+
*/
|
|
491
|
+
export interface PropertyOrderEntry {
|
|
492
|
+
/** The property key in the generated collection (may differ from columnName for relations). */
|
|
493
|
+
key: string;
|
|
494
|
+
/** The ordering context for this property. */
|
|
495
|
+
ctx: PropertyOrderingContext;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
export function sortPropertiesOrder(entries: PropertyOrderEntry[]): string[] {
|
|
499
|
+
return [...entries]
|
|
500
|
+
.sort((a, b) => computePropertyPriority(a.key, a.ctx) - computePropertyPriority(b.key, b.ctx))
|
|
501
|
+
.map((e) => e.key);
|
|
502
|
+
}
|
|
503
|
+
|
|
257
504
|
// ── Generate collection file content ──────────────────────────────────
|
|
258
505
|
|
|
259
506
|
export interface GeneratedFile {
|
|
@@ -273,6 +520,7 @@ export function generateCollectionFile(
|
|
|
273
520
|
joinTables: Set<string>,
|
|
274
521
|
tablesMap: Map<string, TableMeta>,
|
|
275
522
|
enumMap: Map<string, string[]>,
|
|
523
|
+
sampleData?: Record<string, unknown>[],
|
|
276
524
|
): string {
|
|
277
525
|
const collectionName = humanize(tableName);
|
|
278
526
|
const singular = singularize(collectionName);
|
|
@@ -281,7 +529,10 @@ export function generateCollectionFile(
|
|
|
281
529
|
const imports = new Set<string>(['import { PostgresCollection } from "@rebasepro/types";']);
|
|
282
530
|
|
|
283
531
|
let propsOutput = ``;
|
|
284
|
-
|
|
532
|
+
let relationsOutput = ``;
|
|
533
|
+
const orderEntries: PropertyOrderEntry[] = [];
|
|
534
|
+
const propertyBlocks = new Map<string, string>();
|
|
535
|
+
let columnIndex = 0;
|
|
285
536
|
|
|
286
537
|
// Detect composite primary keys
|
|
287
538
|
const isCompositePk = meta.pks.length > 1;
|
|
@@ -289,9 +540,10 @@ export function generateCollectionFile(
|
|
|
289
540
|
// Map columns
|
|
290
541
|
for (const col of meta.columns) {
|
|
291
542
|
// Skip foreign keys since we handle them as relations
|
|
292
|
-
if
|
|
543
|
+
// Exception: Do not skip if it's part of the primary key!
|
|
544
|
+
if (meta.fks.some((fk) => fk.column_name === col.column_name) && !meta.pks.includes(col.column_name)) continue;
|
|
293
545
|
|
|
294
|
-
|
|
546
|
+
const currentIndex = columnIndex++;
|
|
295
547
|
|
|
296
548
|
// Check if this column uses a PostgreSQL enum type
|
|
297
549
|
const colEnumValues = enumMap.get(col.udt_name);
|
|
@@ -302,92 +554,144 @@ export function generateCollectionFile(
|
|
|
302
554
|
|
|
303
555
|
const colNameLower = col.column_name.toLowerCase();
|
|
304
556
|
|
|
305
|
-
//
|
|
557
|
+
// ── Data Inference Engine ────────────────────────────────────────────
|
|
558
|
+
let finalPropType = propType;
|
|
559
|
+
let inferenceExtra = "";
|
|
560
|
+
|
|
561
|
+
if (!isEnumColumn && sampleData && sampleData.length > 0) {
|
|
562
|
+
const values = sampleData.map(r => r[col.column_name]);
|
|
563
|
+
const inferred = inferPropertyFromData(col.column_name, col.data_type, propType, values, meta.pks.includes(col.column_name));
|
|
564
|
+
if (inferred.propType) finalPropType = inferred.propType;
|
|
565
|
+
if (inferred.extra) inferenceExtra = inferred.extra;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// Enum values — generate real enum from the PG enum
|
|
306
569
|
if (isEnumColumn && colEnumValues) {
|
|
307
570
|
const enumEntries = colEnumValues
|
|
308
571
|
.map((v) => `{ id: "${v}", label: "${humanize(v)}" }`)
|
|
309
572
|
.join(", ");
|
|
310
|
-
extra
|
|
573
|
+
extra += `\n enum: [${enumEntries}],`;
|
|
311
574
|
}
|
|
312
575
|
|
|
313
576
|
// Date auto-value heuristics
|
|
314
|
-
if (
|
|
577
|
+
if (finalPropType === "date") {
|
|
315
578
|
if (colNameLower === "created_at" || colNameLower === "createdat") {
|
|
316
|
-
extra
|
|
579
|
+
extra += `\n autoValue: "on_create",\n ui: {\n readOnly: true,\n hideFromCollection: true\n },`;
|
|
317
580
|
} else if (colNameLower === "updated_at" || colNameLower === "updatedat") {
|
|
318
|
-
extra
|
|
581
|
+
extra += `\n autoValue: "on_update",\n ui: {\n readOnly: true,\n hideFromCollection: true\n },`;
|
|
319
582
|
} else if (col.column_default && (col.column_default.includes("now()") || col.column_default.includes("CURRENT_TIMESTAMP"))) {
|
|
320
|
-
extra
|
|
583
|
+
extra += `\n autoValue: "on_create",\n ui: {\n readOnly: true\n },`;
|
|
321
584
|
}
|
|
322
585
|
}
|
|
323
586
|
|
|
324
|
-
// Array/Map heuristics
|
|
325
|
-
if (
|
|
587
|
+
// Array/Map heuristics (Fallback if not inferred)
|
|
588
|
+
if (finalPropType === "array" && !inferenceExtra.includes("of: {")) {
|
|
326
589
|
let innerType = "string";
|
|
327
590
|
if (col.udt_name.startsWith("_")) {
|
|
328
591
|
const baseType = col.udt_name.substring(1);
|
|
329
|
-
// Simple recursive check or hardcoded for inner type:
|
|
330
|
-
// We'll just call mapPgType on the baseType
|
|
331
592
|
innerType = mapPgType(baseType);
|
|
332
593
|
}
|
|
333
|
-
extra
|
|
334
|
-
} else if (
|
|
335
|
-
extra
|
|
594
|
+
extra += `\n of: { name: "${humanize(col.column_name)} Item", type: "${innerType}" },`;
|
|
595
|
+
} else if (finalPropType === "map" && !inferenceExtra.includes("keyValue: true") && !inferenceExtra.includes("properties: {")) {
|
|
596
|
+
extra += `\n keyValue: true,`;
|
|
336
597
|
}
|
|
337
598
|
|
|
338
|
-
// String sub-type heuristics (
|
|
339
|
-
if (
|
|
340
|
-
|
|
341
|
-
|
|
599
|
+
// String sub-type heuristics (Fallback if not handled by inference or enum)
|
|
600
|
+
if (finalPropType === "string" && !isEnumColumn && !inferenceExtra) {
|
|
601
|
+
const isUrl = colNameLower.endsWith("_url") || colNameLower.endsWith("_uri") || colNameLower.endsWith("_link");
|
|
602
|
+
const isMedia = colNameLower.includes("image") || colNameLower.includes("avatar") || colNameLower.includes("photo") || colNameLower.includes("logo") || colNameLower.includes("cover");
|
|
603
|
+
|
|
604
|
+
if (isMedia) {
|
|
605
|
+
extra += `\n storage: {\n storagePath: "${tableName}/${col.column_name}"\n },`;
|
|
606
|
+
} else if (isUrl) {
|
|
607
|
+
extra += `\n ui: {\n url: true\n },`;
|
|
342
608
|
} else if (colNameLower === "description" || colNameLower === "summary" || colNameLower === "excerpt") {
|
|
343
|
-
extra
|
|
609
|
+
extra += `\n multiline: true,`;
|
|
344
610
|
} else if (colNameLower === "content" || colNameLower === "body") {
|
|
345
|
-
extra
|
|
611
|
+
extra += `\n multiline: true,\n markdown: true,`;
|
|
346
612
|
} else if (col.data_type === "text") {
|
|
347
|
-
extra
|
|
613
|
+
extra += `\n multiline: true,`;
|
|
348
614
|
}
|
|
349
615
|
}
|
|
616
|
+
|
|
617
|
+
// Append inference results
|
|
618
|
+
if (inferenceExtra) {
|
|
619
|
+
extra += inferenceExtra;
|
|
620
|
+
if (!extra.endsWith(",")) extra += ",";
|
|
621
|
+
}
|
|
350
622
|
|
|
351
|
-
// Identify IDs
|
|
623
|
+
// Identify IDs (unless already inferred as UUID/CUID by inferenceEngine)
|
|
352
624
|
if (meta.pks.includes(col.column_name)) {
|
|
353
625
|
if (isCompositePk) {
|
|
354
626
|
extra += `\n // Part of composite primary key (${meta.pks.join(", ")})`;
|
|
355
|
-
} else if (
|
|
627
|
+
} else if (finalPropType === "number" && !inferenceExtra.includes("isId:")) {
|
|
356
628
|
extra += `\n isId: "increment",`;
|
|
357
|
-
} else if (col.data_type.toLowerCase() === "uuid") {
|
|
629
|
+
} else if (col.data_type.toLowerCase() === "uuid" && !inferenceExtra.includes("isId:")) {
|
|
358
630
|
extra += `\n isId: "uuid",`;
|
|
359
|
-
} else {
|
|
631
|
+
} else if (!inferenceExtra.includes("isId:")) {
|
|
360
632
|
extra += `\n isId: "uuid", // Verify if this is a UUID or CUID`;
|
|
361
633
|
}
|
|
362
634
|
}
|
|
363
635
|
|
|
364
636
|
if (col.is_nullable === "NO" && !meta.pks.includes(col.column_name) && !col.column_default) {
|
|
365
|
-
extra
|
|
637
|
+
if (extra.includes("validation: {")) {
|
|
638
|
+
extra = extra.replace("validation: {", "validation: {\n required: true,");
|
|
639
|
+
} else {
|
|
640
|
+
extra += `\n validation: {\n required: true\n },`;
|
|
641
|
+
}
|
|
366
642
|
}
|
|
367
643
|
|
|
368
644
|
const humanName = humanize(col.column_name);
|
|
369
645
|
|
|
370
|
-
|
|
646
|
+
orderEntries.push({
|
|
647
|
+
key: col.column_name,
|
|
648
|
+
ctx: {
|
|
649
|
+
propType: finalPropType,
|
|
650
|
+
isPk: meta.pks.includes(col.column_name),
|
|
651
|
+
isEnum: isEnumColumn,
|
|
652
|
+
isStorage: extra.includes("storage: {") || inferenceExtra.includes("storage: {"),
|
|
653
|
+
pgDataType: col.data_type,
|
|
654
|
+
originalIndex: currentIndex,
|
|
655
|
+
},
|
|
656
|
+
});
|
|
657
|
+
|
|
658
|
+
propertyBlocks.set(col.column_name, `
|
|
371
659
|
${col.column_name}: {
|
|
372
660
|
name: "${humanName}",
|
|
373
|
-
|
|
374
|
-
|
|
661
|
+
columnName: "${col.column_name}",
|
|
662
|
+
type: "${finalPropType}",${extra}
|
|
663
|
+
},`);
|
|
375
664
|
}
|
|
376
665
|
|
|
377
666
|
// Map Owning Relations (from this table's FKs to other tables)
|
|
378
667
|
for (const fk of meta.fks) {
|
|
379
668
|
const targetTableName = fk.foreign_table_name;
|
|
380
669
|
if (!joinTables.has(targetTableName)) {
|
|
381
|
-
|
|
670
|
+
let relName = fk.column_name.replace(/_id$/, "");
|
|
671
|
+
if (meta.pks.includes(fk.column_name) && relName === fk.column_name) {
|
|
672
|
+
// If the FK is also the PK and its name doesn't imply a relation (like "id"),
|
|
673
|
+
// use the target table name to avoid conflicting with the PK property.
|
|
674
|
+
relName = targetTableName;
|
|
675
|
+
}
|
|
382
676
|
// Push the relation property key, not the FK column name
|
|
383
|
-
|
|
677
|
+
orderEntries.push({
|
|
678
|
+
key: relName,
|
|
679
|
+
ctx: {
|
|
680
|
+
propType: "relation",
|
|
681
|
+
isPk: false,
|
|
682
|
+
isEnum: false,
|
|
683
|
+
isStorage: false,
|
|
684
|
+
pgDataType: "",
|
|
685
|
+
originalIndex: columnIndex++,
|
|
686
|
+
},
|
|
687
|
+
});
|
|
384
688
|
|
|
385
689
|
const targetCollectionCamel = toCollectionVarName(targetTableName);
|
|
386
690
|
imports.add(`import ${targetCollectionCamel} from "./${targetTableName}";`);
|
|
387
691
|
|
|
388
692
|
const relHumanName = humanize(relName);
|
|
389
693
|
|
|
390
|
-
|
|
694
|
+
propertyBlocks.set(relName, `
|
|
391
695
|
${relName}: {
|
|
392
696
|
name: "${relHumanName}",
|
|
393
697
|
type: "relation",
|
|
@@ -396,26 +700,24 @@ export function generateCollectionFile(
|
|
|
396
700
|
direction: "owning",
|
|
397
701
|
localKey: "${fk.column_name}",
|
|
398
702
|
// mapped from foreign key: ${fk.column_name} -> ${targetTableName}(${fk.foreign_column_name})
|
|
399
|
-
}
|
|
703
|
+
},`);
|
|
400
704
|
}
|
|
401
705
|
}
|
|
402
706
|
|
|
403
707
|
// Map Inverse Relations (1-to-many where OTHER table points to THIS table)
|
|
708
|
+
// These go into the `relations` array so they render as subcollection tabs.
|
|
404
709
|
const inverseFks = allFks.filter((fk) => fk.foreign_table_name === tableName && !joinTables.has(fk.table_name));
|
|
405
710
|
for (const fk of inverseFks) {
|
|
406
711
|
const sourceTableName = fk.table_name;
|
|
407
|
-
propertiesOrder.push(sourceTableName);
|
|
408
712
|
|
|
409
713
|
const targetCollectionCamel = toCollectionVarName(sourceTableName);
|
|
410
714
|
imports.add(`import ${targetCollectionCamel} from "./${sourceTableName}";`);
|
|
411
715
|
|
|
412
716
|
const inverseRelName = fk.column_name.replace(/_id$/, "");
|
|
413
|
-
const relHumanName = humanize(sourceTableName);
|
|
414
717
|
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
type: "relation",
|
|
718
|
+
relationsOutput += `
|
|
719
|
+
{
|
|
720
|
+
relationName: "${sourceTableName}",
|
|
419
721
|
target: () => ${targetCollectionCamel},
|
|
420
722
|
cardinality: "many",
|
|
421
723
|
direction: "inverse",
|
|
@@ -425,6 +727,7 @@ export function generateCollectionFile(
|
|
|
425
727
|
}
|
|
426
728
|
|
|
427
729
|
// Map Many-to-Many Relations (Join Tables)
|
|
730
|
+
// These also go into the `relations` array so they render as subcollection tabs.
|
|
428
731
|
const relatedJoinTables = Array.from(joinTables).filter((jt) => {
|
|
429
732
|
const jtMeta = tablesMap.get(jt);
|
|
430
733
|
return jtMeta ? jtMeta.fks.some((fk) => fk.foreign_table_name === tableName) : false;
|
|
@@ -444,15 +747,10 @@ export function generateCollectionFile(
|
|
|
444
747
|
const otherFk = selfRefFks[1];
|
|
445
748
|
|
|
446
749
|
const relPropName = `${tableName}_via_${otherFk.column_name.replace(/_id$/, "")}`;
|
|
447
|
-
propertiesOrder.push(relPropName);
|
|
448
750
|
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
propsOutput += `
|
|
453
|
-
${relPropName}: {
|
|
454
|
-
name: "${relHumanName}",
|
|
455
|
-
type: "relation",
|
|
751
|
+
relationsOutput += `
|
|
752
|
+
{
|
|
753
|
+
relationName: "${relPropName}",
|
|
456
754
|
target: () => ${tableName}Collection,
|
|
457
755
|
cardinality: "many",
|
|
458
756
|
direction: "owning",
|
|
@@ -469,7 +767,6 @@ export function generateCollectionFile(
|
|
|
469
767
|
|
|
470
768
|
if (otherFk) {
|
|
471
769
|
const targetTableName = otherFk.foreign_table_name;
|
|
472
|
-
propertiesOrder.push(targetTableName);
|
|
473
770
|
|
|
474
771
|
const targetCollectionCamel = toCollectionVarName(targetTableName);
|
|
475
772
|
imports.add(`import ${targetCollectionCamel} from "./${targetTableName}";`);
|
|
@@ -478,19 +775,17 @@ export function generateCollectionFile(
|
|
|
478
775
|
const direction = tableName < targetTableName ? "owning" : "inverse";
|
|
479
776
|
|
|
480
777
|
const thisFk = joinFks.find((fk) => fk.foreign_table_name === tableName);
|
|
481
|
-
const relHumanName = humanize(targetTableName);
|
|
482
778
|
|
|
483
779
|
let throughCode = "";
|
|
484
780
|
if (direction === "owning" && thisFk) {
|
|
485
|
-
throughCode = `\n through: {\n table: "${jt}",\n sourceColumn: "${thisFk.column_name}",\n targetColumn: "${otherFk.column_name}"\n }
|
|
781
|
+
throughCode = `\n through: {\n table: "${jt}",\n sourceColumn: "${thisFk.column_name}",\n targetColumn: "${otherFk.column_name}"\n },`;
|
|
486
782
|
} else if (direction === "inverse") {
|
|
487
783
|
throughCode = `\n // Make sure the target collection configures the 'through' property.`;
|
|
488
784
|
}
|
|
489
785
|
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
type: "relation",
|
|
786
|
+
relationsOutput += `
|
|
787
|
+
{
|
|
788
|
+
relationName: "${targetTableName}",
|
|
494
789
|
target: () => ${targetCollectionCamel},
|
|
495
790
|
cardinality: "many",
|
|
496
791
|
direction: "${direction}",${throughCode}
|
|
@@ -498,21 +793,30 @@ export function generateCollectionFile(
|
|
|
498
793
|
}
|
|
499
794
|
}
|
|
500
795
|
|
|
796
|
+
const relationsBlock = relationsOutput
|
|
797
|
+
? `\n relations: [${relationsOutput}\n ],`
|
|
798
|
+
: "";
|
|
799
|
+
|
|
800
|
+
const sortedPropertiesOrder = sortPropertiesOrder(orderEntries);
|
|
801
|
+
for (const key of sortedPropertiesOrder) {
|
|
802
|
+
propsOutput += propertyBlocks.get(key) || "";
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
const collectionVarName = toCollectionVarName(tableName);
|
|
501
806
|
const fileContent = `${Array.from(imports).join("\n")}
|
|
502
807
|
|
|
503
|
-
const ${
|
|
808
|
+
const ${collectionVarName}: PostgresCollection = {
|
|
504
809
|
name: "${collectionName}",
|
|
505
810
|
singularName: "${singular}",
|
|
506
811
|
slug: "${tableName}",
|
|
507
812
|
table: "${tableName}",
|
|
508
813
|
icon: "${icon}",
|
|
509
|
-
group: "App",
|
|
510
814
|
properties: {${propsOutput}
|
|
511
|
-
}
|
|
512
|
-
propertiesOrder: ${JSON.stringify(
|
|
815
|
+
},${relationsBlock}
|
|
816
|
+
propertiesOrder: ${JSON.stringify(sortedPropertiesOrder, null, 8).replace(/]$/, " ]")}
|
|
513
817
|
};
|
|
514
818
|
|
|
515
|
-
export default ${
|
|
819
|
+
export default ${collectionVarName};
|
|
516
820
|
`;
|
|
517
821
|
|
|
518
822
|
return fileContent;
|