@firebaseextensions/firestore-bigquery-change-tracker 1.1.31 → 1.1.33
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/lib/bigquery/handleFailedTransactions.js +2 -1
- package/lib/bigquery/index.js +31 -26
- package/lib/bigquery/partitioning.js +68 -58
- package/lib/bigquery/utils.js +62 -0
- package/lib/bigquery/utils.test.js +65 -0
- package/package.json +12 -10
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
const firebase = require("firebase-admin");
|
|
4
|
+
const firestore_1 = require("firebase-admin/firestore");
|
|
4
5
|
if (!firebase.apps.length) {
|
|
5
6
|
firebase.initializeApp();
|
|
6
7
|
firebase.firestore().settings({ ignoreUndefinedProperties: true });
|
|
7
8
|
}
|
|
8
9
|
exports.default = async (rows, config, e) => {
|
|
9
|
-
const db =
|
|
10
|
+
const db = (0, firestore_1.getFirestore)();
|
|
10
11
|
const batchArray = [db.batch()];
|
|
11
12
|
let operationCounter = 0;
|
|
12
13
|
let batchIndex = 0;
|
package/lib/bigquery/index.js
CHANGED
|
@@ -13,6 +13,7 @@ const logs = require("../logs");
|
|
|
13
13
|
const partitioning_1 = require("./partitioning");
|
|
14
14
|
const clustering_1 = require("./clustering");
|
|
15
15
|
const checkUpdates_1 = require("./checkUpdates");
|
|
16
|
+
const utils_1 = require("./utils");
|
|
16
17
|
var schema_2 = require("./schema");
|
|
17
18
|
Object.defineProperty(exports, "RawChangelogSchema", { enumerable: true, get: function () { return schema_2.RawChangelogSchema; } });
|
|
18
19
|
Object.defineProperty(exports, "RawChangelogViewSchema", { enumerable: true, get: function () { return schema_2.RawChangelogViewSchema; } });
|
|
@@ -129,26 +130,10 @@ class FirestoreBigQueryEventHistoryTracker {
|
|
|
129
130
|
* A half a second delay is added per check while the function
|
|
130
131
|
* continually re-checks until the referenced dataset and table become available.
|
|
131
132
|
*/
|
|
132
|
-
async
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
const dataset = this.bigqueryDataset();
|
|
137
|
-
const changelogName = this.rawChangeLogTableName();
|
|
138
|
-
const table = dataset.table(changelogName);
|
|
139
|
-
const [datasetExists] = await dataset.exists();
|
|
140
|
-
const [tableExists] = await table.exists();
|
|
141
|
-
if (datasetExists && tableExists) {
|
|
142
|
-
clearInterval(handle);
|
|
143
|
-
return resolve(table);
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
catch (ex) {
|
|
147
|
-
clearInterval(handle);
|
|
148
|
-
logs.failedToInitializeWait(ex.message);
|
|
149
|
-
}
|
|
150
|
-
}, 5000);
|
|
151
|
-
});
|
|
133
|
+
async _waitForInitialization() {
|
|
134
|
+
const dataset = this.bigqueryDataset();
|
|
135
|
+
const changelogName = this.rawChangeLogTableName();
|
|
136
|
+
return (0, utils_1.waitForInitialization)({ dataset, changelogName });
|
|
152
137
|
}
|
|
153
138
|
/**
|
|
154
139
|
* Inserts rows of data into the BigQuery raw change log table.
|
|
@@ -192,14 +177,34 @@ class FirestoreBigQueryEventHistoryTracker {
|
|
|
192
177
|
if (this._initialized) {
|
|
193
178
|
return;
|
|
194
179
|
}
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
180
|
+
try {
|
|
181
|
+
await this.initializeDataset();
|
|
182
|
+
}
|
|
183
|
+
catch (error) {
|
|
184
|
+
const message = (0, utils_1.parseErrorMessage)(error, "initializing dataset");
|
|
185
|
+
throw new Error(`Error initializing dataset: ${message}`);
|
|
186
|
+
}
|
|
187
|
+
try {
|
|
188
|
+
await this.initializeRawChangeLogTable();
|
|
189
|
+
}
|
|
190
|
+
catch (error) {
|
|
191
|
+
const message = (0, utils_1.parseErrorMessage)(error, "initializing raw change log table");
|
|
192
|
+
throw new Error(`Error initializing raw change log table: ${message}`);
|
|
193
|
+
}
|
|
194
|
+
try {
|
|
195
|
+
await this.initializeLatestView();
|
|
196
|
+
}
|
|
197
|
+
catch (error) {
|
|
198
|
+
const message = (0, utils_1.parseErrorMessage)(error, "initializing latest view");
|
|
199
|
+
throw new Error(`Error initializing latest view: ${message}`);
|
|
200
|
+
}
|
|
201
|
+
await this._waitForInitialization();
|
|
198
202
|
this._initialized = true;
|
|
199
203
|
}
|
|
200
|
-
catch (
|
|
201
|
-
|
|
202
|
-
|
|
204
|
+
catch (error) {
|
|
205
|
+
const message = (0, utils_1.parseErrorMessage)(error, "initializing BigQuery resources");
|
|
206
|
+
console.error("Error initializing BigQuery resources: ", message);
|
|
207
|
+
throw error;
|
|
203
208
|
}
|
|
204
209
|
}
|
|
205
210
|
/**
|
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.Partitioning = void 0;
|
|
4
4
|
const firebase = require("firebase-admin");
|
|
5
5
|
const logs = require("../logs");
|
|
6
|
+
const functions = require("firebase-functions");
|
|
6
7
|
const schema_1 = require("./schema");
|
|
7
8
|
const bigquery_1 = require("@google-cloud/bigquery");
|
|
8
9
|
const types_1 = require("../types");
|
|
@@ -87,41 +88,36 @@ class Partitioning {
|
|
|
87
88
|
return !!metadata.schema;
|
|
88
89
|
}
|
|
89
90
|
hasValidTableReference() {
|
|
90
|
-
|
|
91
|
+
if (!this.table) {
|
|
92
|
+
logs.invalidTableReference();
|
|
93
|
+
}
|
|
91
94
|
return !!this.table;
|
|
92
95
|
}
|
|
93
96
|
async isTablePartitioned() {
|
|
94
|
-
if (!this.table)
|
|
95
|
-
return Promise.resolve(false);
|
|
96
|
-
// No table provided, cannot evaluate
|
|
97
|
-
if (this.table.exists()) {
|
|
98
|
-
logs.cannotPartitionExistingTable(this.table);
|
|
99
|
-
return Promise.resolve(false);
|
|
100
|
-
}
|
|
101
|
-
/*** No table exists, return */
|
|
102
97
|
const [tableExists] = await this.table.exists();
|
|
103
|
-
if (!tableExists)
|
|
104
|
-
return
|
|
105
|
-
/*
|
|
98
|
+
if (!this.table || !tableExists)
|
|
99
|
+
return false;
|
|
100
|
+
/* Return true if partition metadata already exists */
|
|
106
101
|
const [metadata] = await this.table.getMetadata();
|
|
107
|
-
if (
|
|
108
|
-
|
|
102
|
+
if (metadata.timePartitioning) {
|
|
103
|
+
logs.cannotPartitionExistingTable(this.table);
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
109
106
|
/** Find schema fields **/
|
|
110
107
|
const schemaFields = await this.metaDataSchemaFields();
|
|
111
|
-
/**
|
|
108
|
+
/** Return false if no schema exists */
|
|
112
109
|
if (!schemaFields)
|
|
113
|
-
return
|
|
110
|
+
return false;
|
|
114
111
|
/* Return false if time partition field not found */
|
|
115
112
|
return schemaFields.some((column) => column.name === this.config.timePartitioningField);
|
|
116
113
|
}
|
|
117
114
|
async isValidPartitionForExistingTable() {
|
|
115
|
+
/** Return false if partition type option has not been set */
|
|
116
|
+
if (!this.isPartitioningEnabled())
|
|
117
|
+
return false;
|
|
118
|
+
/* Return false if table is already partitioned */
|
|
118
119
|
const isPartitioned = await this.isTablePartitioned();
|
|
119
120
|
if (isPartitioned)
|
|
120
|
-
return Promise.resolve(false);
|
|
121
|
-
return this.hasValidCustomPartitionConfig();
|
|
122
|
-
}
|
|
123
|
-
isValidPartitionForNewTable() {
|
|
124
|
-
if (!this.isPartitioningEnabled())
|
|
125
121
|
return false;
|
|
126
122
|
return this.hasValidCustomPartitionConfig();
|
|
127
123
|
}
|
|
@@ -181,57 +177,71 @@ class Partitioning {
|
|
|
181
177
|
const { timePartitioningField } = this.config;
|
|
182
178
|
return fields.map(($) => $.name).includes(timePartitioningField);
|
|
183
179
|
}
|
|
180
|
+
async shouldAddPartitioningToSchema() {
|
|
181
|
+
if (!this.isPartitioningEnabled()) {
|
|
182
|
+
return { proceed: false, message: "Partitioning not enabled" };
|
|
183
|
+
}
|
|
184
|
+
if (!this.hasValidTableReference()) {
|
|
185
|
+
return { proceed: false, message: "Invalid table reference" };
|
|
186
|
+
}
|
|
187
|
+
if (!this.hasValidCustomPartitionConfig()) {
|
|
188
|
+
return { proceed: false, message: "Invalid partition config" };
|
|
189
|
+
}
|
|
190
|
+
if (!this.hasValidTimePartitionType()) {
|
|
191
|
+
return { proceed: false, message: "Invalid partition type" };
|
|
192
|
+
}
|
|
193
|
+
if (!this.hasValidTimePartitionOption()) {
|
|
194
|
+
return { proceed: false, message: "Invalid partition option" };
|
|
195
|
+
}
|
|
196
|
+
if (this.hasHourAndDatePartitionConfig()) {
|
|
197
|
+
return {
|
|
198
|
+
proceed: false,
|
|
199
|
+
message: "Invalid partitioning and field type combination",
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
if (this.customFieldExists()) {
|
|
203
|
+
return { proceed: false, message: "Field already exists on schema" };
|
|
204
|
+
}
|
|
205
|
+
if (await this.isTablePartitioned()) {
|
|
206
|
+
return { proceed: false, message: "Table is already partitioned" };
|
|
207
|
+
}
|
|
208
|
+
if (!this.config.timePartitioningField) {
|
|
209
|
+
return { proceed: false, message: "Partition field not provided" };
|
|
210
|
+
}
|
|
211
|
+
return { proceed: true, message: "" };
|
|
212
|
+
}
|
|
184
213
|
async addPartitioningToSchema(fields = []) {
|
|
185
|
-
|
|
186
|
-
if (!
|
|
187
|
-
|
|
188
|
-
/** return if table is already partitioned **/
|
|
189
|
-
if (await this.isTablePartitioned())
|
|
190
|
-
return;
|
|
191
|
-
/** return if an invalid partition type has been requested**/
|
|
192
|
-
if (!this.hasValidTimePartitionType())
|
|
193
|
-
return;
|
|
194
|
-
/** Return if invalid partitioning and field type combination */
|
|
195
|
-
if (this.hasHourAndDatePartitionConfig())
|
|
196
|
-
return;
|
|
197
|
-
/** return if an invalid partition type has been requested**/
|
|
198
|
-
if (!this.hasValidCustomPartitionConfig())
|
|
199
|
-
return;
|
|
200
|
-
/** return if an invalid partition type has been requested**/
|
|
201
|
-
if (!this.hasValidCustomPartitionConfig())
|
|
202
|
-
return;
|
|
203
|
-
/** update fields with new schema option ** */
|
|
204
|
-
if (!this.hasValidTimePartitionOption())
|
|
205
|
-
return;
|
|
206
|
-
/* Check if partition field has been provided */
|
|
207
|
-
if (!this.config.timePartitioningField)
|
|
208
|
-
return;
|
|
209
|
-
// if (await !this.hasExistingSchema) return Promise.resolve();
|
|
210
|
-
// Field already exists on schema, skip
|
|
211
|
-
if (this.customFieldExists(fields))
|
|
214
|
+
const { proceed, message } = await this.shouldAddPartitioningToSchema();
|
|
215
|
+
if (!proceed) {
|
|
216
|
+
functions.logger.warn(`Did not add partitioning to schema: ${message}`);
|
|
212
217
|
return;
|
|
218
|
+
}
|
|
219
|
+
// Add new partitioning field
|
|
213
220
|
fields.push((0, schema_1.getNewPartitionField)(this.config));
|
|
214
|
-
|
|
215
|
-
logs.addPartitionFieldColumn(this.table.id, this.config.timePartitioningField);
|
|
216
|
-
return;
|
|
221
|
+
functions.logger.log(`Added new partition field: ${this.config.timePartitioningField} to table ID: ${this.table.id}`);
|
|
217
222
|
}
|
|
218
223
|
async updateTableMetadata(options) {
|
|
219
|
-
/**
|
|
224
|
+
/** Return if partition type option has not been set */
|
|
225
|
+
if (!this.isPartitioningEnabled())
|
|
226
|
+
return;
|
|
227
|
+
/** Return if class has invalid table reference */
|
|
228
|
+
if (!this.hasValidTableReference())
|
|
229
|
+
return;
|
|
230
|
+
/** Return if table is already partitioned **/
|
|
220
231
|
if (await this.isTablePartitioned())
|
|
221
232
|
return;
|
|
222
|
-
/**
|
|
233
|
+
/** Return if an invalid partition type has been requested**/
|
|
234
|
+
if (!this.hasValidCustomPartitionConfig())
|
|
235
|
+
return;
|
|
236
|
+
/** Return if an invalid partition type has been requested**/
|
|
223
237
|
if (!this.hasValidTimePartitionType())
|
|
224
238
|
return;
|
|
225
|
-
/**
|
|
239
|
+
/** Update fields with new schema option ** */
|
|
226
240
|
if (!this.hasValidTimePartitionOption())
|
|
227
241
|
return;
|
|
228
242
|
/** Return if invalid partitioning and field type combination */
|
|
229
243
|
if (this.hasHourAndDatePartitionConfig())
|
|
230
244
|
return;
|
|
231
|
-
/** return if an invalid partition type has been requested**/
|
|
232
|
-
if (!this.hasValidCustomPartitionConfig())
|
|
233
|
-
return;
|
|
234
|
-
// if (await !this.hasExistingSchema) return Promise.resolve();
|
|
235
245
|
if (this.config.timePartitioning) {
|
|
236
246
|
options.timePartitioning = { type: this.config.timePartitioning };
|
|
237
247
|
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.parseErrorMessage = exports.waitForInitialization = void 0;
|
|
4
|
+
const logs = require("../logs");
|
|
5
|
+
/**
|
|
6
|
+
* Periodically checks for the existence of a dataset and table until both are found or a maximum number of attempts is reached.
|
|
7
|
+
* @param {WaitForInitializationParams} params - Parameters for initialization including the dataset and the table name to check.
|
|
8
|
+
* @param {number} maxAttempts - Maximum number of attempts before giving up (defaults to 12).
|
|
9
|
+
* @returns {Promise<Table>} A promise that resolves with the Table if it exists, or rejects if it doesn't exist after maxAttempts or an error occurs.
|
|
10
|
+
* @throws {Error} Throws an error if the dataset or table cannot be verified to exist after multiple attempts or if an unexpected error occurs.
|
|
11
|
+
*/
|
|
12
|
+
async function waitForInitialization({ dataset, changelogName }, maxAttempts = 12) {
|
|
13
|
+
return new Promise((resolve, reject) => {
|
|
14
|
+
let attempts = 0;
|
|
15
|
+
let handle = setInterval(async () => {
|
|
16
|
+
try {
|
|
17
|
+
const [datasetExists] = await dataset.exists();
|
|
18
|
+
const table = dataset.table(changelogName);
|
|
19
|
+
const [tableExists] = await table.exists();
|
|
20
|
+
if (datasetExists && tableExists) {
|
|
21
|
+
clearInterval(handle);
|
|
22
|
+
resolve(table);
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
attempts++;
|
|
26
|
+
if (attempts >= maxAttempts) {
|
|
27
|
+
clearInterval(handle);
|
|
28
|
+
reject(new Error("Initialization timed out. Dataset or table could not be verified to exist after multiple attempts."));
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
clearInterval(handle);
|
|
34
|
+
const message = error instanceof Error
|
|
35
|
+
? error.message
|
|
36
|
+
: "An unexpected error occurred";
|
|
37
|
+
logs.failedToInitializeWait(message);
|
|
38
|
+
reject(new Error(message));
|
|
39
|
+
}
|
|
40
|
+
}, 500);
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
exports.waitForInitialization = waitForInitialization;
|
|
44
|
+
function parseErrorMessage(error, process = "") {
|
|
45
|
+
let message;
|
|
46
|
+
if (error instanceof Error) {
|
|
47
|
+
// Standard error handling
|
|
48
|
+
message = error.message;
|
|
49
|
+
}
|
|
50
|
+
else if (typeof error === "object" &&
|
|
51
|
+
error !== null &&
|
|
52
|
+
"message" in error) {
|
|
53
|
+
// Handling errors from APIs or other sources that are not native Error objects
|
|
54
|
+
message = error.message;
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
// Fallback for when error is neither an Error object nor an expected structured object
|
|
58
|
+
message = `An unexpected error occurred${process ? ` during ${process}` : ""}.`;
|
|
59
|
+
}
|
|
60
|
+
return message;
|
|
61
|
+
}
|
|
62
|
+
exports.parseErrorMessage = parseErrorMessage;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const utils_1 = require("./utils");
|
|
4
|
+
const logs = require("../logs");
|
|
5
|
+
jest.mock("@google-cloud/bigquery");
|
|
6
|
+
jest.mock("../../logs");
|
|
7
|
+
const dataset = {
|
|
8
|
+
exists: jest.fn(),
|
|
9
|
+
table: jest.fn(),
|
|
10
|
+
};
|
|
11
|
+
const table = {
|
|
12
|
+
exists: jest.fn(),
|
|
13
|
+
};
|
|
14
|
+
const changelogName = "testTable";
|
|
15
|
+
describe("waitForInitialization", () => {
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
jest.clearAllMocks();
|
|
18
|
+
dataset.table.mockReturnValue(table);
|
|
19
|
+
});
|
|
20
|
+
test("should successfully find the dataset and table", async () => {
|
|
21
|
+
dataset.exists.mockResolvedValue([true]);
|
|
22
|
+
table.exists.mockResolvedValue([true]);
|
|
23
|
+
const result = await (0, utils_1.waitForInitialization)({
|
|
24
|
+
dataset: dataset,
|
|
25
|
+
changelogName,
|
|
26
|
+
});
|
|
27
|
+
expect(result).toBe(table);
|
|
28
|
+
expect(dataset.exists).toHaveBeenCalledTimes(1);
|
|
29
|
+
expect(table.exists).toHaveBeenCalledTimes(1);
|
|
30
|
+
});
|
|
31
|
+
test("should fail after max attempts if table does not exist", async () => {
|
|
32
|
+
dataset.exists.mockResolvedValue([true]);
|
|
33
|
+
table.exists.mockResolvedValue([false]);
|
|
34
|
+
await expect((0, utils_1.waitForInitialization)({ dataset: dataset, changelogName }, 3)).rejects.toThrow("Initialization timed out. Dataset or table could not be verified to exist after multiple attempts.");
|
|
35
|
+
expect(dataset.exists).toHaveBeenCalledTimes(3);
|
|
36
|
+
expect(table.exists).toHaveBeenCalledTimes(3);
|
|
37
|
+
});
|
|
38
|
+
test("should handle and throw an error if dataset.exists throws", async () => {
|
|
39
|
+
const error = new Error("Access denied");
|
|
40
|
+
dataset.exists.mockRejectedValue(error);
|
|
41
|
+
await expect((0, utils_1.waitForInitialization)({
|
|
42
|
+
dataset: dataset,
|
|
43
|
+
changelogName,
|
|
44
|
+
})).rejects.toThrow("Access denied");
|
|
45
|
+
expect(logs.failedToInitializeWait).toHaveBeenCalledWith(error.message);
|
|
46
|
+
});
|
|
47
|
+
test("should handle and throw an error if table.exists throws", async () => {
|
|
48
|
+
dataset.exists.mockResolvedValue([true]);
|
|
49
|
+
const error = new Error("Table error");
|
|
50
|
+
table.exists.mockRejectedValue(error);
|
|
51
|
+
await expect((0, utils_1.waitForInitialization)({
|
|
52
|
+
dataset: dataset,
|
|
53
|
+
changelogName,
|
|
54
|
+
})).rejects.toThrow("Table error");
|
|
55
|
+
expect(logs.failedToInitializeWait).toHaveBeenCalledWith(error.message);
|
|
56
|
+
});
|
|
57
|
+
test("should handle unexpected error types gracefully", async () => {
|
|
58
|
+
dataset.exists.mockRejectedValue("String error");
|
|
59
|
+
await expect((0, utils_1.waitForInitialization)({
|
|
60
|
+
dataset: dataset,
|
|
61
|
+
changelogName,
|
|
62
|
+
})).rejects.toThrow("An unexpected error occurred");
|
|
63
|
+
expect(logs.failedToInitializeWait).toHaveBeenCalledWith("An unexpected error occurred");
|
|
64
|
+
});
|
|
65
|
+
});
|
package/package.json
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"url": "github.com/firebase/extensions.git",
|
|
6
6
|
"directory": "firestore-bigquery-export/firestore-bigquery-change-tracker"
|
|
7
7
|
},
|
|
8
|
-
"version": "1.1.
|
|
8
|
+
"version": "1.1.33",
|
|
9
9
|
"description": "Core change-tracker library for Cloud Firestore Collection BigQuery Exports",
|
|
10
10
|
"main": "./lib/index.js",
|
|
11
11
|
"scripts": {
|
|
@@ -23,10 +23,10 @@
|
|
|
23
23
|
"author": "Jan Wyszynski <wyszynski@google.com>",
|
|
24
24
|
"license": "Apache-2.0",
|
|
25
25
|
"dependencies": {
|
|
26
|
-
"@google-cloud/bigquery": "^
|
|
27
|
-
"@google-cloud/resource-manager": "^
|
|
28
|
-
"firebase-admin": "^
|
|
29
|
-
"firebase-functions": "^
|
|
26
|
+
"@google-cloud/bigquery": "^7.6.0",
|
|
27
|
+
"@google-cloud/resource-manager": "^5.1.0",
|
|
28
|
+
"firebase-admin": "^12.0.0",
|
|
29
|
+
"firebase-functions": "^4.9.0",
|
|
30
30
|
"generate-schema": "^2.6.0",
|
|
31
31
|
"inquirer": "^6.4.0",
|
|
32
32
|
"lodash": "^4.17.14",
|
|
@@ -36,15 +36,17 @@
|
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
38
|
"@types/chai": "^4.1.6",
|
|
39
|
-
"@types/jest": "
|
|
39
|
+
"@types/jest": "29.5.0",
|
|
40
40
|
"@types/node": "14.18.34",
|
|
41
41
|
"@types/traverse": "^0.6.32",
|
|
42
42
|
"chai": "^4.2.0",
|
|
43
|
-
"jest": "^24.9.0",
|
|
44
43
|
"nyc": "^14.0.0",
|
|
45
44
|
"rimraf": "^2.6.3",
|
|
46
|
-
"
|
|
47
|
-
"
|
|
48
|
-
"
|
|
45
|
+
"typescript": "^4.9.4",
|
|
46
|
+
"jest": "29.5.0",
|
|
47
|
+
"jest-environment-node": "29.5.0",
|
|
48
|
+
"mocked-env": "^1.3.2",
|
|
49
|
+
"ts-jest": "29.1.2",
|
|
50
|
+
"jest-config": "29.5.0"
|
|
49
51
|
}
|
|
50
52
|
}
|