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 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: Serializable;
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: SerializableObject | SerializableArray;
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 isSerializable: (value: unknown, seen?: WeakSet<object>) => value is Serializable;
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
- export const isSerializable = (value, seen = new WeakSet()) => {
16
- if (value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
17
- return true;
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
- for (const el of value) {
21
- if (!isSerializable(el, seen)) {
22
- return false;
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
- return true;
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 (typeof value === "object") {
28
- if (seen.has(value)) {
29
- return false;
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
- seen.add(value);
32
- const obj = value;
33
- for (const key in obj) {
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
- return true;
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
- if (!isSerializable(objToSave)) {
45
- console.error(`[${getDate()}] Boma get non JSON-serializable object at saveJSON!`);
46
- throw new Error(`[${getDate()}] Boma get non JSON-serializable object at saveJSON!`);
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(objToSave, null, 2) : JSON.stringify(objToSave);
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 get filesystem error for ${filePath}:`, error);
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() === "" ? null : JSON.parse(savedfile);
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({ filePath, objToSave: dataToAdd, logSaving });
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({ filePath, objToSave: [...oldJSON, ...dataToAdd], format, logSaving });
110
- }
111
- else if (!Array.isArray(oldJSON) && !Array.isArray(dataToAdd)) {
112
- saveJSON({ filePath, objToSave: { ...oldJSON, ...dataToAdd }, format, logSaving });
269
+ saveJSON({
270
+ filePath,
271
+ objToSave: [...oldJSON, ...dataToAdd],
272
+ format,
273
+ logSaving,
274
+ replaceNonSerializable,
275
+ });
276
+ return;
113
277
  }
114
- else if (Array.isArray(dataToAdd) && Object.keys(oldJSON).length === 0) {
115
- saveJSON({ filePath, objToSave: [...dataToAdd], format, logSaving });
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
- else {
118
- throw new Error('Cannot merge array with object');
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "boma",
3
- "version": "2.0.3",
3
+ "version": "2.0.4",
4
4
  "description": "Primitive JSON helper",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
package/readme.md CHANGED
@@ -14,7 +14,12 @@ logs = {
14
14
  }
15
15
  */
16
16
 
17
- saveJSON({ filePath: '/test.json', objToSave: { any: {}, key: {} }, format: true, logSaving: false }); // Format for save line break's
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({ filePath: '/support.log', dataToAdd: { 6sdf89g7dghg: { any: "data" }}, format: false, logSaving: false });
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
- "{ "any": {}, "key": {}, "6sdf89g7dghg": { "any": "data" } }"
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 {