@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
|
@@ -3,6 +3,7 @@ import { getPrimaryKeys } from "../services/entity-helpers";
|
|
|
3
3
|
import { getEnumVarName, getTableName, getTableVarName, resolveCollectionRelations, findRelation } from "@rebasepro/common";
|
|
4
4
|
import { toSnakeCase } from "@rebasepro/utils";
|
|
5
5
|
import { createHash } from "crypto";
|
|
6
|
+
import { logger } from "@rebasepro/server-core";
|
|
6
7
|
// --- Helper Functions ---
|
|
7
8
|
|
|
8
9
|
/**
|
|
@@ -41,6 +42,31 @@ type: "string",
|
|
|
41
42
|
isUuid: isUuid ?? false };
|
|
42
43
|
};
|
|
43
44
|
|
|
45
|
+
/**
|
|
46
|
+
* Given a raw DB column name (e.g. "client_id"), find the Drizzle property key
|
|
47
|
+
* on the collection that maps to that column. A property matches if:
|
|
48
|
+
* (a) it has an explicit `columnName` equal to the given column, OR
|
|
49
|
+
* (b) its snake_case form equals the given column.
|
|
50
|
+
*
|
|
51
|
+
* Returns the property key (the Drizzle object key) if found, or the original
|
|
52
|
+
* column name as a fallback.
|
|
53
|
+
*/
|
|
54
|
+
const resolvePropertyKeyForColumn = (collection: EntityCollection, column: string): string => {
|
|
55
|
+
if (!collection.properties) return column;
|
|
56
|
+
for (const [propKey, prop] of Object.entries(collection.properties)) {
|
|
57
|
+
const p = prop as Property;
|
|
58
|
+
// Explicit columnName match
|
|
59
|
+
if ("columnName" in p && typeof (p as unknown as Record<string, unknown>).columnName === "string") {
|
|
60
|
+
if ((p as unknown as Record<string, unknown>).columnName === column) return propKey;
|
|
61
|
+
}
|
|
62
|
+
// Convention match: snake_case(propKey) === column
|
|
63
|
+
if (toSnakeCase(propKey) === column) return propKey;
|
|
64
|
+
// Exact match (propKey is already the column name)
|
|
65
|
+
if (propKey === column) return propKey;
|
|
66
|
+
}
|
|
67
|
+
return column;
|
|
68
|
+
};
|
|
69
|
+
|
|
44
70
|
const isNumericId = (collection: EntityCollection): boolean => {
|
|
45
71
|
return getPrimaryKeyProp(collection).type === "number";
|
|
46
72
|
};
|
|
@@ -143,7 +169,7 @@ const getDrizzleColumn = (propName: string, prop: Property, collection: EntityCo
|
|
|
143
169
|
}
|
|
144
170
|
// autoValue: database-level default for initial value on INSERT
|
|
145
171
|
if (dateProp.autoValue === "on_create" || dateProp.autoValue === "on_update") {
|
|
146
|
-
columnDefinition +=
|
|
172
|
+
columnDefinition += ".default(sql`now()`)";
|
|
147
173
|
}
|
|
148
174
|
break;
|
|
149
175
|
}
|
|
@@ -206,7 +232,7 @@ const getDrizzleColumn = (propName: string, prop: Property, collection: EntityCo
|
|
|
206
232
|
|
|
207
233
|
// The localKey property is the source of truth for the FK column name.
|
|
208
234
|
if (!relation.localKey) {
|
|
209
|
-
|
|
235
|
+
logger.warn(`Could not generate column for owning relation '${relation.relationName}' on '${collection.name}': 'localKey' is not defined.`);
|
|
210
236
|
return null;
|
|
211
237
|
}
|
|
212
238
|
|
|
@@ -296,7 +322,7 @@ const resolveRawSql = (expression: string): string => {
|
|
|
296
322
|
*/
|
|
297
323
|
const wrapWithRoleCheck = (clause: string, roles: string[]): string => {
|
|
298
324
|
const rolesArrayString = `ARRAY[${roles.map(r => `'${r}'`).join(",")}]`;
|
|
299
|
-
const roleCondition = `string_to_array(auth.roles(), ',')
|
|
325
|
+
const roleCondition = `string_to_array(auth.roles(), ',') && ${rolesArrayString}`;
|
|
300
326
|
return `sql\`(${unwrapSql(clause)}) AND (${roleCondition})\``;
|
|
301
327
|
};
|
|
302
328
|
|
|
@@ -410,13 +436,13 @@ const generateSinglePolicyCode = (collection: EntityCollection, rule: SecurityRu
|
|
|
410
436
|
} else if (needsUsing) {
|
|
411
437
|
// Roles-only rule (e.g. { operation: "select", roles: ["admin"] })
|
|
412
438
|
const rolesArrayString = `ARRAY[${roles.map(r => `'${r}'`).join(",")}]`;
|
|
413
|
-
usingClause = `sql\`string_to_array(auth.roles(), ',')
|
|
439
|
+
usingClause = `sql\`string_to_array(auth.roles(), ',') && ${rolesArrayString}\``;
|
|
414
440
|
}
|
|
415
441
|
if (withCheckClause) {
|
|
416
442
|
withCheckClause = wrapWithRoleCheck(withCheckClause, roles);
|
|
417
443
|
} else if (needsWithCheck) {
|
|
418
444
|
const rolesArrayString = `ARRAY[${roles.map(r => `'${r}'`).join(",")}]`;
|
|
419
|
-
withCheckClause = `sql\`string_to_array(auth.roles(), ',')
|
|
445
|
+
withCheckClause = `sql\`string_to_array(auth.roles(), ',') && ${rolesArrayString}\``;
|
|
420
446
|
}
|
|
421
447
|
}
|
|
422
448
|
|
|
@@ -466,15 +492,22 @@ const computeSharedRelationName = (
|
|
|
466
492
|
|
|
467
493
|
// --- owning one (belongs-to) ---
|
|
468
494
|
if (rel.direction === "owning" && rel.cardinality === "one" && rel.localKey) {
|
|
469
|
-
|
|
495
|
+
// Normalise the localKey to the actual Drizzle property name so that
|
|
496
|
+
// the owning side produces the same relation name as the inverse side
|
|
497
|
+
// (which resolves foreignKeyOnTarget via the same helper).
|
|
498
|
+
const normalisedKey = resolvePropertyKeyForColumn(sourceCollection, rel.localKey);
|
|
499
|
+
return `${getTableName(sourceCollection)}_${normalisedKey}`;
|
|
470
500
|
}
|
|
471
501
|
|
|
472
502
|
// --- inverse many (one-to-many has-many) ---
|
|
473
503
|
if (rel.direction === "inverse" && rel.cardinality === "many" && rel.foreignKeyOnTarget) {
|
|
474
|
-
// The owning table is the *target*, the FK column is foreignKeyOnTarget
|
|
504
|
+
// The owning table is the *target*, the FK column is foreignKeyOnTarget.
|
|
505
|
+
// Resolve to the Drizzle property key on the target so it matches the
|
|
506
|
+
// owning side's normalised localKey.
|
|
475
507
|
try {
|
|
476
508
|
const targetCollection = rel.target();
|
|
477
|
-
|
|
509
|
+
const normalisedFK = resolvePropertyKeyForColumn(targetCollection, rel.foreignKeyOnTarget);
|
|
510
|
+
return `${getTableName(targetCollection)}_${normalisedFK}`;
|
|
478
511
|
} catch {
|
|
479
512
|
return fallback;
|
|
480
513
|
}
|
|
@@ -483,10 +516,11 @@ const computeSharedRelationName = (
|
|
|
483
516
|
// --- inverse one (one-to-one inverse) ---
|
|
484
517
|
if (rel.direction === "inverse" && rel.cardinality === "one") {
|
|
485
518
|
if (rel.foreignKeyOnTarget) {
|
|
486
|
-
// FK lives on the target table
|
|
519
|
+
// FK lives on the target table — resolve to Drizzle property key
|
|
487
520
|
try {
|
|
488
521
|
const targetCollection = rel.target();
|
|
489
|
-
|
|
522
|
+
const normalisedFK = resolvePropertyKeyForColumn(targetCollection, rel.foreignKeyOnTarget);
|
|
523
|
+
return `${getTableName(targetCollection)}_${normalisedFK}`;
|
|
490
524
|
} catch {
|
|
491
525
|
return fallback;
|
|
492
526
|
}
|
|
@@ -521,7 +555,6 @@ export const generateSchema = async (collections: EntityCollection[], stripPolic
|
|
|
521
555
|
let schemaContent = "// This file is auto-generated by the Rebase Drizzle generator. Do not edit manually.\n\n";
|
|
522
556
|
|
|
523
557
|
|
|
524
|
-
|
|
525
558
|
const hasUuid = collections.some(c =>
|
|
526
559
|
c.properties && Object.values(c.properties).some(
|
|
527
560
|
(p: Property) => p.type === "string" && ((p as unknown as Record<string, unknown>).autoValue === "uuid" || (p as unknown as Record<string, unknown>).isId === "uuid")
|
|
@@ -812,23 +845,23 @@ export const generateSchema = async (collections: EntityCollection[], stripPolic
|
|
|
812
845
|
const junctionTableVar = getTableVarName(correspondingRelation.through.table);
|
|
813
846
|
tableRelations.push(` "${relationKey}": many(${junctionTableVar}, { relationName: \"${drizzleRelationName}\" })`);
|
|
814
847
|
} else {
|
|
815
|
-
|
|
848
|
+
logger.warn(`Could not find corresponding owning many-to-many relation for inverse relation '${relationKey}' on '${collection.name}'`);
|
|
816
849
|
}
|
|
817
850
|
} catch (e) {
|
|
818
|
-
|
|
851
|
+
logger.warn(`Could not resolve inverse many-to-many relation '${relationKey}'`, { error: e });
|
|
819
852
|
}
|
|
820
853
|
}
|
|
821
854
|
// joinPath relations don't generate Drizzle relations - they use existing user tables
|
|
822
855
|
}
|
|
823
856
|
} catch (e) {
|
|
824
|
-
|
|
857
|
+
logger.warn(`Could not generate relation ${relationKey} for ${collection.name}`, { error: e });
|
|
825
858
|
}
|
|
826
859
|
}
|
|
827
860
|
|
|
828
861
|
// Synthesize missing reciprocal relations
|
|
829
862
|
for (const otherCollection of collections) {
|
|
830
863
|
if (otherCollection.slug === collection.slug) continue;
|
|
831
|
-
|
|
864
|
+
|
|
832
865
|
const otherRelations = resolveCollectionRelations(otherCollection);
|
|
833
866
|
for (const [otherKey, otherRel] of Object.entries(otherRelations)) {
|
|
834
867
|
if (otherRel.direction === "inverse" && otherRel.foreignKeyOnTarget) {
|
|
@@ -837,11 +870,16 @@ export const generateSchema = async (collections: EntityCollection[], stripPolic
|
|
|
837
870
|
if (otherTarget.slug === collection.slug) {
|
|
838
871
|
const drizzleRelationName = computeSharedRelationName(otherRel, otherCollection, collections);
|
|
839
872
|
const deduplicationKey = `${drizzleRelationName}::owning`;
|
|
840
|
-
|
|
873
|
+
|
|
841
874
|
if (!emittedRelationNames.has(deduplicationKey)) {
|
|
842
875
|
const otherTableVar = getTableVarName(getTableName(otherCollection));
|
|
843
|
-
|
|
844
|
-
|
|
876
|
+
// Resolve foreignKeyOnTarget to the Drizzle property key
|
|
877
|
+
// on THIS collection (the owning table). The raw FK column
|
|
878
|
+
// name (e.g. "client_id") may differ from the property key
|
|
879
|
+
// (e.g. "clientId") when `columnName` is set.
|
|
880
|
+
const drizzleFieldKey = resolvePropertyKeyForColumn(collection, otherRel.foreignKeyOnTarget);
|
|
881
|
+
const synthKey = `_synth_${otherTableVar}_${drizzleFieldKey}`;
|
|
882
|
+
tableRelations.push(` "${synthKey}": one(${otherTableVar}, {\n fields: [${tableVarName}.${drizzleFieldKey}],\n references: [${otherTableVar}.${getPrimaryKeyName(otherCollection)}],\n relationName: \"${drizzleRelationName}\"\n })`);
|
|
845
883
|
emittedRelationNames.add(deduplicationKey);
|
|
846
884
|
}
|
|
847
885
|
}
|
|
@@ -5,6 +5,7 @@ import { pathToFileURL } from "url";
|
|
|
5
5
|
import chokidar from "chokidar";
|
|
6
6
|
import { generateSchema } from "./generate-drizzle-schema-logic";
|
|
7
7
|
import { EntityCollection } from "@rebasepro/types";
|
|
8
|
+
import { logger } from "@rebasepro/server-core";
|
|
8
9
|
|
|
9
10
|
|
|
10
11
|
// --- Helper Functions ---
|
|
@@ -48,7 +49,7 @@ const formatTerminalText = (text: string, options: {
|
|
|
48
49
|
const runGeneration = async (collectionsFilePath?: string, outputPath?: string) => {
|
|
49
50
|
try {
|
|
50
51
|
if (!collectionsFilePath) {
|
|
51
|
-
|
|
52
|
+
logger.error("Error: No collections file path provided. Skipping schema generation.");
|
|
52
53
|
return;
|
|
53
54
|
}
|
|
54
55
|
|
|
@@ -74,7 +75,7 @@ const runGeneration = async (collectionsFilePath?: string, outputPath?: string)
|
|
|
74
75
|
}
|
|
75
76
|
} catch (err: unknown) {
|
|
76
77
|
const message = err instanceof Error ? err.message : String(err);
|
|
77
|
-
|
|
78
|
+
logger.error(`Error loading ${file}`, { detail: message });
|
|
78
79
|
}
|
|
79
80
|
}
|
|
80
81
|
}
|
|
@@ -91,7 +92,6 @@ const runGeneration = async (collectionsFilePath?: string, outputPath?: string)
|
|
|
91
92
|
}
|
|
92
93
|
|
|
93
94
|
|
|
94
|
-
|
|
95
95
|
// Sort collections by slug alphabetically to ensure deterministic schema generation
|
|
96
96
|
collections.sort((a, b) => a.slug.localeCompare(b.slug));
|
|
97
97
|
|
|
@@ -101,20 +101,20 @@ const runGeneration = async (collectionsFilePath?: string, outputPath?: string)
|
|
|
101
101
|
const outputDir = path.dirname(outputPath);
|
|
102
102
|
await fsPromises.mkdir(outputDir, { recursive: true });
|
|
103
103
|
await fsPromises.writeFile(outputPath, schemaContent);
|
|
104
|
-
|
|
104
|
+
logger.info("✅ Drizzle schema generated successfully at", { detail: outputPath });
|
|
105
105
|
} else {
|
|
106
|
-
|
|
107
|
-
|
|
106
|
+
logger.info("✅ Drizzle schema generated successfully.");
|
|
107
|
+
logger.info(String(schemaContent));
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
-
|
|
110
|
+
logger.info(`You can now run ${formatTerminalText("rebase db generate", {
|
|
111
111
|
bold: true,
|
|
112
112
|
backgroundColor: "blue",
|
|
113
113
|
textColor: "black"
|
|
114
114
|
})} to generate the SQL migration files.`);
|
|
115
115
|
|
|
116
116
|
} catch (error) {
|
|
117
|
-
|
|
117
|
+
logger.error("Error generating schema", { error: error });
|
|
118
118
|
}
|
|
119
119
|
};
|
|
120
120
|
|
|
@@ -128,7 +128,7 @@ const main = () => {
|
|
|
128
128
|
const watch = process.argv.includes("--watch");
|
|
129
129
|
|
|
130
130
|
if (!collectionsFilePath) {
|
|
131
|
-
|
|
131
|
+
logger.info("Usage: ts-node generate-drizzle-schema.ts <path-to-collections-file> [--output <path-to-output-file>] [--watch]");
|
|
132
132
|
return;
|
|
133
133
|
}
|
|
134
134
|
|
|
@@ -136,14 +136,14 @@ const main = () => {
|
|
|
136
136
|
const resolvedOutputPath = outputPath ? path.resolve(process.cwd(), outputPath) : undefined;
|
|
137
137
|
|
|
138
138
|
if (watch) {
|
|
139
|
-
|
|
139
|
+
logger.info(`Watching for changes in ${resolvedPath}...`);
|
|
140
140
|
const watcher = chokidar.watch(resolvedPath, {
|
|
141
141
|
persistent: true,
|
|
142
142
|
ignoreInitial: false
|
|
143
143
|
});
|
|
144
144
|
|
|
145
145
|
watcher.on("all", (event, filePath) => {
|
|
146
|
-
|
|
146
|
+
logger.info(`[${event}] ${filePath}. Regenerating schema...`);
|
|
147
147
|
runGeneration(resolvedPath, resolvedOutputPath);
|
|
148
148
|
});
|
|
149
149
|
} else {
|
|
@@ -45,15 +45,15 @@ export function inferPropertyFromData(
|
|
|
45
45
|
const max = Math.max(...numValues);
|
|
46
46
|
// Example heuristic: percentages
|
|
47
47
|
if (min >= 0 && max <= 100 && (colNameLower.includes("percent") || colNameLower.includes("rate") || colNameLower.includes("score"))) {
|
|
48
|
-
extraLines.push(
|
|
48
|
+
extraLines.push(" validation: {\n min: 0,\n max: 100\n }");
|
|
49
49
|
} else if (min >= 0 && (colNameLower.includes("count") || colNameLower.includes("total") || colNameLower.includes("amount"))) {
|
|
50
|
-
extraLines.push(
|
|
50
|
+
extraLines.push(" validation: {\n min: 0\n }");
|
|
51
51
|
}
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
// Currency
|
|
55
55
|
if (colNameLower.includes("price") || colNameLower.includes("cost") || colNameLower.includes("amount") || colNameLower.includes("fee") || pgDataType === "money") {
|
|
56
|
-
extraLines.push(
|
|
56
|
+
extraLines.push(" ui: {\n currency: true\n }");
|
|
57
57
|
}
|
|
58
58
|
}
|
|
59
59
|
|
|
@@ -94,7 +94,7 @@ export function inferPropertyFromData(
|
|
|
94
94
|
extraLines.push(` of: { name: "${humanize(columnName)} Item", type: "${innerType}" }`);
|
|
95
95
|
} else {
|
|
96
96
|
result.propType = "map";
|
|
97
|
-
|
|
97
|
+
|
|
98
98
|
// Infer inner schema
|
|
99
99
|
if (allObjects && validValues.length > 0) {
|
|
100
100
|
const schema: Record<string, string> = {};
|
|
@@ -115,7 +115,7 @@ export function inferPropertyFromData(
|
|
|
115
115
|
}
|
|
116
116
|
}
|
|
117
117
|
}
|
|
118
|
-
|
|
118
|
+
|
|
119
119
|
const keys = Object.keys(schema).filter(k => schema[k] !== "mixed");
|
|
120
120
|
if (keys.length > 0) {
|
|
121
121
|
const props = keys.map(k => {
|
|
@@ -123,10 +123,10 @@ export function inferPropertyFromData(
|
|
|
123
123
|
}).join(",");
|
|
124
124
|
extraLines.push(` properties: {${props}\n }`);
|
|
125
125
|
} else {
|
|
126
|
-
extraLines.push(
|
|
126
|
+
extraLines.push(" keyValue: true");
|
|
127
127
|
}
|
|
128
128
|
} else {
|
|
129
|
-
extraLines.push(
|
|
129
|
+
extraLines.push(" keyValue: true");
|
|
130
130
|
}
|
|
131
131
|
}
|
|
132
132
|
}
|
|
@@ -134,7 +134,7 @@ export function inferPropertyFromData(
|
|
|
134
134
|
// ── String Analysis ──────────────────────────────────────────────────
|
|
135
135
|
if (currentPropType === "string") {
|
|
136
136
|
// Date/Time Strings
|
|
137
|
-
if (validValues.every(v => typeof v ===
|
|
137
|
+
if (validValues.every(v => typeof v === "string" && ISO_8601_REGEX.test(v))) {
|
|
138
138
|
result.propType = "date";
|
|
139
139
|
return result;
|
|
140
140
|
}
|
|
@@ -148,7 +148,7 @@ export function inferPropertyFromData(
|
|
|
148
148
|
maxEnumLength = v.length;
|
|
149
149
|
}
|
|
150
150
|
}
|
|
151
|
-
|
|
151
|
+
|
|
152
152
|
// Ensure no empty string, max length makes sense, and fewer unique values than total values (unless small total)
|
|
153
153
|
if (uniqueValues.size > 0 && uniqueValues.size <= 5 && maxEnumLength <= 50 && validValues.length > uniqueValues.size && !uniqueValues.has("")) {
|
|
154
154
|
const isLikelyId = isPk || colNameLower.endsWith("_id");
|
|
@@ -161,18 +161,18 @@ export function inferPropertyFromData(
|
|
|
161
161
|
}
|
|
162
162
|
|
|
163
163
|
// UUID / CUID Detection
|
|
164
|
-
const allUuid = validValues.every(v => typeof v ===
|
|
165
|
-
const allCuid = validValues.every(v => typeof v ===
|
|
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
166
|
if (allUuid) {
|
|
167
|
-
if (isPk) extraLines.push(
|
|
167
|
+
if (isPk) extraLines.push(" isId: \"uuid\"");
|
|
168
168
|
} else if (allCuid) {
|
|
169
|
-
if (isPk) extraLines.push(
|
|
169
|
+
if (isPk) extraLines.push(" isId: \"cuid\"");
|
|
170
170
|
}
|
|
171
171
|
|
|
172
172
|
// Color Codes
|
|
173
|
-
const allColors = validValues.every(v => typeof v ===
|
|
173
|
+
const allColors = validValues.every(v => typeof v === "string" && COLOR_HEX_REGEX.test(v));
|
|
174
174
|
if (allColors) {
|
|
175
|
-
extraLines.push(
|
|
175
|
+
extraLines.push(" ui: {\n color: true\n }");
|
|
176
176
|
}
|
|
177
177
|
|
|
178
178
|
// Text Lengths, Multiline & Markdown
|
|
@@ -190,9 +190,9 @@ export function inferPropertyFromData(
|
|
|
190
190
|
}
|
|
191
191
|
|
|
192
192
|
if (hasMarkdown) {
|
|
193
|
-
extraLines.push(
|
|
193
|
+
extraLines.push(" multiline: true,\n markdown: true");
|
|
194
194
|
} else if (hasNewlines || maxLength > 100) {
|
|
195
|
-
extraLines.push(
|
|
195
|
+
extraLines.push(" multiline: true");
|
|
196
196
|
}
|
|
197
197
|
|
|
198
198
|
if (maxLength > 0 && maxLength < 10000) { // arbitrary cap to avoid huge limits
|
|
@@ -208,26 +208,26 @@ export function inferPropertyFromData(
|
|
|
208
208
|
const isUrl = colNameLower.endsWith("_url") || colNameLower.endsWith("_uri") || colNameLower.endsWith("_link");
|
|
209
209
|
const isMedia = colNameLower.includes("image") || colNameLower.includes("avatar") || colNameLower.includes("photo") || colNameLower.includes("logo") || colNameLower.includes("cover");
|
|
210
210
|
|
|
211
|
-
const allAbsoluteUrls = validValues.every(v => typeof v ===
|
|
211
|
+
const allAbsoluteUrls = validValues.every(v => typeof v === "string" && (v.startsWith("http://") || v.startsWith("https://")));
|
|
212
212
|
if (allAbsoluteUrls) {
|
|
213
|
-
const isImage = validValues.some(v => typeof v ===
|
|
213
|
+
const isImage = validValues.some(v => typeof v === "string" && v.match(/\.(jpeg|jpg|gif|png|webp|svg)/i));
|
|
214
214
|
if (isImage || isMedia) {
|
|
215
|
-
extraLines.push(
|
|
215
|
+
extraLines.push(" ui: {\n url: \"image\"\n }");
|
|
216
216
|
} else {
|
|
217
|
-
extraLines.push(
|
|
217
|
+
extraLines.push(" ui: {\n url: true\n }");
|
|
218
218
|
}
|
|
219
219
|
} else {
|
|
220
|
-
const hasFileExtension = validValues.some(v => typeof v ===
|
|
220
|
+
const hasFileExtension = validValues.some(v => typeof v === "string" && v.match(/\.[a-zA-Z0-9]+$/));
|
|
221
221
|
if (hasFileExtension) {
|
|
222
222
|
const firstVal = validValues[0] as string;
|
|
223
|
-
const lastSlash = firstVal.lastIndexOf(
|
|
223
|
+
const lastSlash = firstVal.lastIndexOf("/");
|
|
224
224
|
const inferredStoragePath = lastSlash > 0 ? firstVal.substring(0, lastSlash) : "files";
|
|
225
225
|
extraLines.push(` storage: {\n storagePath: "${inferredStoragePath}"\n }`);
|
|
226
226
|
} else if (isUrl) {
|
|
227
227
|
if (isMedia) {
|
|
228
|
-
extraLines.push(
|
|
228
|
+
extraLines.push(" ui: {\n url: \"image\"\n }");
|
|
229
229
|
} else {
|
|
230
|
-
extraLines.push(
|
|
230
|
+
extraLines.push(" ui: {\n url: true\n }");
|
|
231
231
|
}
|
|
232
232
|
}
|
|
233
233
|
}
|
|
@@ -63,7 +63,7 @@ const IRREGULAR_SINGULARS: Record<string, string> = {
|
|
|
63
63
|
data: "datum",
|
|
64
64
|
media: "medium",
|
|
65
65
|
criteria: "criterion",
|
|
66
|
-
phenomena: "phenomenon"
|
|
66
|
+
phenomena: "phenomenon"
|
|
67
67
|
};
|
|
68
68
|
|
|
69
69
|
/** Words ending in 's' that are already singular. */
|
|
@@ -73,7 +73,7 @@ const UNCOUNTABLE = new Set([
|
|
|
73
73
|
"synopsis", "parenthesis", "hypothesis", "emphasis",
|
|
74
74
|
"news", "series", "species", "means", "athletics",
|
|
75
75
|
"economics", "electronics", "mathematics", "physics",
|
|
76
|
-
"politics", "statistics"
|
|
76
|
+
"politics", "statistics"
|
|
77
77
|
]);
|
|
78
78
|
|
|
79
79
|
export function singularize(word: string): string {
|
|
@@ -165,10 +165,10 @@ export function mapPgType(dataType: string): string {
|
|
|
165
165
|
|
|
166
166
|
// Numeric types
|
|
167
167
|
if (
|
|
168
|
-
dt.includes("int") ||
|
|
168
|
+
dt.includes("int") || // integer, smallint, bigint
|
|
169
169
|
dt.includes("numeric") ||
|
|
170
170
|
dt.includes("decimal") ||
|
|
171
|
-
dt.includes("serial") ||
|
|
171
|
+
dt.includes("serial") || // serial, bigserial
|
|
172
172
|
dt === "real" ||
|
|
173
173
|
dt === "float4" ||
|
|
174
174
|
dt === "float8" ||
|
|
@@ -281,7 +281,7 @@ export interface PropertyOrderingContext {
|
|
|
281
281
|
const IDENTITY_EXACT: Record<string, number> = {
|
|
282
282
|
id: 0,
|
|
283
283
|
uuid: 1,
|
|
284
|
-
_id: 2
|
|
284
|
+
_id: 2
|
|
285
285
|
};
|
|
286
286
|
|
|
287
287
|
// — Tier 1: Title / Name — the "display column" (10–19) ———————————————
|
|
@@ -293,7 +293,7 @@ const TITLE_EXACT: Record<string, number> = {
|
|
|
293
293
|
displayname: 13,
|
|
294
294
|
headline: 14,
|
|
295
295
|
subject: 15,
|
|
296
|
-
heading: 16
|
|
296
|
+
heading: 16
|
|
297
297
|
};
|
|
298
298
|
|
|
299
299
|
// — Tier 2: Human identity fields (20–29) —————————————————————————————
|
|
@@ -313,7 +313,7 @@ const HUMAN_IDENTITY_EXACT: Record<string, number> = {
|
|
|
313
313
|
email_address: 26,
|
|
314
314
|
phone: 27,
|
|
315
315
|
phone_number: 27,
|
|
316
|
-
mobile: 27
|
|
316
|
+
mobile: 27
|
|
317
317
|
};
|
|
318
318
|
|
|
319
319
|
// — Tier 3: Core descriptors (30–39) ——————————————————————————————————
|
|
@@ -333,7 +333,7 @@ const DESCRIPTOR_EXACT: Record<string, number> = {
|
|
|
333
333
|
priority: 39,
|
|
334
334
|
order: 39,
|
|
335
335
|
sort_order: 39,
|
|
336
|
-
position: 39
|
|
336
|
+
position: 39
|
|
337
337
|
};
|
|
338
338
|
|
|
339
339
|
// — Tier 12: System timestamps (120–129) ——————————————————————————————
|
|
@@ -348,7 +348,7 @@ const SYSTEM_TIMESTAMP_EXACT: Record<string, number> = {
|
|
|
348
348
|
last_modified: 122,
|
|
349
349
|
deleted_at: 123,
|
|
350
350
|
deletedat: 123,
|
|
351
|
-
archived_at: 124
|
|
351
|
+
archived_at: 124
|
|
352
352
|
};
|
|
353
353
|
|
|
354
354
|
// — Pattern-based rules for partial matches ———————————————————————————
|
|
@@ -370,7 +370,7 @@ const JSON_MAP_NAMES = new Set(["metadata", "meta", "config", "configuration", "
|
|
|
370
370
|
*/
|
|
371
371
|
export function computePropertyPriority(
|
|
372
372
|
columnName: string,
|
|
373
|
-
ctx: PropertyOrderingContext
|
|
373
|
+
ctx: PropertyOrderingContext
|
|
374
374
|
): number {
|
|
375
375
|
// Normalize camelCase/PascalCase to snake_case, then lowercase
|
|
376
376
|
const col = columnName.replace(/([a-z0-9])([A-Z])/g, "$1_$2").toLowerCase();
|
|
@@ -521,7 +521,7 @@ export function generateCollectionFile(
|
|
|
521
521
|
joinTables: Set<string>,
|
|
522
522
|
tablesMap: Map<string, TableMeta>,
|
|
523
523
|
enumMap: Map<string, string[]>,
|
|
524
|
-
sampleData?: Record<string, unknown>[]
|
|
524
|
+
sampleData?: Record<string, unknown>[]
|
|
525
525
|
): string {
|
|
526
526
|
const collectionName = humanize(tableName);
|
|
527
527
|
const singular = singularize(collectionName);
|
|
@@ -529,8 +529,8 @@ export function generateCollectionFile(
|
|
|
529
529
|
|
|
530
530
|
const imports = new Set<string>(['import { PostgresCollection } from "@rebasepro/types";']);
|
|
531
531
|
|
|
532
|
-
let propsOutput =
|
|
533
|
-
let relationsOutput =
|
|
532
|
+
let propsOutput = "";
|
|
533
|
+
let relationsOutput = "";
|
|
534
534
|
const orderEntries: PropertyOrderEntry[] = [];
|
|
535
535
|
const propertyBlocks = new Map<string, string>();
|
|
536
536
|
let columnIndex = 0;
|
|
@@ -559,7 +559,7 @@ export function generateCollectionFile(
|
|
|
559
559
|
// ── Data Inference Engine ────────────────────────────────────────────
|
|
560
560
|
let finalPropType = propType;
|
|
561
561
|
let inferenceExtra = "";
|
|
562
|
-
|
|
562
|
+
|
|
563
563
|
if (!isEnumColumn && sampleData && sampleData.length > 0) {
|
|
564
564
|
const values = sampleData.map(r => r[col.column_name]);
|
|
565
565
|
const inferred = inferPropertyFromData(col.column_name, col.data_type, propType, values, meta.pks.includes(col.column_name));
|
|
@@ -578,11 +578,11 @@ export function generateCollectionFile(
|
|
|
578
578
|
// Date auto-value heuristics
|
|
579
579
|
if (finalPropType === "date") {
|
|
580
580
|
if (colNameLower === "created_at" || colNameLower === "createdat") {
|
|
581
|
-
extra +=
|
|
581
|
+
extra += "\n autoValue: \"on_create\",\n ui: {\n readOnly: true,\n hideFromCollection: true\n },";
|
|
582
582
|
} else if (colNameLower === "updated_at" || colNameLower === "updatedat") {
|
|
583
|
-
extra +=
|
|
583
|
+
extra += "\n autoValue: \"on_update\",\n ui: {\n readOnly: true,\n hideFromCollection: true\n },";
|
|
584
584
|
} else if (col.column_default && (col.column_default.includes("now()") || col.column_default.includes("CURRENT_TIMESTAMP"))) {
|
|
585
|
-
extra +=
|
|
585
|
+
extra += "\n autoValue: \"on_create\",\n ui: {\n readOnly: true\n },";
|
|
586
586
|
}
|
|
587
587
|
}
|
|
588
588
|
|
|
@@ -602,7 +602,7 @@ export function generateCollectionFile(
|
|
|
602
602
|
}
|
|
603
603
|
extra += `\n of: { name: "${humanize(col.column_name)} Item", type: "${innerType}" },`;
|
|
604
604
|
} else if (finalPropType === "map" && !inferenceExtra.includes("keyValue: true") && !inferenceExtra.includes("properties: {")) {
|
|
605
|
-
extra +=
|
|
605
|
+
extra += "\n keyValue: true,";
|
|
606
606
|
}
|
|
607
607
|
|
|
608
608
|
// String sub-type heuristics (Fallback if not handled by inference or enum)
|
|
@@ -613,16 +613,16 @@ export function generateCollectionFile(
|
|
|
613
613
|
if (isMedia) {
|
|
614
614
|
extra += `\n storage: {\n storagePath: "${tableName}/${col.column_name}"\n },`;
|
|
615
615
|
} else if (isUrl) {
|
|
616
|
-
extra +=
|
|
616
|
+
extra += "\n ui: {\n url: true\n },";
|
|
617
617
|
} else if (colNameLower === "description" || colNameLower === "summary" || colNameLower === "excerpt") {
|
|
618
|
-
extra +=
|
|
618
|
+
extra += "\n multiline: true,";
|
|
619
619
|
} else if (colNameLower === "content" || colNameLower === "body") {
|
|
620
|
-
extra +=
|
|
620
|
+
extra += "\n multiline: true,\n markdown: true,";
|
|
621
621
|
} else if (col.data_type === "text") {
|
|
622
|
-
extra +=
|
|
622
|
+
extra += "\n multiline: true,";
|
|
623
623
|
}
|
|
624
624
|
}
|
|
625
|
-
|
|
625
|
+
|
|
626
626
|
// Append inference results
|
|
627
627
|
if (inferenceExtra) {
|
|
628
628
|
extra += inferenceExtra;
|
|
@@ -634,11 +634,11 @@ export function generateCollectionFile(
|
|
|
634
634
|
if (isCompositePk) {
|
|
635
635
|
extra += `\n // Part of composite primary key (${meta.pks.join(", ")})`;
|
|
636
636
|
} else if (finalPropType === "number" && !inferenceExtra.includes("isId:")) {
|
|
637
|
-
extra +=
|
|
637
|
+
extra += "\n isId: \"increment\",";
|
|
638
638
|
} else if (col.data_type.toLowerCase() === "uuid" && !inferenceExtra.includes("isId:")) {
|
|
639
|
-
extra +=
|
|
639
|
+
extra += "\n isId: \"uuid\",";
|
|
640
640
|
} else if (!inferenceExtra.includes("isId:")) {
|
|
641
|
-
extra +=
|
|
641
|
+
extra += "\n isId: \"uuid\", // Verify if this is a UUID or CUID";
|
|
642
642
|
}
|
|
643
643
|
}
|
|
644
644
|
|
|
@@ -651,7 +651,7 @@ export function generateCollectionFile(
|
|
|
651
651
|
if (extra.includes("validation: {")) {
|
|
652
652
|
extra = extra.replace("validation: {", "validation: {\n required: true,");
|
|
653
653
|
} else {
|
|
654
|
-
extra +=
|
|
654
|
+
extra += "\n validation: {\n required: true\n },";
|
|
655
655
|
}
|
|
656
656
|
}
|
|
657
657
|
|
|
@@ -665,8 +665,8 @@ export function generateCollectionFile(
|
|
|
665
665
|
isEnum: isEnumColumn,
|
|
666
666
|
isStorage: extra.includes("storage: {") || inferenceExtra.includes("storage: {"),
|
|
667
667
|
pgDataType: col.data_type,
|
|
668
|
-
originalIndex: currentIndex
|
|
669
|
-
}
|
|
668
|
+
originalIndex: currentIndex
|
|
669
|
+
}
|
|
670
670
|
});
|
|
671
671
|
|
|
672
672
|
propertyBlocks.set(col.column_name, `
|
|
@@ -696,8 +696,8 @@ export function generateCollectionFile(
|
|
|
696
696
|
isEnum: false,
|
|
697
697
|
isStorage: false,
|
|
698
698
|
pgDataType: "",
|
|
699
|
-
originalIndex: columnIndex
|
|
700
|
-
}
|
|
699
|
+
originalIndex: columnIndex++
|
|
700
|
+
}
|
|
701
701
|
});
|
|
702
702
|
|
|
703
703
|
const targetCollectionCamel = toCollectionVarName(targetTableName);
|
|
@@ -794,7 +794,7 @@ export function generateCollectionFile(
|
|
|
794
794
|
if (direction === "owning" && thisFk) {
|
|
795
795
|
throughCode = `\n through: {\n table: "${jt}",\n sourceColumn: "${thisFk.column_name}",\n targetColumn: "${otherFk.column_name}"\n },`;
|
|
796
796
|
} else if (direction === "inverse") {
|
|
797
|
-
throughCode =
|
|
797
|
+
throughCode = "\n // Make sure the target collection configures the 'through' property.";
|
|
798
798
|
}
|
|
799
799
|
|
|
800
800
|
relationsOutput += `
|
|
@@ -860,10 +860,10 @@ export function mergeIndexContent(existingContent: string, newFileNames: string[
|
|
|
860
860
|
[...existingContent.matchAll(/import\s+([a-zA-Z0-9_]+)\s+from\s+"\.\/([^"]+)"/g)].map((m) => m[2])
|
|
861
861
|
);
|
|
862
862
|
const sorted = [...newFileNames].sort();
|
|
863
|
-
|
|
863
|
+
|
|
864
864
|
let newImports = "";
|
|
865
865
|
let newElements = "";
|
|
866
|
-
|
|
866
|
+
|
|
867
867
|
for (const f of sorted) {
|
|
868
868
|
if (!existingImports.has(f)) {
|
|
869
869
|
const varName = toCollectionVarName(f);
|
|
@@ -871,9 +871,9 @@ export function mergeIndexContent(existingContent: string, newFileNames: string[
|
|
|
871
871
|
newElements += ` ${varName},\n`;
|
|
872
872
|
}
|
|
873
873
|
}
|
|
874
|
-
|
|
874
|
+
|
|
875
875
|
if (!newImports) return existingContent;
|
|
876
|
-
|
|
876
|
+
|
|
877
877
|
// Simple injection logic:
|
|
878
878
|
// Add new imports below the last import or at the top
|
|
879
879
|
const importRegex = /import\s+.*?;/g;
|
|
@@ -882,7 +882,7 @@ export function mergeIndexContent(existingContent: string, newFileNames: string[
|
|
|
882
882
|
while ((match = importRegex.exec(existingContent)) !== null) {
|
|
883
883
|
lastImportMatch = match;
|
|
884
884
|
}
|
|
885
|
-
|
|
885
|
+
|
|
886
886
|
let contentWithImports = existingContent;
|
|
887
887
|
if (lastImportMatch) {
|
|
888
888
|
const pos = lastImportMatch.index + lastImportMatch[0].length;
|
|
@@ -890,7 +890,7 @@ export function mergeIndexContent(existingContent: string, newFileNames: string[
|
|
|
890
890
|
} else {
|
|
891
891
|
contentWithImports = newImports + "\n" + existingContent;
|
|
892
892
|
}
|
|
893
|
-
|
|
893
|
+
|
|
894
894
|
// Inject into the `collections = [...]` array
|
|
895
895
|
const arrayRegex = /export\s+const\s+collections\s*=\s*\[([\s\S]*?)\];/;
|
|
896
896
|
return contentWithImports.replace(arrayRegex, (fullMatch, arrayContent) => {
|