@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.
Files changed (60) hide show
  1. package/README.md +69 -89
  2. package/dist/common/src/collections/default-collections.d.ts +5 -8
  3. package/dist/common/src/data/query_builder.d.ts +6 -2
  4. package/dist/common/src/util/permissions.d.ts +14 -6
  5. package/dist/index.es.js +379 -611
  6. package/dist/index.es.js.map +1 -1
  7. package/dist/index.umd.js +375 -607
  8. package/dist/index.umd.js.map +1 -1
  9. package/dist/server-postgresql/src/PostgresBackendDriver.d.ts +2 -0
  10. package/dist/server-postgresql/src/auth/ensure-tables.d.ts +7 -4
  11. package/dist/server-postgresql/src/auth/services.d.ts +17 -42
  12. package/dist/server-postgresql/src/data-transformer.d.ts +0 -3
  13. package/dist/server-postgresql/src/databasePoolManager.d.ts +1 -1
  14. package/dist/server-postgresql/src/schema/auth-schema.d.ts +87 -340
  15. package/dist/server-postgresql/src/services/EntityFetchService.d.ts +2 -1
  16. package/dist/server-postgresql/src/services/EntityPersistService.d.ts +4 -0
  17. package/dist/server-postgresql/src/services/entityService.d.ts +4 -0
  18. package/dist/server-postgresql/src/types.d.ts +3 -0
  19. package/dist/server-postgresql/src/utils/drizzle-conditions.d.ts +5 -1
  20. package/dist/server-postgresql/src/websocket.d.ts +8 -3
  21. package/dist/types/src/controllers/auth.d.ts +2 -2
  22. package/dist/types/src/controllers/client.d.ts +25 -40
  23. package/dist/types/src/controllers/data.d.ts +21 -3
  24. package/dist/types/src/controllers/data_driver.d.ts +5 -0
  25. package/dist/types/src/controllers/email.d.ts +2 -0
  26. package/dist/types/src/types/auth_adapter.d.ts +3 -56
  27. package/dist/types/src/types/backend.d.ts +38 -3
  28. package/dist/types/src/types/backend_hooks.d.ts +2 -17
  29. package/dist/types/src/types/collections.d.ts +30 -6
  30. package/dist/types/src/types/entity_views.d.ts +19 -28
  31. package/dist/types/src/types/properties.d.ts +9 -15
  32. package/dist/types/src/types/user_management_delegate.d.ts +16 -53
  33. package/dist/types/src/users/index.d.ts +0 -1
  34. package/dist/types/src/users/user.d.ts +0 -1
  35. package/package.json +6 -6
  36. package/src/PostgresBackendDriver.ts +10 -0
  37. package/src/PostgresBootstrapper.ts +27 -22
  38. package/src/auth/ensure-tables.ts +82 -129
  39. package/src/auth/services.ts +99 -197
  40. package/src/cli.ts +50 -23
  41. package/src/data-transformer.ts +57 -95
  42. package/src/databasePoolManager.ts +2 -1
  43. package/src/schema/auth-schema.ts +13 -69
  44. package/src/schema/doctor.ts +44 -3
  45. package/src/schema/generate-drizzle-schema-logic.ts +33 -3
  46. package/src/schema/generate-drizzle-schema.ts +2 -6
  47. package/src/schema/introspect-db-logic.ts +7 -0
  48. package/src/services/EntityFetchService.ts +13 -1
  49. package/src/services/EntityPersistService.ts +38 -12
  50. package/src/services/entityService.ts +7 -0
  51. package/src/types.ts +4 -0
  52. package/src/utils/drizzle-conditions.ts +40 -5
  53. package/src/websocket.ts +38 -25
  54. package/test/auth-services.test.ts +7 -150
  55. package/test/doctor.test.ts +6 -2
  56. package/test/relation-pipeline-gaps.test.ts +315 -0
  57. package/dist/server-postgresql/src/schema/default-collections.d.ts +0 -2
  58. package/dist/types/src/users/roles.d.ts +0 -14
  59. package/drizzle.test.config.ts +0 -10
  60. 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
- const err = error as Error & { cause?: unknown };
406
+ if (!(error instanceof Error)) return null;
386
407
 
387
- if (err.cause && typeof err.cause === "object") {
388
- const deeper = this.extractCauseMessage(err.cause);
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 (err.cause instanceof Error && err.cause.message) {
392
- return err.cause.message;
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): (Error & { code?: string; detail?: unknown; hint?: unknown; constraint?: unknown; column?: unknown; table?: unknown; dataType?: unknown }) | null {
435
+ private extractPgError(error: unknown): PostgresError | null {
415
436
  if (!error || typeof error !== "object") return null;
416
-
417
- const err = error as Error & { code?: string; cause?: unknown; detail?: unknown };
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 (err.code && /^[0-9A-Z]{5}$/.test(err.code)) {
421
- return err as Error & { code: string; detail?: unknown; hint?: unknown; constraint?: unknown; column?: unknown; table?: unknown; dataType?: unknown };
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 (err.cause && typeof err.cause === "object") {
426
- return this.extractPgError(err.cause);
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
@@ -0,0 +1,4 @@
1
+ import type { PgTable, AnyPgColumn } from "drizzle-orm/pg-core";
2
+
3
+ /** Drizzle PgTable with column access by name. Runtime Drizzle tables satisfy this shape. */
4
+ export type RebasePgTable = PgTable & Record<string, AnyPgColumn>;
@@ -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 condition = this.buildSingleFilterCondition(fieldColumn, op, value);
57
- if (condition) {
58
- conditions.push(condition);
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, isSQLAdmin, isSchemaAdmin, AuthAdapter } from "@rebasepro/types";
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
- // @ts-ignore
10
- import { AuthConfig } from "@rebasepro/server-core";
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
- const clientSessions = new Map<string, ClientSession>();
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 (typeof error === "object") {
56
- const err = error as Record<string, unknown> & { cause?: unknown; message?: string };
57
- if (err.cause) {
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: unknown) => {
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?: 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
- if ("withAuth" in driver && typeof (driver as unknown as Record<string, unknown>).withAuth === "function") {
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 (driver as unknown as { withAuth: (user: Record<string, unknown>) => Promise<DataDriver> }).withAuth(userForAuth);
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, RoleService, RefreshTokenService, PasswordResetTokenService, Role } from "../src/auth/services";
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: "Admin",
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: [{ id: "admin",
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: "Admin",
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
 
@@ -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
- columnType: "json" } as ArrayProperty)).toBe("json");
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", () => {