@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
@@ -0,0 +1,417 @@
1
+ import {
2
+ serializeDataToServer,
3
+ parsePropertyFromServer,
4
+ normalizeDbValues,
5
+ sanitizeAndConvertDates
6
+ } from "../src/data-transformer";
7
+ import type { EntityCollection, Property, Properties, RelationProperty } from "@rebasepro/types";
8
+
9
+ // ─────────────────────────────────────────────────────────────
10
+ // Fixture helpers
11
+ // ─────────────────────────────────────────────────────────────
12
+ function makeCollection(
13
+ slug: string,
14
+ properties: Properties,
15
+ relations?: EntityCollection["relations"]
16
+ ): EntityCollection {
17
+ return {
18
+ name: slug,
19
+ slug,
20
+ path: slug,
21
+ collectionType: "postgres",
22
+ tableName: slug,
23
+ properties,
24
+ relations
25
+ } as unknown as EntityCollection;
26
+ }
27
+
28
+ // ─────────────────────────────────────────────────────────────
29
+ // serializeDataToServer — typed return (Issue #7 regression)
30
+ // ─────────────────────────────────────────────────────────────
31
+ describe("serializeDataToServer typed return", () => {
32
+ const properties: Properties = {
33
+ title: { type: "string", name: "Title" } as Property,
34
+ count: { type: "number", name: "Count" } as Property
35
+ };
36
+
37
+ it("returns a SerializedEntityData object with scalarData, not raw values", () => {
38
+ const result = serializeDataToServer(
39
+ { title: "Hello", count: 5 },
40
+ properties
41
+ );
42
+ expect(result).toHaveProperty("scalarData");
43
+ expect(result).toHaveProperty("inverseRelationUpdates");
44
+ expect(result).toHaveProperty("joinPathRelationUpdates");
45
+ });
46
+
47
+ it("does NOT include __inverseRelationUpdates on scalarData (dunder elimination)", () => {
48
+ const result = serializeDataToServer(
49
+ { title: "Test" },
50
+ properties
51
+ );
52
+ // The old pattern embedded __dunder properties on the result object
53
+ expect(result.scalarData).not.toHaveProperty("__inverseRelationUpdates");
54
+ expect(result.scalarData).not.toHaveProperty("__joinPathRelationUpdates");
55
+ });
56
+
57
+ it("returns empty arrays when no collection/registry is provided", () => {
58
+ const result = serializeDataToServer(
59
+ { title: "Test" },
60
+ properties
61
+ );
62
+ expect(result.inverseRelationUpdates).toEqual([]);
63
+ expect(result.joinPathRelationUpdates).toEqual([]);
64
+ });
65
+
66
+ it("passes scalar values through correctly", () => {
67
+ const result = serializeDataToServer(
68
+ { title: "Hello World", count: 42 },
69
+ properties
70
+ );
71
+ expect(result.scalarData.title).toBe("Hello World");
72
+ expect(result.scalarData.count).toBe(42);
73
+ });
74
+
75
+ it("handles null/undefined entity gracefully", () => {
76
+ const result = serializeDataToServer(null as any, properties);
77
+ // Object.entries(null) yields nothing → empty object
78
+ expect(result.scalarData).toEqual({});
79
+ expect(result.inverseRelationUpdates).toEqual([]);
80
+ expect(result.joinPathRelationUpdates).toEqual([]);
81
+ });
82
+ });
83
+
84
+ // ─────────────────────────────────────────────────────────────
85
+ // parsePropertyFromServer — relation factory (Issue #1 regression)
86
+ // ─────────────────────────────────────────────────────────────
87
+ describe("parsePropertyFromServer relation factory", () => {
88
+ const targetCollection = makeCollection("authors", {
89
+ name: { type: "string", name: "Name" } as Property
90
+ });
91
+
92
+ const collection = makeCollection("posts", {
93
+ author: {
94
+ type: "relation",
95
+ name: "Author",
96
+ relation: {
97
+ target: () => targetCollection,
98
+ cardinality: "one",
99
+ direction: "owning",
100
+ localKey: "author_id",
101
+ relationName: "author"
102
+ }
103
+ } as unknown as Property
104
+ });
105
+
106
+ it("produces a relation ref with __type 'relation' from a string FK value", () => {
107
+ const property = collection.properties.author as Property;
108
+ const result = parsePropertyFromServer("author-123", property, collection, "author") as Record<string, unknown>;
109
+ expect(result.__type).toBe("relation");
110
+ expect(result.id).toBe("author-123");
111
+ expect(result.path).toBe("authors");
112
+ });
113
+
114
+ it("produces a relation ref with __type 'relation' from a numeric FK value", () => {
115
+ const property = collection.properties.author as Property;
116
+ const result = parsePropertyFromServer(42, property, collection, "author") as Record<string, unknown>;
117
+ expect(result.__type).toBe("relation");
118
+ expect(result.id).toBe("42");
119
+ expect(result.path).toBe("authors");
120
+ });
121
+ });
122
+
123
+ // ─────────────────────────────────────────────────────────────
124
+ // normalizeDbValues — pipeline deduplication (Issue #5 regression)
125
+ // ─────────────────────────────────────────────────────────────
126
+ describe("normalizeDbValues", () => {
127
+ const collection = makeCollection("items", {
128
+ title: { type: "string", name: "Title" } as Property,
129
+ price: { type: "number", name: "Price" } as Property,
130
+ created_at: { type: "date", name: "Created" } as Property
131
+ });
132
+
133
+ it("coerces string numbers to actual numbers", () => {
134
+ const result = normalizeDbValues(
135
+ { title: "Widget", price: "19.99" } as any,
136
+ collection
137
+ );
138
+ expect(result.price).toBe(19.99);
139
+ });
140
+
141
+ it("converts Date objects to { __type: 'date', value: ISO } format", () => {
142
+ const date = new Date("2024-01-15T10:30:00Z");
143
+ const result = normalizeDbValues(
144
+ { title: "Widget", created_at: date } as any,
145
+ collection
146
+ );
147
+ expect(result.created_at).toEqual({
148
+ __type: "date",
149
+ value: "2024-01-15T10:30:00.000Z"
150
+ });
151
+ });
152
+
153
+ it("strips unknown database columns not in properties", () => {
154
+ const result = normalizeDbValues(
155
+ { title: "Widget", internal_counter: 999 } as any,
156
+ collection
157
+ );
158
+ expect(result).not.toHaveProperty("internal_counter");
159
+ expect(result.title).toBe("Widget");
160
+ });
161
+
162
+ it("returns data as-is when properties is empty", () => {
163
+ const empty = makeCollection("empty", {});
164
+ const data = { foo: "bar" };
165
+ const result = normalizeDbValues(data as any, empty);
166
+ expect(result).toEqual({});
167
+ });
168
+
169
+ it("handles null data gracefully", () => {
170
+ const result = normalizeDbValues(null as any, collection);
171
+ expect(result).toBeNull();
172
+ });
173
+
174
+ it("skips relation properties (they are hydrated by Drizzle)", () => {
175
+ const collectionWithRelation = makeCollection("orders", {
176
+ customer: {
177
+ type: "relation",
178
+ name: "Customer",
179
+ relation: {
180
+ target: () => collection,
181
+ cardinality: "one",
182
+ direction: "owning",
183
+ localKey: "customer_id",
184
+ relationName: "customer"
185
+ }
186
+ } as unknown as Property,
187
+ total: { type: "number", name: "Total" } as Property
188
+ });
189
+
190
+ const result = normalizeDbValues(
191
+ { customer: "some-id", total: 42 } as any,
192
+ collectionWithRelation
193
+ );
194
+ // Relation properties should be skipped
195
+ expect(result).not.toHaveProperty("customer");
196
+ expect(result.total).toBe(42);
197
+ });
198
+ });
199
+
200
+ // ─────────────────────────────────────────────────────────────
201
+ // sanitizeAndConvertDates — utility regression
202
+ // ─────────────────────────────────────────────────────────────
203
+ describe("sanitizeAndConvertDates", () => {
204
+ it("converts NaN number to null", () => {
205
+ expect(sanitizeAndConvertDates(NaN)).toBeNull();
206
+ });
207
+
208
+ it("converts 'NaN' string to null", () => {
209
+ expect(sanitizeAndConvertDates("NaN")).toBeNull();
210
+ });
211
+
212
+ it("converts Date to ISO string", () => {
213
+ const date = new Date("2024-01-01T00:00:00Z");
214
+ expect(sanitizeAndConvertDates(date)).toBe("2024-01-01T00:00:00.000Z");
215
+ });
216
+
217
+ it("converts ISO string to ISO string", () => {
218
+ const iso = "2024-01-01T00:00:00Z";
219
+ expect(sanitizeAndConvertDates(iso)).toBe("2024-01-01T00:00:00.000Z");
220
+ });
221
+
222
+ it("passes through non-date strings unchanged", () => {
223
+ expect(sanitizeAndConvertDates("hello")).toBe("hello");
224
+ });
225
+
226
+ it("recursively sanitizes arrays", () => {
227
+ const result = sanitizeAndConvertDates([NaN, "hello", null]);
228
+ expect(result).toEqual([null, "hello", null]);
229
+ });
230
+ });
231
+
232
+ // ─────────────────────────────────────────────────────────────
233
+ // getColumnMeta — type guard regression (Issue #6)
234
+ // ─────────────────────────────────────────────────────────────
235
+ import { getColumnMeta } from "../src/services/entity-helpers";
236
+
237
+ describe("getColumnMeta type guard", () => {
238
+ it("extracts columnType, dataType, primary from a well-formed column", () => {
239
+ const fakeCol = {
240
+ columnType: "PgVarchar",
241
+ dataType: "string",
242
+ primary: false
243
+ };
244
+ const meta = getColumnMeta(fakeCol as any);
245
+ expect(meta.columnType).toBe("PgVarchar");
246
+ expect(meta.dataType).toBe("string");
247
+ expect(meta.primary).toBe(false);
248
+ });
249
+
250
+ it("returns undefined for missing properties instead of crashing", () => {
251
+ const emptyCol = {};
252
+ const meta = getColumnMeta(emptyCol as any);
253
+ expect(meta.columnType).toBeUndefined();
254
+ expect(meta.dataType).toBeUndefined();
255
+ expect(meta.primary).toBeUndefined();
256
+ });
257
+
258
+ it("returns undefined for wrong-typed properties instead of passing them through", () => {
259
+ const badCol = {
260
+ columnType: 42, // should be string
261
+ dataType: true, // should be string
262
+ primary: "yes" // should be boolean
263
+ };
264
+ const meta = getColumnMeta(badCol as any);
265
+ expect(meta.columnType).toBeUndefined();
266
+ expect(meta.dataType).toBeUndefined();
267
+ expect(meta.primary).toBeUndefined();
268
+ });
269
+ });
270
+
271
+ // ─────────────────────────────────────────────────────────────
272
+ // FK column preservation (Issue #5) — normalizeScalarValues
273
+ // ─────────────────────────────────────────────────────────────
274
+ describe("normalizeDbValues FK column preservation", () => {
275
+ it("preserves internal FK columns as primitives when not defined as properties", () => {
276
+ const targetCollection = makeCollection("categories", {
277
+ name: { type: "string", name: "Name" } as Property
278
+ });
279
+
280
+ const categoryRelation = {
281
+ target: () => targetCollection,
282
+ cardinality: "one" as const,
283
+ direction: "owning" as const,
284
+ localKey: "category_id",
285
+ relationName: "category"
286
+ };
287
+
288
+ const collection = makeCollection("products", {
289
+ title: { type: "string", name: "Title" } as Property,
290
+ category: {
291
+ type: "relation",
292
+ name: "Category",
293
+ relationName: "category"
294
+ } as unknown as Property
295
+ }, [categoryRelation] as any);
296
+
297
+ const result = normalizeDbValues(
298
+ { title: "Widget", category_id: "cat-123", category: "ignored" } as any,
299
+ collection
300
+ );
301
+
302
+ // FK column should be preserved as a primitive string
303
+ expect(result.category_id).toBe("cat-123");
304
+ // Relation property should be skipped (db.query handles it)
305
+ expect(result).not.toHaveProperty("category");
306
+ // Regular property should be present
307
+ expect(result.title).toBe("Widget");
308
+ });
309
+
310
+ it("preserves numeric FK columns as numbers", () => {
311
+ const targetCollection = makeCollection("authors", {
312
+ name: { type: "string", name: "Name" } as Property
313
+ });
314
+
315
+ const authorRelation = {
316
+ target: () => targetCollection,
317
+ cardinality: "one" as const,
318
+ direction: "owning" as const,
319
+ localKey: "author_id",
320
+ relationName: "author"
321
+ };
322
+
323
+ const collection = makeCollection("books", {
324
+ title: { type: "string", name: "Title" } as Property,
325
+ author: {
326
+ type: "relation",
327
+ name: "Author",
328
+ relationName: "author"
329
+ } as unknown as Property
330
+ }, [authorRelation] as any);
331
+
332
+ const result = normalizeDbValues(
333
+ { title: "Book", author_id: 42 } as any,
334
+ collection
335
+ );
336
+ expect(result.author_id).toBe(42);
337
+ });
338
+
339
+ it("converts null FK columns to null", () => {
340
+ const targetCollection = makeCollection("authors", {
341
+ name: { type: "string", name: "Name" } as Property
342
+ });
343
+
344
+ const authorRelation = {
345
+ target: () => targetCollection,
346
+ cardinality: "one" as const,
347
+ direction: "owning" as const,
348
+ localKey: "author_id",
349
+ relationName: "author"
350
+ };
351
+
352
+ const collection = makeCollection("books", {
353
+ title: { type: "string", name: "Title" } as Property,
354
+ author: {
355
+ type: "relation",
356
+ name: "Author",
357
+ relationName: "author"
358
+ } as unknown as Property
359
+ }, [authorRelation] as any);
360
+
361
+ const result = normalizeDbValues(
362
+ { title: "Book", author_id: null } as any,
363
+ collection
364
+ );
365
+ expect(result.author_id).toBeNull();
366
+ });
367
+ });
368
+
369
+ // ─────────────────────────────────────────────────────────────
370
+ // Structural dunder guard (Issue #7) — prevent re-introduction
371
+ // ─────────────────────────────────────────────────────────────
372
+ describe("structural dunder guard", () => {
373
+ it("scalarData never contains any __ prefixed keys", () => {
374
+ const properties: Properties = {
375
+ title: { type: "string", name: "Title" } as Property,
376
+ count: { type: "number", name: "Count" } as Property,
377
+ active: { type: "boolean", name: "Active" } as Property
378
+ };
379
+
380
+ const result = serializeDataToServer(
381
+ { title: "Test", count: 10, active: true },
382
+ properties
383
+ );
384
+
385
+ const dunderKeys = Object.keys(result.scalarData).filter(k => k.startsWith("__"));
386
+ expect(dunderKeys).toEqual([]);
387
+ });
388
+
389
+ it("scalarData never contains any __ prefixed keys even with relation properties", () => {
390
+ const targetCollection = makeCollection("tags", {
391
+ label: { type: "string", name: "Label" } as Property
392
+ });
393
+
394
+ const properties: Properties = {
395
+ title: { type: "string", name: "Title" } as Property,
396
+ tag: {
397
+ type: "relation",
398
+ name: "Tag",
399
+ relation: {
400
+ target: () => targetCollection,
401
+ cardinality: "one",
402
+ direction: "owning",
403
+ localKey: "tag_id",
404
+ relationName: "tag"
405
+ }
406
+ } as unknown as Property
407
+ };
408
+
409
+ const result = serializeDataToServer(
410
+ { title: "Test", tag: { id: "t1", path: "tags", __type: "relation" } },
411
+ properties
412
+ );
413
+
414
+ const dunderKeys = Object.keys(result.scalarData).filter(k => k.startsWith("__"));
415
+ expect(dunderKeys).toEqual([]);
416
+ });
417
+ });
@@ -0,0 +1,175 @@
1
+ import { serializePropertyToServer } from "../src/data-transformer";
2
+ import type { Property } from "@rebasepro/types";
3
+
4
+ describe("serializePropertyToServer", () => {
5
+ // ── Relation property ──
6
+ describe("relation property", () => {
7
+ const relationProp: Property = { type: "relation" } as Property;
8
+
9
+ it("should extract id from a relation object", () => {
10
+ const value = { id: "abc-123",
11
+ path: "authors",
12
+ __type: "relation" };
13
+ expect(serializePropertyToServer(value, relationProp)).toBe("abc-123");
14
+ });
15
+
16
+ it("should return null for empty string values", () => {
17
+ expect(serializePropertyToServer("", relationProp)).toBeNull();
18
+ });
19
+
20
+ it("should pass through a plain string ID", () => {
21
+ expect(serializePropertyToServer("uuid-string", relationProp)).toBe("uuid-string");
22
+ });
23
+
24
+ it("should pass through a numeric ID", () => {
25
+ expect(serializePropertyToServer(42, relationProp)).toBe(42);
26
+ });
27
+
28
+ it("should return null for null values", () => {
29
+ expect(serializePropertyToServer(null, relationProp)).toBeNull();
30
+ });
31
+
32
+ it("should return undefined for undefined values", () => {
33
+ expect(serializePropertyToServer(undefined, relationProp)).toBeUndefined();
34
+ });
35
+
36
+ it("should serialize an array of relation objects to array of IDs", () => {
37
+ const value = [
38
+ { id: "id-1",
39
+ path: "authors" },
40
+ { id: "id-2",
41
+ path: "authors" }
42
+ ];
43
+ expect(serializePropertyToServer(value, relationProp)).toEqual(["id-1", "id-2"]);
44
+ });
45
+
46
+ it("should convert empty strings within arrays to null", () => {
47
+ const value = ["", "some-uuid"];
48
+ expect(serializePropertyToServer(value, relationProp)).toEqual([null, "some-uuid"]);
49
+ });
50
+
51
+ it("should handle mixed array of objects and strings", () => {
52
+ const value = [
53
+ { id: "id-1",
54
+ path: "authors" },
55
+ "raw-id-2",
56
+ ""
57
+ ];
58
+ expect(serializePropertyToServer(value, relationProp)).toEqual(["id-1", "raw-id-2", null]);
59
+ });
60
+ });
61
+
62
+ // ── String property ──
63
+ describe("string property", () => {
64
+ const stringProp: Property = { type: "string" } as Property;
65
+
66
+ it("should pass through a regular string", () => {
67
+ expect(serializePropertyToServer("hello", stringProp)).toBe("hello");
68
+ });
69
+
70
+ it("should pass through empty string for non-relation types", () => {
71
+ expect(serializePropertyToServer("", stringProp)).toBe("");
72
+ });
73
+
74
+ it("should pass through null", () => {
75
+ expect(serializePropertyToServer(null, stringProp)).toBeNull();
76
+ });
77
+ });
78
+
79
+ // ── Number property ──
80
+ describe("number property", () => {
81
+ const numberProp: Property = { type: "number" } as Property;
82
+
83
+ it("should pass through numeric values", () => {
84
+ expect(serializePropertyToServer(42, numberProp)).toBe(42);
85
+ });
86
+
87
+ it("should pass through zero", () => {
88
+ expect(serializePropertyToServer(0, numberProp)).toBe(0);
89
+ });
90
+
91
+ it("should pass through null", () => {
92
+ expect(serializePropertyToServer(null, numberProp)).toBeNull();
93
+ });
94
+ });
95
+
96
+ // ── Map property ──
97
+ describe("map property", () => {
98
+ const mapProp: Property = {
99
+ type: "map",
100
+ properties: {
101
+ name: { type: "string" } as Property,
102
+ age: { type: "number" } as Property,
103
+ author: { type: "relation" } as Property
104
+ }
105
+ } as unknown as Property;
106
+
107
+ it("should recursively serialize nested properties", () => {
108
+ const value = {
109
+ name: "Alice",
110
+ age: 30,
111
+ author: { id: "author-1",
112
+ path: "authors" }
113
+ };
114
+ expect(serializePropertyToServer(value, mapProp)).toEqual({
115
+ name: "Alice",
116
+ age: 30,
117
+ author: "author-1"
118
+ });
119
+ });
120
+
121
+ it("should convert empty string relation in map to null", () => {
122
+ const value = {
123
+ name: "Test",
124
+ age: 25,
125
+ author: ""
126
+ };
127
+ expect(serializePropertyToServer(value, mapProp)).toEqual({
128
+ name: "Test",
129
+ age: 25,
130
+ author: null
131
+ });
132
+ });
133
+
134
+ it("should handle unknown sub-keys by passing through", () => {
135
+ const value = {
136
+ name: "Bob",
137
+ unknownField: "should pass through"
138
+ };
139
+ const result = serializePropertyToServer(value, mapProp) as Record<string, unknown>;
140
+ expect(result.name).toBe("Bob");
141
+ expect(result.unknownField).toBe("should pass through");
142
+ });
143
+ });
144
+
145
+ // ── Array property ──
146
+ describe("array property", () => {
147
+ const arrayOfStringsProp: Property = {
148
+ type: "array",
149
+ of: { type: "string" } as Property
150
+ } as unknown as Property;
151
+
152
+ it("should serialize array elements through their sub-property type", () => {
153
+ expect(serializePropertyToServer(["a", "b", "c"], arrayOfStringsProp))
154
+ .toEqual(["a", "b", "c"]);
155
+ });
156
+
157
+ it("should pass through non-array values", () => {
158
+ expect(serializePropertyToServer("not-an-array", arrayOfStringsProp))
159
+ .toBe("not-an-array");
160
+ });
161
+ });
162
+
163
+ // ── Boolean property ──
164
+ describe("boolean property", () => {
165
+ const boolProp: Property = { type: "boolean" } as Property;
166
+
167
+ it("should pass through true", () => {
168
+ expect(serializePropertyToServer(true, boolProp)).toBe(true);
169
+ });
170
+
171
+ it("should pass through false", () => {
172
+ expect(serializePropertyToServer(false, boolProp)).toBe(false);
173
+ });
174
+ });
175
+ });