@qezor/structkit 1.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Abdul Ahad
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,100 @@
1
+ # @qezor/structkit
2
+
3
+ `@qezor/structkit` is a focused data-manipulation toolkit for:
4
+ - arrays
5
+ - objects
6
+ - path access
7
+ - iterative deep equality
8
+ - iterative deep merge and clone
9
+
10
+ It is built to cover the high-use utility surface people often reach for in lodash, without turning into a bag of random helpers.
11
+
12
+ ## Install
13
+
14
+ ```bash
15
+ npm install @qezor/structkit
16
+ ```
17
+
18
+ ## What It Prioritizes
19
+
20
+ - iterative internals for deep operations
21
+ - no callback-style APIs
22
+ - no recursion-heavy deep walkers
23
+ - predictable helpers for arrays, objects, and paths
24
+ - a shared Qezor Bridge contract so other libraries can detect capabilities cleanly
25
+
26
+ ## Main Helpers
27
+
28
+ - `toArray(value)`
29
+ - `chunk(array, size)`
30
+ - `compact(array)`
31
+ - `uniq(array)`
32
+ - `uniqBy(array, iteratee)`
33
+ - `difference(array, values)`
34
+ - `intersection(array, values)`
35
+ - `partition(array, iteratee)`
36
+ - `groupBy(array, iteratee)`
37
+ - `countBy(array, iteratee)`
38
+ - `keyBy(array, iteratee)`
39
+ - `sortBy(array, iteratee)`
40
+ - `flatMap(array, iteratee)`
41
+ - `sumBy(array, iteratee)`
42
+ - `tokenizePath(path)`
43
+ - `get(value, path, defaultValue)`
44
+ - `has(value, path)`
45
+ - `set(value, path, nextValue)`
46
+ - `unset(value, path)`
47
+ - `pick(value, paths)`
48
+ - `omit(value, paths)`
49
+ - `mapValues(object, iteratee)`
50
+ - `defaults(target, ...sources)`
51
+ - `assignDefined(target, ...sources)`
52
+ - `cloneShallow(value)`
53
+ - `cloneDeep(value)`
54
+ - `mergeDeep(target, ...sources)`
55
+ - `isEqual(left, right)`
56
+
57
+ ## Usage
58
+
59
+ ```js
60
+ const {
61
+ groupBy,
62
+ get,
63
+ set,
64
+ mergeDeep,
65
+ isEqual,
66
+ } = require("@qezor/structkit")
67
+
68
+ const users = [
69
+ { id: "u_1", role: "buyer" },
70
+ { id: "u_2", role: "buyer" },
71
+ { id: "u_3", role: "seller" },
72
+ ]
73
+
74
+ const grouped = groupBy(users, "role")
75
+ const state = {}
76
+
77
+ set(state, "queue.region.city", "Pune")
78
+
79
+ const region = get(state, "queue.region.city")
80
+ const merged = mergeDeep({}, { demand: { joinedUnits: 12 } }, { demand: { buyers: 3 } })
81
+ const same = isEqual(merged, { demand: { joinedUnits: 12, buyers: 3 } })
82
+ ```
83
+
84
+ ## Qezor Bridge
85
+
86
+ The root export carries bridge metadata through `Symbol.for("qezor.bridge")`.
87
+
88
+ That lets other Qezor libraries detect things like:
89
+ - `structure`
90
+ - `structure:array`
91
+ - `structure:object`
92
+ - `structure:path`
93
+ - `structure:compare`
94
+
95
+ ## Notes
96
+
97
+ - deep clone, deep merge, and deep equality use iterative walkers instead of recursion
98
+ - arrays are replaced during `mergeDeep()` instead of element-wise merged
99
+ - `omit()` works on cloned data, so the original input is left untouched
100
+ - `set()` mutates the provided target, like many performance-oriented utility helpers do
package/bridge.d.ts ADDED
@@ -0,0 +1,14 @@
1
+ export interface QezorBridge {
2
+ name: string
3
+ kind: string
4
+ version: string
5
+ capabilities: string[]
6
+ adapters: string[]
7
+ dependencies: string[]
8
+ }
9
+
10
+ export const QEZOR_BRIDGE: unique symbol
11
+ export function createQezorBridge(input?: Partial<QezorBridge>): QezorBridge
12
+ export function attachQezorBridge<T extends object>(target: T, input?: Partial<QezorBridge>): T
13
+ export function readQezorBridge(target: unknown): QezorBridge | null
14
+ export function hasQezorCapability(target: unknown, capability: string): boolean
package/bridge.js ADDED
@@ -0,0 +1,3 @@
1
+ "use strict"
2
+
3
+ module.exports = require("./lib/bridge.js")
package/bridge.mjs ADDED
@@ -0,0 +1,11 @@
1
+ import mod from "./bridge.js"
2
+
3
+ export const {
4
+ QEZOR_BRIDGE,
5
+ createQezorBridge,
6
+ attachQezorBridge,
7
+ readQezorBridge,
8
+ hasQezorCapability,
9
+ } = mod
10
+
11
+ export default mod
package/index.d.ts ADDED
@@ -0,0 +1,53 @@
1
+ export type PathToken = string | number
2
+ export type PathInput = string | number | readonly PathToken[]
3
+ export type Iteratee<T = unknown, R = unknown> =
4
+ | ((value: T, indexOrKey?: number | string, collection?: unknown) => R)
5
+ | PathInput
6
+ | null
7
+ | undefined
8
+
9
+ export interface QezorBridge {
10
+ name: string
11
+ kind: string
12
+ version: string
13
+ capabilities: string[]
14
+ adapters: string[]
15
+ dependencies: string[]
16
+ }
17
+
18
+ export const QEZOR_BRIDGE: unique symbol
19
+ export function createQezorBridge(input?: Partial<QezorBridge>): QezorBridge
20
+ export function attachQezorBridge<T extends object>(target: T, input?: Partial<QezorBridge>): T
21
+ export function readQezorBridge(target: unknown): QezorBridge | null
22
+ export function hasQezorCapability(target: unknown, capability: string): boolean
23
+
24
+ export function toArray<T = unknown>(value: unknown): T[]
25
+ export function chunk<T = unknown>(value: unknown, size?: number): T[][]
26
+ export function compact<T = unknown>(value: unknown): T[]
27
+ export function uniq<T = unknown>(value: unknown): T[]
28
+ export function uniqBy<T = unknown>(value: unknown, iteratee?: Iteratee<T, unknown>): T[]
29
+ export function difference<T = unknown>(value: unknown, values: unknown): T[]
30
+ export function intersection<T = unknown>(value: unknown, values: unknown): T[]
31
+ export function partition<T = unknown>(value: unknown, iteratee?: Iteratee<T, unknown>): [T[], T[]]
32
+ export function groupBy<T = unknown>(value: unknown, iteratee?: Iteratee<T, unknown>): Record<string, T[]>
33
+ export function countBy<T = unknown>(value: unknown, iteratee?: Iteratee<T, unknown>): Record<string, number>
34
+ export function keyBy<T = unknown>(value: unknown, iteratee?: Iteratee<T, unknown>): Record<string, T>
35
+ export function sortBy<T = unknown>(value: unknown, iteratee?: Iteratee<T, unknown>): T[]
36
+ export function flatMap<T = unknown, R = unknown>(value: unknown, iteratee?: Iteratee<T, R | R[]>): R[]
37
+ export function sumBy<T = unknown>(value: unknown, iteratee?: Iteratee<T, number>): number
38
+
39
+ export function tokenizePath(path: PathInput): PathToken[]
40
+ export function get<T = unknown>(value: unknown, path: PathInput, defaultValue?: T): T
41
+ export function has(value: unknown, path: PathInput): boolean
42
+ export function set<T extends object>(target: T, path: PathInput, nextValue: unknown): T
43
+ export function unset(target: object, path: PathInput): boolean
44
+ export function pick(value: unknown, paths: PathInput | PathInput[]): Record<string, unknown>
45
+ export function omit<T = unknown>(value: T, paths: PathInput | PathInput[]): T
46
+ export function mapValues<T extends Record<string, unknown>, R = unknown>(value: T, iteratee?: Iteratee<unknown, R>): Record<string, R>
47
+ export function defaults<T extends Record<string, unknown>>(target: T, ...sources: Array<Record<string, unknown>>): T
48
+ export function assignDefined<T extends Record<string, unknown>>(target: T, ...sources: Array<Record<string, unknown>>): T
49
+ export function cloneShallow<T = unknown>(value: T): T
50
+ export function cloneDeep<T = unknown>(value: T): T
51
+ export function mergeDeep<T extends Record<string, unknown>>(target: T, ...sources: Array<Record<string, unknown>>): T
52
+
53
+ export function isEqual(left: unknown, right: unknown): boolean
package/index.js ADDED
@@ -0,0 +1,30 @@
1
+ "use strict"
2
+
3
+ const bridge = require("./lib/bridge.js")
4
+ const array = require("./lib/array.js")
5
+ const object = require("./lib/object.js")
6
+ const compare = require("./lib/compare.js")
7
+
8
+ module.exports = bridge.attachQezorBridge({
9
+ ...array,
10
+ ...object,
11
+ ...compare,
12
+ QEZOR_BRIDGE: bridge.QEZOR_BRIDGE,
13
+ createQezorBridge: bridge.createQezorBridge,
14
+ attachQezorBridge: bridge.attachQezorBridge,
15
+ readQezorBridge: bridge.readQezorBridge,
16
+ hasQezorCapability: bridge.hasQezorCapability,
17
+ }, {
18
+ name: "@qezor/structkit",
19
+ kind: "library",
20
+ version: "1.0.0",
21
+ capabilities: [
22
+ "bridge",
23
+ "structure",
24
+ "structure:array",
25
+ "structure:object",
26
+ "structure:path",
27
+ "structure:compare",
28
+ ],
29
+ adapters: ["plain-data"],
30
+ })
package/index.mjs ADDED
@@ -0,0 +1,39 @@
1
+ import mod from "./index.js"
2
+
3
+ export const {
4
+ toArray,
5
+ chunk,
6
+ compact,
7
+ uniq,
8
+ uniqBy,
9
+ difference,
10
+ intersection,
11
+ partition,
12
+ groupBy,
13
+ countBy,
14
+ keyBy,
15
+ sortBy,
16
+ flatMap,
17
+ sumBy,
18
+ tokenizePath,
19
+ get,
20
+ has,
21
+ set,
22
+ unset,
23
+ pick,
24
+ omit,
25
+ mapValues,
26
+ defaults,
27
+ assignDefined,
28
+ cloneShallow,
29
+ cloneDeep,
30
+ mergeDeep,
31
+ isEqual,
32
+ QEZOR_BRIDGE,
33
+ createQezorBridge,
34
+ attachQezorBridge,
35
+ readQezorBridge,
36
+ hasQezorCapability,
37
+ } = mod
38
+
39
+ export default mod
package/lib/array.js ADDED
@@ -0,0 +1,188 @@
1
+ "use strict"
2
+
3
+ const { createIteratee } = require("./shared.js")
4
+
5
+ function toArray(value) {
6
+ if (value == null) return []
7
+ if (Array.isArray(value)) return value.slice()
8
+ if (typeof value === "string") return Array.from(value)
9
+ if (typeof value[Symbol.iterator] === "function") return Array.from(value)
10
+ if (typeof value === "object") return Object.values(value)
11
+ return [value]
12
+ }
13
+
14
+ function chunk(value, size = 1) {
15
+ const list = toArray(value)
16
+ const width = Math.max(1, Number.parseInt(String(size), 10) || 1)
17
+ const output = []
18
+ for (let index = 0; index < list.length; index += width) {
19
+ output.push(list.slice(index, index + width))
20
+ }
21
+ return output
22
+ }
23
+
24
+ function compact(value) {
25
+ return toArray(value).filter(Boolean)
26
+ }
27
+
28
+ function uniq(value) {
29
+ return [...new Set(toArray(value))]
30
+ }
31
+
32
+ function uniqBy(value, iteratee) {
33
+ const list = toArray(value)
34
+ const resolve = createIteratee(iteratee)
35
+ const seen = new Set()
36
+ const output = []
37
+
38
+ for (let index = 0; index < list.length; index += 1) {
39
+ const item = list[index]
40
+ const marker = resolve(item, index, list)
41
+ if (seen.has(marker)) continue
42
+ seen.add(marker)
43
+ output.push(item)
44
+ }
45
+
46
+ return output
47
+ }
48
+
49
+ function difference(value, values) {
50
+ const blocked = new Set(toArray(values))
51
+ return toArray(value).filter((item) => !blocked.has(item))
52
+ }
53
+
54
+ function intersection(value, values) {
55
+ const allowed = new Set(toArray(values))
56
+ return toArray(value).filter((item) => allowed.has(item))
57
+ }
58
+
59
+ function partition(value, iteratee) {
60
+ const list = toArray(value)
61
+ const resolve = createIteratee(iteratee)
62
+ const truthy = []
63
+ const falsy = []
64
+
65
+ for (let index = 0; index < list.length; index += 1) {
66
+ const item = list[index]
67
+ if (resolve(item, index, list)) {
68
+ truthy.push(item)
69
+ } else {
70
+ falsy.push(item)
71
+ }
72
+ }
73
+
74
+ return [truthy, falsy]
75
+ }
76
+
77
+ function groupBy(value, iteratee) {
78
+ const list = toArray(value)
79
+ const resolve = createIteratee(iteratee)
80
+ const output = {}
81
+
82
+ for (let index = 0; index < list.length; index += 1) {
83
+ const item = list[index]
84
+ const key = String(resolve(item, index, list))
85
+ if (!Object.prototype.hasOwnProperty.call(output, key)) output[key] = []
86
+ output[key].push(item)
87
+ }
88
+
89
+ return output
90
+ }
91
+
92
+ function countBy(value, iteratee) {
93
+ const list = toArray(value)
94
+ const resolve = createIteratee(iteratee)
95
+ const output = {}
96
+
97
+ for (let index = 0; index < list.length; index += 1) {
98
+ const item = list[index]
99
+ const key = String(resolve(item, index, list))
100
+ output[key] = (output[key] || 0) + 1
101
+ }
102
+
103
+ return output
104
+ }
105
+
106
+ function keyBy(value, iteratee) {
107
+ const list = toArray(value)
108
+ const resolve = createIteratee(iteratee)
109
+ const output = {}
110
+
111
+ for (let index = 0; index < list.length; index += 1) {
112
+ const item = list[index]
113
+ output[String(resolve(item, index, list))] = item
114
+ }
115
+
116
+ return output
117
+ }
118
+
119
+ function sortBy(value, iteratee) {
120
+ const list = toArray(value)
121
+ const resolve = createIteratee(iteratee)
122
+ const ranked = []
123
+
124
+ for (let index = 0; index < list.length; index += 1) {
125
+ ranked.push({
126
+ item: list[index],
127
+ index,
128
+ rank: resolve(list[index], index, list),
129
+ })
130
+ }
131
+
132
+ ranked.sort((left, right) => {
133
+ if (left.rank < right.rank) return -1
134
+ if (left.rank > right.rank) return 1
135
+ return left.index - right.index
136
+ })
137
+
138
+ return ranked.map((entry) => entry.item)
139
+ }
140
+
141
+ function flatMap(value, iteratee) {
142
+ const list = toArray(value)
143
+ const resolve = createIteratee(iteratee)
144
+ const output = []
145
+
146
+ for (let index = 0; index < list.length; index += 1) {
147
+ const result = resolve(list[index], index, list)
148
+ if (Array.isArray(result)) {
149
+ for (let inner = 0; inner < result.length; inner += 1) {
150
+ output.push(result[inner])
151
+ }
152
+ } else {
153
+ output.push(result)
154
+ }
155
+ }
156
+
157
+ return output
158
+ }
159
+
160
+ function sumBy(value, iteratee) {
161
+ const list = toArray(value)
162
+ const resolve = createIteratee(iteratee)
163
+ let total = 0
164
+
165
+ for (let index = 0; index < list.length; index += 1) {
166
+ const number = Number(resolve(list[index], index, list))
167
+ if (Number.isFinite(number)) total += number
168
+ }
169
+
170
+ return total
171
+ }
172
+
173
+ module.exports = {
174
+ toArray,
175
+ chunk,
176
+ compact,
177
+ uniq,
178
+ uniqBy,
179
+ difference,
180
+ intersection,
181
+ partition,
182
+ groupBy,
183
+ countBy,
184
+ keyBy,
185
+ sortBy,
186
+ flatMap,
187
+ sumBy,
188
+ }
package/lib/bridge.js ADDED
@@ -0,0 +1,49 @@
1
+ "use strict"
2
+
3
+ const QEZOR_BRIDGE = Symbol.for("qezor.bridge")
4
+
5
+ function normalizeList(values) {
6
+ if (!Array.isArray(values)) return []
7
+ return [...new Set(values.map((value) => String(value).trim()).filter(Boolean))].sort()
8
+ }
9
+
10
+ function createQezorBridge(input = {}) {
11
+ return {
12
+ name: input.name || "unknown",
13
+ kind: input.kind || "library",
14
+ version: input.version || "0.0.0",
15
+ capabilities: normalizeList(input.capabilities),
16
+ adapters: normalizeList(input.adapters),
17
+ dependencies: normalizeList(input.dependencies),
18
+ }
19
+ }
20
+
21
+ function attachQezorBridge(target, input = {}) {
22
+ const bridge = createQezorBridge(input)
23
+ Object.defineProperty(target, QEZOR_BRIDGE, {
24
+ enumerable: false,
25
+ configurable: true,
26
+ writable: false,
27
+ value: bridge,
28
+ })
29
+ return target
30
+ }
31
+
32
+ function readQezorBridge(target) {
33
+ if (!target || (typeof target !== "object" && typeof target !== "function")) return null
34
+ return target[QEZOR_BRIDGE] || null
35
+ }
36
+
37
+ function hasQezorCapability(target, capability) {
38
+ const bridge = readQezorBridge(target)
39
+ if (!bridge) return false
40
+ return bridge.capabilities.includes(String(capability))
41
+ }
42
+
43
+ module.exports = {
44
+ QEZOR_BRIDGE,
45
+ createQezorBridge,
46
+ attachQezorBridge,
47
+ readQezorBridge,
48
+ hasQezorCapability,
49
+ }
package/lib/compare.js ADDED
@@ -0,0 +1,94 @@
1
+ "use strict"
2
+
3
+ const { isObjectLike, isTypedArray } = require("./shared.js")
4
+
5
+ function isEqual(left, right) {
6
+ if (Object.is(left, right)) return true
7
+
8
+ const stack = [{ left, right }]
9
+ const seen = new WeakMap()
10
+
11
+ while (stack.length) {
12
+ const frame = stack.pop()
13
+ const a = frame.left
14
+ const b = frame.right
15
+
16
+ if (Object.is(a, b)) continue
17
+
18
+ const aIsObject = isObjectLike(a)
19
+ const bIsObject = isObjectLike(b)
20
+
21
+ if (!aIsObject || !bIsObject) return false
22
+
23
+ const leftTag = Object.prototype.toString.call(a)
24
+ const rightTag = Object.prototype.toString.call(b)
25
+ if (leftTag !== rightTag) return false
26
+
27
+ let seenRights = seen.get(a)
28
+ if (seenRights && seenRights.has(b)) continue
29
+ if (!seenRights) {
30
+ seenRights = new WeakSet()
31
+ seen.set(a, seenRights)
32
+ }
33
+ seenRights.add(b)
34
+
35
+ if (Array.isArray(a)) {
36
+ if (!Array.isArray(b) || a.length !== b.length) return false
37
+ for (let index = 0; index < a.length; index += 1) {
38
+ stack.push({ left: a[index], right: b[index] })
39
+ }
40
+ continue
41
+ }
42
+
43
+ if (a instanceof Date) {
44
+ if (a.getTime() !== b.getTime()) return false
45
+ continue
46
+ }
47
+
48
+ if (a instanceof RegExp) {
49
+ if (a.source !== b.source || a.flags !== b.flags) return false
50
+ continue
51
+ }
52
+
53
+ if (isTypedArray(a)) {
54
+ if (!isTypedArray(b) || a.length !== b.length) return false
55
+ for (let index = 0; index < a.length; index += 1) {
56
+ if (a[index] !== b[index]) return false
57
+ }
58
+ continue
59
+ }
60
+
61
+ if (a instanceof Map) {
62
+ if (!(b instanceof Map) || a.size !== b.size) return false
63
+ for (const [key, value] of a.entries()) {
64
+ if (!b.has(key)) return false
65
+ stack.push({ left: value, right: b.get(key) })
66
+ }
67
+ continue
68
+ }
69
+
70
+ if (a instanceof Set) {
71
+ if (!(b instanceof Set) || a.size !== b.size) return false
72
+ for (const value of a.values()) {
73
+ if (!b.has(value)) return false
74
+ }
75
+ continue
76
+ }
77
+
78
+ const keysA = Object.keys(a)
79
+ const keysB = Object.keys(b)
80
+ if (keysA.length !== keysB.length) return false
81
+
82
+ for (let index = 0; index < keysA.length; index += 1) {
83
+ const key = keysA[index]
84
+ if (!Object.prototype.hasOwnProperty.call(b, key)) return false
85
+ stack.push({ left: a[key], right: b[key] })
86
+ }
87
+ }
88
+
89
+ return true
90
+ }
91
+
92
+ module.exports = {
93
+ isEqual,
94
+ }
package/lib/object.js ADDED
@@ -0,0 +1,202 @@
1
+ "use strict"
2
+
3
+ const {
4
+ isObjectLike,
5
+ isPlainObject,
6
+ tokenizePath,
7
+ getValueByPath,
8
+ createIteratee,
9
+ cloneShallow,
10
+ cloneDeep,
11
+ } = require("./shared.js")
12
+
13
+ function get(value, path, defaultValue) {
14
+ const resolved = getValueByPath(value, path)
15
+ return resolved === undefined ? defaultValue : resolved
16
+ }
17
+
18
+ function has(value, path) {
19
+ const tokens = tokenizePath(path)
20
+ if (!tokens.length) return false
21
+
22
+ let current = value
23
+ for (let index = 0; index < tokens.length; index += 1) {
24
+ if (!isObjectLike(current) && !Array.isArray(current)) return false
25
+ if (!Object.prototype.hasOwnProperty.call(current, tokens[index])) return false
26
+ current = current[tokens[index]]
27
+ }
28
+
29
+ return true
30
+ }
31
+
32
+ function set(target, path, nextValue) {
33
+ if (!isObjectLike(target) && !Array.isArray(target)) return target
34
+
35
+ const tokens = tokenizePath(path)
36
+ if (!tokens.length) return target
37
+
38
+ let current = target
39
+ for (let index = 0; index < tokens.length - 1; index += 1) {
40
+ const token = tokens[index]
41
+ const nextToken = tokens[index + 1]
42
+ const existing = current[token]
43
+
44
+ if (!isObjectLike(existing) && !Array.isArray(existing)) {
45
+ current[token] = typeof nextToken === "number" ? [] : {}
46
+ }
47
+
48
+ current = current[token]
49
+ }
50
+
51
+ current[tokens[tokens.length - 1]] = nextValue
52
+ return target
53
+ }
54
+
55
+ function unset(target, path) {
56
+ if (!isObjectLike(target) && !Array.isArray(target)) return false
57
+
58
+ const tokens = tokenizePath(path)
59
+ if (!tokens.length) return false
60
+
61
+ let current = target
62
+ for (let index = 0; index < tokens.length - 1; index += 1) {
63
+ if (!isObjectLike(current) && !Array.isArray(current)) return false
64
+ if (!Object.prototype.hasOwnProperty.call(current, tokens[index])) return false
65
+ current = current[tokens[index]]
66
+ }
67
+
68
+ const last = tokens[tokens.length - 1]
69
+ if (!isObjectLike(current) && !Array.isArray(current)) return false
70
+ if (!Object.prototype.hasOwnProperty.call(current, last)) return false
71
+
72
+ if (Array.isArray(current) && typeof last === "number") {
73
+ current.splice(last, 1)
74
+ } else {
75
+ delete current[last]
76
+ }
77
+
78
+ return true
79
+ }
80
+
81
+ function pick(value, paths) {
82
+ const selectedPaths = Array.isArray(paths) ? paths : [paths]
83
+ const output = {}
84
+
85
+ for (const path of selectedPaths) {
86
+ if (!has(value, path)) continue
87
+ set(output, path, get(value, path))
88
+ }
89
+
90
+ return output
91
+ }
92
+
93
+ function omit(value, paths) {
94
+ const output = cloneDeep(value)
95
+ const omittedPaths = Array.isArray(paths) ? paths : [paths]
96
+
97
+ for (const path of omittedPaths) {
98
+ unset(output, path)
99
+ }
100
+
101
+ return output
102
+ }
103
+
104
+ function mapValues(value, iteratee) {
105
+ if (!isPlainObject(value)) return {}
106
+ const resolve = createIteratee(iteratee)
107
+ const output = {}
108
+ const keys = Object.keys(value)
109
+
110
+ for (let index = 0; index < keys.length; index += 1) {
111
+ const key = keys[index]
112
+ output[key] = resolve(value[key], key, value)
113
+ }
114
+
115
+ return output
116
+ }
117
+
118
+ function defaults(target, ...sources) {
119
+ const output = isPlainObject(target) ? target : {}
120
+
121
+ for (const source of sources) {
122
+ if (!isPlainObject(source)) continue
123
+ for (const key of Object.keys(source)) {
124
+ if (output[key] === undefined) output[key] = source[key]
125
+ }
126
+ }
127
+
128
+ return output
129
+ }
130
+
131
+ function assignDefined(target, ...sources) {
132
+ const output = isObjectLike(target) || Array.isArray(target) ? target : {}
133
+
134
+ for (const source of sources) {
135
+ if (!isObjectLike(source) && !Array.isArray(source)) continue
136
+ for (const [key, value] of Object.entries(source)) {
137
+ if (value !== undefined) output[key] = value
138
+ }
139
+ }
140
+
141
+ return output
142
+ }
143
+
144
+ function mergeDeep(target, ...sources) {
145
+ const output = isPlainObject(target) ? target : {}
146
+
147
+ for (const source of sources) {
148
+ if (!isPlainObject(source)) continue
149
+
150
+ const stack = [{ destination: output, source }]
151
+ while (stack.length) {
152
+ const frame = stack.pop()
153
+ const { destination, source: currentSource } = frame
154
+ const keys = Object.keys(currentSource)
155
+
156
+ for (let index = 0; index < keys.length; index += 1) {
157
+ const key = keys[index]
158
+ const nextValue = currentSource[key]
159
+ const currentValue = destination[key]
160
+
161
+ if (isPlainObject(nextValue)) {
162
+ if (!isPlainObject(currentValue)) destination[key] = {}
163
+ stack.push({
164
+ destination: destination[key],
165
+ source: nextValue,
166
+ })
167
+ continue
168
+ }
169
+
170
+ if (Array.isArray(nextValue)) {
171
+ destination[key] = cloneDeep(nextValue)
172
+ continue
173
+ }
174
+
175
+ if (isObjectLike(nextValue)) {
176
+ destination[key] = cloneDeep(nextValue)
177
+ continue
178
+ }
179
+
180
+ destination[key] = nextValue
181
+ }
182
+ }
183
+ }
184
+
185
+ return output
186
+ }
187
+
188
+ module.exports = {
189
+ tokenizePath,
190
+ get,
191
+ has,
192
+ set,
193
+ unset,
194
+ pick,
195
+ omit,
196
+ mapValues,
197
+ defaults,
198
+ assignDefined,
199
+ cloneShallow,
200
+ cloneDeep,
201
+ mergeDeep,
202
+ }
package/lib/shared.js ADDED
@@ -0,0 +1,249 @@
1
+ "use strict"
2
+
3
+ function isObjectLike(value) {
4
+ return value !== null && typeof value === "object"
5
+ }
6
+
7
+ function isPlainObject(value) {
8
+ if (!isObjectLike(value)) return false
9
+ const prototype = Object.getPrototypeOf(value)
10
+ return prototype === Object.prototype || prototype === null
11
+ }
12
+
13
+ function isTypedArray(value) {
14
+ return ArrayBuffer.isView(value) && !(value instanceof DataView)
15
+ }
16
+
17
+ function normalizePathToken(value) {
18
+ if (typeof value === "number" && Number.isSafeInteger(value) && value >= 0) return value
19
+ const text = String(value).trim()
20
+ if (!text) return ""
21
+ if (/^(0|[1-9]\d*)$/.test(text)) return Number(text)
22
+ return text
23
+ }
24
+
25
+ function tokenizePath(path) {
26
+ if (Array.isArray(path)) {
27
+ const tokens = []
28
+ for (const token of path) {
29
+ const normalized = normalizePathToken(token)
30
+ if (normalized === "") continue
31
+ tokens.push(normalized)
32
+ }
33
+ return tokens
34
+ }
35
+
36
+ if (typeof path === "number") return [normalizePathToken(path)]
37
+ if (path == null) return []
38
+
39
+ const text = String(path)
40
+ if (!text.trim()) return []
41
+
42
+ const tokens = []
43
+ let buffer = ""
44
+ let bracket = ""
45
+ let inBracket = false
46
+ let quote = ""
47
+
48
+ for (let index = 0; index < text.length; index += 1) {
49
+ const character = text[index]
50
+
51
+ if (!inBracket) {
52
+ if (character === ".") {
53
+ if (buffer) {
54
+ tokens.push(normalizePathToken(buffer))
55
+ buffer = ""
56
+ }
57
+ continue
58
+ }
59
+
60
+ if (character === "[") {
61
+ if (buffer) {
62
+ tokens.push(normalizePathToken(buffer))
63
+ buffer = ""
64
+ }
65
+ inBracket = true
66
+ bracket = ""
67
+ quote = ""
68
+ continue
69
+ }
70
+
71
+ buffer += character
72
+ continue
73
+ }
74
+
75
+ if (quote) {
76
+ if (character === quote) {
77
+ quote = ""
78
+ } else {
79
+ bracket += character
80
+ }
81
+ continue
82
+ }
83
+
84
+ if (character === "'" || character === "\"") {
85
+ quote = character
86
+ continue
87
+ }
88
+
89
+ if (character === "]") {
90
+ const normalized = normalizePathToken(bracket)
91
+ if (normalized !== "") tokens.push(normalized)
92
+ inBracket = false
93
+ bracket = ""
94
+ continue
95
+ }
96
+
97
+ bracket += character
98
+ }
99
+
100
+ if (buffer) tokens.push(normalizePathToken(buffer))
101
+ return tokens
102
+ }
103
+
104
+ function getValueByPath(value, path) {
105
+ const tokens = tokenizePath(path)
106
+ let current = value
107
+
108
+ for (let index = 0; index < tokens.length; index += 1) {
109
+ if (current == null) return undefined
110
+ current = current[tokens[index]]
111
+ }
112
+
113
+ return current
114
+ }
115
+
116
+ function createIteratee(iteratee) {
117
+ if (typeof iteratee === "function") return iteratee
118
+ if (iteratee == null) return (value) => value
119
+ return (value) => getValueByPath(value, iteratee)
120
+ }
121
+
122
+ function cloneShallow(value) {
123
+ if (Array.isArray(value)) return value.slice()
124
+ if (isPlainObject(value)) return { ...value }
125
+ if (value instanceof Date) return new Date(value.getTime())
126
+ if (value instanceof RegExp) return new RegExp(value.source, value.flags)
127
+ if (value instanceof Map) return new Map(value)
128
+ if (value instanceof Set) return new Set(value)
129
+ if (isTypedArray(value)) return value.slice()
130
+ return value
131
+ }
132
+
133
+ function initClone(value) {
134
+ if (Array.isArray(value)) return []
135
+ if (isPlainObject(value)) return {}
136
+ if (value instanceof Date) return new Date(value.getTime())
137
+ if (value instanceof RegExp) return new RegExp(value.source, value.flags)
138
+ if (value instanceof Map) return new Map()
139
+ if (value instanceof Set) return new Set()
140
+ if (isTypedArray(value)) return value.slice()
141
+ return value
142
+ }
143
+
144
+ function cloneDeep(value) {
145
+ if (!isObjectLike(value)) return value
146
+
147
+ const root = initClone(value)
148
+ if (!isObjectLike(root)) return root
149
+
150
+ const seen = new Map([[value, root]])
151
+ const stack = [{ source: value, target: root }]
152
+
153
+ while (stack.length) {
154
+ const frame = stack.pop()
155
+ const { source, target } = frame
156
+
157
+ if (Array.isArray(source)) {
158
+ for (let index = 0; index < source.length; index += 1) {
159
+ const item = source[index]
160
+ if (!isObjectLike(item)) {
161
+ target[index] = item
162
+ continue
163
+ }
164
+ if (seen.has(item)) {
165
+ target[index] = seen.get(item)
166
+ continue
167
+ }
168
+ const child = initClone(item)
169
+ target[index] = child
170
+ if (isObjectLike(child)) {
171
+ seen.set(item, child)
172
+ stack.push({ source: item, target: child })
173
+ }
174
+ }
175
+ continue
176
+ }
177
+
178
+ if (source instanceof Map) {
179
+ for (const [key, item] of source.entries()) {
180
+ if (!isObjectLike(item)) {
181
+ target.set(key, item)
182
+ continue
183
+ }
184
+ if (seen.has(item)) {
185
+ target.set(key, seen.get(item))
186
+ continue
187
+ }
188
+ const child = initClone(item)
189
+ target.set(key, child)
190
+ if (isObjectLike(child)) {
191
+ seen.set(item, child)
192
+ stack.push({ source: item, target: child })
193
+ }
194
+ }
195
+ continue
196
+ }
197
+
198
+ if (source instanceof Set) {
199
+ for (const item of source.values()) {
200
+ if (!isObjectLike(item)) {
201
+ target.add(item)
202
+ continue
203
+ }
204
+ if (seen.has(item)) {
205
+ target.add(seen.get(item))
206
+ continue
207
+ }
208
+ const child = initClone(item)
209
+ target.add(child)
210
+ if (isObjectLike(child)) {
211
+ seen.set(item, child)
212
+ stack.push({ source: item, target: child })
213
+ }
214
+ }
215
+ continue
216
+ }
217
+
218
+ for (const key of Object.keys(source)) {
219
+ const item = source[key]
220
+ if (!isObjectLike(item)) {
221
+ target[key] = item
222
+ continue
223
+ }
224
+ if (seen.has(item)) {
225
+ target[key] = seen.get(item)
226
+ continue
227
+ }
228
+ const child = initClone(item)
229
+ target[key] = child
230
+ if (isObjectLike(child)) {
231
+ seen.set(item, child)
232
+ stack.push({ source: item, target: child })
233
+ }
234
+ }
235
+ }
236
+
237
+ return root
238
+ }
239
+
240
+ module.exports = {
241
+ isObjectLike,
242
+ isPlainObject,
243
+ isTypedArray,
244
+ tokenizePath,
245
+ getValueByPath,
246
+ createIteratee,
247
+ cloneShallow,
248
+ cloneDeep,
249
+ }
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "@qezor/structkit",
3
+ "version": "1.0.0",
4
+ "description": "Iterative array and object utilities for high-use data shaping without callback hell or recursion-heavy internals.",
5
+ "files": [
6
+ "LICENSE",
7
+ "README.md",
8
+ "index.js",
9
+ "index.mjs",
10
+ "index.d.ts",
11
+ "bridge.js",
12
+ "bridge.mjs",
13
+ "bridge.d.ts",
14
+ "lib",
15
+ "test.js"
16
+ ],
17
+ "main": "index.js",
18
+ "types": "index.d.ts",
19
+ "exports": {
20
+ ".": {
21
+ "types": "./index.d.ts",
22
+ "require": "./index.js",
23
+ "import": "./index.mjs",
24
+ "default": "./index.js"
25
+ },
26
+ "./bridge": {
27
+ "types": "./bridge.d.ts",
28
+ "require": "./bridge.js",
29
+ "import": "./bridge.mjs",
30
+ "default": "./bridge.js"
31
+ }
32
+ },
33
+ "scripts": {
34
+ "test": "node --test test.js",
35
+ "check": "node --test test.js",
36
+ "pack:dry": "npm pack --dry-run",
37
+ "prepublishOnly": "npm test"
38
+ },
39
+ "keywords": [
40
+ "array",
41
+ "object",
42
+ "utility",
43
+ "lodash",
44
+ "iterative",
45
+ "performance"
46
+ ],
47
+ "author": "Abdul Ahad",
48
+ "license": "MIT",
49
+ "publishConfig": {
50
+ "access": "public"
51
+ },
52
+ "engines": {
53
+ "node": ">=18"
54
+ },
55
+ "type": "commonjs",
56
+ "sideEffects": false
57
+ }
package/test.js ADDED
@@ -0,0 +1,90 @@
1
+ "use strict"
2
+
3
+ const test = require("node:test")
4
+ const assert = require("node:assert/strict")
5
+ const structkit = require("./index.js")
6
+
7
+ test("array helpers cover grouping uniqueness and sums", () => {
8
+ const buyers = [
9
+ { id: "u1", role: "buyer", qty: 2 },
10
+ { id: "u2", role: "buyer", qty: 5 },
11
+ { id: "u3", role: "seller", qty: 1 },
12
+ { id: "u3", role: "seller", qty: 1 },
13
+ ]
14
+
15
+ assert.deepEqual(structkit.chunk([1, 2, 3, 4, 5], 2), [[1, 2], [3, 4], [5]])
16
+ assert.deepEqual(structkit.compact([0, 1, false, 2, "", 3]), [1, 2, 3])
17
+ assert.deepEqual(structkit.uniq([1, 1, 2, 3, 3]), [1, 2, 3])
18
+ assert.deepEqual(structkit.uniqBy(buyers, "id").map((item) => item.id), ["u1", "u2", "u3"])
19
+ assert.deepEqual(structkit.groupBy(buyers, "role"), {
20
+ buyer: [buyers[0], buyers[1]],
21
+ seller: [buyers[2], buyers[3]],
22
+ })
23
+ assert.deepEqual(structkit.countBy(buyers, "role"), {
24
+ buyer: 2,
25
+ seller: 2,
26
+ })
27
+ assert.equal(structkit.sumBy(buyers, "qty"), 9)
28
+ })
29
+
30
+ test("path and object helpers work iteratively", () => {
31
+ const state = {}
32
+
33
+ structkit.set(state, "queue.regions[0].city", "Pune")
34
+ structkit.set(state, "queue.regions[0].active", true)
35
+ structkit.set(state, ["queue", "meta", "moq"], 500)
36
+
37
+ assert.equal(structkit.get(state, "queue.regions[0].city"), "Pune")
38
+ assert.equal(structkit.has(state, "queue.meta.moq"), true)
39
+ assert.deepEqual(structkit.tokenizePath("queue.regions[0].city"), ["queue", "regions", 0, "city"])
40
+ assert.deepEqual(structkit.pick(state, ["queue.meta.moq", "queue.regions[0].city"]), {
41
+ queue: {
42
+ meta: { moq: 500 },
43
+ regions: [{ city: "Pune" }],
44
+ },
45
+ })
46
+
47
+ const omitted = structkit.omit(state, "queue.regions[0].active")
48
+ assert.equal(structkit.has(omitted, "queue.regions[0].active"), false)
49
+ assert.equal(structkit.has(state, "queue.regions[0].active"), true)
50
+ })
51
+
52
+ test("cloneDeep and mergeDeep avoid shallow aliasing", () => {
53
+ const input = {
54
+ demand: {
55
+ buyers: [{ id: "u1" }],
56
+ counts: { joined: 2 },
57
+ },
58
+ }
59
+
60
+ const cloned = structkit.cloneDeep(input)
61
+ cloned.demand.buyers[0].id = "u2"
62
+
63
+ assert.equal(input.demand.buyers[0].id, "u1")
64
+
65
+ const merged = structkit.mergeDeep({}, { demand: { joined: 2, nested: { a: 1 } } }, { demand: { buyers: 3, nested: { b: 2 } } })
66
+ assert.deepEqual(merged, {
67
+ demand: {
68
+ joined: 2,
69
+ buyers: 3,
70
+ nested: { a: 1, b: 2 },
71
+ },
72
+ })
73
+ })
74
+
75
+ test("isEqual handles deep and cyclic values without recursion", () => {
76
+ const left = { demand: { buyers: [1, 2, 3] } }
77
+ const right = { demand: { buyers: [1, 2, 3] } }
78
+
79
+ left.self = left
80
+ right.self = right
81
+
82
+ assert.equal(structkit.isEqual(left, right), true)
83
+ assert.equal(structkit.isEqual(left, { demand: { buyers: [1, 2] } }), false)
84
+ })
85
+
86
+ test("bridge metadata is attached", () => {
87
+ const bridge = structkit.readQezorBridge(structkit)
88
+ assert.equal(bridge.name, "@qezor/structkit")
89
+ assert.equal(structkit.hasQezorCapability(structkit, "structure:path"), true)
90
+ })