@rebasepro/server-postgresql 0.0.1-canary.c53f5db → 0.0.1-canary.cbdd980
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/index.es.js +384 -1100
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +315 -1031
- package/dist/index.umd.js.map +1 -1
- package/dist/server-postgresql/src/PostgresBackendDriver.d.ts +2 -1
- package/dist/server-postgresql/src/schema/introspect-db-inference.d.ts +5 -0
- package/dist/server-postgresql/src/schema/introspect-db-logic.d.ts +44 -9
- package/dist/server-postgresql/src/services/EntityPersistService.d.ts +9 -0
- package/dist/types/src/controllers/auth.d.ts +8 -2
- package/dist/types/src/controllers/client.d.ts +13 -0
- package/dist/types/src/controllers/navigation.d.ts +18 -6
- package/dist/types/src/controllers/registry.d.ts +9 -1
- package/dist/types/src/controllers/side_entity_controller.d.ts +7 -0
- package/dist/types/src/rebase_context.d.ts +17 -0
- package/dist/types/src/types/collections.d.ts +21 -1
- package/dist/types/src/types/component_ref.d.ts +47 -0
- package/dist/types/src/types/cron.d.ts +1 -1
- package/dist/types/src/types/entity_views.d.ts +2 -1
- package/dist/types/src/types/index.d.ts +1 -0
- package/dist/types/src/types/properties.d.ts +68 -84
- package/dist/types/src/types/translations.d.ts +2 -0
- package/package.json +5 -5
- package/src/PostgresBackendDriver.ts +23 -6
- package/src/cli.ts +10 -2
- package/src/data-transformer.ts +84 -1
- package/src/schema/doctor.ts +14 -2
- package/src/schema/generate-drizzle-schema-logic.ts +59 -30
- package/src/schema/introspect-db-inference.ts +238 -0
- package/src/schema/introspect-db-logic.ts +365 -61
- package/src/schema/introspect-db.ts +66 -23
- package/src/services/EntityFetchService.ts +16 -0
- package/src/services/EntityPersistService.ts +88 -12
- package/test/generate-drizzle-schema.test.ts +295 -0
- package/test/introspect-db-generation.test.ts +32 -10
- package/test/property-ordering.test.ts +395 -0
- package/test/relations.test.ts +4 -4
- package/jest-all.log +0 -3128
- package/jest.log +0 -49
- package/scratch.ts +0 -41
- package/test-drizzle-bug.ts +0 -18
- package/test-drizzle-out/0000_cultured_freak.sql +0 -7
- package/test-drizzle-out/0001_tiresome_professor_monster.sql +0 -1
- package/test-drizzle-out/meta/0000_snapshot.json +0 -55
- package/test-drizzle-out/meta/0001_snapshot.json +0 -63
- package/test-drizzle-out/meta/_journal.json +0 -20
- package/test-drizzle-prompt.sh +0 -2
- package/test-policy-prompt.sh +0 -3
- package/test-programmatic.ts +0 -30
- package/test-programmatic2.ts +0 -59
- package/test-schema-no-policies.ts +0 -12
- package/test_drizzle_mock.js +0 -3
- package/test_find_changed.mjs +0 -32
- package/test_hash.js +0 -14
- package/test_output.txt +0 -3145
|
@@ -148,27 +148,27 @@ describe("generateCollectionFile", () => {
|
|
|
148
148
|
});
|
|
149
149
|
|
|
150
150
|
describe("enum support", () => {
|
|
151
|
-
it("generates
|
|
151
|
+
it("generates enum for USER-DEFINED columns with matching enum", () => {
|
|
152
152
|
const enumMap = new Map([["order_status", ["pending", "shipped", "delivered"]]]);
|
|
153
153
|
const meta = makeSimpleTable("orders", [
|
|
154
154
|
mkCol("orders", "id", { data_type: "uuid", udt_name: "uuid", is_nullable: "NO" }),
|
|
155
155
|
mkCol("orders", "status", { data_type: "USER-DEFINED", udt_name: "order_status" }),
|
|
156
156
|
]);
|
|
157
157
|
const result = generateCollectionFile("orders", meta, [], new Set(), new Map([["orders", meta]]), enumMap);
|
|
158
|
-
expect(result).toContain('
|
|
158
|
+
expect(result).toContain('enum:');
|
|
159
159
|
expect(result).toContain('{ id: "pending", label: "Pending" }');
|
|
160
160
|
expect(result).toContain('{ id: "shipped", label: "Shipped" }');
|
|
161
161
|
expect(result).toContain('{ id: "delivered", label: "Delivered" }');
|
|
162
162
|
expect(result).toContain('type: "string"');
|
|
163
163
|
});
|
|
164
164
|
|
|
165
|
-
it("does NOT add
|
|
165
|
+
it("does NOT add enum for USER-DEFINED without matching enum", () => {
|
|
166
166
|
const meta = makeSimpleTable("things", [
|
|
167
167
|
mkCol("things", "id", { data_type: "uuid", udt_name: "uuid", is_nullable: "NO" }),
|
|
168
168
|
mkCol("things", "geom", { data_type: "USER-DEFINED", udt_name: "geometry" }),
|
|
169
169
|
]);
|
|
170
170
|
const result = generateCollectionFile("things", meta, [], new Set(), new Map([["things", meta]]), new Map());
|
|
171
|
-
expect(result).not.toContain("
|
|
171
|
+
expect(result).not.toContain("enum");
|
|
172
172
|
});
|
|
173
173
|
|
|
174
174
|
it("humanizes enum value labels with underscores", () => {
|
|
@@ -264,7 +264,7 @@ describe("generateCollectionFile", () => {
|
|
|
264
264
|
]);
|
|
265
265
|
const result = generateCollectionFile("t", meta, [], new Set(), new Map([["t", meta]]), enumMap);
|
|
266
266
|
expect(result).not.toContain("storagePath");
|
|
267
|
-
expect(result).toContain("
|
|
267
|
+
expect(result).toContain("enum:");
|
|
268
268
|
});
|
|
269
269
|
});
|
|
270
270
|
|
|
@@ -285,22 +285,40 @@ describe("generateCollectionFile", () => {
|
|
|
285
285
|
});
|
|
286
286
|
|
|
287
287
|
describe("inverse relation (other table -> this table)", () => {
|
|
288
|
-
it("generates a one-to-many inverse relation", () => {
|
|
288
|
+
it("generates a one-to-many inverse relation in the relations array", () => {
|
|
289
289
|
const allFks: ForeignKeyRow[] = [mkFk("comments", "post_id", "posts")];
|
|
290
290
|
const meta = makeSimpleTable("posts", [
|
|
291
291
|
mkCol("posts", "id", { data_type: "uuid", udt_name: "uuid", is_nullable: "NO" }),
|
|
292
292
|
]);
|
|
293
293
|
const result = generateCollectionFile("posts", meta, allFks, new Set(), new Map([["posts", meta]]), new Map());
|
|
294
294
|
expect(result).toContain('import commentsCollection from "./comments"');
|
|
295
|
+
// Should be in the relations array, not as an inline property
|
|
296
|
+
expect(result).toContain('relations: [');
|
|
297
|
+
expect(result).toContain('relationName: "comments"');
|
|
295
298
|
expect(result).toContain('cardinality: "many"');
|
|
296
299
|
expect(result).toContain('direction: "inverse"');
|
|
297
300
|
expect(result).toContain('inverseRelationName: "post"');
|
|
298
301
|
expect(result).toContain('foreignKeyOnTarget: "post_id"');
|
|
302
|
+
// Should NOT appear as an inline property with type: "relation"
|
|
303
|
+
const propsSection = result.split('properties:')[1].split('relations:')[0];
|
|
304
|
+
expect(propsSection).not.toContain('"relation"');
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
it("does NOT include inverse relations in propertiesOrder", () => {
|
|
308
|
+
const allFks: ForeignKeyRow[] = [mkFk("comments", "post_id", "posts")];
|
|
309
|
+
const meta = makeSimpleTable("posts", [
|
|
310
|
+
mkCol("posts", "id", { data_type: "uuid", udt_name: "uuid", is_nullable: "NO" }),
|
|
311
|
+
]);
|
|
312
|
+
const result = generateCollectionFile("posts", meta, allFks, new Set(), new Map([["posts", meta]]), new Map());
|
|
313
|
+
const orderMatch = result.match(/propertiesOrder:\s*(\[[\s\S]*?\])/);
|
|
314
|
+
expect(orderMatch).toBeTruthy();
|
|
315
|
+
const orderBlock = orderMatch![1];
|
|
316
|
+
expect(orderBlock).not.toContain('"comments"');
|
|
299
317
|
});
|
|
300
318
|
});
|
|
301
319
|
|
|
302
320
|
describe("many-to-many relations", () => {
|
|
303
|
-
it("generates owning M2M with through config
|
|
321
|
+
it("generates owning M2M with through config in relations array", () => {
|
|
304
322
|
const jtFks: ForeignKeyRow[] = [
|
|
305
323
|
mkFk("articles_tags", "article_id", "articles"),
|
|
306
324
|
mkFk("articles_tags", "tag_id", "tags"),
|
|
@@ -317,13 +335,15 @@ describe("generateCollectionFile", () => {
|
|
|
317
335
|
const joinTables = new Set(["articles_tags"]);
|
|
318
336
|
|
|
319
337
|
const result = generateCollectionFile("articles", articlesMeta, [], joinTables, tablesMap, new Map());
|
|
338
|
+
expect(result).toContain('relations: [');
|
|
339
|
+
expect(result).toContain('relationName: "tags"');
|
|
320
340
|
expect(result).toContain('direction: "owning"');
|
|
321
341
|
expect(result).toContain('table: "articles_tags"');
|
|
322
342
|
expect(result).toContain('sourceColumn: "article_id"');
|
|
323
343
|
expect(result).toContain('targetColumn: "tag_id"');
|
|
324
344
|
});
|
|
325
345
|
|
|
326
|
-
it("generates inverse M2M
|
|
346
|
+
it("generates inverse M2M in relations array", () => {
|
|
327
347
|
const jtFks: ForeignKeyRow[] = [
|
|
328
348
|
mkFk("articles_tags", "article_id", "articles"),
|
|
329
349
|
mkFk("articles_tags", "tag_id", "tags"),
|
|
@@ -340,12 +360,13 @@ describe("generateCollectionFile", () => {
|
|
|
340
360
|
const joinTables = new Set(["articles_tags"]);
|
|
341
361
|
|
|
342
362
|
const result = generateCollectionFile("tags", tagsMeta, [], joinTables, tablesMap, new Map());
|
|
363
|
+
expect(result).toContain('relations: [');
|
|
343
364
|
expect(result).toContain('direction: "inverse"');
|
|
344
365
|
});
|
|
345
366
|
});
|
|
346
367
|
|
|
347
368
|
describe("self-referencing M2M", () => {
|
|
348
|
-
it("generates self-ref M2M with _via_
|
|
369
|
+
it("generates self-ref M2M with _via_ relation name in relations array", () => {
|
|
349
370
|
const jtFks: ForeignKeyRow[] = [
|
|
350
371
|
mkFk("user_friends", "user_id", "users"),
|
|
351
372
|
mkFk("user_friends", "friend_id", "users"),
|
|
@@ -362,7 +383,8 @@ describe("generateCollectionFile", () => {
|
|
|
362
383
|
const joinTables = new Set(["user_friends"]);
|
|
363
384
|
|
|
364
385
|
const result = generateCollectionFile("users", usersMeta, [], joinTables, tablesMap, new Map());
|
|
365
|
-
expect(result).toContain(
|
|
386
|
+
expect(result).toContain('relations: [');
|
|
387
|
+
expect(result).toContain('relationName: "users_via_friend"');
|
|
366
388
|
expect(result).toContain('table: "user_friends"');
|
|
367
389
|
expect(result).toContain('sourceColumn: "user_id"');
|
|
368
390
|
expect(result).toContain('targetColumn: "friend_id"');
|
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
import {
|
|
2
|
+
computePropertyPriority, sortPropertiesOrder,
|
|
3
|
+
PropertyOrderingContext, PropertyOrderEntry,
|
|
4
|
+
} from "../src/schema/introspect-db-logic";
|
|
5
|
+
|
|
6
|
+
// ── Helpers ───────────────────────────────────────────────────────────
|
|
7
|
+
|
|
8
|
+
function mkCtx(overrides: Partial<PropertyOrderingContext> = {}): PropertyOrderingContext {
|
|
9
|
+
return {
|
|
10
|
+
propType: "string",
|
|
11
|
+
isPk: false,
|
|
12
|
+
isEnum: false,
|
|
13
|
+
isStorage: false,
|
|
14
|
+
pgDataType: "character varying",
|
|
15
|
+
originalIndex: 0,
|
|
16
|
+
...overrides,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function mkEntry(key: string, overrides: Partial<PropertyOrderingContext> = {}): PropertyOrderEntry {
|
|
21
|
+
return { key, ctx: mkCtx(overrides) };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
25
|
+
// computePropertyPriority() — tier assignment
|
|
26
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
27
|
+
describe("computePropertyPriority", () => {
|
|
28
|
+
describe("Tier 0: Identity / Primary keys", () => {
|
|
29
|
+
it("ranks 'id' PK as tier 0 (score 0)", () => {
|
|
30
|
+
const score = computePropertyPriority("id", mkCtx({ isPk: true }));
|
|
31
|
+
expect(score).toBeGreaterThanOrEqual(0);
|
|
32
|
+
expect(score).toBeLessThan(10);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("ranks any PK column in tier 0", () => {
|
|
36
|
+
const score = computePropertyPriority("uuid", mkCtx({ isPk: true }));
|
|
37
|
+
expect(score).toBeGreaterThanOrEqual(0);
|
|
38
|
+
expect(score).toBeLessThan(10);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("ranks unknown PK name with fallback score 5", () => {
|
|
42
|
+
const score = computePropertyPriority("pk_col", mkCtx({ isPk: true }));
|
|
43
|
+
expect(score).toBeGreaterThanOrEqual(5);
|
|
44
|
+
expect(score).toBeLessThan(10);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe("Tier 1: Title / Name fields", () => {
|
|
49
|
+
it.each(["name", "title", "label", "display_name", "headline", "subject", "heading"])(
|
|
50
|
+
"ranks '%s' in tier 1 (10-19)",
|
|
51
|
+
(col) => {
|
|
52
|
+
const score = computePropertyPriority(col, mkCtx());
|
|
53
|
+
expect(score).toBeGreaterThanOrEqual(10);
|
|
54
|
+
expect(score).toBeLessThan(20);
|
|
55
|
+
},
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
it("ranks 'product_name' as partial match in tier 1b (17-19)", () => {
|
|
59
|
+
const score = computePropertyPriority("product_name", mkCtx());
|
|
60
|
+
expect(score).toBeGreaterThanOrEqual(17);
|
|
61
|
+
expect(score).toBeLessThan(20);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("ranks 'page_title' as partial match in tier 1b", () => {
|
|
65
|
+
const score = computePropertyPriority("page_title", mkCtx());
|
|
66
|
+
expect(score).toBeGreaterThanOrEqual(17);
|
|
67
|
+
expect(score).toBeLessThan(20);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe("Tier 2: Human identity fields", () => {
|
|
72
|
+
it.each(["first_name", "last_name", "full_name", "username", "email", "phone", "phone_number"])(
|
|
73
|
+
"ranks '%s' in tier 2 (20-29)",
|
|
74
|
+
(col) => {
|
|
75
|
+
const score = computePropertyPriority(col, mkCtx());
|
|
76
|
+
expect(score).toBeGreaterThanOrEqual(20);
|
|
77
|
+
expect(score).toBeLessThan(30);
|
|
78
|
+
},
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
it("ranks first_name before last_name", () => {
|
|
82
|
+
const first = computePropertyPriority("first_name", mkCtx());
|
|
83
|
+
const last = computePropertyPriority("last_name", mkCtx());
|
|
84
|
+
expect(first).toBeLessThan(last);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
describe("Tier 3: Core descriptors", () => {
|
|
89
|
+
it.each(["slug", "code", "sku", "type", "status", "role", "category"])(
|
|
90
|
+
"ranks '%s' in tier 3 (30-39)",
|
|
91
|
+
(col) => {
|
|
92
|
+
const score = computePropertyPriority(col, mkCtx());
|
|
93
|
+
expect(score).toBeGreaterThanOrEqual(30);
|
|
94
|
+
expect(score).toBeLessThan(40);
|
|
95
|
+
},
|
|
96
|
+
);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
describe("Tier 4: Short text, enums, booleans", () => {
|
|
100
|
+
it("ranks enum fields in tier 4", () => {
|
|
101
|
+
const score = computePropertyPriority("payment_method", mkCtx({ isEnum: true }));
|
|
102
|
+
expect(score).toBeGreaterThanOrEqual(40);
|
|
103
|
+
expect(score).toBeLessThan(50);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("ranks boolean fields in tier 4", () => {
|
|
107
|
+
const score = computePropertyPriority("active", mkCtx({ propType: "boolean" }));
|
|
108
|
+
expect(score).toBeGreaterThanOrEqual(40);
|
|
109
|
+
expect(score).toBeLessThan(50);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("ranks short string (varchar) in tier 4", () => {
|
|
113
|
+
const score = computePropertyPriority("color", mkCtx({ propType: "string", pgDataType: "character varying" }));
|
|
114
|
+
expect(score).toBeGreaterThanOrEqual(40);
|
|
115
|
+
expect(score).toBeLessThan(50);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
describe("Tier 5: Numbers & user-facing dates", () => {
|
|
120
|
+
it("ranks number fields in tier 5", () => {
|
|
121
|
+
const score = computePropertyPriority("price", mkCtx({ propType: "number" }));
|
|
122
|
+
expect(score).toBeGreaterThanOrEqual(50);
|
|
123
|
+
expect(score).toBeLessThan(60);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("ranks non-system date fields in tier 5", () => {
|
|
127
|
+
const score = computePropertyPriority("published_at", mkCtx({ propType: "date" }));
|
|
128
|
+
expect(score).toBeGreaterThanOrEqual(50);
|
|
129
|
+
expect(score).toBeLessThan(60);
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
describe("Tier 6: Relations", () => {
|
|
134
|
+
it("ranks relation fields in tier 6", () => {
|
|
135
|
+
const score = computePropertyPriority("author", mkCtx({ propType: "relation" }));
|
|
136
|
+
expect(score).toBeGreaterThanOrEqual(60);
|
|
137
|
+
expect(score).toBeLessThan(70);
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
describe("Tier 7: Long text fields", () => {
|
|
142
|
+
it.each(["description", "summary", "excerpt", "bio", "about"])(
|
|
143
|
+
"ranks '%s' in tier 7 (70-79)",
|
|
144
|
+
(col) => {
|
|
145
|
+
const score = computePropertyPriority(col, mkCtx());
|
|
146
|
+
expect(score).toBeGreaterThanOrEqual(70);
|
|
147
|
+
expect(score).toBeLessThan(80);
|
|
148
|
+
},
|
|
149
|
+
);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
describe("Tier 8: Rich content fields", () => {
|
|
153
|
+
it.each(["content", "body", "html"])(
|
|
154
|
+
"ranks '%s' in tier 8 (80-89)",
|
|
155
|
+
(col) => {
|
|
156
|
+
const score = computePropertyPriority(col, mkCtx());
|
|
157
|
+
expect(score).toBeGreaterThanOrEqual(80);
|
|
158
|
+
expect(score).toBeLessThan(90);
|
|
159
|
+
},
|
|
160
|
+
);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
describe("Tier 9: Media / storage fields", () => {
|
|
164
|
+
it("ranks storage fields in tier 9", () => {
|
|
165
|
+
const score = computePropertyPriority("profile_image", mkCtx({ isStorage: true }));
|
|
166
|
+
expect(score).toBeGreaterThanOrEqual(90);
|
|
167
|
+
expect(score).toBeLessThan(100);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it.each(["avatar", "photo_url", "logo", "cover_image", "thumbnail"])(
|
|
171
|
+
"ranks '%s' (media pattern) in tier 9",
|
|
172
|
+
(col) => {
|
|
173
|
+
const score = computePropertyPriority(col, mkCtx());
|
|
174
|
+
expect(score).toBeGreaterThanOrEqual(90);
|
|
175
|
+
expect(score).toBeLessThan(100);
|
|
176
|
+
},
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
it("ranks URL-suffix fields in tier 9", () => {
|
|
180
|
+
const score = computePropertyPriority("website_url", mkCtx());
|
|
181
|
+
expect(score).toBeGreaterThanOrEqual(90);
|
|
182
|
+
expect(score).toBeLessThan(100);
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
describe("Tier 10: JSON / Map types", () => {
|
|
187
|
+
it("ranks map types in tier 10", () => {
|
|
188
|
+
const score = computePropertyPriority("metadata", mkCtx({ propType: "map" }));
|
|
189
|
+
expect(score).toBeGreaterThanOrEqual(100);
|
|
190
|
+
expect(score).toBeLessThan(110);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it("ranks unknown map field names slightly higher", () => {
|
|
194
|
+
const known = computePropertyPriority("metadata", mkCtx({ propType: "map" }));
|
|
195
|
+
const unknown = computePropertyPriority("weird_json", mkCtx({ propType: "map" }));
|
|
196
|
+
expect(unknown).toBeGreaterThan(known);
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
describe("Tier 11: Array types", () => {
|
|
201
|
+
it("ranks array types in tier 11", () => {
|
|
202
|
+
const score = computePropertyPriority("tags", mkCtx({ propType: "array" }));
|
|
203
|
+
expect(score).toBeGreaterThanOrEqual(110);
|
|
204
|
+
expect(score).toBeLessThan(120);
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
describe("Tier 12: System timestamps", () => {
|
|
209
|
+
it.each(["created_at", "updated_at", "deleted_at", "archived_at"])(
|
|
210
|
+
"ranks '%s' in tier 12 (120-129)",
|
|
211
|
+
(col) => {
|
|
212
|
+
const score = computePropertyPriority(col, mkCtx({ propType: "date" }));
|
|
213
|
+
expect(score).toBeGreaterThanOrEqual(120);
|
|
214
|
+
expect(score).toBeLessThan(130);
|
|
215
|
+
},
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
it("ranks created_at before updated_at", () => {
|
|
219
|
+
const created = computePropertyPriority("created_at", mkCtx({ propType: "date" }));
|
|
220
|
+
const updated = computePropertyPriority("updated_at", mkCtx({ propType: "date" }));
|
|
221
|
+
expect(created).toBeLessThan(updated);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it("ranks updated_at before deleted_at", () => {
|
|
225
|
+
const updated = computePropertyPriority("updated_at", mkCtx({ propType: "date" }));
|
|
226
|
+
const deleted = computePropertyPriority("deleted_at", mkCtx({ propType: "date" }));
|
|
227
|
+
expect(updated).toBeLessThan(deleted);
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
describe("tiebreaking with originalIndex", () => {
|
|
232
|
+
it("preserves original column order within the same tier", () => {
|
|
233
|
+
const a = computePropertyPriority("color", mkCtx({ originalIndex: 0 }));
|
|
234
|
+
const b = computePropertyPriority("size", mkCtx({ originalIndex: 1 }));
|
|
235
|
+
expect(a).toBeLessThan(b);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it("tiebreaker is fractional (doesn't cross tier boundaries)", () => {
|
|
239
|
+
const lastInTier = computePropertyPriority("color", mkCtx({ originalIndex: 9999 }));
|
|
240
|
+
// Should still be in tier 4 (42.x) not tier 5 (50+)
|
|
241
|
+
expect(lastInTier).toBeLessThan(50);
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
247
|
+
// sortPropertiesOrder() — full sorting integration
|
|
248
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
249
|
+
describe("sortPropertiesOrder", () => {
|
|
250
|
+
it("places 'id' before 'name' before generic fields", () => {
|
|
251
|
+
const entries: PropertyOrderEntry[] = [
|
|
252
|
+
mkEntry("created_at", { propType: "date", originalIndex: 3 }),
|
|
253
|
+
mkEntry("name", { originalIndex: 1 }),
|
|
254
|
+
mkEntry("id", { isPk: true, originalIndex: 0 }),
|
|
255
|
+
mkEntry("status", { originalIndex: 2 }),
|
|
256
|
+
];
|
|
257
|
+
const result = sortPropertiesOrder(entries);
|
|
258
|
+
expect(result).toEqual(["id", "name", "status", "created_at"]);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it("sorts a realistic user table correctly", () => {
|
|
262
|
+
const entries: PropertyOrderEntry[] = [
|
|
263
|
+
mkEntry("id", { isPk: true, propType: "string", pgDataType: "uuid", originalIndex: 0 }),
|
|
264
|
+
mkEntry("created_at", { propType: "date", originalIndex: 1 }),
|
|
265
|
+
mkEntry("updated_at", { propType: "date", originalIndex: 2 }),
|
|
266
|
+
mkEntry("email", { originalIndex: 3 }),
|
|
267
|
+
mkEntry("first_name", { originalIndex: 4 }),
|
|
268
|
+
mkEntry("last_name", { originalIndex: 5 }),
|
|
269
|
+
mkEntry("avatar", { isStorage: true, originalIndex: 6 }),
|
|
270
|
+
mkEntry("role", { isEnum: true, originalIndex: 7 }),
|
|
271
|
+
mkEntry("active", { propType: "boolean", originalIndex: 8 }),
|
|
272
|
+
mkEntry("bio", { pgDataType: "text", originalIndex: 9 }),
|
|
273
|
+
mkEntry("metadata", { propType: "map", pgDataType: "jsonb", originalIndex: 10 }),
|
|
274
|
+
];
|
|
275
|
+
const result = sortPropertiesOrder(entries);
|
|
276
|
+
expect(result).toEqual([
|
|
277
|
+
"id", // tier 0: PK
|
|
278
|
+
"first_name", // tier 2: human identity
|
|
279
|
+
"last_name", // tier 2: human identity
|
|
280
|
+
"email", // tier 2: human identity
|
|
281
|
+
"role", // tier 4: enum
|
|
282
|
+
"active", // tier 4: boolean
|
|
283
|
+
"bio", // tier 7: long text
|
|
284
|
+
"avatar", // tier 9: storage
|
|
285
|
+
"metadata", // tier 10: map
|
|
286
|
+
"created_at", // tier 12: system timestamp
|
|
287
|
+
"updated_at", // tier 12: system timestamp
|
|
288
|
+
]);
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
it("sorts a realistic product table correctly", () => {
|
|
292
|
+
const entries: PropertyOrderEntry[] = [
|
|
293
|
+
mkEntry("id", { isPk: true, propType: "number", pgDataType: "integer", originalIndex: 0 }),
|
|
294
|
+
mkEntry("created_at", { propType: "date", originalIndex: 1 }),
|
|
295
|
+
mkEntry("sku", { originalIndex: 2 }),
|
|
296
|
+
mkEntry("name", { originalIndex: 3 }),
|
|
297
|
+
mkEntry("description", { pgDataType: "text", originalIndex: 4 }),
|
|
298
|
+
mkEntry("price", { propType: "number", originalIndex: 5 }),
|
|
299
|
+
mkEntry("category", { propType: "relation", originalIndex: 6 }),
|
|
300
|
+
mkEntry("cover_image", { isStorage: true, originalIndex: 7 }),
|
|
301
|
+
mkEntry("active", { propType: "boolean", originalIndex: 8 }),
|
|
302
|
+
];
|
|
303
|
+
const result = sortPropertiesOrder(entries);
|
|
304
|
+
expect(result).toEqual([
|
|
305
|
+
"id", // tier 0: PK
|
|
306
|
+
"name", // tier 1: title/name
|
|
307
|
+
"sku", // tier 3: descriptor
|
|
308
|
+
"category", // tier 3: descriptor (name match overrides relation tier)
|
|
309
|
+
"active", // tier 4: boolean
|
|
310
|
+
"price", // tier 5: number
|
|
311
|
+
"description", // tier 7: long text
|
|
312
|
+
"cover_image", // tier 9: storage
|
|
313
|
+
"created_at", // tier 12: system timestamp
|
|
314
|
+
]);
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
it("sorts a blog post table correctly", () => {
|
|
318
|
+
const entries: PropertyOrderEntry[] = [
|
|
319
|
+
mkEntry("id", { isPk: true, propType: "string", pgDataType: "uuid", originalIndex: 0 }),
|
|
320
|
+
mkEntry("updated_at", { propType: "date", originalIndex: 1 }),
|
|
321
|
+
mkEntry("created_at", { propType: "date", originalIndex: 2 }),
|
|
322
|
+
mkEntry("content", { pgDataType: "text", originalIndex: 3 }),
|
|
323
|
+
mkEntry("title", { originalIndex: 4 }),
|
|
324
|
+
mkEntry("slug", { originalIndex: 5 }),
|
|
325
|
+
mkEntry("status", { isEnum: true, originalIndex: 6 }),
|
|
326
|
+
mkEntry("author", { propType: "relation", originalIndex: 7 }),
|
|
327
|
+
mkEntry("published_at", { propType: "date", originalIndex: 8 }),
|
|
328
|
+
mkEntry("cover_image", { isStorage: true, originalIndex: 9 }),
|
|
329
|
+
mkEntry("excerpt", { pgDataType: "text", originalIndex: 10 }),
|
|
330
|
+
];
|
|
331
|
+
const result = sortPropertiesOrder(entries);
|
|
332
|
+
expect(result).toEqual([
|
|
333
|
+
"id", // tier 0: PK
|
|
334
|
+
"title", // tier 1: title
|
|
335
|
+
"slug", // tier 3: descriptor
|
|
336
|
+
"status", // tier 4: enum
|
|
337
|
+
"published_at", // tier 5: user-facing date
|
|
338
|
+
"author", // tier 6: relation
|
|
339
|
+
"excerpt", // tier 7: long text
|
|
340
|
+
"content", // tier 8: rich content
|
|
341
|
+
"cover_image", // tier 9: storage
|
|
342
|
+
"created_at", // tier 12: system timestamp
|
|
343
|
+
"updated_at", // tier 12: system timestamp
|
|
344
|
+
]);
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
it("preserves original order for properties in the same tier", () => {
|
|
348
|
+
const entries: PropertyOrderEntry[] = [
|
|
349
|
+
mkEntry("color", { propType: "string", pgDataType: "character varying", originalIndex: 0 }),
|
|
350
|
+
mkEntry("size", { propType: "string", pgDataType: "character varying", originalIndex: 1 }),
|
|
351
|
+
mkEntry("weight", { propType: "string", pgDataType: "character varying", originalIndex: 2 }),
|
|
352
|
+
];
|
|
353
|
+
const result = sortPropertiesOrder(entries);
|
|
354
|
+
// All three are tier 4 (short strings), should preserve original order
|
|
355
|
+
expect(result).toEqual(["color", "size", "weight"]);
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
it("handles tables with only system columns", () => {
|
|
359
|
+
const entries: PropertyOrderEntry[] = [
|
|
360
|
+
mkEntry("id", { isPk: true, originalIndex: 0 }),
|
|
361
|
+
mkEntry("created_at", { propType: "date", originalIndex: 1 }),
|
|
362
|
+
mkEntry("updated_at", { propType: "date", originalIndex: 2 }),
|
|
363
|
+
];
|
|
364
|
+
const result = sortPropertiesOrder(entries);
|
|
365
|
+
expect(result).toEqual(["id", "created_at", "updated_at"]);
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
it("handles partial name matches (product_name, page_title)", () => {
|
|
369
|
+
const entries: PropertyOrderEntry[] = [
|
|
370
|
+
mkEntry("id", { isPk: true, originalIndex: 0 }),
|
|
371
|
+
mkEntry("product_name", { originalIndex: 1 }),
|
|
372
|
+
mkEntry("category", { originalIndex: 2 }),
|
|
373
|
+
mkEntry("price", { propType: "number", originalIndex: 3 }),
|
|
374
|
+
];
|
|
375
|
+
const result = sortPropertiesOrder(entries);
|
|
376
|
+
// product_name should come after id but before category and price
|
|
377
|
+
expect(result[0]).toBe("id");
|
|
378
|
+
expect(result[1]).toBe("product_name");
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
it("handles mixed URL and media fields", () => {
|
|
382
|
+
const entries: PropertyOrderEntry[] = [
|
|
383
|
+
mkEntry("name", { originalIndex: 0 }),
|
|
384
|
+
mkEntry("website_url", { originalIndex: 1 }),
|
|
385
|
+
mkEntry("avatar", { isStorage: true, originalIndex: 2 }),
|
|
386
|
+
mkEntry("thumbnail", { originalIndex: 3 }),
|
|
387
|
+
];
|
|
388
|
+
const result = sortPropertiesOrder(entries);
|
|
389
|
+
expect(result[0]).toBe("name");
|
|
390
|
+
// All media/url fields should cluster together after name
|
|
391
|
+
expect(result.indexOf("website_url")).toBeGreaterThan(result.indexOf("name"));
|
|
392
|
+
expect(result.indexOf("avatar")).toBeGreaterThan(result.indexOf("name"));
|
|
393
|
+
expect(result.indexOf("thumbnail")).toBeGreaterThan(result.indexOf("name"));
|
|
394
|
+
});
|
|
395
|
+
});
|
package/test/relations.test.ts
CHANGED
|
@@ -382,8 +382,8 @@ relationName: "author" }
|
|
|
382
382
|
// Should create owning relation on profiles
|
|
383
383
|
expect(cleanResult).toContain("export const profilesRelations = drizzleRelations(profiles, ({ one, many }) => ({ \"author\": one(authors, { fields: [profiles.author_id], references: [authors.id], relationName: \"profiles_author_id\" }) }));");
|
|
384
384
|
|
|
385
|
-
// Should create inverse relation on authors
|
|
386
|
-
expect(cleanResult).toContain("export const authorsRelations = drizzleRelations(authors, ({ one, many }) => ({ \"profile\": one(profiles, {
|
|
385
|
+
// Should create inverse relation on authors — inverse side has NO fields/references
|
|
386
|
+
expect(cleanResult).toContain("export const authorsRelations = drizzleRelations(authors, ({ one, many }) => ({ \"profile\": one(profiles, { relationName: \"profiles_author_id\" }) }));");
|
|
387
387
|
});
|
|
388
388
|
|
|
389
389
|
it("should generate owning one-to-many relations", async () => {
|
|
@@ -818,9 +818,9 @@ relationName: "user" }
|
|
|
818
818
|
`"user": one(users, { fields: [profiles.user_id], references: [users.id], relationName: \"${expectedSharedName}\" })`
|
|
819
819
|
);
|
|
820
820
|
|
|
821
|
-
// Inverse side (users → profiles)
|
|
821
|
+
// Inverse side (users → profiles) — no fields/references, paired by relationName only
|
|
822
822
|
expect(cleanResult).toContain(
|
|
823
|
-
`"profile": one(profiles, {
|
|
823
|
+
`"profile": one(profiles, { relationName: \"${expectedSharedName}\" })`
|
|
824
824
|
);
|
|
825
825
|
|
|
826
826
|
// Both must match
|