bun-query-builder 0.1.29 → 0.1.30
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/bin/cli.js +134 -86
- package/dist/src/index.js +133 -85
- package/package.json +1 -1
package/dist/bin/cli.js
CHANGED
|
@@ -12391,6 +12391,61 @@ function validateIdentifier(name, context) {
|
|
|
12391
12391
|
throw new Error(`[query-builder] Invalid identifier${contextMsg}: '${name}'. Identifiers must start with a letter or underscore and contain only alphanumeric characters, underscores, and dots.`);
|
|
12392
12392
|
}
|
|
12393
12393
|
}
|
|
12394
|
+
function assertSafeWhereOperator(op, context) {
|
|
12395
|
+
if (typeof op !== "string")
|
|
12396
|
+
throw new TypeError(`[query-builder] ${context}: operator must be a string, got ${typeof op}`);
|
|
12397
|
+
const lower = op.toLowerCase();
|
|
12398
|
+
if (!SAFE_WHERE_OPERATORS.has(lower))
|
|
12399
|
+
throw new TypeError(`[query-builder] ${context}: refusing to use '${op}' as a SQL operator \u2014 not in the allowed set (${[...SAFE_WHERE_OPERATORS].join(", ")})`);
|
|
12400
|
+
return op;
|
|
12401
|
+
}
|
|
12402
|
+
function validateQualifiedIdentifier(value, context) {
|
|
12403
|
+
if (typeof value !== "string" || value.length === 0)
|
|
12404
|
+
throw new TypeError(`[query-builder] ${context}: identifier must be a non-empty string, got ${typeof value}`);
|
|
12405
|
+
if (value.length > 129)
|
|
12406
|
+
throw new TypeError(`[query-builder] ${context}: identifier '${value}' too long`);
|
|
12407
|
+
const parts = value.split(".");
|
|
12408
|
+
if (parts.length > 2)
|
|
12409
|
+
throw new TypeError(`[query-builder] ${context}: identifier '${value}' has more than one dot \u2014 only \`table.column\` is allowed`);
|
|
12410
|
+
for (const part of parts) {
|
|
12411
|
+
if (!/^[A-Z_][A-Z0-9_]*$/i.test(part))
|
|
12412
|
+
throw new TypeError(`[query-builder] ${context}: identifier segment '${part}' contains characters outside [A-Za-z0-9_]`);
|
|
12413
|
+
}
|
|
12414
|
+
}
|
|
12415
|
+
function assertSqlFragment(fragment, context) {
|
|
12416
|
+
if (fragment === null || fragment === undefined) {
|
|
12417
|
+
throw new TypeError(`[query-builder] ${context}: fragment must be a SqlFragment, got ${fragment}`);
|
|
12418
|
+
}
|
|
12419
|
+
if (typeof fragment === "string") {
|
|
12420
|
+
warnOnceBareSqlFragment(context);
|
|
12421
|
+
}
|
|
12422
|
+
}
|
|
12423
|
+
function warnOnceBareSqlFragment(context) {
|
|
12424
|
+
if (warnedSqlFragmentContexts.has(context))
|
|
12425
|
+
return;
|
|
12426
|
+
warnedSqlFragmentContexts.add(context);
|
|
12427
|
+
console.warn(`[query-builder] ${context}: bare string passed to a *Raw method. ` + `Prefer \`sql\`...\`\` tagged-template fragments so values are parameterised ` + `instead of concatenated \u2014 concatenating request input into SQL is an ` + `injection vector. This will become a hard error in a future release.`);
|
|
12428
|
+
}
|
|
12429
|
+
function formatSubqueryValue(val) {
|
|
12430
|
+
if (val === null)
|
|
12431
|
+
return "NULL";
|
|
12432
|
+
if (typeof val === "number" && Number.isFinite(val))
|
|
12433
|
+
return String(val);
|
|
12434
|
+
if (typeof val === "boolean")
|
|
12435
|
+
return val ? "1" : "0";
|
|
12436
|
+
if (typeof val === "string")
|
|
12437
|
+
return `'${val.replace(/'/g, "''")}'`;
|
|
12438
|
+
throw new TypeError(`[query-builder] subquery condition: refusing to interpolate value of type ${typeof val}`);
|
|
12439
|
+
}
|
|
12440
|
+
function buildOverClause(partitionBy, orderBy) {
|
|
12441
|
+
const cols = Array.isArray(partitionBy) ? partitionBy : partitionBy ? [partitionBy] : [];
|
|
12442
|
+
const parts = [];
|
|
12443
|
+
if (cols.length)
|
|
12444
|
+
parts.push(`PARTITION BY ${cols.join(", ")}`);
|
|
12445
|
+
if (orderBy && orderBy.length)
|
|
12446
|
+
parts.push(`ORDER BY ${orderBy.map(([c, d]) => `${c} ${d === "desc" ? "DESC" : "ASC"}`).join(", ")}`);
|
|
12447
|
+
return parts.length ? `OVER (${parts.join(" ")})` : "OVER ()";
|
|
12448
|
+
}
|
|
12394
12449
|
|
|
12395
12450
|
class QueryCache {
|
|
12396
12451
|
cache = new Map;
|
|
@@ -12455,6 +12510,16 @@ function sleep(ms) {
|
|
|
12455
12510
|
return new Promise((resolve13) => setTimeout(resolve13, ms));
|
|
12456
12511
|
}
|
|
12457
12512
|
function reorderSelectClauses(sql) {
|
|
12513
|
+
const hit = reorderCache.get(sql);
|
|
12514
|
+
if (hit !== undefined)
|
|
12515
|
+
return hit;
|
|
12516
|
+
const out = computeReorderedClauses(sql);
|
|
12517
|
+
if (reorderCache.size >= REORDER_CACHE_MAX)
|
|
12518
|
+
reorderCache.clear();
|
|
12519
|
+
reorderCache.set(sql, out);
|
|
12520
|
+
return out;
|
|
12521
|
+
}
|
|
12522
|
+
function computeReorderedClauses(sql) {
|
|
12458
12523
|
const KEYWORDS = [
|
|
12459
12524
|
{ key: "GROUP_BY", tokens: /^GROUP\s+BY\b/i },
|
|
12460
12525
|
{ key: "ORDER_BY", tokens: /^ORDER\s+BY\b/i },
|
|
@@ -12540,6 +12605,7 @@ function createQueryBuilder(state) {
|
|
|
12540
12605
|
const _sql = state?.sql ?? getOrCreateBunSql();
|
|
12541
12606
|
const meta = state?.meta;
|
|
12542
12607
|
const schema = state?.schema;
|
|
12608
|
+
const dynamicWhereColumnCache = new Map;
|
|
12543
12609
|
function applyCondition(expr) {
|
|
12544
12610
|
if (Array.isArray(expr)) {
|
|
12545
12611
|
const [col, op, val] = expr;
|
|
@@ -12858,65 +12924,9 @@ function createQueryBuilder(state) {
|
|
|
12858
12924
|
}
|
|
12859
12925
|
}
|
|
12860
12926
|
};
|
|
12861
|
-
const buildOverClause = (partitionBy, orderBy) => {
|
|
12862
|
-
const cols = Array.isArray(partitionBy) ? partitionBy : partitionBy ? [partitionBy] : [];
|
|
12863
|
-
const parts = [];
|
|
12864
|
-
if (cols.length)
|
|
12865
|
-
parts.push(`PARTITION BY ${cols.join(", ")}`);
|
|
12866
|
-
if (orderBy && orderBy.length)
|
|
12867
|
-
parts.push(`ORDER BY ${orderBy.map(([c, d]) => `${c} ${d === "desc" ? "DESC" : "ASC"}`).join(", ")}`);
|
|
12868
|
-
return parts.length ? `OVER (${parts.join(" ")})` : "OVER ()";
|
|
12869
|
-
};
|
|
12870
12927
|
const addWindowFunction = (fnExpr, alias, partitionBy, orderBy) => {
|
|
12871
12928
|
addToSelectClause(`${fnExpr} ${buildOverClause(partitionBy, orderBy)} AS ${alias}`);
|
|
12872
12929
|
};
|
|
12873
|
-
function assertSafeWhereOperator(op, context) {
|
|
12874
|
-
if (typeof op !== "string")
|
|
12875
|
-
throw new TypeError(`[query-builder] ${context}: operator must be a string, got ${typeof op}`);
|
|
12876
|
-
const lower = op.toLowerCase();
|
|
12877
|
-
if (!SAFE_WHERE_OPERATORS.has(lower))
|
|
12878
|
-
throw new TypeError(`[query-builder] ${context}: refusing to use '${op}' as a SQL operator \u2014 not in the allowed set (${[...SAFE_WHERE_OPERATORS].join(", ")})`);
|
|
12879
|
-
return op;
|
|
12880
|
-
}
|
|
12881
|
-
function validateQualifiedIdentifier(value, context) {
|
|
12882
|
-
if (typeof value !== "string" || value.length === 0)
|
|
12883
|
-
throw new TypeError(`[query-builder] ${context}: identifier must be a non-empty string, got ${typeof value}`);
|
|
12884
|
-
if (value.length > 129)
|
|
12885
|
-
throw new TypeError(`[query-builder] ${context}: identifier '${value}' too long`);
|
|
12886
|
-
const parts = value.split(".");
|
|
12887
|
-
if (parts.length > 2)
|
|
12888
|
-
throw new TypeError(`[query-builder] ${context}: identifier '${value}' has more than one dot \u2014 only \`table.column\` is allowed`);
|
|
12889
|
-
for (const part of parts) {
|
|
12890
|
-
if (!/^[A-Z_][A-Z0-9_]*$/i.test(part))
|
|
12891
|
-
throw new TypeError(`[query-builder] ${context}: identifier segment '${part}' contains characters outside [A-Za-z0-9_]`);
|
|
12892
|
-
}
|
|
12893
|
-
}
|
|
12894
|
-
function assertSqlFragment(fragment, context) {
|
|
12895
|
-
if (fragment === null || fragment === undefined) {
|
|
12896
|
-
throw new TypeError(`[query-builder] ${context}: fragment must be a SqlFragment, got ${fragment}`);
|
|
12897
|
-
}
|
|
12898
|
-
if (typeof fragment === "string") {
|
|
12899
|
-
warnOnceBareSqlFragment(context);
|
|
12900
|
-
}
|
|
12901
|
-
}
|
|
12902
|
-
const warnedSqlFragmentContexts = new Set;
|
|
12903
|
-
function warnOnceBareSqlFragment(context) {
|
|
12904
|
-
if (warnedSqlFragmentContexts.has(context))
|
|
12905
|
-
return;
|
|
12906
|
-
warnedSqlFragmentContexts.add(context);
|
|
12907
|
-
console.warn(`[query-builder] ${context}: bare string passed to a *Raw method. ` + `Prefer \`sql\`...\`\` tagged-template fragments so values are parameterised ` + `instead of concatenated \u2014 concatenating request input into SQL is an ` + `injection vector. This will become a hard error in a future release.`);
|
|
12908
|
-
}
|
|
12909
|
-
function formatSubqueryValue(val) {
|
|
12910
|
-
if (val === null)
|
|
12911
|
-
return "NULL";
|
|
12912
|
-
if (typeof val === "number" && Number.isFinite(val))
|
|
12913
|
-
return String(val);
|
|
12914
|
-
if (typeof val === "boolean")
|
|
12915
|
-
return val ? "1" : "0";
|
|
12916
|
-
if (typeof val === "string")
|
|
12917
|
-
return `'${val.replace(/'/g, "''")}'`;
|
|
12918
|
-
throw new TypeError(`[query-builder] subquery condition: refusing to interpolate value of type ${typeof val}`);
|
|
12919
|
-
}
|
|
12920
12930
|
const buildHasSubquery = (parentTable, targetTable, pk, callback) => {
|
|
12921
12931
|
validateIdentifier(parentTable, "relationship subquery (parent table)");
|
|
12922
12932
|
validateIdentifier(targetTable, "relationship subquery (target table)");
|
|
@@ -12949,7 +12959,7 @@ function createQueryBuilder(state) {
|
|
|
12949
12959
|
const subQb = {
|
|
12950
12960
|
where: (col, op, val) => {
|
|
12951
12961
|
validateIdentifier(col, "relationship subquery condition");
|
|
12952
|
-
return `${targetTable}.${col} ${op
|
|
12962
|
+
return `${targetTable}.${col} ${assertSafeWhereOperator(op, "whereHas callback")} ${formatSubqueryValue(val)}`;
|
|
12953
12963
|
}
|
|
12954
12964
|
};
|
|
12955
12965
|
const condition = callback(subQb);
|
|
@@ -12978,7 +12988,7 @@ function createQueryBuilder(state) {
|
|
|
12978
12988
|
const subQb = {
|
|
12979
12989
|
where: (col, op, val) => {
|
|
12980
12990
|
validateIdentifier(col, "relationship subquery condition");
|
|
12981
|
-
return `${targetTable}.${col} ${op
|
|
12991
|
+
return `${targetTable}.${col} ${assertSafeWhereOperator(op, "whereHas callback")} ${formatSubqueryValue(val)}`;
|
|
12982
12992
|
}
|
|
12983
12993
|
};
|
|
12984
12994
|
const condition = callback(subQb);
|
|
@@ -14493,7 +14503,14 @@ function createQueryBuilder(state) {
|
|
|
14493
14503
|
return this;
|
|
14494
14504
|
},
|
|
14495
14505
|
toSQL() {
|
|
14496
|
-
|
|
14506
|
+
const sqlText = reorderSelectClauses(text);
|
|
14507
|
+
return {
|
|
14508
|
+
sql: sqlText,
|
|
14509
|
+
toString: () => sqlText,
|
|
14510
|
+
execute: () => ensureBuilt().execute(),
|
|
14511
|
+
values: () => ensureBuilt().values(),
|
|
14512
|
+
raw: () => ensureBuilt().raw()
|
|
14513
|
+
};
|
|
14497
14514
|
},
|
|
14498
14515
|
async value(column) {
|
|
14499
14516
|
const q = sql`${ensureBuilt()} LIMIT 1`;
|
|
@@ -14947,20 +14964,36 @@ function createQueryBuilder(state) {
|
|
|
14947
14964
|
if (typeof prop === "string" && (prop.startsWith("where") || prop.startsWith("orWhere") || prop.startsWith("andWhere"))) {
|
|
14948
14965
|
const isOr = prop.startsWith("orWhere");
|
|
14949
14966
|
const isAnd = prop.startsWith("andWhere");
|
|
14950
|
-
const
|
|
14951
|
-
|
|
14967
|
+
const cacheKey = `${String(table)}|${prop}`;
|
|
14968
|
+
let chosen = dynamicWhereColumnCache.get(cacheKey);
|
|
14969
|
+
if (chosen === undefined) {
|
|
14970
|
+
const raw = prop.replace(/^(?:or|and)?where/i, "");
|
|
14971
|
+
if (!raw) {
|
|
14972
|
+
dynamicWhereColumnCache.set(cacheKey, "");
|
|
14973
|
+
chosen = "";
|
|
14974
|
+
} else {
|
|
14975
|
+
const lowerFirst = raw.charAt(0).toLowerCase() + raw.slice(1);
|
|
14976
|
+
const snake = raw.replace(/([A-Z])/g, "_$1").toLowerCase().replace(/^_/, "");
|
|
14977
|
+
const available = schema ? Object.keys(schema[String(table)]?.columns ?? {}) : [];
|
|
14978
|
+
chosen = [snake, lowerFirst, lowerFirst.toLowerCase()].find((n) => available.includes(n)) ?? snake;
|
|
14979
|
+
dynamicWhereColumnCache.set(cacheKey, chosen);
|
|
14980
|
+
}
|
|
14981
|
+
}
|
|
14982
|
+
if (chosen === "")
|
|
14952
14983
|
return () => receiver;
|
|
14953
|
-
const
|
|
14954
|
-
const toSnake = (s) => s.replace(/([A-Z])/g, "_$1").toLowerCase().replace(/^_/, "");
|
|
14955
|
-
const snake = toSnake(raw);
|
|
14956
|
-
const tbl = String(table);
|
|
14957
|
-
const available = schema ? Object.keys(schema[tbl]?.columns ?? {}) : [];
|
|
14958
|
-
const chosen = [snake, lowerFirst, lowerFirst.toLowerCase()].find((n) => available.includes(n)) ?? snake;
|
|
14984
|
+
const column = chosen;
|
|
14959
14985
|
return (value) => {
|
|
14960
|
-
const expr = Array.isArray(value) ? sql`${sql(
|
|
14986
|
+
const expr = Array.isArray(value) ? sql`${sql(column)} IN ${sql(value)}` : sql`${sql(column)} = ${value}`;
|
|
14961
14987
|
built = isOr ? sql`${ensureBuilt()} OR ${expr}` : isAnd ? sql`${ensureBuilt()} AND ${expr}` : sql`${ensureBuilt()} WHERE ${expr}`;
|
|
14962
|
-
|
|
14963
|
-
|
|
14988
|
+
if (Array.isArray(value)) {
|
|
14989
|
+
const phs = getPlaceholders(value.length, whereParams.length + 1);
|
|
14990
|
+
addWhereText(isOr ? "OR" : isAnd ? "AND" : "WHERE", `${column} IN (${phs})`);
|
|
14991
|
+
whereParams.push(...value);
|
|
14992
|
+
} else {
|
|
14993
|
+
const ph = getPlaceholder(whereParams.length + 1);
|
|
14994
|
+
addWhereText(isOr ? "OR" : isAnd ? "AND" : "WHERE", `${column} = ${ph}`);
|
|
14995
|
+
whereParams.push(value);
|
|
14996
|
+
}
|
|
14964
14997
|
return receiver;
|
|
14965
14998
|
};
|
|
14966
14999
|
}
|
|
@@ -16208,7 +16241,7 @@ function clearQueryCache() {
|
|
|
16208
16241
|
function setQueryCacheMaxSize(size) {
|
|
16209
16242
|
queryCache.setMaxSize(size);
|
|
16210
16243
|
}
|
|
16211
|
-
var SQL_PATTERNS, SAFE_WHERE_OPERATORS, queryCache;
|
|
16244
|
+
var SQL_PATTERNS, SAFE_WHERE_OPERATORS, warnedSqlFragmentContexts, queryCache, reorderCache, REORDER_CACHE_MAX = 500;
|
|
16212
16245
|
var init_client = __esm(() => {
|
|
16213
16246
|
init_config();
|
|
16214
16247
|
init_db();
|
|
@@ -16243,7 +16276,9 @@ var init_client = __esm(() => {
|
|
|
16243
16276
|
"between",
|
|
16244
16277
|
"not between"
|
|
16245
16278
|
]);
|
|
16279
|
+
warnedSqlFragmentContexts = new Set;
|
|
16246
16280
|
queryCache = new QueryCache;
|
|
16281
|
+
reorderCache = new Map;
|
|
16247
16282
|
});
|
|
16248
16283
|
|
|
16249
16284
|
// src/actions/cache.ts
|
|
@@ -23821,8 +23856,15 @@ function getModelFromRegistry(name) {
|
|
|
23821
23856
|
return _getModel(name);
|
|
23822
23857
|
}
|
|
23823
23858
|
function toPostgresPlaceholders(sql2) {
|
|
23859
|
+
const hit = pgPlaceholderCache.get(sql2);
|
|
23860
|
+
if (hit !== undefined)
|
|
23861
|
+
return hit;
|
|
23824
23862
|
let i2 = 0;
|
|
23825
|
-
|
|
23863
|
+
const out = sql2.replace(/\?/g, () => `$${++i2}`);
|
|
23864
|
+
if (pgPlaceholderCache.size >= PG_PLACEHOLDER_CACHE_MAX)
|
|
23865
|
+
pgPlaceholderCache.clear();
|
|
23866
|
+
pgPlaceholderCache.set(sql2, out);
|
|
23867
|
+
return out;
|
|
23826
23868
|
}
|
|
23827
23869
|
function extractChanges(res) {
|
|
23828
23870
|
if (res == null)
|
|
@@ -23954,21 +23996,25 @@ function timestampsEnabled(definition) {
|
|
|
23954
23996
|
return Boolean(t2?.useTimestamps || t2?.timestampable);
|
|
23955
23997
|
}
|
|
23956
23998
|
function collectBelongsToManyKeys(definition) {
|
|
23999
|
+
const cached = btmKeysCache.get(definition);
|
|
24000
|
+
if (cached)
|
|
24001
|
+
return cached;
|
|
23957
24002
|
const keys = new Set;
|
|
23958
24003
|
const rel = definition.belongsToMany;
|
|
23959
|
-
if (
|
|
23960
|
-
|
|
23961
|
-
|
|
23962
|
-
|
|
23963
|
-
|
|
23964
|
-
|
|
23965
|
-
|
|
23966
|
-
|
|
23967
|
-
}
|
|
23968
|
-
|
|
23969
|
-
|
|
23970
|
-
|
|
24004
|
+
if (rel) {
|
|
24005
|
+
if (Array.isArray(rel)) {
|
|
24006
|
+
for (const item of rel) {
|
|
24007
|
+
if (typeof item === "string")
|
|
24008
|
+
keys.add(item.toLowerCase());
|
|
24009
|
+
else if (item && typeof item === "object" && item.model)
|
|
24010
|
+
keys.add(item.model.toLowerCase());
|
|
24011
|
+
}
|
|
24012
|
+
} else if (typeof rel === "object") {
|
|
24013
|
+
for (const k2 of Object.keys(rel))
|
|
24014
|
+
keys.add(k2);
|
|
24015
|
+
}
|
|
23971
24016
|
}
|
|
24017
|
+
btmKeysCache.set(definition, keys);
|
|
23972
24018
|
return keys;
|
|
23973
24019
|
}
|
|
23974
24020
|
|
|
@@ -25480,11 +25526,13 @@ async function seedModel(definition, count, faker) {
|
|
|
25480
25526
|
await exec.run(`INSERT INTO ${definition.table} (${columns.join(", ")}) VALUES (${columns.map(() => "?").join(", ")})`, Object.values(data));
|
|
25481
25527
|
}
|
|
25482
25528
|
}
|
|
25483
|
-
var SAFE_SQL_IDENTIFIER, _getModel = null, globalDb = null, _executor = null, _executorForDb = null, _executorDialect = null, _executorDatabase = null, SOFT_DELETE_COLUMN = "deleted_at", BTM_RELATED_ALIAS = "__btm_rel__", snakeCaseCache, tableNameCache, relationCache, RELATION_CACHE_MAX = 1000;
|
|
25529
|
+
var SAFE_SQL_IDENTIFIER, _getModel = null, pgPlaceholderCache, PG_PLACEHOLDER_CACHE_MAX = 500, globalDb = null, _executor = null, _executorForDb = null, _executorDialect = null, _executorDatabase = null, SOFT_DELETE_COLUMN = "deleted_at", BTM_RELATED_ALIAS = "__btm_rel__", btmKeysCache, snakeCaseCache, tableNameCache, relationCache, RELATION_CACHE_MAX = 1000;
|
|
25484
25530
|
var init_orm = __esm(() => {
|
|
25485
25531
|
init_config();
|
|
25486
25532
|
init_db();
|
|
25487
25533
|
SAFE_SQL_IDENTIFIER = /^[A-Z_][A-Z0-9_]*$/i;
|
|
25534
|
+
pgPlaceholderCache = new Map;
|
|
25535
|
+
btmKeysCache = new WeakMap;
|
|
25488
25536
|
snakeCaseCache = new Map;
|
|
25489
25537
|
tableNameCache = new Map;
|
|
25490
25538
|
relationCache = new Map;
|
|
@@ -29936,7 +29984,7 @@ function getPrefix() {
|
|
|
29936
29984
|
}
|
|
29937
29985
|
var prefix = getPrefix();
|
|
29938
29986
|
// package.json
|
|
29939
|
-
var version2 = "0.1.
|
|
29987
|
+
var version2 = "0.1.30";
|
|
29940
29988
|
|
|
29941
29989
|
// bin/cli.ts
|
|
29942
29990
|
init_actions();
|
package/dist/src/index.js
CHANGED
|
@@ -12391,6 +12391,61 @@ function validateIdentifier(name, context) {
|
|
|
12391
12391
|
throw new Error(`[query-builder] Invalid identifier${contextMsg}: '${name}'. Identifiers must start with a letter or underscore and contain only alphanumeric characters, underscores, and dots.`);
|
|
12392
12392
|
}
|
|
12393
12393
|
}
|
|
12394
|
+
function assertSafeWhereOperator(op, context) {
|
|
12395
|
+
if (typeof op !== "string")
|
|
12396
|
+
throw new TypeError(`[query-builder] ${context}: operator must be a string, got ${typeof op}`);
|
|
12397
|
+
const lower = op.toLowerCase();
|
|
12398
|
+
if (!SAFE_WHERE_OPERATORS.has(lower))
|
|
12399
|
+
throw new TypeError(`[query-builder] ${context}: refusing to use '${op}' as a SQL operator \u2014 not in the allowed set (${[...SAFE_WHERE_OPERATORS].join(", ")})`);
|
|
12400
|
+
return op;
|
|
12401
|
+
}
|
|
12402
|
+
function validateQualifiedIdentifier(value, context) {
|
|
12403
|
+
if (typeof value !== "string" || value.length === 0)
|
|
12404
|
+
throw new TypeError(`[query-builder] ${context}: identifier must be a non-empty string, got ${typeof value}`);
|
|
12405
|
+
if (value.length > 129)
|
|
12406
|
+
throw new TypeError(`[query-builder] ${context}: identifier '${value}' too long`);
|
|
12407
|
+
const parts = value.split(".");
|
|
12408
|
+
if (parts.length > 2)
|
|
12409
|
+
throw new TypeError(`[query-builder] ${context}: identifier '${value}' has more than one dot \u2014 only \`table.column\` is allowed`);
|
|
12410
|
+
for (const part of parts) {
|
|
12411
|
+
if (!/^[A-Z_][A-Z0-9_]*$/i.test(part))
|
|
12412
|
+
throw new TypeError(`[query-builder] ${context}: identifier segment '${part}' contains characters outside [A-Za-z0-9_]`);
|
|
12413
|
+
}
|
|
12414
|
+
}
|
|
12415
|
+
function assertSqlFragment(fragment, context) {
|
|
12416
|
+
if (fragment === null || fragment === undefined) {
|
|
12417
|
+
throw new TypeError(`[query-builder] ${context}: fragment must be a SqlFragment, got ${fragment}`);
|
|
12418
|
+
}
|
|
12419
|
+
if (typeof fragment === "string") {
|
|
12420
|
+
warnOnceBareSqlFragment(context);
|
|
12421
|
+
}
|
|
12422
|
+
}
|
|
12423
|
+
function warnOnceBareSqlFragment(context) {
|
|
12424
|
+
if (warnedSqlFragmentContexts.has(context))
|
|
12425
|
+
return;
|
|
12426
|
+
warnedSqlFragmentContexts.add(context);
|
|
12427
|
+
console.warn(`[query-builder] ${context}: bare string passed to a *Raw method. ` + `Prefer \`sql\`...\`\` tagged-template fragments so values are parameterised ` + `instead of concatenated \u2014 concatenating request input into SQL is an ` + `injection vector. This will become a hard error in a future release.`);
|
|
12428
|
+
}
|
|
12429
|
+
function formatSubqueryValue(val) {
|
|
12430
|
+
if (val === null)
|
|
12431
|
+
return "NULL";
|
|
12432
|
+
if (typeof val === "number" && Number.isFinite(val))
|
|
12433
|
+
return String(val);
|
|
12434
|
+
if (typeof val === "boolean")
|
|
12435
|
+
return val ? "1" : "0";
|
|
12436
|
+
if (typeof val === "string")
|
|
12437
|
+
return `'${val.replace(/'/g, "''")}'`;
|
|
12438
|
+
throw new TypeError(`[query-builder] subquery condition: refusing to interpolate value of type ${typeof val}`);
|
|
12439
|
+
}
|
|
12440
|
+
function buildOverClause(partitionBy, orderBy) {
|
|
12441
|
+
const cols = Array.isArray(partitionBy) ? partitionBy : partitionBy ? [partitionBy] : [];
|
|
12442
|
+
const parts = [];
|
|
12443
|
+
if (cols.length)
|
|
12444
|
+
parts.push(`PARTITION BY ${cols.join(", ")}`);
|
|
12445
|
+
if (orderBy && orderBy.length)
|
|
12446
|
+
parts.push(`ORDER BY ${orderBy.map(([c, d]) => `${c} ${d === "desc" ? "DESC" : "ASC"}`).join(", ")}`);
|
|
12447
|
+
return parts.length ? `OVER (${parts.join(" ")})` : "OVER ()";
|
|
12448
|
+
}
|
|
12394
12449
|
|
|
12395
12450
|
class QueryCache {
|
|
12396
12451
|
cache = new Map;
|
|
@@ -12455,6 +12510,16 @@ function sleep(ms) {
|
|
|
12455
12510
|
return new Promise((resolve13) => setTimeout(resolve13, ms));
|
|
12456
12511
|
}
|
|
12457
12512
|
function reorderSelectClauses(sql) {
|
|
12513
|
+
const hit = reorderCache.get(sql);
|
|
12514
|
+
if (hit !== undefined)
|
|
12515
|
+
return hit;
|
|
12516
|
+
const out = computeReorderedClauses(sql);
|
|
12517
|
+
if (reorderCache.size >= REORDER_CACHE_MAX)
|
|
12518
|
+
reorderCache.clear();
|
|
12519
|
+
reorderCache.set(sql, out);
|
|
12520
|
+
return out;
|
|
12521
|
+
}
|
|
12522
|
+
function computeReorderedClauses(sql) {
|
|
12458
12523
|
const KEYWORDS = [
|
|
12459
12524
|
{ key: "GROUP_BY", tokens: /^GROUP\s+BY\b/i },
|
|
12460
12525
|
{ key: "ORDER_BY", tokens: /^ORDER\s+BY\b/i },
|
|
@@ -12540,6 +12605,7 @@ function createQueryBuilder(state) {
|
|
|
12540
12605
|
const _sql = state?.sql ?? getOrCreateBunSql();
|
|
12541
12606
|
const meta = state?.meta;
|
|
12542
12607
|
const schema = state?.schema;
|
|
12608
|
+
const dynamicWhereColumnCache = new Map;
|
|
12543
12609
|
function applyCondition(expr) {
|
|
12544
12610
|
if (Array.isArray(expr)) {
|
|
12545
12611
|
const [col, op, val] = expr;
|
|
@@ -12858,65 +12924,9 @@ function createQueryBuilder(state) {
|
|
|
12858
12924
|
}
|
|
12859
12925
|
}
|
|
12860
12926
|
};
|
|
12861
|
-
const buildOverClause = (partitionBy, orderBy) => {
|
|
12862
|
-
const cols = Array.isArray(partitionBy) ? partitionBy : partitionBy ? [partitionBy] : [];
|
|
12863
|
-
const parts = [];
|
|
12864
|
-
if (cols.length)
|
|
12865
|
-
parts.push(`PARTITION BY ${cols.join(", ")}`);
|
|
12866
|
-
if (orderBy && orderBy.length)
|
|
12867
|
-
parts.push(`ORDER BY ${orderBy.map(([c, d]) => `${c} ${d === "desc" ? "DESC" : "ASC"}`).join(", ")}`);
|
|
12868
|
-
return parts.length ? `OVER (${parts.join(" ")})` : "OVER ()";
|
|
12869
|
-
};
|
|
12870
12927
|
const addWindowFunction = (fnExpr, alias, partitionBy, orderBy) => {
|
|
12871
12928
|
addToSelectClause(`${fnExpr} ${buildOverClause(partitionBy, orderBy)} AS ${alias}`);
|
|
12872
12929
|
};
|
|
12873
|
-
function assertSafeWhereOperator(op, context) {
|
|
12874
|
-
if (typeof op !== "string")
|
|
12875
|
-
throw new TypeError(`[query-builder] ${context}: operator must be a string, got ${typeof op}`);
|
|
12876
|
-
const lower = op.toLowerCase();
|
|
12877
|
-
if (!SAFE_WHERE_OPERATORS.has(lower))
|
|
12878
|
-
throw new TypeError(`[query-builder] ${context}: refusing to use '${op}' as a SQL operator \u2014 not in the allowed set (${[...SAFE_WHERE_OPERATORS].join(", ")})`);
|
|
12879
|
-
return op;
|
|
12880
|
-
}
|
|
12881
|
-
function validateQualifiedIdentifier(value, context) {
|
|
12882
|
-
if (typeof value !== "string" || value.length === 0)
|
|
12883
|
-
throw new TypeError(`[query-builder] ${context}: identifier must be a non-empty string, got ${typeof value}`);
|
|
12884
|
-
if (value.length > 129)
|
|
12885
|
-
throw new TypeError(`[query-builder] ${context}: identifier '${value}' too long`);
|
|
12886
|
-
const parts = value.split(".");
|
|
12887
|
-
if (parts.length > 2)
|
|
12888
|
-
throw new TypeError(`[query-builder] ${context}: identifier '${value}' has more than one dot \u2014 only \`table.column\` is allowed`);
|
|
12889
|
-
for (const part of parts) {
|
|
12890
|
-
if (!/^[A-Z_][A-Z0-9_]*$/i.test(part))
|
|
12891
|
-
throw new TypeError(`[query-builder] ${context}: identifier segment '${part}' contains characters outside [A-Za-z0-9_]`);
|
|
12892
|
-
}
|
|
12893
|
-
}
|
|
12894
|
-
function assertSqlFragment(fragment, context) {
|
|
12895
|
-
if (fragment === null || fragment === undefined) {
|
|
12896
|
-
throw new TypeError(`[query-builder] ${context}: fragment must be a SqlFragment, got ${fragment}`);
|
|
12897
|
-
}
|
|
12898
|
-
if (typeof fragment === "string") {
|
|
12899
|
-
warnOnceBareSqlFragment(context);
|
|
12900
|
-
}
|
|
12901
|
-
}
|
|
12902
|
-
const warnedSqlFragmentContexts = new Set;
|
|
12903
|
-
function warnOnceBareSqlFragment(context) {
|
|
12904
|
-
if (warnedSqlFragmentContexts.has(context))
|
|
12905
|
-
return;
|
|
12906
|
-
warnedSqlFragmentContexts.add(context);
|
|
12907
|
-
console.warn(`[query-builder] ${context}: bare string passed to a *Raw method. ` + `Prefer \`sql\`...\`\` tagged-template fragments so values are parameterised ` + `instead of concatenated \u2014 concatenating request input into SQL is an ` + `injection vector. This will become a hard error in a future release.`);
|
|
12908
|
-
}
|
|
12909
|
-
function formatSubqueryValue(val) {
|
|
12910
|
-
if (val === null)
|
|
12911
|
-
return "NULL";
|
|
12912
|
-
if (typeof val === "number" && Number.isFinite(val))
|
|
12913
|
-
return String(val);
|
|
12914
|
-
if (typeof val === "boolean")
|
|
12915
|
-
return val ? "1" : "0";
|
|
12916
|
-
if (typeof val === "string")
|
|
12917
|
-
return `'${val.replace(/'/g, "''")}'`;
|
|
12918
|
-
throw new TypeError(`[query-builder] subquery condition: refusing to interpolate value of type ${typeof val}`);
|
|
12919
|
-
}
|
|
12920
12930
|
const buildHasSubquery = (parentTable, targetTable, pk, callback) => {
|
|
12921
12931
|
validateIdentifier(parentTable, "relationship subquery (parent table)");
|
|
12922
12932
|
validateIdentifier(targetTable, "relationship subquery (target table)");
|
|
@@ -12949,7 +12959,7 @@ function createQueryBuilder(state) {
|
|
|
12949
12959
|
const subQb = {
|
|
12950
12960
|
where: (col, op, val) => {
|
|
12951
12961
|
validateIdentifier(col, "relationship subquery condition");
|
|
12952
|
-
return `${targetTable}.${col} ${op
|
|
12962
|
+
return `${targetTable}.${col} ${assertSafeWhereOperator(op, "whereHas callback")} ${formatSubqueryValue(val)}`;
|
|
12953
12963
|
}
|
|
12954
12964
|
};
|
|
12955
12965
|
const condition = callback(subQb);
|
|
@@ -12978,7 +12988,7 @@ function createQueryBuilder(state) {
|
|
|
12978
12988
|
const subQb = {
|
|
12979
12989
|
where: (col, op, val) => {
|
|
12980
12990
|
validateIdentifier(col, "relationship subquery condition");
|
|
12981
|
-
return `${targetTable}.${col} ${op
|
|
12991
|
+
return `${targetTable}.${col} ${assertSafeWhereOperator(op, "whereHas callback")} ${formatSubqueryValue(val)}`;
|
|
12982
12992
|
}
|
|
12983
12993
|
};
|
|
12984
12994
|
const condition = callback(subQb);
|
|
@@ -14493,7 +14503,14 @@ function createQueryBuilder(state) {
|
|
|
14493
14503
|
return this;
|
|
14494
14504
|
},
|
|
14495
14505
|
toSQL() {
|
|
14496
|
-
|
|
14506
|
+
const sqlText = reorderSelectClauses(text);
|
|
14507
|
+
return {
|
|
14508
|
+
sql: sqlText,
|
|
14509
|
+
toString: () => sqlText,
|
|
14510
|
+
execute: () => ensureBuilt().execute(),
|
|
14511
|
+
values: () => ensureBuilt().values(),
|
|
14512
|
+
raw: () => ensureBuilt().raw()
|
|
14513
|
+
};
|
|
14497
14514
|
},
|
|
14498
14515
|
async value(column) {
|
|
14499
14516
|
const q = sql`${ensureBuilt()} LIMIT 1`;
|
|
@@ -14947,20 +14964,36 @@ function createQueryBuilder(state) {
|
|
|
14947
14964
|
if (typeof prop === "string" && (prop.startsWith("where") || prop.startsWith("orWhere") || prop.startsWith("andWhere"))) {
|
|
14948
14965
|
const isOr = prop.startsWith("orWhere");
|
|
14949
14966
|
const isAnd = prop.startsWith("andWhere");
|
|
14950
|
-
const
|
|
14951
|
-
|
|
14967
|
+
const cacheKey = `${String(table)}|${prop}`;
|
|
14968
|
+
let chosen = dynamicWhereColumnCache.get(cacheKey);
|
|
14969
|
+
if (chosen === undefined) {
|
|
14970
|
+
const raw = prop.replace(/^(?:or|and)?where/i, "");
|
|
14971
|
+
if (!raw) {
|
|
14972
|
+
dynamicWhereColumnCache.set(cacheKey, "");
|
|
14973
|
+
chosen = "";
|
|
14974
|
+
} else {
|
|
14975
|
+
const lowerFirst = raw.charAt(0).toLowerCase() + raw.slice(1);
|
|
14976
|
+
const snake = raw.replace(/([A-Z])/g, "_$1").toLowerCase().replace(/^_/, "");
|
|
14977
|
+
const available = schema ? Object.keys(schema[String(table)]?.columns ?? {}) : [];
|
|
14978
|
+
chosen = [snake, lowerFirst, lowerFirst.toLowerCase()].find((n) => available.includes(n)) ?? snake;
|
|
14979
|
+
dynamicWhereColumnCache.set(cacheKey, chosen);
|
|
14980
|
+
}
|
|
14981
|
+
}
|
|
14982
|
+
if (chosen === "")
|
|
14952
14983
|
return () => receiver;
|
|
14953
|
-
const
|
|
14954
|
-
const toSnake = (s) => s.replace(/([A-Z])/g, "_$1").toLowerCase().replace(/^_/, "");
|
|
14955
|
-
const snake = toSnake(raw);
|
|
14956
|
-
const tbl = String(table);
|
|
14957
|
-
const available = schema ? Object.keys(schema[tbl]?.columns ?? {}) : [];
|
|
14958
|
-
const chosen = [snake, lowerFirst, lowerFirst.toLowerCase()].find((n) => available.includes(n)) ?? snake;
|
|
14984
|
+
const column = chosen;
|
|
14959
14985
|
return (value) => {
|
|
14960
|
-
const expr = Array.isArray(value) ? sql`${sql(
|
|
14986
|
+
const expr = Array.isArray(value) ? sql`${sql(column)} IN ${sql(value)}` : sql`${sql(column)} = ${value}`;
|
|
14961
14987
|
built = isOr ? sql`${ensureBuilt()} OR ${expr}` : isAnd ? sql`${ensureBuilt()} AND ${expr}` : sql`${ensureBuilt()} WHERE ${expr}`;
|
|
14962
|
-
|
|
14963
|
-
|
|
14988
|
+
if (Array.isArray(value)) {
|
|
14989
|
+
const phs = getPlaceholders(value.length, whereParams.length + 1);
|
|
14990
|
+
addWhereText(isOr ? "OR" : isAnd ? "AND" : "WHERE", `${column} IN (${phs})`);
|
|
14991
|
+
whereParams.push(...value);
|
|
14992
|
+
} else {
|
|
14993
|
+
const ph = getPlaceholder(whereParams.length + 1);
|
|
14994
|
+
addWhereText(isOr ? "OR" : isAnd ? "AND" : "WHERE", `${column} = ${ph}`);
|
|
14995
|
+
whereParams.push(value);
|
|
14996
|
+
}
|
|
14964
14997
|
return receiver;
|
|
14965
14998
|
};
|
|
14966
14999
|
}
|
|
@@ -16208,7 +16241,7 @@ function clearQueryCache() {
|
|
|
16208
16241
|
function setQueryCacheMaxSize(size) {
|
|
16209
16242
|
queryCache.setMaxSize(size);
|
|
16210
16243
|
}
|
|
16211
|
-
var SQL_PATTERNS, SAFE_WHERE_OPERATORS, queryCache;
|
|
16244
|
+
var SQL_PATTERNS, SAFE_WHERE_OPERATORS, warnedSqlFragmentContexts, queryCache, reorderCache, REORDER_CACHE_MAX = 500;
|
|
16212
16245
|
var init_client = __esm(() => {
|
|
16213
16246
|
init_config();
|
|
16214
16247
|
init_db();
|
|
@@ -16243,7 +16276,9 @@ var init_client = __esm(() => {
|
|
|
16243
16276
|
"between",
|
|
16244
16277
|
"not between"
|
|
16245
16278
|
]);
|
|
16279
|
+
warnedSqlFragmentContexts = new Set;
|
|
16246
16280
|
queryCache = new QueryCache;
|
|
16281
|
+
reorderCache = new Map;
|
|
16247
16282
|
});
|
|
16248
16283
|
|
|
16249
16284
|
// src/actions/cache.ts
|
|
@@ -23821,8 +23856,15 @@ function getModelFromRegistry(name) {
|
|
|
23821
23856
|
return _getModel(name);
|
|
23822
23857
|
}
|
|
23823
23858
|
function toPostgresPlaceholders(sql2) {
|
|
23859
|
+
const hit = pgPlaceholderCache.get(sql2);
|
|
23860
|
+
if (hit !== undefined)
|
|
23861
|
+
return hit;
|
|
23824
23862
|
let i2 = 0;
|
|
23825
|
-
|
|
23863
|
+
const out = sql2.replace(/\?/g, () => `$${++i2}`);
|
|
23864
|
+
if (pgPlaceholderCache.size >= PG_PLACEHOLDER_CACHE_MAX)
|
|
23865
|
+
pgPlaceholderCache.clear();
|
|
23866
|
+
pgPlaceholderCache.set(sql2, out);
|
|
23867
|
+
return out;
|
|
23826
23868
|
}
|
|
23827
23869
|
function extractChanges(res) {
|
|
23828
23870
|
if (res == null)
|
|
@@ -23954,21 +23996,25 @@ function timestampsEnabled(definition) {
|
|
|
23954
23996
|
return Boolean(t2?.useTimestamps || t2?.timestampable);
|
|
23955
23997
|
}
|
|
23956
23998
|
function collectBelongsToManyKeys(definition) {
|
|
23999
|
+
const cached = btmKeysCache.get(definition);
|
|
24000
|
+
if (cached)
|
|
24001
|
+
return cached;
|
|
23957
24002
|
const keys = new Set;
|
|
23958
24003
|
const rel = definition.belongsToMany;
|
|
23959
|
-
if (
|
|
23960
|
-
|
|
23961
|
-
|
|
23962
|
-
|
|
23963
|
-
|
|
23964
|
-
|
|
23965
|
-
|
|
23966
|
-
|
|
23967
|
-
}
|
|
23968
|
-
|
|
23969
|
-
|
|
23970
|
-
|
|
24004
|
+
if (rel) {
|
|
24005
|
+
if (Array.isArray(rel)) {
|
|
24006
|
+
for (const item of rel) {
|
|
24007
|
+
if (typeof item === "string")
|
|
24008
|
+
keys.add(item.toLowerCase());
|
|
24009
|
+
else if (item && typeof item === "object" && item.model)
|
|
24010
|
+
keys.add(item.model.toLowerCase());
|
|
24011
|
+
}
|
|
24012
|
+
} else if (typeof rel === "object") {
|
|
24013
|
+
for (const k2 of Object.keys(rel))
|
|
24014
|
+
keys.add(k2);
|
|
24015
|
+
}
|
|
23971
24016
|
}
|
|
24017
|
+
btmKeysCache.set(definition, keys);
|
|
23972
24018
|
return keys;
|
|
23973
24019
|
}
|
|
23974
24020
|
|
|
@@ -25480,11 +25526,13 @@ async function seedModel(definition, count, faker) {
|
|
|
25480
25526
|
await exec.run(`INSERT INTO ${definition.table} (${columns.join(", ")}) VALUES (${columns.map(() => "?").join(", ")})`, Object.values(data));
|
|
25481
25527
|
}
|
|
25482
25528
|
}
|
|
25483
|
-
var SAFE_SQL_IDENTIFIER, _getModel = null, globalDb = null, _executor = null, _executorForDb = null, _executorDialect = null, _executorDatabase = null, SOFT_DELETE_COLUMN = "deleted_at", BTM_RELATED_ALIAS = "__btm_rel__", snakeCaseCache, tableNameCache, relationCache, RELATION_CACHE_MAX = 1000;
|
|
25529
|
+
var SAFE_SQL_IDENTIFIER, _getModel = null, pgPlaceholderCache, PG_PLACEHOLDER_CACHE_MAX = 500, globalDb = null, _executor = null, _executorForDb = null, _executorDialect = null, _executorDatabase = null, SOFT_DELETE_COLUMN = "deleted_at", BTM_RELATED_ALIAS = "__btm_rel__", btmKeysCache, snakeCaseCache, tableNameCache, relationCache, RELATION_CACHE_MAX = 1000;
|
|
25484
25530
|
var init_orm = __esm(() => {
|
|
25485
25531
|
init_config();
|
|
25486
25532
|
init_db();
|
|
25487
25533
|
SAFE_SQL_IDENTIFIER = /^[A-Z_][A-Z0-9_]*$/i;
|
|
25534
|
+
pgPlaceholderCache = new Map;
|
|
25535
|
+
btmKeysCache = new WeakMap;
|
|
25488
25536
|
snakeCaseCache = new Map;
|
|
25489
25537
|
tableNameCache = new Map;
|
|
25490
25538
|
relationCache = new Map;
|
package/package.json
CHANGED