boma 2.0.3 → 2.0.4
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/index.d.ts +17 -3
- package/dist/index.js +215 -36
- package/package.json +1 -1
- package/readme.md +15 -3
package/dist/index.d.ts
CHANGED
|
@@ -4,6 +4,8 @@ export type SerializableObject = {
|
|
|
4
4
|
[key: string]: Serializable;
|
|
5
5
|
};
|
|
6
6
|
export type Serializable = SerializablePrimitive | SerializableArray | SerializableObject;
|
|
7
|
+
type JSONLikeObject = Record<string, unknown>;
|
|
8
|
+
type JSONLikeArray = unknown[];
|
|
7
9
|
export interface ReadJSONProps {
|
|
8
10
|
filePath: string;
|
|
9
11
|
parseJSON?: boolean;
|
|
@@ -11,15 +13,17 @@ export interface ReadJSONProps {
|
|
|
11
13
|
}
|
|
12
14
|
export interface SaveJSONProps {
|
|
13
15
|
filePath: string;
|
|
14
|
-
objToSave:
|
|
16
|
+
objToSave: unknown;
|
|
15
17
|
format?: boolean;
|
|
16
18
|
logSaving?: boolean;
|
|
19
|
+
replaceNonSerializable?: boolean;
|
|
17
20
|
}
|
|
18
21
|
export interface addToJSONProps {
|
|
19
22
|
filePath: string;
|
|
20
|
-
dataToAdd:
|
|
23
|
+
dataToAdd: JSONLikeObject | JSONLikeArray;
|
|
21
24
|
format?: boolean;
|
|
22
25
|
logSaving?: boolean;
|
|
26
|
+
replaceNonSerializable?: boolean;
|
|
23
27
|
}
|
|
24
28
|
export type ErrorWithCode = Error & {
|
|
25
29
|
code: string;
|
|
@@ -27,10 +31,20 @@ export type ErrorWithCode = Error & {
|
|
|
27
31
|
export type ErrorWithMessage = Error & {
|
|
28
32
|
message: any;
|
|
29
33
|
};
|
|
34
|
+
export type SerializationIssueKind = 'undefined' | 'function' | 'symbol' | 'bigint' | 'non-finite-number' | 'circular';
|
|
35
|
+
export interface SerializationIssue {
|
|
36
|
+
path: string;
|
|
37
|
+
kind: SerializationIssueKind;
|
|
38
|
+
message: string;
|
|
39
|
+
circularTo?: string;
|
|
40
|
+
}
|
|
30
41
|
export declare const isErrorWithCode: (error: unknown) => error is ErrorWithCode;
|
|
31
42
|
export declare const isErrorWithMessage: (error: unknown) => error is ErrorWithMessage;
|
|
32
43
|
export declare const isSyntaxError: (error: unknown) => error is SyntaxError;
|
|
33
|
-
export declare const
|
|
44
|
+
export declare const getSerializationIssues: (value: unknown, path?: string, ancestors?: WeakMap<object, string>, issues?: SerializationIssue[]) => SerializationIssue[];
|
|
45
|
+
export declare const isSerializable: (value: unknown) => value is Serializable;
|
|
46
|
+
export declare const sanitizeNonSerializable: (value: unknown, path?: string, ancestors?: WeakMap<object, string>) => Serializable;
|
|
34
47
|
export declare const saveJSON: (saveInput: SaveJSONProps) => void;
|
|
35
48
|
export declare const readJSON: (props: ReadJSONProps) => any;
|
|
36
49
|
export declare const addToJSON: (saveInput: addToJSONProps) => void;
|
|
50
|
+
export {};
|
package/dist/index.js
CHANGED
|
@@ -12,47 +12,201 @@ const getDate = () => {
|
|
|
12
12
|
const castedDate = new Date();
|
|
13
13
|
return castedDate.toLocaleTimeString('ru-RU', { hour: '2-digit', minute: '2-digit' });
|
|
14
14
|
};
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
const isObjectLike = (value) => {
|
|
16
|
+
return typeof value === 'object' && value !== null;
|
|
17
|
+
};
|
|
18
|
+
const isPlainMergeableObject = (value) => {
|
|
19
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
20
|
+
};
|
|
21
|
+
const pathJoin = (basePath, key) => {
|
|
22
|
+
if (typeof key === 'number') {
|
|
23
|
+
return `${basePath}[${key}]`;
|
|
24
|
+
}
|
|
25
|
+
return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(key)
|
|
26
|
+
? `${basePath}.${key}`
|
|
27
|
+
: `${basePath}[${JSON.stringify(key)}]`;
|
|
28
|
+
};
|
|
29
|
+
const getIssueMessage = (issue) => {
|
|
30
|
+
if (issue.kind === 'circular') {
|
|
31
|
+
return `Field "${issue.path}" contains circular reference${issue.circularTo ? ` to "${issue.circularTo}"` : ''}`;
|
|
32
|
+
}
|
|
33
|
+
if (issue.kind === 'non-finite-number') {
|
|
34
|
+
return `Field "${issue.path}" contains non-finite number (NaN / Infinity)`;
|
|
35
|
+
}
|
|
36
|
+
return `Field "${issue.path}" has non-serializable value of type "${issue.kind}"`;
|
|
37
|
+
};
|
|
38
|
+
export const getSerializationIssues = (value, path = '$', ancestors = new WeakMap(), issues = []) => {
|
|
39
|
+
if (value === null) {
|
|
40
|
+
return issues;
|
|
41
|
+
}
|
|
42
|
+
const valueType = typeof value;
|
|
43
|
+
if (valueType === 'string' || valueType === 'boolean') {
|
|
44
|
+
return issues;
|
|
45
|
+
}
|
|
46
|
+
if (valueType === 'number') {
|
|
47
|
+
if (!Number.isFinite(value)) {
|
|
48
|
+
issues.push({
|
|
49
|
+
path,
|
|
50
|
+
kind: 'non-finite-number',
|
|
51
|
+
message: `Field "${path}" contains non-finite number (NaN / Infinity)`,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
return issues;
|
|
55
|
+
}
|
|
56
|
+
if (valueType === 'undefined') {
|
|
57
|
+
issues.push({
|
|
58
|
+
path,
|
|
59
|
+
kind: 'undefined',
|
|
60
|
+
message: `Field "${path}" has non-serializable value of type "undefined"`,
|
|
61
|
+
});
|
|
62
|
+
return issues;
|
|
63
|
+
}
|
|
64
|
+
if (valueType === 'function') {
|
|
65
|
+
issues.push({
|
|
66
|
+
path,
|
|
67
|
+
kind: 'function',
|
|
68
|
+
message: `Field "${path}" has non-serializable value of type "function"`,
|
|
69
|
+
});
|
|
70
|
+
return issues;
|
|
71
|
+
}
|
|
72
|
+
if (valueType === 'symbol') {
|
|
73
|
+
issues.push({
|
|
74
|
+
path,
|
|
75
|
+
kind: 'symbol',
|
|
76
|
+
message: `Field "${path}" has non-serializable value of type "symbol"`,
|
|
77
|
+
});
|
|
78
|
+
return issues;
|
|
79
|
+
}
|
|
80
|
+
if (valueType === 'bigint') {
|
|
81
|
+
issues.push({
|
|
82
|
+
path,
|
|
83
|
+
kind: 'bigint',
|
|
84
|
+
message: `Field "${path}" has non-serializable value of type "bigint"`,
|
|
85
|
+
});
|
|
86
|
+
return issues;
|
|
18
87
|
}
|
|
19
88
|
if (Array.isArray(value)) {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
89
|
+
if (ancestors.has(value)) {
|
|
90
|
+
const circularTo = ancestors.get(value);
|
|
91
|
+
issues.push({
|
|
92
|
+
path,
|
|
93
|
+
kind: 'circular',
|
|
94
|
+
circularTo,
|
|
95
|
+
message: `Field "${path}" contains circular reference${circularTo ? ` to "${circularTo}"` : ''}`,
|
|
96
|
+
});
|
|
97
|
+
return issues;
|
|
24
98
|
}
|
|
25
|
-
|
|
99
|
+
ancestors.set(value, path);
|
|
100
|
+
for (let i = 0; i < value.length; i++) {
|
|
101
|
+
getSerializationIssues(value[i], pathJoin(path, i), ancestors, issues);
|
|
102
|
+
}
|
|
103
|
+
ancestors.delete(value);
|
|
104
|
+
return issues;
|
|
26
105
|
}
|
|
27
|
-
if (
|
|
28
|
-
if (
|
|
29
|
-
|
|
106
|
+
if (isObjectLike(value)) {
|
|
107
|
+
if (ancestors.has(value)) {
|
|
108
|
+
const circularTo = ancestors.get(value);
|
|
109
|
+
issues.push({
|
|
110
|
+
path,
|
|
111
|
+
kind: 'circular',
|
|
112
|
+
circularTo,
|
|
113
|
+
message: `Field "${path}" contains circular reference${circularTo ? ` to "${circularTo}"` : ''}`,
|
|
114
|
+
});
|
|
115
|
+
return issues;
|
|
30
116
|
}
|
|
31
|
-
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
if (!isSerializable(obj[key], seen)) {
|
|
35
|
-
return false;
|
|
36
|
-
}
|
|
117
|
+
ancestors.set(value, path);
|
|
118
|
+
for (const key of Object.keys(value)) {
|
|
119
|
+
getSerializationIssues(value[key], pathJoin(path, key), ancestors, issues);
|
|
37
120
|
}
|
|
38
|
-
|
|
121
|
+
ancestors.delete(value);
|
|
122
|
+
return issues;
|
|
123
|
+
}
|
|
124
|
+
return issues;
|
|
125
|
+
};
|
|
126
|
+
export const isSerializable = (value) => {
|
|
127
|
+
return getSerializationIssues(value).length === 0;
|
|
128
|
+
};
|
|
129
|
+
const typeFlagFromValue = (value) => {
|
|
130
|
+
if (value === null)
|
|
131
|
+
return null;
|
|
132
|
+
if (Array.isArray(value))
|
|
133
|
+
return 'array';
|
|
134
|
+
const t = typeof value;
|
|
135
|
+
if (String(t) === 'number' && !Number.isFinite(value)) {
|
|
136
|
+
return 'non-finite-number';
|
|
137
|
+
}
|
|
138
|
+
else if (['undefined', 'function', 'symbol', 'bigint'].includes(String(t))) {
|
|
139
|
+
return String(t);
|
|
140
|
+
}
|
|
141
|
+
else if (String(t) === 'object') {
|
|
142
|
+
return 'object';
|
|
143
|
+
}
|
|
144
|
+
return String(t);
|
|
145
|
+
};
|
|
146
|
+
export const sanitizeNonSerializable = (value, path = '$', ancestors = new WeakMap()) => {
|
|
147
|
+
if (value === null || typeof value === 'string' || typeof value === 'boolean') {
|
|
148
|
+
return value;
|
|
149
|
+
}
|
|
150
|
+
if (typeof value === 'number') {
|
|
151
|
+
return Number.isFinite(value) ? value : 'non-finite-number';
|
|
152
|
+
}
|
|
153
|
+
if (typeof value === 'undefined' ||
|
|
154
|
+
typeof value === 'function' ||
|
|
155
|
+
typeof value === 'symbol' ||
|
|
156
|
+
typeof value === 'bigint') {
|
|
157
|
+
return typeFlagFromValue(value);
|
|
158
|
+
}
|
|
159
|
+
if (Array.isArray(value)) {
|
|
160
|
+
if (ancestors.has(value)) {
|
|
161
|
+
return 'circular';
|
|
162
|
+
}
|
|
163
|
+
ancestors.set(value, path);
|
|
164
|
+
const result = value.map((item, index) => sanitizeNonSerializable(item, pathJoin(path, index), ancestors));
|
|
165
|
+
ancestors.delete(value);
|
|
166
|
+
return result;
|
|
167
|
+
}
|
|
168
|
+
if (isObjectLike(value)) {
|
|
169
|
+
if (ancestors.has(value)) {
|
|
170
|
+
return 'circular';
|
|
171
|
+
}
|
|
172
|
+
ancestors.set(value, path);
|
|
173
|
+
const result = {};
|
|
174
|
+
for (const key of Object.keys(value)) {
|
|
175
|
+
result[key] = sanitizeNonSerializable(value[key], pathJoin(path, key), ancestors);
|
|
176
|
+
}
|
|
177
|
+
ancestors.delete(value);
|
|
178
|
+
return result;
|
|
179
|
+
}
|
|
180
|
+
return typeFlagFromValue(value);
|
|
181
|
+
};
|
|
182
|
+
const logSerializationIssues = (issues) => {
|
|
183
|
+
for (const issue of issues) {
|
|
184
|
+
console.info(`[${getDate()}] Boma serialization info: ${getIssueMessage(issue)}`);
|
|
39
185
|
}
|
|
40
|
-
return false;
|
|
41
186
|
};
|
|
42
187
|
export const saveJSON = (saveInput) => {
|
|
43
|
-
const { filePath, objToSave, format = false, logSaving = false } = saveInput;
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
188
|
+
const { filePath, objToSave, format = false, logSaving = false, replaceNonSerializable = false, } = saveInput;
|
|
189
|
+
const issues = getSerializationIssues(objToSave);
|
|
190
|
+
if (issues.length > 0) {
|
|
191
|
+
logSerializationIssues(issues);
|
|
192
|
+
if (!replaceNonSerializable) {
|
|
193
|
+
const firstIssue = issues[0];
|
|
194
|
+
const errorMessage = `[${getDate()}] Boma got non JSON-serializable object at saveJSON! ${getIssueMessage(firstIssue)}`;
|
|
195
|
+
console.error(errorMessage);
|
|
196
|
+
throw new Error(errorMessage);
|
|
197
|
+
}
|
|
47
198
|
}
|
|
199
|
+
const valueToSave = replaceNonSerializable && issues.length > 0
|
|
200
|
+
? sanitizeNonSerializable(objToSave)
|
|
201
|
+
: objToSave;
|
|
48
202
|
try {
|
|
49
|
-
const json = format ? JSON.stringify(
|
|
203
|
+
const json = format ? JSON.stringify(valueToSave, null, 2) : JSON.stringify(valueToSave);
|
|
50
204
|
writeFileSync(filePath, json, 'utf8');
|
|
51
205
|
logSaving && console.log(`[${getDate()}] Write file ${filePath} successfully`);
|
|
52
206
|
}
|
|
53
207
|
catch (error) {
|
|
54
208
|
if (isErrorWithCode(error)) {
|
|
55
|
-
console.error(`[${getDate()}] Boma
|
|
209
|
+
console.error(`[${getDate()}] Boma got filesystem error for ${filePath}:`, error);
|
|
56
210
|
}
|
|
57
211
|
throw error;
|
|
58
212
|
}
|
|
@@ -62,7 +216,7 @@ export const readJSON = (props) => {
|
|
|
62
216
|
try {
|
|
63
217
|
const savedfile = readFileSync(filePath, 'utf8');
|
|
64
218
|
if (parseJSON) {
|
|
65
|
-
return savedfile.trim() ===
|
|
219
|
+
return savedfile.trim() === '' ? null : JSON.parse(savedfile);
|
|
66
220
|
}
|
|
67
221
|
return savedfile;
|
|
68
222
|
}
|
|
@@ -100,21 +254,46 @@ export const readJSON = (props) => {
|
|
|
100
254
|
}
|
|
101
255
|
};
|
|
102
256
|
export const addToJSON = (saveInput) => {
|
|
103
|
-
const { filePath, dataToAdd, format = false, logSaving = false } = saveInput;
|
|
257
|
+
const { filePath, dataToAdd, format = false, logSaving = false, replaceNonSerializable = false, } = saveInput;
|
|
104
258
|
const oldJSON = readJSON({ filePath, createIfNotFound: true });
|
|
105
259
|
if (oldJSON === null || typeof oldJSON !== 'object') {
|
|
106
|
-
return saveJSON({
|
|
260
|
+
return saveJSON({
|
|
261
|
+
filePath,
|
|
262
|
+
objToSave: dataToAdd,
|
|
263
|
+
format,
|
|
264
|
+
logSaving,
|
|
265
|
+
replaceNonSerializable,
|
|
266
|
+
});
|
|
107
267
|
}
|
|
108
268
|
if (Array.isArray(oldJSON) && Array.isArray(dataToAdd)) {
|
|
109
|
-
saveJSON({
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
269
|
+
saveJSON({
|
|
270
|
+
filePath,
|
|
271
|
+
objToSave: [...oldJSON, ...dataToAdd],
|
|
272
|
+
format,
|
|
273
|
+
logSaving,
|
|
274
|
+
replaceNonSerializable,
|
|
275
|
+
});
|
|
276
|
+
return;
|
|
113
277
|
}
|
|
114
|
-
|
|
115
|
-
saveJSON({
|
|
278
|
+
if (isPlainMergeableObject(oldJSON) && isPlainMergeableObject(dataToAdd)) {
|
|
279
|
+
saveJSON({
|
|
280
|
+
filePath,
|
|
281
|
+
objToSave: { ...oldJSON, ...dataToAdd },
|
|
282
|
+
format,
|
|
283
|
+
logSaving,
|
|
284
|
+
replaceNonSerializable,
|
|
285
|
+
});
|
|
286
|
+
return;
|
|
116
287
|
}
|
|
117
|
-
|
|
118
|
-
|
|
288
|
+
if (Array.isArray(dataToAdd) && isPlainMergeableObject(oldJSON) && Object.keys(oldJSON).length === 0) {
|
|
289
|
+
saveJSON({
|
|
290
|
+
filePath,
|
|
291
|
+
objToSave: [...dataToAdd],
|
|
292
|
+
format,
|
|
293
|
+
logSaving,
|
|
294
|
+
replaceNonSerializable,
|
|
295
|
+
});
|
|
296
|
+
return;
|
|
119
297
|
}
|
|
298
|
+
throw new Error('Cannot merge array with object');
|
|
120
299
|
};
|
package/package.json
CHANGED
package/readme.md
CHANGED
|
@@ -14,7 +14,12 @@ logs = {
|
|
|
14
14
|
}
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
saveJSON({
|
|
17
|
+
saveJSON({
|
|
18
|
+
filePath: '/test.json',
|
|
19
|
+
objToSave: { any: {}, key: {} },
|
|
20
|
+
format: true,
|
|
21
|
+
logSaving: false
|
|
22
|
+
}); // Format for save line break's
|
|
18
23
|
/* Will save:
|
|
19
24
|
{
|
|
20
25
|
"any": {}, // saved line break's
|
|
@@ -22,9 +27,15 @@ saveJSON({ filePath: '/test.json', objToSave: { any: {}, key: {} }, format: true
|
|
|
22
27
|
}
|
|
23
28
|
*/
|
|
24
29
|
|
|
25
|
-
addToJSON({
|
|
30
|
+
addToJSON({
|
|
31
|
+
filePath: '/support.log',
|
|
32
|
+
dataToAdd: { 6sdf89g7dghg: { any: "data", someFunc: () => {} }},
|
|
33
|
+
format: false,
|
|
34
|
+
logSaving: false,
|
|
35
|
+
replaceNonSerializable: true
|
|
36
|
+
});
|
|
26
37
|
/* Will save:
|
|
27
|
-
"{ "
|
|
38
|
+
"{ "6sdf89g7dghg": { "any": "data", "someFunc": "function" } }"
|
|
28
39
|
*/
|
|
29
40
|
```
|
|
30
41
|
|
|
@@ -46,6 +57,7 @@ export interface SaveJSONProps {
|
|
|
46
57
|
objToSave: Serializable;
|
|
47
58
|
format?: boolean
|
|
48
59
|
logSaving?: boolean;
|
|
60
|
+
replaceNonSerializable?: boolean; // Заменять несереализуемые значения их типом
|
|
49
61
|
}
|
|
50
62
|
|
|
51
63
|
export interface addToJSONProps {
|