n8n/packages/@n8n/nodes-langchain/utils/sqlIdentifier.ts
n8n-assistant[bot] 46761a60a9
chore: Bundle/2.x (#41035)
Co-authored-by: Matsu <matias.huhta@n8n.io>
Co-authored-by: Bernhard Wittmann <bernhard.wittmann@n8n.io>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: Elias Meire <elias@meire.dev>
Co-authored-by: n8n-assistant[bot] <n8n-assistant[bot]@users.noreply.github.com>
Co-authored-by: João Almeida <jg.almeida56@gmail.com>
Co-authored-by: Savelii <savelii.sychov@n8n.io>
Co-authored-by: Rick Gale <richardgale90@gmail.com>
Co-authored-by: Emilia <100027345+sovietspaceship@users.noreply.github.com>
Co-authored-by: oleg <me@olegivaniv.com>
Co-authored-by: RomanDavydchuk <roman.davydchuk@n8n.io>
Co-authored-by: Stephen Wright <sjw948@gmail.com>
Co-authored-by: Yuliia Pominchuk <31064937+yuliia-pominchuk@users.noreply.github.com>
Co-authored-by: Shireen Missi <94372015+ShireenMissi@users.noreply.github.com>
Co-authored-by: Miguel Ángel Moreno <1411774+miguelsaddress@users.noreply.github.com>
Co-authored-by: Anne Aguirre <anne.aguirre@n8n.io>
Co-authored-by: Dimitri Lavrenük <20122620+dlavrenuek@users.noreply.github.com>
Co-authored-by: Kai <kai.hartlage@n8n.io>
Co-authored-by: Luigi Gubello <27034386+luigigubello@users.noreply.github.com>
Co-authored-by: Danny Martini <danny@n8n.io>
Co-authored-by: MHMD <45968454+dou-mhmd@users.noreply.github.com>
Co-authored-by: Mutasem Aldmour <4711238+mutdmour@users.noreply.github.com>
Co-authored-by: Iván Ovejero <ivov.src@gmail.com>
Co-authored-by: Charlie Kolb <charlie@n8n.io>
2026-10-14 06:33:56 +00:00

104 lines
3.8 KiB
TypeScript

import { isRecord } from '@n8n/utils/is-record';
import pg from 'pg';
/**
* Matches a plain, unquoted SQL identifier: a leading letter or underscore
* followed by letters, digits, underscores or dollar signs. Values that match
* this pattern are safe to interpolate into SQL both quoted and unquoted.
*/
const SAFE_SQL_IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_$]*$/;
/**
* Quotes a single SQL identifier so it can be safely interpolated into a
* statement. Wraps the value in double quotes and doubles any embedded quotes.
*/
export function escapeSqlIdentifier(name: string): string {
return pg.escapeIdentifier(name);
}
/**
* Quotes a possibly schema-qualified identifier (`schema.table`) by escaping
* each dot-separated part independently. This preserves schema qualification
* while ensuring every segment is safely quoted.
*
* Each part is lower-cased first to mirror PostgreSQL's folding of unquoted
* identifiers: these names were historically interpolated unquoted, so an
* existing mixed-case config like `MyTable` keeps resolving to `mytable`
* rather than a new, case-sensitive `"MyTable"`.
*/
export function escapeQualifiedSqlIdentifier(name: string): string {
return name
.split('.')
.map((part) => escapeSqlIdentifier(part.toLowerCase()))
.join('.');
}
/**
* Returns true when the value is a plain SQL identifier that requires no
* quoting and is safe to interpolate verbatim.
*/
export function isSafeSqlIdentifier(name: string): boolean {
return SAFE_SQL_IDENTIFIER.test(name);
}
/**
* Returns true when the value is either a plain identifier (`name`) or a
* schema-qualified one (`schema.name`) — at most two dot-separated parts, each a
* plain identifier. Non-strings, empty values, empty segments and anything with
* more than two parts (e.g. `a.b.c`) are rejected, matching the documented
* "plain name or optional schema. prefix" contract.
*/
export function isSafeQualifiedSqlIdentifier(name: unknown): boolean {
if (typeof name !== 'string') return false;
const parts = name.split('.');
return parts.length <= 2 && parts.every(isSafeSqlIdentifier);
}
/**
* Returns the first metadata-filter key that could break out of the
* single-quoted SQL literal the store interpolates it into, or undefined when
* every key is safe.
*/
export function findUnsafeMetadataFilterKey(
filter: Record<string, unknown> | undefined,
): string | undefined {
if (!filter) return undefined;
return Object.keys(filter).find((key) => key.includes("'") || key.includes('\\'));
}
/**
* Matches a field name that Oracle accepts as an unquoted SQL/JSON path step.
* Oracle documents the unquoted form as a leading letter followed by letters and
* digits; underscore is allowed here because it is inert in both the path
* grammar and the surrounding string literal, and is common in existing keys.
*/
const SAFE_JSON_PATH_FIELD = /^[A-Za-z_][A-Za-z0-9_]*$/;
/**
* Returns the first metadata-filter key that the Oracle store cannot resolve as
* an unquoted SQL/JSON path step, or undefined when every key is usable.
*
* Walks `$and` and `$or` because the store expands them before building the
* path, so a nested key reaches the same place a top-level one does.
*/
export function findUnsupportedJsonPathFilterKey(filter: unknown): string | undefined {
if (!isRecord(filter)) return undefined;
for (const [key, value] of Object.entries(filter)) {
if (key === '$and' || key === '$or') {
if (!Array.isArray(value)) continue;
for (const condition of value) {
const unsupported = findUnsupportedJsonPathFilterKey(condition);
if (unsupported !== undefined) return unsupported;
}
continue;
}
// Only the field name reaches the path. Operator names in value position
// are matched against a fixed set by the store, and values are bound.
if (!SAFE_JSON_PATH_FIELD.test(key)) return key;
}
return undefined;
}