appwrite-utils-cli 0.0.11 → 0.0.13
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.
|
@@ -151,14 +151,13 @@ export class ImportController {
|
|
|
151
151
|
async processBatch(db, collection, importDef, dataToImport, updateDefs = [], isMembersCollection = false) {
|
|
152
152
|
for (let i = 0; i < dataToImport.length; i += this.batchLimit) {
|
|
153
153
|
const batch = dataToImport.slice(i, i + this.batchLimit);
|
|
154
|
-
batch.map(async (item) => {
|
|
154
|
+
const results = await Promise.allSettled(batch.map(async (item) => {
|
|
155
155
|
let context = this.createContext(db, collection, item);
|
|
156
156
|
let finalItem = await this.transformData(item, importDef.attributeMappings);
|
|
157
157
|
let createIdToUse = undefined;
|
|
158
158
|
let associatedDoc;
|
|
159
159
|
if (isMembersCollection &&
|
|
160
160
|
(finalItem.hasOwnProperty("email") || item.hasOwnProperty("phone"))) {
|
|
161
|
-
console.log("Found members collection, creating user...");
|
|
162
161
|
const usersController = new UsersController(this.config, this.database);
|
|
163
162
|
const userToCreate = AuthUserCreateSchema.safeParse({
|
|
164
163
|
...finalItem,
|
|
@@ -172,7 +171,6 @@ export class ImportController {
|
|
|
172
171
|
createIdToUse = user.$id;
|
|
173
172
|
context.docId = createIdToUse;
|
|
174
173
|
context = { ...context, ...user };
|
|
175
|
-
console.log("Created user, deleting keys in finalItem that exist in user...");
|
|
176
174
|
const associatedDocFound = await this.database.listDocuments(db.$id, context.collId, [Query.equal("$id", createIdToUse)]);
|
|
177
175
|
if (associatedDocFound.documents.length > 0) {
|
|
178
176
|
associatedDoc = associatedDocFound.documents[0];
|
|
@@ -185,37 +183,28 @@ export class ImportController {
|
|
|
185
183
|
deletedKeys.push(key);
|
|
186
184
|
}
|
|
187
185
|
});
|
|
188
|
-
console.log(`Set createIdToUse to ${createIdToUse}. Deleted keys: ${deletedKeys.join(", ")}.`);
|
|
189
186
|
}
|
|
190
187
|
else if (isMembersCollection) {
|
|
191
188
|
logger.error(`Skipping user & contact creation for ${item} due to lack of email...`);
|
|
192
189
|
}
|
|
193
190
|
context = { ...context, ...finalItem };
|
|
194
|
-
|
|
191
|
+
const validated = await this.importDataActions.validateItem(finalItem, importDef.attributeMappings, context);
|
|
192
|
+
if (!validated) {
|
|
195
193
|
console.error("Validation failed for item:", finalItem);
|
|
194
|
+
logger.error("Validation failed for item:", finalItem);
|
|
196
195
|
return;
|
|
197
196
|
}
|
|
198
|
-
let afterContext;
|
|
199
197
|
if ((importDef.type === "create" || !importDef.type) &&
|
|
200
198
|
!associatedDoc) {
|
|
201
199
|
const createdContext = await this.handleCreate(context, finalItem, updateDefs, createIdToUse);
|
|
202
|
-
|
|
203
|
-
afterContext = createdContext;
|
|
204
|
-
}
|
|
205
|
-
logger.info(`Handled create for ${context.docId}}`);
|
|
200
|
+
context = { ...context, ...createdContext };
|
|
206
201
|
}
|
|
207
202
|
else {
|
|
208
203
|
const updatedContext = await this.handleUpdate(context, finalItem, importDef);
|
|
209
|
-
|
|
210
|
-
afterContext = updatedContext;
|
|
211
|
-
}
|
|
212
|
-
logger.info(`Handled update for ${context.docId}`);
|
|
213
|
-
}
|
|
214
|
-
if (afterContext) {
|
|
215
|
-
context = { ...context, ...afterContext };
|
|
204
|
+
context = { ...context, ...updatedContext };
|
|
216
205
|
}
|
|
217
206
|
const afterImportActionContext = structuredClone(context);
|
|
218
|
-
const attributeMappingsWithActions = this.getAttributeMappingsWithActions(importDef.attributeMappings,
|
|
207
|
+
const attributeMappingsWithActions = this.getAttributeMappingsWithActions(importDef.attributeMappings, afterImportActionContext, finalItem);
|
|
219
208
|
if (attributeMappingsWithActions.some((m) => m.postImportActions)) {
|
|
220
209
|
logger.info(`Pushing to post-import actions queue for ${context.docId}`);
|
|
221
210
|
const afterImportOperationContext = ContextObject.parse({
|
|
@@ -232,21 +221,18 @@ export class ImportController {
|
|
|
232
221
|
// attributeMappings: attributeMappingsWithActions,
|
|
233
222
|
// });
|
|
234
223
|
}
|
|
224
|
+
}));
|
|
225
|
+
results.forEach((result) => {
|
|
226
|
+
if (result.status === "rejected") {
|
|
227
|
+
console.error("A process batch promise was rejected:", result.reason);
|
|
228
|
+
logger.error("An error occurred during creation: ", result.reason);
|
|
229
|
+
}
|
|
235
230
|
});
|
|
236
|
-
// results.forEach((result) => {
|
|
237
|
-
// if (result.status === "rejected") {
|
|
238
|
-
// console.error("A process batch promise was rejected:", result.reason);
|
|
239
|
-
// logger.error("An error occurred during creation: ", result.reason);
|
|
240
|
-
// }
|
|
241
|
-
// });
|
|
242
231
|
}
|
|
243
232
|
}
|
|
244
233
|
async handleCreate(context, finalItem, updateDefs, id) {
|
|
245
234
|
const existing = await documentExists(this.database, context.dbId, context.collId, finalItem);
|
|
246
235
|
if (!existing) {
|
|
247
|
-
if (id) {
|
|
248
|
-
console.log(`Creating document with provided ID (member): ${id}`);
|
|
249
|
-
}
|
|
250
236
|
const createdDoc = await this.database.createDocument(context.dbId, context.collId, id || ID.unique(), finalItem);
|
|
251
237
|
context.docId = createdDoc.$id;
|
|
252
238
|
context.createdDoc = createdDoc;
|
|
@@ -259,7 +245,6 @@ export class ImportController {
|
|
|
259
245
|
}
|
|
260
246
|
});
|
|
261
247
|
}
|
|
262
|
-
console.log(`Created document ID: ${createdDoc.$id}`);
|
|
263
248
|
return context;
|
|
264
249
|
}
|
|
265
250
|
else {
|
|
@@ -360,17 +345,18 @@ export class ImportController {
|
|
|
360
345
|
for (const batch of resultsData) {
|
|
361
346
|
const actionOperation = ContextObject.parse(JSON.parse(batch.data));
|
|
362
347
|
const { context, finalItem, attributeMappings } = actionOperation;
|
|
348
|
+
if (finalItem.$id && !context.docId) {
|
|
349
|
+
context.docId = finalItem.$id;
|
|
350
|
+
logger.info(`Setting docId to ${finalItem.$id} because docId not found in context, batch ${batch.$id}, context is ${JSON.stringify(context)}`);
|
|
351
|
+
}
|
|
363
352
|
try {
|
|
364
353
|
await this.importDataActions.executeAfterImportActions(finalItem, attributeMappings, context);
|
|
365
354
|
// Mark batch as processed
|
|
366
355
|
await this.database.deleteDocument("migrations", "batches", batch.$id);
|
|
367
|
-
await updateOperation(this.database, operation.$id, {
|
|
368
|
-
status: "completed",
|
|
369
|
-
batches: [],
|
|
370
|
-
});
|
|
371
356
|
}
|
|
372
357
|
catch (error) {
|
|
373
|
-
logger.error(`Failed to execute batch ${batch.$id}:`, error);
|
|
358
|
+
logger.error(`Failed to execute batch ${batch.$id}:`, error, "Context is :", context);
|
|
359
|
+
await this.database.deleteDocument("migrations", "batches", batch.$id);
|
|
374
360
|
}
|
|
375
361
|
}
|
|
376
362
|
// After processing all batches, update the operation status
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import winston from "winston";
|
|
2
2
|
export const logger = winston.createLogger({
|
|
3
3
|
level: "info",
|
|
4
|
-
format: winston.format.
|
|
4
|
+
format: winston.format.json({ space: 2 }),
|
|
5
5
|
defaultMeta: { service: "appwrite-utils-cli" },
|
|
6
6
|
transports: [
|
|
7
7
|
//
|
|
@@ -9,6 +9,7 @@ export const logger = winston.createLogger({
|
|
|
9
9
|
// - Write all logs with importance level of `info` or less to `combined.log`
|
|
10
10
|
//
|
|
11
11
|
new winston.transports.File({ filename: "error.log", level: "error" }),
|
|
12
|
+
new winston.transports.File({ filename: "warn.log", level: "warn" }),
|
|
12
13
|
new winston.transports.File({ filename: "combined.log" }),
|
|
13
14
|
],
|
|
14
15
|
});
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "appwrite-utils-cli",
|
|
3
3
|
"description": "Appwrite Utility Functions to help with database management, data conversion, data import, migrations, and much more. Meant to be used as a CLI tool, I do not recommend installing this in frontend environments.",
|
|
4
|
-
"version": "0.0.
|
|
4
|
+
"version": "0.0.13",
|
|
5
5
|
"main": "src/main.ts",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"repository": {
|
|
@@ -113,7 +113,6 @@ export class ImportController {
|
|
|
113
113
|
async importCollections(db: ConfigDatabase) {
|
|
114
114
|
const maxParallel = 3; // Maximum number of collections to process in parallel
|
|
115
115
|
let activePromises: Promise<void>[] = []; // Array to keep track of active promises
|
|
116
|
-
|
|
117
116
|
for (const collection of this.config.collections) {
|
|
118
117
|
// Function that returns a promise for processing a single collection
|
|
119
118
|
const processCollection = async (col: ConfigCollection) => {
|
|
@@ -201,7 +200,6 @@ export class ImportController {
|
|
|
201
200
|
);
|
|
202
201
|
await this.processBatch(db, collection, importDef, dataToImport);
|
|
203
202
|
}
|
|
204
|
-
|
|
205
203
|
await setAllPendingAfterImportActionsToReady(
|
|
206
204
|
this.database,
|
|
207
205
|
db.$id,
|
|
@@ -258,145 +256,127 @@ export class ImportController {
|
|
|
258
256
|
) {
|
|
259
257
|
for (let i = 0; i < dataToImport.length; i += this.batchLimit) {
|
|
260
258
|
const batch = dataToImport.slice(i, i + this.batchLimit);
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
let createIdToUse: string | undefined = undefined;
|
|
268
|
-
let associatedDoc: Models.Document | undefined;
|
|
269
|
-
if (
|
|
270
|
-
isMembersCollection &&
|
|
271
|
-
(finalItem.hasOwnProperty("email") || item.hasOwnProperty("phone"))
|
|
272
|
-
) {
|
|
273
|
-
console.log("Found members collection, creating user...");
|
|
274
|
-
const usersController = new UsersController(
|
|
275
|
-
this.config,
|
|
276
|
-
this.database
|
|
277
|
-
);
|
|
278
|
-
const userToCreate = AuthUserCreateSchema.safeParse({
|
|
279
|
-
...finalItem,
|
|
280
|
-
});
|
|
281
|
-
if (!userToCreate.success) {
|
|
282
|
-
console.error(userToCreate.error);
|
|
283
|
-
logger.error(userToCreate.error);
|
|
284
|
-
return;
|
|
285
|
-
}
|
|
286
|
-
const user = await usersController.createUserAndReturn(
|
|
287
|
-
userToCreate.data
|
|
259
|
+
const results = await Promise.allSettled(
|
|
260
|
+
batch.map(async (item: any) => {
|
|
261
|
+
let context = this.createContext(db, collection, item);
|
|
262
|
+
let finalItem = await this.transformData(
|
|
263
|
+
item,
|
|
264
|
+
importDef.attributeMappings
|
|
288
265
|
);
|
|
289
|
-
createIdToUse =
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
"
|
|
294
|
-
)
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
if (user.hasOwnProperty(key)) {
|
|
307
|
-
delete finalItem[key];
|
|
308
|
-
deletedKeys.push(key);
|
|
266
|
+
let createIdToUse: string | undefined = undefined;
|
|
267
|
+
let associatedDoc: Models.Document | undefined;
|
|
268
|
+
if (
|
|
269
|
+
isMembersCollection &&
|
|
270
|
+
(finalItem.hasOwnProperty("email") || item.hasOwnProperty("phone"))
|
|
271
|
+
) {
|
|
272
|
+
const usersController = new UsersController(
|
|
273
|
+
this.config,
|
|
274
|
+
this.database
|
|
275
|
+
);
|
|
276
|
+
const userToCreate = AuthUserCreateSchema.safeParse({
|
|
277
|
+
...finalItem,
|
|
278
|
+
});
|
|
279
|
+
if (!userToCreate.success) {
|
|
280
|
+
console.error(userToCreate.error);
|
|
281
|
+
logger.error(userToCreate.error);
|
|
282
|
+
return;
|
|
309
283
|
}
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
284
|
+
const user = await usersController.createUserAndReturn(
|
|
285
|
+
userToCreate.data
|
|
286
|
+
);
|
|
287
|
+
createIdToUse = user.$id;
|
|
288
|
+
context.docId = createIdToUse;
|
|
289
|
+
context = { ...context, ...user };
|
|
290
|
+
const associatedDocFound = await this.database.listDocuments(
|
|
291
|
+
db.$id,
|
|
292
|
+
context.collId,
|
|
293
|
+
[Query.equal("$id", createIdToUse)]
|
|
294
|
+
);
|
|
295
|
+
if (associatedDocFound.documents.length > 0) {
|
|
296
|
+
associatedDoc = associatedDocFound.documents[0];
|
|
297
|
+
}
|
|
298
|
+
// Delete keys in finalItem that also exist in user
|
|
299
|
+
let deletedKeys: string[] = [];
|
|
300
|
+
Object.keys(finalItem).forEach((key) => {
|
|
301
|
+
if (user.hasOwnProperty(key)) {
|
|
302
|
+
delete finalItem[key];
|
|
303
|
+
deletedKeys.push(key);
|
|
304
|
+
}
|
|
305
|
+
});
|
|
306
|
+
} else if (isMembersCollection) {
|
|
307
|
+
logger.error(
|
|
308
|
+
`Skipping user & contact creation for ${item} due to lack of email...`
|
|
309
|
+
);
|
|
310
|
+
}
|
|
323
311
|
|
|
324
|
-
|
|
325
|
-
|
|
312
|
+
context = { ...context, ...finalItem };
|
|
313
|
+
const validated = await this.importDataActions.validateItem(
|
|
326
314
|
finalItem,
|
|
327
315
|
importDef.attributeMappings,
|
|
328
316
|
context
|
|
329
|
-
))
|
|
330
|
-
) {
|
|
331
|
-
console.error("Validation failed for item:", finalItem);
|
|
332
|
-
return;
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
let afterContext;
|
|
336
|
-
if (
|
|
337
|
-
(importDef.type === "create" || !importDef.type) &&
|
|
338
|
-
!associatedDoc
|
|
339
|
-
) {
|
|
340
|
-
const createdContext = await this.handleCreate(
|
|
341
|
-
context,
|
|
342
|
-
finalItem,
|
|
343
|
-
updateDefs,
|
|
344
|
-
createIdToUse
|
|
345
317
|
);
|
|
346
|
-
if (
|
|
347
|
-
|
|
318
|
+
if (!validated) {
|
|
319
|
+
console.error("Validation failed for item:", finalItem);
|
|
320
|
+
logger.error("Validation failed for item:", finalItem);
|
|
321
|
+
return;
|
|
348
322
|
}
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
323
|
+
|
|
324
|
+
if (
|
|
325
|
+
(importDef.type === "create" || !importDef.type) &&
|
|
326
|
+
!associatedDoc
|
|
327
|
+
) {
|
|
328
|
+
const createdContext = await this.handleCreate(
|
|
329
|
+
context,
|
|
330
|
+
finalItem,
|
|
331
|
+
updateDefs,
|
|
332
|
+
createIdToUse
|
|
333
|
+
);
|
|
334
|
+
context = { ...context, ...createdContext };
|
|
335
|
+
} else {
|
|
336
|
+
const updatedContext = await this.handleUpdate(
|
|
337
|
+
context,
|
|
338
|
+
finalItem,
|
|
339
|
+
importDef
|
|
340
|
+
);
|
|
341
|
+
context = { ...context, ...updatedContext };
|
|
358
342
|
}
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
343
|
+
const afterImportActionContext = structuredClone(context);
|
|
344
|
+
const attributeMappingsWithActions =
|
|
345
|
+
this.getAttributeMappingsWithActions(
|
|
346
|
+
importDef.attributeMappings,
|
|
347
|
+
afterImportActionContext,
|
|
348
|
+
finalItem
|
|
349
|
+
);
|
|
350
|
+
if (attributeMappingsWithActions.some((m) => m.postImportActions)) {
|
|
351
|
+
logger.info(
|
|
352
|
+
`Pushing to post-import actions queue for ${context.docId}`
|
|
353
|
+
);
|
|
354
|
+
const afterImportOperationContext = ContextObject.parse({
|
|
355
|
+
dbId: db.$id,
|
|
356
|
+
collectionId: collection.$id,
|
|
357
|
+
finalItem: finalItem,
|
|
358
|
+
attributeMappings: attributeMappingsWithActions,
|
|
359
|
+
context: afterImportActionContext,
|
|
360
|
+
});
|
|
361
|
+
await createOrFindAfterImportOperation(
|
|
362
|
+
this.database,
|
|
363
|
+
context.collId,
|
|
364
|
+
afterImportOperationContext
|
|
365
|
+
);
|
|
366
|
+
// this.postImportActionsQueue.push({
|
|
367
|
+
// context: afterImportActionContext,
|
|
368
|
+
// finalItem: finalItem,
|
|
369
|
+
// attributeMappings: attributeMappingsWithActions,
|
|
370
|
+
// });
|
|
371
|
+
}
|
|
372
|
+
})
|
|
373
|
+
);
|
|
374
|
+
results.forEach((result) => {
|
|
375
|
+
if (result.status === "rejected") {
|
|
376
|
+
console.error("A process batch promise was rejected:", result.reason);
|
|
377
|
+
logger.error("An error occurred during creation: ", result.reason);
|
|
392
378
|
}
|
|
393
379
|
});
|
|
394
|
-
// results.forEach((result) => {
|
|
395
|
-
// if (result.status === "rejected") {
|
|
396
|
-
// console.error("A process batch promise was rejected:", result.reason);
|
|
397
|
-
// logger.error("An error occurred during creation: ", result.reason);
|
|
398
|
-
// }
|
|
399
|
-
// });
|
|
400
380
|
}
|
|
401
381
|
}
|
|
402
382
|
|
|
@@ -413,9 +393,6 @@ export class ImportController {
|
|
|
413
393
|
finalItem
|
|
414
394
|
);
|
|
415
395
|
if (!existing) {
|
|
416
|
-
if (id) {
|
|
417
|
-
console.log(`Creating document with provided ID (member): ${id}`);
|
|
418
|
-
}
|
|
419
396
|
const createdDoc = await this.database.createDocument(
|
|
420
397
|
context.dbId,
|
|
421
398
|
context.collId,
|
|
@@ -437,8 +414,6 @@ export class ImportController {
|
|
|
437
414
|
}
|
|
438
415
|
});
|
|
439
416
|
}
|
|
440
|
-
|
|
441
|
-
console.log(`Created document ID: ${createdDoc.$id}`);
|
|
442
417
|
return context;
|
|
443
418
|
} else {
|
|
444
419
|
console.log("Document already exists, skipping creation.");
|
|
@@ -572,6 +547,16 @@ export class ImportController {
|
|
|
572
547
|
for (const batch of resultsData) {
|
|
573
548
|
const actionOperation = ContextObject.parse(JSON.parse(batch.data));
|
|
574
549
|
const { context, finalItem, attributeMappings } = actionOperation;
|
|
550
|
+
if (finalItem.$id && !context.docId) {
|
|
551
|
+
context.docId = finalItem.$id;
|
|
552
|
+
logger.info(
|
|
553
|
+
`Setting docId to ${
|
|
554
|
+
finalItem.$id
|
|
555
|
+
} because docId not found in context, batch ${
|
|
556
|
+
batch.$id
|
|
557
|
+
}, context is ${JSON.stringify(context)}`
|
|
558
|
+
);
|
|
559
|
+
}
|
|
575
560
|
try {
|
|
576
561
|
await this.importDataActions.executeAfterImportActions(
|
|
577
562
|
finalItem,
|
|
@@ -584,12 +569,18 @@ export class ImportController {
|
|
|
584
569
|
"batches",
|
|
585
570
|
batch.$id
|
|
586
571
|
);
|
|
587
|
-
await updateOperation(this.database, operation.$id, {
|
|
588
|
-
status: "completed",
|
|
589
|
-
batches: [],
|
|
590
|
-
});
|
|
591
572
|
} catch (error) {
|
|
592
|
-
logger.error(
|
|
573
|
+
logger.error(
|
|
574
|
+
`Failed to execute batch ${batch.$id}:`,
|
|
575
|
+
error,
|
|
576
|
+
"Context is :",
|
|
577
|
+
context
|
|
578
|
+
);
|
|
579
|
+
await this.database.deleteDocument(
|
|
580
|
+
"migrations",
|
|
581
|
+
"batches",
|
|
582
|
+
batch.$id
|
|
583
|
+
);
|
|
593
584
|
}
|
|
594
585
|
}
|
|
595
586
|
|
|
@@ -2,7 +2,7 @@ import winston from "winston";
|
|
|
2
2
|
|
|
3
3
|
export const logger = winston.createLogger({
|
|
4
4
|
level: "info",
|
|
5
|
-
format: winston.format.
|
|
5
|
+
format: winston.format.json({ space: 2 }),
|
|
6
6
|
defaultMeta: { service: "appwrite-utils-cli" },
|
|
7
7
|
transports: [
|
|
8
8
|
//
|
|
@@ -10,6 +10,7 @@ export const logger = winston.createLogger({
|
|
|
10
10
|
// - Write all logs with importance level of `info` or less to `combined.log`
|
|
11
11
|
//
|
|
12
12
|
new winston.transports.File({ filename: "error.log", level: "error" }),
|
|
13
|
+
new winston.transports.File({ filename: "warn.log", level: "warn" }),
|
|
13
14
|
new winston.transports.File({ filename: "combined.log" }),
|
|
14
15
|
],
|
|
15
16
|
});
|