@rebasepro/server-postgresql 0.1.2 → 0.2.1

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 (68) hide show
  1. package/LICENSE +22 -6
  2. package/dist/common/src/util/entities.d.ts +2 -2
  3. package/dist/common/src/util/relations.d.ts +1 -1
  4. package/dist/index.es.js +1160 -612
  5. package/dist/index.es.js.map +1 -1
  6. package/dist/index.umd.js +1158 -610
  7. package/dist/index.umd.js.map +1 -1
  8. package/dist/server-postgresql/src/PostgresAdapter.d.ts +6 -0
  9. package/dist/server-postgresql/src/PostgresBackendDriver.d.ts +2 -1
  10. package/dist/server-postgresql/src/PostgresBootstrapper.d.ts +0 -5
  11. package/dist/server-postgresql/src/auth/ensure-tables.d.ts +2 -1
  12. package/dist/server-postgresql/src/auth/services.d.ts +37 -15
  13. package/dist/server-postgresql/src/index.d.ts +1 -0
  14. package/dist/server-postgresql/src/schema/auth-schema.d.ts +43 -856
  15. package/dist/server-postgresql/src/schema/default-collections.d.ts +2 -0
  16. package/dist/server-postgresql/src/schema/doctor.d.ts +10 -1
  17. package/dist/server-postgresql/src/schema/introspect-db-logic.d.ts +1 -0
  18. package/dist/server-postgresql/src/services/entity-helpers.d.ts +1 -1
  19. package/dist/server-postgresql/src/services/realtimeService.d.ts +12 -0
  20. package/dist/server-postgresql/src/websocket.d.ts +2 -1
  21. package/dist/types/src/controllers/auth.d.ts +9 -8
  22. package/dist/types/src/controllers/client.d.ts +3 -0
  23. package/dist/types/src/types/auth_adapter.d.ts +356 -0
  24. package/dist/types/src/types/collections.d.ts +67 -2
  25. package/dist/types/src/types/database_adapter.d.ts +94 -0
  26. package/dist/types/src/types/entity_actions.d.ts +7 -1
  27. package/dist/types/src/types/entity_callbacks.d.ts +1 -1
  28. package/dist/types/src/types/entity_views.d.ts +36 -1
  29. package/dist/types/src/types/index.d.ts +2 -0
  30. package/dist/types/src/types/plugins.d.ts +1 -1
  31. package/dist/types/src/types/properties.d.ts +24 -5
  32. package/dist/types/src/types/property_config.d.ts +6 -2
  33. package/dist/types/src/types/relations.d.ts +1 -1
  34. package/dist/types/src/types/translations.d.ts +8 -0
  35. package/dist/types/src/users/user.d.ts +5 -0
  36. package/package.json +21 -15
  37. package/src/PostgresAdapter.ts +59 -0
  38. package/src/PostgresBackendDriver.ts +57 -8
  39. package/src/PostgresBootstrapper.ts +35 -15
  40. package/src/auth/ensure-tables.ts +82 -189
  41. package/src/auth/services.ts +421 -170
  42. package/src/cli.ts +44 -13
  43. package/src/data-transformer.ts +78 -8
  44. package/src/history/HistoryService.ts +25 -2
  45. package/src/index.ts +1 -0
  46. package/src/schema/auth-schema.ts +130 -98
  47. package/src/schema/default-collections.ts +68 -0
  48. package/src/schema/doctor-cli.ts +5 -1
  49. package/src/schema/doctor.ts +85 -8
  50. package/src/schema/generate-drizzle-schema-logic.ts +74 -27
  51. package/src/schema/generate-drizzle-schema.ts +13 -3
  52. package/src/schema/introspect-db-inference.ts +5 -5
  53. package/src/schema/introspect-db-logic.ts +9 -2
  54. package/src/schema/introspect-db.ts +14 -3
  55. package/src/services/EntityFetchService.ts +5 -5
  56. package/src/services/RelationService.ts +2 -2
  57. package/src/services/entity-helpers.ts +1 -1
  58. package/src/services/realtimeService.ts +145 -136
  59. package/src/utils/drizzle-conditions.ts +16 -2
  60. package/src/websocket.ts +113 -37
  61. package/test/auth-services.test.ts +163 -74
  62. package/test/data-transformer-hardening.test.ts +57 -0
  63. package/test/data-transformer.test.ts +43 -0
  64. package/test/generate-drizzle-schema.test.ts +7 -5
  65. package/test/introspect-db-utils.test.ts +4 -1
  66. package/test/postgresDataDriver.test.ts +17 -0
  67. package/test/realtimeService.test.ts +7 -7
  68. package/test/websocket.test.ts +139 -0
@@ -172,4 +172,47 @@ path: "authors" }
172
172
  expect(serializePropertyToServer(false, boolProp)).toBe(false);
173
173
  });
174
174
  });
175
+
176
+ // ── Vector property ──
177
+ describe("vector property", () => {
178
+ const vectorProp: Property = { type: "vector", dimensions: 3 } as Property;
179
+
180
+ it("should serialize a Vector instance to a flat array", () => {
181
+ const vectorVal = { __type: "Vector", value: [1.5, -2.0, 3.14] };
182
+ expect(serializePropertyToServer(vectorVal, vectorProp)).toEqual([1.5, -2.0, 3.14]);
183
+ });
184
+
185
+ it("should serialize a raw number array to a flat array", () => {
186
+ expect(serializePropertyToServer([0.5, 0.6, 0.7], vectorProp)).toEqual([0.5, 0.6, 0.7]);
187
+ });
188
+
189
+ it("should return null for null values", () => {
190
+ expect(serializePropertyToServer(null, vectorProp)).toBeNull();
191
+ });
192
+
193
+ it("should return undefined for undefined values", () => {
194
+ expect(serializePropertyToServer(undefined, vectorProp)).toBeUndefined();
195
+ });
196
+ });
197
+
198
+ // ── Binary property ──
199
+ describe("binary property", () => {
200
+ const binaryProp: Property = { type: "binary" } as Property;
201
+
202
+ it("should serialize a base64 data URL to a Buffer", () => {
203
+ const base64Data = "data:application/octet-stream;base64,aGVsbG8=";
204
+ const result = serializePropertyToServer(base64Data, binaryProp);
205
+ expect(Buffer.isBuffer(result)).toBe(true);
206
+ expect((result as Buffer).toString("utf8")).toBe("hello");
207
+ });
208
+
209
+ it("should pass through a Buffer as-is", () => {
210
+ const buffer = Buffer.from("world");
211
+ expect(serializePropertyToServer(buffer, binaryProp)).toBe(buffer);
212
+ });
213
+
214
+ it("should return null for null values", () => {
215
+ expect(serializePropertyToServer(null, binaryProp)).toBeNull();
216
+ });
217
+ });
175
218
  });
@@ -116,6 +116,8 @@ relationName: "tags" }
116
116
  expect(cleanResult).toContain("tag_id: varchar(\"tag_id\").notNull().references(() => tags.id, { onDelete: \"cascade\" })");
117
117
  expect(cleanResult).toContain("(table) => ({ pk: primaryKey({ columns: [table.post_id, table.tag_id] }) })");
118
118
  expect(cleanResult).toContain("export const postsRelations = drizzleRelations(posts, ({ one, many }) => ({ \"tags\": many(postsToTags, { relationName: \"tags\" }) }));");
119
+ const expectedJunctionRelations = "export const postsToTagsRelations = drizzleRelations(postsToTags, ({ one, many }) => ({ \"post_id\": one(posts, { fields: [postsToTags.post_id], references: [posts.id], relationName: \"tags\" }), \"tag_id\": one(tags, { fields: [postsToTags.tag_id], references: [tags.id], relationName: \"posts_to_tags_tag_id\" }) }));";
120
+ expect(cleanResult).toContain(cleanSchema(expectedJunctionRelations));
119
121
  });
120
122
 
121
123
  describe("generateDrizzleSchema Column Types", () => {
@@ -391,7 +393,7 @@ ownerField: "user_id" }
391
393
  expect(result).toContain("pgPolicy");
392
394
  expect(result).toContain('as: "permissive"');
393
395
  expect(result).toContain('for: "all"');
394
- expect(result).toContain("${table.user_id} = auth.uid()");
396
+ expect(result).toContain("user_id = auth.uid()");
395
397
  // 'all' needs both using and withCheck
396
398
  expect(result).toContain("using:");
397
399
  expect(result).toContain("withCheck:");
@@ -565,7 +567,7 @@ using: "{is_locked} = false" }
565
567
 
566
568
  const result = await generateSchema(collections);
567
569
  expect(result).toContain('as: "restrictive"');
568
- expect(result).toContain("${table.is_locked} = false");
570
+ expect(result).toContain("is_locked = false");
569
571
  });
570
572
 
571
573
  it("should generate raw SQL using clause with column references", async () => {
@@ -584,7 +586,7 @@ using: "{published_at} > now() - interval '30 days'" }
584
586
  }];
585
587
 
586
588
  const result = await generateSchema(collections);
587
- expect(result).toContain("${table.published_at} > now() - interval '30 days'");
589
+ expect(result).toContain("published_at > now() - interval '30 days'");
588
590
  });
589
591
 
590
592
  it("should generate raw SQL withCheck clause", async () => {
@@ -609,8 +611,8 @@ using: "{published_at} > now() - interval '30 days'" }
609
611
  const result = await generateSchema(collections);
610
612
  expect(result).toContain("using:");
611
613
  expect(result).toContain("withCheck:");
612
- expect(result).toContain("${table.user_id} = auth.uid()");
613
- expect(result).toContain("${table.status} != 'archived'");
614
+ expect(result).toContain("user_id = auth.uid()");
615
+ expect(result).toContain("status != 'archived'");
614
616
  });
615
617
 
616
618
  it("should use custom policy names when provided", async () => {
@@ -200,10 +200,13 @@ describe("mapPgType", () => {
200
200
  expect(mapPgType("_text")).toBe("array");
201
201
  });
202
202
  it("maps string-like types to string", () => {
203
- for (const t of ["text", "varchar", "character varying", "char", "character", "uuid", "bytea", "inet", "cidr", "macaddr", "macaddr8", "interval"]) {
203
+ for (const t of ["text", "varchar", "character varying", "char", "character", "uuid", "inet", "cidr", "macaddr", "macaddr8", "interval"]) {
204
204
  expect(mapPgType(t)).toBe("string");
205
205
  }
206
206
  });
207
+ it("maps bytea to binary", () => {
208
+ expect(mapPgType("bytea")).toBe("binary");
209
+ });
207
210
  it("defaults unknown types to string", () => {
208
211
  expect(mapPgType("tsvector")).toBe("string");
209
212
  expect(mapPgType("xml")).toBe("string");
@@ -644,5 +644,22 @@ status: "new" });
644
644
  expect(mockRealtimeService.notifyEntityUpdate).toHaveBeenNthCalledWith(2, "call-2", "2", {}, undefined);
645
645
  });
646
646
  });
647
+
648
+ describe("PostgresBackendDriver Admin operations", () => {
649
+ it("fetchAvailableRoles should query roles filtered by current user membership", async () => {
650
+ const executeSqlSpy = jest.spyOn(delegate, "executeSql").mockResolvedValueOnce([
651
+ { rolname: "demo" },
652
+ { rolname: "cloudsqlsuperuser" }
653
+ ]);
654
+
655
+ const result = await delegate.fetchAvailableRoles();
656
+
657
+ expect(executeSqlSpy).toHaveBeenCalledWith(
658
+ "SELECT rolname FROM pg_roles WHERE pg_has_role(current_user, rolname, 'member') ORDER BY rolname;"
659
+ );
660
+ expect(result).toEqual(["demo", "cloudsqlsuperuser"]);
661
+ executeSqlSpy.mockRestore();
662
+ });
663
+ });
647
664
  });
648
665
 
@@ -3,12 +3,13 @@ import { NodePgDatabase } from "drizzle-orm/node-postgres";
3
3
  import { PostgresCollectionRegistry } from "../src/collections/PostgresCollectionRegistry";
4
4
  import { EntityCollection } from "@rebasepro/types";
5
5
 
6
+ const mockFetchCollection = jest.fn().mockResolvedValue([{ id: 1, path: "posts", values: { title: "Refetched Title" } }]);
7
+ const mockFetchEntity = jest.fn().mockResolvedValue({ id: 1, path: "posts", values: { title: "Refetched Entity Title" } });
8
+
6
9
  jest.mock("../src/services/entityService", () => ({
7
10
  EntityService: jest.fn().mockImplementation(() => ({
8
- fetchCollection: jest.fn().mockResolvedValue([{ id: 1,
9
- _rebase_invalidated: false }]),
10
- fetchEntity: jest.fn().mockResolvedValue({ id: 1,
11
- _rebase_invalidated: false }),
11
+ fetchCollection: mockFetchCollection,
12
+ fetchEntity: mockFetchEntity,
12
13
  searchEntities: jest.fn().mockResolvedValue([])
13
14
  }))
14
15
  }));
@@ -134,7 +135,7 @@ values: { _rebase_invalidated: true } } as any;
134
135
  await Promise.resolve();
135
136
 
136
137
  // It should fetch the collection with auth
137
- expect(mockDriver.fetchCollection).toHaveBeenCalled();
138
+ expect(mockFetchCollection).toHaveBeenCalled();
138
139
 
139
140
  // It should send the refetched data
140
141
  expect(ws.send).toHaveBeenCalled();
@@ -207,8 +208,7 @@ values: { _rebase_invalidated: true } } as any;
207
208
  await Promise.resolve();
208
209
 
209
210
  // It should fetch the single entity
210
- expect(mockDriver.fetchEntity).toHaveBeenCalledWith(expect.objectContaining({ path: "posts",
211
- entityId: "1" }));
211
+ expect(mockFetchEntity).toHaveBeenCalledWith("posts", "1", undefined);
212
212
 
213
213
  // It should send entity update
214
214
  expect(ws.send).toHaveBeenCalled();
@@ -0,0 +1,139 @@
1
+ import { describe, it, expect, jest, beforeEach } from "@jest/globals";
2
+ import { WebSocket, WebSocketServer } from "ws";
3
+ import { Server } from "http";
4
+
5
+ let mockWssInstance: any = null;
6
+
7
+ jest.mock("ws", () => {
8
+ return {
9
+ WebSocketServer: jest.fn().mockImplementation(() => {
10
+ const instance = {
11
+ on: jest.fn()
12
+ };
13
+ mockWssInstance = instance;
14
+ return instance;
15
+ }),
16
+ WebSocket: jest.fn()
17
+ };
18
+ });
19
+
20
+ jest.mock("@rebasepro/server-core", () => {
21
+ return {
22
+ extractUserFromToken: jest.fn().mockReturnValue({
23
+ userId: "admin-user",
24
+ roles: ["admin"]
25
+ })
26
+ };
27
+ });
28
+
29
+ import { createPostgresWebSocket } from "../src/websocket";
30
+ import { RealtimeService } from "../src/services/realtimeService";
31
+ import { PostgresBackendDriver } from "../src/PostgresBackendDriver";
32
+
33
+ describe("WebSocket Server SQL error handling", () => {
34
+ let mockServer: Server;
35
+ let mockRealtimeService: RealtimeService;
36
+ let mockDriver: PostgresBackendDriver;
37
+
38
+ beforeEach(() => {
39
+ jest.clearAllMocks();
40
+ mockWssInstance = null;
41
+
42
+ mockServer = {} as Server;
43
+ mockRealtimeService = {
44
+ addClient: jest.fn(),
45
+ registerDataDriverSubscription: jest.fn()
46
+ } as unknown as RealtimeService;
47
+
48
+ // Mock PostgresBackendDriver admin capabilities
49
+ mockDriver = {
50
+ key: "postgres",
51
+ initialised: true,
52
+ admin: {
53
+ executeSql: jest.fn()
54
+ }
55
+ } as unknown as PostgresBackendDriver;
56
+
57
+ // Trigger the wss initialization with requireAuth: true
58
+ createPostgresWebSocket(mockServer, mockRealtimeService, mockDriver, { requireAuth: true });
59
+ });
60
+
61
+ it("should handle EXECUTE_SQL errors cleanly and return ERROR message without throwing", async () => {
62
+ expect(mockWssInstance).toBeDefined();
63
+ expect(mockWssInstance.on).toHaveBeenCalledWith("connection", expect.any(Function));
64
+
65
+ const connectionCallback = mockWssInstance.on.mock.calls.find(
66
+ (call: any[]) => call[0] === "connection"
67
+ )[1];
68
+
69
+ // Simulate client connection
70
+ const mockWs = {
71
+ on: jest.fn(),
72
+ send: jest.fn()
73
+ } as unknown as any;
74
+
75
+ connectionCallback(mockWs);
76
+
77
+ // Retrieve the message callback
78
+ expect(mockWs.on).toHaveBeenCalledWith("message", expect.any(Function));
79
+ const messageCallback = mockWs.on.mock.calls.find(
80
+ (call: any[]) => call[0] === "message"
81
+ )[1];
82
+
83
+ // 1. Authenticate first as an admin
84
+ await messageCallback(
85
+ Buffer.from(
86
+ JSON.stringify({
87
+ type: "AUTHENTICATE",
88
+ requestId: "auth-req",
89
+ payload: {
90
+ token: "mock-admin-token"
91
+ }
92
+ })
93
+ )
94
+ );
95
+
96
+ expect(mockWs.send).toHaveBeenCalled();
97
+ const authResponse = JSON.parse(mockWs.send.mock.calls[0][0]);
98
+ expect(authResponse.type).toBe("AUTH_SUCCESS");
99
+
100
+ // Clear mock send calls before executing SQL
101
+ mockWs.send.mockClear();
102
+
103
+ // Mock executeSql to throw a permission denied error
104
+ (mockDriver.admin.executeSql as jest.Mock).mockRejectedValueOnce(
105
+ new Error("permission denied for table orders")
106
+ );
107
+
108
+ // 2. Simulate receiving EXECUTE_SQL message
109
+ await messageCallback(
110
+ Buffer.from(
111
+ JSON.stringify({
112
+ type: "EXECUTE_SQL",
113
+ requestId: "req-1",
114
+ payload: {
115
+ sql: "SELECT * FROM orders",
116
+ options: { role: "demo" }
117
+ }
118
+ })
119
+ )
120
+ );
121
+
122
+ // Verify executeSql was called
123
+ expect(mockDriver.admin.executeSql).toHaveBeenCalledWith("SELECT * FROM orders", { role: "demo" });
124
+
125
+ // Verify the client received a clean ERROR payload rather than crashing the socket
126
+ expect(mockWs.send).toHaveBeenCalled();
127
+ const sentMessage = JSON.parse(mockWs.send.mock.calls[0][0]);
128
+ expect(sentMessage).toEqual({
129
+ type: "ERROR",
130
+ requestId: "req-1",
131
+ payload: {
132
+ error: {
133
+ message: "permission denied for table orders",
134
+ code: "SQL_ERROR"
135
+ }
136
+ }
137
+ });
138
+ });
139
+ });