@hkdigital/lib-core 0.4.57 → 0.4.58
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/dist/auth/jwt/README.md +275 -0
- package/dist/auth/jwt/util.d.ts +16 -0
- package/dist/auth/jwt/util.js +48 -0
- package/dist/auth/jwt.js +1 -1
- package/dist/generic/data/typedef.d.ts +1 -0
- package/dist/generic/data/typedef.js +2 -0
- package/dist/generic/data/util/README.md +252 -0
- package/dist/generic/data/util/flat-tree.d.ts +18 -0
- package/dist/generic/data/util/flat-tree.js +261 -0
- package/dist/generic/data/util/typedef.d.ts +21 -0
- package/dist/generic/data/util/typedef.js +14 -0
- package/dist/generic/data.d.ts +1 -0
- package/dist/generic/data.js +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
# JWT Utilities
|
|
2
|
+
|
|
3
|
+
A comprehensive JSON Web Token (JWT) library for signing, verifying, and
|
|
4
|
+
decoding JWT tokens with proper error handling and type safety.
|
|
5
|
+
|
|
6
|
+
## Features
|
|
7
|
+
|
|
8
|
+
- **Sign tokens** with customizable algorithms and expiration
|
|
9
|
+
- **Verify tokens** with signature validation and expiration checks
|
|
10
|
+
- **Decode payloads** without verification for inspection purposes
|
|
11
|
+
- **Generate secret keys** for HMAC algorithms
|
|
12
|
+
- **Error handling** with specific error types for different failure modes
|
|
13
|
+
- **Type safety** with JSDoc type annotations
|
|
14
|
+
|
|
15
|
+
## Quick Start
|
|
16
|
+
|
|
17
|
+
```javascript
|
|
18
|
+
import { sign, verify, decodePayload, expiresAtUTC } from '$lib/auth/jwt.js';
|
|
19
|
+
|
|
20
|
+
// Create a token
|
|
21
|
+
const payload = { userId: 123, username: 'john' };
|
|
22
|
+
const secret = 'your-secret-key';
|
|
23
|
+
const token = sign(payload, secret);
|
|
24
|
+
|
|
25
|
+
// Verify and decode
|
|
26
|
+
const decoded = verify(token, secret);
|
|
27
|
+
console.log(decoded.userId); // 123
|
|
28
|
+
|
|
29
|
+
// Decode without verification (for inspection)
|
|
30
|
+
const payload = decodePayload(token);
|
|
31
|
+
console.log(payload.exp); // expiration timestamp
|
|
32
|
+
|
|
33
|
+
// Get human-readable expiration
|
|
34
|
+
const expiresAt = expiresAtUTC(payload);
|
|
35
|
+
console.log(expiresAt); // "Sun, 01 Jan 2023 00:00:00 GMT"
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## API Reference
|
|
39
|
+
|
|
40
|
+
### sign(claims, secretOrPrivateKey, options?)
|
|
41
|
+
|
|
42
|
+
Creates a JSON Web Token with the specified claims.
|
|
43
|
+
|
|
44
|
+
**Parameters:**
|
|
45
|
+
- `claims` (object) - JWT payload/claims
|
|
46
|
+
- `secretOrPrivateKey` (string|Buffer) - Secret for HMAC or private key
|
|
47
|
+
- `options` (object, optional) - Signing options
|
|
48
|
+
|
|
49
|
+
**Options:**
|
|
50
|
+
- `algorithm` (string) - Algorithm to use (default: 'HS512')
|
|
51
|
+
- `expiresIn` (string|number) - Token expiration (default: '24h')
|
|
52
|
+
- `issuer` (string) - Token issuer
|
|
53
|
+
- `audience` (string) - Token audience
|
|
54
|
+
- `subject` (string) - Token subject
|
|
55
|
+
|
|
56
|
+
**Returns:** `string` - JWT token
|
|
57
|
+
|
|
58
|
+
**Example:**
|
|
59
|
+
```javascript
|
|
60
|
+
const token = sign(
|
|
61
|
+
{ userId: 123, role: 'admin' },
|
|
62
|
+
'secret-key',
|
|
63
|
+
{
|
|
64
|
+
algorithm: 'HS256',
|
|
65
|
+
expiresIn: '1h',
|
|
66
|
+
issuer: 'my-app'
|
|
67
|
+
}
|
|
68
|
+
);
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### verify(token, secretOrPrivateKey, options?)
|
|
72
|
+
|
|
73
|
+
Verifies and decodes a JWT token.
|
|
74
|
+
|
|
75
|
+
**Parameters:**
|
|
76
|
+
- `token` (string) - JWT token to verify
|
|
77
|
+
- `secretOrPrivateKey` (string|Buffer) - Secret for HMAC or public key
|
|
78
|
+
- `options` (object, optional) - Verification options
|
|
79
|
+
|
|
80
|
+
**Options:**
|
|
81
|
+
- `algorithms` (string[]) - Allowed algorithms (default: ['HS512'])
|
|
82
|
+
- `issuer` (string) - Expected issuer
|
|
83
|
+
- `audience` (string) - Expected audience
|
|
84
|
+
- `ignoreExpiration` (boolean) - Skip expiration validation
|
|
85
|
+
|
|
86
|
+
**Returns:** `object` - Decoded JWT payload
|
|
87
|
+
|
|
88
|
+
**Throws:**
|
|
89
|
+
- `TokenExpiredError` - Token has expired
|
|
90
|
+
- `InvalidSignatureError` - Invalid signature
|
|
91
|
+
- `JsonWebTokenError` - Other JWT validation errors
|
|
92
|
+
- `NotBeforeError` - Token not yet valid
|
|
93
|
+
|
|
94
|
+
**Example:**
|
|
95
|
+
```javascript
|
|
96
|
+
try {
|
|
97
|
+
const decoded = verify(token, 'secret-key');
|
|
98
|
+
console.log('User ID:', decoded.userId);
|
|
99
|
+
} catch (error) {
|
|
100
|
+
if (error instanceof TokenExpiredError) {
|
|
101
|
+
console.log('Token expired at:', error.expiredAt);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### decodePayload(token)
|
|
107
|
+
|
|
108
|
+
Decodes JWT payload without verification. Useful for inspecting token claims
|
|
109
|
+
before verification or when verification is not required.
|
|
110
|
+
|
|
111
|
+
**Parameters:**
|
|
112
|
+
- `token` (string) - JWT token to decode
|
|
113
|
+
|
|
114
|
+
**Returns:** `object` - Decoded JWT payload
|
|
115
|
+
|
|
116
|
+
**Throws:**
|
|
117
|
+
- `Error` - Invalid token format or malformed payload
|
|
118
|
+
|
|
119
|
+
**Example:**
|
|
120
|
+
```javascript
|
|
121
|
+
// Inspect token without verification
|
|
122
|
+
const payload = decodePayload(token);
|
|
123
|
+
console.log('Token expires:', payload.exp);
|
|
124
|
+
console.log('Issued at:', payload.iat);
|
|
125
|
+
|
|
126
|
+
// Check if token is expired before verification
|
|
127
|
+
if (payload.exp && payload.exp < Date.now() / 1000) {
|
|
128
|
+
console.log('Token is expired');
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### expiresAtUTC(token)
|
|
133
|
+
|
|
134
|
+
Converts the `exp` claim of a decoded token to a human-readable UTC string.
|
|
135
|
+
|
|
136
|
+
**Parameters:**
|
|
137
|
+
- `token` (object) - Decoded JWT payload with optional `exp` claim
|
|
138
|
+
|
|
139
|
+
**Returns:** `string|null` - UTC string or null if no expiration
|
|
140
|
+
|
|
141
|
+
**Example:**
|
|
142
|
+
```javascript
|
|
143
|
+
const payload = decodePayload(token);
|
|
144
|
+
const expiresAt = expiresAtUTC(payload);
|
|
145
|
+
|
|
146
|
+
if (expiresAt) {
|
|
147
|
+
console.log('Token expires at:', expiresAt);
|
|
148
|
+
// "Sun, 01 Jan 2023 00:00:00 GMT"
|
|
149
|
+
} else {
|
|
150
|
+
console.log('Token never expires');
|
|
151
|
+
}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## Error Types
|
|
155
|
+
|
|
156
|
+
The library provides specific error types for different failure modes:
|
|
157
|
+
|
|
158
|
+
### TokenExpiredError
|
|
159
|
+
Thrown when a token has expired.
|
|
160
|
+
- `message` - Error description
|
|
161
|
+
- `expiredAt` - Date when token expired
|
|
162
|
+
|
|
163
|
+
### InvalidSignatureError
|
|
164
|
+
Thrown when token signature is invalid.
|
|
165
|
+
- `message` - Error description
|
|
166
|
+
|
|
167
|
+
### JsonWebTokenError
|
|
168
|
+
Thrown for general JWT validation errors.
|
|
169
|
+
- `message` - Error description
|
|
170
|
+
|
|
171
|
+
### NotBeforeError
|
|
172
|
+
Thrown when token is not yet valid (nbf claim).
|
|
173
|
+
- `message` - Error description
|
|
174
|
+
- `date` - Date when token becomes valid
|
|
175
|
+
|
|
176
|
+
## Security Best Practices
|
|
177
|
+
|
|
178
|
+
### Secret Key Management
|
|
179
|
+
- Use cryptographically strong secrets (256+ bits for HMAC)
|
|
180
|
+
- Store secrets securely (environment variables, key management services)
|
|
181
|
+
- Rotate secrets regularly
|
|
182
|
+
- Never commit secrets to version control
|
|
183
|
+
|
|
184
|
+
```javascript
|
|
185
|
+
import { generateSecretKeyForHmacBase58 } from '$lib/auth/jwt.js';
|
|
186
|
+
|
|
187
|
+
// Generate a secure secret
|
|
188
|
+
const secret = generateSecretKeyForHmacBase58();
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
### Token Validation
|
|
192
|
+
- Always verify tokens before trusting claims
|
|
193
|
+
- Use specific algorithm allowlists in verification
|
|
194
|
+
- Validate issuer, audience, and other claims as needed
|
|
195
|
+
- Handle expiration appropriately for your use case
|
|
196
|
+
|
|
197
|
+
```javascript
|
|
198
|
+
const decoded = verify(token, secret, {
|
|
199
|
+
algorithms: ['HS256'], // Only allow specific algorithms
|
|
200
|
+
issuer: 'trusted-issuer',
|
|
201
|
+
audience: 'my-app'
|
|
202
|
+
});
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
### Token Storage
|
|
206
|
+
- Store tokens securely (httpOnly cookies, secure storage)
|
|
207
|
+
- Use short expiration times when possible
|
|
208
|
+
- Implement token refresh mechanisms
|
|
209
|
+
- Clear tokens on logout
|
|
210
|
+
|
|
211
|
+
## Common Patterns
|
|
212
|
+
|
|
213
|
+
### Token Inspection Middleware
|
|
214
|
+
```javascript
|
|
215
|
+
function inspectToken(token) {
|
|
216
|
+
try {
|
|
217
|
+
const payload = decodePayload(token);
|
|
218
|
+
const expiresAt = expiresAtUTC(payload);
|
|
219
|
+
|
|
220
|
+
return {
|
|
221
|
+
isExpired: payload.exp && payload.exp < Date.now() / 1000,
|
|
222
|
+
expiresAt,
|
|
223
|
+
userId: payload.userId,
|
|
224
|
+
role: payload.role
|
|
225
|
+
};
|
|
226
|
+
} catch (error) {
|
|
227
|
+
return { isValid: false, error: error.message };
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
### Token Refresh Check
|
|
233
|
+
```javascript
|
|
234
|
+
function shouldRefreshToken(token) {
|
|
235
|
+
const payload = decodePayload(token);
|
|
236
|
+
if (!payload.exp) return false;
|
|
237
|
+
|
|
238
|
+
const now = Date.now() / 1000;
|
|
239
|
+
const timeUntilExpiry = payload.exp - now;
|
|
240
|
+
const refreshThreshold = 5 * 60; // 5 minutes
|
|
241
|
+
|
|
242
|
+
return timeUntilExpiry < refreshThreshold && timeUntilExpiry > 0;
|
|
243
|
+
}
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
### Safe Token Verification
|
|
247
|
+
```javascript
|
|
248
|
+
function safeVerifyToken(token, secret) {
|
|
249
|
+
try {
|
|
250
|
+
return { success: true, payload: verify(token, secret) };
|
|
251
|
+
} catch (error) {
|
|
252
|
+
return {
|
|
253
|
+
success: false,
|
|
254
|
+
error: error.constructor.name,
|
|
255
|
+
message: error.message
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
## Testing
|
|
262
|
+
|
|
263
|
+
Run the JWT tests:
|
|
264
|
+
|
|
265
|
+
```bash
|
|
266
|
+
pnpm test:file src/lib/auth/jwt/
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
The test suite covers:
|
|
270
|
+
- Token signing with various options
|
|
271
|
+
- Token verification with different scenarios
|
|
272
|
+
- Error handling for expired/invalid tokens
|
|
273
|
+
- Payload decoding without verification
|
|
274
|
+
- UTC time conversion
|
|
275
|
+
- Edge cases and error conditions
|
package/dist/auth/jwt/util.d.ts
CHANGED
|
@@ -42,3 +42,19 @@ export function verify(token: string, secretOrPrivateKey: import("./typedef.js")
|
|
|
42
42
|
* @returns {Error} - The corresponding internal error
|
|
43
43
|
*/
|
|
44
44
|
export function castJwtError(error: Error): Error;
|
|
45
|
+
/**
|
|
46
|
+
* Decodes the payload of a JSON Web Token without verification
|
|
47
|
+
*
|
|
48
|
+
* @param {string} token - A JSON Web Token
|
|
49
|
+
*
|
|
50
|
+
* @returns {object} claims - The decoded JWT payload
|
|
51
|
+
*/
|
|
52
|
+
export function decodePayload(token: string): object;
|
|
53
|
+
/**
|
|
54
|
+
* Returns the "exp" (expiresAt) property of a token as UTC string
|
|
55
|
+
*
|
|
56
|
+
* @param {object} token - Decoded JWT payload/claims
|
|
57
|
+
*
|
|
58
|
+
* @returns {string|null} "expires at" as UTC string or null if no exp claim
|
|
59
|
+
*/
|
|
60
|
+
export function expiresAtUTC(token: object): string | null;
|
package/dist/auth/jwt/util.js
CHANGED
|
@@ -144,3 +144,51 @@ export function castJwtError(error) {
|
|
|
144
144
|
// Return original error if not a known JWT error
|
|
145
145
|
return error;
|
|
146
146
|
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Decodes the payload of a JSON Web Token without verification
|
|
150
|
+
*
|
|
151
|
+
* @param {string} token - A JSON Web Token
|
|
152
|
+
*
|
|
153
|
+
* @returns {object} claims - The decoded JWT payload
|
|
154
|
+
*/
|
|
155
|
+
export function decodePayload(token) {
|
|
156
|
+
expect.notEmptyString(token);
|
|
157
|
+
|
|
158
|
+
const firstDot = token.indexOf('.');
|
|
159
|
+
|
|
160
|
+
if (firstDot === -1) {
|
|
161
|
+
throw new Error(
|
|
162
|
+
'Invalid token, missing [.] token as payload start indicator'
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const lastDot = token.lastIndexOf('.');
|
|
167
|
+
|
|
168
|
+
if (lastDot === firstDot) {
|
|
169
|
+
throw new Error(
|
|
170
|
+
'Invalid token, missing second [.] token as payload end indicator'
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const payload = token.slice(firstDot + 1, lastDot);
|
|
175
|
+
|
|
176
|
+
return JSON.parse(atob(payload));
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Returns the "exp" (expiresAt) property of a token as UTC string
|
|
181
|
+
*
|
|
182
|
+
* @param {object} token - Decoded JWT payload/claims
|
|
183
|
+
*
|
|
184
|
+
* @returns {string|null} "expires at" as UTC string or null if no exp claim
|
|
185
|
+
*/
|
|
186
|
+
export function expiresAtUTC(token) {
|
|
187
|
+
expect.object(token);
|
|
188
|
+
|
|
189
|
+
if ('exp' in token) {
|
|
190
|
+
return new Date(1000 * token.exp).toUTCString();
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return null;
|
|
194
|
+
}
|
package/dist/auth/jwt.js
CHANGED
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
# Flat Tree Utilities
|
|
2
|
+
|
|
3
|
+
Tree serialization and deserialization utilities that convert between
|
|
4
|
+
hierarchical tree structures and flat tree format (ft1).
|
|
5
|
+
|
|
6
|
+
## Overview
|
|
7
|
+
|
|
8
|
+
The flat tree utilities provide bidirectional conversion between:
|
|
9
|
+
- **Hierarchical Trees**: Standard nested object structures with children
|
|
10
|
+
- **Flat Tree Format (ft1)**: Compact serialized format with separate
|
|
11
|
+
nodes and edges
|
|
12
|
+
|
|
13
|
+
## Key Features
|
|
14
|
+
|
|
15
|
+
- **Format Versioning**: ft1 format with forward compatibility
|
|
16
|
+
- **Property Preservation**: Maintains original property names
|
|
17
|
+
(`children`, `items`, etc.)
|
|
18
|
+
- **Shared Reference Handling**: Uses WeakMap to track objects that
|
|
19
|
+
appear multiple times
|
|
20
|
+
- **ID Collision Avoidance**: No conflicts between user IDs and tree
|
|
21
|
+
structure IDs
|
|
22
|
+
- **Efficient Serialization**: Compact flat edge array format
|
|
23
|
+
|
|
24
|
+
## API Reference
|
|
25
|
+
|
|
26
|
+
### `buildTree(flatTree, options)`
|
|
27
|
+
|
|
28
|
+
Converts ft1 flat tree format to hierarchical tree structure.
|
|
29
|
+
|
|
30
|
+
**Parameters:**
|
|
31
|
+
- `flatTree` - FlatTree object in ft1 format
|
|
32
|
+
- `options` - Optional configuration object
|
|
33
|
+
|
|
34
|
+
**Returns:** Reconstructed hierarchical tree or null
|
|
35
|
+
|
|
36
|
+
**Example:**
|
|
37
|
+
```javascript
|
|
38
|
+
import { buildTree } from '$lib/generic/data/util/flat-tree.js';
|
|
39
|
+
|
|
40
|
+
const flatTree = {
|
|
41
|
+
format: 'ft1',
|
|
42
|
+
properties: ['children'],
|
|
43
|
+
nodes: [
|
|
44
|
+
{ id: 'root', name: 'Root' },
|
|
45
|
+
{ id: 'child1', name: 'Child 1' }
|
|
46
|
+
],
|
|
47
|
+
edges: [0, 0, 1] // root->children->child1
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const hierarchical = buildTree(flatTree);
|
|
51
|
+
// Result: { id: 'root', name: 'Root', children: [{ id: 'child1', name: 'Child 1' }] }
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### `flattenTree(hierarchicalTree, options)`
|
|
55
|
+
|
|
56
|
+
Converts hierarchical tree to ft1 flat tree format.
|
|
57
|
+
|
|
58
|
+
**Parameters:**
|
|
59
|
+
- `hierarchicalTree` - Nested object structure with children
|
|
60
|
+
- `options` - Optional configuration object
|
|
61
|
+
- `childrenKey` (default: 'children') - Primary children property name
|
|
62
|
+
|
|
63
|
+
**Returns:** FlatTree object in ft1 format
|
|
64
|
+
|
|
65
|
+
**Example:**
|
|
66
|
+
```javascript
|
|
67
|
+
import { flattenTree } from '$lib/generic/data/util/flat-tree.js';
|
|
68
|
+
|
|
69
|
+
const hierarchical = {
|
|
70
|
+
id: 'root',
|
|
71
|
+
name: 'Root',
|
|
72
|
+
children: [
|
|
73
|
+
{ id: 'child1', name: 'Child 1' }
|
|
74
|
+
]
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const flatTree = flattenTree(hierarchical);
|
|
78
|
+
// Result: { format: 'ft1', properties: ['children'], nodes: [...], edges: [...] }
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## ft1 Format Specification
|
|
82
|
+
|
|
83
|
+
The ft1 format consists of four main components:
|
|
84
|
+
|
|
85
|
+
### Format Structure
|
|
86
|
+
|
|
87
|
+
```javascript
|
|
88
|
+
{
|
|
89
|
+
format: 'ft1', // Format version identifier
|
|
90
|
+
properties: string[], // Array of property names used in edges
|
|
91
|
+
nodes: object[], // Array of node objects (index 0 = root)
|
|
92
|
+
edges: number[] // Flat array: [from, prop, to, from, prop, to, ...]
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Properties Array
|
|
97
|
+
|
|
98
|
+
Maps property indices to names for edge decoding:
|
|
99
|
+
```javascript
|
|
100
|
+
properties: ['children', 'items', 'metadata']
|
|
101
|
+
// index: 0 1 2
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### Nodes Array
|
|
105
|
+
|
|
106
|
+
Contains node data with children properties removed:
|
|
107
|
+
```javascript
|
|
108
|
+
nodes: [
|
|
109
|
+
{ id: 'root', name: 'Root' }, // index 0 (always root)
|
|
110
|
+
{ id: 'child1', name: 'Child 1' }, // index 1
|
|
111
|
+
{ id: 'item1', name: 'Item 1' } // index 2
|
|
112
|
+
]
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### Edges Array
|
|
116
|
+
|
|
117
|
+
Flat array in groups of 3: `[from_index, property_index, to_index]`
|
|
118
|
+
```javascript
|
|
119
|
+
edges: [
|
|
120
|
+
0, 0, 1, // root(0) -children(0)-> child1(1)
|
|
121
|
+
0, 1, 2 // root(0) -items(1)-> item1(2)
|
|
122
|
+
]
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## Shared Object References
|
|
126
|
+
|
|
127
|
+
Objects that appear multiple times in the tree are automatically
|
|
128
|
+
handled using WeakMap tracking:
|
|
129
|
+
|
|
130
|
+
```javascript
|
|
131
|
+
const sharedNode = { id: 'shared', name: 'Shared' };
|
|
132
|
+
|
|
133
|
+
const hierarchical = {
|
|
134
|
+
id: 'root',
|
|
135
|
+
children: [
|
|
136
|
+
{ id: 'left', children: [sharedNode] },
|
|
137
|
+
{ id: 'right', children: [sharedNode] } // Same object reference
|
|
138
|
+
]
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
const flattened = flattenTree(hierarchical);
|
|
142
|
+
const rebuilt = buildTree(flattened);
|
|
143
|
+
|
|
144
|
+
// Shared reference is preserved
|
|
145
|
+
console.log(rebuilt.children[0].children[0] === rebuilt.children[1].children[0]); // true
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## Property Detection
|
|
149
|
+
|
|
150
|
+
The flattening process automatically detects child-containing properties:
|
|
151
|
+
|
|
152
|
+
```javascript
|
|
153
|
+
const tree = {
|
|
154
|
+
id: 'root',
|
|
155
|
+
children: [{ id: 'child1' }], // Detected as child property
|
|
156
|
+
items: [{ id: 'item1' }], // Detected as child property
|
|
157
|
+
metadata: 'string value' // Not a child property (ignored)
|
|
158
|
+
};
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
## Roundtrip Conversion
|
|
162
|
+
|
|
163
|
+
The utilities maintain data integrity across roundtrip conversions:
|
|
164
|
+
|
|
165
|
+
```javascript
|
|
166
|
+
const original = { /* complex tree */ };
|
|
167
|
+
const flattened = flattenTree(original);
|
|
168
|
+
const rebuilt = buildTree(flattened);
|
|
169
|
+
const reflattened = flattenTree(rebuilt);
|
|
170
|
+
|
|
171
|
+
// Data integrity preserved
|
|
172
|
+
console.log(JSON.stringify(flattened) === JSON.stringify(reflattened)); // true
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
## Error Handling
|
|
176
|
+
|
|
177
|
+
The utilities provide clear error messages for invalid data:
|
|
178
|
+
|
|
179
|
+
- **Unsupported format**: When format is not 'ft1'
|
|
180
|
+
- **Invalid edges**: When edges array length is not multiple of 3
|
|
181
|
+
- **Invalid indices**: When node or property indices are out of bounds
|
|
182
|
+
- **Missing data**: When required fields are missing
|
|
183
|
+
|
|
184
|
+
## Performance Considerations
|
|
185
|
+
|
|
186
|
+
- **WeakMap Usage**: Efficient object reference tracking
|
|
187
|
+
- **Flat Edge Array**: Compact memory usage for large trees
|
|
188
|
+
- **Property Index Mapping**: Fast property name lookup
|
|
189
|
+
- **No Deep Cloning**: Shallow copies preserve performance
|
|
190
|
+
|
|
191
|
+
## Use Cases
|
|
192
|
+
|
|
193
|
+
### Configuration Trees
|
|
194
|
+
```javascript
|
|
195
|
+
const config = {
|
|
196
|
+
server: {
|
|
197
|
+
children: [
|
|
198
|
+
{ name: 'port', value: 3000 },
|
|
199
|
+
{ name: 'host', value: 'localhost' }
|
|
200
|
+
]
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
### File System Trees
|
|
206
|
+
```javascript
|
|
207
|
+
const fileTree = {
|
|
208
|
+
path: '/',
|
|
209
|
+
children: [
|
|
210
|
+
{ path: '/src', children: [{ path: '/src/index.js' }] },
|
|
211
|
+
{ path: '/package.json' }
|
|
212
|
+
]
|
|
213
|
+
};
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
### Menu Structures
|
|
217
|
+
```javascript
|
|
218
|
+
const menu = {
|
|
219
|
+
title: 'Main',
|
|
220
|
+
items: [
|
|
221
|
+
{ title: 'File', items: [{ title: 'New' }, { title: 'Open' }] },
|
|
222
|
+
{ title: 'Edit', items: [{ title: 'Copy' }, { title: 'Paste' }] }
|
|
223
|
+
]
|
|
224
|
+
};
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
## Migration Guide
|
|
228
|
+
|
|
229
|
+
### From Legacy Formats
|
|
230
|
+
|
|
231
|
+
If migrating from older tree formats, ensure your data structure
|
|
232
|
+
follows these patterns:
|
|
233
|
+
|
|
234
|
+
1. **Root Object**: Single root node with child properties
|
|
235
|
+
2. **Array Children**: Child properties should be arrays of objects
|
|
236
|
+
3. **Object Structure**: Each node should be a plain object
|
|
237
|
+
4. **No Circular References**: Avoid circular references (use shared
|
|
238
|
+
objects instead)
|
|
239
|
+
|
|
240
|
+
### Type Safety
|
|
241
|
+
|
|
242
|
+
Use JSDoc type annotations for better development experience:
|
|
243
|
+
|
|
244
|
+
```javascript
|
|
245
|
+
/**
|
|
246
|
+
* @param {import('./typedef.js').FlatTree<MyNodeType>} flatTree
|
|
247
|
+
* @returns {MyNodeType|null}
|
|
248
|
+
*/
|
|
249
|
+
function processTree(flatTree) {
|
|
250
|
+
return buildTree(flatTree);
|
|
251
|
+
}
|
|
252
|
+
```
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build a hierarchical tree from ft1 flat tree format
|
|
3
|
+
*
|
|
4
|
+
* @template {object} T
|
|
5
|
+
* @param {import('./typedef.js').FlatTree<T>} flatTree - Flat tree data
|
|
6
|
+
*
|
|
7
|
+
* @returns {T|null} reconstructed hierarchical tree or null
|
|
8
|
+
*/
|
|
9
|
+
export function buildTree<T extends object>(flatTree: import("./typedef.js").FlatTree<T>): T | null;
|
|
10
|
+
/**
|
|
11
|
+
* Flatten a hierarchical tree into ft1 flat tree format
|
|
12
|
+
*
|
|
13
|
+
* @template {object} T
|
|
14
|
+
* @param {T} hierarchicalTree - Hierarchical tree with nested children
|
|
15
|
+
*
|
|
16
|
+
* @returns {import('./typedef.js').FlatTree<T>} flat tree data
|
|
17
|
+
*/
|
|
18
|
+
export function flattenTree<T extends object>(hierarchicalTree: T): import("./typedef.js").FlatTree<T>;
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
/* ------------------------------------------------------------------ Imports */
|
|
2
|
+
|
|
3
|
+
import * as expect from '../../../util/expect.js';
|
|
4
|
+
|
|
5
|
+
/* ------------------------------------------------------------------ Exports */
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Build a hierarchical tree from ft1 flat tree format
|
|
9
|
+
*
|
|
10
|
+
* @template {object} T
|
|
11
|
+
* @param {import('./typedef.js').FlatTree<T>} flatTree - Flat tree data
|
|
12
|
+
*
|
|
13
|
+
* @returns {T|null} reconstructed hierarchical tree or null
|
|
14
|
+
*/
|
|
15
|
+
export function buildTree(flatTree) {
|
|
16
|
+
expect.object(flatTree);
|
|
17
|
+
|
|
18
|
+
const { format, properties, nodes, edges } = flatTree;
|
|
19
|
+
|
|
20
|
+
if (format !== 'ft1') {
|
|
21
|
+
throw new Error(`Unsupported format: ${format}. Expected 'ft1'`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (!nodes || !nodes.length) {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (!edges || !edges.length) {
|
|
29
|
+
return { ...nodes[0] };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (edges.length % 3 !== 0) {
|
|
33
|
+
throw new Error('Invalid edges array: length must be multiple of 3');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Create copies of all nodes to avoid mutating original data
|
|
37
|
+
const nodesCopy = nodes.map(node => ({ ...node }));
|
|
38
|
+
|
|
39
|
+
// Process edges in groups of 3: [from, prop, to]
|
|
40
|
+
for (let i = 0; i < edges.length; i += 3) {
|
|
41
|
+
const fromIndex = edges[i];
|
|
42
|
+
const propIndex = edges[i + 1];
|
|
43
|
+
const toIndex = edges[i + 2];
|
|
44
|
+
|
|
45
|
+
// Validate indices
|
|
46
|
+
if (fromIndex >= nodesCopy.length || toIndex >= nodesCopy.length) {
|
|
47
|
+
throw new Error(`Invalid node index in edge [${fromIndex}, ${propIndex}, ${toIndex}]`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (propIndex >= properties.length) {
|
|
51
|
+
throw new Error(`Invalid property index: ${propIndex}`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const fromNode = nodesCopy[fromIndex];
|
|
55
|
+
const toNode = nodesCopy[toIndex];
|
|
56
|
+
const propertyName = properties[propIndex];
|
|
57
|
+
|
|
58
|
+
// Add child to parent's property
|
|
59
|
+
/** @type {any} */
|
|
60
|
+
const dynamicFromNode = fromNode;
|
|
61
|
+
|
|
62
|
+
if (!dynamicFromNode[propertyName]) {
|
|
63
|
+
dynamicFromNode[propertyName] = [toNode];
|
|
64
|
+
} else if (Array.isArray(dynamicFromNode[propertyName])) {
|
|
65
|
+
// Check if this node is already in the array (shared reference)
|
|
66
|
+
if (!dynamicFromNode[propertyName].includes(toNode)) {
|
|
67
|
+
dynamicFromNode[propertyName].push(toNode);
|
|
68
|
+
}
|
|
69
|
+
} else {
|
|
70
|
+
// Convert single child to array
|
|
71
|
+
dynamicFromNode[propertyName] = [dynamicFromNode[propertyName], toNode];
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return nodesCopy[0];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Flatten a hierarchical tree into ft1 flat tree format
|
|
80
|
+
*
|
|
81
|
+
* @template {object} T
|
|
82
|
+
* @param {T} hierarchicalTree - Hierarchical tree with nested children
|
|
83
|
+
*
|
|
84
|
+
* @returns {import('./typedef.js').FlatTree<T>} flat tree data
|
|
85
|
+
*/
|
|
86
|
+
export function flattenTree(hierarchicalTree) {
|
|
87
|
+
expect.object(hierarchicalTree);
|
|
88
|
+
|
|
89
|
+
/** @type {T[]} */
|
|
90
|
+
const nodes = [];
|
|
91
|
+
|
|
92
|
+
/** @type {number[]} */
|
|
93
|
+
const edges = [];
|
|
94
|
+
|
|
95
|
+
/** @type {Set<string>} */
|
|
96
|
+
const propertySet = new Set();
|
|
97
|
+
|
|
98
|
+
/** @type {WeakMap<object, number>} */
|
|
99
|
+
const objectToIndex = new WeakMap();
|
|
100
|
+
|
|
101
|
+
// First pass: collect all child-containing properties
|
|
102
|
+
const childProperties = new Set();
|
|
103
|
+
findChildProperties(hierarchicalTree, childProperties);
|
|
104
|
+
|
|
105
|
+
// Add root node (always index 0)
|
|
106
|
+
const rootCopy = extractNodeData(hierarchicalTree, childProperties);
|
|
107
|
+
nodes.push(rootCopy);
|
|
108
|
+
objectToIndex.set(hierarchicalTree, 0);
|
|
109
|
+
|
|
110
|
+
// Process children recursively
|
|
111
|
+
processChildrenFt1(
|
|
112
|
+
hierarchicalTree,
|
|
113
|
+
0,
|
|
114
|
+
nodes,
|
|
115
|
+
edges,
|
|
116
|
+
propertySet,
|
|
117
|
+
objectToIndex,
|
|
118
|
+
childProperties
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
// Convert property set to sorted array for consistent output
|
|
122
|
+
const properties = Array.from(propertySet).sort();
|
|
123
|
+
|
|
124
|
+
// Convert property names to indices in edges array
|
|
125
|
+
for (let i = 1; i < edges.length; i += 3) {
|
|
126
|
+
const propertyName = /** @type {string} */ (edges[i]);
|
|
127
|
+
edges[i] = properties.indexOf(propertyName);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return /** @type {import('./typedef.js').FlatTree<T>} */ ({
|
|
131
|
+
format: 'ft1',
|
|
132
|
+
properties,
|
|
133
|
+
nodes,
|
|
134
|
+
edges
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/* ---------------------------------------------------------- Private methods */
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Extract node data excluding children properties
|
|
142
|
+
*
|
|
143
|
+
* @param {object} node - Source node
|
|
144
|
+
* @param {Set<string>} propertiesToRemove - Set of property names to remove
|
|
145
|
+
*
|
|
146
|
+
* @returns {object} node data without children properties
|
|
147
|
+
*/
|
|
148
|
+
function extractNodeData(node, propertiesToRemove) {
|
|
149
|
+
/** @type {any} */
|
|
150
|
+
const nodeData = { ...node };
|
|
151
|
+
|
|
152
|
+
// Remove all child-containing properties
|
|
153
|
+
for (const key of propertiesToRemove) {
|
|
154
|
+
delete nodeData[key];
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return nodeData;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Find all properties that contain child objects
|
|
162
|
+
*
|
|
163
|
+
* @param {object} node - Node to analyze
|
|
164
|
+
* @param {Set<string>} childProperties - Set to collect property names
|
|
165
|
+
*/
|
|
166
|
+
function findChildProperties(node, childProperties) {
|
|
167
|
+
// Find array properties that contain objects (potential children)
|
|
168
|
+
for (const [key, value] of Object.entries(node)) {
|
|
169
|
+
if (Array.isArray(value) && value.length > 0) {
|
|
170
|
+
// Check if array contains objects (potential children)
|
|
171
|
+
const hasObjects = value.some(item => item && typeof item === 'object');
|
|
172
|
+
if (hasObjects) {
|
|
173
|
+
childProperties.add(key);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Recursively check child nodes
|
|
179
|
+
for (const key of [...childProperties]) { // Copy set to avoid modification during iteration
|
|
180
|
+
const children = node[key];
|
|
181
|
+
if (Array.isArray(children)) {
|
|
182
|
+
for (const child of children) {
|
|
183
|
+
if (child && typeof child === 'object') {
|
|
184
|
+
findChildProperties(child, childProperties);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Recursively process children for ft1 format
|
|
193
|
+
*
|
|
194
|
+
* @param {object} parentNode - Parent node with children
|
|
195
|
+
* @param {number} parentIndex - Parent node index
|
|
196
|
+
* @param {object[]} nodes - Nodes array to populate
|
|
197
|
+
* @param {(number|string)[]} edges - Edges array to populate (mixed types temporarily)
|
|
198
|
+
* @param {Set<string>} propertySet - Set of property names
|
|
199
|
+
* @param {WeakMap<object, number>} objectToIndex - Map of objects to indices
|
|
200
|
+
* @param {Set<string>} childProperties - Set of all child-containing properties
|
|
201
|
+
*/
|
|
202
|
+
function processChildrenFt1(parentNode, parentIndex, nodes, edges, propertySet, objectToIndex, childProperties) {
|
|
203
|
+
// Process all child-containing properties
|
|
204
|
+
for (const property of childProperties) {
|
|
205
|
+
const children = parentNode[property];
|
|
206
|
+
|
|
207
|
+
if (!children) {
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (Array.isArray(children)) {
|
|
212
|
+
for (const child of children) {
|
|
213
|
+
if (child && typeof child === 'object') {
|
|
214
|
+
processChildFt1(child, parentIndex, property, nodes, edges, propertySet, objectToIndex, childProperties);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
} else if (children && typeof children === 'object') {
|
|
218
|
+
processChildFt1(children, parentIndex, property, nodes, edges, propertySet, objectToIndex, childProperties);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Process a single child node for ft1 format
|
|
225
|
+
*
|
|
226
|
+
* @param {object} child - Child node
|
|
227
|
+
* @param {number} parentIndex - Parent node index
|
|
228
|
+
* @param {string} property - Property name where this child belongs
|
|
229
|
+
* @param {object[]} nodes - Nodes array to populate
|
|
230
|
+
* @param {(number|string)[]} edges - Edges array to populate (mixed types temporarily)
|
|
231
|
+
* @param {Set<string>} propertySet - Set of property names
|
|
232
|
+
* @param {WeakMap<object, number>} objectToIndex - Map of objects to indices
|
|
233
|
+
* @param {Set<string>} childProperties - Set of all child-containing properties
|
|
234
|
+
*/
|
|
235
|
+
function processChildFt1(child, parentIndex, property, nodes, edges, propertySet, objectToIndex, childProperties) {
|
|
236
|
+
if (!child || typeof child !== 'object') {
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Track property name
|
|
241
|
+
propertySet.add(property);
|
|
242
|
+
|
|
243
|
+
// Check if we've seen this object before
|
|
244
|
+
let childIndex = objectToIndex.get(child);
|
|
245
|
+
|
|
246
|
+
if (childIndex === undefined) {
|
|
247
|
+
// First time seeing this object - add it to nodes
|
|
248
|
+
childIndex = nodes.length;
|
|
249
|
+
const childCopy = extractNodeData(child, childProperties);
|
|
250
|
+
nodes.push(childCopy);
|
|
251
|
+
objectToIndex.set(child, childIndex);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Add edge (temporarily store property name as string, will convert to index later)
|
|
255
|
+
edges.push(parentIndex, property, childIndex);
|
|
256
|
+
|
|
257
|
+
// If this is a new object, recursively process its children
|
|
258
|
+
if (objectToIndex.get(child) === childIndex) {
|
|
259
|
+
processChildrenFt1(child, childIndex, nodes, edges, propertySet, objectToIndex, childProperties);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Flat tree data structure in ft1 format
|
|
3
|
+
*/
|
|
4
|
+
export type FlatTree<T extends object> = {
|
|
5
|
+
/**
|
|
6
|
+
* - Format version ('ft1')
|
|
7
|
+
*/
|
|
8
|
+
format: string;
|
|
9
|
+
/**
|
|
10
|
+
* - Array of property names used in edges
|
|
11
|
+
*/
|
|
12
|
+
properties: string[];
|
|
13
|
+
/**
|
|
14
|
+
* - Array of node objects (index 0 = root)
|
|
15
|
+
*/
|
|
16
|
+
nodes: T[];
|
|
17
|
+
/**
|
|
18
|
+
* - Flat array: [from, prop, to, from, prop, to, ...]
|
|
19
|
+
*/
|
|
20
|
+
edges: number[];
|
|
21
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/* ------------------------------------------------------------------ Typedef */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Flat tree data structure in ft1 format
|
|
5
|
+
*
|
|
6
|
+
* @template {object} T
|
|
7
|
+
* @typedef {object} FlatTree
|
|
8
|
+
* @property {string} format - Format version ('ft1')
|
|
9
|
+
* @property {string[]} properties - Array of property names used in edges
|
|
10
|
+
* @property {T[]} nodes - Array of node objects (index 0 = root)
|
|
11
|
+
* @property {number[]} edges - Flat array: [from, prop, to, from, prop, to, ...]
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export {};
|
package/dist/generic/data.d.ts
CHANGED
package/dist/generic/data.js
CHANGED