@c9up/atlas 0.2.1 → 0.2.2
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/db.win32-x64-msvc.node +0 -0
- package/dist/testing/DbAssertions.d.ts +6 -0
- package/dist/testing/DbAssertions.d.ts.map +1 -1
- package/dist/testing/DbAssertions.js +77 -1
- package/dist/testing/DbAssertions.js.map +1 -1
- package/index.win32-x64-msvc.node +0 -0
- package/package.json +1 -1
- package/src/testing/DbAssertions.ts +94 -1
package/db.win32-x64-msvc.node
CHANGED
|
Binary file
|
|
@@ -24,6 +24,12 @@ import type { Plugin } from "@c9up/helix";
|
|
|
24
24
|
/** The minimal connection the assertions need — a parameterized query runner. */
|
|
25
25
|
export interface DbConnectionLike {
|
|
26
26
|
query(sql: string, params?: unknown[]): Promise<Record<string, unknown>[]>;
|
|
27
|
+
/**
|
|
28
|
+
* The engine this connection speaks. Absent on a hand-rolled stub, in which
|
|
29
|
+
* case the assertions fall back to `?` placeholders and emit no cast, which
|
|
30
|
+
* is what SQLite and MySQL want.
|
|
31
|
+
*/
|
|
32
|
+
dialect?: string;
|
|
27
33
|
}
|
|
28
34
|
/** The `ctx.db` assertion surface (AdonisJS Lucid database-assertions parity). */
|
|
29
35
|
export interface DbAssertions {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DbAssertions.d.ts","sourceRoot":"","sources":["../../src/testing/DbAssertions.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAE1C,iFAAiF;AACjF,MAAM,WAAW,gBAAgB;IAChC,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"DbAssertions.d.ts","sourceRoot":"","sources":["../../src/testing/DbAssertions.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAE1C,iFAAiF;AACjF,MAAM,WAAW,gBAAgB;IAChC,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC,CAAC;IAC3E;;;;OAIG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;CACjB;AAsHD,kFAAkF;AAClF,MAAM,WAAW,YAAY;IAC5B,qEAAqE;IACrE,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1E,2CAA2C;IAC3C,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9E,2EAA2E;IAC3E,WAAW,CACV,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,MAAM,EAChB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC/B,OAAO,CAAC,IAAI,CAAC,CAAC;IACjB,kCAAkC;IAClC,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC1C;AAED,yDAAyD;AACzD,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,gBAAgB,GAAG,YAAY,CAkCvE;AAED,4EAA4E;AAC5E,wBAAgB,EAAE,CAAC,IAAI,EAAE,gBAAgB,GAAG,MAAM,CAKjD;AAID,OAAO,QAAQ,aAAa,CAAC;IAC5B,UAAU,WAAW;QACpB,EAAE,EAAE,YAAY,CAAC;KACjB;CACD"}
|
|
@@ -20,6 +20,66 @@
|
|
|
20
20
|
* surface is re-verified against the Adonis docs when available. Model-based
|
|
21
21
|
* `assertModelExists`/`assertModelMissing` (entity PK metadata) are pending.
|
|
22
22
|
*/
|
|
23
|
+
/**
|
|
24
|
+
* Column types Postgres will NOT compare against a text-bound parameter.
|
|
25
|
+
*
|
|
26
|
+
* A driver binds a JS string as `text`; `uuid = text` then fails with
|
|
27
|
+
* "operator does not exist". The cast is derived from the COLUMN's declared
|
|
28
|
+
* type, read from the catalog — never guessed from the value, since a text
|
|
29
|
+
* column holding a UUID-shaped string must keep comparing as text.
|
|
30
|
+
*/
|
|
31
|
+
const PG_CASTABLE = new Set([
|
|
32
|
+
"uuid",
|
|
33
|
+
"timestamp without time zone",
|
|
34
|
+
"timestamp with time zone",
|
|
35
|
+
"date",
|
|
36
|
+
"time without time zone",
|
|
37
|
+
"time with time zone",
|
|
38
|
+
"numeric",
|
|
39
|
+
"json",
|
|
40
|
+
"jsonb",
|
|
41
|
+
"inet",
|
|
42
|
+
"interval",
|
|
43
|
+
]);
|
|
44
|
+
/** Catalog lookups are per (table, connection) and never change mid-run. */
|
|
45
|
+
const columnTypeCache = new WeakMap();
|
|
46
|
+
/**
|
|
47
|
+
* `column → declared type` for one table, from `information_schema`.
|
|
48
|
+
*
|
|
49
|
+
* A failure here is not fatal: the assertions simply emit no cast, which is
|
|
50
|
+
* exactly the pre-existing behaviour. A broken catalog read must not turn a
|
|
51
|
+
* passing assertion into an error about introspection.
|
|
52
|
+
*/
|
|
53
|
+
async function columnTypes(conn, table) {
|
|
54
|
+
let perConn = columnTypeCache.get(conn);
|
|
55
|
+
if (!perConn) {
|
|
56
|
+
perConn = new Map();
|
|
57
|
+
columnTypeCache.set(conn, perConn);
|
|
58
|
+
}
|
|
59
|
+
const cached = perConn.get(table);
|
|
60
|
+
if (cached)
|
|
61
|
+
return cached;
|
|
62
|
+
const types = new Map();
|
|
63
|
+
try {
|
|
64
|
+
// `table` may be schema-qualified: compare on the last segment.
|
|
65
|
+
const bare = table.includes(".")
|
|
66
|
+
? table.slice(table.lastIndexOf(".") + 1)
|
|
67
|
+
: table;
|
|
68
|
+
const rows = await conn.query("SELECT column_name, data_type FROM information_schema.columns WHERE table_name = $1", [bare]);
|
|
69
|
+
for (const row of rows) {
|
|
70
|
+
const name = row.column_name;
|
|
71
|
+
const type = row.data_type;
|
|
72
|
+
if (typeof name === "string" && typeof type === "string") {
|
|
73
|
+
types.set(name, type.toLowerCase());
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
// No catalog, no cast — see the doc above.
|
|
79
|
+
}
|
|
80
|
+
perConn.set(table, types);
|
|
81
|
+
return types;
|
|
82
|
+
}
|
|
23
83
|
/** Reject identifiers that could break out of the quoted context. */
|
|
24
84
|
function quoteIdent(name) {
|
|
25
85
|
if (!/^[A-Za-z_][A-Za-z0-9_.]*$/.test(name)) {
|
|
@@ -28,12 +88,28 @@ function quoteIdent(name) {
|
|
|
28
88
|
return `"${name.replace(/\./g, '"."')}"`;
|
|
29
89
|
}
|
|
30
90
|
async function countRows(conn, table, payload) {
|
|
91
|
+
// Postgres numbers its placeholders; MySQL and SQLite use `?`. Emitting `?`
|
|
92
|
+
// on Postgres produced "syntax error at or near AND" — the marker was never
|
|
93
|
+
// dialect-aware.
|
|
94
|
+
const isPostgres = conn.dialect === "postgres";
|
|
31
95
|
let where = "";
|
|
32
96
|
let params = [];
|
|
33
97
|
if (payload) {
|
|
34
98
|
const keys = Object.keys(payload);
|
|
35
99
|
if (keys.length > 0) {
|
|
36
|
-
|
|
100
|
+
const casts = isPostgres ? await columnTypes(conn, table) : undefined;
|
|
101
|
+
const predicates = keys.map((k, i) => {
|
|
102
|
+
if (!isPostgres)
|
|
103
|
+
return `${quoteIdent(k)} = ?`;
|
|
104
|
+
const declared = casts?.get(k);
|
|
105
|
+
const cast = declared &&
|
|
106
|
+
PG_CASTABLE.has(declared) &&
|
|
107
|
+
typeof payload[k] === "string"
|
|
108
|
+
? `::${declared}`
|
|
109
|
+
: "";
|
|
110
|
+
return `${quoteIdent(k)} = $${i + 1}${cast}`;
|
|
111
|
+
});
|
|
112
|
+
where = ` WHERE ${predicates.join(" AND ")}`;
|
|
37
113
|
params = keys.map((k) => payload[k]);
|
|
38
114
|
}
|
|
39
115
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DbAssertions.js","sourceRoot":"","sources":["../../src/testing/DbAssertions.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;
|
|
1
|
+
{"version":3,"file":"DbAssertions.js","sourceRoot":"","sources":["../../src/testing/DbAssertions.ts"],"names":[],"mappings":"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"}
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -26,6 +26,83 @@ import type { Plugin } from "@c9up/helix";
|
|
|
26
26
|
/** The minimal connection the assertions need — a parameterized query runner. */
|
|
27
27
|
export interface DbConnectionLike {
|
|
28
28
|
query(sql: string, params?: unknown[]): Promise<Record<string, unknown>[]>;
|
|
29
|
+
/**
|
|
30
|
+
* The engine this connection speaks. Absent on a hand-rolled stub, in which
|
|
31
|
+
* case the assertions fall back to `?` placeholders and emit no cast, which
|
|
32
|
+
* is what SQLite and MySQL want.
|
|
33
|
+
*/
|
|
34
|
+
dialect?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Column types Postgres will NOT compare against a text-bound parameter.
|
|
39
|
+
*
|
|
40
|
+
* A driver binds a JS string as `text`; `uuid = text` then fails with
|
|
41
|
+
* "operator does not exist". The cast is derived from the COLUMN's declared
|
|
42
|
+
* type, read from the catalog — never guessed from the value, since a text
|
|
43
|
+
* column holding a UUID-shaped string must keep comparing as text.
|
|
44
|
+
*/
|
|
45
|
+
const PG_CASTABLE = new Set([
|
|
46
|
+
"uuid",
|
|
47
|
+
"timestamp without time zone",
|
|
48
|
+
"timestamp with time zone",
|
|
49
|
+
"date",
|
|
50
|
+
"time without time zone",
|
|
51
|
+
"time with time zone",
|
|
52
|
+
"numeric",
|
|
53
|
+
"json",
|
|
54
|
+
"jsonb",
|
|
55
|
+
"inet",
|
|
56
|
+
"interval",
|
|
57
|
+
]);
|
|
58
|
+
|
|
59
|
+
/** Catalog lookups are per (table, connection) and never change mid-run. */
|
|
60
|
+
const columnTypeCache = new WeakMap<
|
|
61
|
+
DbConnectionLike,
|
|
62
|
+
Map<string, Map<string, string>>
|
|
63
|
+
>();
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* `column → declared type` for one table, from `information_schema`.
|
|
67
|
+
*
|
|
68
|
+
* A failure here is not fatal: the assertions simply emit no cast, which is
|
|
69
|
+
* exactly the pre-existing behaviour. A broken catalog read must not turn a
|
|
70
|
+
* passing assertion into an error about introspection.
|
|
71
|
+
*/
|
|
72
|
+
async function columnTypes(
|
|
73
|
+
conn: DbConnectionLike,
|
|
74
|
+
table: string,
|
|
75
|
+
): Promise<Map<string, string>> {
|
|
76
|
+
let perConn = columnTypeCache.get(conn);
|
|
77
|
+
if (!perConn) {
|
|
78
|
+
perConn = new Map();
|
|
79
|
+
columnTypeCache.set(conn, perConn);
|
|
80
|
+
}
|
|
81
|
+
const cached = perConn.get(table);
|
|
82
|
+
if (cached) return cached;
|
|
83
|
+
|
|
84
|
+
const types = new Map<string, string>();
|
|
85
|
+
try {
|
|
86
|
+
// `table` may be schema-qualified: compare on the last segment.
|
|
87
|
+
const bare = table.includes(".")
|
|
88
|
+
? table.slice(table.lastIndexOf(".") + 1)
|
|
89
|
+
: table;
|
|
90
|
+
const rows = await conn.query(
|
|
91
|
+
"SELECT column_name, data_type FROM information_schema.columns WHERE table_name = $1",
|
|
92
|
+
[bare],
|
|
93
|
+
);
|
|
94
|
+
for (const row of rows) {
|
|
95
|
+
const name = row.column_name;
|
|
96
|
+
const type = row.data_type;
|
|
97
|
+
if (typeof name === "string" && typeof type === "string") {
|
|
98
|
+
types.set(name, type.toLowerCase());
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
} catch {
|
|
102
|
+
// No catalog, no cast — see the doc above.
|
|
103
|
+
}
|
|
104
|
+
perConn.set(table, types);
|
|
105
|
+
return types;
|
|
29
106
|
}
|
|
30
107
|
|
|
31
108
|
/** Reject identifiers that could break out of the quoted context. */
|
|
@@ -41,12 +118,28 @@ async function countRows(
|
|
|
41
118
|
table: string,
|
|
42
119
|
payload?: Record<string, unknown>,
|
|
43
120
|
): Promise<number> {
|
|
121
|
+
// Postgres numbers its placeholders; MySQL and SQLite use `?`. Emitting `?`
|
|
122
|
+
// on Postgres produced "syntax error at or near AND" — the marker was never
|
|
123
|
+
// dialect-aware.
|
|
124
|
+
const isPostgres = conn.dialect === "postgres";
|
|
44
125
|
let where = "";
|
|
45
126
|
let params: unknown[] = [];
|
|
46
127
|
if (payload) {
|
|
47
128
|
const keys = Object.keys(payload);
|
|
48
129
|
if (keys.length > 0) {
|
|
49
|
-
|
|
130
|
+
const casts = isPostgres ? await columnTypes(conn, table) : undefined;
|
|
131
|
+
const predicates = keys.map((k, i) => {
|
|
132
|
+
if (!isPostgres) return `${quoteIdent(k)} = ?`;
|
|
133
|
+
const declared = casts?.get(k);
|
|
134
|
+
const cast =
|
|
135
|
+
declared &&
|
|
136
|
+
PG_CASTABLE.has(declared) &&
|
|
137
|
+
typeof payload[k] === "string"
|
|
138
|
+
? `::${declared}`
|
|
139
|
+
: "";
|
|
140
|
+
return `${quoteIdent(k)} = $${i + 1}${cast}`;
|
|
141
|
+
});
|
|
142
|
+
where = ` WHERE ${predicates.join(" AND ")}`;
|
|
50
143
|
params = keys.map((k) => payload[k]);
|
|
51
144
|
}
|
|
52
145
|
}
|