@yunsoft/yuncms-core 0.1.16 → 0.1.17

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yunsoft/yuncms-core",
3
- "version": "0.1.16",
3
+ "version": "0.1.17",
4
4
  "description": "Core services, schema, authentication, permissions and storage primitives for YunCMS.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -0,0 +1,110 @@
1
+ const NOW_PATTERN = /^\$NOW(?:\(([+-]\d+)\s+(second|minute|hour|day|week|month|year)s?\))?$/i;
2
+ const MAX_ADJUSTMENT = 10_000;
3
+
4
+ function dynamicVariableError(code, message, path = null) {
5
+ const error = new Error(message);
6
+ error.code = code;
7
+ if (path) error.path = path;
8
+ return error;
9
+ }
10
+
11
+ function normalizedNow(value) {
12
+ const date = value == null ? new Date() : new Date(value);
13
+ if (Number.isNaN(date.getTime())) {
14
+ throw dynamicVariableError('INVALID_QUERY', 'Dynamic variable context contains an invalid current timestamp');
15
+ }
16
+ return date;
17
+ }
18
+
19
+ function adjustNow(now, amount, unit, path) {
20
+ if (!Number.isSafeInteger(amount) || Math.abs(amount) > MAX_ADJUSTMENT) {
21
+ throw dynamicVariableError(
22
+ 'INVALID_QUERY',
23
+ `$NOW adjustment must be a signed integer between -${MAX_ADJUSTMENT} and ${MAX_ADJUSTMENT}`,
24
+ path,
25
+ );
26
+ }
27
+
28
+ const result = new Date(now.getTime());
29
+ switch (unit.toLowerCase()) {
30
+ case 'second': result.setUTCSeconds(result.getUTCSeconds() + amount); break;
31
+ case 'minute': result.setUTCMinutes(result.getUTCMinutes() + amount); break;
32
+ case 'hour': result.setUTCHours(result.getUTCHours() + amount); break;
33
+ case 'day': result.setUTCDate(result.getUTCDate() + amount); break;
34
+ case 'week': result.setUTCDate(result.getUTCDate() + (amount * 7)); break;
35
+ case 'month': result.setUTCMonth(result.getUTCMonth() + amount); break;
36
+ case 'year': result.setUTCFullYear(result.getUTCFullYear() + amount); break;
37
+ default: throw dynamicVariableError('INVALID_QUERY', `Unsupported $NOW adjustment unit: ${unit}`, path);
38
+ }
39
+ if (Number.isNaN(result.getTime())) {
40
+ throw dynamicVariableError('INVALID_QUERY', '$NOW adjustment produced an invalid timestamp', path);
41
+ }
42
+ return result;
43
+ }
44
+
45
+ export function resolveDynamicVariable(value, context = {}, { allowUnresolved = false, path = null } = {}) {
46
+ if (typeof value !== 'string') return value;
47
+
48
+ if (value === '$CURRENT_USER' || value === '$CURRENT_ROLE') {
49
+ if (allowUnresolved) return value;
50
+ const key = value === '$CURRENT_USER' ? 'user' : 'role';
51
+ const resolved = context?.[key];
52
+ if (resolved == null || resolved === '') {
53
+ throw dynamicVariableError(
54
+ 'FORBIDDEN',
55
+ `${value} is unavailable for the current accountability context`,
56
+ path,
57
+ );
58
+ }
59
+ return String(resolved);
60
+ }
61
+
62
+ if (value === '$NOW' || value.toUpperCase().startsWith('$NOW(')) {
63
+ const match = value.match(NOW_PATTERN);
64
+ if (!match) {
65
+ throw dynamicVariableError(
66
+ 'INVALID_QUERY',
67
+ 'Invalid $NOW value; use $NOW or a signed adjustment such as $NOW(-1 day)',
68
+ path,
69
+ );
70
+ }
71
+ if (allowUnresolved) return value;
72
+ const now = normalizedNow(context?.now);
73
+ return match[1] ? adjustNow(now, Number(match[1]), match[2], path) : now;
74
+ }
75
+
76
+ if (value.startsWith('$CURRENT_USER.') || value.startsWith('$CURRENT_ROLE.')) {
77
+ throw dynamicVariableError(
78
+ 'INVALID_QUERY',
79
+ 'Nested current-user and current-role fields are not supported in YunCMS V1',
80
+ path,
81
+ );
82
+ }
83
+
84
+ return value;
85
+ }
86
+
87
+ export function resolveDynamicVariables(value, context = {}, options = {}) {
88
+ const path = options.path ?? 'filter';
89
+ if (Array.isArray(value)) {
90
+ return value.map((entry, index) => resolveDynamicVariables(entry, context, {
91
+ ...options,
92
+ path: `${path}.${index}`,
93
+ }));
94
+ }
95
+ if (value && typeof value === 'object' && !(value instanceof Date)) {
96
+ return Object.fromEntries(Object.entries(value).map(([key, entry]) => [
97
+ key,
98
+ resolveDynamicVariables(entry, context, { ...options, path: `${path}.${key}` }),
99
+ ]));
100
+ }
101
+ return resolveDynamicVariable(value, context, options);
102
+ }
103
+
104
+ export const DYNAMIC_FILTER_VALUES = Object.freeze([
105
+ '$CURRENT_USER',
106
+ '$CURRENT_ROLE',
107
+ '$NOW',
108
+ '$NOW(-1 day)',
109
+ '$NOW(+1 day)',
110
+ ]);
package/src/index.js CHANGED
@@ -1,5 +1,11 @@
1
1
  export { DEFAULT_SERVER_PORT, loadConfig, loadEnvFileIfPresent } from './config.js';
2
2
  export { isCacheStore, MemoryCacheStore } from './cache.js';
3
+ export {
4
+ DYNAMIC_FILTER_VALUES,
5
+ resolveDynamicVariable,
6
+ resolveDynamicVariables,
7
+ } from './dynamic-variables.js';
8
+
3
9
  export {
4
10
  RedisClient,
5
11
  RedisCacheStore,
@@ -1,4 +1,5 @@
1
1
  import { compileFilter } from './query.js';
2
+ import { resolveDynamicVariables } from './dynamic-variables.js';
2
3
 
3
4
  function validationError(message, path = null) {
4
5
  const error = new Error(message);
@@ -8,13 +9,19 @@ function validationError(message, path = null) {
8
9
  }
9
10
 
10
11
  function compare(operator, actual, expected) {
12
+ const temporalExpected = expected instanceof Date ? expected.getTime() : null;
13
+ const temporalActual = temporalExpected == null
14
+ ? actual
15
+ : actual instanceof Date ? actual.getTime() : new Date(actual).getTime();
16
+ const comparableActual = temporalExpected == null ? actual : temporalActual;
17
+ const comparableExpected = temporalExpected == null ? expected : temporalExpected;
11
18
  switch (operator) {
12
- case '_eq': return actual === expected;
13
- case '_neq': return actual !== expected;
14
- case '_lt': return actual != null && actual < expected;
15
- case '_lte': return actual != null && actual <= expected;
16
- case '_gt': return actual != null && actual > expected;
17
- case '_gte': return actual != null && actual >= expected;
19
+ case '_eq': return comparableActual === comparableExpected;
20
+ case '_neq': return comparableActual !== comparableExpected;
21
+ case '_lt': return comparableActual != null && comparableActual < comparableExpected;
22
+ case '_lte': return comparableActual != null && comparableActual <= comparableExpected;
23
+ case '_gt': return comparableActual != null && comparableActual > comparableExpected;
24
+ case '_gte': return comparableActual != null && comparableActual >= comparableExpected;
18
25
  case '_in': return expected.some((candidate) => actual === candidate);
19
26
  case '_nin': return !expected.some((candidate) => actual === candidate);
20
27
  case '_null': return expected ? actual == null : actual != null;
@@ -47,17 +54,18 @@ function evaluateNode(record, node) {
47
54
 
48
55
  export function assertPermissionValidationRule(rule, schema) {
49
56
  if (rule == null) return null;
50
- compileFilter(rule, schema);
57
+ compileFilter(rule, schema, { allowUnresolvedDynamicVariables: true });
51
58
  return rule;
52
59
  }
53
60
 
54
- export function evaluatePermissionValidation(record, rule, schema) {
61
+ export function evaluatePermissionValidation(record, rule, schema, { dynamicVariables = {} } = {}) {
55
62
  if (rule == null) return true;
56
63
  assertPermissionValidationRule(rule, schema);
57
- return evaluateNode(record, rule);
64
+ const resolvedRule = resolveDynamicVariables(rule, dynamicVariables, { path: 'validation' });
65
+ return evaluateNode(record, resolvedRule);
58
66
  }
59
67
 
60
- export function enforcePermissionValidation(record, rule, schema, { path = 'validation' } = {}) {
61
- if (evaluatePermissionValidation(record, rule, schema)) return record;
68
+ export function enforcePermissionValidation(record, rule, schema, { path = 'validation', dynamicVariables = {} } = {}) {
69
+ if (evaluatePermissionValidation(record, rule, schema, { dynamicVariables })) return record;
62
70
  throw validationError('Record does not satisfy the permission validation rule', path);
63
71
  }
package/src/query.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { quoteIdentifier } from './identifier.js';
2
+ import { resolveDynamicVariables } from './dynamic-variables.js';
2
3
 
3
4
  const QUERY_KEYS = new Set(['fields', 'filter', 'sort', 'limit', 'offset', 'search', 'aggregate', 'groupBy']);
4
5
  const FILTER_OPERATORS = new Set([
@@ -289,7 +290,16 @@ function compileFilterObject(filter, schema, path, limits, state, depth) {
289
290
 
290
291
  export function compileFilter(filter, schema, options = {}) {
291
292
  if (!filter) return { sql: '', params: [] };
292
- const limits = { ...QUERY_LIMITS, ...options };
293
- const compiled = compileFilterObject(filter, schema, 'filter', limits, { nodes: 0 }, 1);
293
+ const {
294
+ dynamicVariables = {},
295
+ allowUnresolvedDynamicVariables = false,
296
+ ...limitOptions
297
+ } = options;
298
+ const limits = { ...QUERY_LIMITS, ...limitOptions };
299
+ const resolvedFilter = resolveDynamicVariables(filter, dynamicVariables, {
300
+ allowUnresolved: allowUnresolvedDynamicVariables,
301
+ path: 'filter',
302
+ });
303
+ const compiled = compileFilterObject(resolvedFilter, schema, 'filter', limits, { nodes: 0 }, 1);
294
304
  return { sql: ` WHERE ${compiled.sql}`, params: compiled.params };
295
305
  }
@@ -419,7 +419,13 @@ async function readRowsByLookup({
419
419
  },
420
420
  };
421
421
  const selection = compileSelectFields([...new Set([...visibleFields, ...trustedInternal])], internalSchema);
422
- const permissionFilter = compileFilter(permission.filter, schema);
422
+ const permissionFilter = compileFilter(permission.filter, schema, {
423
+ dynamicVariables: {
424
+ user: service.accountability?.user ?? null,
425
+ role: service.accountability?.role ?? null,
426
+ now: new Date(),
427
+ },
428
+ });
423
429
  const table = quoteIdentifier(service.collection, 'collection name');
424
430
  const data = [];
425
431
 
@@ -38,6 +38,7 @@ function publicUser(user) {
38
38
  email: user.email,
39
39
  role: user.role ?? null,
40
40
  role_name: user.role_name ?? null,
41
+ admin: Boolean(user.role_admin ?? user.admin),
41
42
  status: user.status,
42
43
  email_verified_at: user.email_verified_at ?? null,
43
44
  };
@@ -191,6 +192,7 @@ export class AuthService extends BaseService {
191
192
  email: result.email,
192
193
  role: result.role,
193
194
  role_name: result.role_name ?? null,
195
+ admin: result.admin === true,
194
196
  },
195
197
  access_token: result.access_token,
196
198
  access_expires_at: result.access_expires_at,
@@ -59,6 +59,7 @@ function publicUser(user) {
59
59
  email: user.email,
60
60
  role: user.role ?? null,
61
61
  role_name: user.role_name ?? null,
62
+ admin: Boolean(user.role_admin ?? user.admin),
62
63
  status: user.status,
63
64
  email_verified_at: user.email_verified_at ?? null,
64
65
  };
@@ -101,7 +101,13 @@ export class FilesService extends BaseService {
101
101
  'System schema is required to enforce a filtered Files permission',
102
102
  );
103
103
  }
104
- const compiled = compileFilter(permission.filter, collectionSchema);
104
+ const compiled = compileFilter(permission.filter, collectionSchema, {
105
+ dynamicVariables: {
106
+ user: this.accountability.user,
107
+ role: this.accountability.role,
108
+ now: new Date(),
109
+ },
110
+ });
105
111
  sql = compiled.sql;
106
112
  params = [...compiled.params];
107
113
  }
@@ -65,7 +65,7 @@ function combineCompiledFilters(...filters) {
65
65
  };
66
66
  }
67
67
 
68
- function createCandidateRecord(schema, id, entries) {
68
+ function createCandidateRecord(schema, id, entries, now = new Date()) {
69
69
  const provided = Object.fromEntries(entries);
70
70
  const candidate = {};
71
71
 
@@ -80,7 +80,7 @@ function createCandidateRecord(schema, id, entries) {
80
80
  }
81
81
  const metadata = parseMetadata(fieldSchema.schema_metadata);
82
82
  if (metadata.defaultPreset === 'now') {
83
- candidate[fieldSchema.field] = new Date();
83
+ candidate[fieldSchema.field] = now;
84
84
  continue;
85
85
  }
86
86
  candidate[fieldSchema.field] = Object.hasOwn(metadata, 'defaultValue')
@@ -150,6 +150,14 @@ export class ItemsService extends BaseService {
150
150
  return permissions.resolve(action, this.collection);
151
151
  }
152
152
 
153
+ dynamicVariables(now = new Date()) {
154
+ return {
155
+ user: this.accountability.user,
156
+ role: this.accountability.role,
157
+ now,
158
+ };
159
+ }
160
+
153
161
  validatePayload(payload, schema, permission, { creating = false } = {}) {
154
162
  if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
155
163
  throw serviceError('INVALID_PAYLOAD', 'Payload must be an object');
@@ -179,9 +187,9 @@ export class ItemsService extends BaseService {
179
187
  return entries;
180
188
  }
181
189
 
182
- compileActionFilters(userFilter, permissionFilter, userSchema, fullSchema, search = null) {
183
- const permissionSql = compileFilter(permissionFilter, fullSchema);
184
- const userSql = compileFilter(userFilter, userSchema);
190
+ compileActionFilters(userFilter, permissionFilter, userSchema, fullSchema, search = null, dynamicVariables = this.dynamicVariables()) {
191
+ const permissionSql = compileFilter(permissionFilter, fullSchema, { dynamicVariables });
192
+ const userSql = compileFilter(userFilter, userSchema, { dynamicVariables });
185
193
  const searchSql = compileSearch(search, userSchema);
186
194
  return combineCompiledFilters(permissionSql, userSql, searchSql);
187
195
  }
@@ -221,7 +229,14 @@ export class ItemsService extends BaseService {
221
229
  const query = await this.normalizeReadQuery(rawQuery);
222
230
  const permission = await this.resolvePermission('read');
223
231
  const accessSchema = schemaForFields(schema, permission.fields);
224
- const filter = this.compileActionFilters(query.filter, permission.filter, accessSchema, schema, query.search);
232
+ const filter = this.compileActionFilters(
233
+ query.filter,
234
+ permission.filter,
235
+ accessSchema,
236
+ schema,
237
+ query.search,
238
+ this.dynamicVariables(),
239
+ );
225
240
  const table = quoteIdentifier(this.collection, 'collection name');
226
241
  const aggregate = compileAggregate(query.aggregate, query.groupBy, accessSchema);
227
242
 
@@ -288,7 +303,8 @@ export class ItemsService extends BaseService {
288
303
  fields: { ...accessSchema.fields, [trustedLookupField]: schema.fields[trustedLookupField] },
289
304
  };
290
305
  const internalSelection = compileSelectFields([...visibleSelection.fields, trustedLookupField], internalSchema);
291
- const permissionFilter = compileFilter(permission.filter, schema);
306
+ const dynamicVariables = this.dynamicVariables();
307
+ const permissionFilter = compileFilter(permission.filter, schema, { dynamicVariables });
292
308
  const table = quoteIdentifier(this.collection, 'collection name');
293
309
  const data = [];
294
310
 
@@ -315,8 +331,9 @@ export class ItemsService extends BaseService {
315
331
  const selected = compileSelectFields(normalizeFields(fields), accessSchema);
316
332
  const table = quoteIdentifier(this.collection, 'collection name');
317
333
  const primaryKey = schema.primary_key;
334
+ const dynamicVariables = this.dynamicVariables();
318
335
  const filter = combineCompiledFilters(
319
- compileFilter(permission.filter, schema),
336
+ compileFilter(permission.filter, schema, { dynamicVariables }),
320
337
  compileFilter({ [primaryKey]: { _eq: id } }, schema),
321
338
  );
322
339
  const [rows] = await this.database.query(
@@ -342,10 +359,13 @@ export class ItemsService extends BaseService {
342
359
  const permission = await this.resolvePermission('create');
343
360
  const filteredPayload = await this.filterMutation('items.create', payload, { operation: 'create' });
344
361
  const callerEntries = this.validatePayload(filteredPayload, schema, permission, { creating: true });
345
- const entries = mergeSystemEntries(callerEntries, schema, this.accountability, 'create');
362
+ const now = new Date();
363
+ const entries = mergeSystemEntries(callerEntries, schema, this.accountability, 'create', now);
346
364
  const id = randomUUID();
347
- const candidate = createCandidateRecord(schema, id, entries);
348
- enforcePermissionValidation(candidate, permission.validation, schema);
365
+ const candidate = createCandidateRecord(schema, id, entries, now);
366
+ enforcePermissionValidation(candidate, permission.validation, schema, {
367
+ dynamicVariables: this.dynamicVariables(now),
368
+ });
349
369
 
350
370
  const values = { [schema.primary_key]: id, ...Object.fromEntries(entries) };
351
371
  const fields = Object.keys(values);
@@ -368,14 +388,17 @@ export class ItemsService extends BaseService {
368
388
  const schema = await this.getCollectionSchema();
369
389
  const permission = await this.resolvePermission('create');
370
390
  const staged = [];
391
+ const now = new Date();
371
392
 
372
393
  for (const payload of payloads) {
373
394
  const filteredPayload = await this.filterMutation('items.create', payload, { operation: 'create', bulk: true });
374
395
  const callerEntries = this.validatePayload(filteredPayload, schema, permission, { creating: true });
375
- const entries = mergeSystemEntries(callerEntries, schema, this.accountability, 'create');
396
+ const entries = mergeSystemEntries(callerEntries, schema, this.accountability, 'create', now);
376
397
  const id = randomUUID();
377
- const candidate = createCandidateRecord(schema, id, entries);
378
- enforcePermissionValidation(candidate, permission.validation, schema);
398
+ const candidate = createCandidateRecord(schema, id, entries, now);
399
+ enforcePermissionValidation(candidate, permission.validation, schema, {
400
+ dynamicVariables: this.dynamicVariables(now),
401
+ });
379
402
  staged.push({ id, values: { [schema.primary_key]: id, ...Object.fromEntries(entries) } });
380
403
  }
381
404
 
@@ -405,11 +428,13 @@ export class ItemsService extends BaseService {
405
428
  const filteredPayload = await this.filterMutation('items.update', payload, { operation: 'update', key: id });
406
429
  const callerEntries = this.validatePayload(filteredPayload, schema, permission);
407
430
  if (callerEntries.length === 0) throw serviceError('INVALID_PAYLOAD', 'Update payload cannot be empty');
408
- const entries = mergeSystemEntries(callerEntries, schema, this.accountability, 'update');
431
+ const now = new Date();
432
+ const dynamicVariables = this.dynamicVariables(now);
433
+ const entries = mergeSystemEntries(callerEntries, schema, this.accountability, 'update', now);
409
434
  const effectiveChanges = Object.fromEntries(entries);
410
435
  const table = quoteIdentifier(this.collection, 'collection name');
411
436
  const filter = combineCompiledFilters(
412
- compileFilter(permission.filter, schema),
437
+ compileFilter(permission.filter, schema, { dynamicVariables }),
413
438
  compileFilter({ [schema.primary_key]: { _eq: id } }, schema),
414
439
  );
415
440
 
@@ -417,7 +442,7 @@ export class ItemsService extends BaseService {
417
442
  const [currentRows] = await this.database.query(`SELECT * FROM ${table}${filter.sql} LIMIT 1`, filter.params);
418
443
  const current = currentRows[0];
419
444
  if (!current) return null;
420
- enforcePermissionValidation({ ...current, ...effectiveChanges }, permission.validation, schema);
445
+ enforcePermissionValidation({ ...current, ...effectiveChanges }, permission.validation, schema, { dynamicVariables });
421
446
  }
422
447
 
423
448
  const setSql = entries.map(([field]) => `${quoteIdentifier(field, 'field name')} = ?`).join(', ');
@@ -442,9 +467,11 @@ export class ItemsService extends BaseService {
442
467
  const filteredPayload = await this.filterMutation('items.update', payload, { operation: 'update', bulk: true, filter: filterInput });
443
468
  const callerEntries = this.validatePayload(filteredPayload, schema, permission);
444
469
  if (callerEntries.length === 0) throw serviceError('INVALID_PAYLOAD', 'Update payload cannot be empty');
445
- const entries = mergeSystemEntries(callerEntries, schema, this.accountability, 'update');
470
+ const now = new Date();
471
+ const dynamicVariables = this.dynamicVariables(now);
472
+ const entries = mergeSystemEntries(callerEntries, schema, this.accountability, 'update', now);
446
473
  const effectiveChanges = Object.fromEntries(entries);
447
- const filter = this.compileActionFilters(filterInput, permission.filter, accessSchema, schema);
474
+ const filter = this.compileActionFilters(filterInput, permission.filter, accessSchema, schema, null, dynamicVariables);
448
475
  const table = quoteIdentifier(this.collection, 'collection name');
449
476
 
450
477
  if (permission.validation) {
@@ -455,7 +482,9 @@ export class ItemsService extends BaseService {
455
482
  if (rows.length > MAX_BULK_VALIDATION_ROWS) {
456
483
  throw serviceError('VALIDATION_BULK_LIMIT', `Permission validation can inspect at most ${MAX_BULK_VALIDATION_ROWS} rows per bulk update`);
457
484
  }
458
- for (const row of rows) enforcePermissionValidation({ ...row, ...effectiveChanges }, permission.validation, schema);
485
+ for (const row of rows) {
486
+ enforcePermissionValidation({ ...row, ...effectiveChanges }, permission.validation, schema, { dynamicVariables });
487
+ }
459
488
  }
460
489
 
461
490
  const setSql = entries.map(([field]) => `${quoteIdentifier(field, 'field name')} = ?`).join(', ');
@@ -473,8 +502,9 @@ export class ItemsService extends BaseService {
473
502
  const filtered = await this.filterMutation('items.delete', { key: id }, { operation: 'delete', key: id });
474
503
  const key = filtered?.key ?? id;
475
504
  const table = quoteIdentifier(this.collection, 'collection name');
505
+ const dynamicVariables = this.dynamicVariables();
476
506
  const filter = combineCompiledFilters(
477
- compileFilter(permission.filter, schema),
507
+ compileFilter(permission.filter, schema, { dynamicVariables }),
478
508
  compileFilter({ [schema.primary_key]: { _eq: key } }, schema),
479
509
  );
480
510
  const [result] = await this.database.query(`DELETE FROM ${table}${filter.sql}`, filter.params);
@@ -223,7 +223,7 @@ export class PermissionsService extends BaseService {
223
223
  const fields = normalizePermissionFields(input.fields ?? null, collectionSchema);
224
224
  const filter = input.filter ?? null;
225
225
  const validation = input.validation ?? null;
226
- if (filter !== null) compileFilter(filter, collectionSchema);
226
+ if (filter !== null) compileFilter(filter, collectionSchema, { allowUnresolvedDynamicVariables: true });
227
227
  assertPermissionValidationRule(validation, collectionSchema);
228
228
 
229
229
  const [roleRows] = await this.database.query(
@@ -290,7 +290,7 @@ export class PermissionsService extends BaseService {
290
290
  params.push(fields == null ? null : JSON.stringify(fields));
291
291
  }
292
292
  if (Object.hasOwn(patch, 'filter')) {
293
- if (patch.filter != null) compileFilter(patch.filter, collectionSchema);
293
+ if (patch.filter != null) compileFilter(patch.filter, collectionSchema, { allowUnresolvedDynamicVariables: true });
294
294
  assignments.push('filter = ?');
295
295
  params.push(patch.filter == null ? null : JSON.stringify(patch.filter));
296
296
  }