@dataloop-ai/components 0.17.62 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dataloop-ai/components",
3
- "version": "0.17.62",
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",
@@ -142,6 +142,8 @@ body {
142
142
  --dl-font-size-h2: 20px;
143
143
  --dl-font-size-h3: 16px;
144
144
  --dl-font-size-h4: 14px;
145
+ --dl-font-size-h5: 12px;
146
+ --dl-font-size-h6: 10px;
145
147
  --dl-font-size-body: 12px;
146
148
  --dl-font-size-small: 10px;
147
149
 
@@ -173,6 +175,7 @@ body {
173
175
  --dl-color-fill-third: #FBFBFB;
174
176
  --dl-color-link: #20ABFA;
175
177
  --dl-color-cell-background: #FFFAE2;
178
+ --dl-color-disabled-slider: #E4E4E4;
176
179
  --q-color-positive: #38D079;
177
180
  --q-color-warning: #E1B75B;
178
181
 
@@ -226,6 +229,7 @@ body {
226
229
  --dl-color-fill-third: #9E9E9E1A;
227
230
  --dl-color-link: #53B2E8;
228
231
  --dl-color-cell-background: #FFFAE2;
232
+ --dl-color-disabled-slider: #64686D;
229
233
  --q-color-positive: #A1E5B6;
230
234
  --q-color-warning: #F8D29A;
231
235
 
@@ -38,7 +38,7 @@
38
38
  v-else
39
39
  color="dl-color-darker"
40
40
  icon="icon-dl-close"
41
- size="8px"
41
+ size="12px"
42
42
  />
43
43
  </span>
44
44
  <span class="subtitle">{{ subtitle }}</span>
@@ -91,7 +91,7 @@ export default defineComponent({
91
91
  <style scoped lang="scss">
92
92
  .popup-header {
93
93
  max-width: 100%;
94
- padding: 0 10px 20px 16px;
94
+ padding: 16px 10px 20px 16px;
95
95
  }
96
96
 
97
97
  .header-content {
@@ -111,6 +111,7 @@ export default defineComponent({
111
111
  display: flex;
112
112
  justify-content: center;
113
113
  align-items: center;
114
+ margin-top: 4px;
114
115
  cursor: pointer;
115
116
  transition: color 200ms;
116
117
  &:hover {
@@ -22,7 +22,7 @@
22
22
  :default-width="width"
23
23
  @save="saveQueryDialogBoxModel = true"
24
24
  @focus="setFocused"
25
- @update:modelValue="handleInputModel"
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 { defineComponent, PropType, ref } from 'vue-demi'
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: 'blue',
243
- operators: 'darkgreen',
244
- keywords: 'bold'
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 = JSON.stringify(toJSON(removeBrackets(value)))
306
- const newQuery = replaceWithAliases(json, props.aliases)
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
- findSuggestions(value)
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
- export function revertAliases(json: string, aliases: Alias[]) {
73
- let newJson = json
74
- aliases.forEach((alias) => {
75
- newJson = newJson.replaceAll(alias.key, alias.alias)
76
- })
77
- return newJson
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(
@@ -57,7 +57,7 @@
57
57
  <div class="right-container capitalize">
58
58
  <dl-button
59
59
  flat
60
- size="m"
60
+ size="12px"
61
61
  label="Reset"
62
62
  :disabled="disabled || readonly"
63
63
  data-test="non-editable-slider-button"
@@ -146,7 +146,7 @@ export default defineComponent({
146
146
  color:
147
147
  this.disabled !== true
148
148
  ? getColor(this.color, 'dl-color-secondary')
149
- : 'var(--dl-color-separator)'
149
+ : 'var(--dl-color-disabled-slider)'
150
150
  }
151
151
  },
152
152
  thumbStyle(): Record<string, any> {
@@ -82,7 +82,7 @@ export default defineComponent({
82
82
  <style scoped>
83
83
  input[type='number'] {
84
84
  width: 31px;
85
- height: 12px;
85
+ height: 20px;
86
86
  padding-top: 3px;
87
87
  padding-left: 5px;
88
88
  padding-bottom: 3px;
@@ -103,6 +103,9 @@
103
103
  display: flex;
104
104
  align-items: center;
105
105
  justify-content: space-between;
106
+ & > * {
107
+ margin: 0px 5px
108
+ }
106
109
  }
107
110
  }
108
111
  }
@@ -16,7 +16,7 @@ import { transformOptions } from '../../shared/types'
16
16
 
17
17
  type Variant = 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'p'
18
18
 
19
- const sizes = ['h1', 'h2', 'h3', 'h4', 'body', 'small']
19
+ const sizes = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'body', 'small']
20
20
 
21
21
  export default defineComponent({
22
22
  name: 'DlTypography',
@@ -108,6 +108,12 @@ export default defineComponent({
108
108
  &--h4 {
109
109
  font-size: var(--dl-font-size-h4);
110
110
  }
111
+ &--h5 {
112
+ font-size: var(--dl-font-size-h5);
113
+ }
114
+ &--h6 {
115
+ font-size: var(--dl-font-size-h6);
116
+ }
111
117
  &--body {
112
118
  font-size: var(--dl-font-size-body);
113
119
  }
@@ -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 = (schema: Schema, aliases: Alias[]) => {
83
- const initialSuggestions = aliases.map((alias) => alias.alias)
84
- const suggestions: Ref<Suggestion[]> = ref(initialSuggestions)
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 = initialSuggestions
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 alias = getAliasObjByAlias(aliases, matchedField)
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 = initialSuggestions
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 aliasObj = getAliasObjByAlias(aliases, field)
218
- if (!aliasObj) return 'warning'
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
- getDataTypeByAliasKey(schema, aliasObj!.key),
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
- return !!getValueMatch(dataType, str)
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 getDataTypeByAliasKey = (
392
+ const getDataType = (
304
393
  schema: Schema,
394
+ aliases: Alias[],
305
395
  key: string
306
396
  ): string | string[] | null => {
307
- const nestedKey = key.split('.')
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: cleanValue(value) }
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: cleanValue(value) }
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: cleanValue(value) }
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: cleanValue(value) }
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: cleanValue(value) }
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] = cleanValue(value)
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) => cleanValue(x.trim()))
68
- andQuery[key] = { $nin: cleanValue(queryValue) }
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) => cleanValue(x.trim()))
75
- andQuery[key] = { $in: cleanValue(queryValue) }
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
- if (query.hasOwnProperty(key)) {
94
- const value = query[key]
95
-
96
- if (key === '$or') {
97
- if (Array.isArray(value)) {
98
- const subQueries = value.map(
99
- (subQuery: { [key: string]: any }) =>
100
- stringifySmartQuery(subQuery)
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
- if (result.length) {
108
- result += ' AND '
109
- }
113
+ if (result.length) {
114
+ result += ' AND '
115
+ }
110
116
 
111
- if (isObject(value)) {
112
- for (const operator in value) {
113
- if (value.hasOwnProperty(operator)) {
114
- let operatorValue = (
115
- value as {
116
- [key: string]:
117
- | string
118
- | number
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
- )[operator]
123
- switch (operator) {
124
- case '$eq':
125
- result += `${key} = ${
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
- .map((x: string | number) =>
161
- isString(x) ? `'${x}'` : x
162
- )
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 = (
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
- .map((x: string | number) =>
177
- isString(x) ? `'${x}'` : x
178
- )
179
- .join(', ')
180
-
181
- result += `${key} NOT-IN ${ninValues}`
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