@rebasepro/server-postgresql 0.1.2 → 0.2.3
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/LICENSE +22 -6
- package/dist/common/src/data/query_builder.d.ts +51 -0
- package/dist/common/src/index.d.ts +1 -0
- package/dist/common/src/util/entities.d.ts +2 -2
- package/dist/common/src/util/relations.d.ts +1 -1
- package/dist/index.es.js +1435 -738
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +1433 -736
- package/dist/index.umd.js.map +1 -1
- package/dist/server-postgresql/src/PostgresAdapter.d.ts +6 -0
- package/dist/server-postgresql/src/PostgresBackendDriver.d.ts +2 -1
- package/dist/server-postgresql/src/PostgresBootstrapper.d.ts +0 -5
- package/dist/server-postgresql/src/auth/ensure-tables.d.ts +2 -1
- package/dist/server-postgresql/src/auth/services.d.ts +37 -15
- package/dist/server-postgresql/src/index.d.ts +1 -0
- package/dist/server-postgresql/src/schema/auth-schema.d.ts +43 -856
- package/dist/server-postgresql/src/schema/default-collections.d.ts +2 -0
- package/dist/server-postgresql/src/schema/doctor.d.ts +10 -1
- package/dist/server-postgresql/src/schema/introspect-db-logic.d.ts +1 -0
- package/dist/server-postgresql/src/services/entity-helpers.d.ts +1 -1
- package/dist/server-postgresql/src/services/realtimeService.d.ts +12 -0
- package/dist/server-postgresql/src/websocket.d.ts +2 -1
- package/dist/types/src/controllers/auth.d.ts +9 -8
- package/dist/types/src/controllers/client.d.ts +3 -0
- package/dist/types/src/controllers/data.d.ts +21 -0
- package/dist/types/src/types/auth_adapter.d.ts +356 -0
- package/dist/types/src/types/collections.d.ts +67 -2
- package/dist/types/src/types/database_adapter.d.ts +94 -0
- package/dist/types/src/types/entity_actions.d.ts +7 -1
- package/dist/types/src/types/entity_callbacks.d.ts +1 -1
- package/dist/types/src/types/entity_views.d.ts +36 -1
- package/dist/types/src/types/index.d.ts +2 -0
- package/dist/types/src/types/plugins.d.ts +1 -1
- package/dist/types/src/types/properties.d.ts +24 -5
- package/dist/types/src/types/property_config.d.ts +6 -2
- package/dist/types/src/types/relations.d.ts +1 -1
- package/dist/types/src/types/translations.d.ts +8 -0
- package/dist/types/src/users/user.d.ts +5 -0
- package/package.json +22 -15
- package/src/PostgresAdapter.ts +59 -0
- package/src/PostgresBackendDriver.ts +66 -13
- package/src/PostgresBootstrapper.ts +35 -15
- package/src/auth/ensure-tables.ts +82 -189
- package/src/auth/services.ts +421 -170
- package/src/cli.ts +49 -13
- package/src/data-transformer.ts +78 -8
- package/src/history/HistoryService.ts +25 -2
- package/src/index.ts +1 -0
- package/src/schema/auth-schema.ts +130 -98
- package/src/schema/default-collections.ts +69 -0
- package/src/schema/doctor-cli.ts +5 -1
- package/src/schema/doctor.ts +166 -48
- package/src/schema/generate-drizzle-schema-logic.ts +74 -27
- package/src/schema/generate-drizzle-schema.ts +13 -3
- package/src/schema/introspect-db-inference.ts +5 -5
- package/src/schema/introspect-db-logic.ts +9 -2
- package/src/schema/introspect-db.ts +14 -3
- package/src/services/EntityFetchService.ts +5 -5
- package/src/services/RelationService.ts +2 -2
- package/src/services/entity-helpers.ts +1 -1
- package/src/services/realtimeService.ts +145 -136
- package/src/utils/drizzle-conditions.ts +16 -2
- package/src/websocket.ts +113 -37
- package/test/auth-services.test.ts +163 -74
- package/test/data-transformer-hardening.test.ts +57 -0
- package/test/data-transformer.test.ts +43 -0
- package/test/generate-drizzle-schema.test.ts +7 -5
- package/test/introspect-db-utils.test.ts +4 -1
- package/test/postgresDataDriver.test.ts +147 -1
- package/test/realtimeService.test.ts +7 -7
- package/test/websocket.test.ts +139 -0
|
@@ -1,19 +1,45 @@
|
|
|
1
1
|
import { NodePgDatabase } from "drizzle-orm/node-postgres";
|
|
2
2
|
import { UserService, RoleService, RefreshTokenService, PasswordResetTokenService, Role } from "../src/auth/services";
|
|
3
|
-
import { users, refreshTokens, passwordResetTokens
|
|
3
|
+
import { users, refreshTokens, passwordResetTokens } from "../src/schema/auth-schema";
|
|
4
|
+
import { UserData } from "@rebasepro/server-core";
|
|
4
5
|
|
|
5
6
|
// Mock the drizzle-orm functions
|
|
6
|
-
jest.mock("drizzle-orm", () =>
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
}))
|
|
7
|
+
jest.mock("drizzle-orm", () => {
|
|
8
|
+
const actual = jest.requireActual("drizzle-orm");
|
|
9
|
+
return {
|
|
10
|
+
...actual,
|
|
11
|
+
eq: jest.fn((field, value) => ({ field, value, type: "eq" })),
|
|
12
|
+
sql: Object.assign(
|
|
13
|
+
jest.fn((strings: TemplateStringsArray, ...values: unknown[]) => ({
|
|
14
|
+
strings,
|
|
15
|
+
values,
|
|
16
|
+
type: "sql"
|
|
17
|
+
})),
|
|
18
|
+
{
|
|
19
|
+
raw: jest.fn((val: string) => ({ val, type: "sql-raw" })),
|
|
20
|
+
join: jest.fn((parts: unknown[], separator: unknown) => ({ parts, separator, type: "sql-join" }))
|
|
21
|
+
}
|
|
22
|
+
),
|
|
23
|
+
relations: jest.fn(() => ({}))
|
|
24
|
+
};
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
function mockUserData(overrides: Partial<UserData>): UserData {
|
|
28
|
+
return {
|
|
29
|
+
id: "user-123",
|
|
30
|
+
email: "test@example.com",
|
|
31
|
+
passwordHash: null,
|
|
32
|
+
displayName: null,
|
|
33
|
+
photoUrl: null,
|
|
34
|
+
emailVerified: false,
|
|
35
|
+
emailVerificationToken: null,
|
|
36
|
+
emailVerificationSentAt: null,
|
|
37
|
+
createdAt: expect.any(Date),
|
|
38
|
+
updatedAt: expect.any(Date),
|
|
39
|
+
metadata: {},
|
|
40
|
+
...overrides
|
|
41
|
+
};
|
|
42
|
+
}
|
|
17
43
|
|
|
18
44
|
describe("Auth Services", () => {
|
|
19
45
|
let db: jest.Mocked<NodePgDatabase<Record<string, unknown>>>;
|
|
@@ -37,9 +63,7 @@ describe("Auth Services", () => {
|
|
|
37
63
|
});
|
|
38
64
|
|
|
39
65
|
mockSelectWhere = jest.fn().mockResolvedValue([]);
|
|
40
|
-
mockSelectFrom = jest.fn()
|
|
41
|
-
where: mockSelectWhere
|
|
42
|
-
});
|
|
66
|
+
mockSelectFrom = jest.fn();
|
|
43
67
|
|
|
44
68
|
mockUpdateReturning = jest.fn().mockResolvedValue([]);
|
|
45
69
|
mockUpdateWhere = jest.fn().mockReturnValue({ returning: mockUpdateReturning });
|
|
@@ -49,13 +73,50 @@ describe("Auth Services", () => {
|
|
|
49
73
|
|
|
50
74
|
mockExecute = jest.fn().mockResolvedValue({ rows: [] });
|
|
51
75
|
|
|
76
|
+
// Set up chainable mock for db.select()
|
|
77
|
+
const mockChain: any = {};
|
|
78
|
+
mockChain.from = jest.fn().mockImplementation((...args) => {
|
|
79
|
+
const result = mockSelectFrom(...args);
|
|
80
|
+
if (result && typeof result.then === "function") {
|
|
81
|
+
return result; // If listUsers mocks selectFrom to return a promise, return it directly
|
|
82
|
+
}
|
|
83
|
+
return mockChain;
|
|
84
|
+
});
|
|
85
|
+
mockChain.innerJoin = jest.fn().mockReturnValue(mockChain);
|
|
86
|
+
mockChain.where = jest.fn().mockImplementation((...args) => {
|
|
87
|
+
mockChain.wherePromise = mockSelectWhere(...args);
|
|
88
|
+
return mockChain;
|
|
89
|
+
});
|
|
90
|
+
mockChain.limit = jest.fn().mockReturnValue(mockChain);
|
|
91
|
+
mockChain.offset = jest.fn().mockReturnValue(mockChain);
|
|
92
|
+
mockChain.orderBy = jest.fn().mockReturnValue(mockChain);
|
|
93
|
+
mockChain.then = jest.fn().mockImplementation(async (onFulfilled) => {
|
|
94
|
+
let val;
|
|
95
|
+
if (mockChain.wherePromise) {
|
|
96
|
+
val = await mockChain.wherePromise;
|
|
97
|
+
mockChain.wherePromise = null;
|
|
98
|
+
} else if (mockSelectWhere.mock.calls.length > 0) {
|
|
99
|
+
const result = mockSelectWhere.mock.results[mockSelectWhere.mock.results.length - 1];
|
|
100
|
+
val = result.type === "return" ? result.value : undefined;
|
|
101
|
+
if (val && typeof val.then === "function") {
|
|
102
|
+
val = await val;
|
|
103
|
+
}
|
|
104
|
+
} else {
|
|
105
|
+
val = [];
|
|
106
|
+
}
|
|
107
|
+
return onFulfilled(val || []);
|
|
108
|
+
});
|
|
109
|
+
|
|
52
110
|
db = {
|
|
53
111
|
insert: jest.fn().mockReturnValue({ values: mockInsertValues }),
|
|
54
|
-
select: jest.fn().mockReturnValue(
|
|
112
|
+
select: jest.fn().mockReturnValue(mockChain),
|
|
55
113
|
update: jest.fn().mockReturnValue({ set: mockUpdateSet }),
|
|
56
114
|
delete: jest.fn().mockReturnValue({ where: mockDeleteWhere }),
|
|
57
115
|
execute: mockExecute
|
|
58
116
|
} as unknown as jest.Mocked<NodePgDatabase<Record<string, unknown>>>;
|
|
117
|
+
|
|
118
|
+
// Set default return value for mockSelectFrom to return mockChain (chainable)
|
|
119
|
+
mockSelectFrom.mockReturnValue(mockChain);
|
|
59
120
|
});
|
|
60
121
|
|
|
61
122
|
describe("UserService", () => {
|
|
@@ -71,30 +132,34 @@ describe("Auth Services", () => {
|
|
|
71
132
|
email: "test@example.com",
|
|
72
133
|
displayName: "Test User"
|
|
73
134
|
};
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
135
|
+
const dbReturnedUser = {
|
|
136
|
+
id: "user-123",
|
|
137
|
+
...newUser,
|
|
138
|
+
createdAt: new Date(),
|
|
139
|
+
updatedAt: new Date()
|
|
140
|
+
};
|
|
141
|
+
mockInsertReturning.mockResolvedValueOnce([dbReturnedUser]);
|
|
79
142
|
|
|
80
143
|
const result = await userService.createUser(newUser);
|
|
81
144
|
|
|
82
145
|
expect(db.insert).toHaveBeenCalledWith(users);
|
|
83
|
-
expect(mockInsertValues).toHaveBeenCalledWith(
|
|
84
|
-
|
|
146
|
+
expect(mockInsertValues).toHaveBeenCalledWith({
|
|
147
|
+
...newUser,
|
|
148
|
+
metadata: {}
|
|
149
|
+
});
|
|
150
|
+
expect(result).toEqual(mockUserData({ displayName: "Test User" }));
|
|
85
151
|
});
|
|
86
152
|
});
|
|
87
153
|
|
|
88
154
|
describe("getUserById", () => {
|
|
89
155
|
it("should return user when found", async () => {
|
|
90
|
-
const mockUser = { id: "user-123",
|
|
91
|
-
email: "test@example.com" };
|
|
156
|
+
const mockUser = { id: "user-123", email: "test@example.com" };
|
|
92
157
|
mockSelectWhere.mockResolvedValueOnce([mockUser]);
|
|
93
158
|
|
|
94
159
|
const result = await userService.getUserById("user-123");
|
|
95
160
|
|
|
96
161
|
expect(db.select).toHaveBeenCalled();
|
|
97
|
-
expect(result).toEqual(
|
|
162
|
+
expect(result).toEqual(mockUserData({}));
|
|
98
163
|
});
|
|
99
164
|
|
|
100
165
|
it("should return null when user not found", async () => {
|
|
@@ -108,13 +173,12 @@ email: "test@example.com" };
|
|
|
108
173
|
|
|
109
174
|
describe("getUserByEmail", () => {
|
|
110
175
|
it("should return user when found by email", async () => {
|
|
111
|
-
const mockUser = { id: "user-123",
|
|
112
|
-
email: "test@example.com" };
|
|
176
|
+
const mockUser = { id: "user-123", email: "test@example.com" };
|
|
113
177
|
mockSelectWhere.mockResolvedValueOnce([mockUser]);
|
|
114
178
|
|
|
115
179
|
const result = await userService.getUserByEmail("test@example.com");
|
|
116
180
|
|
|
117
|
-
expect(result).toEqual(
|
|
181
|
+
expect(result).toEqual(mockUserData({}));
|
|
118
182
|
});
|
|
119
183
|
|
|
120
184
|
it("should lowercase email for lookup", async () => {
|
|
@@ -128,14 +192,14 @@ email: "test@example.com" };
|
|
|
128
192
|
});
|
|
129
193
|
|
|
130
194
|
describe("getUserByIdentity", () => {
|
|
131
|
-
it("should
|
|
132
|
-
const mockUser = { id: "user-123" };
|
|
133
|
-
|
|
134
|
-
mockExecute.mockResolvedValueOnce({ rows: [mockUser] });
|
|
195
|
+
it("should fetch user by identity", async () => {
|
|
196
|
+
const mockUser = { id: "user-123", email: "test@example.com" };
|
|
197
|
+
mockSelectWhere.mockResolvedValueOnce([{ user: mockUser }]);
|
|
135
198
|
|
|
136
199
|
const result = await userService.getUserByIdentity("google", "google-abc");
|
|
137
200
|
|
|
138
|
-
expect(
|
|
201
|
+
expect(db.select).toHaveBeenCalled();
|
|
202
|
+
expect(result).toEqual(expect.objectContaining({ id: "user-123", email: "test@example.com" }));
|
|
139
203
|
});
|
|
140
204
|
});
|
|
141
205
|
|
|
@@ -158,9 +222,11 @@ email: "test@example.com" };
|
|
|
158
222
|
|
|
159
223
|
describe("updateUser", () => {
|
|
160
224
|
it("should update user and return updated record", async () => {
|
|
161
|
-
const updatedUser = {
|
|
162
|
-
|
|
163
|
-
|
|
225
|
+
const updatedUser = {
|
|
226
|
+
id: "user-123",
|
|
227
|
+
email: "test@example.com",
|
|
228
|
+
displayName: "Updated Name"
|
|
229
|
+
};
|
|
164
230
|
mockUpdateReturning.mockResolvedValueOnce([updatedUser]);
|
|
165
231
|
|
|
166
232
|
const result = await userService.updateUser("user-123", { displayName: "Updated Name" });
|
|
@@ -170,7 +236,7 @@ displayName: "Updated Name" };
|
|
|
170
236
|
displayName: "Updated Name",
|
|
171
237
|
updatedAt: expect.any(Date)
|
|
172
238
|
}));
|
|
173
|
-
expect(result).toEqual(
|
|
239
|
+
expect(result).toEqual(mockUserData({ displayName: "Updated Name" }));
|
|
174
240
|
});
|
|
175
241
|
|
|
176
242
|
it("should return null when user not found", async () => {
|
|
@@ -194,17 +260,18 @@ displayName: "Updated Name" };
|
|
|
194
260
|
describe("listUsers", () => {
|
|
195
261
|
it("should return all users", async () => {
|
|
196
262
|
const mockUsers = [
|
|
197
|
-
{ id: "user-1",
|
|
198
|
-
email: "
|
|
199
|
-
{ id: "user-2",
|
|
200
|
-
email: "user2@example.com" }
|
|
263
|
+
{ id: "user-1", email: "user1@example.com" },
|
|
264
|
+
{ id: "user-2", email: "user2@example.com" }
|
|
201
265
|
];
|
|
202
266
|
mockSelectFrom.mockReturnValueOnce(Promise.resolve(mockUsers));
|
|
203
267
|
|
|
204
268
|
const result = await userService.listUsers();
|
|
205
269
|
|
|
206
270
|
expect(db.select).toHaveBeenCalled();
|
|
207
|
-
expect(result).toEqual(
|
|
271
|
+
expect(result).toEqual([
|
|
272
|
+
mockUserData({ id: "user-1", email: "user1@example.com" }),
|
|
273
|
+
mockUserData({ id: "user-2", email: "user2@example.com" })
|
|
274
|
+
]);
|
|
208
275
|
});
|
|
209
276
|
});
|
|
210
277
|
|
|
@@ -264,13 +331,12 @@ email: "user2@example.com" }
|
|
|
264
331
|
|
|
265
332
|
describe("getUserByVerificationToken", () => {
|
|
266
333
|
it("should find user by verification token", async () => {
|
|
267
|
-
const mockUser = { id: "user-123",
|
|
268
|
-
email: "test@example.com" };
|
|
334
|
+
const mockUser = { id: "user-123", email: "test@example.com" };
|
|
269
335
|
mockSelectWhere.mockResolvedValueOnce([mockUser]);
|
|
270
336
|
|
|
271
337
|
const result = await userService.getUserByVerificationToken("token-abc");
|
|
272
338
|
|
|
273
|
-
expect(result).toEqual(
|
|
339
|
+
expect(result).toEqual(mockUserData({}));
|
|
274
340
|
});
|
|
275
341
|
});
|
|
276
342
|
|
|
@@ -279,17 +345,17 @@ email: "test@example.com" };
|
|
|
279
345
|
mockExecute.mockResolvedValueOnce({
|
|
280
346
|
rows: [
|
|
281
347
|
{ id: "admin",
|
|
282
|
-
name: "Admin",
|
|
283
|
-
is_admin: true,
|
|
284
|
-
default_permissions: null,
|
|
285
|
-
collection_permissions: null,
|
|
286
|
-
config: null },
|
|
348
|
+
name: "Admin",
|
|
349
|
+
is_admin: true,
|
|
350
|
+
default_permissions: null,
|
|
351
|
+
collection_permissions: null,
|
|
352
|
+
config: null },
|
|
287
353
|
{ id: "editor",
|
|
288
|
-
name: "Editor",
|
|
289
|
-
is_admin: false,
|
|
290
|
-
default_permissions: { edit: true },
|
|
291
|
-
collection_permissions: null,
|
|
292
|
-
config: null }
|
|
354
|
+
name: "Editor",
|
|
355
|
+
is_admin: false,
|
|
356
|
+
default_permissions: { edit: true },
|
|
357
|
+
collection_permissions: null,
|
|
358
|
+
config: null }
|
|
293
359
|
]
|
|
294
360
|
});
|
|
295
361
|
|
|
@@ -312,11 +378,11 @@ config: null }
|
|
|
312
378
|
mockExecute.mockResolvedValueOnce({
|
|
313
379
|
rows: [
|
|
314
380
|
{ id: "admin",
|
|
315
|
-
name: "Admin",
|
|
316
|
-
is_admin: true,
|
|
317
|
-
default_permissions: null,
|
|
318
|
-
collection_permissions: null,
|
|
319
|
-
config: null }
|
|
381
|
+
name: "Admin",
|
|
382
|
+
is_admin: true,
|
|
383
|
+
default_permissions: null,
|
|
384
|
+
collection_permissions: null,
|
|
385
|
+
config: null }
|
|
320
386
|
]
|
|
321
387
|
});
|
|
322
388
|
|
|
@@ -353,28 +419,27 @@ config: null }
|
|
|
353
419
|
|
|
354
420
|
describe("getUserWithRoles", () => {
|
|
355
421
|
it("should return user with roles", async () => {
|
|
356
|
-
const mockUser = { id: "user-123",
|
|
357
|
-
email: "test@example.com" };
|
|
422
|
+
const mockUser = { id: "user-123", email: "test@example.com" };
|
|
358
423
|
mockSelectWhere.mockResolvedValueOnce([mockUser]);
|
|
359
424
|
mockExecute.mockResolvedValueOnce({
|
|
360
425
|
rows: [{ id: "admin",
|
|
361
|
-
name: "Admin",
|
|
362
|
-
is_admin: true,
|
|
363
|
-
default_permissions: null,
|
|
364
|
-
collection_permissions: null,
|
|
365
|
-
config: null }]
|
|
426
|
+
name: "Admin",
|
|
427
|
+
is_admin: true,
|
|
428
|
+
default_permissions: null,
|
|
429
|
+
collection_permissions: null,
|
|
430
|
+
config: null }]
|
|
366
431
|
});
|
|
367
432
|
|
|
368
433
|
const result = await userService.getUserWithRoles("user-123");
|
|
369
434
|
|
|
370
435
|
expect(result).toEqual({
|
|
371
|
-
user:
|
|
436
|
+
user: mockUserData({}),
|
|
372
437
|
roles: [{ id: "admin",
|
|
373
|
-
name: "Admin",
|
|
374
|
-
isAdmin: true,
|
|
375
|
-
defaultPermissions: null,
|
|
376
|
-
collectionPermissions: null,
|
|
377
|
-
config: null }]
|
|
438
|
+
name: "Admin",
|
|
439
|
+
isAdmin: true,
|
|
440
|
+
defaultPermissions: null,
|
|
441
|
+
collectionPermissions: null,
|
|
442
|
+
config: null }]
|
|
378
443
|
});
|
|
379
444
|
});
|
|
380
445
|
|
|
@@ -386,6 +451,30 @@ config: null }]
|
|
|
386
451
|
expect(result).toBeNull();
|
|
387
452
|
});
|
|
388
453
|
});
|
|
454
|
+
|
|
455
|
+
describe("listUsersPaginated", () => {
|
|
456
|
+
it("should return paginated and filtered users list", async () => {
|
|
457
|
+
mockExecute
|
|
458
|
+
.mockResolvedValueOnce({ rows: [{ total: 1 }] })
|
|
459
|
+
.mockResolvedValueOnce({ rows: [{ id: "user-123", email: "test@example.com" }] });
|
|
460
|
+
|
|
461
|
+
const result = await userService.listUsersPaginated({
|
|
462
|
+
limit: 10,
|
|
463
|
+
offset: 0,
|
|
464
|
+
search: "test",
|
|
465
|
+
orderBy: "email",
|
|
466
|
+
orderDir: "asc"
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
expect(mockExecute).toHaveBeenCalledTimes(2);
|
|
470
|
+
expect(result).toEqual({
|
|
471
|
+
users: [mockUserData({ id: "user-123", email: "test@example.com" })],
|
|
472
|
+
total: 1,
|
|
473
|
+
limit: 10,
|
|
474
|
+
offset: 0
|
|
475
|
+
});
|
|
476
|
+
});
|
|
477
|
+
});
|
|
389
478
|
});
|
|
390
479
|
|
|
391
480
|
describe("RoleService", () => {
|
|
@@ -415,3 +415,60 @@ describe("structural dunder guard", () => {
|
|
|
415
415
|
expect(dunderKeys).toEqual([]);
|
|
416
416
|
});
|
|
417
417
|
});
|
|
418
|
+
|
|
419
|
+
// ─────────────────────────────────────────────────────────────
|
|
420
|
+
// parsePropertyFromServer — vector parsing
|
|
421
|
+
// ─────────────────────────────────────────────────────────────
|
|
422
|
+
describe("parsePropertyFromServer vector parsing", () => {
|
|
423
|
+
const targetCollection = makeCollection("items", {
|
|
424
|
+
embedding: {
|
|
425
|
+
type: "vector",
|
|
426
|
+
dimensions: 3,
|
|
427
|
+
name: "Embedding"
|
|
428
|
+
} as unknown as Property
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
it("parses string representation '[0.1,-0.2,3.14]' from postgres into wrapped vector", () => {
|
|
432
|
+
const property = targetCollection.properties.embedding as Property;
|
|
433
|
+
const result = parsePropertyFromServer("[0.1,-0.2,3.14]", property, targetCollection, "embedding");
|
|
434
|
+
expect(result).toEqual({
|
|
435
|
+
__type: "Vector",
|
|
436
|
+
value: [0.1, -0.2, 3.14]
|
|
437
|
+
});
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
it("parses numeric array [0.1,-0.2,3.14] into wrapped vector", () => {
|
|
441
|
+
const property = targetCollection.properties.embedding as Property;
|
|
442
|
+
const result = parsePropertyFromServer([0.1, -0.2, 3.14], property, targetCollection, "embedding");
|
|
443
|
+
expect(result).toEqual({
|
|
444
|
+
__type: "Vector",
|
|
445
|
+
value: [0.1, -0.2, 3.14]
|
|
446
|
+
});
|
|
447
|
+
});
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
// ─────────────────────────────────────────────────────────────
|
|
451
|
+
// parsePropertyFromServer — binary parsing
|
|
452
|
+
// ─────────────────────────────────────────────────────────────
|
|
453
|
+
describe("parsePropertyFromServer binary parsing", () => {
|
|
454
|
+
const targetCollection = makeCollection("items", {
|
|
455
|
+
data: {
|
|
456
|
+
type: "binary",
|
|
457
|
+
name: "Data"
|
|
458
|
+
} as unknown as Property
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
it("parses database Buffer into a base64 data URL string", () => {
|
|
462
|
+
const property = targetCollection.properties.data as Property;
|
|
463
|
+
const buffer = Buffer.from("hello");
|
|
464
|
+
const result = parsePropertyFromServer(buffer, property, targetCollection, "data");
|
|
465
|
+
expect(result).toBe("data:application/octet-stream;base64,aGVsbG8=");
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
it("parses Buffer object (JSON serialization) into a base64 data URL string", () => {
|
|
469
|
+
const property = targetCollection.properties.data as Property;
|
|
470
|
+
const bufferObj = { type: "Buffer", data: Array.from(Buffer.from("hello")) };
|
|
471
|
+
const result = parsePropertyFromServer(bufferObj, property, targetCollection, "data");
|
|
472
|
+
expect(result).toBe("data:application/octet-stream;base64,aGVsbG8=");
|
|
473
|
+
});
|
|
474
|
+
});
|
|
@@ -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("
|
|
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("
|
|
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("
|
|
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("
|
|
613
|
-
expect(result).toContain("
|
|
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", "
|
|
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");
|