Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
### Version: 5.0.1
#### Date: feb-23-2026
Fix: Added support of special symbols in regex method with safe pattern.

### Version: 5.0.0
#### Date: Feb-16-2026
Breaking: Cache persistence is now a separate plugin. When using a cache policy other than `IGNORE_CACHE`, you must pass `cacheOptions.persistenceStore`. Install `@contentstack/persistence-plugin` and use `new PersistenceStore({ ... })` as the store. The SDK no longer bundles persistence code or accepts `storeType` in `cacheOptions`.
Expand Down
10 changes: 5 additions & 5 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@contentstack/delivery-sdk",
"version": "5.0.0",
"version": "5.0.1",
"type": "module",
"license": "MIT",
"engines": {
Expand Down
15 changes: 9 additions & 6 deletions src/query/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,17 +29,20 @@
return alphanumericRegex.test(input);
}

// Validate if input matches any of the safe, pre-approved patterns
// Validate if input matches safe regex patterns
private isValidRegexPattern(input: string): boolean {
const validRegex = /^[a-zA-Z0-9|^$.*+?()[\]{}\\-]+$/; // Allow only safe regex characters
// Expanded whitelist: includes spaces and common safe special characters
// Allows: alphanumeric, regex metacharacters, regular spaces, and common punctuation
// Blocks: control characters (newlines, tabs, null bytes), backticks, and other dangerous chars
const validRegex = /^[a-zA-Z0-9|^$.*+?()[\]{}:,;&@#%=/!'"_~<> -]+$/;
if (!validRegex.test(input)) {
return false;
return false;

Check warning on line 39 in src/query/query.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🧾 Statement is not covered

Warning! Not covered statement
}

Check warning on line 40 in src/query/query.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🌿 Branch is not covered

Warning! Not covered branch
try {
new RegExp(input);
return true;
new RegExp(input);
return true;
} catch (e) {
return false;
return false;
}
}

Expand Down
19 changes: 19 additions & 0 deletions test/unit/content-validation-comprehensive.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -698,6 +698,25 @@ describe('Content Validation - Comprehensive Test Suite', () => {
expect(() => query.regex('title', '*invalid')).toThrow(ErrorMessages.INVALID_REGEX_PATTERN);
});

it('should accept regex patterns with spaces and special characters in blog queries', () => {
const query = new Query(client, {}, {}, '', 'blog_post');

// Patterns with spaces
expect(() => query.regex('title', '.*blog post.*', 'i')).not.toThrow();
expect(() => query.regex('title', '.*global flex.*', 'i')).not.toThrow();

// Patterns with punctuation
expect(() => query.regex('title', '.*test:value.*', 'i')).not.toThrow();
expect(() => query.regex('title', '.*test,value.*', 'i')).not.toThrow();
expect(() => query.regex('title', '.*test&value.*', 'i')).not.toThrow();
expect(() => query.regex('content', '.*https://example.com.*', 'i')).not.toThrow();
expect(() => query.regex('title', '.*test#tag.*', 'i')).not.toThrow();

// Verify parameters are set correctly
query.regex('title', '.*search term.*', 'i');
expect(query._parameters.title).toEqual({ $regex: '.*search term.*', $options: 'i' });
});

it('should validate query value types', () => {
const query = new Query(client, {}, {}, '', 'blog_post');

Expand Down
17 changes: 17 additions & 0 deletions test/unit/query-optimization-comprehensive.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,23 @@ describe('Query Optimization - Comprehensive Test Suite', () => {
expect(() => query.regex('title', '*invalid')).toThrow(ErrorMessages.INVALID_REGEX_PATTERN);
});

it('should accept regex patterns with spaces and special characters', () => {
// Patterns with spaces (user search scenarios)
expect(() => query.regex('title', '.*test er.*', 'i')).not.toThrow();
expect(() => query.regex('title', '.*global flex.*', 'i')).not.toThrow();
expect(() => query.regex('title', '.*two words.*', 'i')).not.toThrow();

// Patterns with special characters
expect(() => query.regex('title', '.*test:value.*', 'i')).not.toThrow();
expect(() => query.regex('title', '.*test,value.*', 'i')).not.toThrow();
expect(() => query.regex('title', '.*test&value.*', 'i')).not.toThrow();
expect(() => query.regex('email', '.*@example.com.*', 'i')).not.toThrow();
expect(() => query.regex('url', '.*https://site.com.*', 'i')).not.toThrow();
expect(() => query.regex('title', '.*test#tag.*', 'i')).not.toThrow();
expect(() => query.regex('title', ".*test'value.*", 'i')).not.toThrow();
expect(() => query.regex('title', '.*test_value.*', 'i')).not.toThrow();
});

it('should validate containedIn values for proper types', () => {
const consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => {});

Expand Down
67 changes: 65 additions & 2 deletions test/unit/query.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,16 +89,79 @@ describe('Query class', () => {
expect(() => regexQuery.regex("fieldUid", "[a-z")).toThrow("Invalid regexPattern: Must be a valid regular expression");
});

it('should throw error when regex method is called with invalid characters', async () => {
it('should throw error when regex method is called with invalid regex pattern', async () => {
const regexQuery = getQueryObject(client, 'referenced-content-type-uid');
expect(() => regexQuery.regex("fieldUid", "test<script>")).toThrow("Invalid regexPattern: Must be a valid regular expression");
// Use an actually invalid regex pattern (unclosed bracket)
expect(() => regexQuery.regex("fieldUid", "test[invalid(")).toThrow("Invalid regexPattern: Must be a valid regular expression");
});

it('should add a regex parameter to _parameters when regex method is called with valid regex', () => {
query.regex('fieldUid', '^ABCXYZ123');
expect(query._parameters['fieldUid']).toEqual({ $regex: '^ABCXYZ123' });
});

describe('regex with special characters and spaces', () => {
it('should accept regex pattern with spaces', () => {
const regexQuery = getQueryObject(client, 'contentTypeUid');
expect(() => regexQuery.regex('title', '.*test er.*', 'i')).not.toThrow();
expect(regexQuery._parameters['title']).toEqual({ $regex: '.*test er.*', $options: 'i' });
});

it('should accept regex pattern with multiple spaces (e.g. user search)', () => {
const regexQuery = getQueryObject(client, 'contentTypeUid');
expect(() => regexQuery.regex('title', '.*global flex.*', 'i')).not.toThrow();
expect(regexQuery._parameters['title']).toEqual({ $regex: '.*global flex.*', $options: 'i' });
});

it('should accept regex pattern with colon', () => {
const regexQuery = getQueryObject(client, 'contentTypeUid');
expect(() => regexQuery.regex('title', '.*test:value.*', 'i')).not.toThrow();
expect(regexQuery._parameters['title'].$regex).toBe('.*test:value.*');
});

it('should accept regex pattern with comma', () => {
const regexQuery = getQueryObject(client, 'contentTypeUid');
expect(() => regexQuery.regex('title', '.*test,value.*', 'i')).not.toThrow();
expect(regexQuery._parameters['title'].$regex).toBe('.*test,value.*');
});

it('should accept regex pattern with ampersand', () => {
const regexQuery = getQueryObject(client, 'contentTypeUid');
expect(() => regexQuery.regex('title', '.*test&value.*', 'i')).not.toThrow();
expect(regexQuery._parameters['title'].$regex).toBe('.*test&value.*');
});

it('should accept regex pattern with at sign (e.g. email)', () => {
const regexQuery = getQueryObject(client, 'contentTypeUid');
expect(() => regexQuery.regex('email', '.*@example.com.*', 'i')).not.toThrow();
expect(regexQuery._parameters['email'].$regex).toBe('.*@example.com.*');
});

it('should accept regex pattern with semicolon, equals, slash', () => {
const regexQuery = getQueryObject(client, 'contentTypeUid');
expect(() => regexQuery.regex('url', '.*https://example.com.*', 'i')).not.toThrow();
expect(regexQuery._parameters['url'].$regex).toBe('.*https://example.com.*');
});

it('should accept regex pattern with hash and percent', () => {
const regexQuery = getQueryObject(client, 'contentTypeUid');
expect(() => regexQuery.regex('title', '.*test#tag.*', 'i')).not.toThrow();
expect(() => regexQuery.regex('title', '.*test%20.*', 'i')).not.toThrow();
});

it('should accept regex on nested/global field with space in pattern', () => {
const regexQuery = getQueryObject(client, 'contentTypeUid');
expect(() => regexQuery.regex('card.heading', '.*two words.*', 'i')).not.toThrow();
expect(regexQuery._parameters['card.heading']).toEqual({ $regex: '.*two words.*', $options: 'i' });
});

it('should add regex options when third argument provided', () => {
const regexQuery = getQueryObject(client, 'contentTypeUid');
regexQuery.regex('fieldUid', '.*search.*', 'i');
expect(regexQuery._parameters['fieldUid']).toEqual({ $regex: '.*search.*', $options: 'i' });
});
});

it('should add a containedIn parameter to _parameters', () => {
query.containedIn('fieldUid', ['value1', 'value2']);
expect(query._parameters['fieldUid']).toEqual({ '$in': ['value1', 'value2'] });
Expand Down