@rokkit/data 1.0.0-next.100
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/LICENSE +21 -0
- package/README.md +50 -0
- package/package.json +56 -0
- package/src/aggregators.js +24 -0
- package/src/constants.js +72 -0
- package/src/convert.js +36 -0
- package/src/dataset.js +428 -0
- package/src/filter.js +57 -0
- package/src/formatter.js +56 -0
- package/src/hierarchy.js +136 -0
- package/src/index.js +7 -0
- package/src/infer.js +263 -0
- package/src/list.js +232 -0
- package/src/metadata.js +106 -0
- package/src/model.js +100 -0
- package/src/parser.js +111 -0
- package/src/renamer.js +98 -0
- package/src/rollup.js +138 -0
- package/src/types.js +173 -0
- package/src/utils.js +36 -0
- package/src/view.js +181 -0
package/src/list.js
ADDED
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import { writable, get } from 'svelte/store'
|
|
2
|
+
import { ascending } from 'd3-array'
|
|
3
|
+
import { nest } from 'd3-collection'
|
|
4
|
+
import { v4 as uuid } from '@lukeed/uuid'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Compares two items for sorting. When grouped, uses group comparison before item comparison
|
|
8
|
+
*
|
|
9
|
+
* @param {*} a first item
|
|
10
|
+
* @param {*} b second item
|
|
11
|
+
* @param {*} data
|
|
12
|
+
* @returns
|
|
13
|
+
*/
|
|
14
|
+
export function compare(a, b, data) {
|
|
15
|
+
let result = 0
|
|
16
|
+
|
|
17
|
+
if (data.groupKey) {
|
|
18
|
+
result = ascending(data.lookup[a[data.groupKey]], data.lookup[b[data.groupKey]])
|
|
19
|
+
}
|
|
20
|
+
if (result === 0) {
|
|
21
|
+
result = ascending(a[data.sortKey], b[data.sortKey])
|
|
22
|
+
}
|
|
23
|
+
return result
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Search the data for existence of string in the data
|
|
28
|
+
*
|
|
29
|
+
* @param {List} data
|
|
30
|
+
* @param {string} value
|
|
31
|
+
* @returns
|
|
32
|
+
*/
|
|
33
|
+
export function quickSearch(data, value) {
|
|
34
|
+
return data.data.filter((d) => d[data.filterKey].toLowerCase().includes(value.toLowerCase()))
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Implements a list containing data and functionality for sorting, filtering, and grouping.
|
|
39
|
+
*/
|
|
40
|
+
export class List {
|
|
41
|
+
/**
|
|
42
|
+
* Creates a List instance with initial data and default settings for keys and functions.
|
|
43
|
+
* @param {Array<object>} data - The initial array of objects to populate the list.
|
|
44
|
+
*/
|
|
45
|
+
constructor(data) {
|
|
46
|
+
this.data = data
|
|
47
|
+
|
|
48
|
+
this.primaryKey = 'id'
|
|
49
|
+
this.filterKey = undefined
|
|
50
|
+
this.sortKey = undefined
|
|
51
|
+
this.groupKey = undefined
|
|
52
|
+
this.lookup = {}
|
|
53
|
+
this.searchText = ''
|
|
54
|
+
|
|
55
|
+
this.filterUsing = undefined
|
|
56
|
+
this.sortUsing = undefined
|
|
57
|
+
this.filtered = writable(data)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Sets a primary key attribute for list items and assigns a unique identifier if not present.
|
|
62
|
+
*
|
|
63
|
+
* @param {string} value - The property name to be set as the primary key.
|
|
64
|
+
* @returns {List} This list instance for method chaining.
|
|
65
|
+
*/
|
|
66
|
+
key(value) {
|
|
67
|
+
this.primaryKey = value
|
|
68
|
+
this.data = this.data.map((item) => (value in item ? item : { ...item, [value]: uuid() }))
|
|
69
|
+
|
|
70
|
+
return this
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Configures sorting by a given key and sorting function.
|
|
75
|
+
*
|
|
76
|
+
* @param {string} key - The property name to sort by.
|
|
77
|
+
* @param {Function} [using=compare] - The function used to sort the list items.
|
|
78
|
+
* @returns {List} This list instance for method chaining.
|
|
79
|
+
*/
|
|
80
|
+
sortBy(key, using = compare) {
|
|
81
|
+
this.sortKey = key
|
|
82
|
+
this.sortUsing = using
|
|
83
|
+
|
|
84
|
+
return this.sort().refresh()
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Groups list items by a given key and maps group keys for display using an optional lookup object.
|
|
89
|
+
*
|
|
90
|
+
* @param {string} key - The property name to group by.
|
|
91
|
+
* @param {object|null} [lookup=null] - An object mapping group keys to display names.
|
|
92
|
+
* @returns {List} This list instance for method chaining.
|
|
93
|
+
*/
|
|
94
|
+
groupBy(key, lookup = null) {
|
|
95
|
+
this.groupKey = key
|
|
96
|
+
|
|
97
|
+
if (lookup) this.lookup = lookup
|
|
98
|
+
else
|
|
99
|
+
this.lookup = this.data
|
|
100
|
+
.map((item) => item[key])
|
|
101
|
+
.reduce((obj, value) => ({ ...obj, [value]: value }), {})
|
|
102
|
+
|
|
103
|
+
return this.sort().refresh()
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Sets the key and function for filtering list items.
|
|
108
|
+
*
|
|
109
|
+
* @param {string} key - The property name to filter by.
|
|
110
|
+
* @param {Function} [using=quickSearch] - The function used to filter the list items.
|
|
111
|
+
* @returns {List} This list instance for method chaining.
|
|
112
|
+
*/
|
|
113
|
+
filterBy(key, using = quickSearch) {
|
|
114
|
+
this.filterKey = key
|
|
115
|
+
this.filterUsing = using
|
|
116
|
+
return this
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Sorts the list based on the configured sorting function.
|
|
121
|
+
*
|
|
122
|
+
* @returns {List} This list instance for method chaining.
|
|
123
|
+
*/
|
|
124
|
+
sort() {
|
|
125
|
+
if (this.sortUsing) this.data.sort((a, b) => this.sortUsing(a, b, this))
|
|
126
|
+
return this
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Adds a new item to the list, assigning a primary key if necessary, and then sorts and refreshes the list.
|
|
131
|
+
*
|
|
132
|
+
* @param {object} item - The item to add to the list.
|
|
133
|
+
* @returns {List} This list instance for method chaining.
|
|
134
|
+
*/
|
|
135
|
+
add(item) {
|
|
136
|
+
const index = this.data.findIndex((d) => d[this.primaryKey] === item[this.primaryKey])
|
|
137
|
+
if (index === -1) {
|
|
138
|
+
if (!(this.primaryKey in item)) {
|
|
139
|
+
item[this.primaryKey] = uuid()
|
|
140
|
+
}
|
|
141
|
+
this.data.push(item)
|
|
142
|
+
this.sort().refresh()
|
|
143
|
+
}
|
|
144
|
+
return this
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Removes an item from the list by primary key and refreshes the list.
|
|
149
|
+
*
|
|
150
|
+
* @param {object} item - The item to remove from the list.
|
|
151
|
+
* @returns {List} This list instance for method chaining.
|
|
152
|
+
*/
|
|
153
|
+
remove(item) {
|
|
154
|
+
const index = this.data.findIndex((d) => d[this.primaryKey] === item[this.primaryKey])
|
|
155
|
+
if (index > -1) {
|
|
156
|
+
this.data = [...this.data.slice(0, index), ...this.data.slice(index + 1)]
|
|
157
|
+
this.refresh()
|
|
158
|
+
}
|
|
159
|
+
return this
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Modifies an existing item in the list, identified by primary key, and then sorts and refreshes the list.
|
|
164
|
+
*
|
|
165
|
+
* @param {object} item - The item to modify in the list.
|
|
166
|
+
* @returns {List} This list instance for method chaining.
|
|
167
|
+
*/
|
|
168
|
+
modify(item) {
|
|
169
|
+
const index = this.data.findIndex((d) => d[this.primaryKey] === item[this.primaryKey])
|
|
170
|
+
if (index > -1) {
|
|
171
|
+
this.data[index] = { ...item }
|
|
172
|
+
this.sort().refresh()
|
|
173
|
+
}
|
|
174
|
+
return this
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Sets the text to search for in the list's items and then refreshes the list.
|
|
179
|
+
*
|
|
180
|
+
* @param {string} text - The text to search for.
|
|
181
|
+
* @returns {List} This list instance for method chaining.
|
|
182
|
+
*/
|
|
183
|
+
search(text) {
|
|
184
|
+
this.searchText = text
|
|
185
|
+
return this.refresh()
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Refreshes the list based on current sorting, grouping, and filtering configuration.
|
|
190
|
+
*
|
|
191
|
+
* @returns {List} This list instance for method chaining.
|
|
192
|
+
*/
|
|
193
|
+
refresh() {
|
|
194
|
+
let data = [...this.data]
|
|
195
|
+
|
|
196
|
+
if (this.filterKey && this.searchText) {
|
|
197
|
+
data = this.filterUsing(this, this.searchText)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (this.groupKey) {
|
|
201
|
+
data = nest()
|
|
202
|
+
.key((d) => d[this.groupKey])
|
|
203
|
+
.entries(data)
|
|
204
|
+
.map(({ key, values }) => ({
|
|
205
|
+
key,
|
|
206
|
+
name: this.lookup[key],
|
|
207
|
+
data: values
|
|
208
|
+
}))
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
this.filtered.set(data)
|
|
212
|
+
return this
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Gets the current state of the filtered list data.
|
|
216
|
+
*
|
|
217
|
+
* @returns {Array<object>} The current filtered data of the list.
|
|
218
|
+
*/
|
|
219
|
+
current() {
|
|
220
|
+
return get(this.filtered)
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Create a new list
|
|
226
|
+
*
|
|
227
|
+
* @param {Array<object>} data
|
|
228
|
+
* @returns
|
|
229
|
+
*/
|
|
230
|
+
export function list(data) {
|
|
231
|
+
return new List(data)
|
|
232
|
+
}
|
package/src/metadata.js
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { defaultPathOptions } from './constants'
|
|
2
|
+
import { getDeepScanSample } from './infer'
|
|
3
|
+
import { typeOf } from './utils'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Merges currency-related attributes into the corresponding base columns.
|
|
7
|
+
*
|
|
8
|
+
* @param {Array<Object>} input - An array of column definitions.
|
|
9
|
+
* @param {string} currencySuffix - The suffix used to identify currency columns.
|
|
10
|
+
* @returns {Array<Object>} A new array of merged column definitions.
|
|
11
|
+
*/
|
|
12
|
+
function mergeCurrencyAttributes(input, currencySuffix) {
|
|
13
|
+
let currencyColumns = input.filter((col) => col.name.endsWith(currencySuffix))
|
|
14
|
+
const columns = input.filter((col) => !col.name.endsWith(currencySuffix))
|
|
15
|
+
|
|
16
|
+
currencyColumns = currencyColumns
|
|
17
|
+
.map((col) => {
|
|
18
|
+
const matched = columns.find((column) => column.name === col.name.replace(currencySuffix, ''))
|
|
19
|
+
if (matched) {
|
|
20
|
+
matched.fields.currency = col.name
|
|
21
|
+
matched.type = 'currency'
|
|
22
|
+
matched.digits = 2
|
|
23
|
+
return null
|
|
24
|
+
}
|
|
25
|
+
return col
|
|
26
|
+
})
|
|
27
|
+
.filter((col) => col !== null)
|
|
28
|
+
|
|
29
|
+
return [...columns, ...currencyColumns]
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Adds a path modifier to column definitions that match a specific path.
|
|
34
|
+
*
|
|
35
|
+
* @param {Array<Object>} columns - The original array of column definitions.
|
|
36
|
+
* @param {string|null} path - The path used to identify columns that require path modifiers.
|
|
37
|
+
* @param {string} separator - The separator used in conjunction with the path.
|
|
38
|
+
* @returns {Array<Object>} The array of column definitions with path modifiers added as needed.
|
|
39
|
+
*/
|
|
40
|
+
export function addPathModifier(columns, path, separator) {
|
|
41
|
+
if (!path) return columns
|
|
42
|
+
|
|
43
|
+
const index = columns.findIndex((col) => col.name === path)
|
|
44
|
+
if (index > -1) {
|
|
45
|
+
return [
|
|
46
|
+
{ ...columns[index], path: true, separator },
|
|
47
|
+
...columns.slice(0, index),
|
|
48
|
+
...columns.slice(index + 1)
|
|
49
|
+
]
|
|
50
|
+
}
|
|
51
|
+
return columns
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Derives column properties for a given sample and options.
|
|
56
|
+
*
|
|
57
|
+
* @param {Object} sample - The sample object used to derive column properties.
|
|
58
|
+
* @param {Object} options - The options used to modify column properties.
|
|
59
|
+
* It can specify path, separator, and currencySuffix for column customization.
|
|
60
|
+
* @returns {Array<Object>} An array of derived column definitions.
|
|
61
|
+
*/
|
|
62
|
+
export function deriveColumnProperties(sample, options) {
|
|
63
|
+
const { path, separator, currencySuffix } = { ...defaultPathOptions, ...options }
|
|
64
|
+
|
|
65
|
+
let columns = Object.keys(sample).map((key) => ({
|
|
66
|
+
name: key,
|
|
67
|
+
type: typeOf(sample[key]),
|
|
68
|
+
sortable: true,
|
|
69
|
+
filterable: true,
|
|
70
|
+
sorted: 'none',
|
|
71
|
+
fields: { text: key }
|
|
72
|
+
}))
|
|
73
|
+
|
|
74
|
+
columns = addPathModifier(columns, path, separator)
|
|
75
|
+
return mergeCurrencyAttributes(columns, currencySuffix)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Derives the column metadata from the provided data and options.
|
|
80
|
+
*
|
|
81
|
+
* @param {Array} data - The data to derive the column metadata from.
|
|
82
|
+
* @param {Object} [options] - Optional parameters to control the metadata derivation.
|
|
83
|
+
* @param {Array} [options.metadata] - The metadata to use instead of deriving it from the data.
|
|
84
|
+
* @param {boolean} [options.deepScan=false] - Determines if the metadata derivation should perform a deep scan of the data.
|
|
85
|
+
*
|
|
86
|
+
* @returns {import('./types').Metadata} The derived column metadata.
|
|
87
|
+
*/
|
|
88
|
+
export function deriveColumnMetadata(data, options = {}) {
|
|
89
|
+
const { metadata, deepScan = false } = options
|
|
90
|
+
if (Array.isArray(metadata) && metadata.length > 0) return metadata
|
|
91
|
+
if (data.length === 0) return []
|
|
92
|
+
const sample = deepScan ? getDeepScanSample(data) : data[0]
|
|
93
|
+
return Object.entries(sample).map(([name, value]) => ({ name, type: typeOf(value) }))
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Derive column index from metadata.
|
|
98
|
+
* @param {Array} metadata - The metadata array.
|
|
99
|
+
* @returns {Object} - The column index object.
|
|
100
|
+
*/
|
|
101
|
+
// export function deriveColumnIndex(metadata) {
|
|
102
|
+
// return metadata.reduce((acc, col, index) => {
|
|
103
|
+
// acc[col.name] = index
|
|
104
|
+
// return acc
|
|
105
|
+
// }, {})
|
|
106
|
+
// }
|
package/src/model.js
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { identity, clone as copy } from 'ramda'
|
|
2
|
+
import { getDeepScanSample } from './infer'
|
|
3
|
+
import { typeOf } from './utils'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* A model is a representation of a dataset
|
|
7
|
+
* @returns an object with methods to manipulate the model
|
|
8
|
+
*/
|
|
9
|
+
export function model() {
|
|
10
|
+
let data = []
|
|
11
|
+
|
|
12
|
+
const actions = {
|
|
13
|
+
get: () => data,
|
|
14
|
+
/*
|
|
15
|
+
* Creates a clone from another model
|
|
16
|
+
* @param {Object} other - the other model object to clone from
|
|
17
|
+
*/
|
|
18
|
+
clone: (other) => {
|
|
19
|
+
data = copy(other.get())
|
|
20
|
+
return actions
|
|
21
|
+
},
|
|
22
|
+
/**
|
|
23
|
+
* Renames the reference names using a renamer
|
|
24
|
+
* @param {Function} rename - the renamer function
|
|
25
|
+
*/
|
|
26
|
+
renameUsing: (rename = identity) => {
|
|
27
|
+
if (rename !== identity) {
|
|
28
|
+
data = data.map((x) => ({ ...x, name: rename(x.name) }))
|
|
29
|
+
}
|
|
30
|
+
return actions
|
|
31
|
+
},
|
|
32
|
+
/**
|
|
33
|
+
* Analyzes the input data and derives a model from it
|
|
34
|
+
*
|
|
35
|
+
* @param {Array|Object} value - the data to derive the model from
|
|
36
|
+
* @param {boolean} sparseData - indicates that rows may have missing attributes
|
|
37
|
+
* @returns {Object} this
|
|
38
|
+
*/
|
|
39
|
+
from: (value, sparseData) => {
|
|
40
|
+
data = deriveModel(value, sparseData)
|
|
41
|
+
return actions
|
|
42
|
+
},
|
|
43
|
+
/**
|
|
44
|
+
* Merges the model with another model
|
|
45
|
+
*
|
|
46
|
+
* @param {Array} other - the model to merge with
|
|
47
|
+
* @param {Boolean} override - whether to override the type of the first model
|
|
48
|
+
* @returns {Object} this
|
|
49
|
+
*/
|
|
50
|
+
merge: (other, override = false) => {
|
|
51
|
+
data = mergeModels(data, other.get(), override)
|
|
52
|
+
return actions
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return actions
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Derives a model from the input data
|
|
60
|
+
*
|
|
61
|
+
* @param {Array|Object} data - the data to derive the model from
|
|
62
|
+
* @param {Boolean} sparse - whether the data set contains sparse data
|
|
63
|
+
* @returns {Array} - the derived model
|
|
64
|
+
*/
|
|
65
|
+
function deriveModel(data, sparse = false) {
|
|
66
|
+
const result = []
|
|
67
|
+
let item = data
|
|
68
|
+
|
|
69
|
+
if (Array.isArray(data)) item = sparse ? getDeepScanSample(data) : data[0]
|
|
70
|
+
|
|
71
|
+
const kv = Object.entries(item)
|
|
72
|
+
kv.forEach(([key, value]) => {
|
|
73
|
+
result.push({
|
|
74
|
+
name: key,
|
|
75
|
+
type: typeOf(value)
|
|
76
|
+
})
|
|
77
|
+
})
|
|
78
|
+
return result
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Merges two models, returning a new model
|
|
83
|
+
*
|
|
84
|
+
* @param {Array} first - the first model
|
|
85
|
+
* @param {Array} second - the second model
|
|
86
|
+
* @param {Boolean} override - whether to override the type of the first model
|
|
87
|
+
* @returns {Array} - the merged model
|
|
88
|
+
*/
|
|
89
|
+
function mergeModels(first, second, override = false) {
|
|
90
|
+
second.forEach(({ name, type }) => {
|
|
91
|
+
const existing = first.find((x) => x.name === name)
|
|
92
|
+
if (existing) {
|
|
93
|
+
if (existing.type !== type) existing.mixedTypes = true
|
|
94
|
+
if (override) existing.type = type
|
|
95
|
+
} else {
|
|
96
|
+
first.push({ name, type })
|
|
97
|
+
}
|
|
98
|
+
})
|
|
99
|
+
return first
|
|
100
|
+
}
|
package/src/parser.js
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Constructs a regular expression pattern for matching search filter strings.
|
|
3
|
+
* The pattern captures "column", "operator", and "value" groups for filter expressions.
|
|
4
|
+
* Supported operators include common comparison and wildcard operators.
|
|
5
|
+
*
|
|
6
|
+
* Examples of valid expressions this regex can match:
|
|
7
|
+
* - "username:john_doe"
|
|
8
|
+
* - "age>30"
|
|
9
|
+
* - "status!=active"
|
|
10
|
+
* - "title~\"Product Manager\""
|
|
11
|
+
* - "completed!~*yes"
|
|
12
|
+
*
|
|
13
|
+
* The "column" group matches one or more word characters.
|
|
14
|
+
* The "operator" group matches one among the specified comparison or wildcard operators:
|
|
15
|
+
* :, >, <, >=, <=, =<, =>, =, !=, ~, ~*, !~, !~*.
|
|
16
|
+
* The "value" group matches either a double-quoted string or a single unquoted word
|
|
17
|
+
* that doesn't include whitespace or any of the operator characters.
|
|
18
|
+
*
|
|
19
|
+
* @returns {RegExp} - The regular expression pattern to identify search filter elements.
|
|
20
|
+
*/
|
|
21
|
+
export function getRegex() {
|
|
22
|
+
const column = '[\\w]+'
|
|
23
|
+
const operator = ':|>|<|>=|<=|=<|=>|=|!=|~|~\\*|!~|!~\\*'
|
|
24
|
+
const value = '("[^"]+"|[^\\s=:<>!~*]+)'
|
|
25
|
+
|
|
26
|
+
const pattern = `(?<group>((?<column>${column})\\s?(?<operator>${operator})\\s?)(?<value>${value}))`
|
|
27
|
+
|
|
28
|
+
return new RegExp(pattern, 'gm')
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Parses a search string and extracts filters based on a predefined regular expression pattern.
|
|
33
|
+
* Each filter captures column, operator, and value components. Any remaining text that
|
|
34
|
+
* does not fit the pattern is considered a general search term.
|
|
35
|
+
*
|
|
36
|
+
* @param {string} string - The search string to parse for filters.
|
|
37
|
+
* @returns {Object[]} - An array of filter objects each containing the column, operator, and value,
|
|
38
|
+
* plus an additional object for general search if there is remaining text.
|
|
39
|
+
*/
|
|
40
|
+
export function parseFilters(string) {
|
|
41
|
+
const results = []
|
|
42
|
+
const regex = getRegex()
|
|
43
|
+
|
|
44
|
+
// Split the string into an array of tokens
|
|
45
|
+
const tokens = string.matchAll(regex)
|
|
46
|
+
let search = string
|
|
47
|
+
|
|
48
|
+
// Iterate over the tokens
|
|
49
|
+
for (const token of tokens) {
|
|
50
|
+
// Extract the named groups from the token
|
|
51
|
+
const { group, column } = token.groups
|
|
52
|
+
let { operator, value } = token.groups
|
|
53
|
+
search = search.replace(group, '').trim()
|
|
54
|
+
|
|
55
|
+
operator = replaceOperators(operator)
|
|
56
|
+
value = processValue(value, operator)
|
|
57
|
+
|
|
58
|
+
if (column && value) results.push({ column, operator, value })
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (search.length > 0) {
|
|
62
|
+
results.push({
|
|
63
|
+
operator: '~*',
|
|
64
|
+
value: new RegExp(removeQuotes(search), 'i')
|
|
65
|
+
})
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return results
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Replaces various shorthand operators with their standardized equivalent for filtering.
|
|
73
|
+
*
|
|
74
|
+
* @param {string} operator - The operator to be replaced.
|
|
75
|
+
* @returns {string} - The replaced operator string.
|
|
76
|
+
*/
|
|
77
|
+
function replaceOperators(operator) {
|
|
78
|
+
return operator.replace(':', '~*').replace('=>', '>=').replace('=<', '<=')
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Processes the filter value provided, converting it into a format suitable for filtering.
|
|
83
|
+
* If the operator includes a tilde (~), the value is converted to a RegExp.
|
|
84
|
+
*
|
|
85
|
+
* @param {string|number} value - The value to be processed. It could be a string needing quote removal or a number that needs parsing.
|
|
86
|
+
* @param {string} operator - The operator that determines the type of processing to be applied to the value.
|
|
87
|
+
* @returns {string|number|RegExp} - The processed value, which may be a RegExp if the operator involves pattern matching.
|
|
88
|
+
*/
|
|
89
|
+
function processValue(value, operator) {
|
|
90
|
+
// If the value can be parsed as an integer, do so. Otherwise, strip quotes if present.
|
|
91
|
+
|
|
92
|
+
value = !isNaN(parseInt(value)) ? parseInt(value) : removeQuotes(value)
|
|
93
|
+
// If the operator includes a tilde (~), treat the value as a regular expression,
|
|
94
|
+
// case-insensitive if '*' is present, sensitive otherwise.
|
|
95
|
+
if (operator.includes('~')) {
|
|
96
|
+
value = operator.includes('*') ? new RegExp(value, 'i') : new RegExp(value)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return value
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Removes double quotes from the start and end of a string.
|
|
104
|
+
*
|
|
105
|
+
* @param {string} str - The input string from which the surrounding quotes should be removed.
|
|
106
|
+
* @returns {string} - The unquoted string.
|
|
107
|
+
*/
|
|
108
|
+
function removeQuotes(str) {
|
|
109
|
+
const quoteMatch = str.match(/^"([^"]+)"$/)
|
|
110
|
+
return quoteMatch ? quoteMatch[1] : str
|
|
111
|
+
}
|
package/src/renamer.js
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { omit, identity } from 'ramda'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
*
|
|
5
|
+
*
|
|
6
|
+
* @typedef {Object} OptionsToRenameKeys
|
|
7
|
+
* @property {string} [prefix] - The prefix to add to the key.
|
|
8
|
+
* @property {string} [suffix] - The suffix to add to the key.
|
|
9
|
+
* @property {string} [separator='_'] - The separator to use between the key and the prefix or suffix.
|
|
10
|
+
* @property {Array} [keys] - The keys to rename.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* A generator function that produces a function to rename keys.
|
|
15
|
+
*
|
|
16
|
+
* @param {OptionsToRenameKeys} options - Options to rename keys
|
|
17
|
+
* @returns {Object} - A renamer object.
|
|
18
|
+
*
|
|
19
|
+
* Example:
|
|
20
|
+
*
|
|
21
|
+
* renamer({ prefix: 'a', keys: ['a', 'b'] }).get()
|
|
22
|
+
* renamer({ prefix: 'a', separator: '-' }).get()
|
|
23
|
+
* renamer().setPrefix('a')
|
|
24
|
+
* .setSeparator('-')
|
|
25
|
+
* .setKeys(['a', 'b'])
|
|
26
|
+
* .get()
|
|
27
|
+
* renamer().setPrefix('a').get()
|
|
28
|
+
* renamer().setSuffix('b').get()
|
|
29
|
+
* renamer().setKeys(['a', 'b']).get()
|
|
30
|
+
*/
|
|
31
|
+
export function renamer(options = {}) {
|
|
32
|
+
const { prefix, suffix, separator = '_', keys = [] } = options
|
|
33
|
+
const rename = getRenamer(prefix, suffix, separator)
|
|
34
|
+
const renameObject = getObjectRenamer(rename, keys)
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
get: () => ({ rename, renameObject }),
|
|
38
|
+
setPrefix: (value) => renamer({ ...omit(['suffix'], options), prefix: value }),
|
|
39
|
+
setSuffix: (value) => renamer({ ...omit(['prefix'], options), suffix: value }),
|
|
40
|
+
setKeys: (value) => renamer({ ...options, keys: value }),
|
|
41
|
+
setSeparator: (value) => renamer({ ...options, separator: value })
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Generates a renamer function which adds a prefix or suffix to a string.
|
|
47
|
+
*
|
|
48
|
+
* @param {string} prefix - The prefix to add to the key.
|
|
49
|
+
* @param {string} suffix - The suffix to add to the key.
|
|
50
|
+
* @param {string} separator - The separator to use between the key and the prefix or suffix.
|
|
51
|
+
* @returns {Function} - A function that takes a string and adds a prefix or suffix.
|
|
52
|
+
*/
|
|
53
|
+
function getRenamer(prefix, suffix, separator) {
|
|
54
|
+
let rename = identity
|
|
55
|
+
|
|
56
|
+
if (prefix) {
|
|
57
|
+
rename = (x) => [prefix, x].join(separator)
|
|
58
|
+
} else if (suffix) {
|
|
59
|
+
rename = (x) => [x, suffix].join(separator)
|
|
60
|
+
}
|
|
61
|
+
return rename
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Returns a function that renames the keys of an object.
|
|
66
|
+
*
|
|
67
|
+
* @param {Function} rename - The function to rename the keys.
|
|
68
|
+
* @param {Array} keys - The keys to rename.
|
|
69
|
+
* @returns {Function} - A function that takes an object and renames the keys.
|
|
70
|
+
*/
|
|
71
|
+
function getObjectRenamer(rename, keys) {
|
|
72
|
+
if (rename === identity) return identity
|
|
73
|
+
if (keys.length > 0) {
|
|
74
|
+
const lookup = keys.reduce((acc, key) => ({ ...acc, [key]: rename(key) }), {})
|
|
75
|
+
return usingLookup(lookup)
|
|
76
|
+
}
|
|
77
|
+
return forObject(rename)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Renames the keys of an object using a lookup table.
|
|
82
|
+
*
|
|
83
|
+
* @param {Object} lookup - The lookup table.
|
|
84
|
+
* @returns {Function} - A function that takes an object and renames the keys using the lookup table.
|
|
85
|
+
*/
|
|
86
|
+
function usingLookup(lookup) {
|
|
87
|
+
return (row) => Object.keys(row).reduce((acc, key) => ({ ...acc, [lookup[key]]: row[key] }), {})
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Renames the keys of an object using a function.
|
|
92
|
+
*
|
|
93
|
+
* @param {Function} rename - The function to rename the keys.
|
|
94
|
+
* @returns {Function} - A function that takes an object and renames the keys using the function.
|
|
95
|
+
*/
|
|
96
|
+
function forObject(rename) {
|
|
97
|
+
return (row) => Object.keys(row).reduce((acc, key) => ({ ...acc, [rename(key)]: row[key] }), {})
|
|
98
|
+
}
|