@limetech/lime-crm-building-blocks 1.105.1 → 1.105.2
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/CHANGELOG.md +7 -0
- package/dist/cjs/{lime-query-validation-82aa2855.js → lime-query-validation-6d419d03.js} +78 -16
- package/dist/cjs/limebb-lime-query-builder.cjs.entry.js +2 -2
- package/dist/cjs/limebb-lime-query-filter-group_3.cjs.entry.js +1 -1
- package/dist/cjs/limebb-lime-query-response-format-builder.cjs.entry.js +2 -2
- package/dist/cjs/limebb-property-selector.cjs.entry.js +1 -1
- package/dist/cjs/{property-resolution-fb42a46b.js → property-resolution-5f798b03.js} +47 -0
- package/dist/collection/components/lime-query-builder/lime-query-validation.js +78 -17
- package/dist/collection/components/lime-query-builder/property-resolution.js +46 -0
- package/dist/components/lime-query-validation.js +78 -16
- package/dist/components/property-selector.js +47 -1
- package/dist/esm/{lime-query-validation-9e386da8.js → lime-query-validation-237ee440.js} +78 -16
- package/dist/esm/limebb-lime-query-builder.entry.js +2 -2
- package/dist/esm/limebb-lime-query-filter-group_3.entry.js +1 -1
- package/dist/esm/limebb-lime-query-response-format-builder.entry.js +2 -2
- package/dist/esm/limebb-property-selector.entry.js +1 -1
- package/dist/esm/{property-resolution-c21a1369.js → property-resolution-e4e8dcf7.js} +47 -1
- package/dist/lime-crm-building-blocks/lime-crm-building-blocks.esm.js +1 -1
- package/dist/lime-crm-building-blocks/{p-ac9e81c9.entry.js → p-09ce8be4.entry.js} +1 -1
- package/dist/lime-crm-building-blocks/p-11aa4103.js +1 -0
- package/dist/lime-crm-building-blocks/{p-d8696b23.entry.js → p-9c2062bc.entry.js} +1 -1
- package/dist/lime-crm-building-blocks/p-b02c99d5.js +1 -0
- package/dist/lime-crm-building-blocks/{p-908dd7d5.entry.js → p-ee0e42dd.entry.js} +1 -1
- package/dist/lime-crm-building-blocks/{p-1421e1f8.entry.js → p-f7ea292d.entry.js} +1 -1
- package/dist/types/components/lime-query-builder/lime-query-validation.d.ts +14 -0
- package/dist/types/components/lime-query-builder/property-resolution.d.ts +12 -0
- package/package.json +2 -2
- package/dist/lime-crm-building-blocks/p-b748c770.js +0 -1
- package/dist/lime-crm-building-blocks/p-efa5bcd4.js +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
## [1.105.2](https://github.com/Lundalogik/lime-crm-building-blocks/compare/v1.105.1...v1.105.2) (2025-12-01)
|
|
2
|
+
|
|
3
|
+
### Bug Fixes
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
* **lime-query-builder:** validate filter expression keys to prevent backend errors ([c98bc02](https://github.com/Lundalogik/lime-crm-building-blocks/commit/c98bc028f6a6dccabf7fda9808c03e6a59b86256)), closes [Lundalogik/crm-insights-and-intelligence#128](https://github.com/Lundalogik/crm-insights-and-intelligence/issues/128)
|
|
7
|
+
|
|
1
8
|
## [1.105.1](https://github.com/Lundalogik/lime-crm-building-blocks/compare/v1.105.0...v1.105.1) (2025-11-27)
|
|
2
9
|
|
|
3
10
|
### Bug Fixes
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const index_esm = require('./index.esm-d785eb6e.js');
|
|
4
|
-
const propertyResolution = require('./property-resolution-
|
|
4
|
+
const propertyResolution = require('./property-resolution-5f798b03.js');
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Dynamic filter values and placeholders that are valid in Lime Query
|
|
@@ -87,6 +87,46 @@ function validatePlaceholder(value, activeLimetype, limetypes) {
|
|
|
87
87
|
};
|
|
88
88
|
}
|
|
89
89
|
}
|
|
90
|
+
/**
|
|
91
|
+
* Validates a filter expression key (property path).
|
|
92
|
+
* Supports both regular property paths and placeholders.
|
|
93
|
+
*
|
|
94
|
+
* @param key - Property path to validate (e.g., "name", "company.name", "%activeObject%.name")
|
|
95
|
+
* @param limetypes - All limetype definitions
|
|
96
|
+
* @param limetype - The limetype being filtered
|
|
97
|
+
* @param activeLimetype - Active limetype for placeholder resolution
|
|
98
|
+
* @returns Validation result with error message if invalid
|
|
99
|
+
*/
|
|
100
|
+
function validateFilterKey(key, limetypes, limetype, activeLimetype) {
|
|
101
|
+
// 1. Handle empty/missing keys
|
|
102
|
+
if (!key) {
|
|
103
|
+
return { valid: false, error: 'Filter key cannot be empty' };
|
|
104
|
+
}
|
|
105
|
+
// 2. Check if key is a placeholder
|
|
106
|
+
if (key.startsWith('%activeObject%')) {
|
|
107
|
+
const placeholderResult = validatePlaceholder(key, activeLimetype, limetypes);
|
|
108
|
+
if (!placeholderResult.valid) {
|
|
109
|
+
return placeholderResult;
|
|
110
|
+
}
|
|
111
|
+
// Extract property path after the placeholder and validate for hasMany/hasAndBelongsToMany
|
|
112
|
+
const propertyPath = key.replace(/^%activeObject%\.?/, '');
|
|
113
|
+
if (propertyPath && activeLimetype) {
|
|
114
|
+
const { error } = propertyResolution.validatePropertyPath(limetypes, activeLimetype, propertyPath);
|
|
115
|
+
if (error) {
|
|
116
|
+
return { valid: false, error };
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return placeholderResult;
|
|
120
|
+
}
|
|
121
|
+
// 3. Validate regular property path (including intermediate properties)
|
|
122
|
+
const { error } = propertyResolution.validatePropertyPath(limetypes, limetype, key);
|
|
123
|
+
if (error) {
|
|
124
|
+
return { valid: false, error };
|
|
125
|
+
}
|
|
126
|
+
// validatePropertyPath always returns an error if property is undefined,
|
|
127
|
+
// so if we reach here, the property exists and is valid
|
|
128
|
+
return { valid: true };
|
|
129
|
+
}
|
|
90
130
|
/**
|
|
91
131
|
* Validate a response format against limetype schemas
|
|
92
132
|
* Throws errors for invalid property references
|
|
@@ -255,13 +295,19 @@ function validatePropertySelection(selection, limetypes, limetype, visualModeEna
|
|
|
255
295
|
* @param filter
|
|
256
296
|
* @param activeLimetype
|
|
257
297
|
* @param limetypes
|
|
298
|
+
* @param limetype
|
|
258
299
|
*/
|
|
259
|
-
function validateComparisonExpression(filter, activeLimetype, limetypes) {
|
|
300
|
+
function validateComparisonExpression(filter, activeLimetype, limetypes, limetype) {
|
|
260
301
|
// Validate operator
|
|
261
302
|
const allValidOperators = Object.values(index_esm.Zt);
|
|
262
303
|
if (!allValidOperators.includes(filter.op)) {
|
|
263
304
|
throw new Error(`Unsupported filter operator: ${filter.op}`);
|
|
264
305
|
}
|
|
306
|
+
// Validate filter key
|
|
307
|
+
const keyResult = validateFilterKey(filter.key, limetypes, limetype, activeLimetype);
|
|
308
|
+
if (!keyResult.valid) {
|
|
309
|
+
throw new Error(`Invalid filter key '${filter.key}': ${keyResult.error}`);
|
|
310
|
+
}
|
|
265
311
|
// Validate placeholder
|
|
266
312
|
const result = validatePlaceholder(filter.exp, activeLimetype, limetypes);
|
|
267
313
|
if (!result.valid) {
|
|
@@ -273,9 +319,10 @@ function validateComparisonExpression(filter, activeLimetype, limetypes) {
|
|
|
273
319
|
* @param filter
|
|
274
320
|
* @param activeLimetype
|
|
275
321
|
* @param limetypes
|
|
322
|
+
* @param limetype
|
|
276
323
|
* @param visualModeEnabled
|
|
277
324
|
*/
|
|
278
|
-
function validateGroupExpression(filter, activeLimetype, limetypes, visualModeEnabled) {
|
|
325
|
+
function validateGroupExpression(filter, activeLimetype, limetypes, limetype, visualModeEnabled) {
|
|
279
326
|
// Validate operator
|
|
280
327
|
if (filter.op !== index_esm.Zt.AND &&
|
|
281
328
|
filter.op !== index_esm.Zt.OR &&
|
|
@@ -284,12 +331,12 @@ function validateGroupExpression(filter, activeLimetype, limetypes, visualModeEn
|
|
|
284
331
|
}
|
|
285
332
|
// Recursively validate children
|
|
286
333
|
if (filter.op === index_esm.Zt.NOT) {
|
|
287
|
-
validateFilterPlaceholders(filter.exp, activeLimetype, limetypes, visualModeEnabled);
|
|
334
|
+
validateFilterPlaceholders(filter.exp, activeLimetype, limetypes, limetype, visualModeEnabled);
|
|
288
335
|
}
|
|
289
336
|
else if (filter.op === index_esm.Zt.AND || filter.op === index_esm.Zt.OR) {
|
|
290
337
|
const expressions = filter.exp;
|
|
291
338
|
for (const expr of expressions) {
|
|
292
|
-
validateFilterPlaceholders(expr, activeLimetype, limetypes, visualModeEnabled);
|
|
339
|
+
validateFilterPlaceholders(expr, activeLimetype, limetypes, limetype, visualModeEnabled);
|
|
293
340
|
}
|
|
294
341
|
}
|
|
295
342
|
}
|
|
@@ -298,37 +345,51 @@ function validateGroupExpression(filter, activeLimetype, limetypes, visualModeEn
|
|
|
298
345
|
* @param filter Filter expression to validate
|
|
299
346
|
* @param activeLimetype The limetype of the active object
|
|
300
347
|
* @param limetypes Record of all available limetypes
|
|
348
|
+
* @param limetype The limetype being filtered
|
|
301
349
|
* @param visualModeEnabled Whether visual mode is enabled (affects validation)
|
|
302
350
|
*/
|
|
303
|
-
function validateFilterPlaceholders(filter, activeLimetype, limetypes, visualModeEnabled = true) {
|
|
351
|
+
function validateFilterPlaceholders(filter, activeLimetype, limetypes, limetype, visualModeEnabled = true) {
|
|
304
352
|
if (!filter) {
|
|
305
353
|
return;
|
|
306
354
|
}
|
|
307
355
|
if ('key' in filter) {
|
|
308
|
-
validateComparisonExpression(filter, activeLimetype, limetypes);
|
|
356
|
+
validateComparisonExpression(filter, activeLimetype, limetypes, limetype);
|
|
309
357
|
return;
|
|
310
358
|
}
|
|
311
359
|
if ('exp' in filter) {
|
|
312
|
-
validateGroupExpression(filter, activeLimetype, limetypes, visualModeEnabled);
|
|
360
|
+
validateGroupExpression(filter, activeLimetype, limetypes, limetype, visualModeEnabled);
|
|
313
361
|
}
|
|
314
362
|
}
|
|
315
363
|
/**
|
|
316
|
-
* Validate Lime Query filter and collect errors
|
|
364
|
+
* Validate Lime Query filter and collect errors and visual mode limitations
|
|
317
365
|
* @param filter The filter expression or group to validate
|
|
318
366
|
* @param activeLimetype Optional active object limetype for placeholder validation
|
|
319
367
|
* @param limetypes Record of all available limetypes
|
|
368
|
+
* @param limetype The limetype being filtered
|
|
320
369
|
* @param visualModeEnabled Whether visual mode is enabled
|
|
321
|
-
* @returns
|
|
370
|
+
* @returns Object with validation errors and visual mode limitations
|
|
322
371
|
*/
|
|
323
|
-
function validateLimeQueryFilterInternal(filter, activeLimetype, limetypes, visualModeEnabled) {
|
|
372
|
+
function validateLimeQueryFilterInternal(filter, activeLimetype, limetypes, limetype, visualModeEnabled) {
|
|
324
373
|
const errors = [];
|
|
374
|
+
const limitations = [];
|
|
325
375
|
try {
|
|
326
|
-
validateFilterPlaceholders(filter, activeLimetype, limetypes, visualModeEnabled);
|
|
376
|
+
validateFilterPlaceholders(filter, activeLimetype, limetypes, limetype, visualModeEnabled);
|
|
327
377
|
}
|
|
328
378
|
catch (error) {
|
|
329
|
-
|
|
379
|
+
const errorMessage = error.message;
|
|
380
|
+
// Invalid keys are BOTH spec violations AND rendering limitations:
|
|
381
|
+
// - Backend will reject them (validation error)
|
|
382
|
+
// - Visual mode can't show them in property selector (visual limitation)
|
|
383
|
+
if (errorMessage.includes('Invalid filter key') ||
|
|
384
|
+
errorMessage.includes('Cannot filter on many-relation')) {
|
|
385
|
+
errors.push(`Invalid filter: ${errorMessage}`);
|
|
386
|
+
limitations.push(errorMessage);
|
|
387
|
+
}
|
|
388
|
+
else {
|
|
389
|
+
errors.push(`Invalid filter: ${errorMessage}`);
|
|
390
|
+
}
|
|
330
391
|
}
|
|
331
|
-
return errors;
|
|
392
|
+
return { errors, limitations };
|
|
332
393
|
}
|
|
333
394
|
/**
|
|
334
395
|
* Validate orderBy specification
|
|
@@ -497,8 +558,9 @@ function isLimeQuerySupported(limeQuery, limetypes, activeLimetype, visualModeEn
|
|
|
497
558
|
}
|
|
498
559
|
// Validate filter
|
|
499
560
|
if (limeQuery.filter) {
|
|
500
|
-
const
|
|
501
|
-
validationErrors.push(...
|
|
561
|
+
const { errors, limitations } = validateLimeQueryFilterInternal(limeQuery.filter, activeLimetype, limetypes, limeQuery.limetype, visualModeEnabled);
|
|
562
|
+
validationErrors.push(...errors);
|
|
563
|
+
visualModeLimitations.push(...limitations);
|
|
502
564
|
}
|
|
503
565
|
// Validate responseFormat
|
|
504
566
|
if (limeQuery.responseFormat) {
|
|
@@ -4,8 +4,8 @@ Object.defineProperty(exports, '__esModule', { value: true });
|
|
|
4
4
|
|
|
5
5
|
const index = require('./index-ff255a0d.js');
|
|
6
6
|
const index_esm = require('./index.esm-d785eb6e.js');
|
|
7
|
-
const limeQueryValidation = require('./lime-query-validation-
|
|
8
|
-
require('./property-resolution-
|
|
7
|
+
const limeQueryValidation = require('./lime-query-validation-6d419d03.js');
|
|
8
|
+
require('./property-resolution-5f798b03.js');
|
|
9
9
|
|
|
10
10
|
const limeQueryBuilderCss = "*,*:before,*:after{box-sizing:border-box}:host(limebb-lime-query-builder){--header-background-color:rgb(var(--contrast-500));--limebb-lime-query-builder-background-color:rgb(var(--contrast-100));--limebb-lime-query-builder-border-radius:0.75rem;--limebb-lime-query-builder-visual-mode-padding:1rem;--limebb-lime-query-builder-group-color:rgb(var(--color-sky-lighter));box-sizing:border-box;width:calc(100% - 1.5rem);margin:0.75rem auto;display:flex;flex-direction:column;border-radius:var(--limebb-lime-query-builder-border-radius);background-color:var(--limebb-lime-query-builder-background-color);box-shadow:var(--shadow-inflated-16)}.visual-mode{display:flex;flex-direction:column;gap:1rem;padding:var(--limebb-lime-query-builder-visual-mode-padding);border:1px solid var(--header-background-color);border-radius:0 0 var(--limebb-lime-query-builder-border-radius) var(--limebb-lime-query-builder-border-radius)}.code-mode{--code-editor-max-height:70vh;display:flex;flex-direction:column;gap:1rem}.code-mode .validation-errors{padding:0.75rem 1rem;color:rgb(var(--color-red-default));background-color:rgb(var(--color-red-lighter));border-left:0.25rem solid rgb(var(--color-red-default));border-radius:0.25rem;font-size:0.875rem}.code-mode .validation-errors strong{display:block;margin-bottom:0.5rem;font-weight:600}.code-mode .validation-errors ul{margin:0;padding-left:1.5rem}.code-mode .validation-errors li{margin:0.25rem 0}.code-mode .visual-mode-limitations{padding:0.75rem 1rem;color:rgb(var(--color-blue-dark));background-color:rgb(var(--color-blue-lighter));border-left:0.25rem solid rgb(var(--color-blue-default));border-radius:0.25rem;font-size:0.875rem}.code-mode .visual-mode-limitations strong{display:block;margin-bottom:0.5rem;font-weight:600}.code-mode .visual-mode-limitations ul{margin:0;padding-left:1.5rem}.code-mode .visual-mode-limitations li{margin:0.25rem 0}section.description,section.filter,section.query-options{display:flex;flex-direction:column;gap:1rem}section h4{margin:0;font-size:1.125rem;font-weight:600;color:rgb(var(--contrast-1000))}limel-header.is-narrow{--header-top-right-left-border-radius:0;width:calc(100% + var(--limebb-lime-query-builder-visual-mode-padding) * 2);margin-left:calc(var(--limebb-lime-query-builder-visual-mode-padding) * -1)}.query-options-controls{display:flex;flex-direction:column;gap:1rem}";
|
|
11
11
|
const LimebbLimeQueryBuilderStyle0 = limeQueryBuilderCss;
|
|
@@ -4,7 +4,7 @@ Object.defineProperty(exports, '__esModule', { value: true });
|
|
|
4
4
|
|
|
5
5
|
const index = require('./index-ff255a0d.js');
|
|
6
6
|
const index_esm = require('./index.esm-d785eb6e.js');
|
|
7
|
-
const propertyResolution = require('./property-resolution-
|
|
7
|
+
const propertyResolution = require('./property-resolution-5f798b03.js');
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* Get the subheading text for a filter group based on its operator and expression count
|
|
@@ -4,8 +4,8 @@ Object.defineProperty(exports, '__esModule', { value: true });
|
|
|
4
4
|
|
|
5
5
|
const index = require('./index-ff255a0d.js');
|
|
6
6
|
const index_esm = require('./index.esm-d785eb6e.js');
|
|
7
|
-
const limeQueryValidation = require('./lime-query-validation-
|
|
8
|
-
require('./property-resolution-
|
|
7
|
+
const limeQueryValidation = require('./lime-query-validation-6d419d03.js');
|
|
8
|
+
require('./property-resolution-5f798b03.js');
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* Helper functions for working with ResponseFormat objects
|
|
@@ -4,7 +4,7 @@ Object.defineProperty(exports, '__esModule', { value: true });
|
|
|
4
4
|
|
|
5
5
|
const index = require('./index-ff255a0d.js');
|
|
6
6
|
const index_esm = require('./index.esm-d785eb6e.js');
|
|
7
|
-
const propertyResolution = require('./property-resolution-
|
|
7
|
+
const propertyResolution = require('./property-resolution-5f798b03.js');
|
|
8
8
|
const limetype = require('./limetype-f2e4376e.js');
|
|
9
9
|
|
|
10
10
|
const propertySelectorCss = ":host(limebb-property-selector){display:block}limel-menu{display:block;width:100%}";
|
|
@@ -61,6 +61,53 @@ function getPropertyFromPath(limetypes, limetype, path) {
|
|
|
61
61
|
}
|
|
62
62
|
return property;
|
|
63
63
|
}
|
|
64
|
+
/**
|
|
65
|
+
* Validates a property path to ensure no intermediate properties are hasMany or hasAndBelongsToMany relations.
|
|
66
|
+
* These relation types cannot be traversed in filter expressions.
|
|
67
|
+
* @param limetypes All limetype definitions
|
|
68
|
+
* @param limetype The starting limetype
|
|
69
|
+
* @param path The property path to validate (e.g., "company.name")
|
|
70
|
+
* @returns The property at the end of the path if valid, or undefined with error if invalid
|
|
71
|
+
*/
|
|
72
|
+
function validatePropertyPath(limetypes, limetype, path) {
|
|
73
|
+
if (!path || !limetype || !limetypes) {
|
|
74
|
+
return { property: undefined };
|
|
75
|
+
}
|
|
76
|
+
const parts = path.split('.');
|
|
77
|
+
let currentType = limetypes[limetype];
|
|
78
|
+
let property;
|
|
79
|
+
for (let i = 0; i < parts.length; i++) {
|
|
80
|
+
const part = parts[i];
|
|
81
|
+
if (!currentType) {
|
|
82
|
+
return { property: undefined };
|
|
83
|
+
}
|
|
84
|
+
const normalizedProperties = getNormalizedProperties(currentType);
|
|
85
|
+
property = normalizedProperties[part];
|
|
86
|
+
if (!property) {
|
|
87
|
+
return {
|
|
88
|
+
property: undefined,
|
|
89
|
+
error: `Property '${part}' does not exist on limetype '${currentType.name}'`,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
// Check if this property is a hasMany/hasAndBelongsToMany relation
|
|
93
|
+
// These cannot be traversed in filter expressions
|
|
94
|
+
if (property.type === 'hasmany' ||
|
|
95
|
+
property.type === 'hasandbelongstomany') {
|
|
96
|
+
// Build the path up to this point for the error message
|
|
97
|
+
const invalidPath = parts.slice(0, i + 1).join('.');
|
|
98
|
+
return {
|
|
99
|
+
property: undefined,
|
|
100
|
+
error: `Cannot filter on many-relation '${invalidPath}'. Use a related limetype's filter instead.`,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
// If this is a relation, get the related limetype for next iteration
|
|
104
|
+
if (property.relation) {
|
|
105
|
+
currentType = property.relation.getLimetype();
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return { property };
|
|
109
|
+
}
|
|
64
110
|
|
|
65
111
|
exports.getNormalizedProperties = getNormalizedProperties;
|
|
66
112
|
exports.getPropertyFromPath = getPropertyFromPath;
|
|
113
|
+
exports.validatePropertyPath = validatePropertyPath;
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { Operator } from "@limetech/lime-web-components";
|
|
2
|
-
import { getNormalizedProperties } from "./property-resolution";
|
|
3
|
-
import { getPropertyFromPath } from "./property-resolution";
|
|
2
|
+
import { getNormalizedProperties, getPropertyFromPath, validatePropertyPath, } from "./property-resolution";
|
|
4
3
|
/**
|
|
5
4
|
* Dynamic filter values and placeholders that are valid in Lime Query
|
|
6
5
|
*/
|
|
@@ -85,6 +84,46 @@ export function validatePlaceholder(value, activeLimetype, limetypes) {
|
|
|
85
84
|
};
|
|
86
85
|
}
|
|
87
86
|
}
|
|
87
|
+
/**
|
|
88
|
+
* Validates a filter expression key (property path).
|
|
89
|
+
* Supports both regular property paths and placeholders.
|
|
90
|
+
*
|
|
91
|
+
* @param key - Property path to validate (e.g., "name", "company.name", "%activeObject%.name")
|
|
92
|
+
* @param limetypes - All limetype definitions
|
|
93
|
+
* @param limetype - The limetype being filtered
|
|
94
|
+
* @param activeLimetype - Active limetype for placeholder resolution
|
|
95
|
+
* @returns Validation result with error message if invalid
|
|
96
|
+
*/
|
|
97
|
+
export function validateFilterKey(key, limetypes, limetype, activeLimetype) {
|
|
98
|
+
// 1. Handle empty/missing keys
|
|
99
|
+
if (!key) {
|
|
100
|
+
return { valid: false, error: 'Filter key cannot be empty' };
|
|
101
|
+
}
|
|
102
|
+
// 2. Check if key is a placeholder
|
|
103
|
+
if (key.startsWith('%activeObject%')) {
|
|
104
|
+
const placeholderResult = validatePlaceholder(key, activeLimetype, limetypes);
|
|
105
|
+
if (!placeholderResult.valid) {
|
|
106
|
+
return placeholderResult;
|
|
107
|
+
}
|
|
108
|
+
// Extract property path after the placeholder and validate for hasMany/hasAndBelongsToMany
|
|
109
|
+
const propertyPath = key.replace(/^%activeObject%\.?/, '');
|
|
110
|
+
if (propertyPath && activeLimetype) {
|
|
111
|
+
const { error } = validatePropertyPath(limetypes, activeLimetype, propertyPath);
|
|
112
|
+
if (error) {
|
|
113
|
+
return { valid: false, error };
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return placeholderResult;
|
|
117
|
+
}
|
|
118
|
+
// 3. Validate regular property path (including intermediate properties)
|
|
119
|
+
const { error } = validatePropertyPath(limetypes, limetype, key);
|
|
120
|
+
if (error) {
|
|
121
|
+
return { valid: false, error };
|
|
122
|
+
}
|
|
123
|
+
// validatePropertyPath always returns an error if property is undefined,
|
|
124
|
+
// so if we reach here, the property exists and is valid
|
|
125
|
+
return { valid: true };
|
|
126
|
+
}
|
|
88
127
|
/**
|
|
89
128
|
* Validate a response format against limetype schemas
|
|
90
129
|
* Throws errors for invalid property references
|
|
@@ -253,13 +292,19 @@ export function validatePropertySelection(selection, limetypes, limetype, visual
|
|
|
253
292
|
* @param filter
|
|
254
293
|
* @param activeLimetype
|
|
255
294
|
* @param limetypes
|
|
295
|
+
* @param limetype
|
|
256
296
|
*/
|
|
257
|
-
function validateComparisonExpression(filter, activeLimetype, limetypes) {
|
|
297
|
+
function validateComparisonExpression(filter, activeLimetype, limetypes, limetype) {
|
|
258
298
|
// Validate operator
|
|
259
299
|
const allValidOperators = Object.values(Operator);
|
|
260
300
|
if (!allValidOperators.includes(filter.op)) {
|
|
261
301
|
throw new Error(`Unsupported filter operator: ${filter.op}`);
|
|
262
302
|
}
|
|
303
|
+
// Validate filter key
|
|
304
|
+
const keyResult = validateFilterKey(filter.key, limetypes, limetype, activeLimetype);
|
|
305
|
+
if (!keyResult.valid) {
|
|
306
|
+
throw new Error(`Invalid filter key '${filter.key}': ${keyResult.error}`);
|
|
307
|
+
}
|
|
263
308
|
// Validate placeholder
|
|
264
309
|
const result = validatePlaceholder(filter.exp, activeLimetype, limetypes);
|
|
265
310
|
if (!result.valid) {
|
|
@@ -271,9 +316,10 @@ function validateComparisonExpression(filter, activeLimetype, limetypes) {
|
|
|
271
316
|
* @param filter
|
|
272
317
|
* @param activeLimetype
|
|
273
318
|
* @param limetypes
|
|
319
|
+
* @param limetype
|
|
274
320
|
* @param visualModeEnabled
|
|
275
321
|
*/
|
|
276
|
-
function validateGroupExpression(filter, activeLimetype, limetypes, visualModeEnabled) {
|
|
322
|
+
function validateGroupExpression(filter, activeLimetype, limetypes, limetype, visualModeEnabled) {
|
|
277
323
|
// Validate operator
|
|
278
324
|
if (filter.op !== Operator.AND &&
|
|
279
325
|
filter.op !== Operator.OR &&
|
|
@@ -282,12 +328,12 @@ function validateGroupExpression(filter, activeLimetype, limetypes, visualModeEn
|
|
|
282
328
|
}
|
|
283
329
|
// Recursively validate children
|
|
284
330
|
if (filter.op === Operator.NOT) {
|
|
285
|
-
validateFilterPlaceholders(filter.exp, activeLimetype, limetypes, visualModeEnabled);
|
|
331
|
+
validateFilterPlaceholders(filter.exp, activeLimetype, limetypes, limetype, visualModeEnabled);
|
|
286
332
|
}
|
|
287
333
|
else if (filter.op === Operator.AND || filter.op === Operator.OR) {
|
|
288
334
|
const expressions = filter.exp;
|
|
289
335
|
for (const expr of expressions) {
|
|
290
|
-
validateFilterPlaceholders(expr, activeLimetype, limetypes, visualModeEnabled);
|
|
336
|
+
validateFilterPlaceholders(expr, activeLimetype, limetypes, limetype, visualModeEnabled);
|
|
291
337
|
}
|
|
292
338
|
}
|
|
293
339
|
}
|
|
@@ -296,37 +342,51 @@ function validateGroupExpression(filter, activeLimetype, limetypes, visualModeEn
|
|
|
296
342
|
* @param filter Filter expression to validate
|
|
297
343
|
* @param activeLimetype The limetype of the active object
|
|
298
344
|
* @param limetypes Record of all available limetypes
|
|
345
|
+
* @param limetype The limetype being filtered
|
|
299
346
|
* @param visualModeEnabled Whether visual mode is enabled (affects validation)
|
|
300
347
|
*/
|
|
301
|
-
function validateFilterPlaceholders(filter, activeLimetype, limetypes, visualModeEnabled = true) {
|
|
348
|
+
function validateFilterPlaceholders(filter, activeLimetype, limetypes, limetype, visualModeEnabled = true) {
|
|
302
349
|
if (!filter) {
|
|
303
350
|
return;
|
|
304
351
|
}
|
|
305
352
|
if ('key' in filter) {
|
|
306
|
-
validateComparisonExpression(filter, activeLimetype, limetypes);
|
|
353
|
+
validateComparisonExpression(filter, activeLimetype, limetypes, limetype);
|
|
307
354
|
return;
|
|
308
355
|
}
|
|
309
356
|
if ('exp' in filter) {
|
|
310
|
-
validateGroupExpression(filter, activeLimetype, limetypes, visualModeEnabled);
|
|
357
|
+
validateGroupExpression(filter, activeLimetype, limetypes, limetype, visualModeEnabled);
|
|
311
358
|
}
|
|
312
359
|
}
|
|
313
360
|
/**
|
|
314
|
-
* Validate Lime Query filter and collect errors
|
|
361
|
+
* Validate Lime Query filter and collect errors and visual mode limitations
|
|
315
362
|
* @param filter The filter expression or group to validate
|
|
316
363
|
* @param activeLimetype Optional active object limetype for placeholder validation
|
|
317
364
|
* @param limetypes Record of all available limetypes
|
|
365
|
+
* @param limetype The limetype being filtered
|
|
318
366
|
* @param visualModeEnabled Whether visual mode is enabled
|
|
319
|
-
* @returns
|
|
367
|
+
* @returns Object with validation errors and visual mode limitations
|
|
320
368
|
*/
|
|
321
|
-
function validateLimeQueryFilterInternal(filter, activeLimetype, limetypes, visualModeEnabled) {
|
|
369
|
+
function validateLimeQueryFilterInternal(filter, activeLimetype, limetypes, limetype, visualModeEnabled) {
|
|
322
370
|
const errors = [];
|
|
371
|
+
const limitations = [];
|
|
323
372
|
try {
|
|
324
|
-
validateFilterPlaceholders(filter, activeLimetype, limetypes, visualModeEnabled);
|
|
373
|
+
validateFilterPlaceholders(filter, activeLimetype, limetypes, limetype, visualModeEnabled);
|
|
325
374
|
}
|
|
326
375
|
catch (error) {
|
|
327
|
-
|
|
376
|
+
const errorMessage = error.message;
|
|
377
|
+
// Invalid keys are BOTH spec violations AND rendering limitations:
|
|
378
|
+
// - Backend will reject them (validation error)
|
|
379
|
+
// - Visual mode can't show them in property selector (visual limitation)
|
|
380
|
+
if (errorMessage.includes('Invalid filter key') ||
|
|
381
|
+
errorMessage.includes('Cannot filter on many-relation')) {
|
|
382
|
+
errors.push(`Invalid filter: ${errorMessage}`);
|
|
383
|
+
limitations.push(errorMessage);
|
|
384
|
+
}
|
|
385
|
+
else {
|
|
386
|
+
errors.push(`Invalid filter: ${errorMessage}`);
|
|
387
|
+
}
|
|
328
388
|
}
|
|
329
|
-
return errors;
|
|
389
|
+
return { errors, limitations };
|
|
330
390
|
}
|
|
331
391
|
/**
|
|
332
392
|
* Validate orderBy specification
|
|
@@ -495,8 +555,9 @@ export function isLimeQuerySupported(limeQuery, limetypes, activeLimetype, visua
|
|
|
495
555
|
}
|
|
496
556
|
// Validate filter
|
|
497
557
|
if (limeQuery.filter) {
|
|
498
|
-
const
|
|
499
|
-
validationErrors.push(...
|
|
558
|
+
const { errors, limitations } = validateLimeQueryFilterInternal(limeQuery.filter, activeLimetype, limetypes, limeQuery.limetype, visualModeEnabled);
|
|
559
|
+
validationErrors.push(...errors);
|
|
560
|
+
visualModeLimitations.push(...limitations);
|
|
500
561
|
}
|
|
501
562
|
// Validate responseFormat
|
|
502
563
|
if (limeQuery.responseFormat) {
|
|
@@ -59,3 +59,49 @@ export function getPropertyFromPath(limetypes, limetype, path) {
|
|
|
59
59
|
}
|
|
60
60
|
return property;
|
|
61
61
|
}
|
|
62
|
+
/**
|
|
63
|
+
* Validates a property path to ensure no intermediate properties are hasMany or hasAndBelongsToMany relations.
|
|
64
|
+
* These relation types cannot be traversed in filter expressions.
|
|
65
|
+
* @param limetypes All limetype definitions
|
|
66
|
+
* @param limetype The starting limetype
|
|
67
|
+
* @param path The property path to validate (e.g., "company.name")
|
|
68
|
+
* @returns The property at the end of the path if valid, or undefined with error if invalid
|
|
69
|
+
*/
|
|
70
|
+
export function validatePropertyPath(limetypes, limetype, path) {
|
|
71
|
+
if (!path || !limetype || !limetypes) {
|
|
72
|
+
return { property: undefined };
|
|
73
|
+
}
|
|
74
|
+
const parts = path.split('.');
|
|
75
|
+
let currentType = limetypes[limetype];
|
|
76
|
+
let property;
|
|
77
|
+
for (let i = 0; i < parts.length; i++) {
|
|
78
|
+
const part = parts[i];
|
|
79
|
+
if (!currentType) {
|
|
80
|
+
return { property: undefined };
|
|
81
|
+
}
|
|
82
|
+
const normalizedProperties = getNormalizedProperties(currentType);
|
|
83
|
+
property = normalizedProperties[part];
|
|
84
|
+
if (!property) {
|
|
85
|
+
return {
|
|
86
|
+
property: undefined,
|
|
87
|
+
error: `Property '${part}' does not exist on limetype '${currentType.name}'`,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
// Check if this property is a hasMany/hasAndBelongsToMany relation
|
|
91
|
+
// These cannot be traversed in filter expressions
|
|
92
|
+
if (property.type === 'hasmany' ||
|
|
93
|
+
property.type === 'hasandbelongstomany') {
|
|
94
|
+
// Build the path up to this point for the error message
|
|
95
|
+
const invalidPath = parts.slice(0, i + 1).join('.');
|
|
96
|
+
return {
|
|
97
|
+
property: undefined,
|
|
98
|
+
error: `Cannot filter on many-relation '${invalidPath}'. Use a related limetype's filter instead.`,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
// If this is a relation, get the related limetype for next iteration
|
|
102
|
+
if (property.relation) {
|
|
103
|
+
currentType = property.relation.getLimetype();
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return { property };
|
|
107
|
+
}
|