@rebasepro/server-postgresql 0.0.1-canary.4d4fb3e → 0.0.1-canary.ca2cb6e

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 (136) hide show
  1. package/dist/common/src/collections/CollectionRegistry.d.ts +8 -0
  2. package/dist/common/src/util/entities.d.ts +22 -0
  3. package/dist/common/src/util/relations.d.ts +14 -4
  4. package/dist/common/src/util/resolutions.d.ts +1 -1
  5. package/dist/index.es.js +1254 -591
  6. package/dist/index.es.js.map +1 -1
  7. package/dist/index.umd.js +1254 -591
  8. package/dist/index.umd.js.map +1 -1
  9. package/dist/server-postgresql/src/PostgresBackendDriver.d.ts +17 -29
  10. package/dist/server-postgresql/src/auth/services.d.ts +7 -3
  11. package/dist/server-postgresql/src/collections/PostgresCollectionRegistry.d.ts +1 -1
  12. package/dist/server-postgresql/src/connection.d.ts +34 -1
  13. package/dist/server-postgresql/src/data-transformer.d.ts +26 -4
  14. package/dist/server-postgresql/src/databasePoolManager.d.ts +2 -2
  15. package/dist/server-postgresql/src/schema/auth-schema.d.ts +139 -38
  16. package/dist/server-postgresql/src/schema/doctor-cli.d.ts +2 -0
  17. package/dist/server-postgresql/src/schema/doctor.d.ts +43 -0
  18. package/dist/server-postgresql/src/schema/generate-drizzle-schema-logic.d.ts +1 -1
  19. package/dist/server-postgresql/src/schema/test-schema.d.ts +24 -0
  20. package/dist/server-postgresql/src/services/EntityFetchService.d.ts +22 -8
  21. package/dist/server-postgresql/src/services/EntityPersistService.d.ts +1 -1
  22. package/dist/server-postgresql/src/services/RelationService.d.ts +11 -5
  23. package/dist/server-postgresql/src/services/entity-helpers.d.ts +16 -2
  24. package/dist/server-postgresql/src/services/entityService.d.ts +8 -6
  25. package/dist/server-postgresql/src/services/realtimeService.d.ts +2 -0
  26. package/dist/server-postgresql/src/utils/drizzle-conditions.d.ts +2 -2
  27. package/dist/types/src/controllers/auth.d.ts +2 -0
  28. package/dist/types/src/controllers/client.d.ts +119 -7
  29. package/dist/types/src/controllers/collection_registry.d.ts +4 -3
  30. package/dist/types/src/controllers/customization_controller.d.ts +7 -1
  31. package/dist/types/src/controllers/data.d.ts +34 -7
  32. package/dist/types/src/controllers/data_driver.d.ts +20 -28
  33. package/dist/types/src/controllers/database_admin.d.ts +2 -2
  34. package/dist/types/src/controllers/email.d.ts +34 -0
  35. package/dist/types/src/controllers/index.d.ts +1 -0
  36. package/dist/types/src/controllers/local_config_persistence.d.ts +4 -4
  37. package/dist/types/src/controllers/navigation.d.ts +5 -5
  38. package/dist/types/src/controllers/registry.d.ts +6 -3
  39. package/dist/types/src/controllers/side_entity_controller.d.ts +7 -6
  40. package/dist/types/src/controllers/storage.d.ts +24 -26
  41. package/dist/types/src/rebase_context.d.ts +8 -4
  42. package/dist/types/src/types/backend.d.ts +4 -1
  43. package/dist/types/src/types/builders.d.ts +5 -4
  44. package/dist/types/src/types/chips.d.ts +1 -1
  45. package/dist/types/src/types/collections.d.ts +169 -125
  46. package/dist/types/src/types/cron.d.ts +102 -0
  47. package/dist/types/src/types/data_source.d.ts +1 -1
  48. package/dist/types/src/types/entity_actions.d.ts +8 -8
  49. package/dist/types/src/types/entity_callbacks.d.ts +15 -15
  50. package/dist/types/src/types/entity_link_builder.d.ts +1 -1
  51. package/dist/types/src/types/entity_overrides.d.ts +2 -1
  52. package/dist/types/src/types/entity_views.d.ts +8 -8
  53. package/dist/types/src/types/export_import.d.ts +3 -3
  54. package/dist/types/src/types/index.d.ts +1 -0
  55. package/dist/types/src/types/plugins.d.ts +72 -18
  56. package/dist/types/src/types/properties.d.ts +118 -33
  57. package/dist/types/src/types/relations.d.ts +1 -1
  58. package/dist/types/src/types/slots.d.ts +30 -6
  59. package/dist/types/src/types/translations.d.ts +44 -0
  60. package/dist/types/src/types/user_management_delegate.d.ts +1 -0
  61. package/drizzle-test/0000_woozy_junta.sql +6 -0
  62. package/drizzle-test/0001_youthful_arachne.sql +1 -0
  63. package/drizzle-test/0002_lively_dragon_lord.sql +2 -0
  64. package/drizzle-test/0003_mean_king_cobra.sql +2 -0
  65. package/drizzle-test/meta/0000_snapshot.json +47 -0
  66. package/drizzle-test/meta/0001_snapshot.json +48 -0
  67. package/drizzle-test/meta/0002_snapshot.json +38 -0
  68. package/drizzle-test/meta/0003_snapshot.json +48 -0
  69. package/drizzle-test/meta/_journal.json +34 -0
  70. package/drizzle-test-out/0000_tan_trauma.sql +6 -0
  71. package/drizzle-test-out/0001_rapid_drax.sql +1 -0
  72. package/drizzle-test-out/meta/0000_snapshot.json +44 -0
  73. package/drizzle-test-out/meta/0001_snapshot.json +54 -0
  74. package/drizzle-test-out/meta/_journal.json +20 -0
  75. package/drizzle.test.config.ts +10 -0
  76. package/package.json +88 -89
  77. package/scratch.ts +41 -0
  78. package/src/PostgresBackendDriver.ts +63 -79
  79. package/src/PostgresBootstrapper.ts +7 -8
  80. package/src/auth/ensure-tables.ts +158 -86
  81. package/src/auth/services.ts +109 -50
  82. package/src/cli.ts +259 -16
  83. package/src/collections/PostgresCollectionRegistry.ts +6 -6
  84. package/src/connection.ts +70 -48
  85. package/src/data-transformer.ts +155 -116
  86. package/src/databasePoolManager.ts +6 -5
  87. package/src/history/HistoryService.ts +3 -12
  88. package/src/interfaces.ts +3 -3
  89. package/src/schema/auth-schema.ts +26 -3
  90. package/src/schema/doctor-cli.ts +47 -0
  91. package/src/schema/doctor.ts +595 -0
  92. package/src/schema/generate-drizzle-schema-logic.ts +204 -57
  93. package/src/schema/generate-drizzle-schema.ts +6 -6
  94. package/src/schema/test-schema.ts +11 -0
  95. package/src/services/BranchService.ts +5 -5
  96. package/src/services/EntityFetchService.ts +317 -188
  97. package/src/services/EntityPersistService.ts +15 -17
  98. package/src/services/RelationService.ts +299 -37
  99. package/src/services/entity-helpers.ts +39 -13
  100. package/src/services/entityService.ts +11 -9
  101. package/src/services/realtimeService.ts +58 -29
  102. package/src/utils/drizzle-conditions.ts +25 -24
  103. package/src/websocket.ts +52 -21
  104. package/test/auth-services.test.ts +131 -39
  105. package/test/batch-many-to-many-regression.test.ts +573 -0
  106. package/test/branchService.test.ts +22 -12
  107. package/test/data-transformer-hardening.test.ts +417 -0
  108. package/test/data-transformer.test.ts +175 -0
  109. package/test/doctor.test.ts +182 -0
  110. package/test/entityService.errors.test.ts +31 -16
  111. package/test/entityService.relations.test.ts +155 -59
  112. package/test/entityService.subcollection-search.test.ts +107 -57
  113. package/test/entityService.test.ts +105 -47
  114. package/test/generate-drizzle-schema.test.ts +262 -69
  115. package/test/historyService.test.ts +31 -16
  116. package/test/n-plus-one-regression.test.ts +314 -0
  117. package/test/postgresDataDriver.test.ts +260 -168
  118. package/test/realtimeService.test.ts +70 -39
  119. package/test/relation-pipeline-gaps.test.ts +637 -0
  120. package/test/relations.test.ts +492 -39
  121. package/test-drizzle-bug.ts +18 -0
  122. package/test-drizzle-out/0000_cultured_freak.sql +7 -0
  123. package/test-drizzle-out/0001_tiresome_professor_monster.sql +1 -0
  124. package/test-drizzle-out/meta/0000_snapshot.json +55 -0
  125. package/test-drizzle-out/meta/0001_snapshot.json +63 -0
  126. package/test-drizzle-out/meta/_journal.json +20 -0
  127. package/test-drizzle-prompt.sh +2 -0
  128. package/test-policy-prompt.sh +3 -0
  129. package/test-programmatic.ts +30 -0
  130. package/test-programmatic2.ts +59 -0
  131. package/test-schema-no-policies.ts +12 -0
  132. package/test_drizzle_mock.js +2 -2
  133. package/test_find_changed.mjs +3 -1
  134. package/test_hash.js +14 -0
  135. package/tsconfig.json +1 -1
  136. package/vite.config.ts +5 -5
@@ -20,9 +20,10 @@ describe("generateDrizzleSchema", () => {
20
20
  table: "products",
21
21
  name: "Products",
22
22
  properties: {
23
- name: { type: "string", validation: { required: true } },
23
+ name: { type: "string",
24
+ validation: { required: true } },
24
25
  price: { type: "number" },
25
- available: { type: "boolean" },
26
+ available: { type: "boolean" }
26
27
  }
27
28
  }
28
29
  ];
@@ -52,7 +53,8 @@ describe("generateDrizzleSchema", () => {
52
53
  name: "Posts",
53
54
  properties: {
54
55
  title: { type: "string" },
55
- author: { type: "relation", relationName: "author" }
56
+ author: { type: "relation",
57
+ relationName: "author" }
56
58
  },
57
59
  relations: [
58
60
  {
@@ -69,7 +71,7 @@ describe("generateDrizzleSchema", () => {
69
71
  const cleanResult = cleanSchema(result);
70
72
 
71
73
  expect(cleanResult).toContain("author_id: varchar(\"author_id\").references(() => users.id, { onDelete: \"set null\" })");
72
- const expectedRelation = `export const postsRelations = drizzleRelations(posts, ({ one, many }) => ({ author: one(users, { fields: [posts.author_id], references: [users.id], relationName: \"author\" }) }));`;
74
+ const expectedRelation = "export const postsRelations = drizzleRelations(posts, ({ one, many }) => ({ \"author\": one(users, { fields: [posts.author_id], references: [users.id], relationName: \"posts_author_id\" }) }));";
73
75
  expect(cleanResult).toContain(cleanSchema(expectedRelation));
74
76
  });
75
77
 
@@ -80,7 +82,8 @@ describe("generateDrizzleSchema", () => {
80
82
  name: "Posts",
81
83
  properties: {
82
84
  title: { type: "string" },
83
- tags: { type: "relation", relationName: "tags" }
85
+ tags: { type: "relation",
86
+ relationName: "tags" }
84
87
  },
85
88
  relations: [
86
89
  {
@@ -112,15 +115,18 @@ describe("generateDrizzleSchema", () => {
112
115
  expect(cleanResult).toContain("post_id: varchar(\"post_id\").notNull().references(() => posts.id, { onDelete: \"cascade\" })");
113
116
  expect(cleanResult).toContain("tag_id: varchar(\"tag_id\").notNull().references(() => tags.id, { onDelete: \"cascade\" })");
114
117
  expect(cleanResult).toContain("(table) => ({ pk: primaryKey({ columns: [table.post_id, table.tag_id] }) })");
115
- expect(cleanResult).toContain("export const postsRelations = drizzleRelations(posts, ({ one, many }) => ({ tags: many(postsToTags, { relationName: \"tags\" }) }));");
118
+ expect(cleanResult).toContain("export const postsRelations = drizzleRelations(posts, ({ one, many }) => ({ \"tags\": many(postsToTags, { relationName: \"tags\" }) }));");
116
119
  });
117
120
 
118
121
  describe("generateDrizzleSchema Column Types", () => {
119
-
122
+
120
123
  describe("String Property", () => {
121
124
  it("should default to varchar if no columnType or isId is specified", async () => {
122
125
  const collections: EntityCollection[] = [{
123
- slug: "texts", table: "texts", name: "Texts", properties: { t_default: { type: "string" } }
126
+ slug: "texts",
127
+ table: "texts",
128
+ name: "Texts",
129
+ properties: { t_default: { type: "string" } }
124
130
  }];
125
131
  const result = await generateSchema(collections);
126
132
  expect(cleanSchema(result)).toContain("t_default: varchar(\"t_default\")");
@@ -132,9 +138,12 @@ describe("generateDrizzleSchema", () => {
132
138
  table: "texts",
133
139
  name: "Texts",
134
140
  properties: {
135
- t_text: { type: "string", columnType: "text" },
136
- t_char: { type: "string", columnType: "char" },
137
- t_varchar: { type: "string", columnType: "varchar" },
141
+ t_text: { type: "string",
142
+ columnType: "text" },
143
+ t_char: { type: "string",
144
+ columnType: "char" },
145
+ t_varchar: { type: "string",
146
+ columnType: "varchar" }
138
147
  }
139
148
  }];
140
149
  const result = await generateSchema(collections);
@@ -147,23 +156,37 @@ describe("generateDrizzleSchema", () => {
147
156
 
148
157
  it("should prioritize isId='uuid' over default varchar", async () => {
149
158
  const collections: EntityCollection[] = [{
150
- slug: "texts", table: "texts", name: "Texts", properties: { t_uuid: { type: "string", isId: "uuid" } }
159
+ slug: "texts",
160
+ table: "texts",
161
+ name: "Texts",
162
+ properties: { t_uuid: { type: "string",
163
+ isId: "uuid" } }
151
164
  }];
152
165
  const result = await generateSchema(collections);
153
166
  expect(cleanSchema(result)).toContain("t_uuid: uuid(\"t_uuid\").primaryKey().defaultRandom()");
154
167
  });
155
-
168
+
156
169
  it("should combine isId=true with columnType overrides", async () => {
157
170
  const collections: EntityCollection[] = [{
158
- slug: "texts", table: "texts", name: "Texts", properties: { t_id: { type: "string", isId: true, columnType: "text" } }
171
+ slug: "texts",
172
+ table: "texts",
173
+ name: "Texts",
174
+ properties: { t_id: { type: "string",
175
+ isId: true,
176
+ columnType: "text" } }
159
177
  }];
160
178
  const result = await generateSchema(collections);
161
179
  expect(cleanSchema(result)).toContain("t_id: text(\"t_id\").primaryKey()");
162
180
  });
163
-
181
+
164
182
  it("should respect validation.unique along with columnType", async () => {
165
183
  const collections: EntityCollection[] = [{
166
- slug: "texts", table: "texts", name: "Texts", properties: { t_unique: { type: "string", columnType: "char", validation: { unique: true } } }
184
+ slug: "texts",
185
+ table: "texts",
186
+ name: "Texts",
187
+ properties: { t_unique: { type: "string",
188
+ columnType: "char",
189
+ validation: { unique: true } } }
167
190
  }];
168
191
  const result = await generateSchema(collections);
169
192
  expect(cleanSchema(result)).toContain("t_unique: char(\"t_unique\").unique()");
@@ -173,7 +196,10 @@ describe("generateDrizzleSchema", () => {
173
196
  describe("Number Property", () => {
174
197
  it("should default to numeric for normal numbers", async () => {
175
198
  const collections: EntityCollection[] = [{
176
- slug: "nums", table: "nums", name: "Nums", properties: { n_def: { type: "number" } }
199
+ slug: "nums",
200
+ table: "nums",
201
+ name: "Nums",
202
+ properties: { n_def: { type: "number" } }
177
203
  }];
178
204
  const result = await generateSchema(collections);
179
205
  expect(cleanSchema(result)).toContain("n_def: numeric(\"n_def\")");
@@ -181,7 +207,11 @@ describe("generateDrizzleSchema", () => {
181
207
 
182
208
  it("should default to integer if validation.integer is true", async () => {
183
209
  const collections: EntityCollection[] = [{
184
- slug: "nums", table: "nums", name: "Nums", properties: { n_int: { type: "number", validation: { integer: true } } }
210
+ slug: "nums",
211
+ table: "nums",
212
+ name: "Nums",
213
+ properties: { n_int: { type: "number",
214
+ validation: { integer: true } } }
185
215
  }];
186
216
  const result = await generateSchema(collections);
187
217
  expect(cleanSchema(result)).toContain("n_int: integer(\"n_int\")");
@@ -189,7 +219,11 @@ describe("generateDrizzleSchema", () => {
189
219
 
190
220
  it("should default to integer if isId is true", async () => {
191
221
  const collections: EntityCollection[] = [{
192
- slug: "nums", table: "nums", name: "Nums", properties: { n_id: { type: "number", isId: true } }
222
+ slug: "nums",
223
+ table: "nums",
224
+ name: "Nums",
225
+ properties: { n_id: { type: "number",
226
+ isId: true } }
193
227
  }];
194
228
  const result = await generateSchema(collections);
195
229
  expect(cleanSchema(result)).toContain("n_id: integer(\"n_id\").primaryKey()");
@@ -201,13 +235,20 @@ describe("generateDrizzleSchema", () => {
201
235
  table: "numbers",
202
236
  name: "Numbers",
203
237
  properties: {
204
- n_int: { type: "number", columnType: "integer" },
205
- n_real: { type: "number", columnType: "real" },
206
- n_dp: { type: "number", columnType: "double precision" },
207
- n_num: { type: "number", columnType: "numeric" },
208
- n_bigint: { type: "number", columnType: "bigint" },
209
- n_serial: { type: "number", columnType: "serial" },
210
- n_bigserial: { type: "number", columnType: "bigserial" },
238
+ n_int: { type: "number",
239
+ columnType: "integer" },
240
+ n_real: { type: "number",
241
+ columnType: "real" },
242
+ n_dp: { type: "number",
243
+ columnType: "double precision" },
244
+ n_num: { type: "number",
245
+ columnType: "numeric" },
246
+ n_bigint: { type: "number",
247
+ columnType: "bigint" },
248
+ n_serial: { type: "number",
249
+ columnType: "serial" },
250
+ n_bigserial: { type: "number",
251
+ columnType: "bigserial" }
211
252
  }
212
253
  }];
213
254
  const result = await generateSchema(collections);
@@ -224,15 +265,25 @@ describe("generateDrizzleSchema", () => {
224
265
 
225
266
  it("should combine isId='increment' with columnType overrides safely", async () => {
226
267
  const collections: EntityCollection[] = [{
227
- slug: "nums", table: "nums", name: "Nums", properties: { n_inc: { type: "number", isId: "increment", columnType: "bigint" } }
268
+ slug: "nums",
269
+ table: "nums",
270
+ name: "Nums",
271
+ properties: { n_inc: { type: "number",
272
+ isId: "increment",
273
+ columnType: "bigint" } }
228
274
  }];
229
275
  const result = await generateSchema(collections);
230
276
  expect(cleanSchema(result)).toContain("n_inc: bigint(\"n_inc\").generatedByDefaultAsIdentity().primaryKey()");
231
277
  });
232
-
278
+
233
279
  it("should combine validation.unique with columnType override", async () => {
234
280
  const collections: EntityCollection[] = [{
235
- slug: "nums", table: "nums", name: "Nums", properties: { n_uniq: { type: "number", columnType: "real", validation: { unique: true } } }
281
+ slug: "nums",
282
+ table: "nums",
283
+ name: "Nums",
284
+ properties: { n_uniq: { type: "number",
285
+ columnType: "real",
286
+ validation: { unique: true } } }
236
287
  }];
237
288
  const result = await generateSchema(collections);
238
289
  expect(cleanSchema(result)).toContain("n_uniq: real(\"n_uniq\").unique()");
@@ -242,7 +293,10 @@ describe("generateDrizzleSchema", () => {
242
293
  describe("Date Property", () => {
243
294
  it("should default to timestamp with timezone", async () => {
244
295
  const collections: EntityCollection[] = [{
245
- slug: "dates", table: "dates", name: "Dates", properties: { d_def: { type: "date" } }
296
+ slug: "dates",
297
+ table: "dates",
298
+ name: "Dates",
299
+ properties: { d_def: { type: "date" } }
246
300
  }];
247
301
  const result = await generateSchema(collections);
248
302
  expect(cleanSchema(result)).toContain("d_def: timestamp(\"d_def\", { withTimezone: true, mode: 'string' })");
@@ -254,9 +308,12 @@ describe("generateDrizzleSchema", () => {
254
308
  table: "dates",
255
309
  name: "Dates",
256
310
  properties: {
257
- d_date: { type: "date", columnType: "date" },
258
- d_time: { type: "date", columnType: "time" },
259
- d_ts: { type: "date", columnType: "timestamp" },
311
+ d_date: { type: "date",
312
+ columnType: "date" },
313
+ d_time: { type: "date",
314
+ columnType: "time" },
315
+ d_ts: { type: "date",
316
+ columnType: "timestamp" }
260
317
  }
261
318
  }];
262
319
  const result = await generateSchema(collections);
@@ -267,11 +324,14 @@ describe("generateDrizzleSchema", () => {
267
324
  expect(cleanResult).toContain("d_ts: timestamp(\"d_ts\", { withTimezone: true, mode: 'string' }),");
268
325
  });
269
326
  });
270
-
327
+
271
328
  describe("Map & Array Properties", () => {
272
329
  it("should default to jsonb", async () => {
273
330
  const collections: EntityCollection[] = [{
274
- slug: "json_data", table: "json_data", name: "JSON Data", properties: {
331
+ slug: "json_data",
332
+ table: "json_data",
333
+ name: "JSON Data",
334
+ properties: {
275
335
  arr_def: { type: "array" },
276
336
  map_def: { type: "map" }
277
337
  }
@@ -287,10 +347,14 @@ describe("generateDrizzleSchema", () => {
287
347
  table: "json_data",
288
348
  name: "JSON Data",
289
349
  properties: {
290
- a_json: { type: "array", columnType: "json" },
291
- a_jsonb: { type: "array", columnType: "jsonb" },
292
- m_json: { type: "map", columnType: "json" },
293
- m_jsonb: { type: "map", columnType: "jsonb" },
350
+ a_json: { type: "array",
351
+ columnType: "json" },
352
+ a_jsonb: { type: "array",
353
+ columnType: "jsonb" },
354
+ m_json: { type: "map",
355
+ columnType: "json" },
356
+ m_jsonb: { type: "map",
357
+ columnType: "jsonb" }
294
358
  }
295
359
  }];
296
360
  const result = await generateSchema(collections);
@@ -314,10 +378,12 @@ describe("generateDrizzleSchema", () => {
314
378
  name: "Notes",
315
379
  properties: {
316
380
  title: { type: "string" },
317
- user_id: { type: "string", validation: { required: true } }
381
+ user_id: { type: "string",
382
+ validation: { required: true } }
318
383
  },
319
384
  securityRules: [
320
- { operation: "all", ownerField: "user_id" }
385
+ { operation: "all",
386
+ ownerField: "user_id" }
321
387
  ]
322
388
  }];
323
389
 
@@ -341,7 +407,8 @@ describe("generateDrizzleSchema", () => {
341
407
  user_id: { type: "string" }
342
408
  },
343
409
  securityRules: [
344
- { operation: "select", ownerField: "user_id" }
410
+ { operation: "select",
411
+ ownerField: "user_id" }
345
412
  ]
346
413
  }];
347
414
 
@@ -361,7 +428,8 @@ describe("generateDrizzleSchema", () => {
361
428
  user_id: { type: "string" }
362
429
  },
363
430
  securityRules: [
364
- { operation: "insert", ownerField: "user_id" }
431
+ { operation: "insert",
432
+ ownerField: "user_id" }
365
433
  ]
366
434
  }];
367
435
 
@@ -378,7 +446,8 @@ describe("generateDrizzleSchema", () => {
378
446
  name: "Articles",
379
447
  properties: { title: { type: "string" } },
380
448
  securityRules: [
381
- { operation: "select", access: "public" }
449
+ { operation: "select",
450
+ access: "public" }
382
451
  ]
383
452
  }];
384
453
 
@@ -394,7 +463,8 @@ describe("generateDrizzleSchema", () => {
394
463
  name: "Admin Data",
395
464
  properties: { data: { type: "string" } },
396
465
  securityRules: [
397
- { operation: "select", roles: ["admin"] }
466
+ { operation: "select",
467
+ roles: ["admin"] }
398
468
  ]
399
469
  }];
400
470
 
@@ -409,7 +479,8 @@ describe("generateDrizzleSchema", () => {
409
479
  name: "Finance Data",
410
480
  properties: { amount: { type: "number" } },
411
481
  securityRules: [
412
- { operation: "select", roles: ["view"] } // Should not match 'viewer'
482
+ { operation: "select",
483
+ roles: ["view"] } // Should not match 'viewer'
413
484
  ]
414
485
  }];
415
486
 
@@ -426,7 +497,8 @@ describe("generateDrizzleSchema", () => {
426
497
  name: "Multi Role",
427
498
  properties: { data: { type: "string" } },
428
499
  securityRules: [
429
- { operation: "select", roles: ["admin", "editor", "super-admin"] }
500
+ { operation: "select",
501
+ roles: ["admin", "editor", "super-admin"] }
430
502
  ]
431
503
  }];
432
504
 
@@ -441,7 +513,9 @@ describe("generateDrizzleSchema", () => {
441
513
  name: "Reports",
442
514
  properties: { title: { type: "string" } },
443
515
  securityRules: [
444
- { operation: "select", roles: ["admin", "manager"], access: "public" }
516
+ { operation: "select",
517
+ roles: ["admin", "manager"],
518
+ access: "public" }
445
519
  ]
446
520
  }];
447
521
 
@@ -460,7 +534,9 @@ describe("generateDrizzleSchema", () => {
460
534
  user_id: { type: "string" }
461
535
  },
462
536
  securityRules: [
463
- { operation: "select", roles: ["editor"], ownerField: "user_id" }
537
+ { operation: "select",
538
+ roles: ["editor"],
539
+ ownerField: "user_id" }
464
540
  ]
465
541
  }];
466
542
 
@@ -481,7 +557,9 @@ describe("generateDrizzleSchema", () => {
481
557
  is_locked: { type: "boolean" }
482
558
  },
483
559
  securityRules: [
484
- { operation: "update", mode: "restrictive", using: "{is_locked} = false" }
560
+ { operation: "update",
561
+ mode: "restrictive",
562
+ using: "{is_locked} = false" }
485
563
  ]
486
564
  }];
487
565
 
@@ -500,7 +578,8 @@ describe("generateDrizzleSchema", () => {
500
578
  published_at: { type: "date" }
501
579
  },
502
580
  securityRules: [
503
- { operation: "select", using: "{published_at} > now() - interval '30 days'" }
581
+ { operation: "select",
582
+ using: "{published_at} > now() - interval '30 days'" }
504
583
  ]
505
584
  }];
506
585
 
@@ -541,7 +620,9 @@ describe("generateDrizzleSchema", () => {
541
620
  name: "Data",
542
621
  properties: { value: { type: "string" } },
543
622
  securityRules: [
544
- { name: "my_custom_policy", operation: "select", access: "public" }
623
+ { name: "my_custom_policy",
624
+ operation: "select",
625
+ access: "public" }
545
626
  ]
546
627
  }];
547
628
 
@@ -560,10 +641,20 @@ describe("generateDrizzleSchema", () => {
560
641
  is_locked: { type: "boolean" }
561
642
  },
562
643
  securityRules: [
563
- { name: "admin_read", operation: "select", roles: ["admin"], access: "public" },
564
- { name: "owner_read", operation: "select", ownerField: "user_id" },
565
- { name: "owner_write", operation: "insert", ownerField: "user_id" },
566
- { name: "no_locked_update", operation: "update", mode: "restrictive", using: "{is_locked} = false" }
644
+ { name: "admin_read",
645
+ operation: "select",
646
+ roles: ["admin"],
647
+ access: "public" },
648
+ { name: "owner_read",
649
+ operation: "select",
650
+ ownerField: "user_id" },
651
+ { name: "owner_write",
652
+ operation: "insert",
653
+ ownerField: "user_id" },
654
+ { name: "no_locked_update",
655
+ operation: "update",
656
+ mode: "restrictive",
657
+ using: "{is_locked} = false" }
567
658
  ]
568
659
  }];
569
660
 
@@ -586,7 +677,8 @@ describe("generateDrizzleSchema V2 improvements", () => {
586
677
  name: "Admin Data",
587
678
  properties: { data: { type: "string" } },
588
679
  securityRules: [
589
- { operation: "select", roles: ["admin"] }
680
+ { operation: "select",
681
+ roles: ["admin"] }
590
682
  ]
591
683
  }];
592
684
  const result = await generateSchema(collections);
@@ -602,7 +694,9 @@ describe("generateDrizzleSchema V2 improvements", () => {
602
694
  user_id: { type: "string" }
603
695
  },
604
696
  securityRules: [
605
- { name: "owner_rw", operations: ["select", "update"], ownerField: "user_id" }
697
+ { name: "owner_rw",
698
+ operations: ["select", "update"],
699
+ ownerField: "user_id" }
606
700
  ]
607
701
  }];
608
702
  const result = await generateSchema(collections);
@@ -619,7 +713,8 @@ describe("generateDrizzleSchema V2 improvements", () => {
619
713
  name: "Items",
620
714
  properties: { data: { type: "string" } },
621
715
  securityRules: [
622
- { operations: ["select", "delete"], access: "public" }
716
+ { operations: ["select", "delete"],
717
+ access: "public" }
623
718
  ]
624
719
  }];
625
720
  const result = await generateSchema(collections);
@@ -633,7 +728,9 @@ describe("generateDrizzleSchema V2 improvements", () => {
633
728
  name: "Items",
634
729
  properties: { data: { type: "string" } },
635
730
  securityRules: [
636
- { name: "my_policy", operations: ["select"], access: "public" }
731
+ { name: "my_policy",
732
+ operations: ["select"],
733
+ access: "public" }
637
734
  ]
638
735
  }];
639
736
  const result = await generateSchema(collections);
@@ -651,7 +748,9 @@ describe("generateDrizzleSchema V2 improvements", () => {
651
748
  user_id: { type: "string" }
652
749
  },
653
750
  securityRules: [
654
- { name: "owner", operations: ["select", "insert"], ownerField: "user_id" }
751
+ { name: "owner",
752
+ operations: ["select", "insert"],
753
+ ownerField: "user_id" }
655
754
  ]
656
755
  }];
657
756
  const result = await generateSchema(collections);
@@ -671,7 +770,10 @@ describe("generateDrizzleSchema V2 improvements", () => {
671
770
  name: "Items",
672
771
  properties: { data: { type: "string" } },
673
772
  securityRules: [
674
- { name: "test", operation: "delete", operations: ["select", "insert"], access: "public" }
773
+ { name: "test",
774
+ operation: "delete",
775
+ operations: ["select", "insert"],
776
+ access: "public" }
675
777
  ]
676
778
  }];
677
779
  const result = await generateSchema(collections);
@@ -687,7 +789,9 @@ describe("generateDrizzleSchema V2 improvements", () => {
687
789
  name: "Reports",
688
790
  properties: { title: { type: "string" } },
689
791
  securityRules: [
690
- { operation: "select", roles: ["admin"], using: "true" }
792
+ { operation: "select",
793
+ roles: ["admin"],
794
+ using: "true" }
691
795
  ]
692
796
  }];
693
797
  const result = await generateSchema(collections);
@@ -701,7 +805,9 @@ describe("generateDrizzleSchema V2 improvements", () => {
701
805
  name: "Tenant Data",
702
806
  properties: { data: { type: "string" } },
703
807
  securityRules: [
704
- { operation: "select", access: "public", pgRoles: ["app_role", "service_role"] }
808
+ { operation: "select",
809
+ access: "public",
810
+ pgRoles: ["app_role", "service_role"] }
705
811
  ]
706
812
  }];
707
813
  const result = await generateSchema(collections);
@@ -715,7 +821,8 @@ describe("generateDrizzleSchema V2 improvements", () => {
715
821
  name: "Default Data",
716
822
  properties: { data: { type: "string" } },
717
823
  securityRules: [
718
- { operation: "select", access: "public" }
824
+ { operation: "select",
825
+ access: "public" }
719
826
  ]
720
827
  }];
721
828
  const result = await generateSchema(collections);
@@ -723,6 +830,88 @@ describe("generateDrizzleSchema V2 improvements", () => {
723
830
  });
724
831
  });
725
832
 
833
+ describe("generateDrizzleSchema Deterministic Policies", () => {
834
+ it("should generate the same policy name hash for identically configured rules", async () => {
835
+ const collections1: EntityCollection[] = [{
836
+ slug: "test1",
837
+ table: "test_hash",
838
+ name: "Test",
839
+ properties: { data: { type: "string" } },
840
+ securityRules: [
841
+ { operation: "select",
842
+ roles: ["admin", "user"] }
843
+ ]
844
+ }];
845
+
846
+ const collections2: EntityCollection[] = [{
847
+ slug: "test2",
848
+ table: "test_hash", // Same table name
849
+ name: "Test",
850
+ properties: { data: { type: "string" } },
851
+ securityRules: [
852
+ { operation: "select",
853
+ roles: ["admin", "user"] }
854
+ ]
855
+ }];
856
+
857
+ const result1 = await generateSchema(collections1);
858
+ const result2 = await generateSchema(collections2);
859
+
860
+ // Extract the generated policy name, which should look like pgPolicy("test_hash_select_<hash>"
861
+ const match1 = result1.match(/pgPolicy\("test_hash_select_([a-f0-9]{7})"/);
862
+ const match2 = result2.match(/pgPolicy\("test_hash_select_([a-f0-9]{7})"/);
863
+
864
+ expect(match1).not.toBeNull();
865
+ expect(match2).not.toBeNull();
866
+ expect(match1![1]).toEqual(match2![1]);
867
+ });
868
+
869
+ it("should generate the exact same SQL output regardless of array order in configuration", async () => {
870
+ const collectionsUnsorted: EntityCollection[] = [{
871
+ slug: "test_order",
872
+ table: "test_order",
873
+ name: "Test",
874
+ properties: { data: { type: "string" } },
875
+ securityRules: [
876
+ {
877
+ operation: "select",
878
+ roles: ["user", "admin", "manager"], // Unsorted roles
879
+ pgRoles: ["service_role", "app_role"] // Unsorted pgRoles
880
+ }
881
+ ]
882
+ }];
883
+
884
+ const collectionsSorted: EntityCollection[] = [{
885
+ slug: "test_order",
886
+ table: "test_order",
887
+ name: "Test",
888
+ properties: { data: { type: "string" } },
889
+ securityRules: [
890
+ {
891
+ operation: "select",
892
+ roles: ["admin", "manager", "user"], // Sorted roles
893
+ pgRoles: ["app_role", "service_role"] // Sorted pgRoles
894
+ }
895
+ ]
896
+ }];
897
+
898
+ const resultUnsorted = await generateSchema(collectionsUnsorted);
899
+ const resultSorted = await generateSchema(collectionsSorted);
900
+
901
+ // The policy names should match because the configuration arrays should be sorted before hashing
902
+ const matchUnsorted = resultUnsorted.match(/pgPolicy\("test_order_select_([a-f0-9]{7})"/);
903
+ const matchSorted = resultSorted.match(/pgPolicy\("test_order_select_([a-f0-9]{7})"/);
904
+
905
+ expect(matchUnsorted).not.toBeNull();
906
+ expect(matchSorted).not.toBeNull();
907
+ expect(matchUnsorted![1]).toEqual(matchSorted![1]);
908
+
909
+ // The actual generated sql bodies should also be identical due to sorting
910
+ expect(resultUnsorted).toContain("string_to_array(auth.roles(), ',') @> ARRAY['admin','manager','user']");
911
+ expect(resultUnsorted).toContain('to: ["app_role", "service_role"]');
912
+ });
913
+ });
914
+
726
915
  describe("generateDrizzleSchema ID Generation", () => {
727
916
  const cleanSchema = (schema: string) => {
728
917
  return schema
@@ -739,7 +928,8 @@ describe("generateDrizzleSchema ID Generation", () => {
739
928
  table: "items",
740
929
  name: "Items",
741
930
  properties: {
742
- custom_id: { type: "string", isId: "uuid" }
931
+ custom_id: { type: "string",
932
+ isId: "uuid" }
743
933
  }
744
934
  }];
745
935
  const result = await generateSchema(collections);
@@ -754,7 +944,8 @@ describe("generateDrizzleSchema ID Generation", () => {
754
944
  table: "tickets",
755
945
  name: "Tickets",
756
946
  properties: {
757
- ticket_id: { type: "number", isId: "increment" }
947
+ ticket_id: { type: "number",
948
+ isId: "increment" }
758
949
  }
759
950
  }];
760
951
  const result = await generateSchema(collections);
@@ -769,7 +960,8 @@ describe("generateDrizzleSchema ID Generation", () => {
769
960
  table: "events",
770
961
  name: "Events",
771
962
  properties: {
772
- event_id: { type: "string", isId: "sql`gen_random_uuid()`" }
963
+ event_id: { type: "string",
964
+ isId: "sql`gen_random_uuid()`" }
773
965
  }
774
966
  }];
775
967
  const result = await generateSchema(collections);
@@ -784,7 +976,8 @@ describe("generateDrizzleSchema ID Generation", () => {
784
976
  table: "users",
785
977
  name: "Users",
786
978
  properties: {
787
- user_name: { type: "string", isId: true }
979
+ user_name: { type: "string",
980
+ isId: true }
788
981
  }
789
982
  }];
790
983
  const result = await generateSchema(collections);