@rebasepro/server-postgresql 0.6.0 → 0.7.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 (82) hide show
  1. package/package.json +24 -18
  2. package/src/PostgresBackendDriver.ts +65 -44
  3. package/src/PostgresBootstrapper.ts +49 -20
  4. package/src/auth/ensure-tables.ts +56 -1
  5. package/src/auth/services.ts +94 -1
  6. package/src/cli-errors.ts +162 -0
  7. package/src/cli-helpers.ts +183 -0
  8. package/src/cli.ts +198 -251
  9. package/src/data-transformer.ts +9 -1
  10. package/src/schema/auth-default-policies.ts +90 -0
  11. package/src/schema/auth-schema.ts +25 -2
  12. package/src/schema/doctor.ts +2 -4
  13. package/src/schema/generate-drizzle-schema-logic.ts +4 -3
  14. package/src/schema/generate-drizzle-schema.ts +3 -5
  15. package/src/schema/generate-postgres-ddl-logic.ts +524 -0
  16. package/src/schema/generate-postgres-ddl.ts +116 -0
  17. package/src/services/EntityPersistService.ts +18 -16
  18. package/src/services/entityService.ts +28 -3
  19. package/src/utils/pg-array-null-patch.ts +42 -0
  20. package/src/utils/pg-error-utils.ts +16 -0
  21. package/src/utils/table-classification.ts +16 -0
  22. package/src/websocket.ts +9 -0
  23. package/test/array-null-safety.test.ts +335 -0
  24. package/test/auth-default-policies.test.ts +89 -0
  25. package/test/cli-helpers-extended.test.ts +324 -0
  26. package/test/cli-helpers.test.ts +59 -0
  27. package/test/connection.test.ts +292 -0
  28. package/test/data-transformer.test.ts +53 -2
  29. package/test/databasePoolManager.test.ts +289 -0
  30. package/test/doctor-extended.test.ts +443 -0
  31. package/test/e2e/db-e2e.test.ts +293 -0
  32. package/test/e2e/pg-setup.ts +79 -0
  33. package/test/entity-persist-composite-keys.test.ts +451 -0
  34. package/test/generate-postgres-ddl-edge-cases.test.ts +716 -0
  35. package/test/generate-postgres-ddl.test.ts +300 -0
  36. package/test/mfa-service.test.ts +544 -0
  37. package/test/pg-array-null-patch.test.ts +65 -0
  38. package/test/pg-error-utils.test.ts +50 -1
  39. package/test/realtimeService-channels.test.ts +696 -0
  40. package/test/unmapped-tables-safety.test.ts +55 -342
  41. package/vite.config.ts +8 -6
  42. package/vitest.e2e.config.ts +10 -0
  43. package/build-errors.txt +0 -37
  44. package/dist/PostgresAdapter.d.ts +0 -6
  45. package/dist/PostgresBackendDriver.d.ts +0 -110
  46. package/dist/PostgresBootstrapper.d.ts +0 -46
  47. package/dist/auth/ensure-tables.d.ts +0 -10
  48. package/dist/auth/services.d.ts +0 -231
  49. package/dist/cli.d.ts +0 -1
  50. package/dist/collections/PostgresCollectionRegistry.d.ts +0 -47
  51. package/dist/connection.d.ts +0 -65
  52. package/dist/data-transformer.d.ts +0 -55
  53. package/dist/databasePoolManager.d.ts +0 -20
  54. package/dist/history/HistoryService.d.ts +0 -71
  55. package/dist/history/ensure-history-table.d.ts +0 -7
  56. package/dist/index.d.ts +0 -14
  57. package/dist/index.es.js +0 -10764
  58. package/dist/index.es.js.map +0 -1
  59. package/dist/index.umd.js +0 -11055
  60. package/dist/index.umd.js.map +0 -1
  61. package/dist/interfaces.d.ts +0 -18
  62. package/dist/schema/auth-schema.d.ts +0 -2149
  63. package/dist/schema/doctor-cli.d.ts +0 -2
  64. package/dist/schema/doctor.d.ts +0 -52
  65. package/dist/schema/generate-drizzle-schema-logic.d.ts +0 -2
  66. package/dist/schema/generate-drizzle-schema.d.ts +0 -1
  67. package/dist/schema/introspect-db-inference.d.ts +0 -5
  68. package/dist/schema/introspect-db-logic.d.ts +0 -118
  69. package/dist/schema/introspect-db.d.ts +0 -1
  70. package/dist/schema/test-schema.d.ts +0 -24
  71. package/dist/services/BranchService.d.ts +0 -47
  72. package/dist/services/EntityFetchService.d.ts +0 -214
  73. package/dist/services/EntityPersistService.d.ts +0 -40
  74. package/dist/services/RelationService.d.ts +0 -98
  75. package/dist/services/entity-helpers.d.ts +0 -38
  76. package/dist/services/entityService.d.ts +0 -110
  77. package/dist/services/index.d.ts +0 -4
  78. package/dist/services/realtimeService.d.ts +0 -220
  79. package/dist/types.d.ts +0 -3
  80. package/dist/utils/drizzle-conditions.d.ts +0 -138
  81. package/dist/utils/pg-error-utils.d.ts +0 -55
  82. package/dist/websocket.d.ts +0 -11
@@ -0,0 +1,716 @@
1
+ import { EntityCollection, PostgresCollection } from "@rebasepro/types";
2
+ import { generatePostgresDdl, generatePostgresPoliciesDdl } from "../src/schema/generate-postgres-ddl-logic";
3
+
4
+ describe("generatePostgresDdl edge cases", () => {
5
+ const cleanDdl = (ddl: string) => {
6
+ return ddl
7
+ .replace(/--.*$/gm, "") // Remove single-line SQL comments
8
+ .replace(/\n{2,}/g, "\n") // Collapse multiple newlines
9
+ .replace(/\s+/g, " ") // Collapse whitespace
10
+ .trim();
11
+ };
12
+
13
+ // ── 1. Empty collections array ────────────────────────────────────
14
+ describe("empty collections array", () => {
15
+ it("should produce valid DDL with only the header comment", async () => {
16
+ const result = await generatePostgresDdl([]);
17
+ expect(result).toContain("auto-generated");
18
+ // No CREATE TABLE or ALTER TABLE statements
19
+ expect(result).not.toContain("CREATE TABLE");
20
+ expect(result).not.toContain("ALTER TABLE");
21
+ expect(result).not.toContain("CREATE TYPE");
22
+ });
23
+
24
+ it("should produce valid DDL regardless of includePolicies option", async () => {
25
+ const withPolicies = await generatePostgresDdl([], { includePolicies: true });
26
+ const withoutPolicies = await generatePostgresDdl([], { includePolicies: false });
27
+ expect(withPolicies).not.toContain("CREATE TABLE");
28
+ expect(withoutPolicies).not.toContain("CREATE TABLE");
29
+ });
30
+ });
31
+
32
+ // ── 2. Enum values with single quotes ─────────────────────────────
33
+ describe("enum values with single quotes", () => {
34
+ it("should embed single quotes in enum values without escaping (documents current behavior)", async () => {
35
+ const collections: EntityCollection[] = [
36
+ {
37
+ slug: "phrases",
38
+ table: "phrases",
39
+ name: "Phrases",
40
+ properties: {
41
+ mood: {
42
+ type: "string",
43
+ enum: ["it's", "won't"]
44
+ }
45
+ }
46
+ }
47
+ ];
48
+
49
+ const result = await generatePostgresDdl(collections);
50
+ // Current behavior: single quotes inside values are NOT escaped,
51
+ // producing invalid SQL. Document this as a known limitation.
52
+ expect(result).toContain("CREATE TYPE");
53
+ expect(result).toContain("phrases_mood");
54
+ // The raw output will contain the unescaped quotes
55
+ expect(result).toContain("it's");
56
+ expect(result).toContain("won't");
57
+ });
58
+ });
59
+
60
+ // ── 3. Table name with special characters ─────────────────────────
61
+ describe("table name with special characters", () => {
62
+ it("should quote table names containing hyphens", async () => {
63
+ const collections: EntityCollection[] = [
64
+ {
65
+ slug: "my-items",
66
+ table: "my-items",
67
+ name: "My Items",
68
+ properties: {
69
+ name: { type: "string" }
70
+ }
71
+ }
72
+ ];
73
+
74
+ const result = await generatePostgresDdl(collections);
75
+ const cleanResult = cleanDdl(result);
76
+ // Table name should be quoted, preserving hyphens
77
+ expect(cleanResult).toContain("CREATE TABLE \"public\".\"my-items\" (");
78
+ expect(cleanResult).toContain("ALTER TABLE \"public\".\"my-items\" ENABLE ROW LEVEL SECURITY");
79
+ });
80
+
81
+ it("should quote table names containing spaces", async () => {
82
+ const collections: EntityCollection[] = [
83
+ {
84
+ slug: "my items",
85
+ table: "my items",
86
+ name: "My Items",
87
+ properties: {
88
+ name: { type: "string" }
89
+ }
90
+ }
91
+ ];
92
+
93
+ const result = await generatePostgresDdl(collections);
94
+ expect(result).toContain("\"my items\"");
95
+ });
96
+ });
97
+
98
+ // ── 4. Column with reserved SQL keyword names ─────────────────────
99
+ describe("columns with reserved SQL keyword names", () => {
100
+ it("should quote column names that are reserved SQL keywords", async () => {
101
+ const collections: EntityCollection[] = [
102
+ {
103
+ slug: "records",
104
+ table: "records",
105
+ name: "Records",
106
+ properties: {
107
+ order: { type: "number" },
108
+ select: { type: "string" },
109
+ table: { type: "string" },
110
+ user: { type: "string" }
111
+ }
112
+ }
113
+ ];
114
+
115
+ const result = await generatePostgresDdl(collections);
116
+ const cleanResult = cleanDdl(result);
117
+
118
+ // All column names should be double-quoted, which protects them
119
+ // from being interpreted as SQL keywords
120
+ expect(cleanResult).toContain("\"order\" NUMERIC");
121
+ expect(cleanResult).toContain("\"select\" VARCHAR(255)");
122
+ expect(cleanResult).toContain("\"table\" VARCHAR(255)");
123
+ expect(cleanResult).toContain("\"user\" VARCHAR(255)");
124
+ });
125
+
126
+ it("should also generate implicit id column when properties use reserved keywords", async () => {
127
+ const collections: EntityCollection[] = [
128
+ {
129
+ slug: "records",
130
+ table: "records",
131
+ name: "Records",
132
+ properties: {
133
+ order: { type: "number" },
134
+ group: { type: "string" }
135
+ }
136
+ }
137
+ ];
138
+
139
+ const result = await generatePostgresDdl(collections);
140
+ const cleanResult = cleanDdl(result);
141
+ // Implicit id should still be present
142
+ expect(cleanResult).toContain("\"id\" VARCHAR(255) PRIMARY KEY");
143
+ });
144
+ });
145
+
146
+ // ── 5. Number with isId: 'increment' ──────────────────────────────
147
+ describe("number with isId increment", () => {
148
+ it("should generate GENERATED BY DEFAULT AS IDENTITY for numeric auto-increment id", async () => {
149
+ const collections: EntityCollection[] = [
150
+ {
151
+ slug: "counters",
152
+ table: "counters",
153
+ name: "Counters",
154
+ properties: {
155
+ id: { type: "number", isId: "increment" },
156
+ label: { type: "string" }
157
+ }
158
+ }
159
+ ];
160
+
161
+ const result = await generatePostgresDdl(collections);
162
+ const cleanResult = cleanDdl(result);
163
+
164
+ expect(cleanResult).toContain("\"id\" INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY");
165
+ // Ensure no implicit VARCHAR id is added
166
+ expect(cleanResult).not.toContain("VARCHAR(255) PRIMARY KEY");
167
+ });
168
+
169
+ it("should not add duplicate id column when isId increment is used", async () => {
170
+ const collections: EntityCollection[] = [
171
+ {
172
+ slug: "items",
173
+ table: "items",
174
+ name: "Items",
175
+ properties: {
176
+ id: { type: "number", isId: "increment" },
177
+ name: { type: "string" }
178
+ }
179
+ }
180
+ ];
181
+
182
+ const result = await generatePostgresDdl(collections);
183
+ // Count occurrences of PRIMARY KEY — should be exactly 1
184
+ const pkCount = (result.match(/PRIMARY KEY/g) ?? []).length;
185
+ expect(pkCount).toBe(1);
186
+ });
187
+ });
188
+
189
+ // ── 6. Collection with only relation properties ───────────────────
190
+ describe("collection with only relation properties", () => {
191
+ it("should add implicit id column when collection has only relation properties", async () => {
192
+ const usersCollection: EntityCollection = {
193
+ slug: "users",
194
+ table: "users",
195
+ name: "Users",
196
+ properties: {
197
+ name: { type: "string" }
198
+ }
199
+ };
200
+ const profileCollection: EntityCollection = {
201
+ slug: "profiles",
202
+ table: "profiles",
203
+ name: "Profiles",
204
+ properties: {
205
+ owner: { type: "relation", relationName: "owner" }
206
+ },
207
+ relations: [
208
+ {
209
+ relationName: "owner",
210
+ target: () => usersCollection,
211
+ cardinality: "one",
212
+ localKey: "owner_id",
213
+ onDelete: "cascade"
214
+ }
215
+ ]
216
+ };
217
+
218
+ const result = await generatePostgresDdl([usersCollection, profileCollection]);
219
+ const cleanResult = cleanDdl(result);
220
+
221
+ // Implicit id should be added since no explicit PK exists
222
+ expect(cleanResult).toContain("\"id\" VARCHAR(255) PRIMARY KEY");
223
+ // FK column should also be present
224
+ expect(cleanResult).toContain("\"owner_id\" VARCHAR(255)");
225
+ });
226
+
227
+ it("should add implicit id even when collection has zero non-relation properties", async () => {
228
+ const collections: EntityCollection[] = [
229
+ {
230
+ slug: "empty_ish",
231
+ table: "empty_ish",
232
+ name: "Empty-ish",
233
+ properties: {}
234
+ }
235
+ ];
236
+
237
+ const result = await generatePostgresDdl(collections);
238
+ const cleanResult = cleanDdl(result);
239
+
240
+ // Even with no properties, implicit id is added
241
+ expect(cleanResult).toContain("CREATE TABLE \"public\".\"empty_ish\" (");
242
+ expect(cleanResult).toContain("\"id\" VARCHAR(255) PRIMARY KEY");
243
+ });
244
+ });
245
+
246
+ // ── 7. generatePostgresPoliciesDdl standalone ─────────────────────
247
+ describe("generatePostgresPoliciesDdl", () => {
248
+ it("should generate RLS enable statement for each collection", () => {
249
+ const collections: EntityCollection[] = [
250
+ {
251
+ slug: "posts",
252
+ table: "posts",
253
+ name: "Posts",
254
+ properties: {
255
+ title: { type: "string" }
256
+ }
257
+ },
258
+ {
259
+ slug: "comments",
260
+ table: "comments",
261
+ name: "Comments",
262
+ properties: {
263
+ body: { type: "string" }
264
+ }
265
+ }
266
+ ];
267
+
268
+ const result = generatePostgresPoliciesDdl(collections);
269
+
270
+ expect(result).toContain("ALTER TABLE \"public\".\"posts\" ENABLE ROW LEVEL SECURITY");
271
+ expect(result).toContain("ALTER TABLE \"public\".\"comments\" ENABLE ROW LEVEL SECURITY");
272
+ });
273
+
274
+ it("should generate policies for collections with security rules", () => {
275
+ const collections: PostgresCollection[] = [
276
+ {
277
+ slug: "posts",
278
+ table: "posts",
279
+ name: "Posts",
280
+ properties: {
281
+ title: { type: "string" },
282
+ author_id: { type: "string" }
283
+ },
284
+ securityRules: [
285
+ {
286
+ operation: "select",
287
+ access: "public"
288
+ },
289
+ {
290
+ operation: "delete",
291
+ ownerField: "author_id"
292
+ }
293
+ ]
294
+ }
295
+ ];
296
+
297
+ const result = generatePostgresPoliciesDdl(collections);
298
+ const cleanResult = cleanDdl(result);
299
+
300
+ expect(cleanResult).toContain("CREATE POLICY");
301
+ expect(cleanResult).toContain("USING (true)");
302
+ expect(cleanResult).toContain("FOR SELECT");
303
+ expect(cleanResult).toContain("FOR DELETE");
304
+ expect(cleanResult).toContain("author_id = auth.uid()");
305
+ });
306
+
307
+ it("should not generate any policies for collections without security rules", () => {
308
+ const collections: EntityCollection[] = [
309
+ {
310
+ slug: "items",
311
+ table: "items",
312
+ name: "Items",
313
+ properties: {
314
+ name: { type: "string" }
315
+ }
316
+ }
317
+ ];
318
+
319
+ const result = generatePostgresPoliciesDdl(collections);
320
+
321
+ expect(result).toContain("ALTER TABLE");
322
+ expect(result).toContain("ENABLE ROW LEVEL SECURITY");
323
+ expect(result).not.toContain("CREATE POLICY");
324
+ });
325
+
326
+ it("should return only the header comment for an empty collections array", () => {
327
+ const result = generatePostgresPoliciesDdl([]);
328
+ expect(result).toContain("RLS policies");
329
+ expect(result).not.toContain("ALTER TABLE");
330
+ expect(result).not.toContain("CREATE POLICY");
331
+ });
332
+
333
+ it("should use custom schema in policy DDL", () => {
334
+ const collections: PostgresCollection[] = [
335
+ {
336
+ slug: "docs",
337
+ table: "docs",
338
+ name: "Docs",
339
+ schema: "app",
340
+ properties: {
341
+ title: { type: "string" }
342
+ },
343
+ securityRules: [
344
+ {
345
+ operation: "select",
346
+ access: "public"
347
+ }
348
+ ]
349
+ }
350
+ ];
351
+
352
+ const result = generatePostgresPoliciesDdl(collections);
353
+
354
+ expect(result).toContain("ALTER TABLE \"app\".\"docs\" ENABLE ROW LEVEL SECURITY");
355
+ expect(result).toContain("ON \"app\".\"docs\"");
356
+ });
357
+
358
+ it("should handle multiple operations in a single rule", () => {
359
+ const collections: PostgresCollection[] = [
360
+ {
361
+ slug: "entries",
362
+ table: "entries",
363
+ name: "Entries",
364
+ properties: {
365
+ data: { type: "string" }
366
+ },
367
+ securityRules: [
368
+ {
369
+ name: "public_read_write",
370
+ operations: ["select", "insert"],
371
+ access: "public"
372
+ }
373
+ ]
374
+ }
375
+ ];
376
+
377
+ const result = generatePostgresPoliciesDdl(collections);
378
+ const cleanResult = cleanDdl(result);
379
+
380
+ expect(cleanResult).toContain("CREATE POLICY \"public_read_write_select\"");
381
+ expect(cleanResult).toContain("CREATE POLICY \"public_read_write_insert\"");
382
+ });
383
+
384
+ it("should generate restrictive policies with pgRoles", () => {
385
+ const collections: PostgresCollection[] = [
386
+ {
387
+ slug: "secrets",
388
+ table: "secrets",
389
+ name: "Secrets",
390
+ properties: {
391
+ value: { type: "string" }
392
+ },
393
+ securityRules: [
394
+ {
395
+ name: "admin_only",
396
+ operation: "select",
397
+ mode: "restrictive",
398
+ using: "true",
399
+ pgRoles: ["admin_role"]
400
+ }
401
+ ]
402
+ }
403
+ ];
404
+
405
+ const result = generatePostgresPoliciesDdl(collections);
406
+ const cleanResult = cleanDdl(result);
407
+
408
+ expect(cleanResult).toContain("AS RESTRICTIVE");
409
+ expect(cleanResult).toContain("TO \"admin_role\"");
410
+ });
411
+
412
+ it("should fall back to USING (false) when no clause can be built for select", () => {
413
+ const collections: PostgresCollection[] = [
414
+ {
415
+ slug: "locked",
416
+ table: "locked",
417
+ name: "Locked",
418
+ properties: {
419
+ data: { type: "string" }
420
+ },
421
+ securityRules: [
422
+ {
423
+ operation: "select"
424
+ // No access, ownerField, or using — should produce USING (false)
425
+ }
426
+ ]
427
+ }
428
+ ];
429
+
430
+ const result = generatePostgresPoliciesDdl(collections);
431
+ const cleanResult = cleanDdl(result);
432
+
433
+ expect(cleanResult).toContain("USING (false)");
434
+ });
435
+
436
+ it("should generate WITH CHECK (false) for insert without any clause", () => {
437
+ const collections: PostgresCollection[] = [
438
+ {
439
+ slug: "locked",
440
+ table: "locked",
441
+ name: "Locked",
442
+ properties: {
443
+ data: { type: "string" }
444
+ },
445
+ securityRules: [
446
+ {
447
+ operation: "insert"
448
+ // No clause — should produce WITH CHECK (false)
449
+ }
450
+ ]
451
+ }
452
+ ];
453
+
454
+ const result = generatePostgresPoliciesDdl(collections);
455
+ const cleanResult = cleanDdl(result);
456
+
457
+ expect(cleanResult).toContain("WITH CHECK (false)");
458
+ // INSERT should not have USING clause
459
+ expect(cleanResult).not.toContain("USING");
460
+ });
461
+
462
+ it("should generate both USING and WITH CHECK for update", () => {
463
+ const collections: PostgresCollection[] = [
464
+ {
465
+ slug: "items",
466
+ table: "items",
467
+ name: "Items",
468
+ properties: {
469
+ owner_id: { type: "string" }
470
+ },
471
+ securityRules: [
472
+ {
473
+ operation: "update",
474
+ ownerField: "owner_id"
475
+ }
476
+ ]
477
+ }
478
+ ];
479
+
480
+ const result = generatePostgresPoliciesDdl(collections);
481
+ const cleanResult = cleanDdl(result);
482
+
483
+ expect(cleanResult).toContain("FOR UPDATE");
484
+ expect(cleanResult).toContain("USING (owner_id = auth.uid())");
485
+ expect(cleanResult).toContain("WITH CHECK (owner_id = auth.uid())");
486
+ });
487
+
488
+ it("should include DROP POLICY IF EXISTS before CREATE POLICY", () => {
489
+ const collections: PostgresCollection[] = [
490
+ {
491
+ slug: "items",
492
+ table: "items",
493
+ name: "Items",
494
+ properties: {
495
+ data: { type: "string" }
496
+ },
497
+ securityRules: [
498
+ {
499
+ operation: "select",
500
+ access: "public"
501
+ }
502
+ ]
503
+ }
504
+ ];
505
+
506
+ const result = generatePostgresPoliciesDdl(collections);
507
+
508
+ expect(result).toContain("DROP POLICY IF EXISTS");
509
+ // DROP should appear before CREATE
510
+ const dropIdx = result.indexOf("DROP POLICY IF EXISTS");
511
+ const createIdx = result.indexOf("CREATE POLICY");
512
+ expect(dropIdx).toBeLessThan(createIdx);
513
+ });
514
+ });
515
+
516
+ // ── 8. Multiple collections referencing the same enum ─────────────
517
+ describe("multiple collections referencing same enum type", () => {
518
+ it("should create separate enum types per collection (enum names are scoped to table)", async () => {
519
+ const collections: EntityCollection[] = [
520
+ {
521
+ slug: "orders",
522
+ table: "orders",
523
+ name: "Orders",
524
+ properties: {
525
+ status: {
526
+ type: "string",
527
+ enum: ["pending", "active", "closed"]
528
+ }
529
+ }
530
+ },
531
+ {
532
+ slug: "invoices",
533
+ table: "invoices",
534
+ name: "Invoices",
535
+ properties: {
536
+ status: {
537
+ type: "string",
538
+ enum: ["pending", "active", "closed"]
539
+ }
540
+ }
541
+ }
542
+ ];
543
+
544
+ const result = await generatePostgresDdl(collections);
545
+
546
+ // Each collection gets its own scoped enum type
547
+ expect(result).toContain("\"orders_status\"");
548
+ expect(result).toContain("\"invoices_status\"");
549
+ // Both should appear exactly once
550
+ const ordersEnumCount = (result.match(/CREATE TYPE.*orders_status/g) ?? []).length;
551
+ const invoicesEnumCount = (result.match(/CREATE TYPE.*invoices_status/g) ?? []).length;
552
+ expect(ordersEnumCount).toBe(1);
553
+ expect(invoicesEnumCount).toBe(1);
554
+ });
555
+
556
+ it("should not create duplicate enum when same collection has same-named enum", async () => {
557
+ const collections: EntityCollection[] = [
558
+ {
559
+ slug: "products",
560
+ table: "products",
561
+ name: "Products",
562
+ properties: {
563
+ category: {
564
+ type: "string",
565
+ enum: ["electronics", "clothing"]
566
+ },
567
+ size: {
568
+ type: "string",
569
+ enum: ["S", "M", "L"]
570
+ }
571
+ }
572
+ }
573
+ ];
574
+
575
+ const result = await generatePostgresDdl(collections);
576
+
577
+ // Two different enums should be created
578
+ expect(result).toContain("\"products_category\"");
579
+ expect(result).toContain("\"products_size\"");
580
+ const categoryCount = (result.match(/CREATE TYPE.*products_category/g) ?? []).length;
581
+ const sizeCount = (result.match(/CREATE TYPE.*products_size/g) ?? []).length;
582
+ expect(categoryCount).toBe(1);
583
+ expect(sizeCount).toBe(1);
584
+ });
585
+ });
586
+
587
+ // ── 9. Binary type ────────────────────────────────────────────────
588
+ describe("binary type", () => {
589
+ it("should generate BYTEA column for binary type", async () => {
590
+ const collections: EntityCollection[] = [
591
+ {
592
+ slug: "files",
593
+ table: "files",
594
+ name: "Files",
595
+ properties: {
596
+ data: { type: "binary" },
597
+ checksum: { type: "binary" }
598
+ }
599
+ }
600
+ ];
601
+
602
+ const result = await generatePostgresDdl(collections);
603
+ const cleanResult = cleanDdl(result);
604
+
605
+ expect(cleanResult).toContain("\"data\" BYTEA");
606
+ expect(cleanResult).toContain("\"checksum\" BYTEA");
607
+ });
608
+
609
+ it("should support required binary columns", async () => {
610
+ const collections: EntityCollection[] = [
611
+ {
612
+ slug: "blobs",
613
+ table: "blobs",
614
+ name: "Blobs",
615
+ properties: {
616
+ content: { type: "binary", validation: { required: true } }
617
+ }
618
+ }
619
+ ];
620
+
621
+ const result = await generatePostgresDdl(collections);
622
+ const cleanResult = cleanDdl(result);
623
+
624
+ expect(cleanResult).toContain("\"content\" BYTEA NOT NULL");
625
+ });
626
+ });
627
+
628
+ // ── 10. Vector type ───────────────────────────────────────────────
629
+ describe("vector type", () => {
630
+ it("should generate VECTOR(N) column with specified dimensions", async () => {
631
+ const collections: EntityCollection[] = [
632
+ {
633
+ slug: "embeddings",
634
+ table: "embeddings",
635
+ name: "Embeddings",
636
+ properties: {
637
+ embedding: { type: "vector", dimensions: 1536 }
638
+ }
639
+ }
640
+ ];
641
+
642
+ const result = await generatePostgresDdl(collections);
643
+ const cleanResult = cleanDdl(result);
644
+
645
+ expect(cleanResult).toContain("\"embedding\" VECTOR(1536)");
646
+ });
647
+
648
+ it("should support different dimension values", async () => {
649
+ const collections: EntityCollection[] = [
650
+ {
651
+ slug: "vectors",
652
+ table: "vectors",
653
+ name: "Vectors",
654
+ properties: {
655
+ small_vec: { type: "vector", dimensions: 3 },
656
+ medium_vec: { type: "vector", dimensions: 384 },
657
+ large_vec: { type: "vector", dimensions: 4096 }
658
+ }
659
+ }
660
+ ];
661
+
662
+ const result = await generatePostgresDdl(collections);
663
+ const cleanResult = cleanDdl(result);
664
+
665
+ expect(cleanResult).toContain("\"small_vec\" VECTOR(3)");
666
+ expect(cleanResult).toContain("\"medium_vec\" VECTOR(384)");
667
+ expect(cleanResult).toContain("\"large_vec\" VECTOR(4096)");
668
+ });
669
+
670
+ it("should support required vector columns", async () => {
671
+ const collections: EntityCollection[] = [
672
+ {
673
+ slug: "required_vecs",
674
+ table: "required_vecs",
675
+ name: "Required Vecs",
676
+ properties: {
677
+ vec: { type: "vector", dimensions: 768, validation: { required: true } }
678
+ }
679
+ }
680
+ ];
681
+
682
+ const result = await generatePostgresDdl(collections);
683
+ const cleanResult = cleanDdl(result);
684
+
685
+ expect(cleanResult).toContain("\"vec\" VECTOR(768) NOT NULL");
686
+ });
687
+ });
688
+
689
+ // ── Bonus: includePolicies: false ─────────────────────────────────
690
+ describe("includePolicies option", () => {
691
+ it("should not include RLS or policies when includePolicies is false", async () => {
692
+ const collections: PostgresCollection[] = [
693
+ {
694
+ slug: "items",
695
+ table: "items",
696
+ name: "Items",
697
+ properties: {
698
+ name: { type: "string" }
699
+ },
700
+ securityRules: [
701
+ {
702
+ operation: "select",
703
+ access: "public"
704
+ }
705
+ ]
706
+ }
707
+ ];
708
+
709
+ const result = await generatePostgresDdl(collections, { includePolicies: false });
710
+
711
+ expect(result).toContain("CREATE TABLE");
712
+ expect(result).not.toContain("ENABLE ROW LEVEL SECURITY");
713
+ expect(result).not.toContain("CREATE POLICY");
714
+ });
715
+ });
716
+ });