@rokkit/data 1.0.0-next.98 → 1.0.1

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/src/join.js ADDED
@@ -0,0 +1,157 @@
1
+ /**
2
+ * Joins two datasets together based on a condition. Result includes all rows from the first
3
+ * dataset and matching rows from the second dataset. In case of multiple matches, all
4
+ * combinations are returned. When combining the rows, the columns from the first dataset take
5
+ * precedence.
6
+ *
7
+ * inner: only the rows that have a match in both datasets.
8
+ * outer: all rows from the first dataset and matching rows from the second dataset.
9
+ *
10
+ * @param {Array<Object>} first - The first dataset to join.
11
+ * @param {Array<Object>} second - The second dataset to join.
12
+ * @param {Function} condition - The condition to join the datasets on.
13
+ * @returns {Array<Object>} - An object with the inner and outer join result.
14
+ */
15
+ function joinData(first, second, condition) {
16
+ const inner = []
17
+ const outer = []
18
+
19
+ for (const f of first) {
20
+ let matched = false
21
+ for (const s of second) {
22
+ if (condition(f, s)) {
23
+ inner.push({ ...s, ...f })
24
+ matched = true
25
+ }
26
+ }
27
+ if (!matched) outer.push(f)
28
+ }
29
+ return { inner, outer }
30
+ }
31
+
32
+ /**
33
+ * Filters the results of the first dataset based on the condition using the second dataset.
34
+ *
35
+ * @param {Array<Object>} first - The first dataset to filter.
36
+ * @param {Array<Object>} second - The second dataset to filter by.
37
+ * @param {Function} condition - The condition to filter the first dataset by.
38
+ * @returns {Array<Object>} - The filtered dataset.
39
+ */
40
+ export function antiJoin(first, second, condition) {
41
+ return first.filter((f) => !second.find((s) => condition(f, s)))
42
+ }
43
+
44
+ /**
45
+ * Joins two datasets together based on a condition. Result includes all rows from the first
46
+ * dataset and matching rows from the second dataset. In case of multiple matches, all
47
+ * combinations are returned. When combining the rows, the columns from the first dataset take
48
+ * precedence.
49
+ *
50
+ * @param {Array<Object>} first - The first dataset to join.
51
+ * @param {Array<Object>} second - The second dataset to join.
52
+ * @param {Function} condition - The condition to join the datasets on.
53
+ * @returns {Array<Object>} - The joined dataset.
54
+ */
55
+ export function innerJoin(first, second, condition) {
56
+ const { inner } = joinData(first, second, condition)
57
+ return inner
58
+ }
59
+
60
+ /**
61
+ * Performs a left join on two datasets based on a condition.
62
+ *
63
+ * - Result includes all rows from the first dataset and matching rows from the second dataset.
64
+ * - In case of multiple matches, all combinations are returned.
65
+ * - When combining the rows, the columns from the first dataset take precedence.
66
+ * - If there is no match in the second dataset, only the row from the first dataset is returned.
67
+ *
68
+ * This can be used to perform a right join by swapping the datasets.
69
+ *
70
+ * @param {Array<Object>} first - The first dataset to join.
71
+ * @param {Array<Object>} second - The second dataset to join.
72
+ * @param {Function} condition - The condition to join the datasets on.
73
+ * @returns {Array<Object>} - The joined dataset.
74
+ */
75
+ export function leftJoin(first, second, condition) {
76
+ const { inner, outer } = joinData(first, second, condition)
77
+ return inner.concat(outer)
78
+ }
79
+
80
+ /**
81
+ * Performs a right join on two datasets based on a condition.
82
+ *
83
+ * - Result includes all rows from both datasets.
84
+ * - In case of multiple matches, all combinations are returned.
85
+ * - When combining the rows, the columns from the second dataset take precedence.
86
+ * - If there is no match in the first dataset, only the row from the second dataset is returned.
87
+ *
88
+ * @param {Array<Object>} first - The first dataset to join.
89
+ * @param {Array<Object>} second - The second dataset to join.
90
+ * @param {Function} condition - The condition to join the datasets on.
91
+ * @returns {Array<Object>} - The joined dataset.
92
+ */
93
+ export function rightJoin(first, second, condition) {
94
+ return leftJoin(second, first, condition)
95
+ }
96
+
97
+ /**
98
+ * Performs a full join on two datasets based on a condition.
99
+ *
100
+ * - Result includes all rows from both datasets.
101
+ * - In case of multiple matches, all combinations are returned.
102
+ * - When combining the rows, the columns from the second dataset take precedence.
103
+ * - If there is no match in the first dataset, only the row from the second dataset is returned.
104
+ *
105
+ * @param {Array<Object>} first - The first dataset to join.
106
+ * @param {Array<Object>} second - The second dataset to join.
107
+ * @param {Function} condition - The condition to join the datasets on.
108
+ * @returns {Array<Object>} - The joined dataset.
109
+ */
110
+ export function fullJoin(first, second, condition) {
111
+ const { inner, outer } = joinData(first, second, condition)
112
+ const rightOuter = antiJoin(second, first, condition)
113
+ return inner.concat(outer).concat(rightOuter)
114
+ }
115
+
116
+ /**
117
+ * Performs a cross join on two datasets. Result includes all combinations of rows from both datasets.
118
+ *
119
+ * @param {Array<Object>} first - The first dataset to join.
120
+ * @param {Array<Object>} second - The second dataset to join.
121
+ * @returns {Array<Object>} - The joined dataset.
122
+ */
123
+ export function crossJoin(first, second) {
124
+ return first.map((f) => second.map((s) => ({ ...f, ...s }))).flat()
125
+ }
126
+
127
+ /**
128
+ * Performs a semi join on two datasets based on a condition. Result includes all rows from the first
129
+ * dataset that have a match in the second dataset.
130
+ *
131
+ * @param {Array<Object>} first - The first dataset to join.
132
+ * @param {Array<Object>} second - The second dataset to join.
133
+ * @param {Function} condition - The condition to join the datasets on.
134
+ * @returns {Array<Object>} - The joined dataset.
135
+ */
136
+ export function semiJoin(first, second, condition) {
137
+ return first.filter((f) => second.find((s) => condition(f, s)))
138
+ }
139
+
140
+ /**
141
+ * Performs a nested join on two datasets based on a condition. Result includes all rows from the first
142
+ * dataset that have a match in the second dataset. The result is nested with the matching rows from the
143
+ * second dataset.
144
+ *
145
+ * @param {Array<Object>} first - The first dataset to join.
146
+ * @param {Array<Object>} second - The second dataset to join.
147
+ * @param {Function} condition - The condition to join the datasets on.
148
+ * @param {String} [key='children'] - The key to nest the matching rows under.
149
+ * @returns {Array<Object>} - The joined dataset.
150
+ */
151
+ export function nestedJoin(first, second, condition, key = 'children') {
152
+ const result = first.map((f) => ({
153
+ ...f,
154
+ [key]: second.filter((s) => condition(f, s))
155
+ }))
156
+ return result
157
+ }
package/src/metadata.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { defaultPathOptions } from './constants'
2
- import { getDeepScanSample } from './infer'
2
+ import { getSample } from './infer'
3
3
  import { typeOf } from './utils'
4
4
 
5
5
  /**
@@ -88,8 +88,8 @@ export function deriveColumnProperties(sample, options) {
88
88
  export function deriveColumnMetadata(data, options = {}) {
89
89
  const { metadata, deepScan = false } = options
90
90
  if (Array.isArray(metadata) && metadata.length > 0) return metadata
91
- if (data.length === 0) return []
92
- const sample = deepScan ? getDeepScanSample(data) : data[0]
91
+ // if (data.length === 0) return []
92
+ const sample = getSample(data, deepScan)
93
93
  return Object.entries(sample).map(([name, value]) => ({ name, type: typeOf(value) }))
94
94
  }
95
95
 
package/src/model.js CHANGED
@@ -1,7 +1,51 @@
1
- import { identity, clone as copy } from 'ramda'
2
- import { getDeepScanSample } from './infer'
1
+ const copy = (val) => structuredClone(val)
2
+ import { getSample } from './infer'
3
3
  import { typeOf } from './utils'
4
4
 
5
+ /**
6
+ * Derives a model from the input data
7
+ *
8
+ * @param {Array|Object} data - the data to derive the model from
9
+ * @param {Boolean} sparse - whether the data set contains sparse data
10
+ * @returns {Array} - the derived model
11
+ */
12
+ function deriveModel(data, sparse = false) {
13
+ const result = []
14
+ let item = data
15
+
16
+ if (Array.isArray(data)) item = sparse ? getSample(data) : data[0]
17
+
18
+ const kv = Object.entries(item)
19
+ kv.forEach(([key, value]) => {
20
+ result.push({
21
+ name: key,
22
+ type: typeOf(value)
23
+ })
24
+ })
25
+ return result
26
+ }
27
+
28
+ /**
29
+ * Merges two models, returning a new model
30
+ *
31
+ * @param {Array} first - the first model
32
+ * @param {Array} second - the second model
33
+ * @param {Boolean} override - whether to override the type of the first model
34
+ * @returns {Array} - the merged model
35
+ */
36
+ function mergeModels(first, second, override = false) {
37
+ second.forEach(({ name, type }) => {
38
+ const existing = first.find((x) => x.name === name)
39
+ if (existing) {
40
+ if (existing.type !== type) existing.mixedTypes = true
41
+ if (override) existing.type = type
42
+ } else {
43
+ first.push({ name, type })
44
+ }
45
+ })
46
+ return first
47
+ }
48
+
5
49
  /**
6
50
  * A model is a representation of a dataset
7
51
  * @returns an object with methods to manipulate the model
@@ -23,8 +67,8 @@ export function model() {
23
67
  * Renames the reference names using a renamer
24
68
  * @param {Function} rename - the renamer function
25
69
  */
26
- renameUsing: (rename = identity) => {
27
- if (rename !== identity) {
70
+ renameUsing: (rename) => {
71
+ if (rename) {
28
72
  data = data.map((x) => ({ ...x, name: rename(x.name) }))
29
73
  }
30
74
  return actions
@@ -54,47 +98,3 @@ export function model() {
54
98
  }
55
99
  return actions
56
100
  }
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 CHANGED
@@ -28,6 +28,48 @@ export function getRegex() {
28
28
  return new RegExp(pattern, 'gm')
29
29
  }
30
30
 
31
+ /**
32
+ * Replaces various shorthand operators with their standardized equivalent for filtering.
33
+ *
34
+ * @param {string} operator - The operator to be replaced.
35
+ * @returns {string} - The replaced operator string.
36
+ */
37
+ function replaceOperators(operator) {
38
+ return operator.replace(':', '~*').replace('=>', '>=').replace('=<', '<=')
39
+ }
40
+
41
+ /**
42
+ * Removes double quotes from the start and end of a string.
43
+ *
44
+ * @param {string} str - The input string from which the surrounding quotes should be removed.
45
+ * @returns {string} - The unquoted string.
46
+ */
47
+ function removeQuotes(str) {
48
+ const quoteMatch = str.match(/^"([^"]+)"$/)
49
+ return quoteMatch ? quoteMatch[1] : str
50
+ }
51
+
52
+ /**
53
+ * Processes the filter value provided, converting it into a format suitable for filtering.
54
+ * If the operator includes a tilde (~), the value is converted to a RegExp.
55
+ *
56
+ * @param {string|number} value - The value to be processed. It could be a string needing quote removal or a number that needs parsing.
57
+ * @param {string} operator - The operator that determines the type of processing to be applied to the value.
58
+ * @returns {string|number|RegExp} - The processed value, which may be a RegExp if the operator involves pattern matching.
59
+ */
60
+ function processValue(value, operator) {
61
+ // If the value can be parsed as an integer, do so. Otherwise, strip quotes if present.
62
+
63
+ value = !isNaN(parseInt(value)) ? parseInt(value) : removeQuotes(value)
64
+ // If the operator includes a tilde (~), treat the value as a regular expression,
65
+ // case-insensitive if '*' is present, sensitive otherwise.
66
+ if (operator.includes('~')) {
67
+ value = operator.includes('*') ? new RegExp(value, 'i') : new RegExp(value)
68
+ }
69
+
70
+ return value
71
+ }
72
+
31
73
  /**
32
74
  * Parses a search string and extracts filters based on a predefined regular expression pattern.
33
75
  * Each filter captures column, operator, and value components. Any remaining text that
@@ -67,45 +109,3 @@ export function parseFilters(string) {
67
109
 
68
110
  return results
69
111
  }
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
- }
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Shared pipeline execution logic used by both DataSet and ReactiveDataSet.
3
+ *
4
+ * Kept in a plain `.js` file so it can be imported from both regular modules
5
+ * and `.svelte.js` files without triggering rune compilation errors.
6
+ */
7
+
8
+ import { deriveSortableColumn } from './infer.js'
9
+
10
+ /**
11
+ * Sort an array in-place by one or more field specs.
12
+ * @param {any[]} data - Array to sort (mutated in place).
13
+ * @param {...string|Object|Array} fields - Sort field specs (same as DataSet.sortBy).
14
+ * @returns {any[]}
15
+ */
16
+ export function sortDataByFields(data, ...fields) {
17
+ const sorters = fields.map(deriveSortableColumn)
18
+ data.sort((a, b) => {
19
+ let result = 0
20
+ for (let i = 0; i < sorters.length && result === 0; i++) {
21
+ const { name, sorter } = sorters[i]
22
+ result = sorter(a[name], b[name])
23
+ }
24
+ return result
25
+ })
26
+ return data
27
+ }
28
+
29
+ /**
30
+ * Execute a list of pipeline ops in a single fused pass over `data`.
31
+ *
32
+ * Ops:
33
+ * { type: 'filter', fn } — keep rows where fn(row) is truthy
34
+ * { type: 'map', fn } — transform row via fn(row)
35
+ * { type: 'sort', fields } — sort the result by fields
36
+ *
37
+ * Consecutive filter/map ops are merged into one loop.
38
+ * Sort ops are applied after the linear pass.
39
+ *
40
+ * @param {any[]} data
41
+ * @param {Array<{type:string, fn?:Function, fields?:string[]}>} ops
42
+ * @returns {any[]}
43
+ */
44
+ function applyOpsToRow(row, linearOps) {
45
+ let cur = row
46
+ for (const op of linearOps) {
47
+ if (op.type === 'filter') {
48
+ if (!op.fn(cur)) return null
49
+ } else {
50
+ cur = op.fn(cur)
51
+ }
52
+ }
53
+ return cur
54
+ }
55
+
56
+ function applyLinearOps(data, linearOps) {
57
+ const out = []
58
+ for (const row of data) {
59
+ const result = applyOpsToRow(row, linearOps)
60
+ if (result !== null) out.push(result)
61
+ }
62
+ return out
63
+ }
64
+
65
+ function collectLinearOps(ops, i) {
66
+ const linearOps = []
67
+ while (i < ops.length && ops[i].type !== 'sort') linearOps.push(ops[i++])
68
+ return { linearOps, i }
69
+ }
70
+
71
+ function collectSortFields(ops, i) {
72
+ const sortFields = []
73
+ while (i < ops.length && ops[i].type === 'sort') sortFields.push(...ops[i++].fields)
74
+ return { sortFields, i }
75
+ }
76
+
77
+ export function executeFused(data, ops) {
78
+ if (ops.length === 0) return data
79
+
80
+ let result = data
81
+ let i = 0
82
+
83
+ while (i < ops.length) {
84
+ const linear = collectLinearOps(ops, i)
85
+ i = linear.i
86
+ if (linear.linearOps.length > 0) result = applyLinearOps(result, linear.linearOps)
87
+
88
+ const sorted = collectSortFields(ops, i)
89
+ i = sorted.i
90
+ if (sorted.sortFields.length > 0) {
91
+ result = [...result]
92
+ sortDataByFields(result, ...sorted.sortFields)
93
+ }
94
+ }
95
+
96
+ return result
97
+ }
package/src/renamer.js CHANGED
@@ -1,4 +1,11 @@
1
- import { omit, identity } from 'ramda'
1
+ const identity = (x) => x
2
+
3
+ function omit(keys, obj) {
4
+ const skip = new Set(keys)
5
+ const out = {}
6
+ for (const k of Object.keys(obj)) if (!skip.has(k)) out[k] = obj[k]
7
+ return out
8
+ }
2
9
 
3
10
  /**
4
11
  *
@@ -9,39 +16,25 @@ import { omit, identity } from 'ramda'
9
16
  * @property {string} [separator='_'] - The separator to use between the key and the prefix or suffix.
10
17
  * @property {Array} [keys] - The keys to rename.
11
18
  */
12
-
13
19
  /**
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
+ * Renames the keys of an object using a lookup table.
20
21
  *
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()
22
+ * @param {Object} lookup - The lookup table.
23
+ * @returns {Function} - A function that takes an object and renames the keys using the lookup table.
30
24
  */
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
- }
25
+ function usingLookup(lookup) {
26
+ return (row) => Object.keys(row).reduce((acc, key) => ({ ...acc, [lookup[key]]: row[key] }), {})
43
27
  }
44
28
 
29
+ /**
30
+ * Renames the keys of an object using a function.
31
+ *
32
+ * @param {Function} rename - The function to rename the keys.
33
+ * @returns {Function} - A function that takes an object and renames the keys using the function.
34
+ */
35
+ function forObject(rename) {
36
+ return (row) => Object.keys(row).reduce((acc, key) => ({ ...acc, [rename(key)]: row[key] }), {})
37
+ }
45
38
  /**
46
39
  * Generates a renamer function which adds a prefix or suffix to a string.
47
40
  *
@@ -78,21 +71,33 @@ function getObjectRenamer(rename, keys) {
78
71
  }
79
72
 
80
73
  /**
81
- * Renames the keys of an object using a lookup table.
74
+ * A generator function that produces a function to rename keys.
82
75
  *
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.
76
+ * @param {OptionsToRenameKeys} options - Options to rename keys
77
+ * @returns {Object} - A renamer object.
92
78
  *
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.
79
+ * Example:
80
+ *
81
+ * renamer({ prefix: 'a', keys: ['a', 'b'] }).get()
82
+ * renamer({ prefix: 'a', separator: '-' }).get()
83
+ * renamer().setPrefix('a')
84
+ * .setSeparator('-')
85
+ * .setKeys(['a', 'b'])
86
+ * .get()
87
+ * renamer().setPrefix('a').get()
88
+ * renamer().setSuffix('b').get()
89
+ * renamer().setKeys(['a', 'b']).get()
95
90
  */
96
- function forObject(rename) {
97
- return (row) => Object.keys(row).reduce((acc, key) => ({ ...acc, [rename(key)]: row[key] }), {})
91
+ export function renamer(options = {}) {
92
+ const { prefix, suffix, separator = '_', keys = [] } = options
93
+ const rename = getRenamer(prefix, suffix, separator)
94
+ const renameObject = getObjectRenamer(rename, keys)
95
+
96
+ return {
97
+ get: () => ({ rename, renameObject }),
98
+ setPrefix: (value) => renamer({ ...omit(['suffix'], options), prefix: value }),
99
+ setSuffix: (value) => renamer({ ...omit(['prefix'], options), suffix: value }),
100
+ setKeys: (value) => renamer({ ...options, keys: value }),
101
+ setSeparator: (value) => renamer({ ...options, separator: value })
102
+ }
98
103
  }