@rebasepro/server-postgresql 0.3.0 → 0.5.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/README.md +69 -89
- package/dist/common/src/collections/default-collections.d.ts +5 -8
- package/dist/common/src/data/query_builder.d.ts +6 -2
- package/dist/common/src/util/permissions.d.ts +14 -6
- package/dist/index.es.js +379 -611
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +375 -607
- package/dist/index.umd.js.map +1 -1
- package/dist/server-postgresql/src/PostgresBackendDriver.d.ts +2 -0
- package/dist/server-postgresql/src/auth/ensure-tables.d.ts +7 -4
- package/dist/server-postgresql/src/auth/services.d.ts +17 -42
- package/dist/server-postgresql/src/data-transformer.d.ts +0 -3
- package/dist/server-postgresql/src/databasePoolManager.d.ts +1 -1
- package/dist/server-postgresql/src/schema/auth-schema.d.ts +87 -340
- package/dist/server-postgresql/src/services/EntityFetchService.d.ts +2 -1
- package/dist/server-postgresql/src/services/EntityPersistService.d.ts +4 -0
- package/dist/server-postgresql/src/services/entityService.d.ts +4 -0
- package/dist/server-postgresql/src/types.d.ts +3 -0
- package/dist/server-postgresql/src/utils/drizzle-conditions.d.ts +5 -1
- package/dist/server-postgresql/src/websocket.d.ts +8 -3
- package/dist/types/src/controllers/auth.d.ts +2 -2
- package/dist/types/src/controllers/client.d.ts +25 -40
- package/dist/types/src/controllers/data.d.ts +21 -3
- package/dist/types/src/controllers/data_driver.d.ts +5 -0
- package/dist/types/src/controllers/email.d.ts +2 -0
- package/dist/types/src/types/auth_adapter.d.ts +3 -56
- package/dist/types/src/types/backend.d.ts +38 -3
- package/dist/types/src/types/backend_hooks.d.ts +2 -17
- package/dist/types/src/types/collections.d.ts +30 -6
- package/dist/types/src/types/entity_views.d.ts +19 -28
- package/dist/types/src/types/properties.d.ts +9 -15
- package/dist/types/src/types/user_management_delegate.d.ts +16 -53
- package/dist/types/src/users/index.d.ts +0 -1
- package/dist/types/src/users/user.d.ts +0 -1
- package/package.json +6 -6
- package/src/PostgresBackendDriver.ts +10 -0
- package/src/PostgresBootstrapper.ts +27 -22
- package/src/auth/ensure-tables.ts +82 -129
- package/src/auth/services.ts +99 -197
- package/src/cli.ts +50 -23
- package/src/data-transformer.ts +57 -95
- package/src/databasePoolManager.ts +2 -1
- package/src/schema/auth-schema.ts +13 -69
- package/src/schema/doctor.ts +44 -3
- package/src/schema/generate-drizzle-schema-logic.ts +33 -3
- package/src/schema/generate-drizzle-schema.ts +2 -6
- package/src/schema/introspect-db-logic.ts +7 -0
- package/src/services/EntityFetchService.ts +13 -1
- package/src/services/EntityPersistService.ts +38 -12
- package/src/services/entityService.ts +7 -0
- package/src/types.ts +4 -0
- package/src/utils/drizzle-conditions.ts +40 -5
- package/src/websocket.ts +38 -25
- package/test/auth-services.test.ts +7 -150
- package/test/doctor.test.ts +6 -2
- package/test/relation-pipeline-gaps.test.ts +315 -0
- package/dist/server-postgresql/src/schema/default-collections.d.ts +0 -2
- package/dist/types/src/users/roles.d.ts +0 -14
- package/drizzle.test.config.ts +0 -10
- package/src/schema/default-collections.ts +0 -69
package/src/data-transformer.ts
CHANGED
|
@@ -272,41 +272,19 @@ export function serializePropertyToServer(value: unknown, property: Property): u
|
|
|
272
272
|
return value;
|
|
273
273
|
}
|
|
274
274
|
|
|
275
|
-
case "binary":
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
if (base64Data) {
|
|
280
|
-
return Buffer.from(base64Data, "base64");
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
if (Buffer.isBuffer(value)) {
|
|
285
|
-
return value;
|
|
286
|
-
}
|
|
275
|
+
case "binary": {
|
|
276
|
+
const decoded = tryDecodeBase64DataUrl(value);
|
|
277
|
+
if (decoded) return decoded;
|
|
278
|
+
if (Buffer.isBuffer(value)) return value;
|
|
287
279
|
return value;
|
|
280
|
+
}
|
|
288
281
|
|
|
289
282
|
case "string":
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
if (base64Data) {
|
|
294
|
-
return Buffer.from(base64Data, "base64");
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
return value;
|
|
299
|
-
|
|
300
|
-
default:
|
|
301
|
-
if (typeof value === "string") {
|
|
302
|
-
if (value.startsWith("data:application/octet-stream;base64,")) {
|
|
303
|
-
const base64Data = value.split(",")[1];
|
|
304
|
-
if (base64Data) {
|
|
305
|
-
return Buffer.from(base64Data, "base64");
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
}
|
|
283
|
+
default: {
|
|
284
|
+
const decoded = tryDecodeBase64DataUrl(value);
|
|
285
|
+
if (decoded) return decoded;
|
|
309
286
|
return value;
|
|
287
|
+
}
|
|
310
288
|
}
|
|
311
289
|
}
|
|
312
290
|
|
|
@@ -486,24 +464,53 @@ export async function parseDataFromServer<M extends Record<string, unknown>>(
|
|
|
486
464
|
}
|
|
487
465
|
|
|
488
466
|
/**
|
|
489
|
-
*
|
|
467
|
+
* Try to decode a `data:application/octet-stream;base64,...` data URL string
|
|
468
|
+
* into a Buffer. Returns null if the value is not a matching data URL.
|
|
490
469
|
*/
|
|
491
|
-
|
|
492
|
-
if (
|
|
493
|
-
|
|
470
|
+
function tryDecodeBase64DataUrl(value: unknown): Buffer | null {
|
|
471
|
+
if (typeof value !== "string") return null;
|
|
472
|
+
if (!value.startsWith("data:application/octet-stream;base64,")) return null;
|
|
473
|
+
const base64Data = value.split(",")[1];
|
|
474
|
+
return base64Data ? Buffer.from(base64Data, "base64") : null;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* Try to resolve an unknown value into a Buffer.
|
|
479
|
+
* Handles native Buffers and `{ type: "Buffer", data: number[] }` objects (from JSON deserialization).
|
|
480
|
+
* Returns null if the value is not a buffer.
|
|
481
|
+
*/
|
|
482
|
+
function tryResolveBuffer(value: unknown): Buffer | null {
|
|
483
|
+
if (Buffer.isBuffer(value)) return value;
|
|
484
|
+
if (typeof value === "object" && value !== null) {
|
|
485
|
+
const rawVal = value as Record<string, unknown>;
|
|
486
|
+
if (rawVal.type === "Buffer" && Array.isArray(rawVal.data)) {
|
|
487
|
+
return Buffer.from(rawVal.data as number[]);
|
|
488
|
+
}
|
|
494
489
|
}
|
|
490
|
+
return null;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* Convert a Buffer to a UTF-8 string if all bytes are printable ASCII,
|
|
495
|
+
* otherwise return a base64 data URL.
|
|
496
|
+
*/
|
|
497
|
+
function bufferToStringOrBase64(buf: Buffer): string {
|
|
498
|
+
for (let i = 0; i < buf.length; i++) {
|
|
499
|
+
const b = buf[i];
|
|
500
|
+
// Allow standard printable ASCII + common whitespace (\r, \n, \t)
|
|
501
|
+
if ((b < 32 || b > 126) && b !== 9 && b !== 10 && b !== 13) {
|
|
502
|
+
return `data:application/octet-stream;base64,${buf.toString("base64")}`;
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
return buf.toString("utf8");
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
export function parsePropertyFromServer(value: unknown, property: Property, collection: EntityCollection, propertyKey?: string): unknown {
|
|
509
|
+
if (value === null || value === undefined) return value;
|
|
495
510
|
|
|
496
511
|
switch (property.type) {
|
|
497
512
|
case "binary": {
|
|
498
|
-
|
|
499
|
-
if (Buffer.isBuffer(value)) {
|
|
500
|
-
buf = value;
|
|
501
|
-
} else if (typeof value === "object" && value !== null) {
|
|
502
|
-
const rawVal = value as Record<string, unknown>;
|
|
503
|
-
if (rawVal.type === "Buffer" && Array.isArray(rawVal.data)) {
|
|
504
|
-
buf = Buffer.from(rawVal.data as number[]);
|
|
505
|
-
}
|
|
506
|
-
}
|
|
513
|
+
const buf = tryResolveBuffer(value);
|
|
507
514
|
if (buf) {
|
|
508
515
|
return `data:application/octet-stream;base64,${buf.toString("base64")}`;
|
|
509
516
|
}
|
|
@@ -514,32 +521,9 @@ export function parsePropertyFromServer(value: unknown, property: Property, coll
|
|
|
514
521
|
if (typeof value === "string") return value;
|
|
515
522
|
|
|
516
523
|
// Handle Buffer objects (e.g. from PostgreSQL bytea columns)
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
if (Buffer.isBuffer(value)) {
|
|
521
|
-
isBuffer = true;
|
|
522
|
-
buf = value;
|
|
523
|
-
} else if (typeof value === "object" && value !== null) {
|
|
524
|
-
const rawVal = value as Record<string, unknown>;
|
|
525
|
-
if (rawVal.type === "Buffer" && Array.isArray(rawVal.data)) {
|
|
526
|
-
isBuffer = true;
|
|
527
|
-
buf = Buffer.from(rawVal.data as number[]);
|
|
528
|
-
}
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
if (isBuffer && buf) {
|
|
532
|
-
// Heuristic: if all bytes are printable ASCII, return utf8, else base64
|
|
533
|
-
let isPrintable = true;
|
|
534
|
-
for (let i = 0; i < buf.length; i++) {
|
|
535
|
-
const b = buf[i];
|
|
536
|
-
// Allow standard printable ASCII + common whitespace (\r, \n, \t)
|
|
537
|
-
if ((b < 32 || b > 126) && b !== 9 && b !== 10 && b !== 13) {
|
|
538
|
-
isPrintable = false;
|
|
539
|
-
break;
|
|
540
|
-
}
|
|
541
|
-
}
|
|
542
|
-
return isPrintable ? buf.toString("utf8") : `data:application/octet-stream;base64,${buf.toString("base64")}`;
|
|
524
|
+
const buf = tryResolveBuffer(value);
|
|
525
|
+
if (buf) {
|
|
526
|
+
return bufferToStringOrBase64(buf);
|
|
543
527
|
}
|
|
544
528
|
|
|
545
529
|
if (typeof value === "object" && value !== null) {
|
|
@@ -665,32 +649,10 @@ export function parsePropertyFromServer(value: unknown, property: Property, coll
|
|
|
665
649
|
|
|
666
650
|
default: {
|
|
667
651
|
// Fallback for buffers in case they are mapped to something other than string
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
if (Buffer.isBuffer(value)) {
|
|
672
|
-
isBuffer = true;
|
|
673
|
-
buf = value;
|
|
674
|
-
} else if (typeof value === "object" && value !== null) {
|
|
675
|
-
const rawVal = value as Record<string, unknown>;
|
|
676
|
-
if (rawVal.type === "Buffer" && Array.isArray(rawVal.data)) {
|
|
677
|
-
isBuffer = true;
|
|
678
|
-
buf = Buffer.from(rawVal.data as number[]);
|
|
679
|
-
}
|
|
680
|
-
}
|
|
681
|
-
|
|
682
|
-
if (isBuffer && buf) {
|
|
683
|
-
let isPrintable = true;
|
|
684
|
-
for (let i = 0; i < buf.length; i++) {
|
|
685
|
-
const b = buf[i];
|
|
686
|
-
if ((b < 32 || b > 126) && b !== 9 && b !== 10 && b !== 13) {
|
|
687
|
-
isPrintable = false;
|
|
688
|
-
break;
|
|
689
|
-
}
|
|
690
|
-
}
|
|
691
|
-
return isPrintable ? buf.toString("utf8") : `data:application/octet-stream;base64,${buf.toString("base64")}`;
|
|
652
|
+
const buf = tryResolveBuffer(value);
|
|
653
|
+
if (buf) {
|
|
654
|
+
return bufferToStringOrBase64(buf);
|
|
692
655
|
}
|
|
693
|
-
|
|
694
656
|
return value;
|
|
695
657
|
}
|
|
696
658
|
}
|
|
@@ -18,7 +18,7 @@ export class DatabasePoolManager {
|
|
|
18
18
|
}
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
public getDrizzle(databaseName: string): NodePgDatabase<
|
|
21
|
+
public getDrizzle(databaseName: string): NodePgDatabase<Record<string, never>> {
|
|
22
22
|
const existing = this.drizzleInstances.get(databaseName);
|
|
23
23
|
if (existing) {
|
|
24
24
|
return existing;
|
|
@@ -81,5 +81,6 @@ export class DatabasePoolManager {
|
|
|
81
81
|
}
|
|
82
82
|
await Promise.all(promises);
|
|
83
83
|
this.pools.clear();
|
|
84
|
+
this.drizzleInstances.clear();
|
|
84
85
|
}
|
|
85
86
|
}
|
|
@@ -1,15 +1,14 @@
|
|
|
1
|
-
import { pgSchema, pgTable, varchar, uuid, timestamp, boolean, jsonb,
|
|
1
|
+
import { pgSchema, pgTable, varchar, uuid, timestamp, boolean, jsonb, text, unique } from "drizzle-orm/pg-core";
|
|
2
2
|
import { relations } from "drizzle-orm";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Factory function to dynamically create the auth tables bound to the specified schema names.
|
|
6
6
|
*/
|
|
7
|
-
export function createAuthSchema(
|
|
8
|
-
const rolesSchema = rolesSchemaName === "public" ? null : pgSchema(rolesSchemaName);
|
|
7
|
+
export function createAuthSchema(usersSchemaName: string = "rebase") {
|
|
9
8
|
const usersSchema = usersSchemaName === "public" ? null : pgSchema(usersSchemaName);
|
|
10
9
|
|
|
11
|
-
const
|
|
12
|
-
const usersTableCreator =
|
|
10
|
+
const tableCreator = (usersSchema ? usersSchema.table.bind(usersSchema) : pgTable) as typeof pgTable;
|
|
11
|
+
const usersTableCreator = tableCreator;
|
|
13
12
|
|
|
14
13
|
/**
|
|
15
14
|
* Users table - stores both email/password and OAuth users
|
|
@@ -24,48 +23,18 @@ export function createAuthSchema(rolesSchemaName: string = "rebase", usersSchema
|
|
|
24
23
|
emailVerificationToken: varchar("email_verification_token", { length: 255 }),
|
|
25
24
|
emailVerificationSentAt: timestamp("email_verification_sent_at"),
|
|
26
25
|
isAnonymous: boolean("is_anonymous").default(false).notNull(),
|
|
26
|
+
roles: text("roles").array().default([]).notNull(),
|
|
27
27
|
metadata: jsonb("metadata").$type<Record<string, unknown>>().default({}).notNull(),
|
|
28
28
|
createdAt: timestamp("created_at").defaultNow().notNull(),
|
|
29
29
|
updatedAt: timestamp("updated_at").defaultNow().notNull()
|
|
30
30
|
});
|
|
31
31
|
|
|
32
|
-
/**
|
|
33
|
-
* Roles table - defines permission sets
|
|
34
|
-
*/
|
|
35
|
-
const roles = rolesTableCreator("roles", {
|
|
36
|
-
id: varchar("id", { length: 50 }).primaryKey(), // 'admin', 'editor', 'viewer'
|
|
37
|
-
name: varchar("name", { length: 100 }).notNull(),
|
|
38
|
-
isAdmin: boolean("is_admin").default(false).notNull(),
|
|
39
|
-
defaultPermissions: jsonb("default_permissions").$type<{
|
|
40
|
-
read?: boolean;
|
|
41
|
-
create?: boolean;
|
|
42
|
-
edit?: boolean;
|
|
43
|
-
delete?: boolean;
|
|
44
|
-
}>(),
|
|
45
|
-
collectionPermissions: jsonb("collection_permissions").$type<
|
|
46
|
-
Record<string, {
|
|
47
|
-
read?: boolean;
|
|
48
|
-
create?: boolean;
|
|
49
|
-
edit?: boolean;
|
|
50
|
-
delete?: boolean;
|
|
51
|
-
}>
|
|
52
|
-
>()
|
|
53
|
-
});
|
|
54
32
|
|
|
55
|
-
/**
|
|
56
|
-
* User-Role junction table
|
|
57
|
-
*/
|
|
58
|
-
const userRoles = rolesTableCreator("user_roles", {
|
|
59
|
-
userId: uuid("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
|
|
60
|
-
roleId: varchar("role_id", { length: 50 }).notNull().references(() => roles.id, { onDelete: "cascade" })
|
|
61
|
-
}, (table) => ({
|
|
62
|
-
pk: primaryKey({ columns: [table.userId, table.roleId] })
|
|
63
|
-
}));
|
|
64
33
|
|
|
65
34
|
/**
|
|
66
35
|
* Refresh tokens for long-lived sessions
|
|
67
36
|
*/
|
|
68
|
-
const refreshTokens =
|
|
37
|
+
const refreshTokens = tableCreator("refresh_tokens", {
|
|
69
38
|
id: uuid("id").defaultRandom().primaryKey(),
|
|
70
39
|
userId: uuid("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
|
|
71
40
|
tokenHash: varchar("token_hash", { length: 255 }).notNull().unique(),
|
|
@@ -80,7 +49,7 @@ export function createAuthSchema(rolesSchemaName: string = "rebase", usersSchema
|
|
|
80
49
|
/**
|
|
81
50
|
* Password reset tokens for forgot password flow
|
|
82
51
|
*/
|
|
83
|
-
const passwordResetTokens =
|
|
52
|
+
const passwordResetTokens = tableCreator("password_reset_tokens", {
|
|
84
53
|
id: uuid("id").defaultRandom().primaryKey(),
|
|
85
54
|
userId: uuid("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
|
|
86
55
|
tokenHash: varchar("token_hash", { length: 255 }).notNull().unique(),
|
|
@@ -92,7 +61,7 @@ export function createAuthSchema(rolesSchemaName: string = "rebase", usersSchema
|
|
|
92
61
|
/**
|
|
93
62
|
* App config - key/value store for custom settings
|
|
94
63
|
*/
|
|
95
|
-
const appConfig =
|
|
64
|
+
const appConfig = tableCreator("app_config", {
|
|
96
65
|
key: varchar("key", { length: 100 }).primaryKey(),
|
|
97
66
|
value: jsonb("value").notNull(),
|
|
98
67
|
updatedAt: timestamp("updated_at").defaultNow().notNull()
|
|
@@ -101,7 +70,7 @@ export function createAuthSchema(rolesSchemaName: string = "rebase", usersSchema
|
|
|
101
70
|
/**
|
|
102
71
|
* User identities - maps external OAuth profiles back to local users
|
|
103
72
|
*/
|
|
104
|
-
const userIdentities =
|
|
73
|
+
const userIdentities = tableCreator("user_identities", {
|
|
105
74
|
id: uuid("id").defaultRandom().primaryKey(),
|
|
106
75
|
userId: uuid("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
|
|
107
76
|
provider: varchar("provider", { length: 50 }).notNull(), // e.g. 'google', 'linkedin'
|
|
@@ -116,7 +85,7 @@ export function createAuthSchema(rolesSchemaName: string = "rebase", usersSchema
|
|
|
116
85
|
/**
|
|
117
86
|
* MFA factors table - stores enrolled MFA methods
|
|
118
87
|
*/
|
|
119
|
-
const mfaFactors =
|
|
88
|
+
const mfaFactors = tableCreator("mfa_factors", {
|
|
120
89
|
id: uuid("id").defaultRandom().primaryKey(),
|
|
121
90
|
userId: uuid("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
|
|
122
91
|
factorType: varchar("factor_type", { length: 20 }).notNull(), // 'totp'
|
|
@@ -130,7 +99,7 @@ export function createAuthSchema(rolesSchemaName: string = "rebase", usersSchema
|
|
|
130
99
|
/**
|
|
131
100
|
* MFA challenges table - tracks active MFA verification attempts
|
|
132
101
|
*/
|
|
133
|
-
const mfaChallenges =
|
|
102
|
+
const mfaChallenges = tableCreator("mfa_challenges", {
|
|
134
103
|
id: uuid("id").defaultRandom().primaryKey(),
|
|
135
104
|
factorId: uuid("factor_id").notNull().references(() => mfaFactors.id, { onDelete: "cascade" }),
|
|
136
105
|
createdAt: timestamp("created_at").defaultNow().notNull(),
|
|
@@ -142,7 +111,7 @@ export function createAuthSchema(rolesSchemaName: string = "rebase", usersSchema
|
|
|
142
111
|
/**
|
|
143
112
|
* Recovery codes table - backup codes for MFA
|
|
144
113
|
*/
|
|
145
|
-
const recoveryCodes =
|
|
114
|
+
const recoveryCodes = tableCreator("recovery_codes", {
|
|
146
115
|
id: uuid("id").defaultRandom().primaryKey(),
|
|
147
116
|
userId: uuid("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
|
|
148
117
|
codeHash: varchar("code_hash", { length: 255 }).notNull(),
|
|
@@ -151,11 +120,8 @@ export function createAuthSchema(rolesSchemaName: string = "rebase", usersSchema
|
|
|
151
120
|
});
|
|
152
121
|
|
|
153
122
|
return {
|
|
154
|
-
rolesSchema,
|
|
155
123
|
usersSchema,
|
|
156
124
|
users,
|
|
157
|
-
roles,
|
|
158
|
-
userRoles,
|
|
159
125
|
refreshTokens,
|
|
160
126
|
passwordResetTokens,
|
|
161
127
|
appConfig,
|
|
@@ -167,14 +133,11 @@ export function createAuthSchema(rolesSchemaName: string = "rebase", usersSchema
|
|
|
167
133
|
}
|
|
168
134
|
|
|
169
135
|
// Instantiate default schema and tables using the default "rebase" schema
|
|
170
|
-
const defaultAuthSchema = createAuthSchema("rebase"
|
|
136
|
+
const defaultAuthSchema = createAuthSchema("rebase");
|
|
171
137
|
|
|
172
|
-
export const rebaseSchema = defaultAuthSchema.rolesSchema;
|
|
173
138
|
export const usersSchema = defaultAuthSchema.usersSchema;
|
|
174
139
|
|
|
175
140
|
export const users = defaultAuthSchema.users;
|
|
176
|
-
export const roles = defaultAuthSchema.roles;
|
|
177
|
-
export const userRoles = defaultAuthSchema.userRoles;
|
|
178
141
|
export const refreshTokens = defaultAuthSchema.refreshTokens;
|
|
179
142
|
export const passwordResetTokens = defaultAuthSchema.passwordResetTokens;
|
|
180
143
|
export const appConfig = defaultAuthSchema.appConfig;
|
|
@@ -185,7 +148,6 @@ export const recoveryCodes = defaultAuthSchema.recoveryCodes;
|
|
|
185
148
|
|
|
186
149
|
// Relations
|
|
187
150
|
export const usersRelations = relations(users, ({ many }) => ({
|
|
188
|
-
userRoles: many(userRoles),
|
|
189
151
|
refreshTokens: many(refreshTokens),
|
|
190
152
|
passwordResetTokens: many(passwordResetTokens),
|
|
191
153
|
userIdentities: many(userIdentities),
|
|
@@ -193,21 +155,6 @@ export const usersRelations = relations(users, ({ many }) => ({
|
|
|
193
155
|
recoveryCodes: many(recoveryCodes)
|
|
194
156
|
}));
|
|
195
157
|
|
|
196
|
-
export const rolesRelations = relations(roles, ({ many }) => ({
|
|
197
|
-
userRoles: many(userRoles)
|
|
198
|
-
}));
|
|
199
|
-
|
|
200
|
-
export const userRolesRelations = relations(userRoles, ({ one }) => ({
|
|
201
|
-
user: one(users, {
|
|
202
|
-
fields: [userRoles.userId],
|
|
203
|
-
references: [users.id]
|
|
204
|
-
}),
|
|
205
|
-
role: one(roles, {
|
|
206
|
-
fields: [userRoles.roleId],
|
|
207
|
-
references: [roles.id]
|
|
208
|
-
})
|
|
209
|
-
}));
|
|
210
|
-
|
|
211
158
|
export const refreshTokensRelations = relations(refreshTokens, ({ one }) => ({
|
|
212
159
|
user: one(users, {
|
|
213
160
|
fields: [refreshTokens.userId],
|
|
@@ -254,9 +201,6 @@ export const recoveryCodesRelations = relations(recoveryCodes, ({ one }) => ({
|
|
|
254
201
|
// Type exports
|
|
255
202
|
export type User = typeof users.$inferSelect;
|
|
256
203
|
export type NewUser = typeof users.$inferInsert;
|
|
257
|
-
export type Role = typeof roles.$inferSelect;
|
|
258
|
-
export type NewRole = typeof roles.$inferInsert;
|
|
259
|
-
export type UserRole = typeof userRoles.$inferSelect;
|
|
260
204
|
export type RefreshToken = typeof refreshTokens.$inferSelect;
|
|
261
205
|
export type PasswordResetToken = typeof passwordResetTokens.$inferSelect;
|
|
262
206
|
export type AppConfig = typeof appConfig.$inferSelect;
|
package/src/schema/doctor.ts
CHANGED
|
@@ -84,10 +84,28 @@ export function getExpectedColumnType(prop: Property): string | null {
|
|
|
84
84
|
if (dp.columnType === "time") return "time without time zone";
|
|
85
85
|
return "timestamp with time zone";
|
|
86
86
|
}
|
|
87
|
-
case "map":
|
|
88
87
|
case "array": {
|
|
89
|
-
const ap = prop as ArrayProperty
|
|
90
|
-
|
|
88
|
+
const ap = prop as ArrayProperty;
|
|
89
|
+
let colType = ap.columnType;
|
|
90
|
+
if (!colType && ap.of && !Array.isArray(ap.of)) {
|
|
91
|
+
const ofProp = ap.of as Property;
|
|
92
|
+
if (ofProp.type === "string") {
|
|
93
|
+
colType = "text[]";
|
|
94
|
+
} else if (ofProp.type === "number") {
|
|
95
|
+
colType = ofProp.validation?.integer ? "integer[]" : "numeric[]";
|
|
96
|
+
} else if (ofProp.type === "boolean") {
|
|
97
|
+
colType = "boolean[]";
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (colType === "json") return "json";
|
|
102
|
+
if (colType === "jsonb") return "jsonb";
|
|
103
|
+
if (colType && colType.endsWith("[]")) return "ARRAY";
|
|
104
|
+
return "jsonb";
|
|
105
|
+
}
|
|
106
|
+
case "map": {
|
|
107
|
+
const mp = prop as MapProperty;
|
|
108
|
+
if (mp.columnType === "json") return "json";
|
|
91
109
|
return "jsonb";
|
|
92
110
|
}
|
|
93
111
|
case "relation":
|
|
@@ -485,6 +503,29 @@ export async function checkCollectionsVsDatabase(
|
|
|
485
503
|
if (prop.type === "vector" && dbCol.udt_name !== "vector") {
|
|
486
504
|
isMismatch = true;
|
|
487
505
|
}
|
|
506
|
+
if (prop.type === "array") {
|
|
507
|
+
const ap = prop as ArrayProperty;
|
|
508
|
+
let expectedColType = ap.columnType;
|
|
509
|
+
if (!expectedColType && ap.of && !Array.isArray(ap.of)) {
|
|
510
|
+
const ofProp = ap.of as Property;
|
|
511
|
+
if (ofProp.type === "string") expectedColType = "text[]";
|
|
512
|
+
else if (ofProp.type === "number") expectedColType = ofProp.validation?.integer ? "integer[]" : "numeric[]";
|
|
513
|
+
else if (ofProp.type === "boolean") expectedColType = "boolean[]";
|
|
514
|
+
}
|
|
515
|
+
if (expectedColType && expectedColType.endsWith("[]")) {
|
|
516
|
+
if (actualType !== "ARRAY") {
|
|
517
|
+
isMismatch = true;
|
|
518
|
+
} else {
|
|
519
|
+
const expectedUdt = expectedColType === "text[]" ? "_text" :
|
|
520
|
+
expectedColType === "integer[]" ? "_int4" :
|
|
521
|
+
expectedColType === "boolean[]" ? "_bool" :
|
|
522
|
+
expectedColType === "numeric[]" ? "_numeric" : "";
|
|
523
|
+
if (expectedUdt && dbCol.udt_name !== expectedUdt) {
|
|
524
|
+
isMismatch = true;
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
}
|
|
488
529
|
if (isMismatch) {
|
|
489
530
|
issues.push({
|
|
490
531
|
severity: "warning",
|
|
@@ -70,6 +70,8 @@ const getDrizzleColumn = (propName: string, prop: Property, collection: EntityCo
|
|
|
70
70
|
columnDefinition = `${enumName}("${colName}")`;
|
|
71
71
|
} else if ("isId" in stringProp && stringProp.isId === "uuid") {
|
|
72
72
|
columnDefinition = `uuid("${colName}")`;
|
|
73
|
+
} else if (stringProp.columnType === "uuid") {
|
|
74
|
+
columnDefinition = `uuid("${colName}")`;
|
|
73
75
|
} else if (stringProp.columnType === "text") {
|
|
74
76
|
columnDefinition = `text("${colName}")`;
|
|
75
77
|
} else if (stringProp.columnType === "char") {
|
|
@@ -145,11 +147,39 @@ const getDrizzleColumn = (propName: string, prop: Property, collection: EntityCo
|
|
|
145
147
|
}
|
|
146
148
|
break;
|
|
147
149
|
}
|
|
148
|
-
case "map":
|
|
150
|
+
case "map": {
|
|
151
|
+
const mapProp = prop as MapProperty;
|
|
152
|
+
if (mapProp.columnType === "json") {
|
|
153
|
+
columnDefinition = `json("${colName}")`;
|
|
154
|
+
} else {
|
|
155
|
+
columnDefinition = `jsonb("${colName}")`;
|
|
156
|
+
}
|
|
157
|
+
break;
|
|
158
|
+
}
|
|
149
159
|
case "array": {
|
|
150
|
-
const
|
|
151
|
-
|
|
160
|
+
const arrayProp = prop as ArrayProperty;
|
|
161
|
+
let colType = arrayProp.columnType;
|
|
162
|
+
if (!colType && arrayProp.of && !Array.isArray(arrayProp.of)) {
|
|
163
|
+
const ofProp = arrayProp.of as Property;
|
|
164
|
+
if (ofProp.type === "string") {
|
|
165
|
+
colType = "text[]";
|
|
166
|
+
} else if (ofProp.type === "number") {
|
|
167
|
+
colType = ofProp.validation?.integer ? "integer[]" : "numeric[]";
|
|
168
|
+
} else if (ofProp.type === "boolean") {
|
|
169
|
+
colType = "boolean[]";
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (colType === "json") {
|
|
152
174
|
columnDefinition = `json("${colName}")`;
|
|
175
|
+
} else if (colType === "text[]") {
|
|
176
|
+
columnDefinition = `text("${colName}").array()`;
|
|
177
|
+
} else if (colType === "integer[]") {
|
|
178
|
+
columnDefinition = `integer("${colName}").array()`;
|
|
179
|
+
} else if (colType === "boolean[]") {
|
|
180
|
+
columnDefinition = `boolean("${colName}").array()`;
|
|
181
|
+
} else if (colType === "numeric[]") {
|
|
182
|
+
columnDefinition = `numeric("${colName}").array()`;
|
|
153
183
|
} else {
|
|
154
184
|
columnDefinition = `jsonb("${colName}")`;
|
|
155
185
|
}
|
|
@@ -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
|
-
|
|
8
|
+
|
|
9
9
|
|
|
10
10
|
// --- Helper Functions ---
|
|
11
11
|
|
|
@@ -90,11 +90,7 @@ const runGeneration = async (collectionsFilePath?: string, outputPath?: string)
|
|
|
90
90
|
collections = [];
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
-
|
|
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
|
-
);
|
|
93
|
+
|
|
98
94
|
|
|
99
95
|
// Sort collections by slug alphabetically to ensure deterministic schema generation
|
|
100
96
|
collections.sort((a, b) => a.slug.localeCompare(b.slug));
|
|
@@ -589,9 +589,16 @@ export function generateCollectionFile(
|
|
|
589
589
|
// Array/Map heuristics (Fallback if not inferred)
|
|
590
590
|
if (finalPropType === "array" && !inferenceExtra.includes("of: {")) {
|
|
591
591
|
let innerType = "string";
|
|
592
|
+
let colType = "";
|
|
592
593
|
if (col.udt_name.startsWith("_")) {
|
|
593
594
|
const baseType = col.udt_name.substring(1);
|
|
594
595
|
innerType = mapPgType(baseType);
|
|
596
|
+
if (innerType === "string") colType = "text[]";
|
|
597
|
+
else if (innerType === "number") colType = col.udt_name === "_numeric" ? "numeric[]" : "integer[]";
|
|
598
|
+
else if (innerType === "boolean") colType = "boolean[]";
|
|
599
|
+
}
|
|
600
|
+
if (colType) {
|
|
601
|
+
extra += `\n columnType: "${colType}",`;
|
|
595
602
|
}
|
|
596
603
|
extra += `\n of: { name: "${humanize(col.column_name)} Item", type: "${innerType}" },`;
|
|
597
604
|
} else if (finalPropType === "map" && !inferenceExtra.includes("keyValue: true") && !inferenceExtra.includes("properties: {")) {
|
|
@@ -1,6 +1,6 @@
|
|
|
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
|
-
import { Entity, EntityCollection, FilterValues, Relation } from "@rebasepro/types";
|
|
3
|
+
import { Entity, EntityCollection, FilterValues, Relation, LogicalCondition } from "@rebasepro/types";
|
|
4
4
|
import type { VectorSearchParams } from "@rebasepro/types";
|
|
5
5
|
import { resolveCollectionRelations, findRelation, createRelationRef, createRelationRefWithData } from "@rebasepro/common";
|
|
6
6
|
import { DrizzleConditionBuilder } from "../utils/drizzle-conditions";
|
|
@@ -465,6 +465,7 @@ export class EntityFetchService {
|
|
|
465
465
|
offset?: number;
|
|
466
466
|
startAfter?: Record<string, unknown>;
|
|
467
467
|
searchString?: string;
|
|
468
|
+
logical?: LogicalCondition;
|
|
468
469
|
},
|
|
469
470
|
collectionPath: string,
|
|
470
471
|
withConfig?: Record<string, unknown>
|
|
@@ -494,6 +495,11 @@ export class EntityFetchService {
|
|
|
494
495
|
if (filterConditions.length > 0) allConditions.push(...filterConditions);
|
|
495
496
|
}
|
|
496
497
|
|
|
498
|
+
if (options.logical) {
|
|
499
|
+
const logicalCondition = DrizzleConditionBuilder.buildLogicalConditions(options.logical, table, collectionPath);
|
|
500
|
+
if (logicalCondition) allConditions.push(logicalCondition);
|
|
501
|
+
}
|
|
502
|
+
|
|
497
503
|
// Cursor-based pagination (startAfter)
|
|
498
504
|
if (options.startAfter) {
|
|
499
505
|
const cursorConditions = this.buildCursorConditions(table, idField, idInfo, options, collectionPath);
|
|
@@ -700,6 +706,7 @@ export class EntityFetchService {
|
|
|
700
706
|
searchString?: string;
|
|
701
707
|
databaseId?: string;
|
|
702
708
|
vectorSearch?: VectorSearchParams;
|
|
709
|
+
logical?: LogicalCondition;
|
|
703
710
|
} = {}
|
|
704
711
|
): Promise<Entity<M>[]> {
|
|
705
712
|
const collection = getCollectionByPath(collectionPath, this.registry);
|
|
@@ -774,6 +781,11 @@ export class EntityFetchService {
|
|
|
774
781
|
if (filterConditions.length > 0) allConditions.push(...filterConditions);
|
|
775
782
|
}
|
|
776
783
|
|
|
784
|
+
if (options.logical) {
|
|
785
|
+
const logicalCondition = DrizzleConditionBuilder.buildLogicalConditions(options.logical, table, collectionPath);
|
|
786
|
+
if (logicalCondition) allConditions.push(logicalCondition);
|
|
787
|
+
}
|
|
788
|
+
|
|
777
789
|
// Vector distance threshold filter
|
|
778
790
|
if (vectorMeta?.filter) {
|
|
779
791
|
allConditions.push(vectorMeta.filter);
|