Skip to content

Commit 41b6831

Browse files
Copilothotlong
andcommitted
fix: address review feedback — multi-regex overwrite, $null guard, test name
1. normalizeFieldOperators: collect regex-producing operators ($contains, $startsWith, $endsWith) into an array; when multiple exist on the same field, promote to $and so no regex silently overwrites another. 2. memory-matcher checkCondition: exclude $null from the undefined early- return guard so $null: true correctly matches missing fields. 3. Rename test from "$contains inside $or" to "$startsWith inside $or" to match actual test body. 4. Add tests: combined $startsWith + $endsWith on same field, $null: true on records with missing fields. Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
1 parent 07b0307 commit 41b6831

3 files changed

Lines changed: 74 additions & 6 deletions

File tree

packages/plugins/driver-memory/src/memory-driver.test.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -677,7 +677,7 @@ describe('InMemoryDriver', () => {
677677
expect(results.map((r: any) => r.name).sort()).toEqual(['Alice Johnson', 'Charlie Brown']);
678678
});
679679

680-
it('should handle $contains inside $or', async () => {
680+
it('should handle $startsWith inside $or', async () => {
681681
const results = await driver.find(testTable, {
682682
object: testTable,
683683
where: { $or: [{ name: { $startsWith: 'Al' } }, { name: { $startsWith: 'Ev' } }] },
@@ -693,5 +693,28 @@ describe('InMemoryDriver', () => {
693693
});
694694
expect(results).toHaveLength(3);
695695
});
696+
697+
it('should handle combined $startsWith + $endsWith on same field', async () => {
698+
const results = await driver.find(testTable, {
699+
object: testTable,
700+
where: { name: { $startsWith: 'A', $endsWith: 'son' } },
701+
});
702+
expect(results).toHaveLength(1);
703+
expect(results[0].name).toBe('Alice Johnson');
704+
});
705+
706+
it('should filter with $null: true on missing fields', async () => {
707+
// bio is null for Alice Johnson — should match
708+
// Records without bio field at all should also match $null: true
709+
await driver.create(testTable, { id: '5', name: 'Frank', age: 40, email: 'frank@test.com' });
710+
const results = await driver.find(testTable, {
711+
object: testTable,
712+
where: { bio: { $null: true } },
713+
});
714+
// Alice (bio: null), Frank (bio: missing)
715+
expect(results.length).toBeGreaterThanOrEqual(2);
716+
expect(results.map((r: any) => r.name)).toContain('Alice Johnson');
717+
expect(results.map((r: any) => r.name)).toContain('Frank');
718+
});
696719
});
697720
});

packages/plugins/driver-memory/src/memory-driver.ts

Lines changed: 49 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -718,6 +718,8 @@ export class InMemoryDriver implements DriverInterface {
718718
*/
719719
private normalizeFilterCondition(filter: Record<string, any>): Record<string, any> {
720720
const result: Record<string, any> = {};
721+
const extraAndConditions: Record<string, any>[] = [];
722+
721723
for (const key of Object.keys(filter)) {
722724
const value = filter[key];
723725
// Recurse into logical operators
@@ -740,33 +742,63 @@ export class InMemoryDriver implements DriverInterface {
740742
}
741743
// Field-level: value may be primitive (implicit eq) or operator object
742744
if (value && typeof value === 'object' && !Array.isArray(value) && !(value instanceof Date) && !(value instanceof RegExp)) {
743-
result[key] = this.normalizeFieldOperators(value);
745+
const normalized = this.normalizeFieldOperators(value);
746+
// Handle multiple regex conditions on the same field (e.g. $startsWith + $endsWith)
747+
if (normalized.__$andRegex) {
748+
const regexConditions: Record<string, any>[] = normalized.__$andRegex;
749+
delete normalized.__$andRegex;
750+
// Each regex becomes its own { field: { $regex: ... } } inside $and
751+
for (const rc of regexConditions) {
752+
extraAndConditions.push({ [key]: { ...normalized, ...rc } });
753+
}
754+
} else {
755+
result[key] = normalized;
756+
}
744757
} else {
745758
result[key] = value;
746759
}
747760
}
761+
762+
// Merge extra $and conditions from multi-regex fields
763+
if (extraAndConditions.length > 0) {
764+
const existing = result.$and;
765+
const andArray = Array.isArray(existing) ? existing : [];
766+
// Include the rest of result as a condition too
767+
if (Object.keys(result).filter(k => k !== '$and').length > 0) {
768+
const rest = { ...result };
769+
delete rest.$and;
770+
andArray.push(rest);
771+
}
772+
andArray.push(...extraAndConditions);
773+
return { $and: andArray };
774+
}
775+
748776
return result;
749777
}
750778

751779
/**
752780
* Convert non-standard field operators to Mingo-compatible format.
781+
* When multiple regex-producing operators appear on the same field
782+
* (e.g. $startsWith + $endsWith), they are combined via $and.
753783
*/
754784
private normalizeFieldOperators(ops: Record<string, any>): Record<string, any> {
755785
const result: Record<string, any> = {};
786+
const regexConditions: Record<string, any>[] = [];
787+
756788
for (const op of Object.keys(ops)) {
757789
const val = ops[op];
758790
switch (op) {
759791
case '$contains':
760-
result.$regex = new RegExp(this.escapeRegex(val), 'i');
792+
regexConditions.push({ $regex: new RegExp(this.escapeRegex(val), 'i') });
761793
break;
762794
case '$notContains':
763795
result.$not = { $regex: new RegExp(this.escapeRegex(val), 'i') };
764796
break;
765797
case '$startsWith':
766-
result.$regex = new RegExp(`^${this.escapeRegex(val)}`, 'i');
798+
regexConditions.push({ $regex: new RegExp(`^${this.escapeRegex(val)}`, 'i') });
767799
break;
768800
case '$endsWith':
769-
result.$regex = new RegExp(`${this.escapeRegex(val)}$`, 'i');
801+
regexConditions.push({ $regex: new RegExp(`${this.escapeRegex(val)}$`, 'i') });
770802
break;
771803
case '$between':
772804
if (Array.isArray(val) && val.length === 2) {
@@ -788,6 +820,19 @@ export class InMemoryDriver implements DriverInterface {
788820
break;
789821
}
790822
}
823+
824+
// Merge regex conditions: single → inline, multiple → wrap with $and
825+
if (regexConditions.length === 1) {
826+
Object.assign(result, regexConditions[0]);
827+
} else if (regexConditions.length > 1) {
828+
// Cannot have multiple $regex on one object; promote to top-level $and
829+
// The caller (normalizeFilterCondition) handles field-level objects,
830+
// so we return a sentinel that the caller will need to unwrap.
831+
// Simpler approach: just assign the first and use $and for the rest isn't
832+
// possible in flat Mingo format. Use __$and sentinel for the caller.
833+
result.__$andRegex = regexConditions;
834+
}
835+
791836
return result;
792837
}
793838

packages/plugins/driver-memory/src/memory-matcher.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ function checkCondition(value: any, condition: any): boolean {
103103
const target = condition[op];
104104

105105
// Handle undefined values
106-
if (value === undefined && op !== '$exists' && op !== '$ne') {
106+
if (value === undefined && op !== '$exists' && op !== '$ne' && op !== '$null') {
107107
return false;
108108
}
109109

0 commit comments

Comments
 (0)