-
Notifications
You must be signed in to change notification settings - Fork 0
/
index.ts
311 lines (293 loc) · 9.82 KB
/
index.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
/**
* Contains functions for manipulating JSON files asynchronously using the File
* System module from NodeJS.
*
* Only the main functions are exported. Functions not exported are abstractions
* of steps from the main functions.
*
* Functions must be passed an absolute file path to behave as expected.
*
* Functions that requires an optional intentation level parameter don't check
* its value. It's used by `JSON.stringify` and therefore, if the value is less
* than 1 no formatting is done, and if it's greater than 10, the indentation is
* just 10.
*
* When a function fails it returns a rejected Promise with:
* - An intance of [[JSONFileHandlerError]] if it was caused by a misuse of the
* function, for example, when trying to write something that is not an object
* to a JSON file, or when trying to read a file that doesn't exists
* - An instance of `Error` (actually is of
* [`SystemError`](https://nodejs.org/api/errors.html#errors_class_systemerror),
* but Node doesn't exposes the class so it can't be checked using the
* `instanceof` operator) if it was caused by violating an operating system
* constraint, like:
* - Trying to modify a read-only file
* - Trying to create or modify a file inside a read-only directory
* - Trying to modify a file that requires elevated privileges
* - Trying to work with many files at once
* - Trying to modify a file and running out of space in the process
*
* @packageDocumentation
*/
import { isAnObject, isAValidJsonString } from './utils';
import { JSONFileHandlerError } from './models/classes';
import fs from 'fs';
import path from 'path';
import deepMerge from 'deepmerge';
/**
* Checks if the directory where the file is or will be located exists.
*
* @param directoryName - The absolute path of the directory where the file is
* or will be located
*
* @returns A promise that returns whether the directory exists or not when
* resolved
*
* @internal
*/
const checkIfDirectoryExistsForFile = async (
directoryName: string
): Promise<boolean> => {
try {
await fs.promises.access(directoryName);
return await Promise.resolve(true);
} catch (error) {
const oneOrMoreDirectoriesDoNotExist = error.code === 'ENOENT';
return oneOrMoreDirectoriesDoNotExist
? Promise.resolve(false)
: Promise.reject(error);
}
};
/**
* Creates or overwrites the JSON file with a given content.
*
* @param filePath - The absolute path where the file is or will be located
* @param jsonContent - The object to be written
* @param indentationLevel - How much space to use for indentation when
* formatting
*
* @returns A promise that creates or overwrites the JSON file with the given
* content when resolved
*
* @internal
*/
const writeJson = async (
filePath: string,
jsonContent: object,
indentationLevel: number
): Promise<void> => {
const jsonString = JSON.stringify(jsonContent, null, indentationLevel);
return fs.promises.writeFile(filePath, jsonString);
};
/**
* Creates the necessary directories to create the JSON file and then creates it
* with a given content.
*
* @param directoryName - The absolute path of the directory where the file will
* be located
* @param filePath - The absolute path where the file will be located
* @param jsonContent - The object to be written
* @param indentationLevel - How much space to use for indentation when
* formatting
*
* @returns A promise that creates the directories and writes the JSON file when
* resolved
*
* @internal
*/
const createDirectoriesAnWriteJson = async (
directoryName: string,
filePath: string,
jsonContent: object,
indentationLevel: number
): Promise<void> => {
await fs.promises.mkdir(directoryName, { recursive: true });
return writeJson(filePath, jsonContent, indentationLevel);
};
/**
* Creates or overwrites the JSON file with a given content, checking that the
* directories neccesary for its creation exists beforehand if the file needs
* to be created.
*
* @param filePath - The absolute path where the file is or will be located
* @param jsonContent - The object to be written
* @param indentationLevel - How much space to use for indentation when
* formatting
*
* @returns A promise that creates or overwrites the JSON file with the given
* content when resolved
*
* @internal
*/
const write = async (
filePath: string,
jsonContent: object,
indentationLevel: number
): Promise<void> => {
const directoryName = path.dirname(filePath);
const directoryExists = await checkIfDirectoryExistsForFile(directoryName);
return directoryExists
? writeJson(filePath, jsonContent, indentationLevel)
: createDirectoriesAnWriteJson(
directoryName,
filePath,
jsonContent,
indentationLevel
);
};
/**
* Overwrites the content of an existing JSON file, or creates a new one if it
* doesn't exist.
*
* @param filePath - The absolute path where the file is or will be located
* @param jsonContent - The object to be written to the JSON file
* @param indentationLevel - How much space to use for indentation when
* formatting
*
* @returns A promise that overwrites or creates the file when resolved
*/
export const overwrite = async (
filePath: string,
jsonContent: object,
indentationLevel = 2
): Promise<void> => {
if (isAnObject(jsonContent)) {
return write(filePath, jsonContent, indentationLevel);
}
const notAValidObjectError = new JSONFileHandlerError(
'NOT_A_VALID_OBJECT',
filePath
);
return Promise.reject(notAValidObjectError);
};
/**
* Returns the content of a JSON file.
*
* @param filePath - The absolute path where the file is located
*
* @returns A promise resolved with the content
*/
export const read = async (filePath: string): Promise<object> => {
const binaryJsonContent: Buffer = await fs.promises.readFile(filePath);
const stringifiedJsonContent = String(binaryJsonContent);
if (isAValidJsonString(stringifiedJsonContent)) {
const jsonContent = JSON.parse(stringifiedJsonContent);
return jsonContent;
}
if (stringifiedJsonContent.length === 0) {
const emptyFileError = new JSONFileHandlerError('EMPTY_FILE', filePath);
return Promise.reject(emptyFileError);
}
const notAJsonError = new JSONFileHandlerError('NOT_A_JSON', filePath);
return Promise.reject(notAJsonError);
};
/**
* Joins a given content to the content of a JSON file, or creates a new one
* if it doesn't exists.
*
* @param filePath - The absolute path where the file is or will be located
* @param jsonContent - The object to be merged or written to the JSON file
* @param indentationLevel - How much space to use for indentation when
* formatting
*
* @returns A promise that joins the content or creates the file when resolved
*/
export const join = async (
filePath: string,
jsonContent: object,
indentationLevel = 2
): Promise<void> => {
if (isAnObject(jsonContent)) {
try {
const currentJsonContent: object = await read(filePath);
const newJsonContent = deepMerge<object>(currentJsonContent, jsonContent);
return await write(filePath, newJsonContent, indentationLevel);
} catch (error) {
const fileIsEmpty = error.code === 'EMPTY_FILE';
const fileDoesNotExist = error.code === 'ENOENT';
if (fileIsEmpty || fileDoesNotExist) {
return write(filePath, jsonContent, indentationLevel);
}
return Promise.reject(error);
}
}
const notAValidObjectError = new JSONFileHandlerError(
'NOT_A_VALID_OBJECT',
filePath
);
return Promise.reject(notAValidObjectError);
};
/**
* Duplicates a JSON file.
*
* @param filePath - The absolute path of the file to be duplicated
* @param duplicatedFilePath - The absolute path where the file will be
* duplicated
* @param indentationLevel - How much space to use for indentation when
* formatting
*
* @returns A promise that duplicates the file when resolved
*/
const duplicate = async (
filePath: string,
duplicatedFilePath: string,
indentationLevel: number
): Promise<void> => {
try {
const fileContent: object = await read(filePath);
return await write(duplicatedFilePath, fileContent, indentationLevel);
} catch (error) {
return Promise.reject(error);
}
};
/**
* Merges the content of two JSON files into a third file, or duplicates one of
* the files if the other one is empty.
*
* @param firstFilePath - The absolute path of the first file
* @param secondFilePath - The absolute path of the second file
* @param mergedFilePath - The absolute path where the file is or will be
* located
* @param indentationLevel - How much space to use for indentation when
* formatting
*
* @returns A promise that merges the files, or duplicates one of them, when
* resolved
*/
export const merge = async (
firstFilePath: string,
secondFilePath: string,
mergedFilePath: string,
indentationLevel = 2
): Promise<void> => {
let firstFileContent: object;
try {
firstFileContent = await read(firstFilePath);
const secondFileContent: object = await read(secondFilePath);
const mergedContent: object = deepMerge<object>(
firstFileContent,
secondFileContent
);
return await write(mergedFilePath, mergedContent, indentationLevel);
} catch (error) {
if (error.code === 'EMPTY_FILE' && firstFilePath !== secondFilePath) {
if (error.path === secondFilePath) {
// If the the second file is empty, then the first file has content, so
// it's duplicated at `mergedFilePath` using `indentationLevel`
return write(mergedFilePath, firstFileContent, indentationLevel);
} else {
// But if the first file is empty, it's not guaranteed that the second
// file will have content, `duplicate` takes care of that condition
return duplicate(secondFilePath, mergedFilePath, indentationLevel);
}
} else {
return Promise.reject(error);
}
}
};
module.exports = {
read,
overwrite,
join,
merge,
};