@strapi/database 4.6.0-beta.1 → 4.6.0
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/__tests__/index.test.js +93 -0
- package/lib/dialects/postgresql/index.js +10 -2
- package/lib/entity-manager/__tests__/relations-orderer.test.js +147 -68
- package/lib/entity-manager/__tests__/sort-connect-array.test.js +79 -0
- package/lib/entity-manager/index.js +15 -6
- package/lib/entity-manager/regular-relations.js +133 -120
- package/lib/entity-manager/relations-orderer.js +120 -21
- package/lib/errors/index.js +2 -0
- package/lib/errors/invalid-relation.js +14 -0
- package/lib/index.d.ts +9 -0
- package/lib/index.js +47 -5
- package/lib/metadata/relations.js +4 -1
- package/lib/migrations/index.js +1 -1
- package/lib/query/query-builder.js +10 -1
- package/lib/tests/transactions.test.api.js +308 -0
- package/lib/transaction-context.js +17 -0
- package/lib/validations/index.js +20 -0
- package/lib/validations/relations/bidirectional.js +89 -0
- package/lib/validations/relations/index.js +14 -0
- package/package.json +4 -4
|
@@ -4,6 +4,7 @@ const _ = require('lodash/fp');
|
|
|
4
4
|
|
|
5
5
|
const { DatabaseError } = require('../errors');
|
|
6
6
|
const helpers = require('./helpers');
|
|
7
|
+
const transactionCtx = require('../transaction-context');
|
|
7
8
|
|
|
8
9
|
const createQueryBuilder = (uid, db, initialState = {}) => {
|
|
9
10
|
const meta = db.metadata.get(uid);
|
|
@@ -473,10 +474,18 @@ const createQueryBuilder = (uid, db, initialState = {}) => {
|
|
|
473
474
|
try {
|
|
474
475
|
const qb = this.getKnexQuery();
|
|
475
476
|
|
|
477
|
+
if (transactionCtx.get()) {
|
|
478
|
+
qb.transacting(transactionCtx.get());
|
|
479
|
+
}
|
|
480
|
+
|
|
476
481
|
const rows = await qb;
|
|
477
482
|
|
|
478
483
|
if (state.populate && !_.isNil(rows)) {
|
|
479
|
-
await helpers.applyPopulate(_.castArray(rows), state.populate, {
|
|
484
|
+
await helpers.applyPopulate(_.castArray(rows), state.populate, {
|
|
485
|
+
qb: this,
|
|
486
|
+
uid,
|
|
487
|
+
db,
|
|
488
|
+
});
|
|
480
489
|
}
|
|
481
490
|
|
|
482
491
|
let results = rows;
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { createStrapiInstance } = require('../../../../../test/helpers/strapi');
|
|
4
|
+
|
|
5
|
+
let strapi;
|
|
6
|
+
|
|
7
|
+
describe('transactions', () => {
|
|
8
|
+
let original;
|
|
9
|
+
beforeAll(async () => {
|
|
10
|
+
strapi = await createStrapiInstance();
|
|
11
|
+
original = await strapi.db
|
|
12
|
+
.queryBuilder('strapi::core-store')
|
|
13
|
+
.select(['*'])
|
|
14
|
+
.where({ id: 1 })
|
|
15
|
+
.execute();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
afterAll(async () => {
|
|
19
|
+
await strapi.destroy();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
afterEach(async () => {
|
|
23
|
+
await strapi.db
|
|
24
|
+
.queryBuilder('strapi::core-store')
|
|
25
|
+
.update({
|
|
26
|
+
key: original[0].key,
|
|
27
|
+
})
|
|
28
|
+
.where({ id: 1 })
|
|
29
|
+
.execute();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe('using a transaction method', () => {
|
|
33
|
+
test('commits successfully', async () => {
|
|
34
|
+
await strapi.db.transaction(async () => {
|
|
35
|
+
await strapi.db
|
|
36
|
+
.queryBuilder('strapi::core-store')
|
|
37
|
+
.update({
|
|
38
|
+
key: 'wrong key',
|
|
39
|
+
})
|
|
40
|
+
.where({ id: 1 })
|
|
41
|
+
.execute();
|
|
42
|
+
|
|
43
|
+
await strapi.db
|
|
44
|
+
.queryBuilder('strapi::core-store')
|
|
45
|
+
.update({
|
|
46
|
+
key: 'new key',
|
|
47
|
+
})
|
|
48
|
+
.where({ id: 1 })
|
|
49
|
+
.execute();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const end = await strapi.db
|
|
53
|
+
.queryBuilder('strapi::core-store')
|
|
54
|
+
.select(['*'])
|
|
55
|
+
.where({ id: 1 })
|
|
56
|
+
.execute();
|
|
57
|
+
|
|
58
|
+
expect(end[0].key).toEqual('new key');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('rollback successfully', async () => {
|
|
62
|
+
try {
|
|
63
|
+
await strapi.db.transaction(async () => {
|
|
64
|
+
// this is valid
|
|
65
|
+
await strapi.db
|
|
66
|
+
.queryBuilder('strapi::core-store')
|
|
67
|
+
.update({
|
|
68
|
+
key: 'wrong key',
|
|
69
|
+
})
|
|
70
|
+
.where({ id: 1 })
|
|
71
|
+
.execute();
|
|
72
|
+
|
|
73
|
+
// this throws
|
|
74
|
+
await strapi.db
|
|
75
|
+
.queryBuilder('invalid_uid')
|
|
76
|
+
.update({
|
|
77
|
+
key: 'bad key',
|
|
78
|
+
invalid_key: 'error',
|
|
79
|
+
})
|
|
80
|
+
.where({ id: 1 })
|
|
81
|
+
.execute();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
expect('this should not be reached').toBe(false);
|
|
85
|
+
} catch (e) {
|
|
86
|
+
// do nothing
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const end = await strapi.db
|
|
90
|
+
.queryBuilder('strapi::core-store')
|
|
91
|
+
.select(['*'])
|
|
92
|
+
.where({ id: 1 })
|
|
93
|
+
.execute();
|
|
94
|
+
|
|
95
|
+
expect(end[0].key).toEqual(original[0].key);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test('nested rollback -> rollback works', async () => {
|
|
99
|
+
try {
|
|
100
|
+
await strapi.db.transaction(async () => {
|
|
101
|
+
// this is valid
|
|
102
|
+
await strapi.db
|
|
103
|
+
.queryBuilder('strapi::core-store')
|
|
104
|
+
.update({
|
|
105
|
+
key: 'changed key',
|
|
106
|
+
})
|
|
107
|
+
.where({ id: 1 })
|
|
108
|
+
.execute();
|
|
109
|
+
|
|
110
|
+
// here we'll make a nested transaction that throws and then confirm we still have "changed key" from above
|
|
111
|
+
try {
|
|
112
|
+
await strapi.db.transaction(async () => {
|
|
113
|
+
await strapi.db
|
|
114
|
+
.queryBuilder('strapi::core-store')
|
|
115
|
+
.update({
|
|
116
|
+
key: 'changed key - nested',
|
|
117
|
+
})
|
|
118
|
+
.where({ id: 1 })
|
|
119
|
+
.execute();
|
|
120
|
+
|
|
121
|
+
// this should throw and roll back
|
|
122
|
+
await strapi.db
|
|
123
|
+
.queryBuilder('invalid_uid')
|
|
124
|
+
.update({
|
|
125
|
+
invalid_key: 'error',
|
|
126
|
+
})
|
|
127
|
+
.where({ id: 1 })
|
|
128
|
+
.execute();
|
|
129
|
+
});
|
|
130
|
+
} catch (e) {
|
|
131
|
+
// do nothing
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// should equal the result from above
|
|
135
|
+
const result = await strapi.db
|
|
136
|
+
.queryBuilder('strapi::core-store')
|
|
137
|
+
.select(['*'])
|
|
138
|
+
.where({ id: 1 })
|
|
139
|
+
.execute();
|
|
140
|
+
|
|
141
|
+
expect(result[0].key).toEqual('changed key');
|
|
142
|
+
|
|
143
|
+
// this throws
|
|
144
|
+
await strapi.db
|
|
145
|
+
.queryBuilder('invalid_uid')
|
|
146
|
+
.update({
|
|
147
|
+
key: original[0].key,
|
|
148
|
+
invalid_key: 'error',
|
|
149
|
+
})
|
|
150
|
+
.where({ id: 1 })
|
|
151
|
+
.execute();
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
expect('this should not be reached').toBe(false);
|
|
155
|
+
} catch (e) {
|
|
156
|
+
// do nothing
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const end = await strapi.db
|
|
160
|
+
.queryBuilder('strapi::core-store')
|
|
161
|
+
.select(['*'])
|
|
162
|
+
.where({ id: 1 })
|
|
163
|
+
.execute();
|
|
164
|
+
|
|
165
|
+
expect(end[0].key).toEqual(original[0].key);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test('nested commit -> rollback works', async () => {
|
|
169
|
+
try {
|
|
170
|
+
await strapi.db.transaction(async () => {
|
|
171
|
+
// this is valid
|
|
172
|
+
await strapi.db
|
|
173
|
+
.queryBuilder('strapi::core-store')
|
|
174
|
+
.update({
|
|
175
|
+
key: 'changed key',
|
|
176
|
+
})
|
|
177
|
+
.where({ id: 1 })
|
|
178
|
+
.execute();
|
|
179
|
+
|
|
180
|
+
// here we'll make a nested transaction that works, and then later we'll rollback the outer transaction
|
|
181
|
+
try {
|
|
182
|
+
await strapi.db.transaction(async () => {
|
|
183
|
+
await strapi.db
|
|
184
|
+
.queryBuilder('strapi::core-store')
|
|
185
|
+
.update({
|
|
186
|
+
key: 'changed key - nested',
|
|
187
|
+
})
|
|
188
|
+
.where({ id: 1 })
|
|
189
|
+
.execute();
|
|
190
|
+
});
|
|
191
|
+
} catch (e) {
|
|
192
|
+
// do nothing
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// should equal the result from above
|
|
196
|
+
const result = await strapi.db
|
|
197
|
+
.queryBuilder('strapi::core-store')
|
|
198
|
+
.select(['*'])
|
|
199
|
+
.where({ id: 1 })
|
|
200
|
+
.execute();
|
|
201
|
+
|
|
202
|
+
expect(result[0].key).toEqual('changed key - nested');
|
|
203
|
+
|
|
204
|
+
// this throws
|
|
205
|
+
await strapi.db
|
|
206
|
+
.queryBuilder('invalid_uid')
|
|
207
|
+
.update({
|
|
208
|
+
key: original[0].key,
|
|
209
|
+
invalid_key: 'error',
|
|
210
|
+
})
|
|
211
|
+
.where({ id: 1 })
|
|
212
|
+
.execute();
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
expect('this should not be reached').toBe(false);
|
|
216
|
+
} catch (e) {
|
|
217
|
+
// do nothing
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const end = await strapi.db
|
|
221
|
+
.queryBuilder('strapi::core-store')
|
|
222
|
+
.select(['*'])
|
|
223
|
+
.where({ id: 1 })
|
|
224
|
+
.execute();
|
|
225
|
+
|
|
226
|
+
expect(end[0].key).toEqual(original[0].key);
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
describe('using a transaction object', () => {
|
|
231
|
+
test('commits successfully', async () => {
|
|
232
|
+
const trx = await strapi.db.transaction();
|
|
233
|
+
|
|
234
|
+
try {
|
|
235
|
+
await strapi.db
|
|
236
|
+
.queryBuilder('strapi::core-store')
|
|
237
|
+
.update({
|
|
238
|
+
key: 'wrong key',
|
|
239
|
+
})
|
|
240
|
+
.where({ id: 1 })
|
|
241
|
+
.transacting(trx.get())
|
|
242
|
+
.execute();
|
|
243
|
+
|
|
244
|
+
await strapi.db
|
|
245
|
+
.queryBuilder('strapi::core-store')
|
|
246
|
+
.update({
|
|
247
|
+
key: original[0].key,
|
|
248
|
+
})
|
|
249
|
+
.where({ id: 1 })
|
|
250
|
+
.transacting(trx.get())
|
|
251
|
+
.execute();
|
|
252
|
+
|
|
253
|
+
await trx.commit();
|
|
254
|
+
} catch (e) {
|
|
255
|
+
await trx.rollback();
|
|
256
|
+
console.log(e.message);
|
|
257
|
+
expect('this should not be reached').toBe(false);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const end = await strapi.db
|
|
261
|
+
.queryBuilder('strapi::core-store')
|
|
262
|
+
.select(['*'])
|
|
263
|
+
.where({ id: 1 })
|
|
264
|
+
.execute();
|
|
265
|
+
|
|
266
|
+
expect(end[0].key).toEqual('strapi_content_types_schema');
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
test('rollback successfully', async () => {
|
|
270
|
+
const trx = await strapi.db.transaction();
|
|
271
|
+
|
|
272
|
+
try {
|
|
273
|
+
await strapi.db
|
|
274
|
+
.queryBuilder('strapi::core-store')
|
|
275
|
+
.update({
|
|
276
|
+
key: 'wrong key',
|
|
277
|
+
})
|
|
278
|
+
.where({ id: 1 })
|
|
279
|
+
.transacting(trx.get())
|
|
280
|
+
.execute();
|
|
281
|
+
|
|
282
|
+
// this query should throw because it has errors
|
|
283
|
+
await strapi.db
|
|
284
|
+
.queryBuilder('invalid_uid')
|
|
285
|
+
.update({
|
|
286
|
+
key: 123,
|
|
287
|
+
key_not_here: 'this should error',
|
|
288
|
+
})
|
|
289
|
+
.where({ id: 'this should error' })
|
|
290
|
+
.transacting(trx.get())
|
|
291
|
+
.execute();
|
|
292
|
+
|
|
293
|
+
await trx.commit();
|
|
294
|
+
expect('this should not be reached').toBe(false);
|
|
295
|
+
} catch (e) {
|
|
296
|
+
await trx.rollback();
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const end = await strapi.db
|
|
300
|
+
.queryBuilder('strapi::core-store')
|
|
301
|
+
.select(['*'])
|
|
302
|
+
.where({ id: 1 })
|
|
303
|
+
.execute();
|
|
304
|
+
|
|
305
|
+
expect(end[0].key).toEqual('strapi_content_types_schema');
|
|
306
|
+
});
|
|
307
|
+
});
|
|
308
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { AsyncLocalStorage } = require('async_hooks');
|
|
4
|
+
|
|
5
|
+
const storage = new AsyncLocalStorage();
|
|
6
|
+
|
|
7
|
+
const transactionCtx = {
|
|
8
|
+
async run(store, cb) {
|
|
9
|
+
return storage.run(store, cb);
|
|
10
|
+
},
|
|
11
|
+
|
|
12
|
+
get() {
|
|
13
|
+
return storage.getStore();
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
module.exports = transactionCtx;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { validateRelations } = require('./relations');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Validate if the database is in a valid state before starting the server.
|
|
7
|
+
*
|
|
8
|
+
* @param {*} db - Database instance
|
|
9
|
+
*/
|
|
10
|
+
async function validateDatabase(db) {
|
|
11
|
+
const relationErrors = await validateRelations(db);
|
|
12
|
+
const errorList = [...relationErrors];
|
|
13
|
+
|
|
14
|
+
if (errorList.length > 0) {
|
|
15
|
+
errorList.forEach((error) => strapi.log.error(error));
|
|
16
|
+
throw new Error('There are errors in some of your models. Please check the logs above.');
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
module.exports = { validateDatabase };
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const types = require('../../types');
|
|
4
|
+
const { getJoinTableName } = require('../../metadata/relations');
|
|
5
|
+
|
|
6
|
+
const getLinksWithoutMappedBy = (db) => {
|
|
7
|
+
const relationsToUpdate = {};
|
|
8
|
+
|
|
9
|
+
db.metadata.forEach((contentType) => {
|
|
10
|
+
const attributes = contentType.attributes;
|
|
11
|
+
|
|
12
|
+
// For each relation attribute, add the joinTable name to tablesToUpdate
|
|
13
|
+
Object.values(attributes).forEach((attribute) => {
|
|
14
|
+
if (!types.isRelation(attribute.type)) return;
|
|
15
|
+
|
|
16
|
+
if (attribute.inversedBy) {
|
|
17
|
+
const invRelation = db.metadata.get(attribute.target).attributes[attribute.inversedBy];
|
|
18
|
+
|
|
19
|
+
// Both relations use inversedBy.
|
|
20
|
+
if (invRelation.inversedBy) {
|
|
21
|
+
relationsToUpdate[attribute.joinTable.name] = {
|
|
22
|
+
relation: attribute,
|
|
23
|
+
invRelation,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
return Object.values(relationsToUpdate);
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const isLinkTableEmpty = async (db, linkTableName) => {
|
|
34
|
+
// If the table doesn't exist, it's empty
|
|
35
|
+
const exists = await db.getConnection().schema.hasTable(linkTableName);
|
|
36
|
+
if (!exists) return true;
|
|
37
|
+
|
|
38
|
+
const result = await db.getConnection().count('* as count').from(linkTableName);
|
|
39
|
+
return Number(result[0].count) === 0;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Validates bidirectional relations before starting the server.
|
|
44
|
+
* - If both sides use inversedBy, one of the sides must switch to mappedBy.
|
|
45
|
+
* When this happens, two join tables exist in the database.
|
|
46
|
+
* This makes sure you switch the side which does not delete any data.
|
|
47
|
+
*
|
|
48
|
+
* @param {*} db
|
|
49
|
+
* @return {*}
|
|
50
|
+
*/
|
|
51
|
+
const validateBidirectionalRelations = async (db) => {
|
|
52
|
+
const invalidLinks = getLinksWithoutMappedBy(db);
|
|
53
|
+
const errorList = [];
|
|
54
|
+
|
|
55
|
+
for (const { relation, invRelation } of invalidLinks) {
|
|
56
|
+
const contentType = db.metadata.get(invRelation.target);
|
|
57
|
+
const invContentType = db.metadata.get(relation.target);
|
|
58
|
+
|
|
59
|
+
// Generate the join table name based on the relation target table and attribute name.
|
|
60
|
+
const joinTableName = getJoinTableName(contentType.tableName, invRelation.inversedBy);
|
|
61
|
+
const inverseJoinTableName = getJoinTableName(invContentType.tableName, relation.inversedBy);
|
|
62
|
+
|
|
63
|
+
const joinTableEmpty = await isLinkTableEmpty(db, joinTableName);
|
|
64
|
+
const inverseJoinTableEmpty = await isLinkTableEmpty(db, inverseJoinTableName);
|
|
65
|
+
|
|
66
|
+
if (joinTableEmpty) {
|
|
67
|
+
process.emitWarning(
|
|
68
|
+
`Error on attribute "${invRelation.inversedBy}" in model "${contentType.singularName}" (${contentType.uid}).` +
|
|
69
|
+
` Please modify your ${contentType.singularName} schema by renaming the key "inversedBy" to "mappedBy".` +
|
|
70
|
+
` Ex: { "inversedBy": "${relation.inversedBy}" } -> { "mappedBy": "${relation.inversedBy}" }`
|
|
71
|
+
);
|
|
72
|
+
} else if (inverseJoinTableEmpty) {
|
|
73
|
+
// Its safe to delete the inverse join table
|
|
74
|
+
process.emitWarning(
|
|
75
|
+
`Error on attribute "${relation.inversedBy}" in model "${invContentType.singularName}" (${invContentType.uid}).` +
|
|
76
|
+
` Please modify your ${invContentType.singularName} schema by renaming the key "inversedBy" to "mappedBy".` +
|
|
77
|
+
` Ex: { "inversedBy": "${invRelation.inversedBy}" } -> { "mappedBy": "${invRelation.inversedBy}" }`
|
|
78
|
+
);
|
|
79
|
+
} else {
|
|
80
|
+
// Both sides have data in the join table
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return errorList;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
module.exports = {
|
|
88
|
+
validateBidirectionalRelations,
|
|
89
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { validateBidirectionalRelations } = require('./bidirectional');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Validates if relations data and tables are in a valid state before
|
|
7
|
+
* starting the server.
|
|
8
|
+
*/
|
|
9
|
+
const validateRelations = async (db) => {
|
|
10
|
+
const bidirectionalRelationsErrors = await validateBidirectionalRelations(db);
|
|
11
|
+
return [...bidirectionalRelationsErrors];
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
module.exports = { validateRelations };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@strapi/database",
|
|
3
|
-
"version": "4.6.0
|
|
3
|
+
"version": "4.6.0",
|
|
4
4
|
"description": "Strapi's database layer",
|
|
5
5
|
"homepage": "https://strapi.io",
|
|
6
6
|
"bugs": {
|
|
@@ -31,10 +31,10 @@
|
|
|
31
31
|
"test:unit": "jest --verbose"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"date-fns": "2.29.
|
|
34
|
+
"date-fns": "2.29.3",
|
|
35
35
|
"debug": "4.3.4",
|
|
36
36
|
"fs-extra": "10.0.0",
|
|
37
|
-
"knex": "
|
|
37
|
+
"knex": "2.4.0",
|
|
38
38
|
"lodash": "4.17.21",
|
|
39
39
|
"semver": "7.3.8",
|
|
40
40
|
"umzug": "3.1.1"
|
|
@@ -43,5 +43,5 @@
|
|
|
43
43
|
"node": ">=14.19.1 <=18.x.x",
|
|
44
44
|
"npm": ">=6.0.0"
|
|
45
45
|
},
|
|
46
|
-
"gitHead": "
|
|
46
|
+
"gitHead": "a9e55435c489f3379d88565bf3f729deb29bfb45"
|
|
47
47
|
}
|