graphile-sql-expression-validator
Advanced tools
| /** | ||
| * FieldType and FieldDefault: structured JSONB models for PostgreSQL | ||
| * type declarations and default value expressions. | ||
| * | ||
| * These models provide a safe, validated representation that can be: | ||
| * 1. Validated structurally (JSON shape, identifier patterns, allowlists) | ||
| * 2. Converted to pgsql-parser AST nodes | ||
| * 3. Validated at the AST level (reuses the expression validator for defaults) | ||
| * 4. Deparsed to canonical SQL text | ||
| * | ||
| * The key security insight: the structured model eliminates entire attack | ||
| * categories by construction (no subqueries, no column refs, no stacked | ||
| * statements). What remains is identifier validation + allowlist enforcement. | ||
| */ | ||
| import type { SqlExpressionValidatorOptions } from './validator'; | ||
| /** | ||
| * Structured representation of a PostgreSQL data type. | ||
| * | ||
| * @example Simple type | ||
| * { name: "text" } | ||
| * | ||
| * @example Type with arguments | ||
| * { name: "geometry", args: ["Point", 4326] } | ||
| * { name: "numeric", args: [10, 2] } | ||
| * { name: "vector", args: [1536] } | ||
| * | ||
| * @example Array type | ||
| * { name: "text", array_dimensions: 1 } | ||
| * | ||
| * @example Schema-qualified type | ||
| * { name: "my_type", schema: "my_schema" } | ||
| * | ||
| * @example Interval with field range | ||
| * { name: "interval", range: ["day", "second"] } | ||
| */ | ||
| export interface FieldType { | ||
| /** Type name (required). Must be a valid SQL identifier. */ | ||
| name: string; | ||
| /** Schema qualifier (optional). Must be a valid SQL identifier. */ | ||
| schema?: string; | ||
| /** Type arguments (optional). Each is a string identifier, number, or boolean. */ | ||
| args?: (string | number | boolean)[]; | ||
| /** Number of array dimensions (optional). 1 = `text[]`, 2 = `text[][]`. */ | ||
| array_dimensions?: number; | ||
| /** Interval field range (optional). 1-2 elements: ["day"] or ["day", "second"]. */ | ||
| range?: string[]; | ||
| } | ||
| /** | ||
| * Argument to a function in a FieldDefault expression. | ||
| * Can be a literal value or a nested FieldDefault (recursive). | ||
| */ | ||
| export type FieldDefaultArg = string | number | boolean | null | FieldDefault; | ||
| /** | ||
| * Structured representation of a PostgreSQL default value expression. | ||
| * | ||
| * @example Literal values | ||
| * { value: false } | ||
| * { value: 0 } | ||
| * { value: "pooled" } | ||
| * | ||
| * @example Cast expression | ||
| * { value: "{}", cast: { name: "jsonb" } } | ||
| * { value: "15 minutes", cast: { name: "interval" } } | ||
| * | ||
| * @example Simple function call | ||
| * { function: "now" } | ||
| * { function: "gen_random_uuid" } | ||
| * | ||
| * @example Schema-qualified function | ||
| * { function: "current_user_id", schema: "jwt_public" } | ||
| * | ||
| * @example Function with arguments (nested) | ||
| * { function: "encode", args: [{ function: "gen_random_bytes", args: [16] }, "hex"] } | ||
| * | ||
| * @example Function with cast | ||
| * { function: "lpad", args: ["", 32, "0"], cast: { name: "bit", args: [32] } } | ||
| */ | ||
| export interface FieldDefault { | ||
| /** Literal value (string, number, boolean, null, or array). */ | ||
| value?: string | number | boolean | null | unknown[]; | ||
| /** Function name. Must be a valid SQL identifier. */ | ||
| function?: string; | ||
| /** Schema qualifier for function (optional). */ | ||
| schema?: string; | ||
| /** Function arguments (optional, recursive). */ | ||
| args?: FieldDefaultArg[]; | ||
| /** Output type cast (optional). Reuses FieldType shape. */ | ||
| cast?: FieldType; | ||
| } | ||
| export interface FieldTypeValidationOptions { | ||
| /** Allowed schemas for schema-qualified types. */ | ||
| allowedTypeSchemas?: string[]; | ||
| /** Additional forbidden type names beyond the built-in set. */ | ||
| additionalForbiddenTypes?: string[]; | ||
| } | ||
| export interface FieldDefaultValidationOptions extends SqlExpressionValidatorOptions { | ||
| /** Allowed schemas for schema-qualified types in casts. */ | ||
| allowedTypeSchemas?: string[]; | ||
| /** Additional forbidden type names beyond the built-in set. */ | ||
| additionalForbiddenTypes?: string[]; | ||
| /** Maximum nesting depth for recursive args. Defaults to 4. */ | ||
| maxNestingDepth?: number; | ||
| /** | ||
| * Map of schema → allowed functions for schema-qualified calls. | ||
| * Functions in this map do NOT need to also appear in allowedFunctions. | ||
| */ | ||
| allowedSchemaFunctions?: Record<string, string[]>; | ||
| } | ||
| export interface FieldTypeValidationResult { | ||
| valid: boolean; | ||
| /** Canonical SQL text (e.g., "geometry(Point,4326)", "text[]") */ | ||
| canonicalSql?: string; | ||
| /** The TypeName AST node */ | ||
| ast?: Record<string, unknown>; | ||
| error?: string; | ||
| } | ||
| export interface FieldDefaultValidationResult { | ||
| valid: boolean; | ||
| /** Canonical SQL text (e.g., "now()", "'{}'::jsonb") */ | ||
| canonicalSql?: string; | ||
| /** The expression AST node */ | ||
| ast?: Record<string, unknown>; | ||
| error?: string; | ||
| } | ||
| /** | ||
| * PostgreSQL system catalog type casts that are forbidden. | ||
| * These OID-alias types can be used to probe the system catalog. | ||
| * Shared with the expression validator (same list). | ||
| */ | ||
| export declare const FORBIDDEN_TYPES: Set<string>; | ||
| /** | ||
| * Convert a FieldType to a pgsql-parser TypeName AST node. | ||
| * | ||
| * The TypeName node format: | ||
| * { | ||
| * names: [{ String: { sval: "schema" } }, { String: { sval: "name" } }], | ||
| * typmods: [<arg AST nodes>], | ||
| * arrayBounds: [{ Integer: { ival: -1 } }, ...], | ||
| * typemod: -1 | ||
| * } | ||
| */ | ||
| export declare function fieldTypeToAst(ft: FieldType): Record<string, unknown>; | ||
| /** | ||
| * Convert a FieldType to its canonical SQL text representation. | ||
| * | ||
| * Uses the deparser for the base type, then appends array brackets. | ||
| * Handles interval range fields manually since the deparser needs | ||
| * special handling for INTERVAL ... DAY TO SECOND syntax. | ||
| */ | ||
| export declare function fieldTypeToSql(ft: FieldType): string; | ||
| /** | ||
| * Convert a FieldDefault to a pgsql-parser expression AST node. | ||
| * | ||
| * Handles: | ||
| * - Literal values → A_Const | ||
| * - Function calls → FuncCall (with recursive args) | ||
| * - Type casts → TypeCast wrapping the inner expression | ||
| * - Combinations (function + cast, value + cast) | ||
| */ | ||
| export declare function fieldDefaultToAst(fd: FieldDefault, depth?: number): Record<string, unknown>; | ||
| /** | ||
| * Convert a FieldDefault to its canonical SQL text representation. | ||
| */ | ||
| export declare function fieldDefaultToSql(fd: FieldDefault): string; | ||
| /** | ||
| * Validate a FieldType object structurally. | ||
| * | ||
| * Checks: | ||
| * - Object shape (no unknown keys, required keys present) | ||
| * - `name` is a valid SQL identifier | ||
| * - `name` is not a forbidden type (regclass, regtype, etc.) | ||
| * - `schema` (if present) is a valid SQL identifier and on the allowlist | ||
| * - `args` elements are literals (string, number, boolean) | ||
| * - `array_dimensions` is a positive integer | ||
| * - `range` elements are valid interval field qualifiers | ||
| * | ||
| * Then converts to SQL text for canonical output. | ||
| */ | ||
| export declare function validateFieldType(ft: unknown, options?: FieldTypeValidationOptions): FieldTypeValidationResult; | ||
| /** | ||
| * Validate a FieldDefault object. | ||
| * | ||
| * Structural validation + AST conversion + expression validation. | ||
| * | ||
| * 1. Validates the JSON structure (shape, types, required fields) | ||
| * 2. Converts to pgsql-parser expression AST | ||
| * 3. Validates the AST through the expression validator (function/schema allowlists) | ||
| * 4. Deparses to canonical SQL text | ||
| */ | ||
| export declare function validateFieldDefault(fd: unknown, options?: FieldDefaultValidationOptions): FieldDefaultValidationResult; |
| /** | ||
| * FieldType and FieldDefault: structured JSONB models for PostgreSQL | ||
| * type declarations and default value expressions. | ||
| * | ||
| * These models provide a safe, validated representation that can be: | ||
| * 1. Validated structurally (JSON shape, identifier patterns, allowlists) | ||
| * 2. Converted to pgsql-parser AST nodes | ||
| * 3. Validated at the AST level (reuses the expression validator for defaults) | ||
| * 4. Deparsed to canonical SQL text | ||
| * | ||
| * The key security insight: the structured model eliminates entire attack | ||
| * categories by construction (no subqueries, no column refs, no stacked | ||
| * statements). What remains is identifier validation + allowlist enforcement. | ||
| */ | ||
| import { deparseSync } from 'pgsql-deparser'; | ||
| import { validateAst } from './validator'; | ||
| // ─── Constants ─────────────────────────────────────────────────── | ||
| /** Valid SQL identifier pattern. */ | ||
| const IDENTIFIER_PATTERN = /^[a-zA-Z_][a-zA-Z0-9_]*$/; | ||
| /** | ||
| * PostgreSQL system catalog type casts that are forbidden. | ||
| * These OID-alias types can be used to probe the system catalog. | ||
| * Shared with the expression validator (same list). | ||
| */ | ||
| export const FORBIDDEN_TYPES = new Set([ | ||
| 'regclass', | ||
| 'regtype', | ||
| 'regproc', | ||
| 'regprocedure', | ||
| 'regoper', | ||
| 'regoperator', | ||
| 'regnamespace', | ||
| 'regrole', | ||
| 'regconfig', | ||
| 'regdictionary' | ||
| ]); | ||
| /** | ||
| * Valid interval field qualifiers for the `range` property. | ||
| */ | ||
| const VALID_INTERVAL_FIELDS = new Set([ | ||
| 'year', 'month', 'day', 'hour', 'minute', 'second' | ||
| ]); | ||
| /** Allowed keys in a FieldType object. */ | ||
| const FIELD_TYPE_KEYS = new Set(['name', 'schema', 'args', 'array_dimensions', 'range']); | ||
| /** Allowed keys in a FieldDefault object. */ | ||
| const FIELD_DEFAULT_KEYS = new Set(['value', 'function', 'schema', 'args', 'cast']); | ||
| // ─── Internal: AST Builders ────────────────────────────────────── | ||
| /** Build a pgsql-parser String node. */ | ||
| function astString(sval) { | ||
| return { String: { sval } }; | ||
| } | ||
| /** Build a pgsql-parser A_Const node for an integer. */ | ||
| function astConstInt(ival) { | ||
| return { A_Const: { ival: { ival } } }; | ||
| } | ||
| /** Build a pgsql-parser A_Const node for a string. */ | ||
| function astConstStr(sval) { | ||
| return { A_Const: { sval: { sval } } }; | ||
| } | ||
| /** Build a pgsql-parser A_Const node for a boolean. */ | ||
| function astConstBool(boolval) { | ||
| if (boolval) { | ||
| return { A_Const: { boolval: { boolval: true } } }; | ||
| } | ||
| return { A_Const: { boolval: {} } }; | ||
| } | ||
| /** Build a pgsql-parser A_Const node for NULL. */ | ||
| function astConstNull() { | ||
| return { A_Const: { isnull: true } }; | ||
| } | ||
| // ─── Converters: FieldType → AST ───────────────────────────────── | ||
| /** | ||
| * Convert a FieldType to a pgsql-parser TypeName AST node. | ||
| * | ||
| * The TypeName node format: | ||
| * { | ||
| * names: [{ String: { sval: "schema" } }, { String: { sval: "name" } }], | ||
| * typmods: [<arg AST nodes>], | ||
| * arrayBounds: [{ Integer: { ival: -1 } }, ...], | ||
| * typemod: -1 | ||
| * } | ||
| */ | ||
| export function fieldTypeToAst(ft) { | ||
| const names = []; | ||
| if (ft.schema) { | ||
| names.push(astString(ft.schema)); | ||
| } | ||
| names.push(astString(ft.name)); | ||
| const typeName = { | ||
| names, | ||
| typemod: -1 | ||
| }; | ||
| // Type arguments → typmods | ||
| if (ft.args && ft.args.length > 0) { | ||
| const typmods = []; | ||
| for (const arg of ft.args) { | ||
| if (typeof arg === 'number') { | ||
| typmods.push(astConstInt(arg)); | ||
| } | ||
| else if (typeof arg === 'string') { | ||
| // String args in type modifiers are identifiers (e.g., "Point" in geometry(Point, 4326)) | ||
| // PostgreSQL parser represents these as ColumnRef nodes | ||
| typmods.push({ | ||
| ColumnRef: { | ||
| fields: [astString(arg.toLowerCase())] | ||
| } | ||
| }); | ||
| } | ||
| else if (typeof arg === 'boolean') { | ||
| typmods.push(astConstBool(arg)); | ||
| } | ||
| } | ||
| typeName.typmods = typmods; | ||
| } | ||
| // Array dimensions → arrayBounds | ||
| if (ft.array_dimensions && ft.array_dimensions > 0) { | ||
| const arrayBounds = []; | ||
| for (let i = 0; i < ft.array_dimensions; i++) { | ||
| arrayBounds.push({ Integer: { ival: -1 } }); | ||
| } | ||
| typeName.arrayBounds = arrayBounds; | ||
| } | ||
| // Interval range is encoded as a numeric typmod by PostgreSQL. | ||
| // We store it as readable strings; conversion to the numeric bitmask | ||
| // happens in field_type_to_text() at the SQL layer. | ||
| // For AST purposes, we skip typmods for interval range — the deparser | ||
| // handles it via the INTERVAL keyword + field qualifiers. | ||
| return typeName; | ||
| } | ||
| /** | ||
| * Convert a FieldType to its canonical SQL text representation. | ||
| * | ||
| * Uses the deparser for the base type, then appends array brackets. | ||
| * Handles interval range fields manually since the deparser needs | ||
| * special handling for INTERVAL ... DAY TO SECOND syntax. | ||
| */ | ||
| export function fieldTypeToSql(ft) { | ||
| // Build the base type string | ||
| let sql = ''; | ||
| if (ft.schema) { | ||
| sql += ft.schema + '.'; | ||
| } | ||
| sql += ft.name; | ||
| // Type arguments | ||
| if (ft.args && ft.args.length > 0) { | ||
| const argParts = ft.args.map(arg => { | ||
| if (typeof arg === 'string') | ||
| return arg; | ||
| return String(arg); | ||
| }); | ||
| sql += '(' + argParts.join(', ') + ')'; | ||
| } | ||
| // Interval range | ||
| if (ft.range && ft.range.length > 0 && ft.name.toLowerCase() === 'interval') { | ||
| if (ft.range.length === 1) { | ||
| sql += ' ' + ft.range[0]; | ||
| } | ||
| else if (ft.range.length === 2) { | ||
| sql += ' ' + ft.range[0] + ' to ' + ft.range[1]; | ||
| } | ||
| } | ||
| // Array dimensions | ||
| if (ft.array_dimensions && ft.array_dimensions > 0) { | ||
| for (let i = 0; i < ft.array_dimensions; i++) { | ||
| sql += '[]'; | ||
| } | ||
| } | ||
| return sql; | ||
| } | ||
| // ─── Converters: FieldDefault → AST ───────────────────────────── | ||
| /** | ||
| * Convert a FieldDefault to a pgsql-parser expression AST node. | ||
| * | ||
| * Handles: | ||
| * - Literal values → A_Const | ||
| * - Function calls → FuncCall (with recursive args) | ||
| * - Type casts → TypeCast wrapping the inner expression | ||
| * - Combinations (function + cast, value + cast) | ||
| */ | ||
| export function fieldDefaultToAst(fd, depth = 0) { | ||
| if (depth > 10) { | ||
| throw new Error('FieldDefault nesting exceeds maximum depth'); | ||
| } | ||
| let expr; | ||
| if (fd.function !== undefined) { | ||
| // Function call | ||
| const funcname = []; | ||
| if (fd.schema) { | ||
| funcname.push(astString(fd.schema)); | ||
| } | ||
| funcname.push(astString(fd.function)); | ||
| const funcCall = { | ||
| funcname, | ||
| funcformat: 'COERCE_EXPLICIT_CALL' | ||
| }; | ||
| // Function arguments | ||
| if (fd.args && fd.args.length > 0) { | ||
| const astArgs = []; | ||
| for (const arg of fd.args) { | ||
| astArgs.push(fieldDefaultArgToAst(arg, depth + 1)); | ||
| } | ||
| funcCall.args = astArgs; | ||
| } | ||
| expr = { FuncCall: funcCall }; | ||
| } | ||
| else if (fd.value !== undefined) { | ||
| // Literal value | ||
| expr = literalToAst(fd.value); | ||
| } | ||
| else { | ||
| throw new Error('FieldDefault must have either "function" or "value"'); | ||
| } | ||
| // Wrap in TypeCast if cast is present | ||
| if (fd.cast) { | ||
| expr = { | ||
| TypeCast: { | ||
| arg: expr, | ||
| typeName: fieldTypeToAst(fd.cast) | ||
| } | ||
| }; | ||
| } | ||
| return expr; | ||
| } | ||
| /** | ||
| * Convert a FieldDefaultArg to an AST node. | ||
| */ | ||
| function fieldDefaultArgToAst(arg, depth) { | ||
| if (arg === null) { | ||
| return astConstNull(); | ||
| } | ||
| if (typeof arg === 'string') { | ||
| return astConstStr(arg); | ||
| } | ||
| if (typeof arg === 'number') { | ||
| if (Number.isInteger(arg)) { | ||
| return astConstInt(arg); | ||
| } | ||
| // Float — represented as string in pgsql-parser | ||
| return { A_Const: { fval: { fval: String(arg) } } }; | ||
| } | ||
| if (typeof arg === 'boolean') { | ||
| return astConstBool(arg); | ||
| } | ||
| // Nested FieldDefault object | ||
| return fieldDefaultToAst(arg, depth); | ||
| } | ||
| /** | ||
| * Convert a literal value to an A_Const (or A_ArrayExpr) AST node. | ||
| */ | ||
| function literalToAst(value) { | ||
| if (value === null) { | ||
| return astConstNull(); | ||
| } | ||
| if (typeof value === 'string') { | ||
| return astConstStr(value); | ||
| } | ||
| if (typeof value === 'number') { | ||
| if (Number.isInteger(value)) { | ||
| return astConstInt(value); | ||
| } | ||
| return { A_Const: { fval: { fval: String(value) } } }; | ||
| } | ||
| if (typeof value === 'boolean') { | ||
| return astConstBool(value); | ||
| } | ||
| if (Array.isArray(value)) { | ||
| const elements = value.map(el => { | ||
| if (el === null) | ||
| return astConstNull(); | ||
| if (typeof el === 'string') | ||
| return astConstStr(el); | ||
| if (typeof el === 'number') { | ||
| if (Number.isInteger(el)) | ||
| return astConstInt(el); | ||
| return { A_Const: { fval: { fval: String(el) } } }; | ||
| } | ||
| if (typeof el === 'boolean') | ||
| return astConstBool(el); | ||
| throw new Error(`Unsupported array element type: ${typeof el}`); | ||
| }); | ||
| return { A_ArrayExpr: { elements } }; | ||
| } | ||
| throw new Error(`Unsupported literal type: ${typeof value}`); | ||
| } | ||
| /** | ||
| * Convert a FieldDefault to its canonical SQL text representation. | ||
| */ | ||
| export function fieldDefaultToSql(fd) { | ||
| const ast = fieldDefaultToAst(fd); | ||
| return deparseSync([ast]); | ||
| } | ||
| // ─── Structural Validators ─────────────────────────────────────── | ||
| /** | ||
| * Validate a FieldType object structurally. | ||
| * | ||
| * Checks: | ||
| * - Object shape (no unknown keys, required keys present) | ||
| * - `name` is a valid SQL identifier | ||
| * - `name` is not a forbidden type (regclass, regtype, etc.) | ||
| * - `schema` (if present) is a valid SQL identifier and on the allowlist | ||
| * - `args` elements are literals (string, number, boolean) | ||
| * - `array_dimensions` is a positive integer | ||
| * - `range` elements are valid interval field qualifiers | ||
| * | ||
| * Then converts to SQL text for canonical output. | ||
| */ | ||
| export function validateFieldType(ft, options = {}) { | ||
| const { allowedTypeSchemas = [], additionalForbiddenTypes = [] } = options; | ||
| // Must be a non-null object | ||
| if (!ft || typeof ft !== 'object' || Array.isArray(ft)) { | ||
| return { valid: false, error: 'FieldType must be a non-null object' }; | ||
| } | ||
| const obj = ft; | ||
| // No unknown keys | ||
| for (const key of Object.keys(obj)) { | ||
| if (!FIELD_TYPE_KEYS.has(key)) { | ||
| return { valid: false, error: `Unknown FieldType key: "${key}"` }; | ||
| } | ||
| } | ||
| // name: required, valid identifier | ||
| if (typeof obj.name !== 'string') { | ||
| return { valid: false, error: 'FieldType.name is required and must be a string' }; | ||
| } | ||
| if (!IDENTIFIER_PATTERN.test(obj.name)) { | ||
| return { | ||
| valid: false, | ||
| error: `FieldType.name must be a valid SQL identifier, got: "${obj.name}"` | ||
| }; | ||
| } | ||
| // Forbidden types | ||
| const nameLower = obj.name.toLowerCase(); | ||
| if (FORBIDDEN_TYPES.has(nameLower)) { | ||
| return { | ||
| valid: false, | ||
| error: `Forbidden type: "${obj.name}" — system catalog OID-alias types are not allowed` | ||
| }; | ||
| } | ||
| for (const forbidden of additionalForbiddenTypes) { | ||
| if (nameLower === forbidden.toLowerCase()) { | ||
| return { | ||
| valid: false, | ||
| error: `Forbidden type: "${obj.name}"` | ||
| }; | ||
| } | ||
| } | ||
| // schema: optional, valid identifier + allowlist | ||
| if (obj.schema !== undefined) { | ||
| if (typeof obj.schema !== 'string') { | ||
| return { valid: false, error: 'FieldType.schema must be a string' }; | ||
| } | ||
| if (!IDENTIFIER_PATTERN.test(obj.schema)) { | ||
| return { | ||
| valid: false, | ||
| error: `FieldType.schema must be a valid SQL identifier, got: "${obj.schema}"` | ||
| }; | ||
| } | ||
| if (allowedTypeSchemas.length > 0) { | ||
| const schemaLower = obj.schema.toLowerCase(); | ||
| if (!allowedTypeSchemas.some(s => s.toLowerCase() === schemaLower)) { | ||
| return { | ||
| valid: false, | ||
| error: `Type schema "${obj.schema}" is not in the allowed schemas list` | ||
| }; | ||
| } | ||
| } | ||
| } | ||
| // args: optional, must be array of literals | ||
| if (obj.args !== undefined) { | ||
| if (!Array.isArray(obj.args)) { | ||
| return { valid: false, error: 'FieldType.args must be an array' }; | ||
| } | ||
| for (let i = 0; i < obj.args.length; i++) { | ||
| const arg = obj.args[i]; | ||
| if (typeof arg !== 'string' && typeof arg !== 'number' && typeof arg !== 'boolean') { | ||
| return { | ||
| valid: false, | ||
| error: `FieldType.args[${i}] must be a string, number, or boolean, got: ${typeof arg}` | ||
| }; | ||
| } | ||
| // String args must be valid identifiers (they become type modifiers) | ||
| if (typeof arg === 'string' && !IDENTIFIER_PATTERN.test(arg)) { | ||
| return { | ||
| valid: false, | ||
| error: `FieldType.args[${i}] string must be a valid identifier, got: "${arg}"` | ||
| }; | ||
| } | ||
| } | ||
| } | ||
| // array_dimensions: optional, positive integer | ||
| if (obj.array_dimensions !== undefined) { | ||
| if (typeof obj.array_dimensions !== 'number' || !Number.isInteger(obj.array_dimensions)) { | ||
| return { valid: false, error: 'FieldType.array_dimensions must be an integer' }; | ||
| } | ||
| if (obj.array_dimensions < 0 || obj.array_dimensions > 6) { | ||
| return { | ||
| valid: false, | ||
| error: 'FieldType.array_dimensions must be between 0 and 6' | ||
| }; | ||
| } | ||
| } | ||
| // range: optional, 1-2 valid interval field qualifiers | ||
| if (obj.range !== undefined) { | ||
| if (!Array.isArray(obj.range)) { | ||
| return { valid: false, error: 'FieldType.range must be an array' }; | ||
| } | ||
| if (obj.range.length < 1 || obj.range.length > 2) { | ||
| return { valid: false, error: 'FieldType.range must have 1 or 2 elements' }; | ||
| } | ||
| for (let i = 0; i < obj.range.length; i++) { | ||
| const field = obj.range[i]; | ||
| if (typeof field !== 'string') { | ||
| return { | ||
| valid: false, | ||
| error: `FieldType.range[${i}] must be a string` | ||
| }; | ||
| } | ||
| if (!VALID_INTERVAL_FIELDS.has(field.toLowerCase())) { | ||
| return { | ||
| valid: false, | ||
| error: `FieldType.range[${i}] must be a valid interval field (year, month, day, hour, minute, second), got: "${field}"` | ||
| }; | ||
| } | ||
| } | ||
| // range only makes sense on interval | ||
| if (nameLower !== 'interval') { | ||
| return { | ||
| valid: false, | ||
| error: 'FieldType.range is only valid for interval types' | ||
| }; | ||
| } | ||
| } | ||
| // Generate canonical SQL and AST | ||
| const typedFt = obj; | ||
| try { | ||
| const ast = fieldTypeToAst(typedFt); | ||
| const canonicalSql = fieldTypeToSql(typedFt); | ||
| return { valid: true, canonicalSql, ast }; | ||
| } | ||
| catch (e) { | ||
| const msg = e instanceof Error ? e.message : String(e); | ||
| return { valid: false, error: `Failed to convert FieldType to SQL: ${msg}` }; | ||
| } | ||
| } | ||
| /** | ||
| * Validate a FieldDefault object. | ||
| * | ||
| * Structural validation + AST conversion + expression validation. | ||
| * | ||
| * 1. Validates the JSON structure (shape, types, required fields) | ||
| * 2. Converts to pgsql-parser expression AST | ||
| * 3. Validates the AST through the expression validator (function/schema allowlists) | ||
| * 4. Deparses to canonical SQL text | ||
| */ | ||
| export function validateFieldDefault(fd, options = {}) { | ||
| const { maxNestingDepth = 4, allowedTypeSchemas = [], additionalForbiddenTypes = [], allowedSchemaFunctions = {}, ...expressionOptions } = options; | ||
| // Structural validation first | ||
| const structResult = validateFieldDefaultStructure(fd, 0, maxNestingDepth, allowedTypeSchemas, additionalForbiddenTypes); | ||
| if (!structResult.valid) { | ||
| return structResult; | ||
| } | ||
| // Build merged options for the expression validator | ||
| // Include schema-qualified functions in both allowedFunctions and allowedSchemas | ||
| const mergedFunctions = [...(expressionOptions.allowedFunctions ?? [])]; | ||
| const mergedSchemas = [...(expressionOptions.allowedSchemas ?? [])]; | ||
| for (const [schema, funcs] of Object.entries(allowedSchemaFunctions)) { | ||
| if (!mergedSchemas.some(s => s.toLowerCase() === schema.toLowerCase())) { | ||
| mergedSchemas.push(schema); | ||
| } | ||
| for (const func of funcs) { | ||
| if (!mergedFunctions.some(f => f.toLowerCase() === func.toLowerCase())) { | ||
| mergedFunctions.push(func); | ||
| } | ||
| } | ||
| } | ||
| const mergedExpressionOptions = { | ||
| ...expressionOptions, | ||
| allowedFunctions: mergedFunctions.length > 0 ? mergedFunctions : undefined, | ||
| allowedSchemas: mergedSchemas.length > 0 ? mergedSchemas : undefined | ||
| }; | ||
| // Convert to AST | ||
| const typedFd = fd; | ||
| let ast; | ||
| try { | ||
| ast = fieldDefaultToAst(typedFd); | ||
| } | ||
| catch (e) { | ||
| const msg = e instanceof Error ? e.message : String(e); | ||
| return { valid: false, error: `Failed to convert FieldDefault to AST: ${msg}` }; | ||
| } | ||
| // Validate the AST through the expression validator | ||
| const astResult = validateAst(ast, mergedExpressionOptions); | ||
| if (!astResult.valid) { | ||
| return { valid: false, error: astResult.error }; | ||
| } | ||
| return { | ||
| valid: true, | ||
| canonicalSql: astResult.canonicalText, | ||
| ast | ||
| }; | ||
| } | ||
| /** | ||
| * Recursively validate the structure of a FieldDefault object. | ||
| */ | ||
| function validateFieldDefaultStructure(fd, depth, maxDepth, allowedTypeSchemas, additionalForbiddenTypes) { | ||
| if (depth > maxDepth) { | ||
| return { | ||
| valid: false, | ||
| error: `FieldDefault nesting exceeds maximum depth of ${maxDepth}` | ||
| }; | ||
| } | ||
| if (!fd || typeof fd !== 'object' || Array.isArray(fd)) { | ||
| return { valid: false, error: 'FieldDefault must be a non-null object' }; | ||
| } | ||
| const obj = fd; | ||
| // No unknown keys | ||
| for (const key of Object.keys(obj)) { | ||
| if (!FIELD_DEFAULT_KEYS.has(key)) { | ||
| return { valid: false, error: `Unknown FieldDefault key: "${key}"` }; | ||
| } | ||
| } | ||
| // Must have either function or value (not both, not neither) | ||
| const hasFunction = obj.function !== undefined; | ||
| const hasValue = obj.value !== undefined; | ||
| if (!hasFunction && !hasValue) { | ||
| return { valid: false, error: 'FieldDefault must have either "function" or "value"' }; | ||
| } | ||
| if (hasFunction && hasValue) { | ||
| return { valid: false, error: 'FieldDefault cannot have both "function" and "value"' }; | ||
| } | ||
| // schema without function is invalid | ||
| if (obj.schema !== undefined && !hasFunction) { | ||
| return { valid: false, error: 'FieldDefault.schema is only valid with "function"' }; | ||
| } | ||
| // args without function is invalid | ||
| if (obj.args !== undefined && !hasFunction) { | ||
| return { valid: false, error: 'FieldDefault.args is only valid with "function"' }; | ||
| } | ||
| if (hasFunction) { | ||
| // function: must be valid identifier | ||
| if (typeof obj.function !== 'string') { | ||
| return { valid: false, error: 'FieldDefault.function must be a string' }; | ||
| } | ||
| if (!IDENTIFIER_PATTERN.test(obj.function)) { | ||
| return { | ||
| valid: false, | ||
| error: `FieldDefault.function must be a valid SQL identifier, got: "${obj.function}"` | ||
| }; | ||
| } | ||
| // schema: optional, valid identifier | ||
| if (obj.schema !== undefined) { | ||
| if (typeof obj.schema !== 'string') { | ||
| return { valid: false, error: 'FieldDefault.schema must be a string' }; | ||
| } | ||
| if (!IDENTIFIER_PATTERN.test(obj.schema)) { | ||
| return { | ||
| valid: false, | ||
| error: `FieldDefault.schema must be a valid SQL identifier, got: "${obj.schema}"` | ||
| }; | ||
| } | ||
| } | ||
| // args: optional, recursive | ||
| if (obj.args !== undefined) { | ||
| if (!Array.isArray(obj.args)) { | ||
| return { valid: false, error: 'FieldDefault.args must be an array' }; | ||
| } | ||
| for (let i = 0; i < obj.args.length; i++) { | ||
| const arg = obj.args[i]; | ||
| if (arg === null || typeof arg === 'string' || typeof arg === 'number' || typeof arg === 'boolean') { | ||
| continue; // Literal — valid | ||
| } | ||
| if (typeof arg === 'object') { | ||
| // Nested FieldDefault | ||
| const nestedResult = validateFieldDefaultStructure(arg, depth + 1, maxDepth, allowedTypeSchemas, additionalForbiddenTypes); | ||
| if (!nestedResult.valid) { | ||
| return { | ||
| valid: false, | ||
| error: `FieldDefault.args[${i}]: ${nestedResult.error}` | ||
| }; | ||
| } | ||
| } | ||
| else { | ||
| return { | ||
| valid: false, | ||
| error: `FieldDefault.args[${i}] must be a string, number, boolean, null, or FieldDefault object` | ||
| }; | ||
| } | ||
| } | ||
| } | ||
| } | ||
| if (hasValue) { | ||
| // value: must be string, number, boolean, null, or array | ||
| const v = obj.value; | ||
| if (v !== null && typeof v !== 'string' && typeof v !== 'number' && typeof v !== 'boolean' && !Array.isArray(v)) { | ||
| return { | ||
| valid: false, | ||
| error: `FieldDefault.value must be a string, number, boolean, null, or array` | ||
| }; | ||
| } | ||
| if (Array.isArray(v)) { | ||
| for (let i = 0; i < v.length; i++) { | ||
| const el = v[i]; | ||
| if (el !== null && typeof el !== 'string' && typeof el !== 'number' && typeof el !== 'boolean') { | ||
| return { | ||
| valid: false, | ||
| error: `FieldDefault.value[${i}] array elements must be string, number, boolean, or null` | ||
| }; | ||
| } | ||
| } | ||
| } | ||
| } | ||
| // cast: optional, must be valid FieldType | ||
| if (obj.cast !== undefined) { | ||
| const castResult = validateFieldType(obj.cast, { | ||
| allowedTypeSchemas, | ||
| additionalForbiddenTypes | ||
| }); | ||
| if (!castResult.valid) { | ||
| return { valid: false, error: `FieldDefault.cast: ${castResult.error}` }; | ||
| } | ||
| } | ||
| return { valid: true }; | ||
| } |
+190
| /** | ||
| * FieldType and FieldDefault: structured JSONB models for PostgreSQL | ||
| * type declarations and default value expressions. | ||
| * | ||
| * These models provide a safe, validated representation that can be: | ||
| * 1. Validated structurally (JSON shape, identifier patterns, allowlists) | ||
| * 2. Converted to pgsql-parser AST nodes | ||
| * 3. Validated at the AST level (reuses the expression validator for defaults) | ||
| * 4. Deparsed to canonical SQL text | ||
| * | ||
| * The key security insight: the structured model eliminates entire attack | ||
| * categories by construction (no subqueries, no column refs, no stacked | ||
| * statements). What remains is identifier validation + allowlist enforcement. | ||
| */ | ||
| import type { SqlExpressionValidatorOptions } from './validator'; | ||
| /** | ||
| * Structured representation of a PostgreSQL data type. | ||
| * | ||
| * @example Simple type | ||
| * { name: "text" } | ||
| * | ||
| * @example Type with arguments | ||
| * { name: "geometry", args: ["Point", 4326] } | ||
| * { name: "numeric", args: [10, 2] } | ||
| * { name: "vector", args: [1536] } | ||
| * | ||
| * @example Array type | ||
| * { name: "text", array_dimensions: 1 } | ||
| * | ||
| * @example Schema-qualified type | ||
| * { name: "my_type", schema: "my_schema" } | ||
| * | ||
| * @example Interval with field range | ||
| * { name: "interval", range: ["day", "second"] } | ||
| */ | ||
| export interface FieldType { | ||
| /** Type name (required). Must be a valid SQL identifier. */ | ||
| name: string; | ||
| /** Schema qualifier (optional). Must be a valid SQL identifier. */ | ||
| schema?: string; | ||
| /** Type arguments (optional). Each is a string identifier, number, or boolean. */ | ||
| args?: (string | number | boolean)[]; | ||
| /** Number of array dimensions (optional). 1 = `text[]`, 2 = `text[][]`. */ | ||
| array_dimensions?: number; | ||
| /** Interval field range (optional). 1-2 elements: ["day"] or ["day", "second"]. */ | ||
| range?: string[]; | ||
| } | ||
| /** | ||
| * Argument to a function in a FieldDefault expression. | ||
| * Can be a literal value or a nested FieldDefault (recursive). | ||
| */ | ||
| export type FieldDefaultArg = string | number | boolean | null | FieldDefault; | ||
| /** | ||
| * Structured representation of a PostgreSQL default value expression. | ||
| * | ||
| * @example Literal values | ||
| * { value: false } | ||
| * { value: 0 } | ||
| * { value: "pooled" } | ||
| * | ||
| * @example Cast expression | ||
| * { value: "{}", cast: { name: "jsonb" } } | ||
| * { value: "15 minutes", cast: { name: "interval" } } | ||
| * | ||
| * @example Simple function call | ||
| * { function: "now" } | ||
| * { function: "gen_random_uuid" } | ||
| * | ||
| * @example Schema-qualified function | ||
| * { function: "current_user_id", schema: "jwt_public" } | ||
| * | ||
| * @example Function with arguments (nested) | ||
| * { function: "encode", args: [{ function: "gen_random_bytes", args: [16] }, "hex"] } | ||
| * | ||
| * @example Function with cast | ||
| * { function: "lpad", args: ["", 32, "0"], cast: { name: "bit", args: [32] } } | ||
| */ | ||
| export interface FieldDefault { | ||
| /** Literal value (string, number, boolean, null, or array). */ | ||
| value?: string | number | boolean | null | unknown[]; | ||
| /** Function name. Must be a valid SQL identifier. */ | ||
| function?: string; | ||
| /** Schema qualifier for function (optional). */ | ||
| schema?: string; | ||
| /** Function arguments (optional, recursive). */ | ||
| args?: FieldDefaultArg[]; | ||
| /** Output type cast (optional). Reuses FieldType shape. */ | ||
| cast?: FieldType; | ||
| } | ||
| export interface FieldTypeValidationOptions { | ||
| /** Allowed schemas for schema-qualified types. */ | ||
| allowedTypeSchemas?: string[]; | ||
| /** Additional forbidden type names beyond the built-in set. */ | ||
| additionalForbiddenTypes?: string[]; | ||
| } | ||
| export interface FieldDefaultValidationOptions extends SqlExpressionValidatorOptions { | ||
| /** Allowed schemas for schema-qualified types in casts. */ | ||
| allowedTypeSchemas?: string[]; | ||
| /** Additional forbidden type names beyond the built-in set. */ | ||
| additionalForbiddenTypes?: string[]; | ||
| /** Maximum nesting depth for recursive args. Defaults to 4. */ | ||
| maxNestingDepth?: number; | ||
| /** | ||
| * Map of schema → allowed functions for schema-qualified calls. | ||
| * Functions in this map do NOT need to also appear in allowedFunctions. | ||
| */ | ||
| allowedSchemaFunctions?: Record<string, string[]>; | ||
| } | ||
| export interface FieldTypeValidationResult { | ||
| valid: boolean; | ||
| /** Canonical SQL text (e.g., "geometry(Point,4326)", "text[]") */ | ||
| canonicalSql?: string; | ||
| /** The TypeName AST node */ | ||
| ast?: Record<string, unknown>; | ||
| error?: string; | ||
| } | ||
| export interface FieldDefaultValidationResult { | ||
| valid: boolean; | ||
| /** Canonical SQL text (e.g., "now()", "'{}'::jsonb") */ | ||
| canonicalSql?: string; | ||
| /** The expression AST node */ | ||
| ast?: Record<string, unknown>; | ||
| error?: string; | ||
| } | ||
| /** | ||
| * PostgreSQL system catalog type casts that are forbidden. | ||
| * These OID-alias types can be used to probe the system catalog. | ||
| * Shared with the expression validator (same list). | ||
| */ | ||
| export declare const FORBIDDEN_TYPES: Set<string>; | ||
| /** | ||
| * Convert a FieldType to a pgsql-parser TypeName AST node. | ||
| * | ||
| * The TypeName node format: | ||
| * { | ||
| * names: [{ String: { sval: "schema" } }, { String: { sval: "name" } }], | ||
| * typmods: [<arg AST nodes>], | ||
| * arrayBounds: [{ Integer: { ival: -1 } }, ...], | ||
| * typemod: -1 | ||
| * } | ||
| */ | ||
| export declare function fieldTypeToAst(ft: FieldType): Record<string, unknown>; | ||
| /** | ||
| * Convert a FieldType to its canonical SQL text representation. | ||
| * | ||
| * Uses the deparser for the base type, then appends array brackets. | ||
| * Handles interval range fields manually since the deparser needs | ||
| * special handling for INTERVAL ... DAY TO SECOND syntax. | ||
| */ | ||
| export declare function fieldTypeToSql(ft: FieldType): string; | ||
| /** | ||
| * Convert a FieldDefault to a pgsql-parser expression AST node. | ||
| * | ||
| * Handles: | ||
| * - Literal values → A_Const | ||
| * - Function calls → FuncCall (with recursive args) | ||
| * - Type casts → TypeCast wrapping the inner expression | ||
| * - Combinations (function + cast, value + cast) | ||
| */ | ||
| export declare function fieldDefaultToAst(fd: FieldDefault, depth?: number): Record<string, unknown>; | ||
| /** | ||
| * Convert a FieldDefault to its canonical SQL text representation. | ||
| */ | ||
| export declare function fieldDefaultToSql(fd: FieldDefault): string; | ||
| /** | ||
| * Validate a FieldType object structurally. | ||
| * | ||
| * Checks: | ||
| * - Object shape (no unknown keys, required keys present) | ||
| * - `name` is a valid SQL identifier | ||
| * - `name` is not a forbidden type (regclass, regtype, etc.) | ||
| * - `schema` (if present) is a valid SQL identifier and on the allowlist | ||
| * - `args` elements are literals (string, number, boolean) | ||
| * - `array_dimensions` is a positive integer | ||
| * - `range` elements are valid interval field qualifiers | ||
| * | ||
| * Then converts to SQL text for canonical output. | ||
| */ | ||
| export declare function validateFieldType(ft: unknown, options?: FieldTypeValidationOptions): FieldTypeValidationResult; | ||
| /** | ||
| * Validate a FieldDefault object. | ||
| * | ||
| * Structural validation + AST conversion + expression validation. | ||
| * | ||
| * 1. Validates the JSON structure (shape, types, required fields) | ||
| * 2. Converts to pgsql-parser expression AST | ||
| * 3. Validates the AST through the expression validator (function/schema allowlists) | ||
| * 4. Deparses to canonical SQL text | ||
| */ | ||
| export declare function validateFieldDefault(fd: unknown, options?: FieldDefaultValidationOptions): FieldDefaultValidationResult; |
+631
| "use strict"; | ||
| /** | ||
| * FieldType and FieldDefault: structured JSONB models for PostgreSQL | ||
| * type declarations and default value expressions. | ||
| * | ||
| * These models provide a safe, validated representation that can be: | ||
| * 1. Validated structurally (JSON shape, identifier patterns, allowlists) | ||
| * 2. Converted to pgsql-parser AST nodes | ||
| * 3. Validated at the AST level (reuses the expression validator for defaults) | ||
| * 4. Deparsed to canonical SQL text | ||
| * | ||
| * The key security insight: the structured model eliminates entire attack | ||
| * categories by construction (no subqueries, no column refs, no stacked | ||
| * statements). What remains is identifier validation + allowlist enforcement. | ||
| */ | ||
| Object.defineProperty(exports, "__esModule", { value: true }); | ||
| exports.FORBIDDEN_TYPES = void 0; | ||
| exports.fieldTypeToAst = fieldTypeToAst; | ||
| exports.fieldTypeToSql = fieldTypeToSql; | ||
| exports.fieldDefaultToAst = fieldDefaultToAst; | ||
| exports.fieldDefaultToSql = fieldDefaultToSql; | ||
| exports.validateFieldType = validateFieldType; | ||
| exports.validateFieldDefault = validateFieldDefault; | ||
| const pgsql_deparser_1 = require("pgsql-deparser"); | ||
| const validator_1 = require("./validator"); | ||
| // ─── Constants ─────────────────────────────────────────────────── | ||
| /** Valid SQL identifier pattern. */ | ||
| const IDENTIFIER_PATTERN = /^[a-zA-Z_][a-zA-Z0-9_]*$/; | ||
| /** | ||
| * PostgreSQL system catalog type casts that are forbidden. | ||
| * These OID-alias types can be used to probe the system catalog. | ||
| * Shared with the expression validator (same list). | ||
| */ | ||
| exports.FORBIDDEN_TYPES = new Set([ | ||
| 'regclass', | ||
| 'regtype', | ||
| 'regproc', | ||
| 'regprocedure', | ||
| 'regoper', | ||
| 'regoperator', | ||
| 'regnamespace', | ||
| 'regrole', | ||
| 'regconfig', | ||
| 'regdictionary' | ||
| ]); | ||
| /** | ||
| * Valid interval field qualifiers for the `range` property. | ||
| */ | ||
| const VALID_INTERVAL_FIELDS = new Set([ | ||
| 'year', 'month', 'day', 'hour', 'minute', 'second' | ||
| ]); | ||
| /** Allowed keys in a FieldType object. */ | ||
| const FIELD_TYPE_KEYS = new Set(['name', 'schema', 'args', 'array_dimensions', 'range']); | ||
| /** Allowed keys in a FieldDefault object. */ | ||
| const FIELD_DEFAULT_KEYS = new Set(['value', 'function', 'schema', 'args', 'cast']); | ||
| // ─── Internal: AST Builders ────────────────────────────────────── | ||
| /** Build a pgsql-parser String node. */ | ||
| function astString(sval) { | ||
| return { String: { sval } }; | ||
| } | ||
| /** Build a pgsql-parser A_Const node for an integer. */ | ||
| function astConstInt(ival) { | ||
| return { A_Const: { ival: { ival } } }; | ||
| } | ||
| /** Build a pgsql-parser A_Const node for a string. */ | ||
| function astConstStr(sval) { | ||
| return { A_Const: { sval: { sval } } }; | ||
| } | ||
| /** Build a pgsql-parser A_Const node for a boolean. */ | ||
| function astConstBool(boolval) { | ||
| if (boolval) { | ||
| return { A_Const: { boolval: { boolval: true } } }; | ||
| } | ||
| return { A_Const: { boolval: {} } }; | ||
| } | ||
| /** Build a pgsql-parser A_Const node for NULL. */ | ||
| function astConstNull() { | ||
| return { A_Const: { isnull: true } }; | ||
| } | ||
| // ─── Converters: FieldType → AST ───────────────────────────────── | ||
| /** | ||
| * Convert a FieldType to a pgsql-parser TypeName AST node. | ||
| * | ||
| * The TypeName node format: | ||
| * { | ||
| * names: [{ String: { sval: "schema" } }, { String: { sval: "name" } }], | ||
| * typmods: [<arg AST nodes>], | ||
| * arrayBounds: [{ Integer: { ival: -1 } }, ...], | ||
| * typemod: -1 | ||
| * } | ||
| */ | ||
| function fieldTypeToAst(ft) { | ||
| const names = []; | ||
| if (ft.schema) { | ||
| names.push(astString(ft.schema)); | ||
| } | ||
| names.push(astString(ft.name)); | ||
| const typeName = { | ||
| names, | ||
| typemod: -1 | ||
| }; | ||
| // Type arguments → typmods | ||
| if (ft.args && ft.args.length > 0) { | ||
| const typmods = []; | ||
| for (const arg of ft.args) { | ||
| if (typeof arg === 'number') { | ||
| typmods.push(astConstInt(arg)); | ||
| } | ||
| else if (typeof arg === 'string') { | ||
| // String args in type modifiers are identifiers (e.g., "Point" in geometry(Point, 4326)) | ||
| // PostgreSQL parser represents these as ColumnRef nodes | ||
| typmods.push({ | ||
| ColumnRef: { | ||
| fields: [astString(arg.toLowerCase())] | ||
| } | ||
| }); | ||
| } | ||
| else if (typeof arg === 'boolean') { | ||
| typmods.push(astConstBool(arg)); | ||
| } | ||
| } | ||
| typeName.typmods = typmods; | ||
| } | ||
| // Array dimensions → arrayBounds | ||
| if (ft.array_dimensions && ft.array_dimensions > 0) { | ||
| const arrayBounds = []; | ||
| for (let i = 0; i < ft.array_dimensions; i++) { | ||
| arrayBounds.push({ Integer: { ival: -1 } }); | ||
| } | ||
| typeName.arrayBounds = arrayBounds; | ||
| } | ||
| // Interval range is encoded as a numeric typmod by PostgreSQL. | ||
| // We store it as readable strings; conversion to the numeric bitmask | ||
| // happens in field_type_to_text() at the SQL layer. | ||
| // For AST purposes, we skip typmods for interval range — the deparser | ||
| // handles it via the INTERVAL keyword + field qualifiers. | ||
| return typeName; | ||
| } | ||
| /** | ||
| * Convert a FieldType to its canonical SQL text representation. | ||
| * | ||
| * Uses the deparser for the base type, then appends array brackets. | ||
| * Handles interval range fields manually since the deparser needs | ||
| * special handling for INTERVAL ... DAY TO SECOND syntax. | ||
| */ | ||
| function fieldTypeToSql(ft) { | ||
| // Build the base type string | ||
| let sql = ''; | ||
| if (ft.schema) { | ||
| sql += ft.schema + '.'; | ||
| } | ||
| sql += ft.name; | ||
| // Type arguments | ||
| if (ft.args && ft.args.length > 0) { | ||
| const argParts = ft.args.map(arg => { | ||
| if (typeof arg === 'string') | ||
| return arg; | ||
| return String(arg); | ||
| }); | ||
| sql += '(' + argParts.join(', ') + ')'; | ||
| } | ||
| // Interval range | ||
| if (ft.range && ft.range.length > 0 && ft.name.toLowerCase() === 'interval') { | ||
| if (ft.range.length === 1) { | ||
| sql += ' ' + ft.range[0]; | ||
| } | ||
| else if (ft.range.length === 2) { | ||
| sql += ' ' + ft.range[0] + ' to ' + ft.range[1]; | ||
| } | ||
| } | ||
| // Array dimensions | ||
| if (ft.array_dimensions && ft.array_dimensions > 0) { | ||
| for (let i = 0; i < ft.array_dimensions; i++) { | ||
| sql += '[]'; | ||
| } | ||
| } | ||
| return sql; | ||
| } | ||
| // ─── Converters: FieldDefault → AST ───────────────────────────── | ||
| /** | ||
| * Convert a FieldDefault to a pgsql-parser expression AST node. | ||
| * | ||
| * Handles: | ||
| * - Literal values → A_Const | ||
| * - Function calls → FuncCall (with recursive args) | ||
| * - Type casts → TypeCast wrapping the inner expression | ||
| * - Combinations (function + cast, value + cast) | ||
| */ | ||
| function fieldDefaultToAst(fd, depth = 0) { | ||
| if (depth > 10) { | ||
| throw new Error('FieldDefault nesting exceeds maximum depth'); | ||
| } | ||
| let expr; | ||
| if (fd.function !== undefined) { | ||
| // Function call | ||
| const funcname = []; | ||
| if (fd.schema) { | ||
| funcname.push(astString(fd.schema)); | ||
| } | ||
| funcname.push(astString(fd.function)); | ||
| const funcCall = { | ||
| funcname, | ||
| funcformat: 'COERCE_EXPLICIT_CALL' | ||
| }; | ||
| // Function arguments | ||
| if (fd.args && fd.args.length > 0) { | ||
| const astArgs = []; | ||
| for (const arg of fd.args) { | ||
| astArgs.push(fieldDefaultArgToAst(arg, depth + 1)); | ||
| } | ||
| funcCall.args = astArgs; | ||
| } | ||
| expr = { FuncCall: funcCall }; | ||
| } | ||
| else if (fd.value !== undefined) { | ||
| // Literal value | ||
| expr = literalToAst(fd.value); | ||
| } | ||
| else { | ||
| throw new Error('FieldDefault must have either "function" or "value"'); | ||
| } | ||
| // Wrap in TypeCast if cast is present | ||
| if (fd.cast) { | ||
| expr = { | ||
| TypeCast: { | ||
| arg: expr, | ||
| typeName: fieldTypeToAst(fd.cast) | ||
| } | ||
| }; | ||
| } | ||
| return expr; | ||
| } | ||
| /** | ||
| * Convert a FieldDefaultArg to an AST node. | ||
| */ | ||
| function fieldDefaultArgToAst(arg, depth) { | ||
| if (arg === null) { | ||
| return astConstNull(); | ||
| } | ||
| if (typeof arg === 'string') { | ||
| return astConstStr(arg); | ||
| } | ||
| if (typeof arg === 'number') { | ||
| if (Number.isInteger(arg)) { | ||
| return astConstInt(arg); | ||
| } | ||
| // Float — represented as string in pgsql-parser | ||
| return { A_Const: { fval: { fval: String(arg) } } }; | ||
| } | ||
| if (typeof arg === 'boolean') { | ||
| return astConstBool(arg); | ||
| } | ||
| // Nested FieldDefault object | ||
| return fieldDefaultToAst(arg, depth); | ||
| } | ||
| /** | ||
| * Convert a literal value to an A_Const (or A_ArrayExpr) AST node. | ||
| */ | ||
| function literalToAst(value) { | ||
| if (value === null) { | ||
| return astConstNull(); | ||
| } | ||
| if (typeof value === 'string') { | ||
| return astConstStr(value); | ||
| } | ||
| if (typeof value === 'number') { | ||
| if (Number.isInteger(value)) { | ||
| return astConstInt(value); | ||
| } | ||
| return { A_Const: { fval: { fval: String(value) } } }; | ||
| } | ||
| if (typeof value === 'boolean') { | ||
| return astConstBool(value); | ||
| } | ||
| if (Array.isArray(value)) { | ||
| const elements = value.map(el => { | ||
| if (el === null) | ||
| return astConstNull(); | ||
| if (typeof el === 'string') | ||
| return astConstStr(el); | ||
| if (typeof el === 'number') { | ||
| if (Number.isInteger(el)) | ||
| return astConstInt(el); | ||
| return { A_Const: { fval: { fval: String(el) } } }; | ||
| } | ||
| if (typeof el === 'boolean') | ||
| return astConstBool(el); | ||
| throw new Error(`Unsupported array element type: ${typeof el}`); | ||
| }); | ||
| return { A_ArrayExpr: { elements } }; | ||
| } | ||
| throw new Error(`Unsupported literal type: ${typeof value}`); | ||
| } | ||
| /** | ||
| * Convert a FieldDefault to its canonical SQL text representation. | ||
| */ | ||
| function fieldDefaultToSql(fd) { | ||
| const ast = fieldDefaultToAst(fd); | ||
| return (0, pgsql_deparser_1.deparseSync)([ast]); | ||
| } | ||
| // ─── Structural Validators ─────────────────────────────────────── | ||
| /** | ||
| * Validate a FieldType object structurally. | ||
| * | ||
| * Checks: | ||
| * - Object shape (no unknown keys, required keys present) | ||
| * - `name` is a valid SQL identifier | ||
| * - `name` is not a forbidden type (regclass, regtype, etc.) | ||
| * - `schema` (if present) is a valid SQL identifier and on the allowlist | ||
| * - `args` elements are literals (string, number, boolean) | ||
| * - `array_dimensions` is a positive integer | ||
| * - `range` elements are valid interval field qualifiers | ||
| * | ||
| * Then converts to SQL text for canonical output. | ||
| */ | ||
| function validateFieldType(ft, options = {}) { | ||
| const { allowedTypeSchemas = [], additionalForbiddenTypes = [] } = options; | ||
| // Must be a non-null object | ||
| if (!ft || typeof ft !== 'object' || Array.isArray(ft)) { | ||
| return { valid: false, error: 'FieldType must be a non-null object' }; | ||
| } | ||
| const obj = ft; | ||
| // No unknown keys | ||
| for (const key of Object.keys(obj)) { | ||
| if (!FIELD_TYPE_KEYS.has(key)) { | ||
| return { valid: false, error: `Unknown FieldType key: "${key}"` }; | ||
| } | ||
| } | ||
| // name: required, valid identifier | ||
| if (typeof obj.name !== 'string') { | ||
| return { valid: false, error: 'FieldType.name is required and must be a string' }; | ||
| } | ||
| if (!IDENTIFIER_PATTERN.test(obj.name)) { | ||
| return { | ||
| valid: false, | ||
| error: `FieldType.name must be a valid SQL identifier, got: "${obj.name}"` | ||
| }; | ||
| } | ||
| // Forbidden types | ||
| const nameLower = obj.name.toLowerCase(); | ||
| if (exports.FORBIDDEN_TYPES.has(nameLower)) { | ||
| return { | ||
| valid: false, | ||
| error: `Forbidden type: "${obj.name}" — system catalog OID-alias types are not allowed` | ||
| }; | ||
| } | ||
| for (const forbidden of additionalForbiddenTypes) { | ||
| if (nameLower === forbidden.toLowerCase()) { | ||
| return { | ||
| valid: false, | ||
| error: `Forbidden type: "${obj.name}"` | ||
| }; | ||
| } | ||
| } | ||
| // schema: optional, valid identifier + allowlist | ||
| if (obj.schema !== undefined) { | ||
| if (typeof obj.schema !== 'string') { | ||
| return { valid: false, error: 'FieldType.schema must be a string' }; | ||
| } | ||
| if (!IDENTIFIER_PATTERN.test(obj.schema)) { | ||
| return { | ||
| valid: false, | ||
| error: `FieldType.schema must be a valid SQL identifier, got: "${obj.schema}"` | ||
| }; | ||
| } | ||
| if (allowedTypeSchemas.length > 0) { | ||
| const schemaLower = obj.schema.toLowerCase(); | ||
| if (!allowedTypeSchemas.some(s => s.toLowerCase() === schemaLower)) { | ||
| return { | ||
| valid: false, | ||
| error: `Type schema "${obj.schema}" is not in the allowed schemas list` | ||
| }; | ||
| } | ||
| } | ||
| } | ||
| // args: optional, must be array of literals | ||
| if (obj.args !== undefined) { | ||
| if (!Array.isArray(obj.args)) { | ||
| return { valid: false, error: 'FieldType.args must be an array' }; | ||
| } | ||
| for (let i = 0; i < obj.args.length; i++) { | ||
| const arg = obj.args[i]; | ||
| if (typeof arg !== 'string' && typeof arg !== 'number' && typeof arg !== 'boolean') { | ||
| return { | ||
| valid: false, | ||
| error: `FieldType.args[${i}] must be a string, number, or boolean, got: ${typeof arg}` | ||
| }; | ||
| } | ||
| // String args must be valid identifiers (they become type modifiers) | ||
| if (typeof arg === 'string' && !IDENTIFIER_PATTERN.test(arg)) { | ||
| return { | ||
| valid: false, | ||
| error: `FieldType.args[${i}] string must be a valid identifier, got: "${arg}"` | ||
| }; | ||
| } | ||
| } | ||
| } | ||
| // array_dimensions: optional, positive integer | ||
| if (obj.array_dimensions !== undefined) { | ||
| if (typeof obj.array_dimensions !== 'number' || !Number.isInteger(obj.array_dimensions)) { | ||
| return { valid: false, error: 'FieldType.array_dimensions must be an integer' }; | ||
| } | ||
| if (obj.array_dimensions < 0 || obj.array_dimensions > 6) { | ||
| return { | ||
| valid: false, | ||
| error: 'FieldType.array_dimensions must be between 0 and 6' | ||
| }; | ||
| } | ||
| } | ||
| // range: optional, 1-2 valid interval field qualifiers | ||
| if (obj.range !== undefined) { | ||
| if (!Array.isArray(obj.range)) { | ||
| return { valid: false, error: 'FieldType.range must be an array' }; | ||
| } | ||
| if (obj.range.length < 1 || obj.range.length > 2) { | ||
| return { valid: false, error: 'FieldType.range must have 1 or 2 elements' }; | ||
| } | ||
| for (let i = 0; i < obj.range.length; i++) { | ||
| const field = obj.range[i]; | ||
| if (typeof field !== 'string') { | ||
| return { | ||
| valid: false, | ||
| error: `FieldType.range[${i}] must be a string` | ||
| }; | ||
| } | ||
| if (!VALID_INTERVAL_FIELDS.has(field.toLowerCase())) { | ||
| return { | ||
| valid: false, | ||
| error: `FieldType.range[${i}] must be a valid interval field (year, month, day, hour, minute, second), got: "${field}"` | ||
| }; | ||
| } | ||
| } | ||
| // range only makes sense on interval | ||
| if (nameLower !== 'interval') { | ||
| return { | ||
| valid: false, | ||
| error: 'FieldType.range is only valid for interval types' | ||
| }; | ||
| } | ||
| } | ||
| // Generate canonical SQL and AST | ||
| const typedFt = obj; | ||
| try { | ||
| const ast = fieldTypeToAst(typedFt); | ||
| const canonicalSql = fieldTypeToSql(typedFt); | ||
| return { valid: true, canonicalSql, ast }; | ||
| } | ||
| catch (e) { | ||
| const msg = e instanceof Error ? e.message : String(e); | ||
| return { valid: false, error: `Failed to convert FieldType to SQL: ${msg}` }; | ||
| } | ||
| } | ||
| /** | ||
| * Validate a FieldDefault object. | ||
| * | ||
| * Structural validation + AST conversion + expression validation. | ||
| * | ||
| * 1. Validates the JSON structure (shape, types, required fields) | ||
| * 2. Converts to pgsql-parser expression AST | ||
| * 3. Validates the AST through the expression validator (function/schema allowlists) | ||
| * 4. Deparses to canonical SQL text | ||
| */ | ||
| function validateFieldDefault(fd, options = {}) { | ||
| const { maxNestingDepth = 4, allowedTypeSchemas = [], additionalForbiddenTypes = [], allowedSchemaFunctions = {}, ...expressionOptions } = options; | ||
| // Structural validation first | ||
| const structResult = validateFieldDefaultStructure(fd, 0, maxNestingDepth, allowedTypeSchemas, additionalForbiddenTypes); | ||
| if (!structResult.valid) { | ||
| return structResult; | ||
| } | ||
| // Build merged options for the expression validator | ||
| // Include schema-qualified functions in both allowedFunctions and allowedSchemas | ||
| const mergedFunctions = [...(expressionOptions.allowedFunctions ?? [])]; | ||
| const mergedSchemas = [...(expressionOptions.allowedSchemas ?? [])]; | ||
| for (const [schema, funcs] of Object.entries(allowedSchemaFunctions)) { | ||
| if (!mergedSchemas.some(s => s.toLowerCase() === schema.toLowerCase())) { | ||
| mergedSchemas.push(schema); | ||
| } | ||
| for (const func of funcs) { | ||
| if (!mergedFunctions.some(f => f.toLowerCase() === func.toLowerCase())) { | ||
| mergedFunctions.push(func); | ||
| } | ||
| } | ||
| } | ||
| const mergedExpressionOptions = { | ||
| ...expressionOptions, | ||
| allowedFunctions: mergedFunctions.length > 0 ? mergedFunctions : undefined, | ||
| allowedSchemas: mergedSchemas.length > 0 ? mergedSchemas : undefined | ||
| }; | ||
| // Convert to AST | ||
| const typedFd = fd; | ||
| let ast; | ||
| try { | ||
| ast = fieldDefaultToAst(typedFd); | ||
| } | ||
| catch (e) { | ||
| const msg = e instanceof Error ? e.message : String(e); | ||
| return { valid: false, error: `Failed to convert FieldDefault to AST: ${msg}` }; | ||
| } | ||
| // Validate the AST through the expression validator | ||
| const astResult = (0, validator_1.validateAst)(ast, mergedExpressionOptions); | ||
| if (!astResult.valid) { | ||
| return { valid: false, error: astResult.error }; | ||
| } | ||
| return { | ||
| valid: true, | ||
| canonicalSql: astResult.canonicalText, | ||
| ast | ||
| }; | ||
| } | ||
| /** | ||
| * Recursively validate the structure of a FieldDefault object. | ||
| */ | ||
| function validateFieldDefaultStructure(fd, depth, maxDepth, allowedTypeSchemas, additionalForbiddenTypes) { | ||
| if (depth > maxDepth) { | ||
| return { | ||
| valid: false, | ||
| error: `FieldDefault nesting exceeds maximum depth of ${maxDepth}` | ||
| }; | ||
| } | ||
| if (!fd || typeof fd !== 'object' || Array.isArray(fd)) { | ||
| return { valid: false, error: 'FieldDefault must be a non-null object' }; | ||
| } | ||
| const obj = fd; | ||
| // No unknown keys | ||
| for (const key of Object.keys(obj)) { | ||
| if (!FIELD_DEFAULT_KEYS.has(key)) { | ||
| return { valid: false, error: `Unknown FieldDefault key: "${key}"` }; | ||
| } | ||
| } | ||
| // Must have either function or value (not both, not neither) | ||
| const hasFunction = obj.function !== undefined; | ||
| const hasValue = obj.value !== undefined; | ||
| if (!hasFunction && !hasValue) { | ||
| return { valid: false, error: 'FieldDefault must have either "function" or "value"' }; | ||
| } | ||
| if (hasFunction && hasValue) { | ||
| return { valid: false, error: 'FieldDefault cannot have both "function" and "value"' }; | ||
| } | ||
| // schema without function is invalid | ||
| if (obj.schema !== undefined && !hasFunction) { | ||
| return { valid: false, error: 'FieldDefault.schema is only valid with "function"' }; | ||
| } | ||
| // args without function is invalid | ||
| if (obj.args !== undefined && !hasFunction) { | ||
| return { valid: false, error: 'FieldDefault.args is only valid with "function"' }; | ||
| } | ||
| if (hasFunction) { | ||
| // function: must be valid identifier | ||
| if (typeof obj.function !== 'string') { | ||
| return { valid: false, error: 'FieldDefault.function must be a string' }; | ||
| } | ||
| if (!IDENTIFIER_PATTERN.test(obj.function)) { | ||
| return { | ||
| valid: false, | ||
| error: `FieldDefault.function must be a valid SQL identifier, got: "${obj.function}"` | ||
| }; | ||
| } | ||
| // schema: optional, valid identifier | ||
| if (obj.schema !== undefined) { | ||
| if (typeof obj.schema !== 'string') { | ||
| return { valid: false, error: 'FieldDefault.schema must be a string' }; | ||
| } | ||
| if (!IDENTIFIER_PATTERN.test(obj.schema)) { | ||
| return { | ||
| valid: false, | ||
| error: `FieldDefault.schema must be a valid SQL identifier, got: "${obj.schema}"` | ||
| }; | ||
| } | ||
| } | ||
| // args: optional, recursive | ||
| if (obj.args !== undefined) { | ||
| if (!Array.isArray(obj.args)) { | ||
| return { valid: false, error: 'FieldDefault.args must be an array' }; | ||
| } | ||
| for (let i = 0; i < obj.args.length; i++) { | ||
| const arg = obj.args[i]; | ||
| if (arg === null || typeof arg === 'string' || typeof arg === 'number' || typeof arg === 'boolean') { | ||
| continue; // Literal — valid | ||
| } | ||
| if (typeof arg === 'object') { | ||
| // Nested FieldDefault | ||
| const nestedResult = validateFieldDefaultStructure(arg, depth + 1, maxDepth, allowedTypeSchemas, additionalForbiddenTypes); | ||
| if (!nestedResult.valid) { | ||
| return { | ||
| valid: false, | ||
| error: `FieldDefault.args[${i}]: ${nestedResult.error}` | ||
| }; | ||
| } | ||
| } | ||
| else { | ||
| return { | ||
| valid: false, | ||
| error: `FieldDefault.args[${i}] must be a string, number, boolean, null, or FieldDefault object` | ||
| }; | ||
| } | ||
| } | ||
| } | ||
| } | ||
| if (hasValue) { | ||
| // value: must be string, number, boolean, null, or array | ||
| const v = obj.value; | ||
| if (v !== null && typeof v !== 'string' && typeof v !== 'number' && typeof v !== 'boolean' && !Array.isArray(v)) { | ||
| return { | ||
| valid: false, | ||
| error: `FieldDefault.value must be a string, number, boolean, null, or array` | ||
| }; | ||
| } | ||
| if (Array.isArray(v)) { | ||
| for (let i = 0; i < v.length; i++) { | ||
| const el = v[i]; | ||
| if (el !== null && typeof el !== 'string' && typeof el !== 'number' && typeof el !== 'boolean') { | ||
| return { | ||
| valid: false, | ||
| error: `FieldDefault.value[${i}] array elements must be string, number, boolean, or null` | ||
| }; | ||
| } | ||
| } | ||
| } | ||
| } | ||
| // cast: optional, must be valid FieldType | ||
| if (obj.cast !== undefined) { | ||
| const castResult = validateFieldType(obj.cast, { | ||
| allowedTypeSchemas, | ||
| additionalForbiddenTypes | ||
| }); | ||
| if (!castResult.valid) { | ||
| return { valid: false, error: `FieldDefault.cast: ${castResult.error}` }; | ||
| } | ||
| } | ||
| return { valid: true }; | ||
| } |
+2
-0
@@ -36,1 +36,3 @@ /** | ||
| export { createSqlExpressionValidatorPlugin, SqlExpressionValidatorPreset } from './plugin'; | ||
| export { validateFieldType, validateFieldDefault, fieldTypeToAst, fieldTypeToSql, fieldDefaultToAst, fieldDefaultToSql, FORBIDDEN_TYPES } from './field-types'; | ||
| export type { FieldType, FieldDefault, FieldDefaultArg, FieldTypeValidationOptions, FieldDefaultValidationOptions, FieldTypeValidationResult, FieldDefaultValidationResult } from './field-types'; |
+2
-0
@@ -37,1 +37,3 @@ /** | ||
| export { createSqlExpressionValidatorPlugin, SqlExpressionValidatorPreset } from './plugin'; | ||
| // FieldType / FieldDefault structured models | ||
| export { validateFieldType, validateFieldDefault, fieldTypeToAst, fieldTypeToSql, fieldDefaultToAst, fieldDefaultToSql, FORBIDDEN_TYPES } from './field-types'; |
+2
-1
@@ -82,3 +82,4 @@ /** | ||
| 'boolval', | ||
| 'bsval' | ||
| 'bsval', | ||
| 'isnull' | ||
| ]); | ||
@@ -85,0 +86,0 @@ /** |
+2
-0
@@ -36,1 +36,3 @@ /** | ||
| export { createSqlExpressionValidatorPlugin, SqlExpressionValidatorPreset } from './plugin'; | ||
| export { validateFieldType, validateFieldDefault, fieldTypeToAst, fieldTypeToSql, fieldDefaultToAst, fieldDefaultToSql, FORBIDDEN_TYPES } from './field-types'; | ||
| export type { FieldType, FieldDefault, FieldDefaultArg, FieldTypeValidationOptions, FieldDefaultValidationOptions, FieldTypeValidationResult, FieldDefaultValidationResult } from './field-types'; |
+10
-1
@@ -35,3 +35,3 @@ "use strict"; | ||
| Object.defineProperty(exports, "__esModule", { value: true }); | ||
| exports.SqlExpressionValidatorPreset = exports.createSqlExpressionValidatorPlugin = exports.DEFAULT_ALLOWED_FUNCTIONS = exports.validateAst = exports.parseAndValidateSqlExpression = void 0; | ||
| exports.FORBIDDEN_TYPES = exports.fieldDefaultToSql = exports.fieldDefaultToAst = exports.fieldTypeToSql = exports.fieldTypeToAst = exports.validateFieldDefault = exports.validateFieldType = exports.SqlExpressionValidatorPreset = exports.createSqlExpressionValidatorPlugin = exports.DEFAULT_ALLOWED_FUNCTIONS = exports.validateAst = exports.parseAndValidateSqlExpression = void 0; | ||
| // Standalone validation utilities | ||
@@ -46,1 +46,10 @@ var validator_1 = require("./validator"); | ||
| Object.defineProperty(exports, "SqlExpressionValidatorPreset", { enumerable: true, get: function () { return plugin_1.SqlExpressionValidatorPreset; } }); | ||
| // FieldType / FieldDefault structured models | ||
| var field_types_1 = require("./field-types"); | ||
| Object.defineProperty(exports, "validateFieldType", { enumerable: true, get: function () { return field_types_1.validateFieldType; } }); | ||
| Object.defineProperty(exports, "validateFieldDefault", { enumerable: true, get: function () { return field_types_1.validateFieldDefault; } }); | ||
| Object.defineProperty(exports, "fieldTypeToAst", { enumerable: true, get: function () { return field_types_1.fieldTypeToAst; } }); | ||
| Object.defineProperty(exports, "fieldTypeToSql", { enumerable: true, get: function () { return field_types_1.fieldTypeToSql; } }); | ||
| Object.defineProperty(exports, "fieldDefaultToAst", { enumerable: true, get: function () { return field_types_1.fieldDefaultToAst; } }); | ||
| Object.defineProperty(exports, "fieldDefaultToSql", { enumerable: true, get: function () { return field_types_1.fieldDefaultToSql; } }); | ||
| Object.defineProperty(exports, "FORBIDDEN_TYPES", { enumerable: true, get: function () { return field_types_1.FORBIDDEN_TYPES; } }); |
+2
-2
| { | ||
| "name": "graphile-sql-expression-validator", | ||
| "version": "2.11.1", | ||
| "version": "2.12.0", | ||
| "description": "SQL expression validation for PostGraphile v5", | ||
@@ -59,3 +59,3 @@ "author": "Constructive <developers@constructive.io>", | ||
| }, | ||
| "gitHead": "d43f1d7e38cc39a71b4f8d2cafb39f46df12e31a" | ||
| "gitHead": "42ff6bf5fa2e400bb63f78745a1f5a76209677b6" | ||
| } |
+2
-1
@@ -87,3 +87,4 @@ "use strict"; | ||
| 'boolval', | ||
| 'bsval' | ||
| 'bsval', | ||
| 'isnull' | ||
| ]); | ||
@@ -90,0 +91,0 @@ /** |
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
139216
83.69%19
26.67%3282
101.23%