@objectstack/driver-memory 1.0.2 → 1.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,404 +0,0 @@
1
- import { createLogger } from '@objectstack/core';
2
- import { match, getValueByPath } from './memory-matcher.js';
3
- /**
4
- * Example: In-Memory Driver
5
- *
6
- * A minimal reference implementation of the ObjectStack Driver Protocol.
7
- * This driver stores data in a simple JavaScript object (Heap).
8
- */
9
- export class InMemoryDriver {
10
- constructor(config) {
11
- this.name = 'in-memory-driver';
12
- this.version = '0.0.1';
13
- this.supports = {
14
- // Transaction & Connection Management
15
- transactions: false,
16
- // Query Operations
17
- queryFilters: true, // Implemented via memory-matcher
18
- queryAggregations: true, // Implemented
19
- querySorting: true, // Implemented via JS sort
20
- queryPagination: true, // Implemented
21
- queryWindowFunctions: false, // TODO: Not implemented
22
- querySubqueries: false, // TODO: Not implemented
23
- joins: false, // TODO: Not implemented
24
- // Advanced Features
25
- fullTextSearch: false, // TODO: Not implemented
26
- vectorSearch: false, // TODO: Not implemented
27
- geoSpatial: false, // TODO: Not implemented
28
- jsonFields: true, // Native JS object support
29
- arrayFields: true, // Native JS array support
30
- };
31
- /**
32
- * The "Database": A map of TableName -> Array of Records
33
- */
34
- this.db = {};
35
- this.config = config || {};
36
- this.logger = config?.logger || createLogger({ level: 'info', format: 'pretty' });
37
- this.logger.debug('InMemory driver instance created', { config: this.config });
38
- }
39
- // Duck-typed RuntimePlugin hook
40
- install(ctx) {
41
- this.logger.debug('Installing InMemory driver via plugin hook');
42
- if (ctx.engine && ctx.engine.ql && typeof ctx.engine.ql.registerDriver === 'function') {
43
- ctx.engine.ql.registerDriver(this);
44
- this.logger.info('InMemory driver registered with ObjectQL engine');
45
- }
46
- else {
47
- this.logger.warn('Could not register driver - ObjectQL engine not found in context');
48
- }
49
- }
50
- // ===================================
51
- // Lifecycle
52
- // ===================================
53
- async connect() {
54
- this.logger.info('InMemory Database Connected (Virtual)');
55
- }
56
- async disconnect() {
57
- const tableCount = Object.keys(this.db).length;
58
- const recordCount = Object.values(this.db).reduce((sum, table) => sum + table.length, 0);
59
- this.db = {};
60
- this.logger.info('InMemory Database Disconnected & Cleared', {
61
- tableCount,
62
- recordCount
63
- });
64
- }
65
- async checkHealth() {
66
- this.logger.debug('Health check performed', {
67
- tableCount: Object.keys(this.db).length,
68
- status: 'healthy'
69
- });
70
- return true;
71
- }
72
- // ===================================
73
- // Execution
74
- // ===================================
75
- async execute(command, params) {
76
- this.logger.warn('Raw execution not supported in InMemory driver', { command });
77
- return null;
78
- }
79
- // ===================================
80
- // CRUD
81
- // ===================================
82
- async find(object, query, options) {
83
- this.logger.debug('Find operation', { object, query });
84
- const table = this.getTable(object);
85
- let results = table;
86
- // 1. Filter
87
- if (query.where) {
88
- results = results.filter(record => match(record, query.where));
89
- }
90
- // 1.5 Aggregation & Grouping
91
- if (query.groupBy || (query.aggregations && query.aggregations.length > 0)) {
92
- results = this.performAggregation(results, query);
93
- }
94
- // 2. Sort
95
- if (query.orderBy) {
96
- // Normalize sort to array
97
- const sortFields = Array.isArray(query.orderBy) ? query.orderBy : [query.orderBy];
98
- results.sort((a, b) => {
99
- for (const { field, order } of sortFields) {
100
- const valA = getValueByPath(a, field);
101
- const valB = getValueByPath(b, field);
102
- if (valA === valB)
103
- continue;
104
- const comparison = valA > valB ? 1 : -1;
105
- return order === 'desc' ? -comparison : comparison;
106
- }
107
- return 0;
108
- });
109
- }
110
- // 3. Pagination (Offset)
111
- if (query.offset) {
112
- results = results.slice(query.offset);
113
- }
114
- // 4. Pagination (Limit)
115
- if (query.limit) {
116
- results = results.slice(0, query.limit);
117
- }
118
- this.logger.debug('Find completed', { object, resultCount: results.length });
119
- return results;
120
- }
121
- async *findStream(object, query, options) {
122
- this.logger.debug('FindStream operation', { object });
123
- const results = await this.find(object, query, options);
124
- for (const record of results) {
125
- yield record;
126
- }
127
- }
128
- async findOne(object, query, options) {
129
- this.logger.debug('FindOne operation', { object, query });
130
- const results = await this.find(object, { ...query, limit: 1 }, options);
131
- const result = results[0] || null;
132
- this.logger.debug('FindOne completed', { object, found: !!result });
133
- return result;
134
- }
135
- async create(object, data, options) {
136
- this.logger.debug('Create operation', { object, hasData: !!data });
137
- const table = this.getTable(object);
138
- // COMPATIBILITY: Driver must return 'id' as string
139
- const newRecord = {
140
- id: data.id || this.generateId(),
141
- ...data,
142
- created_at: data.created_at || new Date(),
143
- updated_at: data.updated_at || new Date(),
144
- };
145
- table.push(newRecord);
146
- this.logger.debug('Record created', { object, id: newRecord.id, tableSize: table.length });
147
- return newRecord;
148
- }
149
- async update(object, id, data, options) {
150
- this.logger.debug('Update operation', { object, id });
151
- const table = this.getTable(object);
152
- const index = table.findIndex(r => r.id == id);
153
- if (index === -1) {
154
- this.logger.warn('Record not found for update', { object, id });
155
- throw new Error(`Record with ID ${id} not found in ${object}`);
156
- }
157
- const updatedRecord = {
158
- ...table[index],
159
- ...data,
160
- updated_at: new Date()
161
- };
162
- table[index] = updatedRecord;
163
- this.logger.debug('Record updated', { object, id });
164
- return updatedRecord;
165
- }
166
- async upsert(object, data, conflictKeys, options) {
167
- this.logger.debug('Upsert operation', { object, conflictKeys });
168
- const table = this.getTable(object);
169
- let existingRecord = null;
170
- if (data.id) {
171
- existingRecord = table.find(r => r.id === data.id);
172
- }
173
- else if (conflictKeys && conflictKeys.length > 0) {
174
- existingRecord = table.find(r => conflictKeys.every(key => r[key] === data[key]));
175
- }
176
- if (existingRecord) {
177
- this.logger.debug('Record exists, updating', { object, id: existingRecord.id });
178
- return this.update(object, existingRecord.id, data, options);
179
- }
180
- else {
181
- this.logger.debug('Record does not exist, creating', { object });
182
- return this.create(object, data, options);
183
- }
184
- }
185
- async delete(object, id, options) {
186
- this.logger.debug('Delete operation', { object, id });
187
- const table = this.getTable(object);
188
- const index = table.findIndex(r => r.id == id);
189
- if (index === -1) {
190
- this.logger.warn('Record not found for deletion', { object, id });
191
- return false;
192
- }
193
- table.splice(index, 1);
194
- this.logger.debug('Record deleted', { object, id, tableSize: table.length });
195
- return true;
196
- }
197
- async count(object, query, options) {
198
- let results = this.getTable(object);
199
- if (query?.where) {
200
- results = results.filter(record => match(record, query.where));
201
- }
202
- const count = results.length;
203
- this.logger.debug('Count operation', { object, count });
204
- return count;
205
- }
206
- // ===================================
207
- // Bulk Operations
208
- // ===================================
209
- async bulkCreate(object, dataArray, options) {
210
- this.logger.debug('BulkCreate operation', { object, count: dataArray.length });
211
- const results = await Promise.all(dataArray.map(data => this.create(object, data, options)));
212
- this.logger.debug('BulkCreate completed', { object, count: results.length });
213
- return results;
214
- }
215
- async updateMany(object, query, data, options) {
216
- this.logger.debug('UpdateMany operation', { object, query });
217
- const table = this.getTable(object);
218
- let targetRecords = table;
219
- if (query && query.where) {
220
- targetRecords = targetRecords.filter(r => match(r, query.where));
221
- }
222
- const count = targetRecords.length;
223
- // Update each record
224
- for (const record of targetRecords) {
225
- // Find index in original table
226
- const index = table.findIndex(r => r.id === record.id);
227
- if (index !== -1) {
228
- const updated = {
229
- ...table[index],
230
- ...data,
231
- updated_at: new Date()
232
- };
233
- table[index] = updated;
234
- }
235
- }
236
- this.logger.debug('UpdateMany completed', { object, count });
237
- return { count };
238
- }
239
- async deleteMany(object, query, options) {
240
- this.logger.debug('DeleteMany operation', { object, query });
241
- const table = this.getTable(object);
242
- const initialLength = table.length;
243
- // Filter IN PLACE or create new array?
244
- // Creating new array is safer for now.
245
- const remaining = table.filter(r => {
246
- if (!query || !query.where)
247
- return false; // Delete all? No, standard safety implies explicit empty filter for delete all.
248
- // Wait, normally deleteMany({}) deletes all.
249
- // Let's assume if query passed, use it.
250
- const matches = match(r, query.where);
251
- return !matches; // Keep if it DOES NOT match
252
- });
253
- this.db[object] = remaining;
254
- const count = initialLength - remaining.length;
255
- this.logger.debug('DeleteMany completed', { object, count });
256
- return { count };
257
- }
258
- // Compatibility aliases
259
- async bulkUpdate(object, updates, options) {
260
- this.logger.debug('BulkUpdate operation', { object, count: updates.length });
261
- const results = await Promise.all(updates.map(u => this.update(object, u.id, u.data, options)));
262
- this.logger.debug('BulkUpdate completed', { object, count: results.length });
263
- return results;
264
- }
265
- async bulkDelete(object, ids, options) {
266
- this.logger.debug('BulkDelete operation', { object, count: ids.length });
267
- await Promise.all(ids.map(id => this.delete(object, id, options)));
268
- this.logger.debug('BulkDelete completed', { object, count: ids.length });
269
- }
270
- // ===================================
271
- // Schema & Transactions
272
- // ===================================
273
- async syncSchema(object, schema, options) {
274
- if (!this.db[object]) {
275
- this.db[object] = [];
276
- this.logger.info('Created in-memory table', { object });
277
- }
278
- }
279
- async dropTable(object, options) {
280
- if (this.db[object]) {
281
- const recordCount = this.db[object].length;
282
- delete this.db[object];
283
- this.logger.info('Dropped in-memory table', { object, recordCount });
284
- }
285
- }
286
- async beginTransaction() {
287
- throw new Error('Transactions not supported in InMemoryDriver');
288
- }
289
- async commit() { }
290
- async rollback() { }
291
- // ===================================
292
- // Aggregation Logic
293
- // ===================================
294
- performAggregation(records, query) {
295
- const { groupBy, aggregations } = query;
296
- const groups = new Map();
297
- // 1. Group records
298
- if (groupBy && groupBy.length > 0) {
299
- for (const record of records) {
300
- // Create a composite key from group values
301
- const keyParts = groupBy.map(field => {
302
- const val = getValueByPath(record, field);
303
- return val === undefined || val === null ? 'null' : String(val);
304
- });
305
- const key = JSON.stringify(keyParts);
306
- if (!groups.has(key)) {
307
- groups.set(key, []);
308
- }
309
- groups.get(key).push(record);
310
- }
311
- }
312
- else {
313
- // No grouping -> Single group containing all records
314
- // If aggregation is requested without group by, it runs on whole set (even if empty)
315
- if (aggregations && aggregations.length > 0) {
316
- groups.set('all', records);
317
- }
318
- else {
319
- // Should not be here if performAggregation called correctly
320
- groups.set('all', records);
321
- }
322
- }
323
- // 2. Compute aggregates for each group
324
- const resultRows = [];
325
- for (const [key, groupRecords] of groups.entries()) {
326
- const row = {};
327
- // A. Add Group fields to row (if groupBy exists)
328
- if (groupBy && groupBy.length > 0) {
329
- if (groupRecords.length > 0) {
330
- const firstRecord = groupRecords[0];
331
- for (const field of groupBy) {
332
- this.setValueByPath(row, field, getValueByPath(firstRecord, field));
333
- }
334
- }
335
- }
336
- // B. Compute Aggregations
337
- if (aggregations) {
338
- for (const agg of aggregations) {
339
- const value = this.computeAggregate(groupRecords, agg);
340
- row[agg.alias] = value;
341
- }
342
- }
343
- resultRows.push(row);
344
- }
345
- return resultRows;
346
- }
347
- computeAggregate(records, agg) {
348
- const { function: func, field } = agg;
349
- const values = field ? records.map(r => getValueByPath(r, field)) : [];
350
- switch (func) {
351
- case 'count':
352
- if (!field || field === '*')
353
- return records.length;
354
- return values.filter(v => v !== null && v !== undefined).length;
355
- case 'sum':
356
- case 'avg': {
357
- const nums = values.filter(v => typeof v === 'number');
358
- const sum = nums.reduce((a, b) => a + b, 0);
359
- if (func === 'sum')
360
- return sum;
361
- return nums.length > 0 ? sum / nums.length : null;
362
- }
363
- case 'min': {
364
- // Handle comparable values
365
- const valid = values.filter(v => v !== null && v !== undefined);
366
- if (valid.length === 0)
367
- return null;
368
- // Works for numbers and strings
369
- return valid.reduce((min, v) => (v < min ? v : min), valid[0]);
370
- }
371
- case 'max': {
372
- const valid = values.filter(v => v !== null && v !== undefined);
373
- if (valid.length === 0)
374
- return null;
375
- return valid.reduce((max, v) => (v > max ? v : max), valid[0]);
376
- }
377
- default:
378
- return null;
379
- }
380
- }
381
- setValueByPath(obj, path, value) {
382
- const parts = path.split('.');
383
- let current = obj;
384
- for (let i = 0; i < parts.length - 1; i++) {
385
- const part = parts[i];
386
- if (!current[part])
387
- current[part] = {};
388
- current = current[part];
389
- }
390
- current[parts[parts.length - 1]] = value;
391
- }
392
- // ===================================
393
- // Helpers
394
- // ===================================
395
- getTable(name) {
396
- if (!this.db[name]) {
397
- this.db[name] = [];
398
- }
399
- return this.db[name];
400
- }
401
- generateId() {
402
- return Math.random().toString(36).substring(2, 15);
403
- }
404
- }
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=memory-driver.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"memory-driver.test.d.ts","sourceRoot":"","sources":["../src/memory-driver.test.ts"],"names":[],"mappings":""}
@@ -1,93 +0,0 @@
1
- import { describe, it, expect, beforeEach, vi } from 'vitest';
2
- import { InMemoryDriver } from './memory-driver.js';
3
- describe('InMemoryDriver', () => {
4
- let driver;
5
- const testTable = 'test_table';
6
- beforeEach(async () => {
7
- driver = new InMemoryDriver();
8
- await driver.connect();
9
- // No explicit clear DB method exposed, but new instance is clean.
10
- });
11
- describe('Lifecycle', () => {
12
- it('should connect successfully', async () => {
13
- expect(driver.checkHealth()).resolves.toBe(true);
14
- });
15
- it('should clear data on disconnect', async () => {
16
- await driver.create(testTable, { id: '1', name: 'test' });
17
- await driver.disconnect();
18
- const results = await driver.find(testTable, { fields: ['id'], object: testTable });
19
- expect(results).toHaveLength(0);
20
- });
21
- });
22
- describe('Plugin Installation', () => {
23
- it('should register driver with engine', () => {
24
- const registerDriverFn = vi.fn();
25
- const mockEngine = {
26
- ql: {
27
- registerDriver: registerDriverFn
28
- }
29
- };
30
- driver.install({ engine: mockEngine });
31
- expect(registerDriverFn).toHaveBeenCalledWith(driver);
32
- });
33
- it('should handle missing engine gracefully', () => {
34
- const mockCtx = {}; // No engine
35
- // Should not throw
36
- driver.install(mockCtx);
37
- });
38
- });
39
- describe('CRUD Operations', () => {
40
- it('should create and find records', async () => {
41
- const data = { id: '1', name: 'Alice', age: 30 };
42
- const created = await driver.create(testTable, data);
43
- expect(created.id).toBe('1');
44
- const results = await driver.find(testTable, {
45
- fields: ['id', 'name', 'age'],
46
- object: testTable
47
- });
48
- expect(results).toHaveLength(1);
49
- expect(results[0].id).toBe('1');
50
- expect(results[0].name).toBe('Alice');
51
- });
52
- it('should support updating records by ID', async () => {
53
- await driver.create(testTable, { id: '1', name: 'Bob', active: true });
54
- const updateResult = await driver.update(testTable, '1', { active: false });
55
- expect(updateResult.active).toBe(false);
56
- const results = await driver.find(testTable, { fields: ['active'], object: testTable });
57
- expect(results[0].active).toBe(false);
58
- });
59
- it('should support deleting records by ID', async () => {
60
- await driver.create(testTable, { id: '1', name: 'Charlie' });
61
- await driver.create(testTable, { id: '2', name: 'David' });
62
- const deleteResult = await driver.delete(testTable, '1');
63
- expect(deleteResult).toBe(true);
64
- const results = await driver.find(testTable, { fields: ['name'], object: testTable });
65
- expect(results).toHaveLength(1);
66
- expect(results[0].name).toBe('David');
67
- });
68
- });
69
- describe('Query Capability', () => {
70
- it('should filter results', async () => {
71
- await driver.create(testTable, { id: '1', role: 'admin' });
72
- await driver.create(testTable, { id: '2', role: 'user' });
73
- await driver.create(testTable, { id: '3', role: 'user' });
74
- const results = await driver.find(testTable, {
75
- fields: ['id'],
76
- object: testTable,
77
- where: { role: 'user' }
78
- });
79
- expect(results).toHaveLength(2);
80
- });
81
- it('should limit results', async () => {
82
- await driver.create(testTable, { id: '1' });
83
- await driver.create(testTable, { id: '2' });
84
- await driver.create(testTable, { id: '3' });
85
- const results = await driver.find(testTable, {
86
- fields: ['id'],
87
- object: testTable,
88
- limit: 2
89
- });
90
- expect(results).toHaveLength(2);
91
- });
92
- });
93
- });
@@ -1,19 +0,0 @@
1
- /**
2
- * Simple In-Memory Query Matcher
3
- *
4
- * Implements a subset of the ObjectStack Filter Protocol (MongoDB-compatible)
5
- * for evaluating conditions against in-memory JavaScript objects.
6
- */
7
- type RecordType = Record<string, any>;
8
- /**
9
- * matches - Check if a record matches a filter criteria
10
- * @param record The data record to check
11
- * @param filter The filter condition (where clause)
12
- */
13
- export declare function match(record: RecordType, filter: any): boolean;
14
- /**
15
- * Access nested properties via dot-notation (e.g. "user.name")
16
- */
17
- export declare function getValueByPath(obj: any, path: string): any;
18
- export {};
19
- //# sourceMappingURL=memory-matcher.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"memory-matcher.d.ts","sourceRoot":"","sources":["../src/memory-matcher.ts"],"names":[],"mappings":"AACA;;;;;GAKG;AAEH,KAAK,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;AAEtC;;;;GAIG;AACH,wBAAgB,KAAK,CAAC,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,GAAG,GAAG,OAAO,CAyC9D;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,GAAG,GAAG,CAG1D"}
@@ -1,160 +0,0 @@
1
- /**
2
- * Simple In-Memory Query Matcher
3
- *
4
- * Implements a subset of the ObjectStack Filter Protocol (MongoDB-compatible)
5
- * for evaluating conditions against in-memory JavaScript objects.
6
- */
7
- /**
8
- * matches - Check if a record matches a filter criteria
9
- * @param record The data record to check
10
- * @param filter The filter condition (where clause)
11
- */
12
- export function match(record, filter) {
13
- if (!filter || Object.keys(filter).length === 0)
14
- return true;
15
- // 1. Handle Top-Level Logical Operators ($and, $or, $not)
16
- // These usually appear at the root or nested.
17
- // $and: [ { ... }, { ... } ]
18
- if (Array.isArray(filter.$and)) {
19
- if (!filter.$and.every((f) => match(record, f))) {
20
- return false;
21
- }
22
- }
23
- // $or: [ { ... }, { ... } ]
24
- if (Array.isArray(filter.$or)) {
25
- if (!filter.$or.some((f) => match(record, f))) {
26
- return false;
27
- }
28
- }
29
- // $not: { ... }
30
- if (filter.$not) {
31
- if (match(record, filter.$not)) {
32
- return false;
33
- }
34
- }
35
- // 2. Iterate over field constraints
36
- for (const key of Object.keys(filter)) {
37
- // Skip logical operators we already handled (or future ones)
38
- if (key.startsWith('$'))
39
- continue;
40
- const condition = filter[key];
41
- const value = getValueByPath(record, key);
42
- if (!checkCondition(value, condition)) {
43
- return false;
44
- }
45
- }
46
- return true;
47
- }
48
- /**
49
- * Access nested properties via dot-notation (e.g. "user.name")
50
- */
51
- export function getValueByPath(obj, path) {
52
- if (!path.includes('.'))
53
- return obj[path];
54
- return path.split('.').reduce((o, i) => (o ? o[i] : undefined), obj);
55
- }
56
- /**
57
- * Evaluate a specific condition against a value
58
- */
59
- function checkCondition(value, condition) {
60
- // Case A: Implicit Equality (e.g. status: 'active')
61
- // If condition is a primitive or Date/Array (exact match), treat as equality.
62
- if (typeof condition !== 'object' ||
63
- condition === null ||
64
- condition instanceof Date ||
65
- Array.isArray(condition)) {
66
- // Loose equality to handle undefined/null mismatch or string/number coercion if desired.
67
- // But stick to == for JS loose equality which is often convenient in weakly typed queries.
68
- return value == condition;
69
- }
70
- // Case B: Operator Object (e.g. { $gt: 10, $lt: 20 })
71
- const keys = Object.keys(condition);
72
- const isOperatorObject = keys.some(k => k.startsWith('$'));
73
- if (!isOperatorObject) {
74
- // It's just a nested object comparison or implicit equality against an object
75
- // Simplistic check:
76
- return JSON.stringify(value) === JSON.stringify(condition);
77
- }
78
- // Iterate operators
79
- for (const op of keys) {
80
- const target = condition[op];
81
- // Handle undefined values
82
- if (value === undefined && op !== '$exists' && op !== '$ne') {
83
- return false;
84
- }
85
- switch (op) {
86
- case '$eq':
87
- if (value != target)
88
- return false;
89
- break;
90
- case '$ne':
91
- if (value == target)
92
- return false;
93
- break;
94
- // Numeric / Date
95
- case '$gt':
96
- if (!(value > target))
97
- return false;
98
- break;
99
- case '$gte':
100
- if (!(value >= target))
101
- return false;
102
- break;
103
- case '$lt':
104
- if (!(value < target))
105
- return false;
106
- break;
107
- case '$lte':
108
- if (!(value <= target))
109
- return false;
110
- break;
111
- case '$between':
112
- // target should be [min, max]
113
- if (Array.isArray(target) && (value < target[0] || value > target[1]))
114
- return false;
115
- break;
116
- // Sets
117
- case '$in':
118
- if (!Array.isArray(target) || !target.includes(value))
119
- return false;
120
- break;
121
- case '$nin':
122
- if (Array.isArray(target) && target.includes(value))
123
- return false;
124
- break;
125
- // Existence
126
- case '$exists':
127
- const exists = value !== undefined && value !== null;
128
- if (exists !== !!target)
129
- return false;
130
- break;
131
- // Strings
132
- case '$contains':
133
- if (typeof value !== 'string' || !value.includes(target))
134
- return false;
135
- break;
136
- case '$startsWith':
137
- if (typeof value !== 'string' || !value.startsWith(target))
138
- return false;
139
- break;
140
- case '$endsWith':
141
- if (typeof value !== 'string' || !value.endsWith(target))
142
- return false;
143
- break;
144
- case '$regex':
145
- try {
146
- const re = new RegExp(target, condition.$options || '');
147
- if (!re.test(String(value)))
148
- return false;
149
- }
150
- catch (e) {
151
- return false;
152
- }
153
- break;
154
- default:
155
- // Unknown operator, ignore or fail. Ignoring safe for optional features.
156
- break;
157
- }
158
- }
159
- return true;
160
- }