@rebasepro/server-postgresql 0.2.1 → 0.2.4

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 (57) hide show
  1. package/dist/common/src/collections/default-collections.d.ts +12 -0
  2. package/dist/common/src/collections/index.d.ts +1 -0
  3. package/dist/common/src/data/query_builder.d.ts +51 -0
  4. package/dist/common/src/index.d.ts +1 -0
  5. package/dist/common/src/util/permissions.d.ts +1 -0
  6. package/dist/index.es.js +1202 -369
  7. package/dist/index.es.js.map +1 -1
  8. package/dist/index.umd.js +1200 -367
  9. package/dist/index.umd.js.map +1 -1
  10. package/dist/server-postgresql/src/PostgresBackendDriver.d.ts +1 -1
  11. package/dist/server-postgresql/src/PostgresBootstrapper.d.ts +1 -0
  12. package/dist/server-postgresql/src/auth/services.d.ts +43 -1
  13. package/dist/server-postgresql/src/connection.d.ts +25 -0
  14. package/dist/server-postgresql/src/schema/auth-schema.d.ts +2382 -35
  15. package/dist/server-postgresql/src/services/EntityFetchService.d.ts +4 -0
  16. package/dist/server-postgresql/src/services/entityService.d.ts +2 -0
  17. package/dist/server-postgresql/src/services/realtimeService.d.ts +20 -0
  18. package/dist/server-postgresql/src/utils/drizzle-conditions.d.ts +18 -0
  19. package/dist/types/src/controllers/auth.d.ts +2 -24
  20. package/dist/types/src/controllers/client.d.ts +0 -3
  21. package/dist/types/src/controllers/collection_registry.d.ts +1 -1
  22. package/dist/types/src/controllers/data.d.ts +21 -0
  23. package/dist/types/src/controllers/data_driver.d.ts +18 -0
  24. package/dist/types/src/controllers/registry.d.ts +5 -4
  25. package/dist/types/src/rebase_context.d.ts +1 -1
  26. package/dist/types/src/types/auth_adapter.d.ts +2 -4
  27. package/dist/types/src/types/collections.d.ts +0 -4
  28. package/dist/types/src/types/component_ref.d.ts +1 -1
  29. package/dist/types/src/types/cron.d.ts +1 -1
  30. package/dist/types/src/types/entity_views.d.ts +1 -0
  31. package/dist/types/src/types/export_import.d.ts +1 -1
  32. package/dist/types/src/types/formex.d.ts +2 -2
  33. package/dist/types/src/types/properties.d.ts +2 -2
  34. package/dist/types/src/types/translations.d.ts +28 -12
  35. package/dist/types/src/types/user_management_delegate.d.ts +6 -4
  36. package/dist/types/src/users/roles.d.ts +0 -8
  37. package/package.json +7 -6
  38. package/src/PostgresBackendDriver.ts +13 -7
  39. package/src/PostgresBootstrapper.ts +27 -8
  40. package/src/auth/ensure-tables.ts +79 -17
  41. package/src/auth/services.ts +292 -23
  42. package/src/cli.ts +5 -0
  43. package/src/connection.ts +77 -0
  44. package/src/data-transformer.ts +2 -2
  45. package/src/schema/auth-schema.ts +80 -14
  46. package/src/schema/default-collections.ts +1 -0
  47. package/src/schema/doctor.ts +82 -41
  48. package/src/schema/generate-drizzle-schema.ts +6 -6
  49. package/src/services/EntityFetchService.ts +69 -10
  50. package/src/services/entityService.ts +2 -0
  51. package/src/services/realtimeService.ts +214 -2
  52. package/src/utils/drizzle-conditions.ts +74 -2
  53. package/src/websocket.ts +10 -2
  54. package/test/auth-services.test.ts +15 -28
  55. package/test/drizzle-conditions.test.ts +168 -0
  56. package/test/postgresDataDriver.test.ts +130 -1
  57. package/vite.config.ts +1 -1
@@ -8,8 +8,8 @@ export function createAuthSchema(rolesSchemaName: string = "rebase", usersSchema
8
8
  const rolesSchema = rolesSchemaName === "public" ? null : pgSchema(rolesSchemaName);
9
9
  const usersSchema = usersSchemaName === "public" ? null : pgSchema(usersSchemaName);
10
10
 
11
- const rolesTableCreator: any = rolesSchema ? rolesSchema.table.bind(rolesSchema) : pgTable;
12
- const usersTableCreator: any = usersSchema ? usersSchema.table.bind(usersSchema) : pgTable;
11
+ const rolesTableCreator = (rolesSchema ? rolesSchema.table.bind(rolesSchema) : pgTable) as typeof pgTable;
12
+ const usersTableCreator = (usersSchema ? usersSchema.table.bind(usersSchema) : pgTable) as typeof pgTable;
13
13
 
14
14
  /**
15
15
  * Users table - stores both email/password and OAuth users
@@ -23,7 +23,8 @@ export function createAuthSchema(rolesSchemaName: string = "rebase", usersSchema
23
23
  emailVerified: boolean("email_verified").default(false).notNull(),
24
24
  emailVerificationToken: varchar("email_verification_token", { length: 255 }),
25
25
  emailVerificationSentAt: timestamp("email_verification_sent_at"),
26
- metadata: jsonb("metadata").$type<Record<string, any>>().default({}).notNull(),
26
+ isAnonymous: boolean("is_anonymous").default(false).notNull(),
27
+ metadata: jsonb("metadata").$type<Record<string, unknown>>().default({}).notNull(),
27
28
  createdAt: timestamp("created_at").defaultNow().notNull(),
28
29
  updatedAt: timestamp("updated_at").defaultNow().notNull()
29
30
  });
@@ -48,12 +49,7 @@ export function createAuthSchema(rolesSchemaName: string = "rebase", usersSchema
48
49
  edit?: boolean;
49
50
  delete?: boolean;
50
51
  }>
51
- >(),
52
- config: jsonb("config").$type<{
53
- createCollections?: boolean;
54
- editCollections?: "own" | "all" | boolean;
55
- deleteCollections?: "own" | "all" | boolean;
56
- }>()
52
+ >()
57
53
  });
58
54
 
59
55
  /**
@@ -62,7 +58,7 @@ export function createAuthSchema(rolesSchemaName: string = "rebase", usersSchema
62
58
  const userRoles = rolesTableCreator("user_roles", {
63
59
  userId: uuid("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
64
60
  roleId: varchar("role_id", { length: 50 }).notNull().references(() => roles.id, { onDelete: "cascade" })
65
- }, (table: any) => ({
61
+ }, (table) => ({
66
62
  pk: primaryKey({ columns: [table.userId, table.roleId] })
67
63
  }));
68
64
 
@@ -77,7 +73,7 @@ export function createAuthSchema(rolesSchemaName: string = "rebase", usersSchema
77
73
  userAgent: varchar("user_agent", { length: 500 }),
78
74
  ipAddress: varchar("ip_address", { length: 45 }),
79
75
  createdAt: timestamp("created_at").defaultNow().notNull()
80
- }, (table: any) => ({
76
+ }, (table) => ({
81
77
  uniqueDeviceSession: unique("unique_device_session").on(table.userId, table.userAgent, table.ipAddress)
82
78
  }));
83
79
 
@@ -113,10 +109,47 @@ export function createAuthSchema(rolesSchemaName: string = "rebase", usersSchema
113
109
  profileData: jsonb("profile_data"),
114
110
  createdAt: timestamp("created_at").defaultNow().notNull(),
115
111
  updatedAt: timestamp("updated_at").defaultNow().notNull()
116
- }, (table: any) => ({
112
+ }, (table) => ({
117
113
  uniqueProviderId: unique("unique_provider_id").on(table.provider, table.providerId)
118
114
  }));
119
115
 
116
+ /**
117
+ * MFA factors table - stores enrolled MFA methods
118
+ */
119
+ const mfaFactors = rolesTableCreator("mfa_factors", {
120
+ id: uuid("id").defaultRandom().primaryKey(),
121
+ userId: uuid("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
122
+ factorType: varchar("factor_type", { length: 20 }).notNull(), // 'totp'
123
+ secretEncrypted: varchar("secret_encrypted", { length: 500 }).notNull(),
124
+ friendlyName: varchar("friendly_name", { length: 255 }),
125
+ verified: boolean("verified").default(false).notNull(),
126
+ createdAt: timestamp("created_at").defaultNow().notNull(),
127
+ updatedAt: timestamp("updated_at").defaultNow().notNull()
128
+ });
129
+
130
+ /**
131
+ * MFA challenges table - tracks active MFA verification attempts
132
+ */
133
+ const mfaChallenges = rolesTableCreator("mfa_challenges", {
134
+ id: uuid("id").defaultRandom().primaryKey(),
135
+ factorId: uuid("factor_id").notNull().references(() => mfaFactors.id, { onDelete: "cascade" }),
136
+ createdAt: timestamp("created_at").defaultNow().notNull(),
137
+ verifiedAt: timestamp("verified_at"),
138
+ ipAddress: varchar("ip_address", { length: 45 }),
139
+ expiresAt: timestamp("expires_at").notNull()
140
+ });
141
+
142
+ /**
143
+ * Recovery codes table - backup codes for MFA
144
+ */
145
+ const recoveryCodes = rolesTableCreator("recovery_codes", {
146
+ id: uuid("id").defaultRandom().primaryKey(),
147
+ userId: uuid("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
148
+ codeHash: varchar("code_hash", { length: 255 }).notNull(),
149
+ usedAt: timestamp("used_at"),
150
+ createdAt: timestamp("created_at").defaultNow().notNull()
151
+ });
152
+
120
153
  return {
121
154
  rolesSchema,
122
155
  usersSchema,
@@ -126,7 +159,10 @@ export function createAuthSchema(rolesSchemaName: string = "rebase", usersSchema
126
159
  refreshTokens,
127
160
  passwordResetTokens,
128
161
  appConfig,
129
- userIdentities
162
+ userIdentities,
163
+ mfaFactors,
164
+ mfaChallenges,
165
+ recoveryCodes
130
166
  };
131
167
  }
132
168
 
@@ -143,13 +179,18 @@ export const refreshTokens = defaultAuthSchema.refreshTokens;
143
179
  export const passwordResetTokens = defaultAuthSchema.passwordResetTokens;
144
180
  export const appConfig = defaultAuthSchema.appConfig;
145
181
  export const userIdentities = defaultAuthSchema.userIdentities;
182
+ export const mfaFactors = defaultAuthSchema.mfaFactors;
183
+ export const mfaChallenges = defaultAuthSchema.mfaChallenges;
184
+ export const recoveryCodes = defaultAuthSchema.recoveryCodes;
146
185
 
147
186
  // Relations
148
187
  export const usersRelations = relations(users, ({ many }) => ({
149
188
  userRoles: many(userRoles),
150
189
  refreshTokens: many(refreshTokens),
151
190
  passwordResetTokens: many(passwordResetTokens),
152
- userIdentities: many(userIdentities)
191
+ userIdentities: many(userIdentities),
192
+ mfaFactors: many(mfaFactors),
193
+ recoveryCodes: many(recoveryCodes)
153
194
  }));
154
195
 
155
196
  export const rolesRelations = relations(roles, ({ many }) => ({
@@ -188,6 +229,28 @@ export const userIdentitiesRelations = relations(userIdentities, ({ one }) => ({
188
229
  })
189
230
  }));
190
231
 
232
+ export const mfaFactorsRelations = relations(mfaFactors, ({ one, many }) => ({
233
+ user: one(users, {
234
+ fields: [mfaFactors.userId],
235
+ references: [users.id]
236
+ }),
237
+ challenges: many(mfaChallenges)
238
+ }));
239
+
240
+ export const mfaChallengesRelations = relations(mfaChallenges, ({ one }) => ({
241
+ factor: one(mfaFactors, {
242
+ fields: [mfaChallenges.factorId],
243
+ references: [mfaFactors.id]
244
+ })
245
+ }));
246
+
247
+ export const recoveryCodesRelations = relations(recoveryCodes, ({ one }) => ({
248
+ user: one(users, {
249
+ fields: [recoveryCodes.userId],
250
+ references: [users.id]
251
+ })
252
+ }));
253
+
191
254
  // Type exports
192
255
  export type User = typeof users.$inferSelect;
193
256
  export type NewUser = typeof users.$inferInsert;
@@ -199,3 +262,6 @@ export type PasswordResetToken = typeof passwordResetTokens.$inferSelect;
199
262
  export type AppConfig = typeof appConfig.$inferSelect;
200
263
  export type UserIdentity = typeof userIdentities.$inferSelect;
201
264
  export type NewUserIdentity = typeof userIdentities.$inferInsert;
265
+ export type MfaFactorRow = typeof mfaFactors.$inferSelect;
266
+ export type MfaChallengeRow = typeof mfaChallenges.$inferSelect;
267
+ export type RecoveryCodeRow = typeof recoveryCodes.$inferSelect;
@@ -5,6 +5,7 @@ export const defaultUsersCollection: PostgresCollection = {
5
5
  singularName: "User",
6
6
  slug: "users",
7
7
  table: "users",
8
+ schema: "rebase",
8
9
  icon: "Users",
9
10
  group: "Settings",
10
11
  properties: {
@@ -265,6 +265,8 @@ export async function checkCollectionsVsSdk(
265
265
  // ── Phase 2: Collections ↔ Database ──────────────────────────────────────
266
266
 
267
267
  interface DbColumn {
268
+ table_schema: string;
269
+ table_name: string;
268
270
  column_name: string;
269
271
  data_type: string;
270
272
  is_nullable: string;
@@ -288,27 +290,45 @@ export async function checkCollectionsVsDatabase(
288
290
  const { Pool } = pgModule.default ?? pgModule;
289
291
  const pool = new Pool({ connectionString: databaseUrl });
290
292
 
293
+ // Determine all schemas defined by the collections, plus public and rebase
294
+ const schemas = Array.from(new Set([
295
+ "public",
296
+ "rebase",
297
+ ...collections
298
+ .filter(isPostgresCollection)
299
+ .map(c => c.schema)
300
+ .filter((s): s is string => !!s)
301
+ ]));
302
+
291
303
  try {
292
- // Fetch all tables in the public schema
293
- const tablesResult = await pool.query<{ table_name: string }>(
294
- "SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' AND table_type = 'BASE TABLE'"
304
+ // Fetch all tables in the defined schemas
305
+ const tablesResult = await pool.query<{ table_schema: string; table_name: string }>(
306
+ `SELECT table_schema, table_name
307
+ FROM information_schema.tables
308
+ WHERE table_schema = ANY($1) AND table_type = 'BASE TABLE'`,
309
+ [schemas]
295
310
  );
296
- const existingTables = new Set(tablesResult.rows.map((r) => r.table_name));
311
+ const existingTables = new Set(tablesResult.rows.map((r) =>
312
+ r.table_schema === "public" ? r.table_name : `${r.table_schema}.${r.table_name}`
313
+ ));
297
314
 
298
- // Fetch all columns
315
+ // Fetch all columns in the defined schemas
299
316
  const columnsResult = await pool.query<DbColumn>(
300
- `SELECT table_name, column_name, data_type, is_nullable, udt_name
317
+ `SELECT table_schema, table_name, column_name, data_type, is_nullable, udt_name
301
318
  FROM information_schema.columns
302
- WHERE table_schema = 'public'
303
- ORDER BY table_name, ordinal_position`
319
+ WHERE table_schema = ANY($1)
320
+ ORDER BY table_schema, table_name, ordinal_position`,
321
+ [schemas]
304
322
  );
305
323
  const columnsByTable = new Map<string, DbColumn[]>();
306
324
  for (const row of columnsResult.rows) {
307
- const tableName = (row as unknown as Record<string, string>).table_name;
308
- if (!columnsByTable.has(tableName)) {
309
- columnsByTable.set(tableName, []);
325
+ const tableSchema = row.table_schema;
326
+ const tableName = row.table_name;
327
+ const key = tableSchema === "public" ? tableName : `${tableSchema}.${tableName}`;
328
+ if (!columnsByTable.has(key)) {
329
+ columnsByTable.set(key, []);
310
330
  }
311
- columnsByTable.get(tableName)!.push(row);
331
+ columnsByTable.get(key)!.push(row);
312
332
  }
313
333
 
314
334
  // Fetch enums
@@ -326,18 +346,22 @@ export async function checkCollectionsVsDatabase(
326
346
  enumsByName.get(row.enum_name)!.push(row.enum_value);
327
347
  }
328
348
 
329
- // Fetch foreign key constraints
349
+ // Fetch foreign key constraints in the defined schemas
330
350
  const fksResult = await pool.query<{
331
351
  constraint_name: string;
352
+ table_schema: string;
332
353
  table_name: string;
333
354
  column_name: string;
355
+ foreign_table_schema: string;
334
356
  foreign_table_name: string;
335
357
  foreign_column_name: string;
336
358
  }>(
337
359
  `SELECT
338
360
  tc.constraint_name,
361
+ tc.table_schema,
339
362
  tc.table_name,
340
363
  kcu.column_name,
364
+ ccu.table_schema AS foreign_table_schema,
341
365
  ccu.table_name AS foreign_table_name,
342
366
  ccu.column_name AS foreign_column_name
343
367
  FROM information_schema.table_constraints AS tc
@@ -345,14 +369,18 @@ export async function checkCollectionsVsDatabase(
345
369
  ON tc.constraint_name = kcu.constraint_name AND tc.table_schema = kcu.table_schema
346
370
  JOIN information_schema.constraint_column_usage AS ccu
347
371
  ON ccu.constraint_name = tc.constraint_name AND ccu.table_schema = tc.table_schema
348
- WHERE tc.constraint_type = 'FOREIGN KEY' AND tc.table_schema = 'public'`
372
+ WHERE tc.constraint_type = 'FOREIGN KEY' AND tc.table_schema = ANY($1)`,
373
+ [schemas]
349
374
  );
350
375
  const fksByTable = new Map<string, typeof fksResult.rows>();
351
376
  for (const row of fksResult.rows) {
352
- if (!fksByTable.has(row.table_name)) {
353
- fksByTable.set(row.table_name, []);
377
+ const tableSchema = row.table_schema;
378
+ const tableName = row.table_name;
379
+ const key = tableSchema === "public" ? tableName : `${tableSchema}.${tableName}`;
380
+ if (!fksByTable.has(key)) {
381
+ fksByTable.set(key, []);
354
382
  }
355
- fksByTable.get(row.table_name)!.push(row);
383
+ fksByTable.get(key)!.push(row);
356
384
  }
357
385
 
358
386
  // ── Compare each collection against the database ─────────────────
@@ -361,20 +389,22 @@ export async function checkCollectionsVsDatabase(
361
389
 
362
390
  for (const collection of postgresCollections) {
363
391
  const tableName = getTableName(collection);
392
+ const schemaName = collection.schema || "public";
393
+ const fullTableName = schemaName === "public" ? tableName : `${schemaName}.${tableName}`;
364
394
 
365
395
  // Check table existence
366
- if (!existingTables.has(tableName)) {
396
+ if (!existingTables.has(fullTableName)) {
367
397
  issues.push({
368
398
  severity: "error",
369
399
  category: "missing_table",
370
- table: tableName,
371
- message: `Table "${tableName}" does not exist in the database.`,
400
+ table: fullTableName,
401
+ message: `Table "${fullTableName}" does not exist in the database.`,
372
402
  fix: "Run `rebase db push` or `rebase db generate && rebase db migrate`"
373
403
  });
374
404
  continue; // Skip column checks for missing tables
375
405
  }
376
406
 
377
- const dbColumns = columnsByTable.get(tableName) ?? [];
407
+ const dbColumns = columnsByTable.get(fullTableName) ?? [];
378
408
  const dbColumnMap = new Map(dbColumns.map((c) => [c.column_name, c]));
379
409
 
380
410
  // System columns that Rebase always creates
@@ -392,27 +422,36 @@ export async function checkCollectionsVsDatabase(
392
422
  issues.push({
393
423
  severity: "error",
394
424
  category: "missing_column",
395
- table: tableName,
425
+ table: fullTableName,
396
426
  column: fkColName,
397
- message: `Foreign key column "${fkColName}" for relation "${propName}" is missing from table "${tableName}".`,
427
+ message: `Foreign key column "${fkColName}" for relation "${propName}" is missing from table "${fullTableName}".`,
398
428
  fix: "Run `rebase db push` or `rebase db generate && rebase db migrate`"
399
429
  });
400
430
  }
401
431
 
402
432
  // Check FK constraint exists
403
- const tableFks = fksByTable.get(tableName) ?? [];
404
- const hasFk = tableFks.some((fk) => fk.column_name === fkColName);
433
+ const tableFks = fksByTable.get(fullTableName) ?? [];
434
+ let targetTableName = "unknown";
435
+ let targetSchemaName = "public";
436
+ try {
437
+ const targetColl = relation.target();
438
+ targetTableName = getTableName(targetColl);
439
+ targetSchemaName = targetColl.schema || "public";
440
+ } catch { /* ignore */ }
441
+
442
+ const hasFk = tableFks.some((fk) =>
443
+ fk.column_name === fkColName &&
444
+ fk.foreign_table_name === targetTableName &&
445
+ fk.foreign_table_schema === targetSchemaName
446
+ );
447
+
405
448
  if (dbColumnMap.has(fkColName) && !hasFk) {
406
- let targetTableName = "unknown";
407
- try {
408
- targetTableName = getTableName(relation.target());
409
- } catch { /* ignore */ }
410
449
  issues.push({
411
450
  severity: "warning",
412
451
  category: "missing_foreign_key",
413
- table: tableName,
452
+ table: fullTableName,
414
453
  column: fkColName,
415
- message: `Column "${fkColName}" exists but has no FOREIGN KEY constraint referencing "${targetTableName}".`,
454
+ message: `Column "${fkColName}" exists but has no FOREIGN KEY constraint referencing "${targetSchemaName === "public" ? targetTableName : `${targetSchemaName}.${targetTableName}`}".`,
416
455
  fix: "Run `rebase db push` or add the constraint manually"
417
456
  });
418
457
  }
@@ -430,9 +469,9 @@ export async function checkCollectionsVsDatabase(
430
469
  issues.push({
431
470
  severity: "error",
432
471
  category: "missing_column",
433
- table: tableName,
472
+ table: fullTableName,
434
473
  column: colName,
435
- message: `Column "${colName}" is defined in collection "${collection.slug}" but missing from table "${tableName}".`,
474
+ message: `Column "${colName}" is defined in collection "${collection.slug}" but missing from table "${fullTableName}".`,
436
475
  fix: "Run `rebase db push` or `rebase db generate && rebase db migrate`"
437
476
  });
438
477
  continue;
@@ -450,11 +489,11 @@ export async function checkCollectionsVsDatabase(
450
489
  issues.push({
451
490
  severity: "warning",
452
491
  category: "type_mismatch",
453
- table: tableName,
492
+ table: fullTableName,
454
493
  column: colName,
455
494
  expected: prop.type === "vector" ? "vector" : expectedType,
456
495
  actual: dbCol.udt_name === "vector" ? "vector" : actualType,
457
- message: `Column "${colName}" in table "${tableName}": expected type "${prop.type === "vector" ? "vector" : expectedType}" but found "${dbCol.udt_name === "vector" ? "vector" : actualType}".`,
496
+ message: `Column "${colName}" in table "${fullTableName}": expected type "${prop.type === "vector" ? "vector" : expectedType}" but found "${dbCol.udt_name === "vector" ? "vector" : actualType}".`,
458
497
  fix: "Review collection property type or run a migration"
459
498
  });
460
499
  }
@@ -470,7 +509,7 @@ export async function checkCollectionsVsDatabase(
470
509
  issues.push({
471
510
  severity: "warning",
472
511
  category: "missing_enum",
473
- table: tableName,
512
+ table: fullTableName,
474
513
  column: colName,
475
514
  expected: enumName,
476
515
  message: `Enum type "${enumName}" is defined in collection but not found in the database.`,
@@ -492,11 +531,11 @@ export async function checkCollectionsVsDatabase(
492
531
  issues.push({
493
532
  severity: "warning",
494
533
  category: "enum_value_mismatch",
495
- table: tableName,
534
+ table: fullTableName,
496
535
  column: colName,
497
536
  expected: expectedValues.join(", "),
498
537
  actual: dbEnumValues.join(", "),
499
- message: `Enum values for "${colName}" in table "${tableName}" are out of sync (${parts.join("; ")}).`,
538
+ message: `Enum values for "${colName}" in table "${fullTableName}" are out of sync (${parts.join("; ")}).`,
500
539
  fix: "Run `rebase db push` to update the enum"
501
540
  });
502
541
  }
@@ -510,12 +549,14 @@ export async function checkCollectionsVsDatabase(
510
549
  for (const relation of Object.values(resolvedRelations)) {
511
550
  if (relation.cardinality === "many" && relation.direction === "owning" && relation.through) {
512
551
  const junctionTable = relation.through.table;
513
- if (!existingTables.has(junctionTable)) {
552
+ const junctionSchema = collection.schema || "public";
553
+ const fullJunctionTable = junctionSchema === "public" ? junctionTable : `${junctionSchema}.${junctionTable}`;
554
+ if (!existingTables.has(fullJunctionTable)) {
514
555
  issues.push({
515
556
  severity: "error",
516
557
  category: "missing_table",
517
- table: junctionTable,
518
- message: `Junction table "${junctionTable}" for many-to-many relation "${relation.relationName}" is missing.`,
558
+ table: fullJunctionTable,
559
+ message: `Junction table "${fullJunctionTable}" for many-to-many relation "${relation.relationName}" is missing.`,
519
560
  fix: "Run `rebase db push` or `rebase db generate && rebase db migrate`"
520
561
  });
521
562
  }
@@ -5,7 +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 { defaultUsersCollection } from "./default-collections";
8
+ import { defaultUsersCollection } from "@rebasepro/common";
9
9
 
10
10
  // --- Helper Functions ---
11
11
 
@@ -90,11 +90,11 @@ const runGeneration = async (collectionsFilePath?: string, outputPath?: string)
90
90
  collections = [];
91
91
  }
92
92
 
93
- // Inject default collections if not overridden by the developer
94
- const hasUsersCollection = collections.some(c => c.slug === "users");
95
- if (!hasUsersCollection) {
96
- collections.push(defaultUsersCollection);
97
- }
93
+ // Always inject defaults first; developer collections override via generic dedup
94
+ // Map keyed by slug last-write-wins, so developer collections overwrite defaults
95
+ collections = Array.from(
96
+ new Map([defaultUsersCollection, ...collections].map(c => [c.slug, c])).values()
97
+ );
98
98
 
99
99
  // Sort collections by slug alphabetically to ensure deterministic schema generation
100
100
  collections.sort((a, b) => a.slug.localeCompare(b.slug));
@@ -1,6 +1,7 @@
1
1
  import { and, asc, count, desc, eq, getTableName, gt, lt, or, SQL, TableRelationalConfig, TablesRelationalConfig } from "drizzle-orm";
2
2
  import { AnyPgColumn, PgTable } from "drizzle-orm/pg-core";
3
3
  import { Entity, EntityCollection, FilterValues, Relation } from "@rebasepro/types";
4
+ import type { VectorSearchParams } from "@rebasepro/types";
4
5
  import { resolveCollectionRelations, findRelation, createRelationRef, createRelationRefWithData } from "@rebasepro/common";
5
6
  import { DrizzleConditionBuilder } from "../utils/drizzle-conditions";
6
7
  import {
@@ -698,6 +699,7 @@ export class EntityFetchService {
698
699
  startAfter?: Record<string, unknown>;
699
700
  searchString?: string;
700
701
  databaseId?: string;
702
+ vectorSearch?: VectorSearchParams;
701
703
  } = {}
702
704
  ): Promise<Entity<M>[]> {
703
705
  const collection = getCollectionByPath(collectionPath, this.registry);
@@ -722,7 +724,9 @@ export class EntityFetchService {
722
724
  const withConfig = this.buildWithConfig(collection);
723
725
  const hasRelations = withConfig && Object.keys(withConfig).length > 0;
724
726
 
725
- if (qb && !options.searchString && !hasRelations) {
727
+ // Skip db.query path when vectorSearch is present — it doesn't support
728
+ // custom SELECT expressions needed for the _distance column.
729
+ if (qb && !options.searchString && !hasRelations && !options.vectorSearch) {
726
730
  try {
727
731
  const queryOpts = this.buildDrizzleQueryOptions<M>(
728
732
  table, idField, idInfo, options, collectionPath, undefined
@@ -746,7 +750,15 @@ export class EntityFetchService {
746
750
  }
747
751
 
748
752
  // Fallback: db.select + processEntityResults (N+1 for relations)
749
- let query = this.db.select().from(table).$dynamic();
753
+ // When vectorSearch is present, add _distance to the SELECT.
754
+ let vectorMeta: { orderBy: SQL; filter?: SQL; distanceSelect: SQL } | undefined;
755
+ if (options.vectorSearch) {
756
+ vectorMeta = DrizzleConditionBuilder.buildVectorSearchConditions(table, options.vectorSearch);
757
+ }
758
+
759
+ let query = vectorMeta
760
+ ? this.db.select({ table_row: table, _distance: vectorMeta.distanceSelect }).from(table).$dynamic()
761
+ : this.db.select().from(table).$dynamic();
750
762
  const allConditions: SQL[] = [];
751
763
 
752
764
  if (options.searchString) {
@@ -762,13 +774,21 @@ export class EntityFetchService {
762
774
  if (filterConditions.length > 0) allConditions.push(...filterConditions);
763
775
  }
764
776
 
777
+ // Vector distance threshold filter
778
+ if (vectorMeta?.filter) {
779
+ allConditions.push(vectorMeta.filter);
780
+ }
781
+
765
782
  if (allConditions.length > 0) {
766
783
  const finalCondition = DrizzleConditionBuilder.combineConditionsWithAnd(allConditions);
767
784
  if (finalCondition) query = query.where(finalCondition);
768
785
  }
769
786
 
770
787
  const orderExpressions = [];
771
- if (options.orderBy) {
788
+ // Vector search overrides ORDER BY with distance (ascending = closest first)
789
+ if (vectorMeta) {
790
+ orderExpressions.push(asc(vectorMeta.orderBy));
791
+ } else if (options.orderBy) {
772
792
  const orderByField = this.resolveOrderByField(table, options.orderBy, collection);
773
793
  if (orderByField) {
774
794
  orderExpressions.push(options.order === "asc" ? asc(orderByField) : desc(orderByField));
@@ -786,13 +806,24 @@ export class EntityFetchService {
786
806
  }
787
807
  }
788
808
 
789
- const limitValue = options.searchString ? (options.limit || 50) : options.limit;
809
+ const limitValue = options.vectorSearch
810
+ ? (options.limit || 10)
811
+ : options.searchString ? (options.limit || 50) : options.limit;
790
812
  if (limitValue) query = query.limit(limitValue);
791
813
 
792
814
  // Offset (numeric pagination)
793
815
  if (options.offset && options.offset > 0) query = query.offset(options.offset);
794
816
 
795
- const results = await query;
817
+ const rawResults = await query;
818
+
819
+ // When vector search is active, unwrap the nested select shape and
820
+ // attach _distance to each entity's values.
821
+ const results = vectorMeta
822
+ ? (rawResults as { table_row: Record<string, unknown>; _distance: unknown }[]).map(r => ({
823
+ ...r.table_row,
824
+ _distance: typeof r._distance === "number" ? r._distance : parseFloat(String(r._distance))
825
+ }))
826
+ : rawResults as Record<string, unknown>[];
796
827
 
797
828
  return this.processEntityResults<M>(results, collection, collectionPath, idInfo, options.databaseId, false, idInfoArray);
798
829
  }
@@ -919,6 +950,7 @@ export class EntityFetchService {
919
950
  startAfter?: Record<string, unknown>;
920
951
  searchString?: string;
921
952
  databaseId?: string;
953
+ vectorSearch?: VectorSearchParams;
922
954
  } = {}
923
955
  ): Promise<Entity<M>[]> {
924
956
  // Handle multi-segment paths by resolving through relations
@@ -1164,6 +1196,7 @@ export class EntityFetchService {
1164
1196
  startAfter?: Record<string, unknown>;
1165
1197
  searchString?: string;
1166
1198
  databaseId?: string;
1199
+ vectorSearch?: VectorSearchParams;
1167
1200
  } = {},
1168
1201
  include?: string[]
1169
1202
  ): Promise<Record<string, unknown>[]> {
@@ -1181,7 +1214,8 @@ export class EntityFetchService {
1181
1214
  const tableName = getTableName(table);
1182
1215
 
1183
1216
  const qb = this.getQueryBuilder(tableName);
1184
- if (qb && !options.searchString) {
1217
+ // Skip db.query path when vectorSearch is present — needs custom SELECT
1218
+ if (qb && !options.searchString && !options.vectorSearch) {
1185
1219
  try {
1186
1220
  const withConfig = (include && include.length > 0)
1187
1221
  ? this.buildWithConfig(collection, include)
@@ -1389,6 +1423,7 @@ export class EntityFetchService {
1389
1423
  offset?: number;
1390
1424
  startAfter?: Record<string, unknown>;
1391
1425
  searchString?: string;
1426
+ vectorSearch?: VectorSearchParams;
1392
1427
  } = {}
1393
1428
  ): Promise<Record<string, unknown>[]> {
1394
1429
  const collection = getCollectionByPath(collectionPath, this.registry);
@@ -1397,7 +1432,14 @@ export class EntityFetchService {
1397
1432
  const idInfo = idInfoArray[0];
1398
1433
  const idField = table[idInfo.fieldName as keyof typeof table] as AnyPgColumn;
1399
1434
 
1400
- let query = this.db.select().from(table).$dynamic();
1435
+ let vectorMeta: { orderBy: SQL; filter?: SQL; distanceSelect: SQL } | undefined;
1436
+ if (options.vectorSearch) {
1437
+ vectorMeta = DrizzleConditionBuilder.buildVectorSearchConditions(table, options.vectorSearch);
1438
+ }
1439
+
1440
+ let query = vectorMeta
1441
+ ? this.db.select({ table_row: table, _distance: vectorMeta.distanceSelect }).from(table).$dynamic()
1442
+ : this.db.select().from(table).$dynamic();
1401
1443
  const allConditions: SQL[] = [];
1402
1444
 
1403
1445
  if (options.searchString) {
@@ -1413,13 +1455,19 @@ export class EntityFetchService {
1413
1455
  if (filterConditions.length > 0) allConditions.push(...filterConditions);
1414
1456
  }
1415
1457
 
1458
+ if (vectorMeta?.filter) {
1459
+ allConditions.push(vectorMeta.filter);
1460
+ }
1461
+
1416
1462
  if (allConditions.length > 0) {
1417
1463
  const finalCondition = DrizzleConditionBuilder.combineConditionsWithAnd(allConditions);
1418
1464
  if (finalCondition) query = query.where(finalCondition);
1419
1465
  }
1420
1466
 
1421
1467
  const orderExpressions = [];
1422
- if (options.orderBy) {
1468
+ if (vectorMeta) {
1469
+ orderExpressions.push(asc(vectorMeta.orderBy));
1470
+ } else if (options.orderBy) {
1423
1471
  const orderByField = this.resolveOrderByField(table, options.orderBy, collection);
1424
1472
  if (orderByField) {
1425
1473
  orderExpressions.push(options.order === "asc" ? asc(orderByField) : desc(orderByField));
@@ -1428,13 +1476,24 @@ export class EntityFetchService {
1428
1476
  orderExpressions.push(desc(idField));
1429
1477
  if (orderExpressions.length > 0) query = query.orderBy(...orderExpressions);
1430
1478
 
1431
- const limitValue = options.searchString ? (options.limit || 50) : options.limit;
1479
+ const limitValue = options.vectorSearch
1480
+ ? (options.limit || 10)
1481
+ : options.searchString ? (options.limit || 50) : options.limit;
1432
1482
  if (limitValue) query = query.limit(limitValue);
1433
1483
 
1434
1484
  // Offset (numeric pagination)
1435
1485
  if (options.offset && options.offset > 0) query = query.offset(options.offset);
1436
1486
 
1437
- return await query as Record<string, unknown>[];
1487
+ const rawResults = await query;
1488
+
1489
+ if (vectorMeta) {
1490
+ return (rawResults as { table_row: Record<string, unknown>; _distance: unknown }[]).map(r => ({
1491
+ ...r.table_row,
1492
+ _distance: typeof r._distance === "number" ? r._distance : parseFloat(String(r._distance))
1493
+ }));
1494
+ }
1495
+
1496
+ return rawResults as Record<string, unknown>[];
1438
1497
  }
1439
1498
 
1440
1499
  /**
@@ -1,5 +1,6 @@
1
1
  // import { NodePgDatabase } from "drizzle-orm/node-postgres";
2
2
  import { Entity, FilterValues } from "@rebasepro/types";
3
+ import type { VectorSearchParams } from "@rebasepro/types";
3
4
  import { EntityFetchService } from "./EntityFetchService";
4
5
  import { EntityPersistService } from "./EntityPersistService";
5
6
  import { RelationService } from "./RelationService";
@@ -66,6 +67,7 @@ export class EntityService implements EntityRepository {
66
67
  startAfter?: Record<string, unknown>;
67
68
  searchString?: string;
68
69
  databaseId?: string;
70
+ vectorSearch?: VectorSearchParams;
69
71
  } = {}
70
72
  ): Promise<Entity<M>[]> {
71
73
  return this.fetchService.fetchCollection<M>(collectionPath, options);