@rebasepro/server-postgresql 0.6.1 → 0.8.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 (55) hide show
  1. package/dist/PostgresBackendDriver.d.ts +8 -0
  2. package/dist/auth/services.d.ts +21 -1
  3. package/dist/cli-errors.d.ts +29 -0
  4. package/dist/cli-helpers.d.ts +7 -0
  5. package/dist/collections/PostgresCollectionRegistry.d.ts +2 -2
  6. package/dist/index.es.js +2987 -230
  7. package/dist/index.es.js.map +1 -1
  8. package/dist/schema/auth-default-policies.d.ts +12 -0
  9. package/dist/schema/auth-schema.d.ts +227 -0
  10. package/dist/schema/generate-postgres-ddl-logic.d.ts +5 -0
  11. package/dist/schema/generate-postgres-ddl.d.ts +1 -0
  12. package/dist/services/entityService.d.ts +1 -1
  13. package/dist/utils/pg-error-utils.d.ts +10 -0
  14. package/dist/utils/table-classification.d.ts +8 -0
  15. package/package.json +15 -9
  16. package/src/PostgresBackendDriver.ts +200 -68
  17. package/src/PostgresBootstrapper.ts +34 -2
  18. package/src/auth/ensure-tables.ts +56 -1
  19. package/src/auth/services.ts +94 -1
  20. package/src/cli-errors.ts +162 -0
  21. package/src/cli-helpers.ts +183 -0
  22. package/src/cli.ts +264 -245
  23. package/src/collections/PostgresCollectionRegistry.ts +6 -6
  24. package/src/data-transformer.ts +2 -2
  25. package/src/schema/auth-default-policies.ts +97 -0
  26. package/src/schema/auth-schema.ts +25 -2
  27. package/src/schema/doctor.ts +2 -4
  28. package/src/schema/generate-drizzle-schema-logic.ts +20 -82
  29. package/src/schema/generate-drizzle-schema.ts +3 -5
  30. package/src/schema/generate-postgres-ddl-logic.ts +487 -0
  31. package/src/schema/generate-postgres-ddl.ts +116 -0
  32. package/src/services/EntityPersistService.ts +10 -8
  33. package/src/services/entityService.ts +28 -3
  34. package/src/services/realtimeService.ts +26 -2
  35. package/src/utils/pg-error-utils.ts +16 -0
  36. package/src/utils/table-classification.ts +16 -0
  37. package/src/websocket.ts +9 -0
  38. package/test/auth-default-policies.test.ts +89 -0
  39. package/test/cli-helpers-extended.test.ts +324 -0
  40. package/test/cli-helpers.test.ts +59 -0
  41. package/test/connection.test.ts +292 -0
  42. package/test/databasePoolManager.test.ts +289 -0
  43. package/test/doctor-extended.test.ts +443 -0
  44. package/test/e2e/db-e2e.test.ts +293 -0
  45. package/test/e2e/pg-setup.ts +79 -0
  46. package/test/entity-callbacks-redaction.test.ts +86 -0
  47. package/test/entity-persist-composite-keys.test.ts +451 -0
  48. package/test/generate-postgres-ddl-edge-cases.test.ts +716 -0
  49. package/test/generate-postgres-ddl.test.ts +300 -0
  50. package/test/mfa-service.test.ts +544 -0
  51. package/test/pg-error-utils.test.ts +50 -1
  52. package/test/postgresDataDriver.test.ts +2 -1
  53. package/test/realtimeService-channels.test.ts +696 -0
  54. package/test/unmapped-tables-safety.test.ts +55 -342
  55. package/vitest.e2e.config.ts +10 -0
@@ -1,7 +1,7 @@
1
1
  import { eq, SQL } from "drizzle-orm";
2
2
  import { AnyPgColumn } from "drizzle-orm/pg-core";
3
3
  import { NodePgDatabase } from "drizzle-orm/node-postgres";
4
- import { CollectionWithRelations, EntityCollection, Properties, Property, Relation, RelationProperty, Vector, BinaryProperty } from "@rebasepro/types";
4
+ import { EntityCollection, Properties, Property, Relation, RelationProperty, Vector, BinaryProperty } from "@rebasepro/types";
5
5
  import { getTableName, resolveCollectionRelations, findRelation, createRelationRef, DEFAULT_ONE_OF_TYPE, DEFAULT_ONE_OF_VALUE } from "@rebasepro/common";
6
6
  import { PostgresCollectionRegistry } from "./collections/PostgresCollectionRegistry";
7
7
  import { DrizzleConditionBuilder } from "./utils/drizzle-conditions";
@@ -551,7 +551,7 @@ export function parsePropertyFromServer(value: unknown, property: Property, coll
551
551
  relationDef = findRelation(resolvedRelations, propertyKey);
552
552
  }
553
553
  if (!relationDef) {
554
- relationDef = (collection as CollectionWithRelations).relations?.find((rel: Relation) => rel.relationName === (property as RelationProperty).relationName);
554
+ relationDef = collection.relations?.find((rel: Relation) => rel.relationName === (property as RelationProperty).relationName);
555
555
  }
556
556
 
557
557
  if (!relationDef) {
@@ -0,0 +1,97 @@
1
+ import { EntityCollection, SecurityRule, SecurityOperation, AuthCollectionConfig, PolicyExpression, isPostgresCollection, policy } from "@rebasepro/types";
2
+ import { getTableName } from "@rebasepro/common";
3
+
4
+ /**
5
+ * Default RLS for authentication collections.
6
+ *
7
+ * Users are persisted through the same write path as any other collection —
8
+ * the only thing that distinguishes them is the `auth` flag. That means
9
+ * privileged columns (most importantly `roles`) would be editable by any
10
+ * authenticated user unless the collection author remembers to write a correct
11
+ * admin-only write policy.
12
+ *
13
+ * To make this safe by default, the schema generator injects two policies for
14
+ * every write operation on an auth collection. Enforcement lives in the
15
+ * database (RLS), so it applies to every write path — REST, WebSocket, and any
16
+ * internal caller — not just one entry point:
17
+ *
18
+ * 1. A **restrictive** admin gate. Restrictive policies are AND'd with every
19
+ * other policy, so this guarantees a write is rejected unless the caller is
20
+ * an admin (or the trusted server context) — even if the author also wrote
21
+ * a permissive rule such as "a user may edit their own row". Without this,
22
+ * a permissive owner rule would let a user change their own `roles`.
23
+ *
24
+ * 2. A **permissive** admin grant. Restrictive policies only constrain; at
25
+ * least one permissive policy must also pass for the write to be allowed.
26
+ * This grants the baseline write so admins (and the server context) can
27
+ * manage users even when the collection has no other permissive write rule.
28
+ *
29
+ * Both mirror the framework's trusted-server convention: the write is allowed
30
+ * when there is no authenticated user context (`auth.uid() IS NULL`, used by
31
+ * the built-in auth flows that run as the service role) OR the caller holds the
32
+ * `admin` role. Net effect: only admins (or the server) can write the row, and
33
+ * therefore only admins can change privileged columns like `roles`.
34
+ *
35
+ * Author-provided write rules are preserved but cannot loosen this guarantee —
36
+ * a non-admin write is always blocked by the restrictive gate. To take full
37
+ * control of an auth collection's write authorization, set
38
+ * `disableDefaultAuthPolicies: true` on the collection.
39
+ */
40
+ // Expressed structurally (not as raw SQL) so the admin UI can evaluate it
41
+ // exactly — the framework's most security-critical policy must be reflected
42
+ // precisely, not left as an un-evaluable raw clause. Compiles to
43
+ // `auth.uid() IS NULL OR (string_to_array(auth.roles(), ',') && ARRAY['admin'])`.
44
+ const ADMIN_WRITE_EXPR: PolicyExpression = policy.or(
45
+ policy.not(policy.authenticated()),
46
+ policy.rolesOverlap(["admin"])
47
+ );
48
+
49
+ /** Write operations that must be admin-gated by default on auth collections. */
50
+ const DEFAULT_GUARDED_OPS: SecurityOperation[] = ["insert", "update", "delete"];
51
+
52
+ /** Whether a collection is flagged as an authentication collection. */
53
+ function isAuthCollection(collection: EntityCollection): boolean {
54
+ const auth = collection.auth;
55
+ return auth === true || (typeof auth === "object" && (auth as AuthCollectionConfig)?.enabled === true);
56
+ }
57
+
58
+ /**
59
+ * Returns the security rules that should be applied to a collection: the
60
+ * author's explicit `securityRules`, plus, for auth collections, an
61
+ * auto-injected restrictive admin gate and permissive admin grant on every
62
+ * write operation. Together these guarantee only admins (or the trusted server
63
+ * context) can write the row.
64
+ *
65
+ * Non-auth collections, and auth collections that opt out via
66
+ * `disableDefaultAuthPolicies`, are returned unchanged.
67
+ */
68
+ export function getEffectiveSecurityRules(collection: EntityCollection): SecurityRule[] {
69
+ const explicit = (isPostgresCollection(collection) ? collection.securityRules : undefined) ?? [];
70
+
71
+ if (!isAuthCollection(collection) || collection.disableDefaultAuthPolicies) {
72
+ return explicit;
73
+ }
74
+
75
+ const tableName = getTableName(collection);
76
+
77
+ // Restrictive gate: AND'd with all other policies, so no permissive rule
78
+ // (e.g. an owner "edit your own row" rule) can let a non-admin through.
79
+ const requireAdminGate: SecurityRule = {
80
+ name: `${tableName}_require_admin_write`,
81
+ mode: "restrictive",
82
+ operations: [...DEFAULT_GUARDED_OPS],
83
+ condition: ADMIN_WRITE_EXPR,
84
+ check: ADMIN_WRITE_EXPR
85
+ };
86
+
87
+ // Permissive grant: a restrictive policy alone denies everything, so this
88
+ // grants the baseline write to admins / the server context.
89
+ const allowAdminWrite: SecurityRule = {
90
+ name: `${tableName}_default_admin_write`,
91
+ operations: [...DEFAULT_GUARDED_OPS],
92
+ condition: ADMIN_WRITE_EXPR,
93
+ check: ADMIN_WRITE_EXPR
94
+ };
95
+
96
+ return [...explicit, allowAdminWrite, requireAdminGate];
97
+ }
@@ -118,6 +118,18 @@ export function createAuthSchema(usersSchemaName = "rebase") {
118
118
  createdAt: timestamp("created_at").defaultNow().notNull()
119
119
  });
120
120
 
121
+ /**
122
+ * Magic link tokens for passwordless email login
123
+ */
124
+ const magicLinkTokens = tableCreator("magic_link_tokens", {
125
+ id: uuid("id").defaultRandom().primaryKey(),
126
+ userId: uuid("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
127
+ tokenHash: varchar("token_hash", { length: 255 }).notNull().unique(),
128
+ expiresAt: timestamp("expires_at").notNull(),
129
+ usedAt: timestamp("used_at"),
130
+ createdAt: timestamp("created_at").defaultNow().notNull()
131
+ });
132
+
121
133
  return {
122
134
  usersSchema,
123
135
  users,
@@ -127,7 +139,8 @@ export function createAuthSchema(usersSchemaName = "rebase") {
127
139
  userIdentities,
128
140
  mfaFactors,
129
141
  mfaChallenges,
130
- recoveryCodes
142
+ recoveryCodes,
143
+ magicLinkTokens
131
144
  };
132
145
  }
133
146
 
@@ -144,6 +157,7 @@ export const userIdentities = defaultAuthSchema.userIdentities;
144
157
  export const mfaFactors = defaultAuthSchema.mfaFactors;
145
158
  export const mfaChallenges = defaultAuthSchema.mfaChallenges;
146
159
  export const recoveryCodes = defaultAuthSchema.recoveryCodes;
160
+ export const magicLinkTokens = defaultAuthSchema.magicLinkTokens;
147
161
 
148
162
  // Relations
149
163
  export const usersRelations = relations(users, ({ many }) => ({
@@ -151,7 +165,8 @@ export const usersRelations = relations(users, ({ many }) => ({
151
165
  passwordResetTokens: many(passwordResetTokens),
152
166
  userIdentities: many(userIdentities),
153
167
  mfaFactors: many(mfaFactors),
154
- recoveryCodes: many(recoveryCodes)
168
+ recoveryCodes: many(recoveryCodes),
169
+ magicLinkTokens: many(magicLinkTokens)
155
170
  }));
156
171
 
157
172
  export const refreshTokensRelations = relations(refreshTokens, ({ one }) => ({
@@ -197,6 +212,13 @@ export const recoveryCodesRelations = relations(recoveryCodes, ({ one }) => ({
197
212
  })
198
213
  }));
199
214
 
215
+ export const magicLinkTokensRelations = relations(magicLinkTokens, ({ one }) => ({
216
+ user: one(users, {
217
+ fields: [magicLinkTokens.userId],
218
+ references: [users.id]
219
+ })
220
+ }));
221
+
200
222
  // Type exports
201
223
  export type User = typeof users.$inferSelect;
202
224
  export type NewUser = typeof users.$inferInsert;
@@ -208,3 +230,4 @@ export type NewUserIdentity = typeof userIdentities.$inferInsert;
208
230
  export type MfaFactorRow = typeof mfaFactors.$inferSelect;
209
231
  export type MfaChallengeRow = typeof mfaChallenges.$inferSelect;
210
232
  export type RecoveryCodeRow = typeof recoveryCodes.$inferSelect;
233
+ export type MagicLinkToken = typeof magicLinkTokens.$inferSelect;
@@ -143,8 +143,7 @@ export async function loadCollections(collectionsPath: string): Promise<EntityCo
143
143
  const filePath = path.join(resolvedPath, file);
144
144
  try {
145
145
  const fileUrl = pathToFileURL(filePath).href;
146
- const dynamicImport = new Function("url", "return import(url)");
147
- const mod = await dynamicImport(fileUrl);
146
+ const mod = await import(fileUrl);
148
147
  if (mod?.default) {
149
148
  collections.push(mod.default);
150
149
  }
@@ -156,8 +155,7 @@ export async function loadCollections(collectionsPath: string): Promise<EntityCo
156
155
  }
157
156
  } else {
158
157
  const fileUrl = pathToFileURL(resolvedPath).href + `?t=${Date.now()}`;
159
- const dynamicImport = new Function("url", "return import(url)");
160
- const imported = await dynamicImport(fileUrl);
158
+ const imported = await import(fileUrl);
161
159
  const loaded = imported.backendCollections || imported.collections;
162
160
  if (Array.isArray(loaded)) {
163
161
  collections.push(...loaded);
@@ -1,9 +1,10 @@
1
1
  import { EntityCollection, NumberProperty, Property, Relation, RelationProperty, SecurityOperation, SecurityRule, StringProperty, isPostgresCollection, DateProperty, ArrayProperty, MapProperty, ReferenceProperty, VectorProperty, BinaryProperty } from "@rebasepro/types";
2
2
  import { getPrimaryKeys } from "../services/entity-helpers";
3
- import { getEnumVarName, getTableName, getTableVarName, resolveCollectionRelations, findRelation } from "@rebasepro/common";
3
+ import { getEnumVarName, getTableName, getTableVarName, resolveCollectionRelations, findRelation, securityRuleToConditions, policyToPostgres } from "@rebasepro/common";
4
4
  import { toSnakeCase } from "@rebasepro/utils";
5
5
  import { createHash } from "crypto";
6
6
  import { logger } from "@rebasepro/server-core";
7
+ import { getEffectiveSecurityRules } from "./auth-default-policies";
7
8
  // --- Helper Functions ---
8
9
 
9
10
  /**
@@ -98,7 +99,7 @@ const getDrizzleColumn = (propName: string, prop: Property, collection: EntityCo
98
99
  columnDefinition = `uuid("${colName}")`;
99
100
  } else if (stringProp.columnType === "uuid") {
100
101
  columnDefinition = `uuid("${colName}")`;
101
- } else if (stringProp.columnType === "text") {
102
+ } else if (stringProp.columnType === "text" || stringProp.ui?.markdown || stringProp.ui?.multiline) {
102
103
  columnDefinition = `text("${colName}")`;
103
104
  } else if (stringProp.columnType === "char") {
104
105
  columnDefinition = `char("${colName}")`;
@@ -307,62 +308,9 @@ const getDrizzleColumn = (propName: string, prop: Property, collection: EntityCo
307
308
  };
308
309
 
309
310
  /**
310
- * Resolves a raw SQL string, replacing `{column_name}` with `${table.column_name}`.
311
- * The result is wrapped in a Drizzle sql`` template literal.
311
+ * Wraps a compiled SQL clause in a Drizzle `sql\`...\`` template literal.
312
312
  */
313
- const resolveRawSql = (expression: string): string => {
314
- // Replace {column_name} with column_name directly (so Drizzle-kit can parse it as a static string)
315
- const resolved = expression.replace(/\{(\w+)\}/g, (_, col) => col);
316
- return `sql\`${resolved}\``;
317
- };
318
-
319
- /**
320
- * Wraps a SQL clause with a role check using AND.
321
- * Generates: `(<clause>) AND (string_to_array(auth.roles(), ',') && ARRAY['<role1>','<role2>'])`
322
- */
323
- const wrapWithRoleCheck = (clause: string, roles: string[]): string => {
324
- const rolesArrayString = `ARRAY[${roles.map(r => `'${r}'`).join(",")}]`;
325
- const roleCondition = `string_to_array(auth.roles(), ',') && ${rolesArrayString}`;
326
- return `sql\`(${unwrapSql(clause)}) AND (${roleCondition})\``;
327
- };
328
-
329
- /**
330
- * Extracts the inner expression from a `sql\`...\`` wrapper.
331
- */
332
- const unwrapSql = (sqlExpr: string): string => {
333
- const match = sqlExpr.match(/^sql`(.*)`$/s);
334
- return match ? match[1] : sqlExpr;
335
- };
336
-
337
- /**
338
- * Builds the USING clause for a policy based on shortcuts or raw SQL.
339
- */
340
- const buildUsingClause = (rule: SecurityRule, collection: EntityCollection): string | null => {
341
- if (rule.using) {
342
- return resolveRawSql(rule.using);
343
- }
344
- if (rule.access === "public") {
345
- return "sql`true`";
346
- }
347
- if (rule.ownerField) {
348
- const prop = collection.properties?.[rule.ownerField];
349
- const colName = resolveColumnName(rule.ownerField, prop);
350
- return `sql\`${colName} = auth.uid()\``;
351
- }
352
- return null;
353
- };
354
-
355
- /**
356
- * Builds the WITH CHECK clause for a policy based on shortcuts or raw SQL.
357
- * Falls back to the USING clause if not explicitly provided.
358
- */
359
- const buildWithCheckClause = (rule: SecurityRule, collection: EntityCollection): string | null => {
360
- if (rule.withCheck) {
361
- return resolveRawSql(rule.withCheck);
362
- }
363
- // For insert/update/all, fall back to using clause if withCheck not specified
364
- return buildUsingClause(rule, collection);
365
- };
313
+ const wrapSql = (clause: string): string => `sql\`${clause}\``;
366
314
 
367
315
  /**
368
316
  * Generates a deterministic hash based on the rule configuration.
@@ -377,7 +325,9 @@ const getPolicyNameHash = (rule: SecurityRule): string => {
377
325
  rol: rule.roles?.slice().sort(),
378
326
  pg: rule.pgRoles?.slice().sort(),
379
327
  u: rule.using,
380
- w: rule.withCheck
328
+ w: rule.withCheck,
329
+ c: rule.condition,
330
+ ch: rule.check
381
331
  });
382
332
  return createHash("sha1").update(data).digest("hex").substring(0, 7);
383
333
  };
@@ -416,7 +366,6 @@ const generatePolicyCode = (collection: EntityCollection, rule: SecurityRule, in
416
366
  */
417
367
  const generateSinglePolicyCode = (collection: EntityCollection, rule: SecurityRule, operation: SecurityOperation, policyName: string): string => {
418
368
  const mode = rule.mode ?? "permissive";
419
- const roles = rule.roles ? [...rule.roles].sort() : undefined;
420
369
 
421
370
  // Determine which clauses this operation needs:
422
371
  // SELECT, DELETE → USING only
@@ -425,26 +374,13 @@ const generateSinglePolicyCode = (collection: EntityCollection, rule: SecurityRu
425
374
  const needsUsing = operation !== "insert";
426
375
  const needsWithCheck = operation !== "select" && operation !== "delete";
427
376
 
428
- let usingClause = needsUsing ? buildUsingClause(rule, collection) : null;
429
- let withCheckClause = needsWithCheck ? buildWithCheckClause(rule, collection) : null;
430
-
431
- // If roles are specified, wrap existing clauses with role check,
432
- // or generate a roles-only clause.
433
- if (roles && roles.length > 0) {
434
- if (usingClause) {
435
- usingClause = wrapWithRoleCheck(usingClause, roles);
436
- } else if (needsUsing) {
437
- // Roles-only rule (e.g. { operation: "select", roles: ["admin"] })
438
- const rolesArrayString = `ARRAY[${roles.map(r => `'${r}'`).join(",")}]`;
439
- usingClause = `sql\`string_to_array(auth.roles(), ',') && ${rolesArrayString}\``;
440
- }
441
- if (withCheckClause) {
442
- withCheckClause = wrapWithRoleCheck(withCheckClause, roles);
443
- } else if (needsWithCheck) {
444
- const rolesArrayString = `ARRAY[${roles.map(r => `'${r}'`).join(",")}]`;
445
- withCheckClause = `sql\`string_to_array(auth.roles(), ',') && ${rolesArrayString}\``;
446
- }
447
- }
377
+ // Desugar the rule (access / ownerField / roles / structured condition / raw
378
+ // SQL) into the shared PolicyExpression model, then compile to SQL — the same
379
+ // normalization the DDL generator and the client-side evaluator use.
380
+ const { usingExpr, withCheckExpr } = securityRuleToConditions(rule);
381
+
382
+ let usingClause = needsUsing && usingExpr ? wrapSql(policyToPostgres(usingExpr, collection)) : null;
383
+ let withCheckClause = needsWithCheck && withCheckExpr ? wrapSql(policyToPostgres(withCheckExpr, collection)) : null;
448
384
 
449
385
  // Fallback: if we still have no clauses, deny all (safety net)
450
386
  if (!usingClause && needsUsing) {
@@ -616,7 +552,9 @@ export const generateSchema = async (collections: EntityCollection[], stripPolic
616
552
  const enumVarName = getEnumVarName(collectionPath, propName);
617
553
  const enumDbName = `${collectionPath}_${resolveColumnName(propName, prop)}`;
618
554
  const values = Array.isArray(prop.enum)
619
- ? prop.enum.map(v => String(v.id ?? v))
555
+ ? (prop.enum as (string | number | { id: string | number })[]).map((v: string | number | { id: string | number }) =>
556
+ String(typeof v === "object" && v !== null && "id" in v ? v.id : v)
557
+ )
620
558
  : Object.keys(prop.enum);
621
559
  if (values.length > 0) {
622
560
  schemaContent += `export const ${enumVarName} = pgEnum(\"${enumDbName}\", [${values.map(v => `'${v}'`).join(", ")}]);\n`;
@@ -707,8 +645,8 @@ export const generateSchema = async (collections: EntityCollection[], stripPolic
707
645
 
708
646
  schemaContent += `${Array.from(columns).join(",\n")}`;
709
647
 
710
- const securityRules = isPostgresCollection(collection) ? collection.securityRules : undefined;
711
- if (!stripPolicies && securityRules && securityRules.length > 0) {
648
+ const securityRules = getEffectiveSecurityRules(collection);
649
+ if (!stripPolicies && securityRules.length > 0) {
712
650
  schemaContent += "\n}, (table) => ([\n";
713
651
  securityRules.forEach((rule: SecurityRule, idx: number) => {
714
652
  schemaContent += generatePolicyCode(collection, rule, idx);
@@ -68,8 +68,7 @@ const runGeneration = async (collectionsFilePath?: string, outputPath?: string)
68
68
  const filePath = path.join(resolvedPath, file);
69
69
  try {
70
70
  const fileUrl = pathToFileURL(filePath).href;
71
- const dynamicImport = new Function("url", "return import(url)");
72
- const module = await dynamicImport(fileUrl);
71
+ const module = await import(fileUrl);
73
72
  if (module && module.default) {
74
73
  collections.push(module.default);
75
74
  }
@@ -81,8 +80,7 @@ const runGeneration = async (collectionsFilePath?: string, outputPath?: string)
81
80
  }
82
81
  } else {
83
82
  const fileUrl = pathToFileURL(resolvedPath).href + `?t=${Date.now()}`;
84
- const dynamicImport = new Function("url", "return import(url)");
85
- const imported = await dynamicImport(fileUrl);
83
+ const imported = await import(fileUrl);
86
84
  collections = imported.backendCollections || imported.collections;
87
85
  }
88
86
 
@@ -101,7 +99,7 @@ const runGeneration = async (collectionsFilePath?: string, outputPath?: string)
101
99
  const outputDir = path.dirname(outputPath);
102
100
  await fsPromises.mkdir(outputDir, { recursive: true });
103
101
  await fsPromises.writeFile(outputPath, schemaContent);
104
- logger.info("✅ Drizzle schema generated successfully at", { detail: outputPath });
102
+ logger.info(`✅ Drizzle schema generated successfully at ${outputPath}`);
105
103
  } else {
106
104
  logger.info("✅ Drizzle schema generated successfully.");
107
105
  logger.info(String(schemaContent));