@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.
- package/dist/common/src/collections/CollectionRegistry.d.ts +8 -0
- package/dist/common/src/util/entities.d.ts +22 -0
- package/dist/common/src/util/relations.d.ts +14 -4
- package/dist/common/src/util/resolutions.d.ts +1 -1
- package/dist/index.es.js +1254 -591
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +1254 -591
- package/dist/index.umd.js.map +1 -1
- package/dist/server-postgresql/src/PostgresBackendDriver.d.ts +17 -29
- package/dist/server-postgresql/src/auth/services.d.ts +7 -3
- package/dist/server-postgresql/src/collections/PostgresCollectionRegistry.d.ts +1 -1
- package/dist/server-postgresql/src/connection.d.ts +34 -1
- package/dist/server-postgresql/src/data-transformer.d.ts +26 -4
- package/dist/server-postgresql/src/databasePoolManager.d.ts +2 -2
- package/dist/server-postgresql/src/schema/auth-schema.d.ts +139 -38
- package/dist/server-postgresql/src/schema/doctor-cli.d.ts +2 -0
- package/dist/server-postgresql/src/schema/doctor.d.ts +43 -0
- package/dist/server-postgresql/src/schema/generate-drizzle-schema-logic.d.ts +1 -1
- package/dist/server-postgresql/src/schema/test-schema.d.ts +24 -0
- package/dist/server-postgresql/src/services/EntityFetchService.d.ts +22 -8
- package/dist/server-postgresql/src/services/EntityPersistService.d.ts +1 -1
- package/dist/server-postgresql/src/services/RelationService.d.ts +11 -5
- package/dist/server-postgresql/src/services/entity-helpers.d.ts +16 -2
- package/dist/server-postgresql/src/services/entityService.d.ts +8 -6
- package/dist/server-postgresql/src/services/realtimeService.d.ts +2 -0
- package/dist/server-postgresql/src/utils/drizzle-conditions.d.ts +2 -2
- package/dist/types/src/controllers/auth.d.ts +2 -0
- package/dist/types/src/controllers/client.d.ts +119 -7
- package/dist/types/src/controllers/collection_registry.d.ts +4 -3
- package/dist/types/src/controllers/customization_controller.d.ts +7 -1
- package/dist/types/src/controllers/data.d.ts +34 -7
- package/dist/types/src/controllers/data_driver.d.ts +20 -28
- package/dist/types/src/controllers/database_admin.d.ts +2 -2
- package/dist/types/src/controllers/email.d.ts +34 -0
- package/dist/types/src/controllers/index.d.ts +1 -0
- package/dist/types/src/controllers/local_config_persistence.d.ts +4 -4
- package/dist/types/src/controllers/navigation.d.ts +5 -5
- package/dist/types/src/controllers/registry.d.ts +6 -3
- package/dist/types/src/controllers/side_entity_controller.d.ts +7 -6
- package/dist/types/src/controllers/storage.d.ts +24 -26
- package/dist/types/src/rebase_context.d.ts +8 -4
- package/dist/types/src/types/backend.d.ts +4 -1
- package/dist/types/src/types/builders.d.ts +5 -4
- package/dist/types/src/types/chips.d.ts +1 -1
- package/dist/types/src/types/collections.d.ts +169 -125
- package/dist/types/src/types/cron.d.ts +102 -0
- package/dist/types/src/types/data_source.d.ts +1 -1
- package/dist/types/src/types/entity_actions.d.ts +8 -8
- package/dist/types/src/types/entity_callbacks.d.ts +15 -15
- package/dist/types/src/types/entity_link_builder.d.ts +1 -1
- package/dist/types/src/types/entity_overrides.d.ts +2 -1
- package/dist/types/src/types/entity_views.d.ts +8 -8
- package/dist/types/src/types/export_import.d.ts +3 -3
- package/dist/types/src/types/index.d.ts +1 -0
- package/dist/types/src/types/plugins.d.ts +72 -18
- package/dist/types/src/types/properties.d.ts +118 -33
- package/dist/types/src/types/relations.d.ts +1 -1
- package/dist/types/src/types/slots.d.ts +30 -6
- package/dist/types/src/types/translations.d.ts +44 -0
- package/dist/types/src/types/user_management_delegate.d.ts +1 -0
- package/drizzle-test/0000_woozy_junta.sql +6 -0
- package/drizzle-test/0001_youthful_arachne.sql +1 -0
- package/drizzle-test/0002_lively_dragon_lord.sql +2 -0
- package/drizzle-test/0003_mean_king_cobra.sql +2 -0
- package/drizzle-test/meta/0000_snapshot.json +47 -0
- package/drizzle-test/meta/0001_snapshot.json +48 -0
- package/drizzle-test/meta/0002_snapshot.json +38 -0
- package/drizzle-test/meta/0003_snapshot.json +48 -0
- package/drizzle-test/meta/_journal.json +34 -0
- package/drizzle-test-out/0000_tan_trauma.sql +6 -0
- package/drizzle-test-out/0001_rapid_drax.sql +1 -0
- package/drizzle-test-out/meta/0000_snapshot.json +44 -0
- package/drizzle-test-out/meta/0001_snapshot.json +54 -0
- package/drizzle-test-out/meta/_journal.json +20 -0
- package/drizzle.test.config.ts +10 -0
- package/package.json +88 -89
- package/scratch.ts +41 -0
- package/src/PostgresBackendDriver.ts +63 -79
- package/src/PostgresBootstrapper.ts +7 -8
- package/src/auth/ensure-tables.ts +158 -86
- package/src/auth/services.ts +109 -50
- package/src/cli.ts +259 -16
- package/src/collections/PostgresCollectionRegistry.ts +6 -6
- package/src/connection.ts +70 -48
- package/src/data-transformer.ts +155 -116
- package/src/databasePoolManager.ts +6 -5
- package/src/history/HistoryService.ts +3 -12
- package/src/interfaces.ts +3 -3
- package/src/schema/auth-schema.ts +26 -3
- package/src/schema/doctor-cli.ts +47 -0
- package/src/schema/doctor.ts +595 -0
- package/src/schema/generate-drizzle-schema-logic.ts +204 -57
- package/src/schema/generate-drizzle-schema.ts +6 -6
- package/src/schema/test-schema.ts +11 -0
- package/src/services/BranchService.ts +5 -5
- package/src/services/EntityFetchService.ts +317 -188
- package/src/services/EntityPersistService.ts +15 -17
- package/src/services/RelationService.ts +299 -37
- package/src/services/entity-helpers.ts +39 -13
- package/src/services/entityService.ts +11 -9
- package/src/services/realtimeService.ts +58 -29
- package/src/utils/drizzle-conditions.ts +25 -24
- package/src/websocket.ts +52 -21
- package/test/auth-services.test.ts +131 -39
- package/test/batch-many-to-many-regression.test.ts +573 -0
- package/test/branchService.test.ts +22 -12
- package/test/data-transformer-hardening.test.ts +417 -0
- package/test/data-transformer.test.ts +175 -0
- package/test/doctor.test.ts +182 -0
- package/test/entityService.errors.test.ts +31 -16
- package/test/entityService.relations.test.ts +155 -59
- package/test/entityService.subcollection-search.test.ts +107 -57
- package/test/entityService.test.ts +105 -47
- package/test/generate-drizzle-schema.test.ts +262 -69
- package/test/historyService.test.ts +31 -16
- package/test/n-plus-one-regression.test.ts +314 -0
- package/test/postgresDataDriver.test.ts +260 -168
- package/test/realtimeService.test.ts +70 -39
- package/test/relation-pipeline-gaps.test.ts +637 -0
- package/test/relations.test.ts +492 -39
- package/test-drizzle-bug.ts +18 -0
- package/test-drizzle-out/0000_cultured_freak.sql +7 -0
- package/test-drizzle-out/0001_tiresome_professor_monster.sql +1 -0
- package/test-drizzle-out/meta/0000_snapshot.json +55 -0
- package/test-drizzle-out/meta/0001_snapshot.json +63 -0
- package/test-drizzle-out/meta/_journal.json +20 -0
- package/test-drizzle-prompt.sh +2 -0
- package/test-policy-prompt.sh +3 -0
- package/test-programmatic.ts +30 -0
- package/test-programmatic2.ts +59 -0
- package/test-schema-no-policies.ts +12 -0
- package/test_drizzle_mock.js +2 -2
- package/test_find_changed.mjs +3 -1
- package/test_hash.js +14 -0
- package/tsconfig.json +1 -1
- 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
|
+
});
|