@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
|
@@ -17,6 +17,18 @@ import { EntityFetchService } from "./EntityFetchService";
|
|
|
17
17
|
import { DrizzleClient } from "../interfaces";
|
|
18
18
|
import { PostgresCollectionRegistry } from "../collections/PostgresCollectionRegistry";
|
|
19
19
|
|
|
20
|
+
/** Shape of PostgreSQL errors with diagnostic metadata. */
|
|
21
|
+
interface PostgresError extends Error {
|
|
22
|
+
code?: string;
|
|
23
|
+
detail?: string;
|
|
24
|
+
hint?: string;
|
|
25
|
+
constraint?: string;
|
|
26
|
+
column?: string;
|
|
27
|
+
table?: string;
|
|
28
|
+
dataType?: string;
|
|
29
|
+
cause?: unknown;
|
|
30
|
+
}
|
|
31
|
+
|
|
20
32
|
/**
|
|
21
33
|
* Service for handling all entity write operations.
|
|
22
34
|
* Handles saving, deleting, and updating entities.
|
|
@@ -53,6 +65,15 @@ export class EntityPersistService {
|
|
|
53
65
|
.where(eq(idField, parsedId));
|
|
54
66
|
}
|
|
55
67
|
|
|
68
|
+
/**
|
|
69
|
+
* Delete all entities from a collection
|
|
70
|
+
*/
|
|
71
|
+
async deleteAll(collectionPath: string, _databaseId?: string): Promise<void> {
|
|
72
|
+
const collection = getCollectionByPath(collectionPath, this.registry);
|
|
73
|
+
const table = getTableForCollection(collection, this.registry);
|
|
74
|
+
await this.db.delete(table);
|
|
75
|
+
}
|
|
76
|
+
|
|
56
77
|
/**
|
|
57
78
|
* Save an entity (create or update)
|
|
58
79
|
*/
|
|
@@ -382,14 +403,14 @@ export class EntityPersistService {
|
|
|
382
403
|
*/
|
|
383
404
|
private extractCauseMessage(error: unknown): string | null {
|
|
384
405
|
if (!error || typeof error !== "object") return null;
|
|
385
|
-
|
|
406
|
+
if (!(error instanceof Error)) return null;
|
|
386
407
|
|
|
387
|
-
if (
|
|
388
|
-
const deeper = this.extractCauseMessage(
|
|
408
|
+
if (error.cause && typeof error.cause === "object") {
|
|
409
|
+
const deeper = this.extractCauseMessage(error.cause);
|
|
389
410
|
if (deeper) return deeper;
|
|
390
411
|
// The cause itself has a message
|
|
391
|
-
if (
|
|
392
|
-
return
|
|
412
|
+
if (error.cause instanceof Error && error.cause.message) {
|
|
413
|
+
return error.cause.message;
|
|
393
414
|
}
|
|
394
415
|
}
|
|
395
416
|
return null;
|
|
@@ -411,19 +432,24 @@ export class EntityPersistService {
|
|
|
411
432
|
* Extract the underlying PostgreSQL error from a Drizzle wrapper.
|
|
412
433
|
* Drizzle wraps PG errors in a `cause` property.
|
|
413
434
|
*/
|
|
414
|
-
private extractPgError(error: unknown):
|
|
435
|
+
private extractPgError(error: unknown): PostgresError | null {
|
|
415
436
|
if (!error || typeof error !== "object") return null;
|
|
416
|
-
|
|
417
|
-
|
|
437
|
+
if (!(error instanceof Error)) {
|
|
438
|
+
// Check non-Error objects for a cause chain (Drizzle sometimes wraps oddly)
|
|
439
|
+
if ("cause" in error && (error as Record<string, unknown>).cause && typeof (error as Record<string, unknown>).cause === "object") {
|
|
440
|
+
return this.extractPgError((error as Record<string, unknown>).cause);
|
|
441
|
+
}
|
|
442
|
+
return null;
|
|
443
|
+
}
|
|
418
444
|
|
|
419
445
|
// Check if the error itself has a PG error code
|
|
420
|
-
if (
|
|
421
|
-
return
|
|
446
|
+
if ("code" in error && typeof (error as PostgresError).code === "string" && /^[0-9A-Z]{5}$/.test((error as PostgresError).code!)) {
|
|
447
|
+
return error as PostgresError;
|
|
422
448
|
}
|
|
423
449
|
|
|
424
450
|
// Check the cause chain (Drizzle wraps PG errors)
|
|
425
|
-
if (
|
|
426
|
-
return this.extractPgError(
|
|
451
|
+
if (error.cause && typeof error.cause === "object") {
|
|
452
|
+
return this.extractPgError(error.cause);
|
|
427
453
|
}
|
|
428
454
|
|
|
429
455
|
return null;
|
|
@@ -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
|
package/src/types.ts
ADDED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { and, eq, or, sql, SQL, ilike, inArray } from "drizzle-orm";
|
|
2
2
|
import { AnyPgColumn, PgTable, PgVarchar, PgText, PgChar } from "drizzle-orm/pg-core";
|
|
3
|
-
import { FilterValues, WhereFilterOp, Relation, JoinStep } from "@rebasepro/types";
|
|
3
|
+
import { FilterValues, WhereFilterOp, Relation, JoinStep, LogicalCondition, FilterCondition } from "@rebasepro/types";
|
|
4
4
|
import { getColumnName, resolveCollectionRelations } from "@rebasepro/common";
|
|
5
5
|
import { PostgresCollectionRegistry } from "../collections/PostgresCollectionRegistry";
|
|
6
6
|
import { ConditionBuilderStatic } from "../interfaces";
|
|
@@ -37,7 +37,6 @@ export class DrizzleConditionBuilder {
|
|
|
37
37
|
for (const [field, filterParam] of Object.entries(filter)) {
|
|
38
38
|
if (!filterParam) continue;
|
|
39
39
|
|
|
40
|
-
const [op, value] = filterParam as [WhereFilterOp, any];
|
|
41
40
|
let fieldColumn = table[field as keyof typeof table] as AnyPgColumn;
|
|
42
41
|
|
|
43
42
|
if (!fieldColumn) {
|
|
@@ -53,15 +52,51 @@ export class DrizzleConditionBuilder {
|
|
|
53
52
|
continue;
|
|
54
53
|
}
|
|
55
54
|
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
55
|
+
const paramsList = Array.isArray(filterParam) && filterParam.length > 0 && Array.isArray(filterParam[0])
|
|
56
|
+
? (filterParam as [WhereFilterOp, any][])
|
|
57
|
+
: [filterParam as [WhereFilterOp, any]];
|
|
58
|
+
|
|
59
|
+
for (const [op, value] of paramsList) {
|
|
60
|
+
const condition = this.buildSingleFilterCondition(fieldColumn, op, value);
|
|
61
|
+
if (condition) {
|
|
62
|
+
conditions.push(condition);
|
|
63
|
+
}
|
|
59
64
|
}
|
|
60
65
|
}
|
|
61
66
|
|
|
62
67
|
return conditions;
|
|
63
68
|
}
|
|
64
69
|
|
|
70
|
+
/**
|
|
71
|
+
* Build logical conditions recursively from LogicalCondition or FilterCondition
|
|
72
|
+
*/
|
|
73
|
+
static buildLogicalConditions(
|
|
74
|
+
cond: LogicalCondition | FilterCondition,
|
|
75
|
+
table: PgTable<any>,
|
|
76
|
+
collectionPath: string
|
|
77
|
+
): SQL | null {
|
|
78
|
+
if ("type" in cond) {
|
|
79
|
+
const subSQLs = cond.conditions
|
|
80
|
+
.map(c => this.buildLogicalConditions(c, table, collectionPath))
|
|
81
|
+
.filter((sql): sql is SQL => sql !== null);
|
|
82
|
+
if (subSQLs.length === 0) return null;
|
|
83
|
+
return (cond.type === "or" ? or(...subSQLs) : and(...subSQLs)) ?? null;
|
|
84
|
+
} else {
|
|
85
|
+
let fieldColumn = table[cond.column as keyof typeof table] as AnyPgColumn;
|
|
86
|
+
if (!fieldColumn) {
|
|
87
|
+
const relationKey = `${cond.column}_id`;
|
|
88
|
+
if (relationKey in table) {
|
|
89
|
+
fieldColumn = table[relationKey as keyof typeof table] as AnyPgColumn;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (!fieldColumn) {
|
|
93
|
+
console.warn(`Filtering by field '${cond.column}', but it does not exist in table for collection '${collectionPath}'`);
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
return this.buildSingleFilterCondition(fieldColumn, cond.operator as WhereFilterOp, cond.value);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
65
100
|
/**
|
|
66
101
|
* Build a single filter condition for a specific operator and value
|
|
67
102
|
*/
|
package/src/websocket.ts
CHANGED
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
import { RealtimeService } from "./services/realtimeService";
|
|
2
2
|
import { PostgresBackendDriver } from "./PostgresBackendDriver";
|
|
3
|
-
import { DataDriver, DeleteEntityProps, FetchCollectionProps, FetchEntityProps, SaveEntityProps, TableMetadata, BranchInfo,
|
|
3
|
+
import type { DataDriver, DeleteEntityProps, FetchCollectionProps, FetchEntityProps, SaveEntityProps, TableMetadata, BranchInfo, AuthAdapter } from "@rebasepro/types";
|
|
4
|
+
import { isSQLAdmin, isSchemaAdmin } from "@rebasepro/types";
|
|
5
|
+
import type { User } from "@rebasepro/types";
|
|
4
6
|
import { WebSocketServer, WebSocket } from "ws";
|
|
5
7
|
import { Server } from "http";
|
|
6
8
|
import { inspect } from "util";
|
|
7
|
-
// @ts-ignore
|
|
8
9
|
import { extractUserFromToken, AccessTokenPayload } from "@rebasepro/server-core";
|
|
9
|
-
|
|
10
|
-
|
|
10
|
+
|
|
11
|
+
/** Minimal subset of RebaseAuthConfig used by the WebSocket layer. */
|
|
12
|
+
interface WsAuthConfig {
|
|
13
|
+
requireAuth?: boolean;
|
|
14
|
+
jwtSecret?: string;
|
|
15
|
+
}
|
|
11
16
|
|
|
12
17
|
/**
|
|
13
18
|
* Normalized user identity for WebSocket sessions.
|
|
@@ -27,7 +32,7 @@ interface ClientSession {
|
|
|
27
32
|
messageWindowStart: number;
|
|
28
33
|
}
|
|
29
34
|
|
|
30
|
-
|
|
35
|
+
|
|
31
36
|
|
|
32
37
|
/** Maximum messages per client per window */
|
|
33
38
|
const WS_RATE_LIMIT = 2000;
|
|
@@ -52,14 +57,14 @@ const ADMIN_ONLY_TYPES = new Set([
|
|
|
52
57
|
*/
|
|
53
58
|
function extractErrorMessage(error: unknown): string {
|
|
54
59
|
if (!error) return "Unknown error";
|
|
55
|
-
if (
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
return extractErrorMessage(err.cause);
|
|
59
|
-
}
|
|
60
|
-
if (typeof err.message === "string") {
|
|
61
|
-
return err.message;
|
|
60
|
+
if (error instanceof Error) {
|
|
61
|
+
if ("cause" in error && error.cause) {
|
|
62
|
+
return extractErrorMessage(error.cause);
|
|
62
63
|
}
|
|
64
|
+
return error.message;
|
|
65
|
+
}
|
|
66
|
+
if (typeof error === "object" && "message" in error && typeof (error as { message: unknown }).message === "string") {
|
|
67
|
+
return (error as { message: string }).message;
|
|
63
68
|
}
|
|
64
69
|
return String(error);
|
|
65
70
|
}
|
|
@@ -72,21 +77,20 @@ function isAdminSession(session: ClientSession | undefined): boolean {
|
|
|
72
77
|
// Fast path: new adapter-aware sessions set isAdmin directly
|
|
73
78
|
if (session.user.isAdmin) return true;
|
|
74
79
|
if (!session.user.roles) return false;
|
|
75
|
-
return session.user.roles.some((r
|
|
76
|
-
if (typeof r === "string") return r === "admin";
|
|
77
|
-
if (r && typeof r === "object" && "isAdmin" in r) return (r as { isAdmin: boolean }).isAdmin;
|
|
78
|
-
if (r && typeof r === "object" && "id" in r) return (r as { id: string }).id === "admin";
|
|
79
|
-
return false;
|
|
80
|
-
});
|
|
80
|
+
return session.user.roles.some((r) => r === "admin");
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
export function createPostgresWebSocket(
|
|
84
84
|
server: Server,
|
|
85
85
|
realtimeService: RealtimeService,
|
|
86
86
|
driver: PostgresBackendDriver,
|
|
87
|
-
authConfig?:
|
|
87
|
+
authConfig?: WsAuthConfig,
|
|
88
88
|
authAdapter?: AuthAdapter
|
|
89
89
|
) {
|
|
90
|
+
// Session map scoped to this factory invocation — prevents stale sessions
|
|
91
|
+
// leaking across hot reloads or multiple factory calls.
|
|
92
|
+
const clientSessions = new Map<string, ClientSession>();
|
|
93
|
+
|
|
90
94
|
const isProduction = process.env.NODE_ENV === "production";
|
|
91
95
|
/** Debug logger that is suppressed in production to prevent PII/data leaks */
|
|
92
96
|
const wsDebug = (...args: unknown[]) => { if (!isProduction) console.debug(...args); };
|
|
@@ -249,18 +253,29 @@ code } }
|
|
|
249
253
|
// Helper to get correctly scoped delegate for the current request
|
|
250
254
|
const getScopedDelegate = async (): Promise<DataDriver> => {
|
|
251
255
|
const session = clientSessions.get(clientId);
|
|
252
|
-
|
|
256
|
+
// Check if the driver supports RLS-scoped delegates
|
|
257
|
+
if (typeof driver.withAuth === "function") {
|
|
253
258
|
try {
|
|
254
|
-
const userForAuth = session?.user
|
|
259
|
+
const userForAuth: User = session?.user
|
|
255
260
|
? {
|
|
256
261
|
uid: session.user.userId,
|
|
262
|
+
displayName: null,
|
|
263
|
+
email: null,
|
|
264
|
+
photoURL: null,
|
|
265
|
+
providerId: "websocket",
|
|
266
|
+
isAnonymous: false,
|
|
257
267
|
roles: session.user.roles ?? []
|
|
258
268
|
}
|
|
259
269
|
: {
|
|
260
270
|
uid: "anon",
|
|
271
|
+
displayName: null,
|
|
272
|
+
email: null,
|
|
273
|
+
photoURL: null,
|
|
274
|
+
providerId: "websocket",
|
|
275
|
+
isAnonymous: true,
|
|
261
276
|
roles: ["anon"]
|
|
262
277
|
};
|
|
263
|
-
return await
|
|
278
|
+
return await driver.withAuth(userForAuth);
|
|
264
279
|
} catch (e) {
|
|
265
280
|
console.error("Failed to create RLS scoped delegate for WS request", e);
|
|
266
281
|
throw new Error("Internal authentication error");
|
|
@@ -547,15 +562,13 @@ colors: true }));
|
|
|
547
562
|
}
|
|
548
563
|
break;
|
|
549
564
|
|
|
550
|
-
// Route subscription messages to RealtimeService
|
|
565
|
+
// Route subscription messages, broadcast channels, and presence to RealtimeService
|
|
551
566
|
case "subscribe_collection":
|
|
552
567
|
case "subscribe_entity":
|
|
553
568
|
case "unsubscribe":
|
|
554
|
-
// Broadcast channels
|
|
555
569
|
case "join_channel":
|
|
556
570
|
case "leave_channel":
|
|
557
571
|
case "broadcast":
|
|
558
|
-
// Presence
|
|
559
572
|
case "presence_track":
|
|
560
573
|
case "presence_untrack":
|
|
561
574
|
case "presence_state": {
|
|
@@ -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", () => {
|