@ditojs/utils 1.24.0 → 1.26.0

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": "@ditojs/utils",
3
- "version": "1.24.0",
3
+ "version": "1.26.0",
4
4
  "type": "module",
5
5
  "description": "Dito.js Utility Functions – Dito.js is a declarative and modern web framework, based on Objection.js, Koa.js and Vue.js",
6
6
  "repository": "https://github.com/ditojs/dito/tree/master/packages/utils",
@@ -32,7 +32,7 @@
32
32
  "typescript": "^4.9.5"
33
33
  },
34
34
  "types": "types",
35
- "gitHead": "0ca96ce0335509daa9ebbac278ae2d346015baad",
35
+ "gitHead": "198c9832c863003c8c8fcd147bf61a5f99a39c91",
36
36
  "scripts": {
37
37
  "types": "tsc --noEmit types/index.d.ts"
38
38
  },
@@ -1,4 +1,4 @@
1
- // Vitest Snapshot v1
1
+ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
2
 
3
3
  exports[`index > exports all symbols 1`] = `
4
4
  {
@@ -62,7 +62,9 @@ exports[`index > exports all symbols 1`] = `
62
62
  "isString": [Function],
63
63
  "isUrl": [Function],
64
64
  "labelize": [Function],
65
+ "mapConcurrently": [Function],
65
66
  "mapKeys": [Function],
67
+ "mapSequentially": [Function],
66
68
  "mapValues": [Function],
67
69
  "merge": [Function],
68
70
  "mixin": [Function],
@@ -1,12 +1,31 @@
1
1
  import { parseDataPath } from './parseDataPath.js'
2
2
  import { normalizeDataPath } from './normalizeDataPath.js'
3
3
 
4
+ const NOT_FOUND = Symbol('NOT_FOUND')
5
+
4
6
  export function getEntriesAtDataPath(
5
7
  obj,
6
8
  path,
7
9
  handleError = () => { throw new Error(`Invalid path: ${path}`) }
8
10
  ) {
9
11
  const parsedPath = parseDataPath(path)
12
+
13
+ const prefixEntries = (obj, index, getEntries) => {
14
+ const prefix = normalizeDataPath(parsedPath.slice(0, index))
15
+ return Object.entries(obj).reduce(
16
+ (map, [key, value]) => {
17
+ const pathKey = prefix ? `${prefix}/${key}` : key
18
+ for (const [path, val] of Object.entries(getEntries(value))) {
19
+ if (val !== NOT_FOUND) {
20
+ map[`${pathKey}/${path}`] = val
21
+ }
22
+ }
23
+ return map
24
+ },
25
+ {}
26
+ )
27
+ }
28
+
10
29
  let index = 0
11
30
  for (const part of parsedPath) {
12
31
  if (obj && typeof obj === 'object') {
@@ -16,18 +35,23 @@ export function getEntriesAtDataPath(
16
35
  continue
17
36
  } else if (part === '*') {
18
37
  // Support wildcards on arrays and objects
19
- const pathStart = normalizeDataPath(parsedPath.slice(0, index))
20
- const pathEnd = parsedPath.slice(index + 1)
21
- return Object.entries(obj).reduce(
22
- (map, [key, value]) => {
23
- const entries = getEntriesAtDataPath(value, pathEnd, handleError)
24
- const pathToKey = pathStart ? `${pathStart}/${key}` : key
25
- for (const [subPath, subVal] of Object.entries(entries)) {
26
- map[`${pathToKey}/${subPath}`] = subVal
27
- }
28
- return map
29
- },
30
- {}
38
+ const pathNested = parsedPath.slice(index + 1)
39
+ return prefixEntries(
40
+ obj,
41
+ index,
42
+ value => getEntriesAtDataPath(value, pathNested, handleError)
43
+ )
44
+ } else if (part === '**') {
45
+ // Support wildcards on arrays and objects
46
+ const pathNested = parsedPath.slice(index + 1)
47
+ const pathSelf = parsedPath.slice(index)
48
+ return prefixEntries(
49
+ obj,
50
+ index,
51
+ value => ({
52
+ ...getEntriesAtDataPath(value, pathNested, () => NOT_FOUND),
53
+ ...getEntriesAtDataPath(value, pathSelf, () => NOT_FOUND)
54
+ })
31
55
  )
32
56
  }
33
57
  }
@@ -52,7 +52,7 @@ describe('getEntriesAtDataPath()', () => {
52
52
  .toStrictEqual({})
53
53
  })
54
54
 
55
- it('should return wildcard matches', () => {
55
+ it('should return shallow wildcard matches', () => {
56
56
  const data = {
57
57
  object1: {
58
58
  array: [
@@ -141,4 +141,64 @@ describe('getEntriesAtDataPath()', () => {
141
141
  'object3/array/1/two/name': 'two.two'
142
142
  })
143
143
  })
144
+
145
+ it('should return deep wildcard matches', () => {
146
+ const data = {
147
+ object: {
148
+ array: [
149
+ { name: 'one' },
150
+ { name: 'two' },
151
+ {
152
+ object: {
153
+ one: { name: 'three' },
154
+ two: { name: 'four' }
155
+ },
156
+ array: [
157
+ { name: 'five' },
158
+ { name: 'six' },
159
+ {
160
+ object: {
161
+ one: { name: 'seven' },
162
+ two: { name: 'eight' }
163
+ }
164
+ }
165
+ ]
166
+ }
167
+ ]
168
+ }
169
+ }
170
+
171
+ const expected = {
172
+ 'object/array/0/name': 'one',
173
+ 'object/array/1/name': 'two',
174
+ 'object/array/2/object/one/name': 'three',
175
+ 'object/array/2/object/two/name': 'four',
176
+ 'object/array/2/array/0/name': 'five',
177
+ 'object/array/2/array/1/name': 'six',
178
+ 'object/array/2/array/2/object/one/name': 'seven',
179
+ 'object/array/2/array/2/object/two/name': 'eight'
180
+ }
181
+
182
+ let result = getEntriesAtDataPath(data, '**/name')
183
+ expect(result).toEqual(expected)
184
+ expect(Object.values(result)).toEqual(Object.values(expected))
185
+
186
+ result = getEntriesAtDataPath(data, 'object/**/name')
187
+ expect(result).toEqual(expected)
188
+ expect(Object.values(result)).toEqual(Object.values(expected))
189
+
190
+ expect(getEntriesAtDataPath(data, 'object/**/array/**/name')).toEqual({
191
+ 'object/array/2/array/0/name': 'five',
192
+ 'object/array/2/array/1/name': 'six',
193
+ 'object/array/2/array/2/object/one/name': 'seven',
194
+ 'object/array/2/array/2/object/two/name': 'eight'
195
+ })
196
+
197
+ expect(getEntriesAtDataPath(data, 'object/**/object/**/name')).toEqual({
198
+ 'object/array/2/object/one/name': 'three',
199
+ 'object/array/2/object/two/name': 'four',
200
+ 'object/array/2/array/2/object/one/name': 'seven',
201
+ 'object/array/2/array/2/object/two/name': 'eight'
202
+ })
203
+ })
144
204
  })
@@ -1,6 +1,8 @@
1
- import { isArray } from '../base/index.js'
1
+ import { isArray, asArray } from '../base/index.js'
2
2
  import { parseDataPath } from './parseDataPath.js'
3
3
 
4
+ const NOT_FOUND = Symbol('NOT_FOUND')
5
+
4
6
  export function getValueAtDataPath(
5
7
  obj,
6
8
  path,
@@ -16,13 +18,20 @@ export function getValueAtDataPath(
16
18
  continue
17
19
  } else if (part === '*') {
18
20
  // Support wildcards on arrays and objects
19
- const subPath = parsedPath.slice(index + 1)
21
+ const pathNested = parsedPath.slice(index + 1)
22
+ const values = isArray(obj) ? obj : Object.values(obj)
23
+ return values.map(
24
+ value => getValueAtDataPath(value, pathNested, handleError)
25
+ ).flat(1)
26
+ } else if (part === '**') {
27
+ // Support deep wildcards on arrays and objects
28
+ const pathNested = parsedPath.slice(index + 1)
29
+ const pathSelf = parsedPath.slice(index)
20
30
  const values = isArray(obj) ? obj : Object.values(obj)
21
- const results = values.map(
22
- value => getValueAtDataPath(value, subPath, handleError)
23
- )
24
- const flat = subPath.filter(part => part === '*').length
25
- return flat > 0 ? results.flat(flat) : results
31
+ return values.map(value => [
32
+ ...asArray(getValueAtDataPath(value, pathNested, () => NOT_FOUND)),
33
+ ...asArray(getValueAtDataPath(value, pathSelf, () => NOT_FOUND))
34
+ ]).flat(1).filter(value => value !== NOT_FOUND)
26
35
  }
27
36
  }
28
37
  return handleError?.(obj, part, index)
@@ -40,7 +40,7 @@ describe('getValueAtDataPath()', () => {
40
40
  .toBe('Error: unknown, 1')
41
41
  })
42
42
 
43
- it('should return wildcard matches', () => {
43
+ it('should return shallow wildcard matches', () => {
44
44
  const data = {
45
45
  object1: {
46
46
  array: [
@@ -73,5 +73,52 @@ describe('getValueAtDataPath()', () => {
73
73
  ])
74
74
  expect(getValueAtDataPath(data, '*/*/*/name'))
75
75
  .toEqual(['one', 'two', 'one', 'two'])
76
+
77
+ expect(getValueAtDataPath(data, '**/name'))
78
+ .toEqual(['one', 'two', 'one', 'two'])
79
+ })
80
+
81
+ it('should return deep wildcard matches', () => {
82
+ const data = {
83
+ object: {
84
+ object: {
85
+ array: [
86
+ { name: 'one' },
87
+ { name: 'two' },
88
+ {
89
+ object: {
90
+ one: { name: 'three' },
91
+ two: { name: 'four' }
92
+ },
93
+ array: [
94
+ { name: 'five' },
95
+ { name: 'six' },
96
+ {
97
+ object: {
98
+ one: { name: 'seven' },
99
+ two: { name: 'eight' }
100
+ }
101
+ }
102
+ ]
103
+ }
104
+ ]
105
+ }
106
+ }
107
+ }
108
+
109
+ expect(getValueAtDataPath(data, 'object/**/name'))
110
+ .toEqual(['one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight'])
111
+
112
+ expect(getValueAtDataPath(data, 'object/**/object/one/name'))
113
+ .toEqual(['three', 'seven'])
114
+
115
+ expect(getValueAtDataPath(data, 'object/**/object/two/name'))
116
+ .toEqual(['four', 'eight'])
117
+
118
+ expect(getValueAtDataPath(data, 'object/**/array/0/name'))
119
+ .toEqual(['one', 'five'])
120
+
121
+ expect(getValueAtDataPath(data, 'object/**/array/1/name'))
122
+ .toEqual(['two', 'six'])
76
123
  })
77
124
  })
@@ -31,7 +31,7 @@ describe('parseDataPath()', () => {
31
31
  .toStrictEqual(['..', 'object', 'value'])
32
32
  expect(parseDataPath('./object/value'))
33
33
  .toStrictEqual(['.', 'object', 'value'])
34
- // This happens when concanating a data path with another absolute one, an
34
+ // This happens when concatenating a data path with another absolute one, an
35
35
  // empty space will be interpreted as "start from scratch" when normalizing.
36
36
  expect(parseDataPath('//object/value'))
37
37
  .toStrictEqual(['', 'object', 'value'])
package/src/index.js CHANGED
@@ -4,6 +4,7 @@ export * from './array/index.js'
4
4
  export * from './object/index.js'
5
5
  export * from './string/index.js'
6
6
  export * from './timer/index.js'
7
+ export * from './promise/index.js'
7
8
  export * from './function/index.js'
8
9
  export * from './dataPath/index.js'
9
10
  export * from './html/index.js'
@@ -0,0 +1,2 @@
1
+ export * from './mapConcurrently.js'
2
+ export * from './mapSequentially.js'
@@ -0,0 +1,33 @@
1
+ export async function mapConcurrently(
2
+ input,
3
+ callback,
4
+ { concurrency = 0 } = {}
5
+ ) {
6
+ const array = await input
7
+ if (!concurrency) {
8
+ return Promise.all(array.map(callback))
9
+ }
10
+
11
+ const promises = []
12
+ const results = []
13
+ let index = 0
14
+
15
+ function next() {
16
+ if (index < array.length) {
17
+ const i = index++
18
+ return Promise.resolve(callback(array[i], i, array)).then(result => {
19
+ results[i] = result
20
+ return next()
21
+ })
22
+ }
23
+ return null
24
+ }
25
+
26
+ while (concurrency-- > 0) {
27
+ const promise = next()
28
+ if (!promise) break
29
+ promises.push(promise)
30
+ }
31
+
32
+ return Promise.all(promises).then(() => results)
33
+ }
@@ -0,0 +1,9 @@
1
+ export async function mapSequentially(input, callback) {
2
+ const array = await input
3
+ const results = []
4
+ let index = 0
5
+ for (const item of array) {
6
+ results.push(await callback(item, index++))
7
+ }
8
+ return results
9
+ }
@@ -19,7 +19,7 @@ describe('format()', () => {
19
19
  it('should use the en-US locale by default', () => {
20
20
  expect(format(integer)).toBe('123,456,789')
21
21
  expect(format(float)).toBe('123,456.789')
22
- expect(format(date)).toBe('June 9, 2012 at 10:45:30 PM')
22
+ expect(format(date)).toBe('June 9, 2012 at 10:45:30 PM')
23
23
  })
24
24
 
25
25
  it('should format numbers with different locale and default options', () => {
@@ -188,7 +188,7 @@ describe('format()', () => {
188
188
  ).toBe('May 24, 2016')
189
189
  expect(
190
190
  format('2016-05-24T15:54:14.876Z', { date: true, time: true })
191
- ).toBe('May 24, 2016 at 05:54:14 PM')
191
+ ).toBe('May 24, 2016 at 05:54:14 PM')
192
192
  expect(format('123456789', { number: true })).toBe('123,456,789')
193
193
  expect(format('123456.789', { number: true })).toBe('123,456.789')
194
194
  expect(format('Hello World')).toBe('Hello World')
@@ -8,8 +8,8 @@ describe('formatDate()', () => {
8
8
  })
9
9
 
10
10
  it('should use the en-US locale by default', () => {
11
- expect(formatDate(date)).toBe('June 9, 2012 at 10:45:30 PM')
12
- expect(formatDate(date, {})).toBe('June 9, 2012 at 10:45:30 PM')
11
+ expect(formatDate(date)).toBe('June 9, 2012 at 10:45:30 PM')
12
+ expect(formatDate(date, {})).toBe('June 9, 2012 at 10:45:30 PM')
13
13
  })
14
14
 
15
15
  it('should format dates with different locale and default options', () => {