@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 +1 -1
- package/src/dynamic-variables.js +110 -0
- package/src/index.js +6 -0
- package/src/permission-validation.js +19 -11
- package/src/query.js +12 -2
- package/src/relation-expansion.js +7 -1
- package/src/services/auth-service.js +2 -0
- package/src/services/external-auth-service.js +1 -0
- package/src/services/files-service.js +7 -1
- package/src/services/items-service.js +51 -21
- package/src/services/permissions-service.js +2 -2
package/package.json
CHANGED
|
@@ -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
|
|
13
|
-
case '_neq': return
|
|
14
|
-
case '_lt': return
|
|
15
|
-
case '_lte': return
|
|
16
|
-
case '_gt': return
|
|
17
|
-
case '_gte': return
|
|
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
|
-
|
|
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
|
|
293
|
-
|
|
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,
|
|
@@ -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] =
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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)
|
|
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
|
}
|