@rebasepro/server-postgresql 0.0.1-canary.ca2cb6e → 0.0.1-canary.dbf160a
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/server-postgresql/src/schema/introspect-db-logic.d.ts +82 -0
- package/dist/server-postgresql/src/schema/introspect-db.d.ts +1 -0
- package/package.json +6 -5
- package/src/cli.ts +59 -1
- package/src/schema/introspect-db-logic.ts +592 -0
- package/src/schema/introspect-db.ts +211 -0
- package/test/introspect-db-generation.test.ts +436 -0
- package/test/introspect-db-utils.test.ts +392 -0
- 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
|
+
});
|