@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.
- package/dist/common/src/collections/default-collections.d.ts +12 -0
- package/dist/common/src/collections/index.d.ts +1 -0
- package/dist/common/src/util/permissions.d.ts +1 -0
- package/dist/index.es.js +844 -160
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +842 -158
- package/dist/index.umd.js.map +1 -1
- package/dist/server-postgresql/src/PostgresBackendDriver.d.ts +1 -1
- package/dist/server-postgresql/src/PostgresBootstrapper.d.ts +1 -0
- package/dist/server-postgresql/src/auth/services.d.ts +43 -1
- package/dist/server-postgresql/src/connection.d.ts +25 -0
- package/dist/server-postgresql/src/schema/auth-schema.d.ts +2382 -35
- package/dist/server-postgresql/src/services/EntityFetchService.d.ts +4 -0
- package/dist/server-postgresql/src/services/entityService.d.ts +2 -0
- package/dist/server-postgresql/src/services/realtimeService.d.ts +20 -0
- package/dist/server-postgresql/src/utils/drizzle-conditions.d.ts +18 -0
- package/dist/types/src/controllers/auth.d.ts +2 -24
- package/dist/types/src/controllers/client.d.ts +0 -3
- package/dist/types/src/controllers/collection_registry.d.ts +1 -1
- package/dist/types/src/controllers/data_driver.d.ts +18 -0
- package/dist/types/src/controllers/registry.d.ts +5 -4
- package/dist/types/src/rebase_context.d.ts +1 -1
- package/dist/types/src/types/auth_adapter.d.ts +2 -4
- package/dist/types/src/types/collections.d.ts +0 -4
- package/dist/types/src/types/component_ref.d.ts +1 -1
- package/dist/types/src/types/cron.d.ts +1 -1
- package/dist/types/src/types/entity_views.d.ts +1 -0
- package/dist/types/src/types/export_import.d.ts +1 -1
- package/dist/types/src/types/formex.d.ts +2 -2
- package/dist/types/src/types/properties.d.ts +2 -2
- package/dist/types/src/types/translations.d.ts +28 -12
- package/dist/types/src/types/user_management_delegate.d.ts +6 -4
- package/dist/types/src/users/roles.d.ts +0 -8
- package/package.json +6 -6
- package/src/PostgresBackendDriver.ts +4 -2
- package/src/PostgresBootstrapper.ts +27 -8
- package/src/auth/ensure-tables.ts +79 -17
- package/src/auth/services.ts +292 -23
- package/src/connection.ts +77 -0
- package/src/data-transformer.ts +2 -2
- package/src/schema/auth-schema.ts +80 -14
- package/src/schema/generate-drizzle-schema.ts +6 -6
- package/src/services/EntityFetchService.ts +69 -10
- package/src/services/entityService.ts +2 -0
- package/src/services/realtimeService.ts +214 -2
- package/src/utils/drizzle-conditions.ts +74 -2
- package/src/websocket.ts +10 -2
- package/test/auth-services.test.ts +15 -28
- package/test/drizzle-conditions.test.ts +168 -0
- 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
|
+
}
|
package/src/data-transformer.ts
CHANGED
|
@@ -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
|
|
267
|
-
return (value as
|
|
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
|
|
12
|
-
const usersTableCreator
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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 "
|
|
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
|
-
//
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
collections.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
|
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 (
|
|
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.
|
|
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
|
-
|
|
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);
|