@rebasepro/server-postgresql 0.2.4 → 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 +5 -8
- package/dist/index.es.js +286 -365
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +283 -362
- package/dist/index.umd.js.map +1 -1
- package/dist/server-postgresql/src/PostgresBackendDriver.d.ts +2 -0
- package/dist/server-postgresql/src/auth/services.d.ts +6 -31
- package/dist/server-postgresql/src/schema/auth-schema.d.ts +87 -340
- package/dist/server-postgresql/src/services/EntityPersistService.d.ts +4 -0
- package/dist/server-postgresql/src/services/entityService.d.ts +4 -0
- 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 +4 -0
- package/dist/types/src/controllers/data_driver.d.ts +5 -0
- package/dist/types/src/types/auth_adapter.d.ts +3 -56
- 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/properties.d.ts +7 -5
- 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 +3 -12
- package/src/auth/ensure-tables.ts +52 -101
- package/src/auth/services.ts +71 -170
- 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/introspect-db-logic.ts +7 -0
- package/src/services/EntityPersistService.ts +9 -0
- package/src/services/entityService.ts +7 -0
- package/test/auth-services.test.ts +7 -150
- package/test/doctor.test.ts +6 -2
- package/dist/server-postgresql/src/schema/default-collections.d.ts +0 -2
- package/dist/types/src/users/roles.d.ts +0 -14
- package/src/schema/default-collections.ts +0 -69
|
@@ -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
|
}
|
|
@@ -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: {")) {
|
|
@@ -53,6 +53,15 @@ export class EntityPersistService {
|
|
|
53
53
|
.where(eq(idField, parsedId));
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
/**
|
|
57
|
+
* Delete all entities from a collection
|
|
58
|
+
*/
|
|
59
|
+
async deleteAll(collectionPath: string, _databaseId?: string): Promise<void> {
|
|
60
|
+
const collection = getCollectionByPath(collectionPath, this.registry);
|
|
61
|
+
const table = getTableForCollection(collection, this.registry);
|
|
62
|
+
await this.db.delete(table);
|
|
63
|
+
}
|
|
64
|
+
|
|
56
65
|
/**
|
|
57
66
|
* Save an entity (create or update)
|
|
58
67
|
*/
|
|
@@ -169,6 +169,13 @@ export class EntityService implements EntityRepository {
|
|
|
169
169
|
return this.persistService.deleteEntity(collectionPath, entityId, databaseId);
|
|
170
170
|
}
|
|
171
171
|
|
|
172
|
+
/**
|
|
173
|
+
* Delete all entities from a collection
|
|
174
|
+
*/
|
|
175
|
+
async deleteAll(collectionPath: string, databaseId?: string): Promise<void> {
|
|
176
|
+
return this.persistService.deleteAll(collectionPath, databaseId);
|
|
177
|
+
}
|
|
178
|
+
|
|
172
179
|
|
|
173
180
|
/**
|
|
174
181
|
* Execute raw SQL
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { NodePgDatabase } from "drizzle-orm/node-postgres";
|
|
2
|
-
import { UserService,
|
|
2
|
+
import { UserService, RefreshTokenService, PasswordResetTokenService, Role } from "../src/auth/services";
|
|
3
3
|
import { users, refreshTokens, passwordResetTokens } from "../src/schema/auth-schema";
|
|
4
4
|
import { UserData } from "@rebasepro/server-core";
|
|
5
5
|
|
|
@@ -344,18 +344,7 @@ describe("Auth Services", () => {
|
|
|
344
344
|
describe("getUserRoles", () => {
|
|
345
345
|
it("should return roles for user", async () => {
|
|
346
346
|
mockExecute.mockResolvedValueOnce({
|
|
347
|
-
rows: [
|
|
348
|
-
{ id: "admin",
|
|
349
|
-
name: "Admin",
|
|
350
|
-
is_admin: true,
|
|
351
|
-
default_permissions: null,
|
|
352
|
-
collection_permissions: null },
|
|
353
|
-
{ id: "editor",
|
|
354
|
-
name: "Editor",
|
|
355
|
-
is_admin: false,
|
|
356
|
-
default_permissions: { edit: true },
|
|
357
|
-
collection_permissions: null }
|
|
358
|
-
]
|
|
347
|
+
rows: [{ roles: ["admin", "editor"] }]
|
|
359
348
|
});
|
|
360
349
|
|
|
361
350
|
const roles = await userService.getUserRoles("user-123");
|
|
@@ -363,7 +352,7 @@ describe("Auth Services", () => {
|
|
|
363
352
|
expect(roles).toHaveLength(2);
|
|
364
353
|
expect(roles[0]).toEqual({
|
|
365
354
|
id: "admin",
|
|
366
|
-
name: "
|
|
355
|
+
name: "admin",
|
|
367
356
|
isAdmin: true,
|
|
368
357
|
defaultPermissions: null,
|
|
369
358
|
collectionPermissions: null
|
|
@@ -374,13 +363,7 @@ describe("Auth Services", () => {
|
|
|
374
363
|
describe("getUserRoleIds", () => {
|
|
375
364
|
it("should return role IDs for user", async () => {
|
|
376
365
|
mockExecute.mockResolvedValueOnce({
|
|
377
|
-
rows: [
|
|
378
|
-
{ id: "admin",
|
|
379
|
-
name: "Admin",
|
|
380
|
-
is_admin: true,
|
|
381
|
-
default_permissions: null,
|
|
382
|
-
collection_permissions: null }
|
|
383
|
-
]
|
|
366
|
+
rows: [{ roles: ["admin"] }]
|
|
384
367
|
});
|
|
385
368
|
|
|
386
369
|
const roleIds = await userService.getUserRoleIds("user-123");
|
|
@@ -393,10 +376,7 @@ describe("Auth Services", () => {
|
|
|
393
376
|
it("should delete existing and insert new roles", async () => {
|
|
394
377
|
await userService.setUserRoles("user-123", ["admin", "editor"]);
|
|
395
378
|
|
|
396
|
-
// First call deletes existing roles
|
|
397
379
|
expect(mockExecute).toHaveBeenCalled();
|
|
398
|
-
// Subsequent calls insert new roles
|
|
399
|
-
expect(mockExecute.mock.calls.length).toBeGreaterThanOrEqual(1);
|
|
400
380
|
});
|
|
401
381
|
});
|
|
402
382
|
|
|
@@ -408,7 +388,7 @@ describe("Auth Services", () => {
|
|
|
408
388
|
});
|
|
409
389
|
|
|
410
390
|
it("should use editor as default role", async () => {
|
|
411
|
-
await userService.assignDefaultRole("user-123");
|
|
391
|
+
await userService.assignDefaultRole("user-123", "editor");
|
|
412
392
|
|
|
413
393
|
expect(mockExecute).toHaveBeenCalled();
|
|
414
394
|
});
|
|
@@ -419,11 +399,7 @@ describe("Auth Services", () => {
|
|
|
419
399
|
const mockUser = { id: "user-123", email: "test@example.com" };
|
|
420
400
|
mockSelectWhere.mockResolvedValueOnce([mockUser]);
|
|
421
401
|
mockExecute.mockResolvedValueOnce({
|
|
422
|
-
rows: [{
|
|
423
|
-
name: "Admin",
|
|
424
|
-
is_admin: true,
|
|
425
|
-
default_permissions: null,
|
|
426
|
-
collection_permissions: null }]
|
|
402
|
+
rows: [{ roles: ["admin"] }]
|
|
427
403
|
});
|
|
428
404
|
|
|
429
405
|
const result = await userService.getUserWithRoles("user-123");
|
|
@@ -431,7 +407,7 @@ describe("Auth Services", () => {
|
|
|
431
407
|
expect(result).toEqual({
|
|
432
408
|
user: mockUserData({}),
|
|
433
409
|
roles: [{ id: "admin",
|
|
434
|
-
name: "
|
|
410
|
+
name: "admin",
|
|
435
411
|
isAdmin: true,
|
|
436
412
|
defaultPermissions: null,
|
|
437
413
|
collectionPermissions: null }]
|
|
@@ -472,125 +448,6 @@ describe("Auth Services", () => {
|
|
|
472
448
|
});
|
|
473
449
|
});
|
|
474
450
|
|
|
475
|
-
describe("RoleService", () => {
|
|
476
|
-
let roleService: RoleService;
|
|
477
|
-
|
|
478
|
-
beforeEach(() => {
|
|
479
|
-
roleService = new RoleService(db);
|
|
480
|
-
});
|
|
481
|
-
|
|
482
|
-
describe("getRoleById", () => {
|
|
483
|
-
it("should return role when found", async () => {
|
|
484
|
-
mockExecute.mockResolvedValueOnce({
|
|
485
|
-
rows: [{ id: "admin",
|
|
486
|
-
name: "Admin",
|
|
487
|
-
is_admin: true,
|
|
488
|
-
default_permissions: null,
|
|
489
|
-
collection_permissions: null }]
|
|
490
|
-
});
|
|
491
|
-
|
|
492
|
-
const result = await roleService.getRoleById("admin");
|
|
493
|
-
|
|
494
|
-
expect(result).toEqual({
|
|
495
|
-
id: "admin",
|
|
496
|
-
name: "Admin",
|
|
497
|
-
isAdmin: true,
|
|
498
|
-
defaultPermissions: null,
|
|
499
|
-
collectionPermissions: null
|
|
500
|
-
});
|
|
501
|
-
});
|
|
502
|
-
|
|
503
|
-
it("should return null when role not found", async () => {
|
|
504
|
-
mockExecute.mockResolvedValueOnce({ rows: [] });
|
|
505
|
-
|
|
506
|
-
const result = await roleService.getRoleById("nonexistent");
|
|
507
|
-
|
|
508
|
-
expect(result).toBeNull();
|
|
509
|
-
});
|
|
510
|
-
});
|
|
511
|
-
|
|
512
|
-
describe("listRoles", () => {
|
|
513
|
-
it("should return all roles", async () => {
|
|
514
|
-
mockExecute.mockResolvedValueOnce({
|
|
515
|
-
rows: [
|
|
516
|
-
{ id: "admin",
|
|
517
|
-
name: "Admin",
|
|
518
|
-
is_admin: true,
|
|
519
|
-
default_permissions: null,
|
|
520
|
-
collection_permissions: null },
|
|
521
|
-
{ id: "editor",
|
|
522
|
-
name: "Editor",
|
|
523
|
-
is_admin: false,
|
|
524
|
-
default_permissions: null,
|
|
525
|
-
collection_permissions: null }
|
|
526
|
-
]
|
|
527
|
-
});
|
|
528
|
-
|
|
529
|
-
const roles = await roleService.listRoles();
|
|
530
|
-
|
|
531
|
-
expect(roles).toHaveLength(2);
|
|
532
|
-
});
|
|
533
|
-
});
|
|
534
|
-
|
|
535
|
-
describe("createRole", () => {
|
|
536
|
-
it("should create a role", async () => {
|
|
537
|
-
mockExecute.mockResolvedValueOnce({
|
|
538
|
-
rows: [{ id: "custom",
|
|
539
|
-
name: "Custom Role",
|
|
540
|
-
is_admin: false,
|
|
541
|
-
default_permissions: null,
|
|
542
|
-
collection_permissions: null }]
|
|
543
|
-
});
|
|
544
|
-
|
|
545
|
-
const role = await roleService.createRole({
|
|
546
|
-
id: "custom",
|
|
547
|
-
name: "Custom Role",
|
|
548
|
-
defaultPermissions: null
|
|
549
|
-
});
|
|
550
|
-
|
|
551
|
-
expect(role.id).toBe("custom");
|
|
552
|
-
expect(role.name).toBe("Custom Role");
|
|
553
|
-
});
|
|
554
|
-
});
|
|
555
|
-
|
|
556
|
-
describe("updateRole", () => {
|
|
557
|
-
it("should update a role", async () => {
|
|
558
|
-
mockExecute
|
|
559
|
-
.mockResolvedValueOnce({ rows: [{ id: "admin",
|
|
560
|
-
name: "Admin",
|
|
561
|
-
is_admin: true,
|
|
562
|
-
default_permissions: null,
|
|
563
|
-
collection_permissions: null }] })
|
|
564
|
-
.mockResolvedValueOnce({ rows: [] })
|
|
565
|
-
.mockResolvedValueOnce({ rows: [{ id: "admin",
|
|
566
|
-
name: "Super Admin",
|
|
567
|
-
is_admin: true,
|
|
568
|
-
default_permissions: null,
|
|
569
|
-
collection_permissions: null }] });
|
|
570
|
-
|
|
571
|
-
const result = await roleService.updateRole("admin", { name: "Super Admin" });
|
|
572
|
-
|
|
573
|
-
expect(result?.name).toBe("Super Admin");
|
|
574
|
-
});
|
|
575
|
-
|
|
576
|
-
it("should return null when role not found", async () => {
|
|
577
|
-
mockExecute.mockResolvedValueOnce({ rows: [] });
|
|
578
|
-
|
|
579
|
-
const result = await roleService.updateRole("nonexistent", { name: "Test" });
|
|
580
|
-
|
|
581
|
-
expect(result).toBeNull();
|
|
582
|
-
});
|
|
583
|
-
});
|
|
584
|
-
|
|
585
|
-
describe("deleteRole", () => {
|
|
586
|
-
it("should delete a role", async () => {
|
|
587
|
-
await roleService.deleteRole("custom");
|
|
588
|
-
|
|
589
|
-
expect(mockExecute).toHaveBeenCalled();
|
|
590
|
-
});
|
|
591
|
-
});
|
|
592
|
-
});
|
|
593
|
-
|
|
594
451
|
describe("RefreshTokenService", () => {
|
|
595
452
|
let refreshTokenService: RefreshTokenService;
|
|
596
453
|
|
package/test/doctor.test.ts
CHANGED
|
@@ -135,8 +135,12 @@ columnType: "time" } as DateProperty)).toBe("time without time zone");
|
|
|
135
135
|
it("should map json types correctly", () => {
|
|
136
136
|
expect(getExpectedColumnType({ type: "map" })).toBe("jsonb");
|
|
137
137
|
expect(getExpectedColumnType({ type: "array" })).toBe("jsonb");
|
|
138
|
-
expect(getExpectedColumnType({ type: "array",
|
|
139
|
-
|
|
138
|
+
expect(getExpectedColumnType({ type: "array", columnType: "json" } as ArrayProperty)).toBe("json");
|
|
139
|
+
|
|
140
|
+
// Native array element type mappings
|
|
141
|
+
expect(getExpectedColumnType({ type: "array", of: { type: "string" } } as ArrayProperty)).toBe("ARRAY");
|
|
142
|
+
expect(getExpectedColumnType({ type: "array", of: { type: "number", validation: { integer: true } } } as ArrayProperty)).toBe("ARRAY");
|
|
143
|
+
expect(getExpectedColumnType({ type: "array", of: { type: "boolean" } } as ArrayProperty)).toBe("ARRAY");
|
|
140
144
|
});
|
|
141
145
|
|
|
142
146
|
it("should map enum string to USER-DEFINED", () => {
|
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
import { PostgresCollection } from "@rebasepro/types";
|
|
2
|
-
|
|
3
|
-
export const defaultUsersCollection: PostgresCollection = {
|
|
4
|
-
name: "Users",
|
|
5
|
-
singularName: "User",
|
|
6
|
-
slug: "users",
|
|
7
|
-
table: "users",
|
|
8
|
-
schema: "rebase",
|
|
9
|
-
icon: "Users",
|
|
10
|
-
group: "Settings",
|
|
11
|
-
properties: {
|
|
12
|
-
id: {
|
|
13
|
-
name: "ID",
|
|
14
|
-
type: "string",
|
|
15
|
-
isId: "uuid"
|
|
16
|
-
},
|
|
17
|
-
email: {
|
|
18
|
-
name: "Email",
|
|
19
|
-
type: "string",
|
|
20
|
-
validation: { required: true, unique: true }
|
|
21
|
-
},
|
|
22
|
-
password_hash: {
|
|
23
|
-
name: "Password Hash",
|
|
24
|
-
type: "string",
|
|
25
|
-
ui: { hideFromCollection: true }
|
|
26
|
-
},
|
|
27
|
-
display_name: {
|
|
28
|
-
name: "Display Name",
|
|
29
|
-
type: "string"
|
|
30
|
-
},
|
|
31
|
-
photo_url: {
|
|
32
|
-
name: "Photo URL",
|
|
33
|
-
type: "string"
|
|
34
|
-
},
|
|
35
|
-
email_verified: {
|
|
36
|
-
name: "Email Verified",
|
|
37
|
-
type: "boolean",
|
|
38
|
-
defaultValue: false
|
|
39
|
-
},
|
|
40
|
-
email_verification_token: {
|
|
41
|
-
name: "Email Verification Token",
|
|
42
|
-
type: "string",
|
|
43
|
-
ui: { hideFromCollection: true }
|
|
44
|
-
},
|
|
45
|
-
email_verification_sent_at: {
|
|
46
|
-
name: "Email Verification Sent At",
|
|
47
|
-
type: "date",
|
|
48
|
-
ui: { hideFromCollection: true }
|
|
49
|
-
},
|
|
50
|
-
metadata: {
|
|
51
|
-
name: "Metadata",
|
|
52
|
-
type: "map",
|
|
53
|
-
defaultValue: {},
|
|
54
|
-
ui: { hideFromCollection: true }
|
|
55
|
-
},
|
|
56
|
-
created_at: {
|
|
57
|
-
name: "Created At",
|
|
58
|
-
type: "date",
|
|
59
|
-
autoValue: "on_create",
|
|
60
|
-
ui: { readOnly: true, hideFromCollection: true }
|
|
61
|
-
},
|
|
62
|
-
updated_at: {
|
|
63
|
-
name: "Updated At",
|
|
64
|
-
type: "date",
|
|
65
|
-
autoValue: "on_update",
|
|
66
|
-
ui: { readOnly: true, hideFromCollection: true }
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
};
|