@rapiq/sql 2.0.0-beta.0
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/LICENSE +21 -0
- package/README.md +59 -0
- package/dist/index.d.mts +580 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +876 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +66 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,876 @@
|
|
|
1
|
+
import { AdapterError, FieldOperator, Filter, FilterFieldOperator, FilterRegexFlag, Filters, ITSELF, createFilterRegex } from "@rapiq/core";
|
|
2
|
+
//#region src/helpers/name-split.ts
|
|
3
|
+
function splitFirst(input) {
|
|
4
|
+
const separatorIndex = input.indexOf(".");
|
|
5
|
+
if (separatorIndex === -1) return [input, ""];
|
|
6
|
+
return [input.substring(0, separatorIndex), input.substring(separatorIndex + 1)];
|
|
7
|
+
}
|
|
8
|
+
function splitLast(input) {
|
|
9
|
+
const separatorIndex = input.lastIndexOf(".");
|
|
10
|
+
if (separatorIndex === -1) return [input, ""];
|
|
11
|
+
return [input.substring(0, separatorIndex), input.substring(separatorIndex + 1)];
|
|
12
|
+
}
|
|
13
|
+
//#endregion
|
|
14
|
+
//#region src/helpers/relation-alias.ts
|
|
15
|
+
/**
|
|
16
|
+
* The tightest identifier limit among the shipped dialect presets:
|
|
17
|
+
* PostgreSQL truncates identifiers to 63 bytes (silently, with a
|
|
18
|
+
* NOTICE), so an alias longer than that would lose its uniqueness
|
|
19
|
+
* guarantee on the way into the database.
|
|
20
|
+
*/
|
|
21
|
+
const MAX_ALIAS_LENGTH = 63;
|
|
22
|
+
/**
|
|
23
|
+
* FNV-1a (32 bit) — deterministic, dependency-free disambiguator for
|
|
24
|
+
* aliases that exceed the identifier limit.
|
|
25
|
+
*/
|
|
26
|
+
function hashPath(input) {
|
|
27
|
+
let hash = 2166136261;
|
|
28
|
+
for (let i = 0; i < input.length; i++) {
|
|
29
|
+
hash ^= input.charCodeAt(i);
|
|
30
|
+
hash = Math.imul(hash, 16777619);
|
|
31
|
+
}
|
|
32
|
+
return (hash >>> 0).toString(36);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Default join-alias derivation: every path segment is length-prefixed
|
|
36
|
+
* (e.g. `role.realm` -> `r4_role_5_realm`). Unlike replacing dots with
|
|
37
|
+
* underscores, this is injective even when relation names contain `_`.
|
|
38
|
+
* Aliases that would exceed the database identifier limit are bounded:
|
|
39
|
+
* a readable prefix plus a hash of the full path — otherwise engines
|
|
40
|
+
* such as PostgreSQL would truncate them (silently collapsing long,
|
|
41
|
+
* distinct paths onto one alias).
|
|
42
|
+
*/
|
|
43
|
+
function buildRelationAlias(path) {
|
|
44
|
+
const alias = `r${path.split(".").map((segment) => `${segment.length}_${segment}`).join("_")}`;
|
|
45
|
+
if (alias.length <= MAX_ALIAS_LENGTH) return alias;
|
|
46
|
+
const suffix = `_h${hashPath(path)}`;
|
|
47
|
+
return `${alias.substring(0, MAX_ALIAS_LENGTH - suffix.length)}${suffix}`;
|
|
48
|
+
}
|
|
49
|
+
//#endregion
|
|
50
|
+
//#region src/helpers/field.ts
|
|
51
|
+
function parseField(input, rootAlias, relationAlias = buildRelationAlias) {
|
|
52
|
+
const [relation, name] = splitLast(input);
|
|
53
|
+
if (!name) {
|
|
54
|
+
if (rootAlias) return {
|
|
55
|
+
prefix: rootAlias,
|
|
56
|
+
name: relation
|
|
57
|
+
};
|
|
58
|
+
return { name: relation };
|
|
59
|
+
}
|
|
60
|
+
return {
|
|
61
|
+
relation,
|
|
62
|
+
prefix: relationAlias(relation),
|
|
63
|
+
name
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
//#endregion
|
|
67
|
+
//#region src/helpers/like.ts
|
|
68
|
+
/**
|
|
69
|
+
* Escape LIKE pattern wildcards (incl. the MSSQL bracket range)
|
|
70
|
+
* so user input matches literally under `ESCAPE '\'`.
|
|
71
|
+
*/
|
|
72
|
+
function escapeLikePattern(input) {
|
|
73
|
+
return input.replace(/[\\%_[]/g, (character) => `\\${character}`);
|
|
74
|
+
}
|
|
75
|
+
//#endregion
|
|
76
|
+
//#region src/helpers/param-placeholder-indexer.ts
|
|
77
|
+
var ParamPlaceholderIndexer = class {
|
|
78
|
+
index;
|
|
79
|
+
constructor(index) {
|
|
80
|
+
this.index = index ?? 0;
|
|
81
|
+
}
|
|
82
|
+
next() {
|
|
83
|
+
this.index++;
|
|
84
|
+
return this.index;
|
|
85
|
+
}
|
|
86
|
+
reset() {
|
|
87
|
+
this.index = 0;
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
//#endregion
|
|
91
|
+
//#region src/helpers/root-alias.ts
|
|
92
|
+
function toRootAliasFn(input) {
|
|
93
|
+
if (typeof input === "function") return input;
|
|
94
|
+
return () => input;
|
|
95
|
+
}
|
|
96
|
+
function isRootAliasFn(input) {
|
|
97
|
+
return typeof input === "function";
|
|
98
|
+
}
|
|
99
|
+
//#endregion
|
|
100
|
+
//#region src/adapter/fields/base.ts
|
|
101
|
+
var FieldsBaseAdapter = class {
|
|
102
|
+
relations;
|
|
103
|
+
/**
|
|
104
|
+
* selection fields.
|
|
105
|
+
*
|
|
106
|
+
* e.g. ['name', 'project.id']
|
|
107
|
+
*/
|
|
108
|
+
value;
|
|
109
|
+
constructor(relations) {
|
|
110
|
+
this.relations = relations;
|
|
111
|
+
this.value = [];
|
|
112
|
+
}
|
|
113
|
+
clear() {
|
|
114
|
+
this.value = [];
|
|
115
|
+
}
|
|
116
|
+
add(input, operator) {
|
|
117
|
+
const name = this.buildField(input);
|
|
118
|
+
this.value.push({
|
|
119
|
+
name,
|
|
120
|
+
operator
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Escaped selection columns (excluded fields are dropped).
|
|
125
|
+
*/
|
|
126
|
+
getColumns() {
|
|
127
|
+
const output = [];
|
|
128
|
+
for (let i = 0; i < this.value.length; i++) {
|
|
129
|
+
const element = this.value[i];
|
|
130
|
+
if (element.operator === FieldOperator.EXCLUDE) continue;
|
|
131
|
+
output.push(element.name);
|
|
132
|
+
}
|
|
133
|
+
return output;
|
|
134
|
+
}
|
|
135
|
+
buildField(input) {
|
|
136
|
+
const output = parseField(input, this.rootAlias(), (path) => this.relations.buildAlias(path));
|
|
137
|
+
if (output.relation) this.relations.add(output.relation);
|
|
138
|
+
if (output.prefix) return `${this.escapeField(output.prefix)}.${this.escapeField(output.name)}`;
|
|
139
|
+
return this.escapeField(output.name);
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
//#endregion
|
|
143
|
+
//#region src/adapter/fields/module.ts
|
|
144
|
+
var FieldsAdapter = class extends FieldsBaseAdapter {
|
|
145
|
+
options;
|
|
146
|
+
constructor(relations, options) {
|
|
147
|
+
super(relations);
|
|
148
|
+
this.options = options;
|
|
149
|
+
}
|
|
150
|
+
escapeField(field) {
|
|
151
|
+
if (this.options.escapeField) return this.options.escapeField(field);
|
|
152
|
+
return field;
|
|
153
|
+
}
|
|
154
|
+
rootAlias() {
|
|
155
|
+
if (this.options.rootAlias) return this.options.rootAlias;
|
|
156
|
+
}
|
|
157
|
+
execute() {}
|
|
158
|
+
};
|
|
159
|
+
//#endregion
|
|
160
|
+
//#region src/adapter/filters/base.ts
|
|
161
|
+
var FiltersBaseAdapter = class {
|
|
162
|
+
/**
|
|
163
|
+
* where conditions
|
|
164
|
+
*
|
|
165
|
+
* e.g. ['"foo.bar" = "1"' ]
|
|
166
|
+
*/
|
|
167
|
+
conditions;
|
|
168
|
+
params;
|
|
169
|
+
relations;
|
|
170
|
+
paramPlaceholderIndexer;
|
|
171
|
+
fieldPrefix;
|
|
172
|
+
constructor(relations) {
|
|
173
|
+
this.conditions = [];
|
|
174
|
+
this.params = [];
|
|
175
|
+
this.relations = relations;
|
|
176
|
+
this.paramPlaceholderIndexer = new ParamPlaceholderIndexer();
|
|
177
|
+
this.fieldPrefix = "";
|
|
178
|
+
}
|
|
179
|
+
clear() {
|
|
180
|
+
this.conditions = [];
|
|
181
|
+
this.params = [];
|
|
182
|
+
this.paramPlaceholderIndexer.reset();
|
|
183
|
+
this.fieldPrefix = "";
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Whether the dialect can build regular-expression conditions.
|
|
187
|
+
* Anchored operators fall back to LIKE otherwise.
|
|
188
|
+
*/
|
|
189
|
+
isRegexpSupported() {
|
|
190
|
+
return true;
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Fold an expression for a case-insensitive equality comparison
|
|
194
|
+
* (eq/ne/in/nin on strings). Dialects whose plain `=` already
|
|
195
|
+
* compares case-insensitively return the input unchanged.
|
|
196
|
+
*/
|
|
197
|
+
caseFold(input) {
|
|
198
|
+
return `lower(${input})`;
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Whether equality comparisons on this field may case-fold at all.
|
|
202
|
+
* Backends with column metadata override this to exempt non-string
|
|
203
|
+
* columns — folding them is wasted work at best and a type error at
|
|
204
|
+
* worst (e.g. `lower(integer)` on postgres).
|
|
205
|
+
*/
|
|
206
|
+
isCaseFoldable(_field) {
|
|
207
|
+
return true;
|
|
208
|
+
}
|
|
209
|
+
where(field, operator, value) {
|
|
210
|
+
return this.whereRaw(`${this.buildField(field)} ${operator} ${this.buildParamPlaceholder()}`, value);
|
|
211
|
+
}
|
|
212
|
+
whereRaw(sql, ...values) {
|
|
213
|
+
this.conditions.push(sql);
|
|
214
|
+
if (values) this.params.push(...values);
|
|
215
|
+
return this;
|
|
216
|
+
}
|
|
217
|
+
buildParamPlaceholder() {
|
|
218
|
+
return this.paramPlaceholder(this.paramPlaceholderIndexer.next());
|
|
219
|
+
}
|
|
220
|
+
buildParamsPlaceholders(input) {
|
|
221
|
+
return input.map((_) => this.paramPlaceholder(this.paramPlaceholderIndexer.next()));
|
|
222
|
+
}
|
|
223
|
+
buildField(input) {
|
|
224
|
+
let inputNormalized;
|
|
225
|
+
if (this.fieldPrefix) inputNormalized = this.fieldPrefix + input;
|
|
226
|
+
else inputNormalized = input;
|
|
227
|
+
if (inputNormalized.split(".").includes(ITSELF)) throw AdapterError.featureUnsupported("filters:itself");
|
|
228
|
+
const output = parseField(inputNormalized, this.rootAlias(), (path) => this.relations.buildAlias(path));
|
|
229
|
+
if (output.relation) this.relations.add(output.relation);
|
|
230
|
+
if (output.prefix) return `${this.escapeField(output.prefix)}.${this.escapeField(output.name)}`;
|
|
231
|
+
return this.escapeField(output.name);
|
|
232
|
+
}
|
|
233
|
+
merge(query, operator = "and", isInverted = false) {
|
|
234
|
+
if (query.conditions.length > 0) {
|
|
235
|
+
let sql = query.conditions.join(` ${operator} `);
|
|
236
|
+
if (query.conditions.length > 1) sql = `(${sql})`;
|
|
237
|
+
this.conditions.push(`${isInverted ? "not " : ""}${sql}`);
|
|
238
|
+
this.params.push(...query.params);
|
|
239
|
+
}
|
|
240
|
+
return this;
|
|
241
|
+
}
|
|
242
|
+
setChildAttributes(child) {
|
|
243
|
+
child.setFieldPrefix(this.fieldPrefix);
|
|
244
|
+
child.setLastPlaceholderIndexer(this.paramPlaceholderIndexer);
|
|
245
|
+
return child;
|
|
246
|
+
}
|
|
247
|
+
setLastPlaceholderIndexer(input) {
|
|
248
|
+
this.paramPlaceholderIndexer = input;
|
|
249
|
+
}
|
|
250
|
+
getFieldPrefix() {
|
|
251
|
+
return this.fieldPrefix;
|
|
252
|
+
}
|
|
253
|
+
setFieldPrefix(prefix) {
|
|
254
|
+
this.fieldPrefix = prefix;
|
|
255
|
+
}
|
|
256
|
+
getQuery() {
|
|
257
|
+
return this.conditions.join(" and ");
|
|
258
|
+
}
|
|
259
|
+
getQueryAndParameters() {
|
|
260
|
+
return [this.getQuery(), this.params];
|
|
261
|
+
}
|
|
262
|
+
};
|
|
263
|
+
//#endregion
|
|
264
|
+
//#region src/adapter/filters/module.ts
|
|
265
|
+
var FiltersAdapter = class FiltersAdapter extends FiltersBaseAdapter {
|
|
266
|
+
options;
|
|
267
|
+
constructor(relations, options) {
|
|
268
|
+
super(relations);
|
|
269
|
+
this.options = options;
|
|
270
|
+
}
|
|
271
|
+
rootAlias() {
|
|
272
|
+
return this.options.rootAlias;
|
|
273
|
+
}
|
|
274
|
+
paramPlaceholder(index) {
|
|
275
|
+
return this.options.paramPlaceholder(index);
|
|
276
|
+
}
|
|
277
|
+
escapeField(field) {
|
|
278
|
+
return this.options.escapeField(field);
|
|
279
|
+
}
|
|
280
|
+
isRegexpSupported() {
|
|
281
|
+
return typeof this.options.regexp !== "undefined";
|
|
282
|
+
}
|
|
283
|
+
regexp(field, placeholder, ignoreCase) {
|
|
284
|
+
if (!this.options.regexp) throw AdapterError.featureUnsupported("regexp");
|
|
285
|
+
return this.options.regexp(field, placeholder, ignoreCase);
|
|
286
|
+
}
|
|
287
|
+
caseFold(input) {
|
|
288
|
+
if (this.options.caseFold) return this.options.caseFold(input);
|
|
289
|
+
return super.caseFold(input);
|
|
290
|
+
}
|
|
291
|
+
child() {
|
|
292
|
+
const child = new FiltersAdapter(this.relations, this.options);
|
|
293
|
+
this.setChildAttributes(child);
|
|
294
|
+
return child;
|
|
295
|
+
}
|
|
296
|
+
execute() {}
|
|
297
|
+
};
|
|
298
|
+
//#endregion
|
|
299
|
+
//#region src/visitor/fields.ts
|
|
300
|
+
var FieldsVisitor = class {
|
|
301
|
+
adapter;
|
|
302
|
+
options = {};
|
|
303
|
+
constructor(adapter, options = {}) {
|
|
304
|
+
this.adapter = adapter;
|
|
305
|
+
this.options = options;
|
|
306
|
+
}
|
|
307
|
+
visitField(expr) {
|
|
308
|
+
this.adapter.add(expr.name, expr.operator);
|
|
309
|
+
return this.adapter;
|
|
310
|
+
}
|
|
311
|
+
visitFields(expr) {
|
|
312
|
+
for (const item of expr.value) item.accept(this);
|
|
313
|
+
return this.adapter;
|
|
314
|
+
}
|
|
315
|
+
};
|
|
316
|
+
//#endregion
|
|
317
|
+
//#region src/visitor/filters.ts
|
|
318
|
+
var FiltersVisitor = class FiltersVisitor {
|
|
319
|
+
adapter;
|
|
320
|
+
options;
|
|
321
|
+
caseSensitiveFields;
|
|
322
|
+
constructor(adapter, options = {}) {
|
|
323
|
+
this.adapter = adapter;
|
|
324
|
+
this.options = options;
|
|
325
|
+
this.caseSensitiveFields = new Set(options.caseSensitive || []);
|
|
326
|
+
}
|
|
327
|
+
visitFilterEqual(expr) {
|
|
328
|
+
if (expr.value === null) return this.adapter.whereRaw(`${this.adapter.buildField(expr.field)} is null`);
|
|
329
|
+
if (this.isCaseInsensitive(expr.field, expr.value)) {
|
|
330
|
+
const field = this.adapter.buildField(expr.field);
|
|
331
|
+
return this.adapter.whereRaw(`${this.adapter.caseFold(field)} = ${this.adapter.caseFold(this.adapter.buildParamPlaceholder())}`, expr.value);
|
|
332
|
+
}
|
|
333
|
+
return this.adapter.where(expr.field, "=", expr.value);
|
|
334
|
+
}
|
|
335
|
+
visitFilterNotEqual(expr) {
|
|
336
|
+
const field = this.adapter.buildField(expr.field);
|
|
337
|
+
if (expr.value === null) return this.adapter.whereRaw(`${field} is not null`);
|
|
338
|
+
if (this.isCaseInsensitive(expr.field, expr.value)) return this.whereComplement(field, `${this.adapter.caseFold(field)} <> ${this.adapter.caseFold(this.adapter.buildParamPlaceholder())}`, expr.value);
|
|
339
|
+
return this.whereComplement(field, `${field} <> ${this.adapter.buildParamPlaceholder()}`, expr.value);
|
|
340
|
+
}
|
|
341
|
+
visitFilterLessThan(expr) {
|
|
342
|
+
return this.adapter.where(expr.field, "<", expr.value);
|
|
343
|
+
}
|
|
344
|
+
visitFilterLessThanEqual(expr) {
|
|
345
|
+
return this.adapter.where(expr.field, "<=", expr.value);
|
|
346
|
+
}
|
|
347
|
+
visitFilterGreaterThan(expr) {
|
|
348
|
+
return this.adapter.where(expr.field, ">", expr.value);
|
|
349
|
+
}
|
|
350
|
+
visitFilterGreaterThanEqual(expr) {
|
|
351
|
+
return this.adapter.where(expr.field, ">=", expr.value);
|
|
352
|
+
}
|
|
353
|
+
visitFilterExists(expr) {
|
|
354
|
+
return this.adapter.whereRaw(`${this.adapter.buildField(expr.field)} is ${expr.value ? "not " : ""}null`);
|
|
355
|
+
}
|
|
356
|
+
visitFilterIn(expr) {
|
|
357
|
+
return this.whereIn(expr.field, expr.value, false);
|
|
358
|
+
}
|
|
359
|
+
visitFilterNotIn(expr) {
|
|
360
|
+
return this.whereIn(expr.field, expr.value, true);
|
|
361
|
+
}
|
|
362
|
+
visitFilterMod(expr) {
|
|
363
|
+
const params = this.adapter.buildParamsPlaceholders(expr.value);
|
|
364
|
+
const sql = `mod(${this.adapter.buildField(expr.field)}, ${params[0]}) = ${params[1]}`;
|
|
365
|
+
return this.adapter.whereRaw(sql, ...expr.value);
|
|
366
|
+
}
|
|
367
|
+
visitFilterSize() {
|
|
368
|
+
throw AdapterError.featureUnsupported("filters:size");
|
|
369
|
+
}
|
|
370
|
+
visitFilterElemMatch(expr) {
|
|
371
|
+
const oldPrefix = this.adapter.getFieldPrefix();
|
|
372
|
+
this.adapter.setFieldPrefix(`${oldPrefix}${expr.field}.`);
|
|
373
|
+
expr.value.accept(this);
|
|
374
|
+
this.adapter.setFieldPrefix(oldPrefix);
|
|
375
|
+
return this.adapter;
|
|
376
|
+
}
|
|
377
|
+
visitFilterStartsWith(expr) {
|
|
378
|
+
return this.whereAnchored(expr.field, expr.value, FilterRegexFlag.STARTS_WITH);
|
|
379
|
+
}
|
|
380
|
+
visitFilterNotStartsWith(expr) {
|
|
381
|
+
return this.whereAnchored(expr.field, expr.value, FilterRegexFlag.STARTS_WITH | FilterRegexFlag.NEGATION);
|
|
382
|
+
}
|
|
383
|
+
visitFilterEndsWith(expr) {
|
|
384
|
+
return this.whereAnchored(expr.field, expr.value, FilterRegexFlag.ENDS_WITH);
|
|
385
|
+
}
|
|
386
|
+
visitFilterNotEndsWith(expr) {
|
|
387
|
+
return this.whereAnchored(expr.field, expr.value, FilterRegexFlag.ENDS_WITH | FilterRegexFlag.NEGATION);
|
|
388
|
+
}
|
|
389
|
+
visitFilterContains(expr) {
|
|
390
|
+
return this.whereAnchored(expr.field, expr.value, FilterRegexFlag.CONTAINS);
|
|
391
|
+
}
|
|
392
|
+
visitFilterNotContains(expr) {
|
|
393
|
+
return this.whereAnchored(expr.field, expr.value, FilterRegexFlag.CONTAINS | FilterRegexFlag.NEGATION);
|
|
394
|
+
}
|
|
395
|
+
visitFilterRegex(expr) {
|
|
396
|
+
const isRegExp = expr.value instanceof RegExp;
|
|
397
|
+
if (!isRegExp && typeof expr.value !== "string") throw AdapterError.featureUnsupported("filters:regex:value");
|
|
398
|
+
const source = isRegExp ? expr.value.source : expr.value;
|
|
399
|
+
const ignoreCase = isRegExp ? expr.value.ignoreCase : false;
|
|
400
|
+
const sql = this.adapter.regexp(this.adapter.buildField(expr.field), this.adapter.buildParamPlaceholder(), ignoreCase);
|
|
401
|
+
return this.adapter.whereRaw(sql, source);
|
|
402
|
+
}
|
|
403
|
+
visitFilters(expr) {
|
|
404
|
+
const adapter = this.adapter.child();
|
|
405
|
+
const visitor = new FiltersVisitor(adapter, this.options);
|
|
406
|
+
for (let i = 0; i < expr.value.length; i++) {
|
|
407
|
+
const child = expr.value[i];
|
|
408
|
+
if (child instanceof Filter) child.accept(visitor);
|
|
409
|
+
if (child instanceof Filters) child.accept(visitor);
|
|
410
|
+
}
|
|
411
|
+
switch (expr.operator) {
|
|
412
|
+
case "or":
|
|
413
|
+
this.adapter.merge(adapter, "or");
|
|
414
|
+
break;
|
|
415
|
+
case "nor":
|
|
416
|
+
this.adapter.merge(adapter, "or", true);
|
|
417
|
+
break;
|
|
418
|
+
case "not":
|
|
419
|
+
this.adapter.merge(adapter, "and", true);
|
|
420
|
+
break;
|
|
421
|
+
default: this.adapter.merge(adapter, "and");
|
|
422
|
+
}
|
|
423
|
+
return adapter;
|
|
424
|
+
}
|
|
425
|
+
visitFilter(expr) {
|
|
426
|
+
throw AdapterError.operatorUnsupported(expr.operator);
|
|
427
|
+
}
|
|
428
|
+
/**
|
|
429
|
+
* Render an IN/NOT IN condition.
|
|
430
|
+
* A null element also matches the absence of a value (IS NULL);
|
|
431
|
+
* an empty list matches no row (every row when negated).
|
|
432
|
+
*/
|
|
433
|
+
whereIn(fieldName, input, negated) {
|
|
434
|
+
if (input.length === 0) return this.adapter.whereRaw(negated ? "1 = 1" : "1 = 0");
|
|
435
|
+
const values = input.filter((value) => value !== null);
|
|
436
|
+
const field = this.adapter.buildField(fieldName);
|
|
437
|
+
const nullCondition = `${field} is ${negated ? "not " : ""}null`;
|
|
438
|
+
if (values.length === 0) return this.adapter.whereRaw(nullCondition);
|
|
439
|
+
let inCondition;
|
|
440
|
+
if (this.isCaseFoldableField(fieldName) && values.some((value) => typeof value === "string")) {
|
|
441
|
+
const placeholders = values.map((value) => {
|
|
442
|
+
const placeholder = this.adapter.buildParamPlaceholder();
|
|
443
|
+
return typeof value === "string" ? this.adapter.caseFold(placeholder) : placeholder;
|
|
444
|
+
});
|
|
445
|
+
inCondition = `${this.adapter.caseFold(field)} ${negated ? "not " : ""}in(${placeholders.join(", ")})`;
|
|
446
|
+
} else inCondition = `${field} ${negated ? "not " : ""}in(${this.adapter.buildParamsPlaceholders(values).join(", ")})`;
|
|
447
|
+
if (values.length === input.length) {
|
|
448
|
+
if (negated) return this.whereComplement(field, inCondition, ...values);
|
|
449
|
+
return this.adapter.whereRaw(inCondition, ...values);
|
|
450
|
+
}
|
|
451
|
+
return this.adapter.whereRaw(`(${inCondition} ${negated ? "and" : "or"} ${nullCondition})`, ...values);
|
|
452
|
+
}
|
|
453
|
+
/**
|
|
454
|
+
* Render a startsWith/endsWith/contains condition (or its negation)
|
|
455
|
+
* as a regexp condition, falling back to LIKE on regexp-less dialects.
|
|
456
|
+
*/
|
|
457
|
+
whereAnchored(field, value, flag) {
|
|
458
|
+
if (!this.adapter.isRegexpSupported()) {
|
|
459
|
+
const escaped = escapeLikePattern(`${value}`);
|
|
460
|
+
let pattern;
|
|
461
|
+
if (flag & FilterRegexFlag.STARTS_WITH) pattern = `${escaped}%`;
|
|
462
|
+
else if (flag & FilterRegexFlag.ENDS_WITH) pattern = `%${escaped}`;
|
|
463
|
+
else pattern = `%${escaped}%`;
|
|
464
|
+
return this.whereLike(field, pattern, !!(flag & FilterRegexFlag.NEGATION));
|
|
465
|
+
}
|
|
466
|
+
const regex = createFilterRegex(`${value}`, flag);
|
|
467
|
+
if (!(flag & FilterRegexFlag.NEGATION)) return this.visitFilterRegex(new Filter(FilterFieldOperator.REGEX, field, regex));
|
|
468
|
+
const fieldBuilt = this.adapter.buildField(field);
|
|
469
|
+
const sql = this.adapter.regexp(fieldBuilt, this.adapter.buildParamPlaceholder(), regex.ignoreCase);
|
|
470
|
+
return this.whereComplement(fieldBuilt, sql, regex.source);
|
|
471
|
+
}
|
|
472
|
+
whereLike(field, pattern, negated = false) {
|
|
473
|
+
const fieldBuilt = this.adapter.buildField(field);
|
|
474
|
+
const condition = `${fieldBuilt} ${negated ? "not " : ""}like ${this.adapter.buildParamPlaceholder()} escape '\\'`;
|
|
475
|
+
if (negated) return this.whereComplement(fieldBuilt, condition, pattern);
|
|
476
|
+
return this.adapter.whereRaw(condition, pattern);
|
|
477
|
+
}
|
|
478
|
+
/**
|
|
479
|
+
* Render a negated condition null-inclusively. Negated operators are
|
|
480
|
+
* exact complements of their positive twins (complement law), but the
|
|
481
|
+
* bare SQL negation follows three-valued logic and drops NULL rows.
|
|
482
|
+
*/
|
|
483
|
+
whereComplement(field, sql, ...values) {
|
|
484
|
+
return this.adapter.whereRaw(`(${sql} or ${field} is null)`, ...values);
|
|
485
|
+
}
|
|
486
|
+
/**
|
|
487
|
+
* Equality-family comparisons (eq/ne/in/nin) match string values
|
|
488
|
+
* case-insensitively unless the field is opted out via the
|
|
489
|
+
* `caseSensitive` visitor option or the adapter exempts it
|
|
490
|
+
* (non-string columns never fold).
|
|
491
|
+
*/
|
|
492
|
+
isCaseInsensitive(field, value) {
|
|
493
|
+
return typeof value === "string" && this.isCaseFoldableField(field);
|
|
494
|
+
}
|
|
495
|
+
isCaseFoldableField(field) {
|
|
496
|
+
const path = `${this.adapter.getFieldPrefix()}${field}`;
|
|
497
|
+
return !this.caseSensitiveFields.has(path) && this.adapter.isCaseFoldable(path);
|
|
498
|
+
}
|
|
499
|
+
};
|
|
500
|
+
//#endregion
|
|
501
|
+
//#region src/visitor/pagination.ts
|
|
502
|
+
var PaginationVisitor = class {
|
|
503
|
+
adapter;
|
|
504
|
+
options = {};
|
|
505
|
+
constructor(adapter, options = {}) {
|
|
506
|
+
this.adapter = adapter;
|
|
507
|
+
this.options = options;
|
|
508
|
+
}
|
|
509
|
+
visitPagination(expr) {
|
|
510
|
+
this.adapter.setLimit(expr.limit);
|
|
511
|
+
this.adapter.setOffset(expr.offset);
|
|
512
|
+
return this.adapter;
|
|
513
|
+
}
|
|
514
|
+
};
|
|
515
|
+
//#endregion
|
|
516
|
+
//#region src/visitor/relations.ts
|
|
517
|
+
var RelationsVisitor = class {
|
|
518
|
+
adapter;
|
|
519
|
+
options = {};
|
|
520
|
+
constructor(adapter, options = {}) {
|
|
521
|
+
this.adapter = adapter;
|
|
522
|
+
this.options = options;
|
|
523
|
+
}
|
|
524
|
+
visitRelation(expr) {
|
|
525
|
+
this.adapter.add(expr.name);
|
|
526
|
+
return this.adapter;
|
|
527
|
+
}
|
|
528
|
+
visitRelations(expr) {
|
|
529
|
+
for (const item of expr.value) item.accept(this);
|
|
530
|
+
return this.adapter;
|
|
531
|
+
}
|
|
532
|
+
};
|
|
533
|
+
//#endregion
|
|
534
|
+
//#region src/visitor/sort.ts
|
|
535
|
+
var SortsVisitor = class {
|
|
536
|
+
adapter;
|
|
537
|
+
options;
|
|
538
|
+
constructor(adapter, options = {}) {
|
|
539
|
+
this.adapter = adapter;
|
|
540
|
+
this.options = options;
|
|
541
|
+
}
|
|
542
|
+
visitSort(expr) {
|
|
543
|
+
this.adapter.add(expr.name, expr.operator);
|
|
544
|
+
return this.adapter;
|
|
545
|
+
}
|
|
546
|
+
visitSorts(expr) {
|
|
547
|
+
for (const item of expr.value) item.accept(this);
|
|
548
|
+
return this.adapter;
|
|
549
|
+
}
|
|
550
|
+
};
|
|
551
|
+
//#endregion
|
|
552
|
+
//#region src/visitor/module.ts
|
|
553
|
+
var QueryVisitor = class {
|
|
554
|
+
container;
|
|
555
|
+
options;
|
|
556
|
+
fields;
|
|
557
|
+
filters;
|
|
558
|
+
pagination;
|
|
559
|
+
relations;
|
|
560
|
+
sorts;
|
|
561
|
+
constructor(adapter, options = {}) {
|
|
562
|
+
this.container = adapter;
|
|
563
|
+
this.options = options;
|
|
564
|
+
this.fields = new FieldsVisitor(adapter.fields, options);
|
|
565
|
+
this.filters = new FiltersVisitor(adapter.filters, options);
|
|
566
|
+
this.pagination = new PaginationVisitor(adapter.pagination, options);
|
|
567
|
+
this.relations = new RelationsVisitor(adapter.relations, options);
|
|
568
|
+
this.sorts = new SortsVisitor(adapter.sort, options);
|
|
569
|
+
}
|
|
570
|
+
visitQuery(input) {
|
|
571
|
+
input.relations.accept(this.relations);
|
|
572
|
+
input.fields.accept(this.fields);
|
|
573
|
+
input.filters.accept(this.filters);
|
|
574
|
+
input.pagination.accept(this.pagination);
|
|
575
|
+
input.sorts.accept(this.sorts);
|
|
576
|
+
return this.container;
|
|
577
|
+
}
|
|
578
|
+
visitFields(expr) {
|
|
579
|
+
return expr.accept(this.fields);
|
|
580
|
+
}
|
|
581
|
+
visitField(expr) {
|
|
582
|
+
return expr.accept(this.fields);
|
|
583
|
+
}
|
|
584
|
+
visitFilters(expr) {
|
|
585
|
+
return expr.accept(this.filters);
|
|
586
|
+
}
|
|
587
|
+
visitFilter(expr) {
|
|
588
|
+
return expr.accept(this.filters);
|
|
589
|
+
}
|
|
590
|
+
visitPagination(expr) {
|
|
591
|
+
return expr.accept(this.pagination);
|
|
592
|
+
}
|
|
593
|
+
visitRelations(input) {
|
|
594
|
+
return input.accept(this.relations);
|
|
595
|
+
}
|
|
596
|
+
visitRelation(expr) {
|
|
597
|
+
return expr.accept(this.relations);
|
|
598
|
+
}
|
|
599
|
+
visitSorts(expr) {
|
|
600
|
+
return expr.accept(this.sorts);
|
|
601
|
+
}
|
|
602
|
+
visitSort(expr) {
|
|
603
|
+
return expr.accept(this.sorts);
|
|
604
|
+
}
|
|
605
|
+
};
|
|
606
|
+
//#endregion
|
|
607
|
+
//#region src/adapter/pagination/base.ts
|
|
608
|
+
var PaginationBaseAdapter = class {
|
|
609
|
+
limit;
|
|
610
|
+
offset;
|
|
611
|
+
clear() {
|
|
612
|
+
this.limit = void 0;
|
|
613
|
+
this.offset = void 0;
|
|
614
|
+
}
|
|
615
|
+
setLimit(limit) {
|
|
616
|
+
this.limit = limit;
|
|
617
|
+
}
|
|
618
|
+
setOffset(offset) {
|
|
619
|
+
this.offset = offset;
|
|
620
|
+
}
|
|
621
|
+
};
|
|
622
|
+
//#endregion
|
|
623
|
+
//#region src/adapter/pagination/module.ts
|
|
624
|
+
var PaginationAdapter = class extends PaginationBaseAdapter {
|
|
625
|
+
execute() {}
|
|
626
|
+
};
|
|
627
|
+
//#endregion
|
|
628
|
+
//#region src/adapter/relations/base.ts
|
|
629
|
+
var RelationsBaseAdapter = class {
|
|
630
|
+
/**
|
|
631
|
+
* joins
|
|
632
|
+
*
|
|
633
|
+
* [alias].project.user.name
|
|
634
|
+
*
|
|
635
|
+
* JOIN xxx.yyy as yyy
|
|
636
|
+
*
|
|
637
|
+
* { path: "xxx.zzz", name: "zzz" }[]
|
|
638
|
+
* @protected
|
|
639
|
+
*/
|
|
640
|
+
value;
|
|
641
|
+
relationAlias;
|
|
642
|
+
constructor(options = {}) {
|
|
643
|
+
this.value = [];
|
|
644
|
+
this.relationAlias = options.relationAlias ?? buildRelationAlias;
|
|
645
|
+
}
|
|
646
|
+
/**
|
|
647
|
+
* Join alias for a relation path (e.g. `role.realm` ->
|
|
648
|
+
* `r4_role_5_realm`).
|
|
649
|
+
* The single derivation point shared by join application and the
|
|
650
|
+
* field references built by the fields/filters/sort adapters.
|
|
651
|
+
*/
|
|
652
|
+
buildAlias(path) {
|
|
653
|
+
return this.relationAlias(path);
|
|
654
|
+
}
|
|
655
|
+
clear() {
|
|
656
|
+
this.value = [];
|
|
657
|
+
}
|
|
658
|
+
add(relation) {
|
|
659
|
+
if (this.has(relation)) return true;
|
|
660
|
+
let path;
|
|
661
|
+
let last = relation;
|
|
662
|
+
while (last) {
|
|
663
|
+
let relationName;
|
|
664
|
+
[relationName, last] = splitFirst(last);
|
|
665
|
+
path = path ? `${path}.${relationName}` : relationName;
|
|
666
|
+
if (this.has(path)) continue;
|
|
667
|
+
this.value.push({
|
|
668
|
+
path,
|
|
669
|
+
name: relationName
|
|
670
|
+
});
|
|
671
|
+
}
|
|
672
|
+
return true;
|
|
673
|
+
}
|
|
674
|
+
has(name) {
|
|
675
|
+
return this.value.some((join) => join.path === name);
|
|
676
|
+
}
|
|
677
|
+
/**
|
|
678
|
+
* Canonical relation paths (parents included), e.g. ['items', 'items.realm'].
|
|
679
|
+
*/
|
|
680
|
+
getPaths() {
|
|
681
|
+
return this.value.map((join) => join.path);
|
|
682
|
+
}
|
|
683
|
+
};
|
|
684
|
+
//#endregion
|
|
685
|
+
//#region src/adapter/relations/module.ts
|
|
686
|
+
var RelationsAdapter = class extends RelationsBaseAdapter {
|
|
687
|
+
options;
|
|
688
|
+
constructor(options = {}) {
|
|
689
|
+
super(options);
|
|
690
|
+
this.options = options;
|
|
691
|
+
}
|
|
692
|
+
execute() {}
|
|
693
|
+
};
|
|
694
|
+
//#endregion
|
|
695
|
+
//#region src/adapter/sort/base.ts
|
|
696
|
+
var SortBaseAdapter = class {
|
|
697
|
+
relations;
|
|
698
|
+
value;
|
|
699
|
+
constructor(relations) {
|
|
700
|
+
this.relations = relations;
|
|
701
|
+
this.value = {};
|
|
702
|
+
}
|
|
703
|
+
clear() {
|
|
704
|
+
this.value = {};
|
|
705
|
+
}
|
|
706
|
+
add(input, value) {
|
|
707
|
+
const name = this.normalizeField(input);
|
|
708
|
+
this.value[name] = value;
|
|
709
|
+
}
|
|
710
|
+
/**
|
|
711
|
+
* Escaped ORDER BY entries, e.g. ['"user"."age" DESC'].
|
|
712
|
+
*/
|
|
713
|
+
getOrderBy() {
|
|
714
|
+
const keys = Object.keys(this.value);
|
|
715
|
+
const output = [];
|
|
716
|
+
for (const key_ of keys) {
|
|
717
|
+
const key = key_;
|
|
718
|
+
output.push(`${key} ${this.value[key]}`);
|
|
719
|
+
}
|
|
720
|
+
return output;
|
|
721
|
+
}
|
|
722
|
+
normalizeField(input) {
|
|
723
|
+
const output = parseField(input, this.rootAlias(), (path) => this.relations.buildAlias(path));
|
|
724
|
+
if (output.relation) this.relations.add(output.relation);
|
|
725
|
+
if (output.prefix) return `${this.escapeField(output.prefix)}.${this.escapeField(output.name)}`;
|
|
726
|
+
return this.escapeField(output.name);
|
|
727
|
+
}
|
|
728
|
+
};
|
|
729
|
+
//#endregion
|
|
730
|
+
//#region src/adapter/sort/module.ts
|
|
731
|
+
var SortAdapter = class extends SortBaseAdapter {
|
|
732
|
+
options;
|
|
733
|
+
constructor(relations, options) {
|
|
734
|
+
super(relations);
|
|
735
|
+
this.options = options;
|
|
736
|
+
}
|
|
737
|
+
escapeField(field) {
|
|
738
|
+
if (this.options.escapeField) return this.options.escapeField(field);
|
|
739
|
+
return field;
|
|
740
|
+
}
|
|
741
|
+
rootAlias() {
|
|
742
|
+
if (this.options.rootAlias) return this.options.rootAlias;
|
|
743
|
+
}
|
|
744
|
+
execute() {}
|
|
745
|
+
};
|
|
746
|
+
//#endregion
|
|
747
|
+
//#region src/adapter/module.ts
|
|
748
|
+
var Adapter = class {
|
|
749
|
+
relations;
|
|
750
|
+
fields;
|
|
751
|
+
filters;
|
|
752
|
+
pagination;
|
|
753
|
+
sort;
|
|
754
|
+
constructor(options) {
|
|
755
|
+
this.relations = new RelationsAdapter({
|
|
756
|
+
join: () => true,
|
|
757
|
+
relationAlias: options.relationAlias
|
|
758
|
+
});
|
|
759
|
+
this.fields = new FieldsAdapter(this.relations, {
|
|
760
|
+
escapeField: options.escapeField,
|
|
761
|
+
rootAlias: options.rootAlias
|
|
762
|
+
});
|
|
763
|
+
this.filters = new FiltersAdapter(this.relations, {
|
|
764
|
+
paramPlaceholder: options.paramPlaceholder,
|
|
765
|
+
regexp: options.regexp,
|
|
766
|
+
caseFold: options.caseFold,
|
|
767
|
+
escapeField: options.escapeField,
|
|
768
|
+
rootAlias: options.rootAlias
|
|
769
|
+
});
|
|
770
|
+
this.pagination = new PaginationAdapter();
|
|
771
|
+
this.sort = new SortAdapter(this.relations, {
|
|
772
|
+
escapeField: options.escapeField,
|
|
773
|
+
rootAlias: options.rootAlias
|
|
774
|
+
});
|
|
775
|
+
}
|
|
776
|
+
clear() {
|
|
777
|
+
this.fields.clear();
|
|
778
|
+
this.filters.clear();
|
|
779
|
+
this.pagination.clear();
|
|
780
|
+
this.sort.clear();
|
|
781
|
+
this.relations.clear();
|
|
782
|
+
}
|
|
783
|
+
/**
|
|
784
|
+
* Walk `query` into the sub-adapters and collect the accumulated
|
|
785
|
+
* clause fragments. Plain SQL has no backend target — it returns the
|
|
786
|
+
* fragments for the caller to assemble.
|
|
787
|
+
*/
|
|
788
|
+
execute(query, options = {}) {
|
|
789
|
+
if (options.clear ?? true) this.clear();
|
|
790
|
+
query.accept(new QueryVisitor(this, options.visitor));
|
|
791
|
+
const [where, params] = this.filters.getQueryAndParameters();
|
|
792
|
+
return {
|
|
793
|
+
columns: this.fields.getColumns(),
|
|
794
|
+
where,
|
|
795
|
+
params,
|
|
796
|
+
orderBy: this.sort.getOrderBy(),
|
|
797
|
+
limit: this.pagination.limit,
|
|
798
|
+
offset: this.pagination.offset,
|
|
799
|
+
relations: this.relations.getPaths()
|
|
800
|
+
};
|
|
801
|
+
}
|
|
802
|
+
};
|
|
803
|
+
//#endregion
|
|
804
|
+
//#region src/dialect/mssql.ts
|
|
805
|
+
const mssql = {
|
|
806
|
+
paramPlaceholder: () => "?",
|
|
807
|
+
escapeField: (field) => `[${field}]`,
|
|
808
|
+
caseFold: (input) => input
|
|
809
|
+
};
|
|
810
|
+
//#endregion
|
|
811
|
+
//#region src/dialect/mysql.ts
|
|
812
|
+
const mysql = {
|
|
813
|
+
regexp: (field, placeholder) => `${field} regexp ${placeholder} = 1`,
|
|
814
|
+
paramPlaceholder: () => "?",
|
|
815
|
+
escapeField: (field) => `\`${field}\``,
|
|
816
|
+
caseFold: (input) => input
|
|
817
|
+
};
|
|
818
|
+
//#endregion
|
|
819
|
+
//#region src/dialect/oracle.ts
|
|
820
|
+
const oracle = {
|
|
821
|
+
regexp: (field, placeholder, ignoreCase) => ignoreCase ? `regexp_like(${field}, ${placeholder}, 'i')` : `regexp_like(${field}, ${placeholder})`,
|
|
822
|
+
escapeField: (field) => `"${field}"`,
|
|
823
|
+
paramPlaceholder: (index) => `:${index}`
|
|
824
|
+
};
|
|
825
|
+
//#endregion
|
|
826
|
+
//#region src/dialect/pg.ts
|
|
827
|
+
const pg = {
|
|
828
|
+
regexp: (field, placeholder, ignoreCase) => {
|
|
829
|
+
return `${field} ${ignoreCase ? "~*" : "~"} ${placeholder}`;
|
|
830
|
+
},
|
|
831
|
+
escapeField: (field) => `"${field}"`,
|
|
832
|
+
paramPlaceholder: (index) => `$${index}`
|
|
833
|
+
};
|
|
834
|
+
//#endregion
|
|
835
|
+
//#region src/dialect/sqlite.ts
|
|
836
|
+
const sqlite = {
|
|
837
|
+
paramPlaceholder: mysql.paramPlaceholder,
|
|
838
|
+
escapeField: mysql.escapeField
|
|
839
|
+
};
|
|
840
|
+
//#endregion
|
|
841
|
+
//#region src/dialect/resolve.ts
|
|
842
|
+
const presets = {
|
|
843
|
+
pg,
|
|
844
|
+
postgres: pg,
|
|
845
|
+
postgresql: pg,
|
|
846
|
+
"aurora-postgres": pg,
|
|
847
|
+
cockroachdb: pg,
|
|
848
|
+
mysql,
|
|
849
|
+
mysql2: mysql,
|
|
850
|
+
mariadb: mysql,
|
|
851
|
+
"aurora-mysql": mysql,
|
|
852
|
+
sqlite,
|
|
853
|
+
sqlite3: sqlite,
|
|
854
|
+
"better-sqlite3": sqlite,
|
|
855
|
+
sqljs: sqlite,
|
|
856
|
+
capacitor: sqlite,
|
|
857
|
+
cordova: sqlite,
|
|
858
|
+
"react-native": sqlite,
|
|
859
|
+
nativescript: sqlite,
|
|
860
|
+
expo: sqlite,
|
|
861
|
+
mssql,
|
|
862
|
+
oracle,
|
|
863
|
+
oracledb: oracle
|
|
864
|
+
};
|
|
865
|
+
/**
|
|
866
|
+
* Resolve a driver/connection type name (e.g. TypeORM's
|
|
867
|
+
* `connection.options.type` or a knex client name) to the
|
|
868
|
+
* matching {@link DialectOptions} preset.
|
|
869
|
+
*/
|
|
870
|
+
function resolveDialect(name) {
|
|
871
|
+
return presets[name.toLowerCase()];
|
|
872
|
+
}
|
|
873
|
+
//#endregion
|
|
874
|
+
export { Adapter, FieldsAdapter, FieldsBaseAdapter, FieldsVisitor, FiltersAdapter, FiltersBaseAdapter, FiltersVisitor, PaginationAdapter, PaginationBaseAdapter, PaginationVisitor, ParamPlaceholderIndexer, QueryVisitor, RelationsAdapter, RelationsBaseAdapter, RelationsVisitor, SortAdapter, SortBaseAdapter, SortsVisitor, buildRelationAlias, escapeLikePattern, isRootAliasFn, mssql, mysql, oracle, parseField, pg, resolveDialect, splitFirst, splitLast, sqlite, toRootAliasFn };
|
|
875
|
+
|
|
876
|
+
//# sourceMappingURL=index.mjs.map
|