boma 2.0.3 → 2.0.5

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,22 +4,29 @@ 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;
10
12
  createIfNotFound?: boolean | SerializableObject | SerializableArray;
13
+ silent?: boolean;
11
14
  }
12
15
  export interface SaveJSONProps {
13
16
  filePath: string;
14
- objToSave: Serializable;
17
+ objToSave: unknown;
15
18
  format?: boolean;
16
19
  logSaving?: boolean;
20
+ replaceNonSerializable?: boolean;
21
+ silent?: boolean;
17
22
  }
18
23
  export interface addToJSONProps {
19
24
  filePath: string;
20
- dataToAdd: SerializableObject | SerializableArray;
25
+ dataToAdd: JSONLikeObject | JSONLikeArray;
21
26
  format?: boolean;
22
27
  logSaving?: boolean;
28
+ replaceNonSerializable?: boolean;
29
+ silent?: boolean;
23
30
  }
24
31
  export type ErrorWithCode = Error & {
25
32
  code: string;
@@ -27,10 +34,22 @@ export type ErrorWithCode = Error & {
27
34
  export type ErrorWithMessage = Error & {
28
35
  message: any;
29
36
  };
37
+ export type SerializationIssueKind = 'undefined' | 'function' | 'symbol' | 'bigint' | 'non-finite-number' | 'circular';
38
+ export interface SerializationIssue {
39
+ path: string;
40
+ kind: SerializationIssueKind;
41
+ message: string;
42
+ circularTo?: string;
43
+ }
30
44
  export declare const isErrorWithCode: (error: unknown) => error is ErrorWithCode;
31
45
  export declare const isErrorWithMessage: (error: unknown) => error is ErrorWithMessage;
32
46
  export declare const isSyntaxError: (error: unknown) => error is SyntaxError;
33
- export declare const isSerializable: (value: unknown, seen?: WeakSet<object>) => value is Serializable;
47
+ export declare const isObjectLike: (value: unknown) => value is object;
48
+ export declare const isPlainMergeableObject: (value: unknown) => value is Record<string, unknown>;
49
+ export declare const getSerializationIssues: (value: unknown, path?: string, ancestors?: WeakMap<object, string>, issues?: SerializationIssue[]) => SerializationIssue[];
50
+ export declare const isSerializable: (value: unknown) => value is Serializable;
51
+ export declare const sanitizeNonSerializable: (value: unknown, path?: string, ancestors?: WeakMap<object, string>) => Serializable;
34
52
  export declare const saveJSON: (saveInput: SaveJSONProps) => void;
35
53
  export declare const readJSON: (props: ReadJSONProps) => any;
36
54
  export declare const addToJSON: (saveInput: addToJSONProps) => void;
55
+ export {};
package/dist/index.js CHANGED
@@ -12,57 +12,211 @@ 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
+ export const isObjectLike = (value) => {
16
+ return typeof value === 'object' && value !== null;
17
+ };
18
+ export 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 = '[OBJECT]', 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, silent = true, } = 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
- logSaving && console.log(`[${getDate()}] Write file ${filePath} successfully`);
205
+ !silent && 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
  }
59
213
  };
60
214
  export const readJSON = (props) => {
61
- const { filePath, createIfNotFound = false, parseJSON = true } = props;
215
+ const { filePath, createIfNotFound = false, parseJSON = true, silent = true } = 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
  }
@@ -71,7 +225,7 @@ export const readJSON = (props) => {
71
225
  switch (err.code) {
72
226
  case 'ENOENT':
73
227
  if (createIfNotFound) {
74
- console.log('Try to create: ', filePath);
228
+ !silent && console.log('Try to create: ', filePath);
75
229
  try {
76
230
  const initialContent = typeof createIfNotFound === 'boolean' ? '{}' : JSON.stringify(createIfNotFound);
77
231
  writeFileSync(filePath, initialContent, 'utf8');
@@ -81,7 +235,7 @@ export const readJSON = (props) => {
81
235
  }
82
236
  return typeof createIfNotFound === 'boolean' ? {} : createIfNotFound;
83
237
  }
84
- console.log('File not found: ', filePath);
238
+ console.info("Boma can't find file: ", filePath);
85
239
  return parseJSON ? {} : null;
86
240
  case 'EACCES':
87
241
  console.error(`Access denied for ${filePath}`);
@@ -100,21 +254,50 @@ export const readJSON = (props) => {
100
254
  }
101
255
  };
102
256
  export const addToJSON = (saveInput) => {
103
- const { filePath, dataToAdd, format = false, logSaving = false } = saveInput;
104
- const oldJSON = readJSON({ filePath, createIfNotFound: true });
257
+ const { filePath, dataToAdd, format = false, logSaving = false, replaceNonSerializable = false, silent = true, } = saveInput;
258
+ const oldJSON = readJSON({ filePath, createIfNotFound: true, silent });
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
+ silent
267
+ });
107
268
  }
108
269
  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 });
270
+ saveJSON({
271
+ filePath,
272
+ objToSave: [...oldJSON, ...dataToAdd],
273
+ format,
274
+ logSaving,
275
+ replaceNonSerializable,
276
+ silent
277
+ });
278
+ return;
113
279
  }
114
- else if (Array.isArray(dataToAdd) && Object.keys(oldJSON).length === 0) {
115
- saveJSON({ filePath, objToSave: [...dataToAdd], format, logSaving });
280
+ if (isPlainMergeableObject(oldJSON) && isPlainMergeableObject(dataToAdd)) {
281
+ saveJSON({
282
+ filePath,
283
+ objToSave: { ...oldJSON, ...dataToAdd },
284
+ format,
285
+ logSaving,
286
+ replaceNonSerializable,
287
+ silent
288
+ });
289
+ return;
116
290
  }
117
- else {
118
- throw new Error('Cannot merge array with object');
291
+ if (Array.isArray(dataToAdd) && isPlainMergeableObject(oldJSON) && Object.keys(oldJSON).length === 0) {
292
+ saveJSON({
293
+ filePath,
294
+ objToSave: [...dataToAdd],
295
+ format,
296
+ logSaving,
297
+ replaceNonSerializable,
298
+ silent
299
+ });
300
+ return;
119
301
  }
302
+ throw new Error('Cannot merge array with object');
120
303
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "boma",
3
- "version": "2.0.3",
3
+ "version": "2.0.5",
4
4
  "description": "Primitive JSON helper",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
package/readme.md CHANGED
@@ -6,7 +6,12 @@
6
6
  # Usage
7
7
 
8
8
  ```JS
9
- const logs = readJSON({ filePath: '/support.log', createIfNotFound: {}, parseJSON: true });
9
+ const logs = readJSON({
10
+ filePath: '/support.log',
11
+ createIfNotFound: {},
12
+ parseJSON: true,
13
+ silent: true, // Do not log warnings (only file not found will be logged)
14
+ });
10
15
  /*
11
16
  logs = {
12
17
  any: {}, //
@@ -14,7 +19,13 @@ logs = {
14
19
  }
15
20
  */
16
21
 
17
- saveJSON({ filePath: '/test.json', objToSave: { any: {}, key: {} }, format: true, logSaving: false }); // Format for save line break's
22
+ saveJSON({
23
+ filePath: '/test.json',
24
+ objToSave: { any: {}, key: {} },
25
+ format: true,
26
+ logSaving: false,
27
+ silent: true, // Do not log warnings
28
+ }); // Format for save line break's
18
29
  /* Will save:
19
30
  {
20
31
  "any": {}, // saved line break's
@@ -22,13 +33,21 @@ saveJSON({ filePath: '/test.json', objToSave: { any: {}, key: {} }, format: true
22
33
  }
23
34
  */
24
35
 
25
- addToJSON({ filePath: '/support.log', dataToAdd: { 6sdf89g7dghg: { any: "data" }}, format: false, logSaving: false });
36
+ addToJSON({
37
+ filePath: '/support.log',
38
+ dataToAdd: { 6sdf89g7dghg: { any: "data", someFunc: () => {} }},
39
+ format: false,
40
+ logSaving: false,
41
+ replaceNonSerializable: true
42
+ });
26
43
  /* Will save:
27
- "{ "any": {}, "key": {}, "6sdf89g7dghg": { "any": "data" } }"
44
+ "{ "6sdf89g7dghg": { "any": "data", "someFunc": "function" } }"
28
45
  */
29
46
  ```
30
47
 
31
48
  ## Types
49
+ Main types:
50
+
32
51
  ```typescript
33
52
  export type SerializablePrimitive = string | number | boolean | null;
34
53
  export type SerializableArray = Serializable[];
@@ -46,6 +65,8 @@ export interface SaveJSONProps {
46
65
  objToSave: Serializable;
47
66
  format?: boolean
48
67
  logSaving?: boolean;
68
+ replaceNonSerializable?: boolean; // Заменять несереализуемые значения их типом
69
+ silent?: boolean; // Не выводим предупреждения
49
70
  }
50
71
 
51
72
  export interface addToJSONProps {
@@ -53,5 +74,15 @@ export interface addToJSONProps {
53
74
  dataToAdd: SerializableObject | SerializableArray;
54
75
  format?: boolean
55
76
  logSaving?: boolean;
77
+ silent?: boolean; // Не выводим предупреждения
56
78
  }
79
+
80
+ // We also export some helpers:
81
+ export const isSerializable = (value: unknown) => boolean;
82
+ export const isObjectLike = (value: unknown) => boolean;
83
+ export const isPlainMergeableObject = (value: unknown) => boolean;
84
+
85
+ export const isErrorWithCode = (error: unknown) => boolean;
86
+ export const isErrorWithMessage = (error: unknown) => boolean;
87
+ export const isSyntaxError = (error: unknown) => boolean;
57
88
  ```