@rebasepro/server-postgresql 0.2.3 → 0.2.5
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 +9 -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 +1075 -470
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +1071 -466
- package/dist/index.umd.js.map +1 -1
- package/dist/server-postgresql/src/PostgresBackendDriver.d.ts +3 -1
- package/dist/server-postgresql/src/PostgresBootstrapper.d.ts +1 -0
- package/dist/server-postgresql/src/auth/services.d.ts +48 -31
- package/dist/server-postgresql/src/connection.d.ts +25 -0
- package/dist/server-postgresql/src/schema/auth-schema.d.ts +2135 -41
- package/dist/server-postgresql/src/services/EntityFetchService.d.ts +4 -0
- package/dist/server-postgresql/src/services/EntityPersistService.d.ts +4 -0
- package/dist/server-postgresql/src/services/entityService.d.ts +6 -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 +4 -26
- package/dist/types/src/controllers/client.d.ts +25 -43
- package/dist/types/src/controllers/collection_registry.d.ts +1 -1
- package/dist/types/src/controllers/data.d.ts +4 -0
- package/dist/types/src/controllers/data_driver.d.ts +23 -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 +5 -60
- package/dist/types/src/types/backend.d.ts +2 -2
- package/dist/types/src/types/backend_hooks.d.ts +2 -17
- 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 +9 -7
- package/dist/types/src/types/translations.d.ts +28 -12
- package/dist/types/src/types/user_management_delegate.d.ts +22 -57
- 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 +14 -2
- package/src/PostgresBootstrapper.ts +30 -20
- package/src/auth/ensure-tables.ts +116 -103
- package/src/auth/services.ts +347 -177
- package/src/connection.ts +77 -0
- package/src/data-transformer.ts +2 -2
- package/src/schema/auth-schema.ts +85 -75
- package/src/schema/doctor.ts +44 -3
- package/src/schema/generate-drizzle-schema-logic.ts +33 -3
- package/src/schema/generate-drizzle-schema.ts +6 -6
- package/src/schema/introspect-db-logic.ts +7 -0
- package/src/services/EntityFetchService.ts +69 -10
- package/src/services/EntityPersistService.ts +9 -0
- package/src/services/entityService.ts +9 -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 +10 -166
- package/test/doctor.test.ts +6 -2
- package/test/drizzle-conditions.test.ts +168 -0
- package/vite.config.ts +1 -1
- package/dist/server-postgresql/src/schema/default-collections.d.ts +0 -2
- package/dist/types/src/users/roles.d.ts +0 -22
- package/src/schema/default-collections.ts +0 -69
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);
|
|
@@ -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
|
|
@@ -23,53 +22,19 @@ export function createAuthSchema(rolesSchemaName: string = "rebase", usersSchema
|
|
|
23
22
|
emailVerified: boolean("email_verified").default(false).notNull(),
|
|
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
|
+
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
|
});
|
|
30
31
|
|
|
31
|
-
/**
|
|
32
|
-
* Roles table - defines permission sets
|
|
33
|
-
*/
|
|
34
|
-
const roles = rolesTableCreator("roles", {
|
|
35
|
-
id: varchar("id", { length: 50 }).primaryKey(), // 'admin', 'editor', 'viewer'
|
|
36
|
-
name: varchar("name", { length: 100 }).notNull(),
|
|
37
|
-
isAdmin: boolean("is_admin").default(false).notNull(),
|
|
38
|
-
defaultPermissions: jsonb("default_permissions").$type<{
|
|
39
|
-
read?: boolean;
|
|
40
|
-
create?: boolean;
|
|
41
|
-
edit?: boolean;
|
|
42
|
-
delete?: boolean;
|
|
43
|
-
}>(),
|
|
44
|
-
collectionPermissions: jsonb("collection_permissions").$type<
|
|
45
|
-
Record<string, {
|
|
46
|
-
read?: boolean;
|
|
47
|
-
create?: boolean;
|
|
48
|
-
edit?: boolean;
|
|
49
|
-
delete?: boolean;
|
|
50
|
-
}>
|
|
51
|
-
>(),
|
|
52
|
-
config: jsonb("config").$type<{
|
|
53
|
-
createCollections?: boolean;
|
|
54
|
-
editCollections?: "own" | "all" | boolean;
|
|
55
|
-
deleteCollections?: "own" | "all" | boolean;
|
|
56
|
-
}>()
|
|
57
|
-
});
|
|
58
32
|
|
|
59
|
-
/**
|
|
60
|
-
* User-Role junction table
|
|
61
|
-
*/
|
|
62
|
-
const userRoles = rolesTableCreator("user_roles", {
|
|
63
|
-
userId: uuid("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
|
|
64
|
-
roleId: varchar("role_id", { length: 50 }).notNull().references(() => roles.id, { onDelete: "cascade" })
|
|
65
|
-
}, (table: any) => ({
|
|
66
|
-
pk: primaryKey({ columns: [table.userId, table.roleId] })
|
|
67
|
-
}));
|
|
68
33
|
|
|
69
34
|
/**
|
|
70
35
|
* Refresh tokens for long-lived sessions
|
|
71
36
|
*/
|
|
72
|
-
const refreshTokens =
|
|
37
|
+
const refreshTokens = tableCreator("refresh_tokens", {
|
|
73
38
|
id: uuid("id").defaultRandom().primaryKey(),
|
|
74
39
|
userId: uuid("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
|
|
75
40
|
tokenHash: varchar("token_hash", { length: 255 }).notNull().unique(),
|
|
@@ -77,14 +42,14 @@ export function createAuthSchema(rolesSchemaName: string = "rebase", usersSchema
|
|
|
77
42
|
userAgent: varchar("user_agent", { length: 500 }),
|
|
78
43
|
ipAddress: varchar("ip_address", { length: 45 }),
|
|
79
44
|
createdAt: timestamp("created_at").defaultNow().notNull()
|
|
80
|
-
}, (table
|
|
45
|
+
}, (table) => ({
|
|
81
46
|
uniqueDeviceSession: unique("unique_device_session").on(table.userId, table.userAgent, table.ipAddress)
|
|
82
47
|
}));
|
|
83
48
|
|
|
84
49
|
/**
|
|
85
50
|
* Password reset tokens for forgot password flow
|
|
86
51
|
*/
|
|
87
|
-
const passwordResetTokens =
|
|
52
|
+
const passwordResetTokens = tableCreator("password_reset_tokens", {
|
|
88
53
|
id: uuid("id").defaultRandom().primaryKey(),
|
|
89
54
|
userId: uuid("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
|
|
90
55
|
tokenHash: varchar("token_hash", { length: 255 }).notNull().unique(),
|
|
@@ -96,7 +61,7 @@ export function createAuthSchema(rolesSchemaName: string = "rebase", usersSchema
|
|
|
96
61
|
/**
|
|
97
62
|
* App config - key/value store for custom settings
|
|
98
63
|
*/
|
|
99
|
-
const appConfig =
|
|
64
|
+
const appConfig = tableCreator("app_config", {
|
|
100
65
|
key: varchar("key", { length: 100 }).primaryKey(),
|
|
101
66
|
value: jsonb("value").notNull(),
|
|
102
67
|
updatedAt: timestamp("updated_at").defaultNow().notNull()
|
|
@@ -105,7 +70,7 @@ export function createAuthSchema(rolesSchemaName: string = "rebase", usersSchema
|
|
|
105
70
|
/**
|
|
106
71
|
* User identities - maps external OAuth profiles back to local users
|
|
107
72
|
*/
|
|
108
|
-
const userIdentities =
|
|
73
|
+
const userIdentities = tableCreator("user_identities", {
|
|
109
74
|
id: uuid("id").defaultRandom().primaryKey(),
|
|
110
75
|
userId: uuid("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
|
|
111
76
|
provider: varchar("provider", { length: 50 }).notNull(), // e.g. 'google', 'linkedin'
|
|
@@ -113,77 +78,122 @@ export function createAuthSchema(rolesSchemaName: string = "rebase", usersSchema
|
|
|
113
78
|
profileData: jsonb("profile_data"),
|
|
114
79
|
createdAt: timestamp("created_at").defaultNow().notNull(),
|
|
115
80
|
updatedAt: timestamp("updated_at").defaultNow().notNull()
|
|
116
|
-
}, (table
|
|
81
|
+
}, (table) => ({
|
|
117
82
|
uniqueProviderId: unique("unique_provider_id").on(table.provider, table.providerId)
|
|
118
83
|
}));
|
|
119
84
|
|
|
85
|
+
/**
|
|
86
|
+
* MFA factors table - stores enrolled MFA methods
|
|
87
|
+
*/
|
|
88
|
+
const mfaFactors = tableCreator("mfa_factors", {
|
|
89
|
+
id: uuid("id").defaultRandom().primaryKey(),
|
|
90
|
+
userId: uuid("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
|
|
91
|
+
factorType: varchar("factor_type", { length: 20 }).notNull(), // 'totp'
|
|
92
|
+
secretEncrypted: varchar("secret_encrypted", { length: 500 }).notNull(),
|
|
93
|
+
friendlyName: varchar("friendly_name", { length: 255 }),
|
|
94
|
+
verified: boolean("verified").default(false).notNull(),
|
|
95
|
+
createdAt: timestamp("created_at").defaultNow().notNull(),
|
|
96
|
+
updatedAt: timestamp("updated_at").defaultNow().notNull()
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* MFA challenges table - tracks active MFA verification attempts
|
|
101
|
+
*/
|
|
102
|
+
const mfaChallenges = tableCreator("mfa_challenges", {
|
|
103
|
+
id: uuid("id").defaultRandom().primaryKey(),
|
|
104
|
+
factorId: uuid("factor_id").notNull().references(() => mfaFactors.id, { onDelete: "cascade" }),
|
|
105
|
+
createdAt: timestamp("created_at").defaultNow().notNull(),
|
|
106
|
+
verifiedAt: timestamp("verified_at"),
|
|
107
|
+
ipAddress: varchar("ip_address", { length: 45 }),
|
|
108
|
+
expiresAt: timestamp("expires_at").notNull()
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Recovery codes table - backup codes for MFA
|
|
113
|
+
*/
|
|
114
|
+
const recoveryCodes = tableCreator("recovery_codes", {
|
|
115
|
+
id: uuid("id").defaultRandom().primaryKey(),
|
|
116
|
+
userId: uuid("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
|
|
117
|
+
codeHash: varchar("code_hash", { length: 255 }).notNull(),
|
|
118
|
+
usedAt: timestamp("used_at"),
|
|
119
|
+
createdAt: timestamp("created_at").defaultNow().notNull()
|
|
120
|
+
});
|
|
121
|
+
|
|
120
122
|
return {
|
|
121
|
-
rolesSchema,
|
|
122
123
|
usersSchema,
|
|
123
124
|
users,
|
|
124
|
-
roles,
|
|
125
|
-
userRoles,
|
|
126
125
|
refreshTokens,
|
|
127
126
|
passwordResetTokens,
|
|
128
127
|
appConfig,
|
|
129
|
-
userIdentities
|
|
128
|
+
userIdentities,
|
|
129
|
+
mfaFactors,
|
|
130
|
+
mfaChallenges,
|
|
131
|
+
recoveryCodes
|
|
130
132
|
};
|
|
131
133
|
}
|
|
132
134
|
|
|
133
135
|
// Instantiate default schema and tables using the default "rebase" schema
|
|
134
|
-
const defaultAuthSchema = createAuthSchema("rebase"
|
|
136
|
+
const defaultAuthSchema = createAuthSchema("rebase");
|
|
135
137
|
|
|
136
|
-
export const rebaseSchema = defaultAuthSchema.rolesSchema;
|
|
137
138
|
export const usersSchema = defaultAuthSchema.usersSchema;
|
|
138
139
|
|
|
139
140
|
export const users = defaultAuthSchema.users;
|
|
140
|
-
export const roles = defaultAuthSchema.roles;
|
|
141
|
-
export const userRoles = defaultAuthSchema.userRoles;
|
|
142
141
|
export const refreshTokens = defaultAuthSchema.refreshTokens;
|
|
143
142
|
export const passwordResetTokens = defaultAuthSchema.passwordResetTokens;
|
|
144
143
|
export const appConfig = defaultAuthSchema.appConfig;
|
|
145
144
|
export const userIdentities = defaultAuthSchema.userIdentities;
|
|
145
|
+
export const mfaFactors = defaultAuthSchema.mfaFactors;
|
|
146
|
+
export const mfaChallenges = defaultAuthSchema.mfaChallenges;
|
|
147
|
+
export const recoveryCodes = defaultAuthSchema.recoveryCodes;
|
|
146
148
|
|
|
147
149
|
// Relations
|
|
148
150
|
export const usersRelations = relations(users, ({ many }) => ({
|
|
149
|
-
userRoles: many(userRoles),
|
|
150
151
|
refreshTokens: many(refreshTokens),
|
|
151
152
|
passwordResetTokens: many(passwordResetTokens),
|
|
152
|
-
userIdentities: many(userIdentities)
|
|
153
|
+
userIdentities: many(userIdentities),
|
|
154
|
+
mfaFactors: many(mfaFactors),
|
|
155
|
+
recoveryCodes: many(recoveryCodes)
|
|
153
156
|
}));
|
|
154
157
|
|
|
155
|
-
export const
|
|
156
|
-
|
|
158
|
+
export const refreshTokensRelations = relations(refreshTokens, ({ one }) => ({
|
|
159
|
+
user: one(users, {
|
|
160
|
+
fields: [refreshTokens.userId],
|
|
161
|
+
references: [users.id]
|
|
162
|
+
})
|
|
157
163
|
}));
|
|
158
164
|
|
|
159
|
-
export const
|
|
165
|
+
export const passwordResetTokensRelations = relations(passwordResetTokens, ({ one }) => ({
|
|
160
166
|
user: one(users, {
|
|
161
|
-
fields: [
|
|
167
|
+
fields: [passwordResetTokens.userId],
|
|
162
168
|
references: [users.id]
|
|
163
|
-
}),
|
|
164
|
-
role: one(roles, {
|
|
165
|
-
fields: [userRoles.roleId],
|
|
166
|
-
references: [roles.id]
|
|
167
169
|
})
|
|
168
170
|
}));
|
|
169
171
|
|
|
170
|
-
export const
|
|
172
|
+
export const userIdentitiesRelations = relations(userIdentities, ({ one }) => ({
|
|
171
173
|
user: one(users, {
|
|
172
|
-
fields: [
|
|
174
|
+
fields: [userIdentities.userId],
|
|
173
175
|
references: [users.id]
|
|
174
176
|
})
|
|
175
177
|
}));
|
|
176
178
|
|
|
177
|
-
export const
|
|
179
|
+
export const mfaFactorsRelations = relations(mfaFactors, ({ one, many }) => ({
|
|
178
180
|
user: one(users, {
|
|
179
|
-
fields: [
|
|
181
|
+
fields: [mfaFactors.userId],
|
|
180
182
|
references: [users.id]
|
|
183
|
+
}),
|
|
184
|
+
challenges: many(mfaChallenges)
|
|
185
|
+
}));
|
|
186
|
+
|
|
187
|
+
export const mfaChallengesRelations = relations(mfaChallenges, ({ one }) => ({
|
|
188
|
+
factor: one(mfaFactors, {
|
|
189
|
+
fields: [mfaChallenges.factorId],
|
|
190
|
+
references: [mfaFactors.id]
|
|
181
191
|
})
|
|
182
192
|
}));
|
|
183
193
|
|
|
184
|
-
export const
|
|
194
|
+
export const recoveryCodesRelations = relations(recoveryCodes, ({ one }) => ({
|
|
185
195
|
user: one(users, {
|
|
186
|
-
fields: [
|
|
196
|
+
fields: [recoveryCodes.userId],
|
|
187
197
|
references: [users.id]
|
|
188
198
|
})
|
|
189
199
|
}));
|
|
@@ -191,11 +201,11 @@ export const userIdentitiesRelations = relations(userIdentities, ({ one }) => ({
|
|
|
191
201
|
// Type exports
|
|
192
202
|
export type User = typeof users.$inferSelect;
|
|
193
203
|
export type NewUser = typeof users.$inferInsert;
|
|
194
|
-
export type Role = typeof roles.$inferSelect;
|
|
195
|
-
export type NewRole = typeof roles.$inferInsert;
|
|
196
|
-
export type UserRole = typeof userRoles.$inferSelect;
|
|
197
204
|
export type RefreshToken = typeof refreshTokens.$inferSelect;
|
|
198
205
|
export type PasswordResetToken = typeof passwordResetTokens.$inferSelect;
|
|
199
206
|
export type AppConfig = typeof appConfig.$inferSelect;
|
|
200
207
|
export type UserIdentity = typeof userIdentities.$inferSelect;
|
|
201
208
|
export type NewUserIdentity = typeof userIdentities.$inferInsert;
|
|
209
|
+
export type MfaFactorRow = typeof mfaFactors.$inferSelect;
|
|
210
|
+
export type MfaChallengeRow = typeof mfaChallenges.$inferSelect;
|
|
211
|
+
export type RecoveryCodeRow = typeof recoveryCodes.$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
|
-
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));
|
|
@@ -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: {")) {
|