@truto/sqlite-builder 2.0.2-canary.3 → 2.0.2-canary.31
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/README.md +900 -0
- package/index.js +1 -0
- package/package.json +1 -1
- package/dist/constants.d.ts +0 -10
- package/dist/constants.d.ts.map +0 -1
- package/dist/filter.d.ts +0 -6
- package/dist/filter.d.ts.map +0 -1
- package/dist/fragment.d.ts +0 -14
- package/dist/fragment.d.ts.map +0 -1
- package/dist/index.d.ts +0 -7
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -548
- package/dist/index.js.map +0 -13
- package/dist/sql.d.ts +0 -51
- package/dist/sql.d.ts.map +0 -1
- package/dist/types.d.ts +0 -74
- package/dist/types.d.ts.map +0 -1
package/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
module.exports = {};
|
package/package.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"name":"@truto/sqlite-builder","version":"2.0.2-canary.
|
|
1
|
+
{"name":"@truto/sqlite-builder","version":"2.0.2-canary.31","description":"debug canary","license":"MIT","main":"index.js"}
|
package/dist/constants.d.ts
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Common regex patterns and limits used across the codebase
|
|
3
|
-
*/
|
|
4
|
-
export declare const MAX_QUERY_LENGTH = 102400;
|
|
5
|
-
export declare const MAX_IDENTIFIER_LENGTH = 255;
|
|
6
|
-
export declare const MAX_PATTERN_LENGTH = 1024;
|
|
7
|
-
export declare const STACKED_QUERY_REGEX: RegExp;
|
|
8
|
-
export declare const QUALIFIED_IDENTIFIER_REGEX: RegExp;
|
|
9
|
-
export declare const SIMPLE_IDENTIFIER_REGEX: RegExp;
|
|
10
|
-
//# sourceMappingURL=constants.d.ts.map
|
package/dist/constants.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AAAA;;GAEG;AAGH,eAAO,MAAM,gBAAgB,SAAS,CAAA;AAKtC,eAAO,MAAM,qBAAqB,MAAM,CAAA;AAMxC,eAAO,MAAM,kBAAkB,OAAO,CAAA;AAMtC,eAAO,MAAM,mBAAmB,QAAe,CAAA;AAG/C,eAAO,MAAM,0BAA0B,QACgB,CAAA;AAGvD,eAAO,MAAM,uBAAuB,QAA6B,CAAA"}
|
package/dist/filter.d.ts
DELETED
package/dist/filter.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"filter.d.ts","sourceRoot":"","sources":["../src/filter.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAGV,YAAY,EACZ,UAAU,EACX,MAAM,SAAS,CAAA;AAgZhB;;GAEG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,UAAU,GAAG,YAAY,CAuB9D"}
|
package/dist/fragment.d.ts
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import type { SqlFragment } from './types';
|
|
2
|
-
/**
|
|
3
|
-
* Create and register an immutable, branded SQL fragment.
|
|
4
|
-
*
|
|
5
|
-
* The returned object is frozen (including its values array) so callers cannot
|
|
6
|
-
* mutate a fragment after the integrity checks that produced it.
|
|
7
|
-
*/
|
|
8
|
-
export declare function createFragment(text: string, values: readonly unknown[]): SqlFragment;
|
|
9
|
-
/**
|
|
10
|
-
* Type guard: was this value minted by the library (and therefore trusted to
|
|
11
|
-
* contribute raw SQL text)?
|
|
12
|
-
*/
|
|
13
|
-
export declare function isSqlFragment(value: unknown): value is SqlFragment;
|
|
14
|
-
//# sourceMappingURL=fragment.d.ts.map
|
package/dist/fragment.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"fragment.d.ts","sourceRoot":"","sources":["../src/fragment.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAA;AAkB1C;;;;;GAKG;AACH,wBAAgB,cAAc,CAC5B,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,SAAS,OAAO,EAAE,GACzB,WAAW,CASb;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,WAAW,CAMlE"}
|
package/dist/index.d.ts
DELETED
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* truto-sqlite-builder - Safe, zero-dependency template-literal tag for SQLite queries
|
|
3
|
-
*/
|
|
4
|
-
export { compileFilter } from './filter';
|
|
5
|
-
export { sql } from './sql';
|
|
6
|
-
export type { FilterResult, JsonFilter, SqlQuery } from './types';
|
|
7
|
-
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAA;AACxC,OAAO,EAAE,GAAG,EAAE,MAAM,OAAO,CAAA;AAC3B,YAAY,EAAE,YAAY,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAA"}
|
package/dist/index.js
DELETED
|
@@ -1,548 +0,0 @@
|
|
|
1
|
-
// src/constants.ts
|
|
2
|
-
var MAX_QUERY_LENGTH = 102400;
|
|
3
|
-
var MAX_IDENTIFIER_LENGTH = 255;
|
|
4
|
-
var MAX_PATTERN_LENGTH = 1024;
|
|
5
|
-
var STACKED_QUERY_REGEX = /;[\s\S]*\S/;
|
|
6
|
-
var QUALIFIED_IDENTIFIER_REGEX = /^[A-Za-z_][A-Za-z0-9_]*(\.[A-Za-z_][A-Za-z0-9_]*)*$/;
|
|
7
|
-
var SIMPLE_IDENTIFIER_REGEX = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
8
|
-
|
|
9
|
-
// src/fragment.ts
|
|
10
|
-
var fragmentRegistry = new WeakSet;
|
|
11
|
-
function createFragment(text, values) {
|
|
12
|
-
const fragment = Object.freeze({
|
|
13
|
-
text,
|
|
14
|
-
values: Object.freeze([...values])
|
|
15
|
-
});
|
|
16
|
-
fragmentRegistry.add(fragment);
|
|
17
|
-
return fragment;
|
|
18
|
-
}
|
|
19
|
-
function isSqlFragment(value) {
|
|
20
|
-
return typeof value === "object" && value !== null && fragmentRegistry.has(value);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
// src/sql.ts
|
|
24
|
-
function formatDate(date) {
|
|
25
|
-
const year = date.getFullYear();
|
|
26
|
-
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
27
|
-
const day = String(date.getDate()).padStart(2, "0");
|
|
28
|
-
const hours = String(date.getHours()).padStart(2, "0");
|
|
29
|
-
const minutes = String(date.getMinutes()).padStart(2, "0");
|
|
30
|
-
const seconds = String(date.getSeconds()).padStart(2, "0");
|
|
31
|
-
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
|
|
32
|
-
}
|
|
33
|
-
function sqlValue(value) {
|
|
34
|
-
if (value === null || value === undefined) {
|
|
35
|
-
return null;
|
|
36
|
-
}
|
|
37
|
-
if (typeof value === "string") {
|
|
38
|
-
return value;
|
|
39
|
-
}
|
|
40
|
-
if (typeof value === "number" || typeof value === "boolean") {
|
|
41
|
-
return value;
|
|
42
|
-
}
|
|
43
|
-
if (value instanceof Date) {
|
|
44
|
-
return formatDate(value);
|
|
45
|
-
}
|
|
46
|
-
if (value instanceof Buffer || value instanceof Uint8Array) {
|
|
47
|
-
throw new TypeError("Buffer/Uint8Array values must be used with sql.blob() for safe BLOB handling");
|
|
48
|
-
}
|
|
49
|
-
throw new TypeError(`Unsupported value type: ${typeof value}`);
|
|
50
|
-
}
|
|
51
|
-
function quoteSingleIdentifier(identifier) {
|
|
52
|
-
if (identifier.length > MAX_IDENTIFIER_LENGTH) {
|
|
53
|
-
throw new TypeError(`Identifier part too long: ${identifier.length} characters (max: ${MAX_IDENTIFIER_LENGTH})`);
|
|
54
|
-
}
|
|
55
|
-
if (!SIMPLE_IDENTIFIER_REGEX.test(identifier)) {
|
|
56
|
-
throw new TypeError(`Invalid identifier part: ${identifier}. Must be a valid ANSI identifier.`);
|
|
57
|
-
}
|
|
58
|
-
return `"${identifier}"`;
|
|
59
|
-
}
|
|
60
|
-
function quoteQualifiedIdentifier(identifier) {
|
|
61
|
-
if (!QUALIFIED_IDENTIFIER_REGEX.test(identifier)) {
|
|
62
|
-
throw new TypeError(`Invalid identifier: ${identifier}. Must be a valid identifier or qualified identifier (e.g., table.column)`);
|
|
63
|
-
}
|
|
64
|
-
const parts = identifier.split(".");
|
|
65
|
-
return parts.map(quoteSingleIdentifier).join(".");
|
|
66
|
-
}
|
|
67
|
-
function sqlIdent(identifier) {
|
|
68
|
-
if (Array.isArray(identifier)) {
|
|
69
|
-
if (identifier.length === 0) {
|
|
70
|
-
throw new TypeError("Identifier array cannot be empty");
|
|
71
|
-
}
|
|
72
|
-
const fragments = [];
|
|
73
|
-
for (const item of identifier) {
|
|
74
|
-
if (isSqlFragment(item)) {
|
|
75
|
-
fragments.push(item);
|
|
76
|
-
} else if (typeof item === "string") {
|
|
77
|
-
if (!item) {
|
|
78
|
-
throw new TypeError("All identifiers must be non-empty strings");
|
|
79
|
-
}
|
|
80
|
-
if (!QUALIFIED_IDENTIFIER_REGEX.test(item)) {
|
|
81
|
-
throw new TypeError(`Invalid identifier: ${item}. Must be a valid identifier or qualified identifier (e.g., table.column)`);
|
|
82
|
-
}
|
|
83
|
-
fragments.push(createFragment(quoteQualifiedIdentifier(item), []));
|
|
84
|
-
} else {
|
|
85
|
-
throw new TypeError("Array items must be strings or SQL fragments");
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
const text = fragments.map((f) => f.text).join(", ");
|
|
89
|
-
const values = fragments.flatMap((f) => [...f.values]);
|
|
90
|
-
return createFragment(text, values);
|
|
91
|
-
}
|
|
92
|
-
if (!identifier || typeof identifier !== "string") {
|
|
93
|
-
throw new TypeError("Identifier must be a non-empty string");
|
|
94
|
-
}
|
|
95
|
-
if (!QUALIFIED_IDENTIFIER_REGEX.test(identifier)) {
|
|
96
|
-
throw new TypeError(`Invalid identifier: ${identifier}. Must be a valid identifier or qualified identifier (e.g., table.column)`);
|
|
97
|
-
}
|
|
98
|
-
return createFragment(quoteQualifiedIdentifier(identifier), []);
|
|
99
|
-
}
|
|
100
|
-
function sqlIn(array) {
|
|
101
|
-
if (!Array.isArray(array)) {
|
|
102
|
-
throw new TypeError("sql.in() requires an array");
|
|
103
|
-
}
|
|
104
|
-
if (array.length === 0) {
|
|
105
|
-
throw new TypeError("sql.in() cannot be used with empty arrays");
|
|
106
|
-
}
|
|
107
|
-
if (array.length > 1000) {
|
|
108
|
-
console.warn(`sql.in(): Large array with ${array.length} items. Consider using temporary tables for better performance.`);
|
|
109
|
-
}
|
|
110
|
-
const placeholders = array.map(() => "?").join(",");
|
|
111
|
-
const values = array.map(sqlValue);
|
|
112
|
-
return createFragment(`(${placeholders})`, values);
|
|
113
|
-
}
|
|
114
|
-
function sqlRaw(rawSql) {
|
|
115
|
-
if (typeof rawSql !== "string") {
|
|
116
|
-
throw new TypeError("sql.raw() requires a string");
|
|
117
|
-
}
|
|
118
|
-
return createFragment(rawSql, []);
|
|
119
|
-
}
|
|
120
|
-
function sqlBlob(data) {
|
|
121
|
-
if (!(data instanceof Buffer) && !(data instanceof Uint8Array)) {
|
|
122
|
-
throw new TypeError("sql.blob() requires a Buffer or Uint8Array");
|
|
123
|
-
}
|
|
124
|
-
return createFragment("?", [data]);
|
|
125
|
-
}
|
|
126
|
-
var SEPARATOR_FORBIDDEN_TOKENS = [
|
|
127
|
-
"'",
|
|
128
|
-
'"',
|
|
129
|
-
"`",
|
|
130
|
-
"[",
|
|
131
|
-
"]",
|
|
132
|
-
";",
|
|
133
|
-
"\\",
|
|
134
|
-
"\x00",
|
|
135
|
-
"--",
|
|
136
|
-
"/*",
|
|
137
|
-
"*/"
|
|
138
|
-
];
|
|
139
|
-
function assertSafeSeparator(separator) {
|
|
140
|
-
for (const token of SEPARATOR_FORBIDDEN_TOKENS) {
|
|
141
|
-
if (separator.includes(token)) {
|
|
142
|
-
throw new TypeError(`Unsafe sql.join() separator: contains forbidden token ${JSON.stringify(token)}. Pass a SqlFragment (e.g. sql.raw) if you need parameterized separators.`);
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
let depth = 0;
|
|
146
|
-
for (const char of separator) {
|
|
147
|
-
if (char === "(") {
|
|
148
|
-
depth++;
|
|
149
|
-
} else if (char === ")") {
|
|
150
|
-
depth--;
|
|
151
|
-
if (depth < 0) {
|
|
152
|
-
throw new TypeError("Unsafe sql.join() separator: unbalanced parentheses");
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
if (depth !== 0) {
|
|
157
|
-
throw new TypeError("Unsafe sql.join() separator: unbalanced parentheses");
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
function sqlJoin(fragments, separator = ", ") {
|
|
161
|
-
if (!Array.isArray(fragments)) {
|
|
162
|
-
throw new TypeError("sql.join() requires an array of fragments");
|
|
163
|
-
}
|
|
164
|
-
for (const fragment of fragments) {
|
|
165
|
-
if (!isSqlFragment(fragment)) {
|
|
166
|
-
throw new TypeError("sql.join() requires SQL fragments created by the sql tag or its helpers");
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
if (fragments.length === 0) {
|
|
170
|
-
return createFragment("", []);
|
|
171
|
-
}
|
|
172
|
-
let separatorText;
|
|
173
|
-
let separatorValues = [];
|
|
174
|
-
if (isSqlFragment(separator)) {
|
|
175
|
-
separatorText = separator.text;
|
|
176
|
-
separatorValues = separator.values;
|
|
177
|
-
} else if (typeof separator === "string") {
|
|
178
|
-
assertSafeSeparator(separator);
|
|
179
|
-
separatorText = separator;
|
|
180
|
-
} else {
|
|
181
|
-
throw new TypeError("sql.join() separator must be a string or a SQL fragment");
|
|
182
|
-
}
|
|
183
|
-
let text = "";
|
|
184
|
-
const values = [];
|
|
185
|
-
fragments.forEach((fragment, index) => {
|
|
186
|
-
if (index > 0) {
|
|
187
|
-
text += separatorText;
|
|
188
|
-
values.push(...separatorValues);
|
|
189
|
-
}
|
|
190
|
-
text += fragment.text;
|
|
191
|
-
values.push(...fragment.values);
|
|
192
|
-
});
|
|
193
|
-
return createFragment(text, values);
|
|
194
|
-
}
|
|
195
|
-
function scanSql(text) {
|
|
196
|
-
let placeholderCount = 0;
|
|
197
|
-
let code = "";
|
|
198
|
-
let i = 0;
|
|
199
|
-
const length = text.length;
|
|
200
|
-
while (i < length) {
|
|
201
|
-
const char = text[i];
|
|
202
|
-
const next = text[i + 1];
|
|
203
|
-
if (char === "-" && next === "-") {
|
|
204
|
-
i += 2;
|
|
205
|
-
while (i < length && text[i] !== `
|
|
206
|
-
`) {
|
|
207
|
-
i++;
|
|
208
|
-
}
|
|
209
|
-
continue;
|
|
210
|
-
}
|
|
211
|
-
if (char === "/" && next === "*") {
|
|
212
|
-
i += 2;
|
|
213
|
-
while (i < length && !(text[i] === "*" && text[i + 1] === "/")) {
|
|
214
|
-
i++;
|
|
215
|
-
}
|
|
216
|
-
i += 2;
|
|
217
|
-
continue;
|
|
218
|
-
}
|
|
219
|
-
if (char === "'" || char === '"' || char === "`") {
|
|
220
|
-
const quote = char;
|
|
221
|
-
i++;
|
|
222
|
-
while (i < length) {
|
|
223
|
-
if (text[i] === quote) {
|
|
224
|
-
if (text[i + 1] === quote) {
|
|
225
|
-
i += 2;
|
|
226
|
-
continue;
|
|
227
|
-
}
|
|
228
|
-
i++;
|
|
229
|
-
break;
|
|
230
|
-
}
|
|
231
|
-
i++;
|
|
232
|
-
}
|
|
233
|
-
continue;
|
|
234
|
-
}
|
|
235
|
-
if (char === "[") {
|
|
236
|
-
i++;
|
|
237
|
-
while (i < length && text[i] !== "]") {
|
|
238
|
-
i++;
|
|
239
|
-
}
|
|
240
|
-
i++;
|
|
241
|
-
continue;
|
|
242
|
-
}
|
|
243
|
-
if (char === "?") {
|
|
244
|
-
placeholderCount++;
|
|
245
|
-
}
|
|
246
|
-
code += char;
|
|
247
|
-
i++;
|
|
248
|
-
}
|
|
249
|
-
return { placeholderCount, code };
|
|
250
|
-
}
|
|
251
|
-
function sql(strings, ...values) {
|
|
252
|
-
let text = strings[0] || "";
|
|
253
|
-
const queryValues = [];
|
|
254
|
-
for (let i = 0;i < values.length; i++) {
|
|
255
|
-
const value = values[i];
|
|
256
|
-
if (isSqlFragment(value)) {
|
|
257
|
-
text += value.text;
|
|
258
|
-
queryValues.push(...value.values);
|
|
259
|
-
} else {
|
|
260
|
-
text += "?";
|
|
261
|
-
queryValues.push(sqlValue(value));
|
|
262
|
-
}
|
|
263
|
-
text += strings[i + 1] || "";
|
|
264
|
-
}
|
|
265
|
-
if (text.length > MAX_QUERY_LENGTH) {
|
|
266
|
-
throw new Error(`Query too long: ${text.length} bytes (max: ${MAX_QUERY_LENGTH})`);
|
|
267
|
-
}
|
|
268
|
-
const { placeholderCount, code } = scanSql(text);
|
|
269
|
-
if (placeholderCount !== queryValues.length) {
|
|
270
|
-
throw new Error(`Placeholder count (${placeholderCount}) does not match bound value count (${queryValues.length}). ` + 'Did a raw fragment contain a "?" without supplying its value?');
|
|
271
|
-
}
|
|
272
|
-
if (STACKED_QUERY_REGEX.test(code)) {
|
|
273
|
-
throw new Error("Stacked queries are not allowed");
|
|
274
|
-
}
|
|
275
|
-
return createFragment(text, queryValues);
|
|
276
|
-
}
|
|
277
|
-
sql.value = sqlValue;
|
|
278
|
-
sql.ident = sqlIdent;
|
|
279
|
-
sql.in = sqlIn;
|
|
280
|
-
sql.raw = sqlRaw;
|
|
281
|
-
sql.blob = sqlBlob;
|
|
282
|
-
sql.join = sqlJoin;
|
|
283
|
-
|
|
284
|
-
// src/filter.ts
|
|
285
|
-
var MAX_NESTING_DEPTH = 10;
|
|
286
|
-
var MAX_OPERATORS = 100;
|
|
287
|
-
var VALID_OPERATORS = new Set([
|
|
288
|
-
"gt",
|
|
289
|
-
"gte",
|
|
290
|
-
"lt",
|
|
291
|
-
"lte",
|
|
292
|
-
"ne",
|
|
293
|
-
"in",
|
|
294
|
-
"nin",
|
|
295
|
-
"like",
|
|
296
|
-
"ilike",
|
|
297
|
-
"regex",
|
|
298
|
-
"exists",
|
|
299
|
-
"and",
|
|
300
|
-
"or"
|
|
301
|
-
]);
|
|
302
|
-
function isJsonPath(field) {
|
|
303
|
-
return field.includes(".");
|
|
304
|
-
}
|
|
305
|
-
function compileJsonPath(field) {
|
|
306
|
-
const parts = field.split(".");
|
|
307
|
-
const columnName = parts[0];
|
|
308
|
-
const jsonPath = parts.slice(1);
|
|
309
|
-
if (!columnName || jsonPath.length === 0 || jsonPath.some((part) => !part)) {
|
|
310
|
-
throw new SyntaxError(`Invalid JSON path: ${field}`);
|
|
311
|
-
}
|
|
312
|
-
return {
|
|
313
|
-
columnName,
|
|
314
|
-
jsonPath: "$." + jsonPath.join(".")
|
|
315
|
-
};
|
|
316
|
-
}
|
|
317
|
-
function isValidSqlIdentifier(identifier) {
|
|
318
|
-
return identifier.length <= MAX_IDENTIFIER_LENGTH && SIMPLE_IDENTIFIER_REGEX.test(identifier);
|
|
319
|
-
}
|
|
320
|
-
function assertPatternWithinLimit(operator, pattern) {
|
|
321
|
-
if (pattern.length > MAX_PATTERN_LENGTH) {
|
|
322
|
-
throw new RangeError(`${operator} pattern too long: ${pattern.length} characters (max: ${MAX_PATTERN_LENGTH})`);
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
function compileFieldCondition(field, condition, context, alias) {
|
|
326
|
-
if (condition === null || condition === undefined || typeof condition === "string" || typeof condition === "number" || typeof condition === "boolean" || condition instanceof Date) {
|
|
327
|
-
context.operatorCount++;
|
|
328
|
-
if (context.operatorCount > MAX_OPERATORS) {
|
|
329
|
-
throw new RangeError(`Too many operators (max: ${MAX_OPERATORS})`);
|
|
330
|
-
}
|
|
331
|
-
if (isJsonPath(field)) {
|
|
332
|
-
const { columnName, jsonPath } = compileJsonPath(field);
|
|
333
|
-
const identFragment2 = sql.ident(columnName);
|
|
334
|
-
const fullFieldExpr = alias ? `${alias}.${identFragment2.text}` : identFragment2.text;
|
|
335
|
-
if (condition === null || condition === undefined) {
|
|
336
|
-
context.values.push(jsonPath);
|
|
337
|
-
return `(json_extract(${fullFieldExpr}, ?) IS NULL)`;
|
|
338
|
-
} else {
|
|
339
|
-
context.values.push(jsonPath, sql.value(condition));
|
|
340
|
-
return `(json_extract(${fullFieldExpr}, ?) = ?)`;
|
|
341
|
-
}
|
|
342
|
-
} else {
|
|
343
|
-
const identFragment2 = sql.ident(field);
|
|
344
|
-
const fullFieldExpr = alias ? `${alias}.${identFragment2.text}` : identFragment2.text;
|
|
345
|
-
if (condition === null || condition === undefined) {
|
|
346
|
-
return `(${fullFieldExpr} IS NULL)`;
|
|
347
|
-
} else {
|
|
348
|
-
context.values.push(sql.value(condition));
|
|
349
|
-
return `(${fullFieldExpr} = ?)`;
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
if (typeof condition !== "object" || condition === null) {
|
|
354
|
-
throw new TypeError("Condition must be a value or operator object");
|
|
355
|
-
}
|
|
356
|
-
const operators = condition;
|
|
357
|
-
const clauses = [];
|
|
358
|
-
for (const op of Object.keys(operators)) {
|
|
359
|
-
if (!VALID_OPERATORS.has(op)) {
|
|
360
|
-
throw new SyntaxError(`Unknown operator: ${op}`);
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
const identFragment = sql.ident(isJsonPath(field) ? compileJsonPath(field).columnName : field);
|
|
364
|
-
const baseFieldExpr = alias ? `${alias}.${identFragment.text}` : identFragment.text;
|
|
365
|
-
const fieldExpr = isJsonPath(field) ? `json_extract(${baseFieldExpr}, ?)` : baseFieldExpr;
|
|
366
|
-
if (isJsonPath(field)) {
|
|
367
|
-
context.values.push(compileJsonPath(field).jsonPath);
|
|
368
|
-
}
|
|
369
|
-
if ("exists" in operators) {
|
|
370
|
-
context.operatorCount++;
|
|
371
|
-
if (context.operatorCount > MAX_OPERATORS) {
|
|
372
|
-
throw new RangeError(`Too many operators (max: ${MAX_OPERATORS})`);
|
|
373
|
-
}
|
|
374
|
-
return operators.exists ? `(${fieldExpr} IS NOT NULL)` : `(${fieldExpr} IS NULL)`;
|
|
375
|
-
}
|
|
376
|
-
for (const [op, value] of Object.entries(operators)) {
|
|
377
|
-
context.operatorCount++;
|
|
378
|
-
if (context.operatorCount > MAX_OPERATORS) {
|
|
379
|
-
throw new RangeError(`Too many operators (max: ${MAX_OPERATORS})`);
|
|
380
|
-
}
|
|
381
|
-
switch (op) {
|
|
382
|
-
case "gt":
|
|
383
|
-
context.values.push(sql.value(value));
|
|
384
|
-
clauses.push(`${fieldExpr} > ?`);
|
|
385
|
-
break;
|
|
386
|
-
case "gte":
|
|
387
|
-
context.values.push(sql.value(value));
|
|
388
|
-
clauses.push(`${fieldExpr} >= ?`);
|
|
389
|
-
break;
|
|
390
|
-
case "lt":
|
|
391
|
-
context.values.push(sql.value(value));
|
|
392
|
-
clauses.push(`${fieldExpr} < ?`);
|
|
393
|
-
break;
|
|
394
|
-
case "lte":
|
|
395
|
-
context.values.push(sql.value(value));
|
|
396
|
-
clauses.push(`${fieldExpr} <= ?`);
|
|
397
|
-
break;
|
|
398
|
-
case "ne":
|
|
399
|
-
if (value === null || value === undefined) {
|
|
400
|
-
clauses.push(`${fieldExpr} IS NOT NULL`);
|
|
401
|
-
} else {
|
|
402
|
-
context.values.push(sql.value(value));
|
|
403
|
-
clauses.push(`${fieldExpr} <> ?`);
|
|
404
|
-
}
|
|
405
|
-
break;
|
|
406
|
-
case "in": {
|
|
407
|
-
if (!Array.isArray(value)) {
|
|
408
|
-
throw new TypeError("IN operator requires an array");
|
|
409
|
-
}
|
|
410
|
-
if (value.length === 0) {
|
|
411
|
-
throw new TypeError("IN operator cannot be used with empty arrays");
|
|
412
|
-
}
|
|
413
|
-
if (value.length > 999) {
|
|
414
|
-
throw new RangeError("IN operator cannot be used with arrays larger than 999 items");
|
|
415
|
-
}
|
|
416
|
-
const inFragment = sql.in(value);
|
|
417
|
-
context.values.push(...inFragment.values);
|
|
418
|
-
clauses.push(`${fieldExpr} IN ${inFragment.text}`);
|
|
419
|
-
break;
|
|
420
|
-
}
|
|
421
|
-
case "nin": {
|
|
422
|
-
if (!Array.isArray(value)) {
|
|
423
|
-
throw new TypeError("NIN operator requires an array");
|
|
424
|
-
}
|
|
425
|
-
if (value.length === 0) {
|
|
426
|
-
throw new TypeError("NIN operator cannot be used with empty arrays");
|
|
427
|
-
}
|
|
428
|
-
if (value.length > 999) {
|
|
429
|
-
throw new RangeError("NIN operator cannot be used with arrays larger than 999 items");
|
|
430
|
-
}
|
|
431
|
-
const ninFragment = sql.in(value);
|
|
432
|
-
context.values.push(...ninFragment.values);
|
|
433
|
-
clauses.push(`${fieldExpr} NOT IN ${ninFragment.text}`);
|
|
434
|
-
break;
|
|
435
|
-
}
|
|
436
|
-
case "like":
|
|
437
|
-
if (typeof value !== "string") {
|
|
438
|
-
throw new TypeError("LIKE operator requires a string pattern");
|
|
439
|
-
}
|
|
440
|
-
assertPatternWithinLimit("LIKE", value);
|
|
441
|
-
context.values.push(value);
|
|
442
|
-
clauses.push(`${fieldExpr} LIKE ?`);
|
|
443
|
-
break;
|
|
444
|
-
case "ilike":
|
|
445
|
-
if (typeof value !== "string") {
|
|
446
|
-
throw new TypeError("ILIKE operator requires a string pattern");
|
|
447
|
-
}
|
|
448
|
-
assertPatternWithinLimit("ILIKE", value);
|
|
449
|
-
context.values.push(value);
|
|
450
|
-
clauses.push(`${fieldExpr} LIKE ? COLLATE NOCASE`);
|
|
451
|
-
break;
|
|
452
|
-
case "regex":
|
|
453
|
-
if (typeof value !== "string") {
|
|
454
|
-
throw new TypeError("REGEX operator requires a string pattern");
|
|
455
|
-
}
|
|
456
|
-
assertPatternWithinLimit("REGEX", value);
|
|
457
|
-
context.values.push(value);
|
|
458
|
-
clauses.push(`${fieldExpr} REGEXP ?`);
|
|
459
|
-
break;
|
|
460
|
-
}
|
|
461
|
-
}
|
|
462
|
-
if (clauses.length === 0) {
|
|
463
|
-
throw new SyntaxError("Operator object must contain at least one valid operator");
|
|
464
|
-
}
|
|
465
|
-
return clauses.length === 1 ? `(${clauses[0]})` : `(${clauses.join(" AND ")})`;
|
|
466
|
-
}
|
|
467
|
-
function compileFilterRecursive(filter, context, alias) {
|
|
468
|
-
if (context.depth >= MAX_NESTING_DEPTH) {
|
|
469
|
-
throw new RangeError(`Nesting depth too deep (max: ${MAX_NESTING_DEPTH})`);
|
|
470
|
-
}
|
|
471
|
-
if (typeof filter !== "object" || filter === null) {
|
|
472
|
-
throw new TypeError("Filter must be an object");
|
|
473
|
-
}
|
|
474
|
-
context.depth++;
|
|
475
|
-
const clauses = [];
|
|
476
|
-
if ("and" in filter && filter.and) {
|
|
477
|
-
context.operatorCount++;
|
|
478
|
-
if (context.operatorCount > MAX_OPERATORS) {
|
|
479
|
-
throw new RangeError(`Too many operators (max: ${MAX_OPERATORS})`);
|
|
480
|
-
}
|
|
481
|
-
if (!Array.isArray(filter.and)) {
|
|
482
|
-
throw new TypeError("AND operator must be an array");
|
|
483
|
-
}
|
|
484
|
-
if (filter.and.length === 0) {
|
|
485
|
-
throw new TypeError("AND operator cannot be used with empty arrays");
|
|
486
|
-
}
|
|
487
|
-
const andClauses = filter.and.map((subFilter) => compileFilterRecursive(subFilter, context, alias));
|
|
488
|
-
clauses.push(`(${andClauses.join(" AND ")})`);
|
|
489
|
-
}
|
|
490
|
-
if ("or" in filter && filter.or) {
|
|
491
|
-
context.operatorCount++;
|
|
492
|
-
if (context.operatorCount > MAX_OPERATORS) {
|
|
493
|
-
throw new RangeError(`Too many operators (max: ${MAX_OPERATORS})`);
|
|
494
|
-
}
|
|
495
|
-
if (!Array.isArray(filter.or)) {
|
|
496
|
-
throw new TypeError("OR operator must be an array");
|
|
497
|
-
}
|
|
498
|
-
if (filter.or.length === 0) {
|
|
499
|
-
throw new TypeError("OR operator cannot be used with empty arrays");
|
|
500
|
-
}
|
|
501
|
-
const orClauses = filter.or.map((subFilter) => compileFilterRecursive(subFilter, context, alias));
|
|
502
|
-
clauses.push(`(${orClauses.join(" OR ")})`);
|
|
503
|
-
}
|
|
504
|
-
const fieldEntries = Object.entries(filter).filter(([field, condition]) => field !== "and" && field !== "or" && !field.startsWith("$") && condition !== undefined);
|
|
505
|
-
for (const [field, condition] of fieldEntries) {
|
|
506
|
-
if (Array.isArray(condition)) {
|
|
507
|
-
throw new SyntaxError(`Field '${field}' cannot have array value. Use logical operators 'and'/'or' instead.`);
|
|
508
|
-
}
|
|
509
|
-
clauses.push(compileFieldCondition(field, condition, context, alias));
|
|
510
|
-
}
|
|
511
|
-
const aliasEntries = Object.entries(filter).filter(([key, value]) => key.startsWith("$") && value !== undefined && typeof value === "object" && value !== null);
|
|
512
|
-
for (const [aliasKey, aliasFilter] of aliasEntries) {
|
|
513
|
-
const aliasName = aliasKey.slice(1);
|
|
514
|
-
if (!isValidSqlIdentifier(aliasName)) {
|
|
515
|
-
throw new SyntaxError(`Invalid alias identifier: ${aliasName}`);
|
|
516
|
-
}
|
|
517
|
-
const aliasClause = compileFilterRecursive(aliasFilter, context, aliasName);
|
|
518
|
-
clauses.push(aliasClause);
|
|
519
|
-
}
|
|
520
|
-
context.depth--;
|
|
521
|
-
if (clauses.length === 0) {
|
|
522
|
-
throw new SyntaxError("Filter must contain at least one condition");
|
|
523
|
-
}
|
|
524
|
-
if (clauses.length === 1) {
|
|
525
|
-
return clauses[0];
|
|
526
|
-
} else {
|
|
527
|
-
return `(${clauses.join(" AND ")})`;
|
|
528
|
-
}
|
|
529
|
-
}
|
|
530
|
-
function compileFilter(filter) {
|
|
531
|
-
const context = {
|
|
532
|
-
depth: 0,
|
|
533
|
-
operatorCount: 0,
|
|
534
|
-
values: []
|
|
535
|
-
};
|
|
536
|
-
const text = `(${compileFilterRecursive(filter, context)})`;
|
|
537
|
-
if (text.length > MAX_QUERY_LENGTH) {
|
|
538
|
-
throw new RangeError(`Compiled filter too long: ${text.length} bytes (max: ${MAX_QUERY_LENGTH})`);
|
|
539
|
-
}
|
|
540
|
-
return createFragment(text, context.values);
|
|
541
|
-
}
|
|
542
|
-
export {
|
|
543
|
-
sql,
|
|
544
|
-
compileFilter
|
|
545
|
-
};
|
|
546
|
-
|
|
547
|
-
//# debugId=B02F5A27F175BBBF64756E2164756E21
|
|
548
|
-
//# sourceMappingURL=index.js.map
|