@rokkit/data 1.0.0-next.94

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/index.js ADDED
@@ -0,0 +1,6 @@
1
+ import './types'
2
+ export * from './utils'
3
+ export { renamer } from './renamer'
4
+ export { model } from './model'
5
+ export { dataset } from './dataset'
6
+ export { dataview } from './view'
package/src/infer.js ADDED
@@ -0,0 +1,263 @@
1
+ import { ascending, descending } from 'd3-array'
2
+ import { compact } from '@rokkit/core'
3
+ import { omit } from 'ramda'
4
+ import { createFormatter } from './formatter'
5
+ import { defaultViewOptions, defaultActionOrder } from './constants'
6
+ import { getType } from './utils'
7
+ import { deriveColumnProperties } from './metadata'
8
+
9
+ /**
10
+ * Derives the sorting information for a column based on the type of the input value.
11
+ *
12
+ * @param {import('./types').SortableColumn} value - The value that determines the sorting behavior of the column.
13
+ * @returns {import('./types').ColumnSorter} An object containing the 'name' of the column and a 'sorter' function.
14
+ * @throws {Error} Throws an error if the value type is not supported or the value format is invalid.
15
+ */
16
+ export function deriveSortableColumn(value) {
17
+ const type = getType(value)
18
+
19
+ const configGenerators = {
20
+ array: arrayConfigGenerator,
21
+ object: objectConfigGenerator,
22
+ string: stringConfigGenerator
23
+ }
24
+
25
+ const configGenerator = configGenerators[type]
26
+ if (!configGenerator) {
27
+ throw new Error(`Invalid value type: ${type}`)
28
+ }
29
+
30
+ return configGenerator(value)
31
+ }
32
+
33
+ /**
34
+ * Generates a sorting configuration for an array value.
35
+ *
36
+ * @param {Array} value - The array from which to derive a sorting configuration.
37
+ * @returns {Object} A validated sorting configuration based on the array.
38
+ */
39
+ function arrayConfigGenerator(value) {
40
+ arrayValidator(value)
41
+ return { name: value[0], sorter: value[1] ? ascending : descending }
42
+ }
43
+
44
+ /**
45
+ * Generates a sorting configuration for an object value.
46
+ *
47
+ * @param {Object} value - The object from which to derive a sorting configuration.
48
+ * @returns {Object} A validated sorting configuration based on the object.
49
+ */
50
+ function objectConfigGenerator(value) {
51
+ objectValidator(value)
52
+ return { name: value.name, sorter: value.sorter ?? ascending }
53
+ }
54
+
55
+ /**
56
+ * Generates a sorting configuration for a string value.
57
+ *
58
+ * @param {string} value - The string from which to derive a sorting configuration.
59
+ * @returns {Object} A sorting configuration using the string name and ascending sort function.
60
+ */
61
+ function stringConfigGenerator(value) {
62
+ return { name: value, sorter: ascending }
63
+ }
64
+
65
+ /**
66
+ * Validates an array to ensure it can be used to create a sorting configuration.
67
+ *
68
+ * @param {Array} value - The array to validate.
69
+ * @throws {Error} Throws an error if the array does not correspond to a [string, boolean] pair.
70
+ */
71
+ function arrayValidator(value) {
72
+ if (value.length !== 2 || typeof value[0] !== 'string' || typeof value[1] !== 'boolean') {
73
+ throw new Error('Array should be a pair of [string, boolean]')
74
+ }
75
+ }
76
+
77
+ /**
78
+ * Validates an object to ensure it has the necessary properties for creating a sorting configuration.
79
+ *
80
+ * @param {Object} value - The object to validate.
81
+ * @throws {Error} Throws an error if the object does not have a 'name' property or has an invalid 'sorter'.
82
+ */
83
+ function objectValidator(value) {
84
+ if (typeof value.name !== 'string') {
85
+ throw new Error('The property "name" is required and must be a string')
86
+ }
87
+ if (value.sorter !== undefined && typeof value.sorter !== 'function') {
88
+ throw new Error('Sorter should be a function')
89
+ }
90
+ }
91
+
92
+ /**
93
+ * Creates a deep scan sample object that contains a union of all keys from all
94
+ * objects in the array, pulling the values from the last item that contains the key.
95
+ *
96
+ * @param {Array<Object>} data - An array of objects to deep scan and collect sample values.
97
+ * @returns {Object} A sample object consolidating all unique keys with their latest non-null values.
98
+ */
99
+ export function getDeepScanSample(data) {
100
+ return data.reduce(
101
+ (acc, cur) => ({
102
+ ...acc,
103
+ ...compact(omit(Object.keys(acc), cur))
104
+ }),
105
+ {}
106
+ )
107
+ }
108
+ /**
109
+ * Derives a unique list of column names from an array of data objects.
110
+ * Each data object represents a row and this function compiles a list of all unique property keys
111
+ * present across these objects.
112
+ *
113
+ * @param {Object[]} data - The array of data objects to analyze for column names.
114
+ * @returns {string[]} - An array of strings representing the unique column names found across all data objects.
115
+ */
116
+ export function deriveColumns(data, options) {
117
+ if (data.length === 0) return []
118
+
119
+ const { scanMode } = { ...defaultViewOptions, ...options }
120
+ const sample = scanMode === 'fast' ? data[0] : getDeepScanSample(data)
121
+
122
+ return deriveColumnProperties(sample, options)
123
+ }
124
+
125
+ /**
126
+ * Derives the data types of columns based on the data provided in an array of objects.
127
+ * The function assumes that all values of a single field (column) are of the same type. It uses
128
+ * the first object in the data array as a reference for the field names. Each field is categorized
129
+ * as either 'string' or 'number' based on whether any of the field values in the data set are non-numeric.
130
+ *
131
+ * @param {Object[]} data - The array of data objects to assess for column data types.
132
+ * @returns {Object} - An object with keys being the data types ('string' & 'number') and values
133
+ * being arrays of field names that correspond to that data type.
134
+ */
135
+ export function deriveDataTypes(data) {
136
+ const dataTypes = Object.keys(data[0])
137
+ .map((field) => ({
138
+ field,
139
+ type: data.map((d) => d[field]).some(isNaN) ? 'string' : 'number'
140
+ }))
141
+ .reduce((acc, cur) => ({ ...acc, [cur.type]: [...acc[cur.type], cur.field] }), {
142
+ string: [],
143
+ number: []
144
+ })
145
+ return dataTypes
146
+ }
147
+
148
+ /**
149
+ * Infers the data type of a set of values by examining the values in the array.
150
+ * Assumes all values in the array should have the same type, unless they are 'null'.
151
+ * If multiple types are detected (excluding 'null'), the function will return 'mixed'.
152
+ * If all values are 'null', it returns 'null', and it returns 'undefined' for an empty array.
153
+ *
154
+ * @param {Array} values - An array of values for which the data type is to be inferred.
155
+ * @returns {string} - A string representing the inferred data type. Possible values are
156
+ * 'undefined', 'null', 'mixed', or any data type returned by the getType function.
157
+ */
158
+ export function inferDataType(values) {
159
+ if (values.length === 0) {
160
+ return 'undefined'
161
+ }
162
+
163
+ const nonNullValues = values.filter((value) => value !== null)
164
+
165
+ if (nonNullValues.length === 0) {
166
+ return 'null'
167
+ }
168
+
169
+ let type = getType(nonNullValues[0])
170
+
171
+ for (let i = 1; i < nonNullValues.length; i++) {
172
+ if (getType(nonNullValues[i]) !== type) {
173
+ type = 'mixed'
174
+ break
175
+ }
176
+ }
177
+
178
+ return type
179
+ }
180
+
181
+ /**
182
+ * Derives column metadata from the data to be used in a tabular component.
183
+ *
184
+ * @param {Array} data - The data to derive column metadata from.
185
+ * @returns {Array<import('./types').ColumnMetadata>} - The derived column metadata.
186
+ */
187
+ export function deriveMetadata(dataArray, options = {}) {
188
+ let { columns = [] } = options
189
+ if (dataArray.length === 0) return columns
190
+ const { actions, language } = { ...defaultViewOptions, ...options }
191
+
192
+ if (columns.length === 0) {
193
+ columns = deriveColumns(dataArray, options)
194
+ } else {
195
+ columns.forEach((col) => {
196
+ // if (col.sortable && !col.order) col.order = 'none'
197
+ if (!col.fields) col.fields = { text: col.name }
198
+ })
199
+ }
200
+ columns = addFormatters(columns, language)
201
+ if (actions.length > 0) columns = deriveActions(columns, actions)
202
+
203
+ return columns
204
+ }
205
+
206
+ /**
207
+ * Adds a formatter function to each column in the array based on the column type. If a formatter
208
+ * function is already present, it will not be overwritten. The formatter is created based on the
209
+ * override formatter if present, or the column type and language.
210
+ *
211
+ * @param {Array<import('./types').ColumnMetadata>} columns - The array of column metadata to add formatters to.
212
+ * @param {string} language - The language to use for formatting.
213
+ * @returns {Array<import('./types').ColumnMetadata>} - The array of column metadata with formatters added.
214
+ */
215
+ export function addFormatters(columns, language) {
216
+ return columns.map((column) => {
217
+ const formatter = column.formatter ?? column.type
218
+ if (typeof formatter !== 'function') {
219
+ const digits = column.digits ?? (['number', 'currency'].includes(column.type) ? 2 : 0)
220
+ column.formatter = createFormatter(formatter, language, digits)
221
+ }
222
+ return column
223
+ })
224
+ }
225
+
226
+ /**
227
+ * Converts an array of action names to an array of action objects.
228
+ *
229
+ * @param {Array<string|import('./types').Action>} input - The input to convert to actions.
230
+ * @returns {Array<import('./types').Action>} - The converted actions.
231
+ */
232
+ export function convertToActions(input) {
233
+ if (!Array.isArray(input)) throw new Error('Actions must be an array')
234
+ const actions = input
235
+ .map((action) => {
236
+ if (typeof action === 'string') {
237
+ return { name: action, label: action, order: defaultActionOrder[action] }
238
+ }
239
+ return { order: defaultActionOrder[action.name], ...action }
240
+ })
241
+ .sort((a, b) => a.order - b.order)
242
+ return actions
243
+ }
244
+ /**
245
+ * Adds actions to the column metadata. This function updates the columns array in place.
246
+ *
247
+ * @param {Array<import('./types').ColumnMetadata>} columns - The column metadata to update.
248
+ * @param {Array<string|import('./types').Action>} input - The actions to add to the column metadata.
249
+ * @returns {Array<import('./types').ColumnMetadata>} - The updated column metadata.
250
+ */
251
+ export function deriveActions(columns, input) {
252
+ // convert array of strings to array of objects
253
+ const actions = convertToActions(input)
254
+ const existing = columns.filter(({ action }) => action).map(({ action }) => action)
255
+
256
+ const actionColumns = actions
257
+ .filter(({ name }) => !existing.includes(name))
258
+ .map(({ name, label }) => ({
259
+ label,
260
+ action: name
261
+ }))
262
+ return [...actionColumns, ...columns]
263
+ }
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
+ }
@@ -0,0 +1,106 @@
1
+ import { defaultPathOptions } from './constants'
2
+ import { getDeepScanSample } from './infer'
3
+ import { getType } 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: getType(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: getType(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 { getType } 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 model = []
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
+ model.push({
74
+ name: key,
75
+ type: getType(value)
76
+ })
77
+ })
78
+ return model
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
+ }