Skip to content

Commit ef8b93d

Browse files
Merge pull request #329 from contentstack/staging
DX | 23-02-2026 | Release
2 parents 7066ee7 + 62e9280 commit ef8b93d

File tree

7 files changed

+120
-14
lines changed

7 files changed

+120
-14
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
### Version: 5.0.1
2+
#### Date: feb-23-2026
3+
Fix: Added support of special symbols in regex method with safe pattern.
4+
15
### Version: 5.0.0
26
#### Date: Feb-16-2026
37
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`.

package-lock.json

Lines changed: 5 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@contentstack/delivery-sdk",
3-
"version": "5.0.0",
3+
"version": "5.0.1",
44
"type": "module",
55
"license": "MIT",
66
"engines": {

src/query/query.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,17 +29,20 @@ export class Query extends BaseQuery {
2929
return alphanumericRegex.test(input);
3030
}
3131

32-
// Validate if input matches any of the safe, pre-approved patterns
32+
// Validate if input matches safe regex patterns
3333
private isValidRegexPattern(input: string): boolean {
34-
const validRegex = /^[a-zA-Z0-9|^$.*+?()[\]{}\\-]+$/; // Allow only safe regex characters
34+
// Expanded whitelist: includes spaces and common safe special characters
35+
// Allows: alphanumeric, regex metacharacters, regular spaces, and common punctuation
36+
// Blocks: control characters (newlines, tabs, null bytes), backticks, and other dangerous chars
37+
const validRegex = /^[a-zA-Z0-9|^$.*+?()[\]{}:,;&@#%=/!'"_~<> -]+$/;
3538
if (!validRegex.test(input)) {
36-
return false;
39+
return false;
3740
}
3841
try {
39-
new RegExp(input);
40-
return true;
42+
new RegExp(input);
43+
return true;
4144
} catch (e) {
42-
return false;
45+
return false;
4346
}
4447
}
4548

test/unit/content-validation-comprehensive.spec.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -698,6 +698,25 @@ describe('Content Validation - Comprehensive Test Suite', () => {
698698
expect(() => query.regex('title', '*invalid')).toThrow(ErrorMessages.INVALID_REGEX_PATTERN);
699699
});
700700

701+
it('should accept regex patterns with spaces and special characters in blog queries', () => {
702+
const query = new Query(client, {}, {}, '', 'blog_post');
703+
704+
// Patterns with spaces
705+
expect(() => query.regex('title', '.*blog post.*', 'i')).not.toThrow();
706+
expect(() => query.regex('title', '.*global flex.*', 'i')).not.toThrow();
707+
708+
// Patterns with punctuation
709+
expect(() => query.regex('title', '.*test:value.*', 'i')).not.toThrow();
710+
expect(() => query.regex('title', '.*test,value.*', 'i')).not.toThrow();
711+
expect(() => query.regex('title', '.*test&value.*', 'i')).not.toThrow();
712+
expect(() => query.regex('content', '.*https://example.com.*', 'i')).not.toThrow();
713+
expect(() => query.regex('title', '.*test#tag.*', 'i')).not.toThrow();
714+
715+
// Verify parameters are set correctly
716+
query.regex('title', '.*search term.*', 'i');
717+
expect(query._parameters.title).toEqual({ $regex: '.*search term.*', $options: 'i' });
718+
});
719+
701720
it('should validate query value types', () => {
702721
const query = new Query(client, {}, {}, '', 'blog_post');
703722

test/unit/query-optimization-comprehensive.spec.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,23 @@ describe('Query Optimization - Comprehensive Test Suite', () => {
195195
expect(() => query.regex('title', '*invalid')).toThrow(ErrorMessages.INVALID_REGEX_PATTERN);
196196
});
197197

198+
it('should accept regex patterns with spaces and special characters', () => {
199+
// Patterns with spaces (user search scenarios)
200+
expect(() => query.regex('title', '.*test er.*', 'i')).not.toThrow();
201+
expect(() => query.regex('title', '.*global flex.*', 'i')).not.toThrow();
202+
expect(() => query.regex('title', '.*two words.*', 'i')).not.toThrow();
203+
204+
// Patterns with special characters
205+
expect(() => query.regex('title', '.*test:value.*', 'i')).not.toThrow();
206+
expect(() => query.regex('title', '.*test,value.*', 'i')).not.toThrow();
207+
expect(() => query.regex('title', '.*test&value.*', 'i')).not.toThrow();
208+
expect(() => query.regex('email', '.*@example.com.*', 'i')).not.toThrow();
209+
expect(() => query.regex('url', '.*https://site.com.*', 'i')).not.toThrow();
210+
expect(() => query.regex('title', '.*test#tag.*', 'i')).not.toThrow();
211+
expect(() => query.regex('title', ".*test'value.*", 'i')).not.toThrow();
212+
expect(() => query.regex('title', '.*test_value.*', 'i')).not.toThrow();
213+
});
214+
198215
it('should validate containedIn values for proper types', () => {
199216
const consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
200217

test/unit/query.spec.ts

Lines changed: 65 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,16 +89,79 @@ describe('Query class', () => {
8989
expect(() => regexQuery.regex("fieldUid", "[a-z")).toThrow("Invalid regexPattern: Must be a valid regular expression");
9090
});
9191

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

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

103+
describe('regex with special characters and spaces', () => {
104+
it('should accept regex pattern with spaces', () => {
105+
const regexQuery = getQueryObject(client, 'contentTypeUid');
106+
expect(() => regexQuery.regex('title', '.*test er.*', 'i')).not.toThrow();
107+
expect(regexQuery._parameters['title']).toEqual({ $regex: '.*test er.*', $options: 'i' });
108+
});
109+
110+
it('should accept regex pattern with multiple spaces (e.g. user search)', () => {
111+
const regexQuery = getQueryObject(client, 'contentTypeUid');
112+
expect(() => regexQuery.regex('title', '.*global flex.*', 'i')).not.toThrow();
113+
expect(regexQuery._parameters['title']).toEqual({ $regex: '.*global flex.*', $options: 'i' });
114+
});
115+
116+
it('should accept regex pattern with colon', () => {
117+
const regexQuery = getQueryObject(client, 'contentTypeUid');
118+
expect(() => regexQuery.regex('title', '.*test:value.*', 'i')).not.toThrow();
119+
expect(regexQuery._parameters['title'].$regex).toBe('.*test:value.*');
120+
});
121+
122+
it('should accept regex pattern with comma', () => {
123+
const regexQuery = getQueryObject(client, 'contentTypeUid');
124+
expect(() => regexQuery.regex('title', '.*test,value.*', 'i')).not.toThrow();
125+
expect(regexQuery._parameters['title'].$regex).toBe('.*test,value.*');
126+
});
127+
128+
it('should accept regex pattern with ampersand', () => {
129+
const regexQuery = getQueryObject(client, 'contentTypeUid');
130+
expect(() => regexQuery.regex('title', '.*test&value.*', 'i')).not.toThrow();
131+
expect(regexQuery._parameters['title'].$regex).toBe('.*test&value.*');
132+
});
133+
134+
it('should accept regex pattern with at sign (e.g. email)', () => {
135+
const regexQuery = getQueryObject(client, 'contentTypeUid');
136+
expect(() => regexQuery.regex('email', '.*@example.com.*', 'i')).not.toThrow();
137+
expect(regexQuery._parameters['email'].$regex).toBe('.*@example.com.*');
138+
});
139+
140+
it('should accept regex pattern with semicolon, equals, slash', () => {
141+
const regexQuery = getQueryObject(client, 'contentTypeUid');
142+
expect(() => regexQuery.regex('url', '.*https://example.com.*', 'i')).not.toThrow();
143+
expect(regexQuery._parameters['url'].$regex).toBe('.*https://example.com.*');
144+
});
145+
146+
it('should accept regex pattern with hash and percent', () => {
147+
const regexQuery = getQueryObject(client, 'contentTypeUid');
148+
expect(() => regexQuery.regex('title', '.*test#tag.*', 'i')).not.toThrow();
149+
expect(() => regexQuery.regex('title', '.*test%20.*', 'i')).not.toThrow();
150+
});
151+
152+
it('should accept regex on nested/global field with space in pattern', () => {
153+
const regexQuery = getQueryObject(client, 'contentTypeUid');
154+
expect(() => regexQuery.regex('card.heading', '.*two words.*', 'i')).not.toThrow();
155+
expect(regexQuery._parameters['card.heading']).toEqual({ $regex: '.*two words.*', $options: 'i' });
156+
});
157+
158+
it('should add regex options when third argument provided', () => {
159+
const regexQuery = getQueryObject(client, 'contentTypeUid');
160+
regexQuery.regex('fieldUid', '.*search.*', 'i');
161+
expect(regexQuery._parameters['fieldUid']).toEqual({ $regex: '.*search.*', $options: 'i' });
162+
});
163+
});
164+
102165
it('should add a containedIn parameter to _parameters', () => {
103166
query.containedIn('fieldUid', ['value1', 'value2']);
104167
expect(query._parameters['fieldUid']).toEqual({ '$in': ['value1', 'value2'] });

0 commit comments

Comments
 (0)