resobjectify 1.1.0 → 2.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/README.md +225 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.js +153 -0
- package/dist/src/objectify.d.ts +11 -0
- package/dist/src/objectify.js +115 -0
- package/dist/types.d.ts +125 -0
- package/dist/types.js +2 -0
- package/package.json +18 -4
- package/index.js +0 -44
- package/test/app.js +0 -11
package/README.md
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
# ResObjectify
|
|
2
|
+
|
|
3
|
+
Transform flat rows into nested arrays or object maps.
|
|
4
|
+
|
|
5
|
+
Useful when you receive denormalized rows (for example from SQL joins) and need API-ready output.
|
|
6
|
+
|
|
7
|
+
## When To Use
|
|
8
|
+
|
|
9
|
+
- Input is flat rows with repeated parent data (typical SQL join output).
|
|
10
|
+
- You want nested API response shapes with minimal manual grouping code.
|
|
11
|
+
- You need either array output or object-map output depending on use case.
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install resobjectify
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Import
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
import { objectify, fieldsBuilder, type Fields } from "resobjectify";
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## API At A Glance
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
objectify(data, fields, object?);
|
|
29
|
+
fieldsBuilder().field(...).group(...).build();
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
- `objectify(data, fields)` returns arrays by default.
|
|
33
|
+
- `objectify(data, fields, true)` returns an object map keyed by the first field.
|
|
34
|
+
- `fieldsBuilder` builds the same `Fields` tuple structure as writing arrays manually.
|
|
35
|
+
|
|
36
|
+
## Quick Example
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
import { objectify, type Fields } from "resobjectify";
|
|
40
|
+
|
|
41
|
+
type Row = {
|
|
42
|
+
order_id: number;
|
|
43
|
+
customer: string;
|
|
44
|
+
item_id: number;
|
|
45
|
+
item_name: string;
|
|
46
|
+
qty: number;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
type Result = {
|
|
50
|
+
id: number;
|
|
51
|
+
customer: string;
|
|
52
|
+
items: { id: number; name: string; qty: number }[];
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const rows: Row[] = [
|
|
56
|
+
{ order_id: 1, customer: "Acme", item_id: 10, item_name: "Keyboard", qty: 1 },
|
|
57
|
+
{ order_id: 1, customer: "Acme", item_id: 11, item_name: "Mouse", qty: 2 },
|
|
58
|
+
{ order_id: 2, customer: "Beta", item_id: 20, item_name: "Monitor", qty: 1 },
|
|
59
|
+
{ order_id: 2, customer: "Beta", item_id: null },
|
|
60
|
+
];
|
|
61
|
+
|
|
62
|
+
const fields: Fields<Result, Row> = [
|
|
63
|
+
{ key: "order_id", as: "id" },
|
|
64
|
+
"customer",
|
|
65
|
+
[
|
|
66
|
+
"items",
|
|
67
|
+
[
|
|
68
|
+
{ key: "item_id", as: "id" },
|
|
69
|
+
{ key: "item_name", as: "name" },
|
|
70
|
+
"qty"
|
|
71
|
+
]
|
|
72
|
+
]
|
|
73
|
+
];
|
|
74
|
+
|
|
75
|
+
const result = objectify<Result, Row>(rows, fields);
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
`result`:
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
[
|
|
82
|
+
{
|
|
83
|
+
id: 1,
|
|
84
|
+
customer: "Acme",
|
|
85
|
+
items: [
|
|
86
|
+
{ id: 10, name: "Keyboard", qty: 1 },
|
|
87
|
+
{ id: 11, name: "Mouse", qty: 2 },
|
|
88
|
+
],
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
id: 2,
|
|
92
|
+
customer: "Beta",
|
|
93
|
+
items: [{ id: 20, name: "Monitor", qty: 1 }],
|
|
94
|
+
},
|
|
95
|
+
];
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Behavior in this example:
|
|
99
|
+
|
|
100
|
+
- The first field at each level is the grouping key (`order_id` at root, `item_id` in `items`).
|
|
101
|
+
- Rows with `null` or `undefined` grouping keys are skipped at that level.
|
|
102
|
+
|
|
103
|
+
## Object Output Mode
|
|
104
|
+
|
|
105
|
+
Pass `true` as the third argument to return a top-level object map.
|
|
106
|
+
Nested groups inherit that mode unless overridden.
|
|
107
|
+
|
|
108
|
+
```ts
|
|
109
|
+
const mapped = objectify<Result, Row>(rows, fields, true);
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
`mapped`:
|
|
113
|
+
|
|
114
|
+
```ts
|
|
115
|
+
{
|
|
116
|
+
1: {
|
|
117
|
+
id: 1,
|
|
118
|
+
customer: "Acme",
|
|
119
|
+
items: {
|
|
120
|
+
10: { id: 10, name: "Keyboard", qty: 1 },
|
|
121
|
+
11: { id: 11, name: "Mouse", qty: 2 },
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
2: {
|
|
125
|
+
id: 2,
|
|
126
|
+
customer: "Beta",
|
|
127
|
+
items: {
|
|
128
|
+
20: { id: 20, name: "Monitor", qty: 1 },
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Override a nested group mode with `{ object: false }` (or `{ object: true }`):
|
|
135
|
+
|
|
136
|
+
```ts
|
|
137
|
+
const fieldsWithArrayItems: Fields<Result, Row> = [
|
|
138
|
+
{ key: "order_id", as: "id" },
|
|
139
|
+
"customer",
|
|
140
|
+
[
|
|
141
|
+
{ name: "items", object: false }, // overrides the third argument for this group
|
|
142
|
+
[
|
|
143
|
+
{ key: "item_id", as: "id" },
|
|
144
|
+
{ key: "item_name", as: "name" },
|
|
145
|
+
"qty"
|
|
146
|
+
]
|
|
147
|
+
]
|
|
148
|
+
];
|
|
149
|
+
|
|
150
|
+
const mappedWithArrayItems = objectify<Result, Row>(rows, fieldsWithArrayItems, true);
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
`mappedWithArrayItems` keeps top-level object mode but returns `items` as an array.
|
|
154
|
+
If a level contains only one field, that level is always emitted as an array.
|
|
155
|
+
|
|
156
|
+
## Using `fieldsBuilder`
|
|
157
|
+
|
|
158
|
+
`fieldsBuilder` is a fluent helper to build the same `Fields` structure without manually writing nested arrays.
|
|
159
|
+
|
|
160
|
+
How it works:
|
|
161
|
+
|
|
162
|
+
- `field("key")` adds a key field.
|
|
163
|
+
- `field("key", "alias")` adds a renamed key field.
|
|
164
|
+
- `field("key", { json: true | false })` adds options without alias.
|
|
165
|
+
- `field("key", "alias", { json: true | false })` combines alias + options.
|
|
166
|
+
- `group(name, callback)` starts a nested level.
|
|
167
|
+
- `group(name, { object: true | false }, callback)` overrides output mode for that group.
|
|
168
|
+
- `build()` returns the final `Fields` tuple you pass to `objectify`.
|
|
169
|
+
|
|
170
|
+
```ts
|
|
171
|
+
const built = fieldsBuilder<Result, Row>()
|
|
172
|
+
.field("order_id", "id")
|
|
173
|
+
.field("customer")
|
|
174
|
+
.group("items", (g) =>
|
|
175
|
+
g.field("item_id", "id")
|
|
176
|
+
.field("item_name", "name")
|
|
177
|
+
.field("qty")
|
|
178
|
+
).build();
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
`built`:
|
|
182
|
+
|
|
183
|
+
```ts
|
|
184
|
+
[
|
|
185
|
+
{ key: "order_id", as: "id" },
|
|
186
|
+
"customer",
|
|
187
|
+
["items",
|
|
188
|
+
[
|
|
189
|
+
{ key: "item_id", as: "id" },
|
|
190
|
+
{ key: "item_name", as: "name" },
|
|
191
|
+
"qty"
|
|
192
|
+
]
|
|
193
|
+
],
|
|
194
|
+
];
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
## JSON Parsing
|
|
198
|
+
|
|
199
|
+
Use `json: true` to parse JSON text values.
|
|
200
|
+
|
|
201
|
+
```ts
|
|
202
|
+
type MetaRow = { id: number; meta: string | null };
|
|
203
|
+
type MetaResult = { id: number; meta: { tier: number } | null };
|
|
204
|
+
|
|
205
|
+
const metaRows: MetaRow[] = [
|
|
206
|
+
{ id: 1, meta: '{"tier":1}' },
|
|
207
|
+
{ id: 2, meta: null },
|
|
208
|
+
{ id: 3, meta: "bad-json" },
|
|
209
|
+
];
|
|
210
|
+
|
|
211
|
+
const metaFields: Fields<MetaResult, MetaRow> = ["id", { key: "meta", json: true }];
|
|
212
|
+
const parsed = objectify<MetaResult, MetaRow>(metaRows, metaFields);
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
`parsed`:
|
|
216
|
+
|
|
217
|
+
```ts
|
|
218
|
+
[
|
|
219
|
+
{ id: 1, meta: { tier: 1 } },
|
|
220
|
+
{ id: 2, meta: null },
|
|
221
|
+
{ id: 3, meta: null },
|
|
222
|
+
];
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
With `json: true`, invalid JSON values are converted to `null` and a parsing error is logged.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { FieldsBuilder, Fields, Prettify, Row } from "./types";
|
|
2
|
+
export type { Fields, FieldsBuilder } from "./types";
|
|
3
|
+
export declare function fieldsBuilder<R = Row, T = Row>(): FieldsBuilder<R, T>;
|
|
4
|
+
export declare function objectify<R = unknown, T = Row>(
|
|
5
|
+
data: T[],
|
|
6
|
+
fields: Fields<R, T>,
|
|
7
|
+
object: true,
|
|
8
|
+
): Record<PropertyKey, Prettify<R>>;
|
|
9
|
+
export declare function objectify<R = unknown, T = Row>(
|
|
10
|
+
data: T[],
|
|
11
|
+
fields: Fields<R, T>,
|
|
12
|
+
object?: false,
|
|
13
|
+
): Prettify<R>[];
|
|
14
|
+
export declare function objectify<R = unknown>(
|
|
15
|
+
data: Row[],
|
|
16
|
+
fields: Fields<R>,
|
|
17
|
+
object: true,
|
|
18
|
+
): Record<PropertyKey, Prettify<R>>;
|
|
19
|
+
export declare function objectify<R = unknown>(
|
|
20
|
+
data: Row[],
|
|
21
|
+
fields: Fields<R>,
|
|
22
|
+
object?: false,
|
|
23
|
+
): Prettify<R>[];
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.fieldsBuilder = fieldsBuilder;
|
|
4
|
+
exports.objectify = objectify;
|
|
5
|
+
function fieldsBuilder() {
|
|
6
|
+
const fields = [];
|
|
7
|
+
function newField(key, as, options) {
|
|
8
|
+
if (!as && !options) {
|
|
9
|
+
return key;
|
|
10
|
+
}
|
|
11
|
+
let entry = typeof key === "object" ? key : { key };
|
|
12
|
+
if (as !== undefined) {
|
|
13
|
+
entry = { ...entry, as: as };
|
|
14
|
+
}
|
|
15
|
+
if (options !== undefined) {
|
|
16
|
+
entry = { ...entry, ...options };
|
|
17
|
+
}
|
|
18
|
+
return entry;
|
|
19
|
+
}
|
|
20
|
+
function newGroup(groupField, options) {
|
|
21
|
+
if (!options) {
|
|
22
|
+
return groupField;
|
|
23
|
+
}
|
|
24
|
+
let entry = typeof groupField === "object" ? groupField : { name: groupField };
|
|
25
|
+
if (options) {
|
|
26
|
+
entry = { ...entry, ...options };
|
|
27
|
+
}
|
|
28
|
+
return entry;
|
|
29
|
+
}
|
|
30
|
+
const field = (field, asOrOptions, options) => {
|
|
31
|
+
const isOptions = typeof asOrOptions === "object"
|
|
32
|
+
&& asOrOptions !== null
|
|
33
|
+
&& "json" in asOrOptions;
|
|
34
|
+
const resolvedAs = isOptions ? undefined : asOrOptions;
|
|
35
|
+
const resolvedOptions = isOptions ? asOrOptions : options;
|
|
36
|
+
//Create the new field entry
|
|
37
|
+
const entry = newField(field, resolvedAs, resolvedOptions);
|
|
38
|
+
fields.push(entry);
|
|
39
|
+
return api;
|
|
40
|
+
};
|
|
41
|
+
const group = (name, optionsOrBuild, build) => {
|
|
42
|
+
const options = typeof optionsOrBuild === "object" ? optionsOrBuild : undefined;
|
|
43
|
+
const buildFn = typeof optionsOrBuild === "function" ? optionsOrBuild : build;
|
|
44
|
+
if (!buildFn) {
|
|
45
|
+
throw new Error("Group builder requires a builder callback.");
|
|
46
|
+
}
|
|
47
|
+
const nested = buildFn(fieldsBuilder()).build();
|
|
48
|
+
const groupField = newGroup(name, options);
|
|
49
|
+
fields.push([groupField, nested]);
|
|
50
|
+
return api;
|
|
51
|
+
};
|
|
52
|
+
const build = () => {
|
|
53
|
+
if (fields.length === 0 || Array.isArray(fields[0])) {
|
|
54
|
+
throw new Error("Fields builder requires the first field to be a key field.");
|
|
55
|
+
}
|
|
56
|
+
return fields;
|
|
57
|
+
};
|
|
58
|
+
const api = { field, group, build };
|
|
59
|
+
return api;
|
|
60
|
+
}
|
|
61
|
+
function objectify(data, fields, object = false) {
|
|
62
|
+
// If the fields is a single field or object is false, group the result in an array, otherwise group the result in an object
|
|
63
|
+
const result = fields.length === 1 || !object ? [] : {};
|
|
64
|
+
const [keyField, ...restFields] = fields;
|
|
65
|
+
const key = getKeyField(keyField);
|
|
66
|
+
const name = getFieldName(keyField);
|
|
67
|
+
// Pre-group by the current key so each recursion only sees its parent slice,
|
|
68
|
+
// which removes the need for parent checks or duplicate tracking.
|
|
69
|
+
const groups = groupByKey(data, key);
|
|
70
|
+
for (const [keyValue, rows] of groups) {
|
|
71
|
+
const row = rows[0];
|
|
72
|
+
const obj = {};
|
|
73
|
+
for (const field of fields) {
|
|
74
|
+
// If the field is not an array, it is a key field, so we can get the value from the row
|
|
75
|
+
if (!Array.isArray(field)) {
|
|
76
|
+
const fieldName = getFieldName(field);
|
|
77
|
+
obj[fieldName] = getFieldValue(row, field);
|
|
78
|
+
}
|
|
79
|
+
else { // If the field is an array, it is a group field, so we need to objectify the nested fields recursively
|
|
80
|
+
const [rawGroupField, nestedFields] = field;
|
|
81
|
+
const groupField = rawGroupField;
|
|
82
|
+
const nestedObject = isObject(groupField, object);
|
|
83
|
+
obj[getGroupName(groupField)] = nestedObject
|
|
84
|
+
? objectify(rows, nestedFields, true)
|
|
85
|
+
: objectify(rows, nestedFields, false);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
if (obj[name] != null) {
|
|
89
|
+
// If the result is an array, we need to push the object to the array
|
|
90
|
+
if (Array.isArray(result)) {
|
|
91
|
+
result.push(restFields.length ? obj : obj[name]);
|
|
92
|
+
}
|
|
93
|
+
else { // If the result is an object, we need to set the object to the key
|
|
94
|
+
result[keyValue] = obj;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if (object) {
|
|
99
|
+
return result;
|
|
100
|
+
}
|
|
101
|
+
return result;
|
|
102
|
+
}
|
|
103
|
+
// Groups rows by the current key, preserving first-seen order.
|
|
104
|
+
function groupByKey(rows, key) {
|
|
105
|
+
const groups = new Map();
|
|
106
|
+
for (const row of rows) {
|
|
107
|
+
const keyValue = row[key];
|
|
108
|
+
const group = groups.get(keyValue);
|
|
109
|
+
if (group) {
|
|
110
|
+
group.push(row);
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
groups.set(keyValue, [row]);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return groups;
|
|
117
|
+
}
|
|
118
|
+
function getKeyField(field) {
|
|
119
|
+
return (typeof field === "string" ? field : field.key);
|
|
120
|
+
}
|
|
121
|
+
function getFieldName(field) {
|
|
122
|
+
var _a;
|
|
123
|
+
return (typeof field === "string" ? field : ((_a = field.as) !== null && _a !== void 0 ? _a : field.key));
|
|
124
|
+
}
|
|
125
|
+
function getFieldValue(row, field) {
|
|
126
|
+
if (typeof field === "string") {
|
|
127
|
+
return row[field];
|
|
128
|
+
}
|
|
129
|
+
const key = getKeyField(field);
|
|
130
|
+
if (field.json) {
|
|
131
|
+
try {
|
|
132
|
+
return JSON.parse(row[key]);
|
|
133
|
+
}
|
|
134
|
+
catch (error) {
|
|
135
|
+
console.error(`"${row[key]}" is not a valid JSON`, error);
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return row[key];
|
|
140
|
+
}
|
|
141
|
+
function getGroupName(field) {
|
|
142
|
+
if (typeof field === "string" || typeof field === "number" || typeof field === "symbol") {
|
|
143
|
+
return field;
|
|
144
|
+
}
|
|
145
|
+
return field.name;
|
|
146
|
+
}
|
|
147
|
+
function isObject(field, defaultValue) {
|
|
148
|
+
var _a;
|
|
149
|
+
if (typeof field !== "object" || field == null) {
|
|
150
|
+
return defaultValue;
|
|
151
|
+
}
|
|
152
|
+
return (_a = field.object) !== null && _a !== void 0 ? _a : defaultValue;
|
|
153
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Fields, Prettify, Row } from "../types";
|
|
2
|
+
/**
|
|
3
|
+
* Transforms flat rows into nested objects/arrays based on a field definition.
|
|
4
|
+
*
|
|
5
|
+
* When `object` is `true`, top-level output is keyed by the first field value.
|
|
6
|
+
* Otherwise the output is an array.
|
|
7
|
+
*/
|
|
8
|
+
export declare function objectify<R = unknown, T = Row>(data: T[], fields: Fields<R, T>, object: true): Record<PropertyKey, Prettify<R>>;
|
|
9
|
+
export declare function objectify<R = unknown, T = Row>(data: T[], fields: Fields<R, T>, object?: false): Prettify<R>[];
|
|
10
|
+
export declare function objectify<R = unknown>(data: Row[], fields: Fields<R>, object: true): Record<PropertyKey, Prettify<R>>;
|
|
11
|
+
export declare function objectify<R = unknown>(data: Row[], fields: Fields<R>, object?: false): Prettify<R>[];
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.objectify = objectify;
|
|
4
|
+
function objectify(data, fields, object = false) {
|
|
5
|
+
// If the fields is a single field or object is false, group the result in an array, otherwise group the result in an object
|
|
6
|
+
const result = fields.length === 1 || !object ? [] : {};
|
|
7
|
+
const [keyField, ...restFields] = fields;
|
|
8
|
+
const key = getKeyField(keyField);
|
|
9
|
+
const name = getFieldName(keyField);
|
|
10
|
+
// Pre-group by the current key so each recursion only sees its parent slice,
|
|
11
|
+
// which removes the need for parent checks or duplicate tracking.
|
|
12
|
+
const groups = groupByKey(data, key);
|
|
13
|
+
for (const [keyValue, rows] of groups) {
|
|
14
|
+
const row = rows[0];
|
|
15
|
+
const obj = {};
|
|
16
|
+
for (const field of fields) {
|
|
17
|
+
// If the field is not an array, it is a key field, so we can get the value from the row
|
|
18
|
+
if (!Array.isArray(field)) {
|
|
19
|
+
const fieldName = getFieldName(field);
|
|
20
|
+
obj[fieldName] = getFieldValue(row, field);
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
// If the field is an array, it is a group field, so we need to objectify the nested fields recursively
|
|
24
|
+
const [rawGroupField, nestedFields] = field;
|
|
25
|
+
const groupField = rawGroupField;
|
|
26
|
+
const nestedObject = isObject(groupField, object);
|
|
27
|
+
obj[getGroupName(groupField)] = nestedObject
|
|
28
|
+
? objectify(rows, nestedFields, true)
|
|
29
|
+
: objectify(rows, nestedFields, false);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
if (obj[name] != null) {
|
|
33
|
+
// If the result is an array, we need to push the object to the array
|
|
34
|
+
if (Array.isArray(result)) {
|
|
35
|
+
result.push(restFields.length ? obj : obj[name]);
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
// If the result is an object, we need to set the object to the key
|
|
39
|
+
result[keyValue] = obj;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
if (object) {
|
|
44
|
+
return result;
|
|
45
|
+
}
|
|
46
|
+
return result;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Groups rows by the provided key while preserving insertion order.
|
|
50
|
+
*/
|
|
51
|
+
function groupByKey(rows, key) {
|
|
52
|
+
const groups = new Map();
|
|
53
|
+
for (const row of rows) {
|
|
54
|
+
const keyValue = row[key];
|
|
55
|
+
const group = groups.get(keyValue);
|
|
56
|
+
if (group) {
|
|
57
|
+
group.push(row);
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
groups.set(keyValue, [row]);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return groups;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Resolves the source key from either shorthand or object field syntax.
|
|
67
|
+
*/
|
|
68
|
+
function getKeyField(field) {
|
|
69
|
+
return (typeof field === "string" ? field : field.key);
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Resolves the output property name for a key field.
|
|
73
|
+
*/
|
|
74
|
+
function getFieldName(field) {
|
|
75
|
+
var _a;
|
|
76
|
+
return (typeof field === "string" ? field : ((_a = field.as) !== null && _a !== void 0 ? _a : field.key));
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Reads a row value and optionally parses it as JSON.
|
|
80
|
+
*/
|
|
81
|
+
function getFieldValue(row, field) {
|
|
82
|
+
if (typeof field === "string") {
|
|
83
|
+
return row[field];
|
|
84
|
+
}
|
|
85
|
+
const key = getKeyField(field);
|
|
86
|
+
if (field.json) {
|
|
87
|
+
try {
|
|
88
|
+
return JSON.parse(row[key]);
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
console.error(`"${row[key]}" is not a valid JSON`, error);
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return row[key];
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Resolves the output name for a group field definition.
|
|
99
|
+
*/
|
|
100
|
+
function getGroupName(field) {
|
|
101
|
+
if (typeof field === "string" || typeof field === "number" || typeof field === "symbol") {
|
|
102
|
+
return field;
|
|
103
|
+
}
|
|
104
|
+
return field.name;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Resolves whether a group should be emitted as object or array.
|
|
108
|
+
*/
|
|
109
|
+
function isObject(field, defaultValue) {
|
|
110
|
+
var _a;
|
|
111
|
+
if (typeof field !== "object" || field == null) {
|
|
112
|
+
return defaultValue;
|
|
113
|
+
}
|
|
114
|
+
return (_a = field.object) !== null && _a !== void 0 ? _a : defaultValue;
|
|
115
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic object-like row shape used across the library.
|
|
3
|
+
*/
|
|
4
|
+
export type Row = Record<PropertyKey, unknown>;
|
|
5
|
+
/**
|
|
6
|
+
* String-only key names for a given row type.
|
|
7
|
+
*/
|
|
8
|
+
export type KeyName<T = Row> = Extract<keyof T, string>;
|
|
9
|
+
/**
|
|
10
|
+
* Fallback string type used when strict key extraction resolves to `never`.
|
|
11
|
+
*/
|
|
12
|
+
export type DefaultString = string & {};
|
|
13
|
+
/**
|
|
14
|
+
* Extracts the element type from readonly arrays/tuples.
|
|
15
|
+
*/
|
|
16
|
+
type ArrayElement<T> = T extends readonly (infer U)[] ? U : never;
|
|
17
|
+
type StringKey<K> = Extract<K, string>;
|
|
18
|
+
/**
|
|
19
|
+
* Values treated as terminal leaves during key-path discovery.
|
|
20
|
+
*/
|
|
21
|
+
type Primitives = string | number | boolean | null | undefined | Date;
|
|
22
|
+
type Array = readonly unknown[];
|
|
23
|
+
type Object = Record<PropertyKey, unknown>;
|
|
24
|
+
type ChildPart<T> = Extract<T, Array | Object>;
|
|
25
|
+
/**
|
|
26
|
+
* Detects whether a type has a broad index signature.
|
|
27
|
+
*/
|
|
28
|
+
type HasIndexSignature<T> = string extends keyof T ? true : number extends keyof T ? true : symbol extends keyof T ? true : false;
|
|
29
|
+
/**
|
|
30
|
+
* Recursively collects keys whose values end at primitive leaves.
|
|
31
|
+
*/
|
|
32
|
+
type LeafKeysImpl<T> = [T] extends [Primitives] ? never : [T] extends [Array] ? LeafKeysImpl<ArrayElement<T>> : [T] extends [Object] ? {
|
|
33
|
+
[K in keyof T]-?: T[K] extends Primitives ? StringKey<K> : LeafKeysImpl<ChildPart<T[K]>>;
|
|
34
|
+
}[keyof T] : never;
|
|
35
|
+
/**
|
|
36
|
+
* Recursively collects keys that point to nested object/array branches.
|
|
37
|
+
*/
|
|
38
|
+
type BranchKeysImpl<T> = [T] extends [Primitives] ? never : [T] extends [Array] ? BranchKeysImpl<ArrayElement<T>> : [T] extends [Object] ? HasIndexSignature<T> extends true ? BranchKeysImpl<ChildPart<T[keyof T]>> : {
|
|
39
|
+
[K in keyof T]-?: T[K] extends Primitives ? never : StringKey<K> | BranchKeysImpl<ChildPart<T[K]>>;
|
|
40
|
+
}[keyof T] : never;
|
|
41
|
+
/**
|
|
42
|
+
* Leaf property names for the result type, with a string fallback.
|
|
43
|
+
*/
|
|
44
|
+
export type LeafKeys<T> = [LeafKeysImpl<T>] extends [never] ? DefaultString : LeafKeysImpl<T>;
|
|
45
|
+
/**
|
|
46
|
+
* Branch property names for nested groups, with a string fallback.
|
|
47
|
+
*/
|
|
48
|
+
export type BranchKeys<T> = [BranchKeysImpl<T>] extends [never] ? DefaultString : BranchKeysImpl<T>;
|
|
49
|
+
/**
|
|
50
|
+
* Expands inferred/intersection types into a cleaner object shape.
|
|
51
|
+
*/
|
|
52
|
+
export type Prettify<T> = {
|
|
53
|
+
[K in keyof T]: T[K];
|
|
54
|
+
} & {};
|
|
55
|
+
/**
|
|
56
|
+
* Return type for `objectify`: either an array or keyed object map.
|
|
57
|
+
*/
|
|
58
|
+
export type Result<T = unknown> = T[] | Record<PropertyKey, T>;
|
|
59
|
+
/**
|
|
60
|
+
* Selects a source key from a row and optional output behavior.
|
|
61
|
+
*/
|
|
62
|
+
export type KeyField<R = Row, T = Row> = KeyName<T> | {
|
|
63
|
+
key: KeyName<T>;
|
|
64
|
+
as?: LeafKeys<R>;
|
|
65
|
+
json?: boolean;
|
|
66
|
+
};
|
|
67
|
+
/**
|
|
68
|
+
* Describes a nested group in the output object.
|
|
69
|
+
*/
|
|
70
|
+
export type GroupField<R = Row> = BranchKeys<R> | {
|
|
71
|
+
name: BranchKeys<R>;
|
|
72
|
+
object?: boolean;
|
|
73
|
+
};
|
|
74
|
+
/**
|
|
75
|
+
* Runtime-friendly group field variant used by builder internals.
|
|
76
|
+
*/
|
|
77
|
+
export type SimpleGroupField = DefaultString | {
|
|
78
|
+
name: DefaultString;
|
|
79
|
+
object?: boolean;
|
|
80
|
+
};
|
|
81
|
+
/**
|
|
82
|
+
* Single field definition: direct key field or nested group tuple.
|
|
83
|
+
*/
|
|
84
|
+
export type Field<R = Row, T = Row> = KeyField<R, T> | [GroupField<R>, Fields<R, T>];
|
|
85
|
+
/**
|
|
86
|
+
* Full field set, requiring the first entry to be a key field.
|
|
87
|
+
*/
|
|
88
|
+
export type Fields<R = Row, T = Row> = [KeyField<R, T>, ...Field<R, T>[]];
|
|
89
|
+
/**
|
|
90
|
+
* Extra options when defining a key field.
|
|
91
|
+
*/
|
|
92
|
+
export type KeyFieldOptions = {
|
|
93
|
+
json?: boolean;
|
|
94
|
+
};
|
|
95
|
+
/**
|
|
96
|
+
* Extra options when defining a group field.
|
|
97
|
+
*/
|
|
98
|
+
export type GroupFieldOptions = {
|
|
99
|
+
object?: boolean;
|
|
100
|
+
};
|
|
101
|
+
/**
|
|
102
|
+
* Fluent API used to compose field definitions.
|
|
103
|
+
*/
|
|
104
|
+
export type FieldsBuilder<R = Row, T = Row> = {
|
|
105
|
+
/**
|
|
106
|
+
* Adds a key field to the current field set.
|
|
107
|
+
*/
|
|
108
|
+
field: {
|
|
109
|
+
(field: KeyField<R, T>): FieldsBuilder<R, T>;
|
|
110
|
+
(field: KeyName<T>, as?: LeafKeys<R>, options?: KeyFieldOptions): FieldsBuilder<R, T>;
|
|
111
|
+
(key: KeyName<T>, options?: KeyFieldOptions): FieldsBuilder<R, T>;
|
|
112
|
+
};
|
|
113
|
+
/**
|
|
114
|
+
* Adds a nested group with its own child field builder.
|
|
115
|
+
*/
|
|
116
|
+
group: {
|
|
117
|
+
(name: GroupField<R> | SimpleGroupField, fields: (builder: FieldsBuilder<R, T>) => FieldsBuilder<R, T>): FieldsBuilder<R, T>;
|
|
118
|
+
(name: GroupField<R> | SimpleGroupField, options: GroupFieldOptions, fields: (builder: FieldsBuilder<R, T>) => FieldsBuilder<R, T>): FieldsBuilder<R, T>;
|
|
119
|
+
};
|
|
120
|
+
/**
|
|
121
|
+
* Finalizes and returns the accumulated field definition tuple.
|
|
122
|
+
*/
|
|
123
|
+
build(): Fields<R, T>;
|
|
124
|
+
};
|
|
125
|
+
export {};
|
package/dist/types.js
ADDED
package/package.json
CHANGED
|
@@ -1,11 +1,25 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "resobjectify",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "Parse an array of one dimensional objects to nested array/object",
|
|
5
|
-
"main": "index.js",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
6
7
|
"scripts": {
|
|
7
|
-
"
|
|
8
|
+
"build": "tsc -p tsconfig.json",
|
|
9
|
+
"lint": "biome check .",
|
|
10
|
+
"lint:fix": "biome check --write .",
|
|
11
|
+
"format": "biome format --write .",
|
|
12
|
+
"test": "vitest run",
|
|
13
|
+
"test:watch": "vitest"
|
|
8
14
|
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist"
|
|
17
|
+
],
|
|
9
18
|
"author": "Yaro96",
|
|
10
|
-
"license": "ISC"
|
|
19
|
+
"license": "ISC",
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"@biomejs/biome": "2.4.3",
|
|
22
|
+
"typescript": "5.9.3",
|
|
23
|
+
"vitest": "3.2.4"
|
|
24
|
+
}
|
|
11
25
|
}
|
package/index.js
DELETED
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
function objectify(arr, fields, object = false, index = 0, parents = []) {
|
|
2
|
-
/*arr=[{id:111,code:'aaa',name:"prova", rule_id:1, formula:"asd", meter_id:8,area_id:31},
|
|
3
|
-
{id:111,code:'aaa',name:"prova", rule_id:2, formula:"asd", meter_id:8,area_id:95},
|
|
4
|
-
{id:111,code:'aaa',name:"prova", rule_id:1, formula:"asd", meter_id:10,area_id:31},
|
|
5
|
-
{id:111,code:'aaa',name:"prova", rule_id:1, formula:"asd", meter_id:10,area_id:95},
|
|
6
|
-
{id:111,code:'aaa',name:"prova", rule_id:2, formula:"asd", meter_id:10,area_id:95}]*/
|
|
7
|
-
//fields=[{key:"id", as:"area_id"},{key:"code", as:"area_code"},"name", ["rules",["rule_id",{key:"formula", as:"rule"},[{name:"meters",object:true},[{key:"meter_id", as:"id"}]],[{name:"areas",object:false},["area_id"]]]]]
|
|
8
|
-
let result = fields.length == 1 ? [] : object ? {} : [];
|
|
9
|
-
let added = [];
|
|
10
|
-
|
|
11
|
-
for (let j = index; j < arr.length; j++) {
|
|
12
|
-
if (!checkParents(arr, j, index, parents) || added.includes(arr[j][fields[0].key || fields[0]]))
|
|
13
|
-
continue;
|
|
14
|
-
|
|
15
|
-
let obj = {};
|
|
16
|
-
for (let f of fields) {
|
|
17
|
-
if (!Array.isArray(f)) {
|
|
18
|
-
obj[f.as || f.key || f] = f.json ? (arr[j][f.key || f] === null ? null : JSON.parse(arr[j][f.key || f])) : arr[j][f.key || f];
|
|
19
|
-
} else {
|
|
20
|
-
obj[f[0].name || f[0]] = objectify(arr, f[1], f[0].object != undefined ? f[0].object : object, j, [...parents, fields[0].key || fields[0]]);
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
added.push(arr[j][fields[0].key || fields[0]]);
|
|
24
|
-
if (obj[fields[0].as || fields[0].key || fields[0]] != null) {
|
|
25
|
-
if (fields.length == 1)
|
|
26
|
-
result.push(obj[fields[0].as || fields[0].key || fields[0]]);
|
|
27
|
-
else if (object)
|
|
28
|
-
result[arr[j][fields[0].key || fields[0]]] = obj;
|
|
29
|
-
else
|
|
30
|
-
result.push(obj);
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
return result;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function checkParents(arr, j, index, parents) {
|
|
37
|
-
for (let parent of parents) {
|
|
38
|
-
if (arr[j][parent] != arr[index][parent])
|
|
39
|
-
return false;
|
|
40
|
-
}
|
|
41
|
-
return true;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
module.exports = objectify;
|
package/test/app.js
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
let objectify=require("resobjectify")
|
|
2
|
-
|
|
3
|
-
let arr=[{id:111,code:'aaa',name:"prova", rule_id:1, formula:"asd", meter_id:8,area_id:31},
|
|
4
|
-
{id:111,code:'aaa',name:"prova", rule_id:1, formula:"asd", meter_id:8,area_id:95},
|
|
5
|
-
{id:111,code:'aaa',name:"prova", rule_id:1, formula:"asd", meter_id:10,area_id:31},
|
|
6
|
-
{id:111,code:'aaa',name:"prova", rule_id:1, formula:"asd", meter_id:10,area_id:95},
|
|
7
|
-
{id:111,code:'aaa',name:"prova", rule_id:2, formula:"asd", meter_id:10,area_id:95}];
|
|
8
|
-
|
|
9
|
-
let fields=[{key:"id", as:"area_id"},{key:"code", as:"area_code"},"name", ["rules",["rule_id",{key:"formula", as:"rule"},[{name:"meters",object:true},[{key:"meter_id", as:"id"}]],[{name:"areas",object:false},["area_id"]]]]];
|
|
10
|
-
|
|
11
|
-
console.log(JSON.stringify(objectify(arr,fields)))
|