@rebasepro/server-postgresql 0.2.3 → 0.3.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 (50) 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/util/permissions.d.ts +1 -0
  4. package/dist/index.es.js +844 -160
  5. package/dist/index.es.js.map +1 -1
  6. package/dist/index.umd.js +842 -158
  7. package/dist/index.umd.js.map +1 -1
  8. package/dist/server-postgresql/src/PostgresBackendDriver.d.ts +1 -1
  9. package/dist/server-postgresql/src/PostgresBootstrapper.d.ts +1 -0
  10. package/dist/server-postgresql/src/auth/services.d.ts +43 -1
  11. package/dist/server-postgresql/src/connection.d.ts +25 -0
  12. package/dist/server-postgresql/src/schema/auth-schema.d.ts +2382 -35
  13. package/dist/server-postgresql/src/services/EntityFetchService.d.ts +4 -0
  14. package/dist/server-postgresql/src/services/entityService.d.ts +2 -0
  15. package/dist/server-postgresql/src/services/realtimeService.d.ts +20 -0
  16. package/dist/server-postgresql/src/utils/drizzle-conditions.d.ts +18 -0
  17. package/dist/types/src/controllers/auth.d.ts +2 -24
  18. package/dist/types/src/controllers/client.d.ts +0 -3
  19. package/dist/types/src/controllers/collection_registry.d.ts +1 -1
  20. package/dist/types/src/controllers/data_driver.d.ts +18 -0
  21. package/dist/types/src/controllers/registry.d.ts +5 -4
  22. package/dist/types/src/rebase_context.d.ts +1 -1
  23. package/dist/types/src/types/auth_adapter.d.ts +2 -4
  24. package/dist/types/src/types/collections.d.ts +0 -4
  25. package/dist/types/src/types/component_ref.d.ts +1 -1
  26. package/dist/types/src/types/cron.d.ts +1 -1
  27. package/dist/types/src/types/entity_views.d.ts +1 -0
  28. package/dist/types/src/types/export_import.d.ts +1 -1
  29. package/dist/types/src/types/formex.d.ts +2 -2
  30. package/dist/types/src/types/properties.d.ts +2 -2
  31. package/dist/types/src/types/translations.d.ts +28 -12
  32. package/dist/types/src/types/user_management_delegate.d.ts +6 -4
  33. package/dist/types/src/users/roles.d.ts +0 -8
  34. package/package.json +6 -6
  35. package/src/PostgresBackendDriver.ts +4 -2
  36. package/src/PostgresBootstrapper.ts +27 -8
  37. package/src/auth/ensure-tables.ts +79 -17
  38. package/src/auth/services.ts +292 -23
  39. package/src/connection.ts +77 -0
  40. package/src/data-transformer.ts +2 -2
  41. package/src/schema/auth-schema.ts +80 -14
  42. package/src/schema/generate-drizzle-schema.ts +6 -6
  43. package/src/services/EntityFetchService.ts +69 -10
  44. package/src/services/entityService.ts +2 -0
  45. package/src/services/realtimeService.ts +214 -2
  46. package/src/utils/drizzle-conditions.ts +74 -2
  47. package/src/websocket.ts +10 -2
  48. package/test/auth-services.test.ts +15 -28
  49. package/test/drizzle-conditions.test.ts +168 -0
  50. package/vite.config.ts +1 -1
package/src/connection.ts CHANGED
@@ -82,3 +82,80 @@ export function createPostgresDatabaseConnection(
82
82
  pool,
83
83
  connectionString };
84
84
  }
85
+
86
+ /**
87
+ * Create a direct (non-pooled) connection for operations that require
88
+ * session-level features incompatible with PgBouncer transaction mode,
89
+ * such as LISTEN/NOTIFY, prepared statements, or advisory locks.
90
+ *
91
+ * Uses a smaller pool since this is only for specific use cases.
92
+ */
93
+ export function createDirectDatabaseConnection(
94
+ connectionString: string,
95
+ schema?: Record<string, unknown>,
96
+ poolConfig?: PostgresPoolConfig
97
+ ) {
98
+ const opts = {
99
+ ...DEFAULT_POOL,
100
+ max: 5,
101
+ ...poolConfig
102
+ };
103
+
104
+ const pgPoolConfig: PoolConfig = {
105
+ connectionString,
106
+ max: opts.max,
107
+ idleTimeoutMillis: opts.idleTimeoutMillis,
108
+ connectionTimeoutMillis: opts.connectionTimeoutMillis,
109
+ query_timeout: opts.queryTimeout,
110
+ statement_timeout: opts.statementTimeout,
111
+ keepAlive: opts.keepAlive,
112
+ keepAliveInitialDelayMillis: 0
113
+ };
114
+
115
+ const pool = new Pool(pgPoolConfig);
116
+
117
+ pool.on("error", (err) => {
118
+ console.error("[pg-direct-pool] Unexpected pool error:", err.message);
119
+ });
120
+
121
+ const db = schema ? drizzle(pool, { schema }) : drizzle(pool);
122
+
123
+ return { db, pool, connectionString };
124
+ }
125
+
126
+ /**
127
+ * Create a read-only connection for routing read queries to replicas.
128
+ * Uses a moderate pool size since reads are distributed across replicas.
129
+ */
130
+ export function createReadReplicaConnection(
131
+ connectionString: string,
132
+ schema?: Record<string, unknown>,
133
+ poolConfig?: PostgresPoolConfig
134
+ ) {
135
+ const opts = {
136
+ ...DEFAULT_POOL,
137
+ max: 10,
138
+ ...poolConfig
139
+ };
140
+
141
+ const pgPoolConfig: PoolConfig = {
142
+ connectionString,
143
+ max: opts.max,
144
+ idleTimeoutMillis: opts.idleTimeoutMillis,
145
+ connectionTimeoutMillis: opts.connectionTimeoutMillis,
146
+ query_timeout: opts.queryTimeout,
147
+ statement_timeout: opts.statementTimeout,
148
+ keepAlive: opts.keepAlive,
149
+ keepAliveInitialDelayMillis: 0
150
+ };
151
+
152
+ const pool = new Pool(pgPoolConfig);
153
+
154
+ pool.on("error", (err) => {
155
+ console.error("[pg-replica-pool] Unexpected pool error:", err.message);
156
+ });
157
+
158
+ const db = schema ? drizzle(pool, { schema }) : drizzle(pool);
159
+
160
+ return { db, pool, connectionString };
161
+ }
@@ -263,8 +263,8 @@ export function serializePropertyToServer(value: unknown, property: Property): u
263
263
  if (value instanceof Vector) {
264
264
  return value.value;
265
265
  }
266
- if (value && typeof value === "object" && "value" in value && Array.isArray((value as any).value)) {
267
- return (value as any).value.map(Number);
266
+ if (value && typeof value === "object" && "value" in value && Array.isArray((value as { value: unknown }).value)) {
267
+ return (value as { value: unknown[] }).value.map(Number);
268
268
  }
269
269
  if (Array.isArray(value)) {
270
270
  return value.map(Number);
@@ -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,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);