@rebasepro/server-postgresql 0.0.1-canary.eae7889 → 0.0.1-canary.f81da60
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 +171 -180
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +171 -180
- package/dist/index.umd.js.map +1 -1
- package/dist/server-postgresql/src/PostgresBackendDriver.d.ts +6 -0
- package/dist/server-postgresql/src/schema/introspect-db-logic.d.ts +82 -0
- package/dist/server-postgresql/src/schema/introspect-db.d.ts +1 -0
- package/dist/types/src/controllers/auth.d.ts +2 -2
- package/dist/types/src/controllers/collection_registry.d.ts +2 -1
- package/dist/types/src/controllers/data_driver.d.ts +36 -1
- package/dist/types/src/types/backend_hooks.d.ts +187 -0
- package/dist/types/src/types/collections.d.ts +11 -10
- package/dist/types/src/types/cron.d.ts +1 -1
- package/dist/types/src/types/entity_views.d.ts +4 -6
- package/dist/types/src/types/formex.d.ts +40 -0
- package/dist/types/src/types/index.d.ts +2 -0
- package/dist/types/src/types/plugins.d.ts +6 -3
- package/dist/types/src/types/properties.d.ts +61 -89
- package/dist/types/src/types/slots.d.ts +20 -10
- package/dist/types/src/types/translations.d.ts +4 -0
- package/package.json +6 -5
- package/src/PostgresBackendDriver.ts +9 -0
- package/src/cli.ts +59 -1
- package/src/schema/generate-drizzle-schema-logic.ts +7 -25
- package/src/schema/introspect-db-logic.ts +592 -0
- package/src/schema/introspect-db.ts +211 -0
- package/src/services/EntityPersistService.ts +7 -1
- package/test/generate-drizzle-schema.test.ts +47 -0
- package/test/introspect-db-generation.test.ts +436 -0
- package/test/introspect-db-utils.test.ts +392 -0
- package/test/relations.test.ts +4 -4
- package/test/unmapped-tables-safety.test.ts +345 -0
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
import {
|
|
2
|
+
singularize, humanize, toCollectionVarName, getIconForTable,
|
|
3
|
+
mapPgType, buildEnumMap, buildTablesMap, identifyJoinTables,
|
|
4
|
+
generateIndexContent, mergeIndexContent, safeHostFromUrl,
|
|
5
|
+
EnumValue, TableRow, TableColumn, PrimaryKeyRow, ForeignKeyRow,
|
|
6
|
+
} from "../src/schema/introspect-db-logic";
|
|
7
|
+
|
|
8
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
9
|
+
// singularize()
|
|
10
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
11
|
+
describe("singularize", () => {
|
|
12
|
+
it("strips trailing s from regular plurals", () => {
|
|
13
|
+
expect(singularize("users")).toBe("user");
|
|
14
|
+
expect(singularize("products")).toBe("product");
|
|
15
|
+
expect(singularize("posts")).toBe("post");
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("converts -ies to -y", () => {
|
|
19
|
+
expect(singularize("categories")).toBe("category");
|
|
20
|
+
expect(singularize("companies")).toBe("company");
|
|
21
|
+
expect(singularize("stories")).toBe("story");
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("converts -ves to -f", () => {
|
|
25
|
+
expect(singularize("wolves")).toBe("wolf");
|
|
26
|
+
expect(singularize("leaves")).toBe("leaf");
|
|
27
|
+
expect(singularize("knives")).toBe("knif"); // known edge-case
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("converts -ches, -shes, -sses, -xes, -zes", () => {
|
|
31
|
+
expect(singularize("batches")).toBe("batch");
|
|
32
|
+
expect(singularize("wishes")).toBe("wish");
|
|
33
|
+
expect(singularize("classes")).toBe("class");
|
|
34
|
+
expect(singularize("boxes")).toBe("box");
|
|
35
|
+
expect(singularize("buzzes")).toBe("buzz");
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("converts -ses (not -sses) by removing trailing s", () => {
|
|
39
|
+
expect(singularize("responses")).toBe("response");
|
|
40
|
+
expect(singularize("databases")).toBe("database");
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("converts -ices to -ex", () => {
|
|
44
|
+
expect(singularize("indices")).toBe("index");
|
|
45
|
+
expect(singularize("vertices")).toBe("vertex");
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("handles irregular plurals", () => {
|
|
49
|
+
expect(singularize("people")).toBe("person");
|
|
50
|
+
expect(singularize("children")).toBe("child");
|
|
51
|
+
expect(singularize("men")).toBe("man");
|
|
52
|
+
expect(singularize("women")).toBe("woman");
|
|
53
|
+
expect(singularize("mice")).toBe("mouse");
|
|
54
|
+
expect(singularize("geese")).toBe("goose");
|
|
55
|
+
expect(singularize("teeth")).toBe("tooth");
|
|
56
|
+
expect(singularize("feet")).toBe("foot");
|
|
57
|
+
expect(singularize("data")).toBe("datum");
|
|
58
|
+
expect(singularize("media")).toBe("medium");
|
|
59
|
+
expect(singularize("criteria")).toBe("criterion");
|
|
60
|
+
expect(singularize("phenomena")).toBe("phenomenon");
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("preserves casing on irregular plurals", () => {
|
|
64
|
+
expect(singularize("People")).toBe("Person");
|
|
65
|
+
expect(singularize("Children")).toBe("Child");
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("leaves uncountable words unchanged", () => {
|
|
69
|
+
expect(singularize("status")).toBe("status");
|
|
70
|
+
expect(singularize("news")).toBe("news");
|
|
71
|
+
expect(singularize("series")).toBe("series");
|
|
72
|
+
expect(singularize("species")).toBe("species");
|
|
73
|
+
expect(singularize("analysis")).toBe("analysis");
|
|
74
|
+
expect(singularize("diagnosis")).toBe("diagnosis");
|
|
75
|
+
expect(singularize("crisis")).toBe("crisis");
|
|
76
|
+
expect(singularize("thesis")).toBe("thesis");
|
|
77
|
+
expect(singularize("bus")).toBe("bus");
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("leaves already-singular words unchanged", () => {
|
|
81
|
+
expect(singularize("user")).toBe("user");
|
|
82
|
+
expect(singularize("address")).toBe("address");
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("does not strip -ss words", () => {
|
|
86
|
+
expect(singularize("boss")).toBe("boss");
|
|
87
|
+
expect(singularize("glass")).toBe("glass");
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
92
|
+
// humanize()
|
|
93
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
94
|
+
describe("humanize", () => {
|
|
95
|
+
it("converts snake_case to Title Case", () => {
|
|
96
|
+
expect(humanize("created_at")).toBe("Created At");
|
|
97
|
+
expect(humanize("customer_id")).toBe("Customer Id");
|
|
98
|
+
expect(humanize("first_name")).toBe("First Name");
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("capitalizes single words", () => {
|
|
102
|
+
expect(humanize("name")).toBe("Name");
|
|
103
|
+
expect(humanize("id")).toBe("Id");
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("handles multiple underscores", () => {
|
|
107
|
+
expect(humanize("user_profile_image")).toBe("User Profile Image");
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("handles already capitalized input", () => {
|
|
111
|
+
expect(humanize("Name")).toBe("Name");
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
116
|
+
// toCollectionVarName()
|
|
117
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
118
|
+
describe("toCollectionVarName", () => {
|
|
119
|
+
it("converts snake_case to camelCase + Collection", () => {
|
|
120
|
+
expect(toCollectionVarName("company_token")).toBe("companyTokenCollection");
|
|
121
|
+
expect(toCollectionVarName("user_profile")).toBe("userProfileCollection");
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("handles single-word tables", () => {
|
|
125
|
+
expect(toCollectionVarName("users")).toBe("usersCollection");
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("handles multi-segment names", () => {
|
|
129
|
+
expect(toCollectionVarName("user_account_settings")).toBe("userAccountSettingsCollection");
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
134
|
+
// getIconForTable()
|
|
135
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
136
|
+
describe("getIconForTable", () => {
|
|
137
|
+
const cases: [string, string][] = [
|
|
138
|
+
["users", "Users"], ["accounts", "Users"], ["members", "Users"],
|
|
139
|
+
["customers", "Users"], ["clients", "Users"], ["patients", "Users"],
|
|
140
|
+
["posts", "FileText"], ["articles", "FileText"], ["blog_entries", "FileText"],
|
|
141
|
+
["pages", "FileText"],
|
|
142
|
+
["products", "Package"], ["items", "Package"],
|
|
143
|
+
["orders", "ShoppingCart"], ["cart", "ShoppingCart"],
|
|
144
|
+
["purchases", "ShoppingCart"], ["invoices", "ShoppingCart"],
|
|
145
|
+
["settings", "Settings"], ["app_config", "Settings"],
|
|
146
|
+
["tags", "Tag"], ["categories", "Tag"],
|
|
147
|
+
["images", "Image"], ["photos", "Image"], ["media_assets", "Image"],
|
|
148
|
+
["notifications", "Mail"], ["messages", "Mail"], ["emails", "Mail"],
|
|
149
|
+
["audit_log", "Activity"], ["events", "Activity"],
|
|
150
|
+
["subscriptions", "CreditCard"], ["plans", "CreditCard"], ["billing", "CreditCard"],
|
|
151
|
+
["comments", "MessageCircle"], ["reviews", "MessageCircle"], ["feedback", "MessageCircle"],
|
|
152
|
+
];
|
|
153
|
+
|
|
154
|
+
it.each(cases)("returns %s -> %s", (table, icon) => {
|
|
155
|
+
expect(getIconForTable(table)).toBe(icon);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("falls back to Database for unknown tables", () => {
|
|
159
|
+
expect(getIconForTable("foobar")).toBe("Database");
|
|
160
|
+
expect(getIconForTable("xyz_data")).toBe("Database");
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
165
|
+
// mapPgType()
|
|
166
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
167
|
+
describe("mapPgType", () => {
|
|
168
|
+
it("maps integer types to number", () => {
|
|
169
|
+
for (const t of ["integer", "smallint", "bigint", "int4", "int8"]) {
|
|
170
|
+
expect(mapPgType(t)).toBe("number");
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
it("maps serial types to number", () => {
|
|
174
|
+
// serial/bigserial don't contain 'int' — they need explicit matching
|
|
175
|
+
for (const t of ["serial", "bigserial"]) {
|
|
176
|
+
expect(mapPgType(t)).toBe("number");
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
it("maps decimal types to number", () => {
|
|
180
|
+
for (const t of ["numeric", "decimal", "real", "float4", "float8", "double precision", "money"]) {
|
|
181
|
+
expect(mapPgType(t)).toBe("number");
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
it("maps boolean types", () => {
|
|
185
|
+
expect(mapPgType("boolean")).toBe("boolean");
|
|
186
|
+
expect(mapPgType("bool")).toBe("boolean");
|
|
187
|
+
});
|
|
188
|
+
it("maps date/time types to date", () => {
|
|
189
|
+
for (const t of ["timestamp", "timestamptz", "date", "time", "timetz", "timestamp with time zone"]) {
|
|
190
|
+
expect(mapPgType(t)).toBe("date");
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
it("maps JSON types to map", () => {
|
|
194
|
+
expect(mapPgType("json")).toBe("map");
|
|
195
|
+
expect(mapPgType("jsonb")).toBe("map");
|
|
196
|
+
});
|
|
197
|
+
it("maps ARRAY and underscore-prefixed types to array", () => {
|
|
198
|
+
expect(mapPgType("ARRAY")).toBe("array");
|
|
199
|
+
expect(mapPgType("_int4")).toBe("array");
|
|
200
|
+
expect(mapPgType("_text")).toBe("array");
|
|
201
|
+
});
|
|
202
|
+
it("maps string-like types to string", () => {
|
|
203
|
+
for (const t of ["text", "varchar", "character varying", "char", "character", "uuid", "bytea", "inet", "cidr", "macaddr", "macaddr8", "interval"]) {
|
|
204
|
+
expect(mapPgType(t)).toBe("string");
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
it("defaults unknown types to string", () => {
|
|
208
|
+
expect(mapPgType("tsvector")).toBe("string");
|
|
209
|
+
expect(mapPgType("xml")).toBe("string");
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
214
|
+
// buildEnumMap()
|
|
215
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
216
|
+
describe("buildEnumMap", () => {
|
|
217
|
+
it("groups enum values by name in order", () => {
|
|
218
|
+
const vals: EnumValue[] = [
|
|
219
|
+
{ enum_name: "status", enum_value: "active", sort_order: 1 },
|
|
220
|
+
{ enum_name: "status", enum_value: "inactive", sort_order: 2 },
|
|
221
|
+
{ enum_name: "role", enum_value: "admin", sort_order: 1 },
|
|
222
|
+
{ enum_name: "role", enum_value: "user", sort_order: 2 },
|
|
223
|
+
];
|
|
224
|
+
const map = buildEnumMap(vals);
|
|
225
|
+
expect(map.get("status")).toEqual(["active", "inactive"]);
|
|
226
|
+
expect(map.get("role")).toEqual(["admin", "user"]);
|
|
227
|
+
});
|
|
228
|
+
it("returns empty map for no enums", () => {
|
|
229
|
+
expect(buildEnumMap([]).size).toBe(0);
|
|
230
|
+
});
|
|
231
|
+
it("handles single-value enums", () => {
|
|
232
|
+
const map = buildEnumMap([{ enum_name: "flag", enum_value: "yes", sort_order: 1 }]);
|
|
233
|
+
expect(map.get("flag")).toEqual(["yes"]);
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
238
|
+
// buildTablesMap()
|
|
239
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
240
|
+
describe("buildTablesMap", () => {
|
|
241
|
+
it("groups columns, pks and fks by table", () => {
|
|
242
|
+
const tables: TableRow[] = [{ table_name: "users" }, { table_name: "posts" }];
|
|
243
|
+
const cols: TableColumn[] = [
|
|
244
|
+
{ table_name: "users", column_name: "id", data_type: "uuid", udt_name: "uuid", is_nullable: "NO", column_default: null },
|
|
245
|
+
{ table_name: "posts", column_name: "id", data_type: "uuid", udt_name: "uuid", is_nullable: "NO", column_default: null },
|
|
246
|
+
{ table_name: "posts", column_name: "user_id", data_type: "uuid", udt_name: "uuid", is_nullable: "NO", column_default: null },
|
|
247
|
+
];
|
|
248
|
+
const pks: PrimaryKeyRow[] = [
|
|
249
|
+
{ table_name: "users", column_name: "id" },
|
|
250
|
+
{ table_name: "posts", column_name: "id" },
|
|
251
|
+
];
|
|
252
|
+
const fks: ForeignKeyRow[] = [
|
|
253
|
+
{ table_name: "posts", column_name: "user_id", foreign_table_name: "users", foreign_column_name: "id" },
|
|
254
|
+
];
|
|
255
|
+
const map = buildTablesMap(tables, cols, pks, fks);
|
|
256
|
+
expect(map.size).toBe(2);
|
|
257
|
+
expect(map.get("users")!.pks).toEqual(["id"]);
|
|
258
|
+
expect(map.get("posts")!.fks).toHaveLength(1);
|
|
259
|
+
expect(map.get("posts")!.columns).toHaveLength(2);
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
264
|
+
// identifyJoinTables()
|
|
265
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
266
|
+
describe("identifyJoinTables", () => {
|
|
267
|
+
const mkCol = (table: string, col: string): TableColumn => ({
|
|
268
|
+
table_name: table, column_name: col, data_type: "uuid",
|
|
269
|
+
udt_name: "uuid", is_nullable: "NO", column_default: null,
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it("detects a pure junction table with exactly 2 FKs", () => {
|
|
273
|
+
const tablesMap = new Map([
|
|
274
|
+
["posts_to_tags", {
|
|
275
|
+
name: "posts_to_tags",
|
|
276
|
+
columns: [mkCol("posts_to_tags", "post_id"), mkCol("posts_to_tags", "tag_id")],
|
|
277
|
+
pks: [],
|
|
278
|
+
fks: [
|
|
279
|
+
{ table_name: "posts_to_tags", column_name: "post_id", foreign_table_name: "posts", foreign_column_name: "id" },
|
|
280
|
+
{ table_name: "posts_to_tags", column_name: "tag_id", foreign_table_name: "tags", foreign_column_name: "id" },
|
|
281
|
+
],
|
|
282
|
+
}],
|
|
283
|
+
]);
|
|
284
|
+
expect(identifyJoinTables(tablesMap)).toEqual(new Set(["posts_to_tags"]));
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
it("allows id, created_at, updated_at metadata columns on join tables", () => {
|
|
288
|
+
const tablesMap = new Map([
|
|
289
|
+
["posts_tags", {
|
|
290
|
+
name: "posts_tags",
|
|
291
|
+
columns: [
|
|
292
|
+
mkCol("posts_tags", "id"),
|
|
293
|
+
mkCol("posts_tags", "post_id"),
|
|
294
|
+
mkCol("posts_tags", "tag_id"),
|
|
295
|
+
mkCol("posts_tags", "created_at"),
|
|
296
|
+
],
|
|
297
|
+
pks: ["id"],
|
|
298
|
+
fks: [
|
|
299
|
+
{ table_name: "posts_tags", column_name: "post_id", foreign_table_name: "posts", foreign_column_name: "id" },
|
|
300
|
+
{ table_name: "posts_tags", column_name: "tag_id", foreign_table_name: "tags", foreign_column_name: "id" },
|
|
301
|
+
],
|
|
302
|
+
}],
|
|
303
|
+
]);
|
|
304
|
+
expect(identifyJoinTables(tablesMap).has("posts_tags")).toBe(true);
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
it("does NOT flag tables with extra non-metadata columns", () => {
|
|
308
|
+
const tablesMap = new Map([
|
|
309
|
+
["enrollments", {
|
|
310
|
+
name: "enrollments",
|
|
311
|
+
columns: [
|
|
312
|
+
mkCol("enrollments", "student_id"),
|
|
313
|
+
mkCol("enrollments", "course_id"),
|
|
314
|
+
mkCol("enrollments", "grade"), // extra column
|
|
315
|
+
],
|
|
316
|
+
pks: [],
|
|
317
|
+
fks: [
|
|
318
|
+
{ table_name: "enrollments", column_name: "student_id", foreign_table_name: "students", foreign_column_name: "id" },
|
|
319
|
+
{ table_name: "enrollments", column_name: "course_id", foreign_table_name: "courses", foreign_column_name: "id" },
|
|
320
|
+
],
|
|
321
|
+
}],
|
|
322
|
+
]);
|
|
323
|
+
expect(identifyJoinTables(tablesMap).size).toBe(0);
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
it("does NOT flag tables with only 1 FK", () => {
|
|
327
|
+
const tablesMap = new Map([
|
|
328
|
+
["posts", {
|
|
329
|
+
name: "posts",
|
|
330
|
+
columns: [mkCol("posts", "id"), mkCol("posts", "user_id")],
|
|
331
|
+
pks: ["id"],
|
|
332
|
+
fks: [
|
|
333
|
+
{ table_name: "posts", column_name: "user_id", foreign_table_name: "users", foreign_column_name: "id" },
|
|
334
|
+
],
|
|
335
|
+
}],
|
|
336
|
+
]);
|
|
337
|
+
expect(identifyJoinTables(tablesMap).size).toBe(0);
|
|
338
|
+
});
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
342
|
+
// generateIndexContent()
|
|
343
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
344
|
+
describe("generateIndexContent", () => {
|
|
345
|
+
it("generates sorted export lines", () => {
|
|
346
|
+
const result = generateIndexContent(["zebra", "apple", "mango"]);
|
|
347
|
+
const lines = result.trim().split("\n");
|
|
348
|
+
expect(lines[0]).toContain("apple");
|
|
349
|
+
expect(lines[1]).toContain("mango");
|
|
350
|
+
expect(lines[2]).toContain("zebra");
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
it("generates import statements and collections array", () => {
|
|
354
|
+
const result = generateIndexContent(["users"]);
|
|
355
|
+
expect(result).toContain('import usersCollection from "./users";');
|
|
356
|
+
expect(result).toContain('export const collections = [');
|
|
357
|
+
expect(result).toContain(' usersCollection,');
|
|
358
|
+
expect(result).toContain('];');
|
|
359
|
+
});
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
363
|
+
// mergeIndexContent()
|
|
364
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
365
|
+
describe("mergeIndexContent", () => {
|
|
366
|
+
it("adds new exports without duplicating existing ones", () => {
|
|
367
|
+
const existing = 'import usersCollection from "./users";\n\nexport const collections = [\n usersCollection,\n];\n';
|
|
368
|
+
const result = mergeIndexContent(existing, ["users", "posts"]);
|
|
369
|
+
expect(result.match(/import usersCollection from ".\/users";/g)!.length).toBe(1);
|
|
370
|
+
expect(result).toContain('import postsCollection from "./posts";');
|
|
371
|
+
expect(result).toContain('usersCollection,');
|
|
372
|
+
expect(result).toContain('postsCollection,');
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
it("returns existing content trimmed + newline when no new files", () => {
|
|
376
|
+
const existing = 'import aCollection from "./a";\n\nexport const collections = [\n aCollection,\n];\n';
|
|
377
|
+
const result = mergeIndexContent(existing, ["a"]);
|
|
378
|
+
expect(result.trim()).toBe(existing.trim());
|
|
379
|
+
});
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
383
|
+
// safeHostFromUrl()
|
|
384
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
385
|
+
describe("safeHostFromUrl", () => {
|
|
386
|
+
it("extracts host after @", () => {
|
|
387
|
+
expect(safeHostFromUrl("postgres://user:pass@localhost:5432/db")).toBe("localhost:5432/db");
|
|
388
|
+
});
|
|
389
|
+
it("returns fallback for URLs without @", () => {
|
|
390
|
+
expect(safeHostFromUrl("localhost:5432/db")).toBe("(local connection)");
|
|
391
|
+
});
|
|
392
|
+
});
|
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
|