@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.
- 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/data/query_builder.d.ts +51 -0
- package/dist/common/src/index.d.ts +1 -0
- package/dist/common/src/util/permissions.d.ts +1 -0
- package/dist/index.es.js +1202 -369
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +1200 -367
- 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.d.ts +21 -0
- 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 +7 -6
- package/src/PostgresBackendDriver.ts +13 -7
- package/src/PostgresBootstrapper.ts +27 -8
- package/src/auth/ensure-tables.ts +79 -17
- package/src/auth/services.ts +292 -23
- package/src/cli.ts +5 -0
- package/src/connection.ts +77 -0
- package/src/data-transformer.ts +2 -2
- package/src/schema/auth-schema.ts +80 -14
- package/src/schema/default-collections.ts +1 -0
- package/src/schema/doctor.ts +82 -41
- 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/test/postgresDataDriver.test.ts +130 -1
- 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
|
|
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;
|
package/src/schema/doctor.ts
CHANGED
|
@@ -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
|
|
293
|
-
const tablesResult = await pool.query<{ table_name: string }>(
|
|
294
|
-
|
|
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) =>
|
|
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 =
|
|
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
|
|
308
|
-
|
|
309
|
-
|
|
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(
|
|
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 =
|
|
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
|
-
|
|
353
|
-
|
|
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(
|
|
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(
|
|
396
|
+
if (!existingTables.has(fullTableName)) {
|
|
367
397
|
issues.push({
|
|
368
398
|
severity: "error",
|
|
369
399
|
category: "missing_table",
|
|
370
|
-
table:
|
|
371
|
-
message: `Table "${
|
|
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(
|
|
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:
|
|
425
|
+
table: fullTableName,
|
|
396
426
|
column: fkColName,
|
|
397
|
-
message: `Foreign key column "${fkColName}" for relation "${propName}" is missing from table "${
|
|
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(
|
|
404
|
-
|
|
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:
|
|
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:
|
|
472
|
+
table: fullTableName,
|
|
434
473
|
column: colName,
|
|
435
|
-
message: `Column "${colName}" is defined in collection "${collection.slug}" but missing from table "${
|
|
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:
|
|
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 "${
|
|
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:
|
|
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:
|
|
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 "${
|
|
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
|
-
|
|
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:
|
|
518
|
-
message: `Junction table "${
|
|
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 "
|
|
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);
|