json-sql-enhanced
Version:
Node.js library for mapping MongoDB-style query objects to SQL queries with enhanced operators and multi-dialect support
326 lines (276 loc) • 10.7 kB
JavaScript
;
// Utility function to convert simple regex patterns to LIKE patterns
function convertRegexToLike(pattern) {
if (typeof pattern !== 'string') {
return null;
}
// Handle exact match: ^text$
const exactMatch = pattern.match(/^\^(.+)\$$/);
if (exactMatch) {
// Unescape common regex escapes for LIKE
return exactMatch[1].replaceAll('\\.', '.');
}
// Handle starts with: ^text
const startsWith = pattern.match(/^\^(.+)$/);
if (startsWith) {
return startsWith[1].replaceAll('\\.', '.') + '%';
}
// Handle ends with: text$
const endsWith = pattern.match(/^(.+)\$$/);
if (endsWith) {
return '%' + endsWith[1].replaceAll('\\.', '.');
}
// Handle contains (simple text without special regex chars except escaped dots)
if (!/[.*+?^${}()|[\]\\]/.test(pattern.replaceAll('\\.', '.'))) {
return '%' + pattern.replaceAll('\\.', '.') + '%';
}
return null;
}
module.exports = function (dialect) {
// MSSQL-specific $regex implementation
dialect.operators.comparison.add('$regex', {
inversedOperator: '$nregex',
fn(field, value) {
// Get the actual pattern and options
const pattern = value;
let flags = '';
// Check if there's an $options in the current condition
if (dialect.currentCondition && dialect.currentCondition.$options) {
flags = dialect.currentCondition.$options;
}
// Convert simple patterns to LIKE for better performance
const likePattern = convertRegexToLike(pattern);
if (likePattern) {
const valueParameter = dialect.builder._pushValue(likePattern);
if (flags.includes('i')) {
return `LOWER(${field}) LIKE LOWER(${valueParameter})`;
}
return `${field} LIKE ${valueParameter}`;
}
// MSSQL doesn't have native regex, fall back to LIKE with wildcards
const likeApproximation = pattern
.replaceAll('\\.', '.') // Unescape dots
.replaceAll('.*', '%') // .* -> %
.replaceAll('.', '_') // . -> _
.replaceAll('+', '') // Remove + (not perfect)
.replaceAll('*', '%'); // * -> %
const valueParameter = dialect.builder._pushValue(likeApproximation);
if (flags.includes('i')) {
return `LOWER(${field}) LIKE LOWER(${valueParameter})`;
}
return `${field} LIKE ${valueParameter}`;
},
});
// MSSQL-specific $nregex implementation
dialect.operators.comparison.add('$nregex', {
inversedOperator: '$regex',
fn(field, value) {
const regexCondition = dialect.operators.comparison
.get('$regex')
.fn.call(this, field, value);
return `NOT (${regexCondition})`;
},
});
// MSSQL-specific $elemMatch using JSON functions (SQL Server 2016+)
dialect.operators.comparison.add('$elemMatch', {
fn(field, value) {
const conditions = [];
for (const key in value) {
if (!Object.prototype.hasOwnProperty.call(value, key)) {
continue;
}
const subValue = value[key];
if (
typeof subValue === 'object' &&
subValue !== null &&
!Array.isArray(subValue)
) {
// Handle nested operators
for (const operator in subValue) {
if (!Object.prototype.hasOwnProperty.call(subValue, operator)) {
continue;
}
if (operator.startsWith('$')) {
conditions.push(
handleMongoOperator(operator, subValue, field, key, dialect)
);
} else {
const valueParameter = dialect.builder._pushValue(subValue);
conditions.push(
`EXISTS (SELECT 1 FROM OPENJSON(${field}) AS element WHERE JSON_VALUE(element.value, '$.${operator}') = ${valueParameter})`
);
}
}
} else {
const valueParameter = dialect.builder._pushValue(subValue);
conditions.push(
`EXISTS (SELECT 1 FROM OPENJSON(${field}) AS element WHERE JSON_VALUE(element.value, '$.${key}') = ${valueParameter})`
);
}
}
// Validate that field contains JSON
const typeCheck = `ISJSON(${field}) = 1`;
return conditions.length > 0
? `(${typeCheck} AND (${conditions.join(' AND ')}))`
: typeCheck;
},
});
// Helper function to handle MongoDB operators in $elemMatch
function handleMongoOperator(operator, subValue, field, key, dialect) {
if (operator === '$regex') {
const options = subValue.$options || '';
const pattern = subValue[operator];
const likePattern = convertRegexToLike(pattern);
if (likePattern) {
const valueParameter = dialect.builder._pushValue(likePattern);
if (options.includes('i')) {
return `EXISTS (SELECT 1 FROM OPENJSON(${field}) AS element WHERE LOWER(JSON_VALUE(element.value, '$.${key}')) LIKE LOWER(${valueParameter}))`;
}
return `EXISTS (SELECT 1 FROM OPENJSON(${field}) AS element WHERE JSON_VALUE(element.value, '$.${key}') LIKE ${valueParameter})`;
}
// Fallback for complex patterns
const likeApproximation = pattern
.replaceAll('\\.', '.')
.replaceAll('.*', '%')
.replaceAll('.', '_')
.replaceAll('+', '')
.replaceAll('*', '%');
const valueParameter = dialect.builder._pushValue(likeApproximation);
if (options.includes('i')) {
return `EXISTS (SELECT 1 FROM OPENJSON(${field}) AS element WHERE LOWER(JSON_VALUE(element.value, '$.${key}')) LIKE LOWER(${valueParameter}))`;
}
return `EXISTS (SELECT 1 FROM OPENJSON(${field}) AS element WHERE JSON_VALUE(element.value, '$.${key}') LIKE ${valueParameter})`;
}
if (operator === '$exists') {
if (subValue[operator]) {
return `EXISTS (SELECT 1 FROM OPENJSON(${field}) AS element WHERE JSON_VALUE(element.value, '$.${key}') IS NOT NULL)`;
}
return `EXISTS (SELECT 1 FROM OPENJSON(${field}) AS element WHERE JSON_VALUE(element.value, '$.${key}') IS NULL)`;
}
if (operator === '$ne') {
const valueParameter = dialect.builder._pushValue(subValue[operator]);
return `EXISTS (SELECT 1 FROM OPENJSON(${field}) AS element WHERE JSON_VALUE(element.value, '$.${key}') != ${valueParameter})`;
}
// Default case for other operators
const valueParameter = dialect.builder._pushValue(subValue[operator]);
return `EXISTS (SELECT 1 FROM OPENJSON(${field}) AS element WHERE JSON_VALUE(element.value, '$.${key}') = ${valueParameter})`;
}
// MSSQL-specific $size implementation
dialect.operators.comparison.add('$size', {
fn(field, value) {
return handleSizeOperator(field, value, dialect);
},
});
// Helper function for $size operator
function handleSizeOperator(field, value, dialect) {
// Handle nested operators like { $gt: 0 }
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
const conditions = [];
for (const operator in value) {
if (!Object.prototype.hasOwnProperty.call(value, operator)) {
continue;
}
if (operator.startsWith('$')) {
const condition = buildSizeCondition(
field,
operator,
value[operator],
dialect
);
if (condition) {
conditions.push(condition);
}
}
}
return conditions.length > 0 ? conditions.join(' AND ') : '1=1';
}
// Simple size comparison
const valueParameter = dialect.builder._pushValue(value);
return `(SELECT COUNT(*) FROM OPENJSON(${field})) = ${valueParameter}`;
}
// Helper function to build size condition
function buildSizeCondition(field, operator, value, dialect) {
const lengthExpression = `(SELECT COUNT(*) FROM OPENJSON(${field}))`;
const operatorImpl = dialect.operators.comparison.get(operator);
if (!operatorImpl) {
return null;
}
let condition;
if (typeof operatorImpl === 'function') {
const temporaryField = lengthExpression.replaceAll('"', '');
condition = operatorImpl.call(dialect, temporaryField, value);
condition = condition.replaceAll(
dialect.wrapIdentifier(temporaryField),
lengthExpression
);
} else if (operatorImpl.fn) {
const temporaryField = lengthExpression.replaceAll('"', '');
condition = operatorImpl.fn.call(dialect, temporaryField, value);
condition = condition.replaceAll(
dialect.wrapIdentifier(temporaryField),
lengthExpression
);
}
return condition;
}
// MSSQL-specific $exists implementation
dialect.operators.comparison.add('$exists', {
fn(field, value) {
if (value) {
return `${field} IS NOT NULL`;
}
return `${field} IS NULL`;
},
});
// MSSQL-specific $not implementation
dialect.operators.comparison.add('$not', {
fn(field, value) {
return handleNotOperator(field, value, dialect);
},
});
// Helper function for $not operator
function handleNotOperator(field, value, dialect) {
if (typeof value === 'object' && value !== null) {
const conditions = [];
for (const operator in value) {
if (!Object.prototype.hasOwnProperty.call(value, operator)) {
continue;
}
const condition = buildNotCondition(field, operator, value, dialect);
if (condition) {
conditions.push(condition);
}
}
return conditions.join(' AND ');
}
const valueParameter = dialect.builder._pushValue(value);
return `${field} != ${valueParameter}`;
}
// Helper function to build not condition
function buildNotCondition(field, operator, value, dialect) {
if (operator === '$regex') {
const options = value.$options || '';
const pattern = value[operator];
const likePattern = convertRegexToLike(pattern);
if (likePattern) {
const valueParameter = dialect.builder._pushValue(likePattern);
if (options.includes('i')) {
return `NOT (LOWER(${field}) LIKE LOWER(${valueParameter}))`;
}
return `NOT (${field} LIKE ${valueParameter})`;
}
} else if (operator === '$elemMatch') {
const elementMatchCondition = dialect.operators.comparison
.get('$elemMatch')
.fn.call(dialect, field, value[operator]);
return `NOT (${elementMatchCondition})`;
}
return null;
}
// $options operator (handled by $regex, but registered to avoid errors)
dialect.operators.comparison.add('$options', {
fn(_field, _options) {
return '';
},
});
};