@dataloop-ai/components 0.17.63 → 0.17.64
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 +3 -1
- package/src/components/compound/DlSearches/DlSmartSearch/DlSmartSearch.vue +62 -11
- package/src/components/compound/DlSearches/DlSmartSearch/utils/highlightSyntax.ts +1 -1
- package/src/components/compound/DlSearches/DlSmartSearch/utils/utils.ts +13 -6
- package/src/demos/SmartSearchDemo/DlSmartSearchDemo.vue +57 -6
- package/src/hooks/use-suggestions.ts +116 -17
- package/src/utils/parse-smart-query.ts +112 -111
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dataloop-ai/components",
|
|
3
|
-
"version": "0.17.
|
|
3
|
+
"version": "0.17.64",
|
|
4
4
|
"exports": {
|
|
5
5
|
".": "./index.ts",
|
|
6
6
|
"./models": "./models.ts",
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
"@dataloop-ai/icons": "^3.0.6",
|
|
26
26
|
"@types/lodash": "^4.14.184",
|
|
27
27
|
"chart.js": "^3.9.1",
|
|
28
|
+
"flat": "^5.0.2",
|
|
28
29
|
"lodash": "^4.17.21",
|
|
29
30
|
"moment": "^2.29.4",
|
|
30
31
|
"sass": "^1.51.0",
|
|
@@ -45,6 +46,7 @@
|
|
|
45
46
|
"@storybook/client-api": "^7.0.4",
|
|
46
47
|
"@storybook/vue3": "^7.0.4",
|
|
47
48
|
"@storybook/vue3-vite": "^7.0.4",
|
|
49
|
+
"@types/flat": "^5.0.2",
|
|
48
50
|
"@types/jsdom": "^16.2.14",
|
|
49
51
|
"@types/node": "^18.7.18",
|
|
50
52
|
"@types/resize-observer-browser": "^0.1.7",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
:default-width="width"
|
|
23
23
|
@save="saveQueryDialogBoxModel = true"
|
|
24
24
|
@focus="setFocused"
|
|
25
|
-
@update:modelValue="
|
|
25
|
+
@update:modelValue="debouncedInputModel"
|
|
26
26
|
@dql-edit="jsonEditorModel = !jsonEditorModel"
|
|
27
27
|
/>
|
|
28
28
|
</div>
|
|
@@ -183,7 +183,15 @@
|
|
|
183
183
|
</div>
|
|
184
184
|
</template>
|
|
185
185
|
<script lang="ts">
|
|
186
|
-
import {
|
|
186
|
+
import {
|
|
187
|
+
defineComponent,
|
|
188
|
+
PropType,
|
|
189
|
+
ref,
|
|
190
|
+
nextTick,
|
|
191
|
+
toRef,
|
|
192
|
+
onMounted,
|
|
193
|
+
watch
|
|
194
|
+
} from 'vue-demi'
|
|
187
195
|
import { DlTypography, DlMenu } from '../../../essential'
|
|
188
196
|
import { DlButton } from '../../../basic'
|
|
189
197
|
import { DlSelect } from '../../DlSelect'
|
|
@@ -209,6 +217,7 @@ import {
|
|
|
209
217
|
} from './utils/utils'
|
|
210
218
|
import { v4 } from 'uuid'
|
|
211
219
|
import { parseSmartQuery, stringifySmartQuery } from '../../../../utils'
|
|
220
|
+
import { debounce } from 'lodash'
|
|
212
221
|
|
|
213
222
|
export default defineComponent({
|
|
214
223
|
components: {
|
|
@@ -223,7 +232,15 @@ export default defineComponent({
|
|
|
223
232
|
DlMenu,
|
|
224
233
|
DlSelect
|
|
225
234
|
},
|
|
235
|
+
model: {
|
|
236
|
+
prop: 'modelValue',
|
|
237
|
+
event: 'update:modelValue'
|
|
238
|
+
},
|
|
226
239
|
props: {
|
|
240
|
+
modelValue: {
|
|
241
|
+
type: Object,
|
|
242
|
+
default: {} as { [key: string]: any }
|
|
243
|
+
},
|
|
227
244
|
status: {
|
|
228
245
|
type: Object as PropType<SearchStatus>,
|
|
229
246
|
default: () => ({ type: 'info', message: '' })
|
|
@@ -239,9 +256,9 @@ export default defineComponent({
|
|
|
239
256
|
colorSchema: {
|
|
240
257
|
type: Object as PropType<ColorSchema>,
|
|
241
258
|
default: () => ({
|
|
242
|
-
fields: '
|
|
243
|
-
operators: '
|
|
244
|
-
keywords: '
|
|
259
|
+
fields: 'var(--dl-color-secondary)',
|
|
260
|
+
operators: 'var(--dl-color-positive)',
|
|
261
|
+
keywords: 'var(--dl-color-medium)'
|
|
245
262
|
})
|
|
246
263
|
},
|
|
247
264
|
isLoading: {
|
|
@@ -267,10 +284,17 @@ export default defineComponent({
|
|
|
267
284
|
width: {
|
|
268
285
|
type: String,
|
|
269
286
|
default: '450px'
|
|
287
|
+
},
|
|
288
|
+
/**
|
|
289
|
+
* If true, the validation will be a closed set based on the schema provided
|
|
290
|
+
*/
|
|
291
|
+
strict: {
|
|
292
|
+
type: Boolean,
|
|
293
|
+
default: false
|
|
270
294
|
}
|
|
271
295
|
},
|
|
272
|
-
emits: ['save-query', 'remove-query', 'search-query'],
|
|
273
|
-
setup(props) {
|
|
296
|
+
emits: ['save-query', 'remove-query', 'search-query', 'update:modelValue'],
|
|
297
|
+
setup(props, { emit }) {
|
|
274
298
|
const inputModel = ref('')
|
|
275
299
|
const jsonEditorModel = ref(false)
|
|
276
300
|
const searchBarWidth = ref('100%')
|
|
@@ -295,21 +319,30 @@ export default defineComponent({
|
|
|
295
319
|
value: ''
|
|
296
320
|
})
|
|
297
321
|
|
|
322
|
+
const strictRef = toRef(props, 'strict')
|
|
323
|
+
|
|
298
324
|
const { suggestions, error, findSuggestions } = useSuggestions(
|
|
299
325
|
props.schema,
|
|
300
|
-
props.aliases
|
|
326
|
+
props.aliases,
|
|
327
|
+
{ strict: strictRef }
|
|
301
328
|
)
|
|
302
329
|
|
|
303
330
|
const handleInputModel = (value: string) => {
|
|
304
331
|
inputModel.value = value
|
|
305
|
-
const json =
|
|
306
|
-
|
|
332
|
+
const json = toJSON(removeBrackets(value))
|
|
333
|
+
emit('update:modelValue', json)
|
|
334
|
+
const stringified = JSON.stringify(json)
|
|
335
|
+
const newQuery = replaceWithAliases(stringified, props.aliases)
|
|
307
336
|
activeQuery.value.query = newQuery
|
|
308
|
-
|
|
337
|
+
nextTick(() => {
|
|
338
|
+
findSuggestions(value)
|
|
339
|
+
})
|
|
309
340
|
isQuerying.value = false
|
|
310
341
|
oldInputQuery.value = value
|
|
311
342
|
}
|
|
312
343
|
|
|
344
|
+
const debouncedInputModel = debounce(handleInputModel, 300)
|
|
345
|
+
|
|
313
346
|
const toJSON = (value: string) => {
|
|
314
347
|
return parseSmartQuery(
|
|
315
348
|
replaceWithJsDates(value) ?? inputModel.value
|
|
@@ -328,6 +361,23 @@ export default defineComponent({
|
|
|
328
361
|
toJSON(inputModel.value)
|
|
329
362
|
}
|
|
330
363
|
}
|
|
364
|
+
|
|
365
|
+
const modelRef: any = toRef(props, 'modelValue')
|
|
366
|
+
|
|
367
|
+
watch(modelRef, (val: any) => {
|
|
368
|
+
if (val) {
|
|
369
|
+
const stringQuery = stringifySmartQuery(val)
|
|
370
|
+
debouncedInputModel(stringQuery)
|
|
371
|
+
}
|
|
372
|
+
})
|
|
373
|
+
|
|
374
|
+
onMounted(() => {
|
|
375
|
+
if (props.modelValue) {
|
|
376
|
+
const stringQuery = stringifySmartQuery(props.modelValue)
|
|
377
|
+
debouncedInputModel(stringQuery)
|
|
378
|
+
}
|
|
379
|
+
})
|
|
380
|
+
|
|
331
381
|
return {
|
|
332
382
|
uuid: `dl-smart-search-${v4()}`,
|
|
333
383
|
inputModel,
|
|
@@ -349,6 +399,7 @@ export default defineComponent({
|
|
|
349
399
|
preventUpdate,
|
|
350
400
|
selectedOption,
|
|
351
401
|
handleInputModel,
|
|
402
|
+
debouncedInputModel,
|
|
352
403
|
setFocused,
|
|
353
404
|
findSuggestions,
|
|
354
405
|
toJSON
|
|
@@ -95,7 +95,7 @@ function renderText(text: string) {
|
|
|
95
95
|
const words = text?.split(/(\s+)/)
|
|
96
96
|
const output = words?.map((word) => {
|
|
97
97
|
if (styleModel.keywords.values.includes(word)) {
|
|
98
|
-
return `<strong style='${SPAN_STYLES}'>${word}</strong>`
|
|
98
|
+
return `<strong style='${SPAN_STYLES}; color:${styleModel.keywords.color}'>${word}</strong>`
|
|
99
99
|
} else if (styleModel.fields.values.includes(word)) {
|
|
100
100
|
return `<span style='color:${styleModel.fields.color}; ${SPAN_STYLES}'>${word}</span>`
|
|
101
101
|
} else if (styleModel.operators.values.includes(word)) {
|
|
@@ -69,12 +69,19 @@ export function replaceWithAliases(json: string, aliases: Alias[]) {
|
|
|
69
69
|
})
|
|
70
70
|
return newJson
|
|
71
71
|
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
72
|
+
|
|
73
|
+
export function revertAliases(str: string, aliases: Alias[]) {
|
|
74
|
+
const words: string[] = []
|
|
75
|
+
for (const alias of aliases) {
|
|
76
|
+
words.push(alias.key)
|
|
77
|
+
}
|
|
78
|
+
const replacement = (match: string) => {
|
|
79
|
+
const index = words.indexOf(match)
|
|
80
|
+
return aliases[index].alias
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const regex = new RegExp(words.join('|'), 'gi')
|
|
84
|
+
return str.replace(regex, replacement)
|
|
78
85
|
}
|
|
79
86
|
|
|
80
87
|
export function createColorSchema(
|
|
@@ -6,26 +6,30 @@
|
|
|
6
6
|
dense
|
|
7
7
|
label="Disabled"
|
|
8
8
|
/>
|
|
9
|
+
<dl-checkbox
|
|
10
|
+
v-model="strictState"
|
|
11
|
+
dense
|
|
12
|
+
label="Strict"
|
|
13
|
+
/>
|
|
9
14
|
</div>
|
|
10
15
|
<div
|
|
11
16
|
style="width: 100px"
|
|
12
17
|
class="props"
|
|
13
18
|
/>
|
|
14
19
|
<dl-smart-search
|
|
20
|
+
v-model="queryObject"
|
|
15
21
|
:aliases="aliases"
|
|
16
22
|
:schema="schema"
|
|
17
|
-
:color-schema="
|
|
18
|
-
fields: 'blue',
|
|
19
|
-
operators: 'green',
|
|
20
|
-
keywords: 'bold'
|
|
21
|
-
}"
|
|
23
|
+
:color-schema="colorSchema"
|
|
22
24
|
:filters="filters"
|
|
23
25
|
:disabled="switchState"
|
|
24
26
|
:is-loading="isLoading"
|
|
27
|
+
:strict="strictState"
|
|
25
28
|
@remove-query="handleRemoveQuery"
|
|
26
29
|
@save-query="handleSaveQuery"
|
|
27
30
|
@search-query="handleSearchQuery"
|
|
28
31
|
/>
|
|
32
|
+
{{ queryObject }}
|
|
29
33
|
</div>
|
|
30
34
|
</template>
|
|
31
35
|
|
|
@@ -33,7 +37,6 @@
|
|
|
33
37
|
import { defineComponent } from 'vue-demi'
|
|
34
38
|
import { DlSmartSearch, DlCheckbox } from '../../components'
|
|
35
39
|
import { Query } from '../../components/types'
|
|
36
|
-
import { aliases, schema } from './schema'
|
|
37
40
|
|
|
38
41
|
export default defineComponent({
|
|
39
42
|
name: 'DlSmartSearchDemo',
|
|
@@ -42,11 +45,59 @@ export default defineComponent({
|
|
|
42
45
|
DlCheckbox
|
|
43
46
|
},
|
|
44
47
|
data() {
|
|
48
|
+
const schema: any = {
|
|
49
|
+
id: ['string', 'number'],
|
|
50
|
+
filename: 'string',
|
|
51
|
+
name: 'string',
|
|
52
|
+
url: 'string',
|
|
53
|
+
type: 'string',
|
|
54
|
+
dataset: 'string',
|
|
55
|
+
datasetId: 'string',
|
|
56
|
+
dir: 'string',
|
|
57
|
+
thumbnail: 'string',
|
|
58
|
+
createdAt: 'date',
|
|
59
|
+
annotated: 'boolean',
|
|
60
|
+
hidden: 'boolean',
|
|
61
|
+
metadata: {
|
|
62
|
+
system: {
|
|
63
|
+
width: 'number',
|
|
64
|
+
height: 'number',
|
|
65
|
+
'*': 'any'
|
|
66
|
+
},
|
|
67
|
+
test: 'any',
|
|
68
|
+
'*': 'any'
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const colorSchema: any = {
|
|
73
|
+
fields: 'var(--dl-color-secondary)',
|
|
74
|
+
operators: 'var(--dl-color-positive)',
|
|
75
|
+
keywords: 'var(--dl-color-medium)'
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const aliases: any = [
|
|
79
|
+
{
|
|
80
|
+
alias: 'ItemID',
|
|
81
|
+
key: 'id'
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
alias: 'ItemHeight',
|
|
85
|
+
key: 'metadata.system.height'
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
alias: 'ItemWidth',
|
|
89
|
+
key: 'metadata.system.width'
|
|
90
|
+
}
|
|
91
|
+
]
|
|
92
|
+
|
|
45
93
|
return {
|
|
46
94
|
schema,
|
|
47
95
|
aliases,
|
|
96
|
+
colorSchema,
|
|
48
97
|
switchState: false,
|
|
98
|
+
strictState: false,
|
|
49
99
|
isLoading: false,
|
|
100
|
+
queryObject: {},
|
|
50
101
|
filters: {
|
|
51
102
|
saved: [
|
|
52
103
|
{
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { Ref, ref } from 'vue-demi'
|
|
2
2
|
import { splitByQuotes } from '../utils/splitByQuotes'
|
|
3
|
+
import { flatten } from 'flat'
|
|
4
|
+
import { isObject } from 'lodash'
|
|
3
5
|
|
|
4
6
|
export type Schema = {
|
|
5
7
|
[key: string]:
|
|
@@ -50,6 +52,15 @@ const operatorToDataTypeMap: OperatorToDataTypeMap = {
|
|
|
50
52
|
$nin: []
|
|
51
53
|
}
|
|
52
54
|
|
|
55
|
+
const knownDataTypes = [
|
|
56
|
+
'number',
|
|
57
|
+
'boolean',
|
|
58
|
+
'string',
|
|
59
|
+
'date',
|
|
60
|
+
'datetime',
|
|
61
|
+
'time'
|
|
62
|
+
]
|
|
63
|
+
|
|
53
64
|
type Suggestion = string
|
|
54
65
|
|
|
55
66
|
type Expression = {
|
|
@@ -79,14 +90,37 @@ export const dateIntervalPattern = new RegExp(
|
|
|
79
90
|
'gi'
|
|
80
91
|
)
|
|
81
92
|
|
|
82
|
-
export const useSuggestions = (
|
|
83
|
-
|
|
84
|
-
|
|
93
|
+
export const useSuggestions = (
|
|
94
|
+
schema: Schema,
|
|
95
|
+
aliases: Alias[],
|
|
96
|
+
options: { strict?: Ref<boolean> } = {}
|
|
97
|
+
) => {
|
|
98
|
+
const { strict } = options
|
|
99
|
+
const initialSuggestions = Object.keys(schema)
|
|
100
|
+
const aliasedKeys = aliases.map((alias) => alias.key)
|
|
101
|
+
const aliasedSuggestions = initialSuggestions.map((suggestion) =>
|
|
102
|
+
aliasedKeys.includes(suggestion)
|
|
103
|
+
? aliases.find((alias) => alias.key === suggestion)?.alias
|
|
104
|
+
: suggestion
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
for (const alias of aliases) {
|
|
108
|
+
if (aliasedSuggestions.includes(alias.alias)) {
|
|
109
|
+
continue
|
|
110
|
+
}
|
|
111
|
+
aliasedSuggestions.push(alias.alias)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const sortString = (a: string, b: string) =>
|
|
115
|
+
a.localeCompare(b, undefined, { sensitivity: 'base' })
|
|
116
|
+
const sortedSuggestions = aliasedSuggestions.sort(sortString)
|
|
117
|
+
|
|
118
|
+
const suggestions: Ref<Suggestion[]> = ref(sortedSuggestions)
|
|
85
119
|
const error: Ref<string | null> = ref(null)
|
|
86
120
|
|
|
87
121
|
const findSuggestions = (input: string) => {
|
|
88
122
|
input = input.replace(/\s+/g, ' ').trimStart()
|
|
89
|
-
localSuggestions =
|
|
123
|
+
localSuggestions = sortedSuggestions
|
|
90
124
|
|
|
91
125
|
const words = splitByQuotes(input, space)
|
|
92
126
|
const expressions = mapWordsToExpressions(words)
|
|
@@ -110,9 +144,7 @@ export const useSuggestions = (schema: Schema, aliases: Alias[]) => {
|
|
|
110
144
|
continue
|
|
111
145
|
}
|
|
112
146
|
|
|
113
|
-
const
|
|
114
|
-
if (!alias) continue
|
|
115
|
-
const dataType = getDataTypeByAliasKey(schema, alias.key)
|
|
147
|
+
const dataType = getDataType(schema, aliases, matchedField)
|
|
116
148
|
if (!dataType) {
|
|
117
149
|
localSuggestions = []
|
|
118
150
|
continue
|
|
@@ -139,7 +171,9 @@ export const useSuggestions = (schema: Schema, aliases: Alias[]) => {
|
|
|
139
171
|
}
|
|
140
172
|
|
|
141
173
|
if (Array.isArray(dataType)) {
|
|
142
|
-
localSuggestions = dataType
|
|
174
|
+
localSuggestions = dataType.filter(
|
|
175
|
+
(type) => !knownDataTypes.includes(type)
|
|
176
|
+
)
|
|
143
177
|
|
|
144
178
|
if (!value) continue
|
|
145
179
|
|
|
@@ -176,11 +210,11 @@ export const useSuggestions = (schema: Schema, aliases: Alias[]) => {
|
|
|
176
210
|
if (!matchedKeyword || !isNextCharSpace(input, matchedKeyword))
|
|
177
211
|
continue
|
|
178
212
|
|
|
179
|
-
localSuggestions =
|
|
213
|
+
localSuggestions = sortedSuggestions
|
|
180
214
|
}
|
|
181
215
|
|
|
182
216
|
error.value = input.length
|
|
183
|
-
? getError(schema, aliases, expressions)
|
|
217
|
+
? getError(schema, aliases, expressions, { strict })
|
|
184
218
|
: null
|
|
185
219
|
|
|
186
220
|
suggestions.value = localSuggestions
|
|
@@ -194,11 +228,38 @@ const errors = {
|
|
|
194
228
|
INVALID_VALUE: (field: string) => `Invalid value for "${field}" field`
|
|
195
229
|
}
|
|
196
230
|
|
|
231
|
+
const isInputAllowed = (input: string, allowedKeys: string[]): boolean => {
|
|
232
|
+
for (const key of allowedKeys) {
|
|
233
|
+
const keyParts = key.split('.')
|
|
234
|
+
const inputParts = input.split('.')
|
|
235
|
+
|
|
236
|
+
if (keyParts.length > inputParts.length) {
|
|
237
|
+
continue
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
let isMatch = true
|
|
241
|
+
for (let i = 0; i < keyParts.length; i++) {
|
|
242
|
+
if (keyParts[i] !== '*' && keyParts[i] !== inputParts[i]) {
|
|
243
|
+
isMatch = false
|
|
244
|
+
break
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (isMatch) {
|
|
249
|
+
return true
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return false
|
|
254
|
+
}
|
|
255
|
+
|
|
197
256
|
const getError = (
|
|
198
257
|
schema: Schema,
|
|
199
258
|
aliases: Alias[],
|
|
200
|
-
expressions: Expression[]
|
|
259
|
+
expressions: Expression[],
|
|
260
|
+
options: { strict?: Ref<boolean> } = {}
|
|
201
261
|
): string | null => {
|
|
262
|
+
const { strict } = options
|
|
202
263
|
const hasErrorInStructure = expressions
|
|
203
264
|
.flatMap((exp) => Object.values(exp))
|
|
204
265
|
.some((el, index, arr) => {
|
|
@@ -214,11 +275,29 @@ const getError = (
|
|
|
214
275
|
.filter(({ field, value }) => field !== null && value !== null)
|
|
215
276
|
.reduce<string | null>((acc, { field, value, operator }, _, arr) => {
|
|
216
277
|
if (acc === 'warning') return acc
|
|
217
|
-
const
|
|
218
|
-
|
|
278
|
+
const key: string = getAliasObjByAlias(aliases, field)?.key ?? field
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Handle nested keys to validate if the key exists in the schema or not.
|
|
282
|
+
*/
|
|
283
|
+
const keys: string[] = []
|
|
284
|
+
for (const key of Object.keys(schema)) {
|
|
285
|
+
if (isObject(schema[key]) && !Array.isArray(schema[key])) {
|
|
286
|
+
const flattened = flatten({ [key]: schema[key] })
|
|
287
|
+
keys.push(...Object.keys(flattened))
|
|
288
|
+
} else {
|
|
289
|
+
keys.push(key)
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const isValid = isInputAllowed(key, keys)
|
|
294
|
+
if (!keys.includes(key) && !isValid) {
|
|
295
|
+
return strict.value ? errors.INVALID_EXPRESSION : 'warning'
|
|
296
|
+
}
|
|
297
|
+
|
|
219
298
|
const valid = isValidByDataType(
|
|
220
299
|
validateBracketValues(value),
|
|
221
|
-
|
|
300
|
+
getDataType(schema, aliases, key),
|
|
222
301
|
operator
|
|
223
302
|
)
|
|
224
303
|
|
|
@@ -236,8 +315,16 @@ const isValidByDataType = (
|
|
|
236
315
|
dataType: string | string[],
|
|
237
316
|
operator: string // TODO: use operator
|
|
238
317
|
): boolean => {
|
|
318
|
+
if (dataType === 'any') {
|
|
319
|
+
return true
|
|
320
|
+
}
|
|
321
|
+
|
|
239
322
|
if (Array.isArray(dataType)) {
|
|
240
|
-
|
|
323
|
+
let isOneOf = !!getValueMatch(dataType, str)
|
|
324
|
+
for (const type of dataType) {
|
|
325
|
+
isOneOf = isOneOf || isValidByDataType(str, type, operator)
|
|
326
|
+
}
|
|
327
|
+
return isOneOf
|
|
241
328
|
}
|
|
242
329
|
|
|
243
330
|
switch (dataType) {
|
|
@@ -283,6 +370,8 @@ const isValidString = (str: string) => {
|
|
|
283
370
|
}
|
|
284
371
|
|
|
285
372
|
const getOperatorByDataType = (dataType: string) => {
|
|
373
|
+
if (dataType === 'boolean') return ['$eq', '$neq']
|
|
374
|
+
|
|
286
375
|
return Object.keys(operatorToDataTypeMap).filter((key) => {
|
|
287
376
|
const value = operatorToDataTypeMap[key]
|
|
288
377
|
return value.length === 0 || value.includes(dataType)
|
|
@@ -300,19 +389,29 @@ const mapWordsToExpression = (words: string[]): Expression => {
|
|
|
300
389
|
}
|
|
301
390
|
}
|
|
302
391
|
|
|
303
|
-
const
|
|
392
|
+
const getDataType = (
|
|
304
393
|
schema: Schema,
|
|
394
|
+
aliases: Alias[],
|
|
305
395
|
key: string
|
|
306
396
|
): string | string[] | null => {
|
|
307
|
-
const
|
|
397
|
+
const aliasedKey = getAliasObjByAlias(aliases, key)?.key ?? key
|
|
398
|
+
|
|
399
|
+
const nestedKey = aliasedKey.split('.')
|
|
308
400
|
|
|
309
401
|
if (nestedKey.length === 1) {
|
|
310
402
|
return (schema[nestedKey[0]] as string | string[]) ?? null
|
|
311
403
|
}
|
|
312
404
|
|
|
313
405
|
let value = schema[nestedKey[0]] as Schema
|
|
406
|
+
if (!value) return null
|
|
407
|
+
|
|
314
408
|
for (let i = 1; i < nestedKey.length; i++) {
|
|
409
|
+
if (!value) return null
|
|
410
|
+
|
|
315
411
|
const nextKey = nestedKey[i]
|
|
412
|
+
if (!value[nextKey] && value['*']) {
|
|
413
|
+
return 'any'
|
|
414
|
+
}
|
|
316
415
|
value = (value[nextKey] as Schema) ?? null
|
|
317
416
|
}
|
|
318
417
|
|
|
@@ -1,6 +1,26 @@
|
|
|
1
1
|
/* eslint-disable no-empty */
|
|
2
2
|
|
|
3
|
-
import { isFinite, isObject, isString } from 'lodash'
|
|
3
|
+
import { isBoolean, isFinite, isNumber, isObject, isString } from 'lodash'
|
|
4
|
+
|
|
5
|
+
const GeneratePureValue = (value: any) => {
|
|
6
|
+
if (typeof value === 'string') {
|
|
7
|
+
if (value === 'true') {
|
|
8
|
+
return true
|
|
9
|
+
}
|
|
10
|
+
if (value === 'false') {
|
|
11
|
+
return false
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
const num = Number(value)
|
|
16
|
+
if (isFinite(num)) {
|
|
17
|
+
return num
|
|
18
|
+
}
|
|
19
|
+
return value.replaceAll('"', '').replaceAll("'", '')
|
|
20
|
+
} catch (e) {}
|
|
21
|
+
}
|
|
22
|
+
return value
|
|
23
|
+
}
|
|
4
24
|
|
|
5
25
|
export const parseSmartQuery = (query: string) => {
|
|
6
26
|
const queryArr = query.split(' OR ')
|
|
@@ -15,44 +35,31 @@ export const parseSmartQuery = (query: string) => {
|
|
|
15
35
|
let key: string
|
|
16
36
|
let value: string | number | object
|
|
17
37
|
|
|
18
|
-
const cleanValue = (value: any) => {
|
|
19
|
-
if (typeof value === 'string') {
|
|
20
|
-
try {
|
|
21
|
-
const num = Number(value)
|
|
22
|
-
if (isFinite(num)) {
|
|
23
|
-
return num
|
|
24
|
-
}
|
|
25
|
-
} catch (e) {}
|
|
26
|
-
return value.replaceAll('"', '').replaceAll("'", '')
|
|
27
|
-
}
|
|
28
|
-
return value
|
|
29
|
-
}
|
|
30
|
-
|
|
31
38
|
for (const term of andTerms) {
|
|
32
39
|
switch (true) {
|
|
33
40
|
case term.includes('>='):
|
|
34
41
|
[key, value] = term.split('>=').map((x) => x.trim())
|
|
35
|
-
andQuery[key] = { $gte:
|
|
42
|
+
andQuery[key] = { $gte: GeneratePureValue(value) }
|
|
36
43
|
break
|
|
37
44
|
case term.includes('<='):
|
|
38
45
|
[key, value] = term.split('<=').map((x) => x.trim())
|
|
39
|
-
andQuery[key] = { $lte:
|
|
46
|
+
andQuery[key] = { $lte: GeneratePureValue(value) }
|
|
40
47
|
break
|
|
41
48
|
case term.includes('>'):
|
|
42
49
|
[key, value] = term.split('>').map((x) => x.trim())
|
|
43
|
-
andQuery[key] = { $gt:
|
|
50
|
+
andQuery[key] = { $gt: GeneratePureValue(value) }
|
|
44
51
|
break
|
|
45
52
|
case term.includes('<'):
|
|
46
53
|
[key, value] = term.split('<').map((x) => x.trim())
|
|
47
|
-
andQuery[key] = { $lt:
|
|
54
|
+
andQuery[key] = { $lt: GeneratePureValue(value) }
|
|
48
55
|
break
|
|
49
56
|
case term.includes('!='):
|
|
50
57
|
[key, value] = term.split('!=').map((x) => x.trim())
|
|
51
|
-
andQuery[key] = { $ne:
|
|
58
|
+
andQuery[key] = { $ne: GeneratePureValue(value) }
|
|
52
59
|
break
|
|
53
60
|
case term.includes('='):
|
|
54
61
|
[key, value] = term.split('=').map((x) => x.trim())
|
|
55
|
-
andQuery[key] =
|
|
62
|
+
andQuery[key] = GeneratePureValue(value)
|
|
56
63
|
break
|
|
57
64
|
case term.includes('IN'):
|
|
58
65
|
[key, value] = term.split('IN').map((x) => x.trim())
|
|
@@ -64,15 +71,15 @@ export const parseSmartQuery = (query: string) => {
|
|
|
64
71
|
.split('NOT-IN')
|
|
65
72
|
.map((x) => x.trim())[1]
|
|
66
73
|
.split(',')
|
|
67
|
-
.map((x) =>
|
|
68
|
-
andQuery[key] = { $nin:
|
|
74
|
+
.map((x) => GeneratePureValue(x.trim()))
|
|
75
|
+
andQuery[key] = { $nin: GeneratePureValue(queryValue) }
|
|
69
76
|
} else {
|
|
70
77
|
queryValue = term
|
|
71
78
|
.split('IN')
|
|
72
79
|
.map((x) => x.trim())[1]
|
|
73
80
|
.split(',')
|
|
74
|
-
.map((x) =>
|
|
75
|
-
andQuery[key] = { $in:
|
|
81
|
+
.map((x) => GeneratePureValue(x.trim()))
|
|
82
|
+
andQuery[key] = { $in: GeneratePureValue(queryValue) }
|
|
76
83
|
}
|
|
77
84
|
break
|
|
78
85
|
}
|
|
@@ -90,104 +97,98 @@ export const stringifySmartQuery = (query: { [key: string]: any }) => {
|
|
|
90
97
|
let result = ''
|
|
91
98
|
|
|
92
99
|
for (const key in query) {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
if (
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
(subQuery
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
result += subQueries.join(' OR ')
|
|
103
|
-
}
|
|
104
|
-
continue
|
|
100
|
+
const value = query[key]
|
|
101
|
+
|
|
102
|
+
if (key === '$or') {
|
|
103
|
+
if (Array.isArray(value)) {
|
|
104
|
+
const subQueries = value.map(
|
|
105
|
+
(subQuery: { [key: string]: any }) =>
|
|
106
|
+
stringifySmartQuery(subQuery)
|
|
107
|
+
)
|
|
108
|
+
result += subQueries.join(' OR ')
|
|
105
109
|
}
|
|
110
|
+
continue
|
|
111
|
+
}
|
|
106
112
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
113
|
+
if (result.length) {
|
|
114
|
+
result += ' AND '
|
|
115
|
+
}
|
|
110
116
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
117
|
+
if (isObject(value)) {
|
|
118
|
+
for (const operator in value) {
|
|
119
|
+
if (value.hasOwnProperty(operator)) {
|
|
120
|
+
let operatorValue = (
|
|
121
|
+
value as {
|
|
122
|
+
[key: string]: string | number | string[] | number[]
|
|
123
|
+
}
|
|
124
|
+
)[operator]
|
|
125
|
+
switch (operator) {
|
|
126
|
+
case '$eq':
|
|
127
|
+
result += `${key} = ${
|
|
128
|
+
isString(operatorValue)
|
|
129
|
+
? `'${operatorValue}'`
|
|
130
|
+
: operatorValue
|
|
131
|
+
}`
|
|
132
|
+
break
|
|
133
|
+
case '$ne':
|
|
134
|
+
result += `${key} != ${
|
|
135
|
+
isString(operatorValue)
|
|
136
|
+
? `'${operatorValue}'`
|
|
137
|
+
: operatorValue
|
|
138
|
+
}`
|
|
139
|
+
break
|
|
140
|
+
case '$gt':
|
|
141
|
+
result += `${key} > ${operatorValue}`
|
|
142
|
+
break
|
|
143
|
+
case '$gte':
|
|
144
|
+
result += `${key} >= ${operatorValue}`
|
|
145
|
+
break
|
|
146
|
+
case '$lt':
|
|
147
|
+
result += `${key} < ${operatorValue}`
|
|
148
|
+
break
|
|
149
|
+
case '$lte':
|
|
150
|
+
result += `${key} <= ${operatorValue}`
|
|
151
|
+
break
|
|
152
|
+
case '$in':
|
|
153
|
+
if (!Array.isArray(operatorValue)) {
|
|
154
|
+
operatorValue = [operatorValue] as
|
|
119
155
|
| string[]
|
|
120
156
|
| number[]
|
|
121
157
|
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
isString(operatorValue)
|
|
127
|
-
? `'${operatorValue}'`
|
|
128
|
-
: operatorValue
|
|
129
|
-
}`
|
|
130
|
-
break
|
|
131
|
-
case '$ne':
|
|
132
|
-
result += `${key} != ${
|
|
133
|
-
isString(operatorValue)
|
|
134
|
-
? `'${operatorValue}'`
|
|
135
|
-
: operatorValue
|
|
136
|
-
}`
|
|
137
|
-
break
|
|
138
|
-
case '$gt':
|
|
139
|
-
result += `${key} > ${operatorValue}`
|
|
140
|
-
break
|
|
141
|
-
case '$gte':
|
|
142
|
-
result += `${key} >= ${operatorValue}`
|
|
143
|
-
break
|
|
144
|
-
case '$lt':
|
|
145
|
-
result += `${key} < ${operatorValue}`
|
|
146
|
-
break
|
|
147
|
-
case '$lte':
|
|
148
|
-
result += `${key} <= ${operatorValue}`
|
|
149
|
-
break
|
|
150
|
-
case '$in':
|
|
151
|
-
if (!Array.isArray(operatorValue)) {
|
|
152
|
-
operatorValue = [operatorValue] as
|
|
153
|
-
| string[]
|
|
154
|
-
| number[]
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
const inValues: string = (
|
|
158
|
-
operatorValue as any[]
|
|
158
|
+
|
|
159
|
+
const inValues: string = (operatorValue as any[])
|
|
160
|
+
.map((x: string | number) =>
|
|
161
|
+
isString(x) ? `'${x}'` : x
|
|
159
162
|
)
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
const ninValues: string = (
|
|
174
|
-
operatorValue as any[]
|
|
163
|
+
.join(', ')
|
|
164
|
+
result += `${key} IN ${inValues} `
|
|
165
|
+
break
|
|
166
|
+
case '$nin':
|
|
167
|
+
if (!Array.isArray(operatorValue)) {
|
|
168
|
+
operatorValue = [operatorValue] as
|
|
169
|
+
| string[]
|
|
170
|
+
| number[]
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const ninValues: string = (operatorValue as any[])
|
|
174
|
+
.map((x: string | number) =>
|
|
175
|
+
isString(x) ? `'${x}'` : x
|
|
175
176
|
)
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
break
|
|
183
|
-
default:
|
|
184
|
-
throw new Error(`Invalid operator: ${operator}`)
|
|
185
|
-
}
|
|
177
|
+
.join(', ')
|
|
178
|
+
|
|
179
|
+
result += `${key} NOT-IN ${ninValues}`
|
|
180
|
+
break
|
|
181
|
+
default:
|
|
182
|
+
throw new Error(`Invalid operator: ${operator}`)
|
|
186
183
|
}
|
|
187
184
|
}
|
|
188
|
-
} else {
|
|
189
|
-
result += `${key} = '${value}'`
|
|
190
185
|
}
|
|
186
|
+
} else if (isNumber(value)) {
|
|
187
|
+
result += `${key} = ${value}`
|
|
188
|
+
} else if (isBoolean(value)) {
|
|
189
|
+
result += `${key} = ${value}`
|
|
190
|
+
} else {
|
|
191
|
+
result += `${key} = '${value}'`
|
|
191
192
|
}
|
|
192
193
|
}
|
|
193
194
|
|