prostgles-server 2.0.160 → 2.0.163

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/Prostgles.ts CHANGED
@@ -107,6 +107,7 @@ export type UpdateRequestDataBatch = {
107
107
  export type UpdateRequestData = UpdateRequestDataOne | UpdateRequestDataBatch;
108
108
 
109
109
  export type ValidateRow = (row: AnyObject) => AnyObject | Promise<AnyObject>;
110
+ export type ValidateUpdateRow = (args: { update: AnyObject, filter: AnyObject }) => AnyObject | Promise<AnyObject>;
110
111
 
111
112
  export type SelectRule = {
112
113
 
@@ -166,9 +167,21 @@ export type InsertRule = {
166
167
  export type UpdateRule = {
167
168
 
168
169
  /**
169
- * Fields allowed to be updated. Tip: Use false to exclude field
170
+ * Fields allowed to be updated. Tip: Use false/0 to exclude field
170
171
  */
171
172
  fields: FieldFilter;
173
+
174
+ /**
175
+ * Row level FGAC
176
+ * Used when the editable fields change based on the updated row
177
+ * If specified then the fields from the first matching filter table.count({ ...filter, ...updateFilter }) > 0 will be used
178
+ * If none matching then the "fields" will be used
179
+ * Specify in decreasing order of specificity otherwise a more general filter will match first
180
+ */
181
+ dynamicFields?: {
182
+ filter: AnyObject;
183
+ fields: FieldFilter;
184
+ }[];
172
185
 
173
186
  /**
174
187
  * Filter added to every query (e.g. user_id) to restrict access
@@ -194,8 +207,10 @@ export type UpdateRule = {
194
207
  /**
195
208
  * Validation logic to check/update data for each request
196
209
  */
197
- validate?: ValidateRow
198
- }
210
+ validate?: ValidateUpdateRow;
211
+
212
+ };
213
+
199
214
  export type DeleteRule = {
200
215
 
201
216
  /**
@@ -1065,7 +1080,7 @@ const RULE_TO_METHODS = [
1065
1080
  methods: RULE_METHODS.update,
1066
1081
  no_limits: <UpdateRule>{ fields: "*", filterFields: "*", returningFields: "*" },
1067
1082
  table_only: true,
1068
- allowed_params: <Array<keyof UpdateRule>>["fields", "filterFields", "forcedFilter", "forcedData", "returningFields", "validate"] ,
1083
+ allowed_params: <Array<keyof UpdateRule>>["fields", "filterFields", "forcedFilter", "forcedData", "returningFields", "validate", "dynamicFields"] ,
1069
1084
  hint: ` expecting "*" | true | { fields: string | string[] | {} }`
1070
1085
  },
1071
1086
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prostgles-server",
3
- "version": "2.0.160",
3
+ "version": "2.0.163",
4
4
  "description": "",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -29,7 +29,7 @@
29
29
  "bluebird": "^3.7.2",
30
30
  "file-type": "^16.5.3",
31
31
  "pg-promise": "^10.11.1",
32
- "prostgles-types": "^1.5.132",
32
+ "prostgles-types": "^1.5.134",
33
33
  "sharp": "^0.30.5"
34
34
  },
35
35
  "devDependencies": {
@@ -1 +1 @@
1
- 50285
1
+ 18727
@@ -10,7 +10,7 @@
10
10
  "license": "ISC",
11
11
  "dependencies": {
12
12
  "@types/node": "^14.14.16",
13
- "prostgles-client": "^1.5.146",
13
+ "prostgles-client": "^1.5.147",
14
14
  "prostgles-types": "^1.5.68",
15
15
  "socket.io-client": "^4.5.1"
16
16
  }
@@ -67,17 +67,17 @@
67
67
  "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
68
68
  },
69
69
  "node_modules/prostgles-client": {
70
- "version": "1.5.146",
71
- "resolved": "https://registry.npmjs.org/prostgles-client/-/prostgles-client-1.5.146.tgz",
72
- "integrity": "sha512-yslX4cBxLUelQfs+4051lv3CITl61YsuQLMNaw+B33ck0fTmN8ZhUVjDS0N4BCipM94i4kMbjLSz8BUCG54X4g==",
70
+ "version": "1.5.147",
71
+ "resolved": "https://registry.npmjs.org/prostgles-client/-/prostgles-client-1.5.147.tgz",
72
+ "integrity": "sha512-OHKHn9/Ynv/v3QSB53ZvhVvWp4pWsrLIy8ZDXloQprB8IUXSS2SdCWnbdkBoD9JH9Z4tUBoq88lTdS7G1KZY3w==",
73
73
  "dependencies": {
74
- "prostgles-types": "^1.5.132"
74
+ "prostgles-types": "^1.5.133"
75
75
  }
76
76
  },
77
77
  "node_modules/prostgles-types": {
78
- "version": "1.5.132",
79
- "resolved": "https://registry.npmjs.org/prostgles-types/-/prostgles-types-1.5.132.tgz",
80
- "integrity": "sha512-Ol+NTzRpcyQS4ED/Ptx9RAfnEXzHBNt/e70bo9LeQ5phLNUbAqjNQkksJLU5SPnu39+egRzRW8fNn5XSd9EySw=="
78
+ "version": "1.5.133",
79
+ "resolved": "https://registry.npmjs.org/prostgles-types/-/prostgles-types-1.5.133.tgz",
80
+ "integrity": "sha512-WVSuoiWAo1hgDl+9QSBPVhszwhxuX1WRi6CdE4epede1eD2Q5rQfO22bli0BBCQXyhE6l/A0Tt6UxhAIlDDSmw=="
81
81
  },
82
82
  "node_modules/socket.io-client": {
83
83
  "version": "4.5.1",
@@ -176,17 +176,17 @@
176
176
  "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
177
177
  },
178
178
  "prostgles-client": {
179
- "version": "1.5.146",
180
- "resolved": "https://registry.npmjs.org/prostgles-client/-/prostgles-client-1.5.146.tgz",
181
- "integrity": "sha512-yslX4cBxLUelQfs+4051lv3CITl61YsuQLMNaw+B33ck0fTmN8ZhUVjDS0N4BCipM94i4kMbjLSz8BUCG54X4g==",
179
+ "version": "1.5.147",
180
+ "resolved": "https://registry.npmjs.org/prostgles-client/-/prostgles-client-1.5.147.tgz",
181
+ "integrity": "sha512-OHKHn9/Ynv/v3QSB53ZvhVvWp4pWsrLIy8ZDXloQprB8IUXSS2SdCWnbdkBoD9JH9Z4tUBoq88lTdS7G1KZY3w==",
182
182
  "requires": {
183
- "prostgles-types": "^1.5.132"
183
+ "prostgles-types": "^1.5.133"
184
184
  }
185
185
  },
186
186
  "prostgles-types": {
187
- "version": "1.5.132",
188
- "resolved": "https://registry.npmjs.org/prostgles-types/-/prostgles-types-1.5.132.tgz",
189
- "integrity": "sha512-Ol+NTzRpcyQS4ED/Ptx9RAfnEXzHBNt/e70bo9LeQ5phLNUbAqjNQkksJLU5SPnu39+egRzRW8fNn5XSd9EySw=="
187
+ "version": "1.5.133",
188
+ "resolved": "https://registry.npmjs.org/prostgles-types/-/prostgles-types-1.5.133.tgz",
189
+ "integrity": "sha512-WVSuoiWAo1hgDl+9QSBPVhszwhxuX1WRi6CdE4epede1eD2Q5rQfO22bli0BBCQXyhE6l/A0Tt6UxhAIlDDSmw=="
190
190
  },
191
191
  "socket.io-client": {
192
192
  "version": "4.5.1",
@@ -13,7 +13,7 @@
13
13
  "license": "ISC",
14
14
  "dependencies": {
15
15
  "@types/node": "^14.14.16",
16
- "prostgles-client": "^1.5.146",
16
+ "prostgles-client": "^1.5.147",
17
17
  "prostgles-types": "^1.5.68",
18
18
  "socket.io-client": "^4.5.1"
19
19
  }
@@ -78,6 +78,7 @@ async function client_only(db, auth, log, methods) {
78
78
  await db.planes.delete();
79
79
  let inserts = new Array(100).fill(null).map((d, i) => ({ id: i, flight_number: `FN${i}`, x: Math.random(), y: i }));
80
80
  await db.planes.insert(inserts);
81
+ const CLOCK_DRIFT = 2000;
81
82
  if ((await db.planes.count()) !== 100)
82
83
  throw "Not 100 planes";
83
84
  /**
@@ -94,16 +95,19 @@ async function client_only(db, auth, log, methods) {
94
95
  const p10 = planes.filter(p => p.x == 10);
95
96
  log(Date.now() + ": sub stats: x10 -> " + p10.length + " x20 ->" + planes.filter(p => p.x == 20).length);
96
97
  if (p10.length === 100) {
97
- // db.planes.findOne({}, { select: { last_updated: "$max"}}).then(log);
98
- sP.unsubscribe();
99
- log(Date.now() + ": sub: db.planes.update({}, { x: 20, last_updated });");
100
- const dLastUpdated = Math.max(...p10.map(v => +v.last_updated));
101
- const last_updated = Date.now();
102
- if (dLastUpdated >= last_updated)
103
- throw "dLastUpdated >= last_updated should not happen";
104
- await db.planes.update({}, { x: 20, last_updated });
105
- log(Date.now() + ": sub: Updated to x20", await db.planes.count({ x: 20 }));
106
- // db.planes.findOne({}, { select: { last_updated: "$max"}}).then(log)
98
+ /** 2 second delay to account for client-server clock drift */
99
+ setTimeout(async () => {
100
+ // db.planes.findOne({}, { select: { last_updated: "$max"}}).then(log);
101
+ sP.unsubscribe();
102
+ log(Date.now() + ": sub: db.planes.update({}, { x: 20, last_updated });");
103
+ const dLastUpdated = Math.max(...p10.map(v => +v.last_updated));
104
+ const last_updated = Date.now();
105
+ if (dLastUpdated >= last_updated)
106
+ throw "dLastUpdated >= last_updated should not happen";
107
+ await db.planes.update({}, { x: 20, last_updated });
108
+ log(Date.now() + ": sub: Updated to x20", await db.planes.count({ x: 20 }));
109
+ // db.planes.findOne({}, { select: { last_updated: "$max"}}).then(log)
110
+ }, CLOCK_DRIFT);
107
111
  }
108
112
  });
109
113
  let updt = 0;
@@ -127,7 +131,7 @@ async function client_only(db, auth, log, methods) {
127
131
  if (x20 === 100) {
128
132
  // log(22)
129
133
  // console.timeEnd("test")
130
- log(Date.now() + ": sync end: Finished replication test. Inserting 100 rows then updating two times took: " + (Date.now() - start) + "ms");
134
+ log(Date.now() + ": sync end: Finished replication test. Inserting 100 rows then updating two times took: " + (Date.now() - start - CLOCK_DRIFT) + "ms");
131
135
  resolve(true);
132
136
  }
133
137
  });
@@ -166,6 +170,14 @@ async function client_only(db, auth, log, methods) {
166
170
  { id: 1, public: 'public data' },
167
171
  { id: 2, public: 'public data' }
168
172
  ]);
173
+ const cols = await db.insert_rules.getColumns();
174
+ assert_1.strict.equal(cols.filter(({ insert, update: u, select: s, delete: d }) => insert && !u && !s && !d).length, 3, "Validated getColumns failed");
175
+ /* Validated insert */
176
+ const expectB = await db.insert_rules.insert({ name: "a" }, { returning: "*" });
177
+ assert_1.strict.deepStrictEqual(expectB, { name: "b" }, "Validated insert failed");
178
+ /* forced UUID insert */
179
+ const row = await db.uuid_text.insert({}, { returning: "*" });
180
+ assert_1.strict.equal(row.id, 'c81089e1-c4c1-45d7-a73d-e2d613cb7c3e');
169
181
  });
170
182
  // await tryRun("Duplicate subscription", async () => {
171
183
  // return new Promise(async (resolve, reject) => {
@@ -189,14 +201,6 @@ async function client_only(db, auth, log, methods) {
189
201
  // });
190
202
  // })
191
203
  // })
192
- const cols = await db.insert_rules.getColumns();
193
- assert_1.strict.equal(cols.filter(({ insert, update: u, select: s, delete: d }) => insert && !u && !s && !d).length, 3, "Validated getColumns failed");
194
- /* Validated insert */
195
- const expectB = await db.insert_rules.insert({ name: "a" }, { returning: "*" });
196
- assert_1.strict.deepStrictEqual(expectB, { name: "b" }, "Validated insert failed");
197
- /* forced UUID insert */
198
- const row = await db.uuid_text.insert({}, { returning: "*" });
199
- assert_1.strict.equal(row.id, 'c81089e1-c4c1-45d7-a73d-e2d613cb7c3e');
200
204
  await testRealtime();
201
205
  // auth.login({ username: "john", password: "secret" });
202
206
  // await tout();
@@ -210,6 +214,27 @@ async function client_only(db, auth, log, methods) {
210
214
  { id: 1, public: 'public data' },
211
215
  { id: 2, public: 'public data' }
212
216
  ]);
217
+ const dynamicCols = await db.uuid_text.getColumns(undefined, {
218
+ rule: "update",
219
+ filter: {
220
+ id: 'c81089e1-c4c1-45d7-a73d-e2d613cb7c3e'
221
+ },
222
+ data: {
223
+ id: "dwadwa"
224
+ }
225
+ });
226
+ assert_1.strict.equal(dynamicCols.length, 1);
227
+ assert_1.strict.equal(dynamicCols[0].name, "id");
228
+ const defaultCols = await db.uuid_text.getColumns(undefined, {
229
+ rule: "update",
230
+ filter: {
231
+ id: 'not matching'
232
+ },
233
+ data: {
234
+ id: "dwadwa"
235
+ }
236
+ });
237
+ throw defaultCols.map(c => c.name);
213
238
  }, log);
214
239
  }
215
240
  }
@@ -93,6 +93,8 @@ export default async function client_only(db: DBHandlerClient, auth: Auth, log:
93
93
  let inserts = new Array(100).fill(null).map((d, i) => ({ id: i, flight_number: `FN${i}`, x: Math.random(), y: i }));
94
94
  await db.planes.insert(inserts);
95
95
 
96
+ const CLOCK_DRIFT = 2000;
97
+
96
98
  if((await db.planes.count()) !== 100) throw "Not 100 planes";
97
99
 
98
100
  /**
@@ -112,17 +114,22 @@ export default async function client_only(db: DBHandlerClient, auth: Auth, log:
112
114
  log(Date.now() + ": sub stats: x10 -> " + p10.length + " x20 ->" + planes.filter(p => p.x == 20).length);
113
115
 
114
116
  if(p10.length === 100){
115
- // db.planes.findOne({}, { select: { last_updated: "$max"}}).then(log);
116
-
117
- sP.unsubscribe();
118
- log(Date.now() + ": sub: db.planes.update({}, { x: 20, last_updated });");
119
- const dLastUpdated = Math.max(...p10.map(v => +v.last_updated))
120
- const last_updated = Date.now();
121
- if(dLastUpdated >= last_updated) throw "dLastUpdated >= last_updated should not happen"
122
- await db.planes.update({}, { x: 20, last_updated });
123
- log(Date.now() + ": sub: Updated to x20" , await db.planes.count({ x: 20 }))
124
-
125
- // db.planes.findOne({}, { select: { last_updated: "$max"}}).then(log)
117
+
118
+ /** 2 second delay to account for client-server clock drift */
119
+ setTimeout(async () => {
120
+
121
+ // db.planes.findOne({}, { select: { last_updated: "$max"}}).then(log);
122
+
123
+ sP.unsubscribe();
124
+ log(Date.now() + ": sub: db.planes.update({}, { x: 20, last_updated });");
125
+ const dLastUpdated = Math.max(...p10.map(v => +v.last_updated))
126
+ const last_updated = Date.now();
127
+ if(dLastUpdated >= last_updated) throw "dLastUpdated >= last_updated should not happen"
128
+ await db.planes.update({}, { x: 20, last_updated });
129
+ log(Date.now() + ": sub: Updated to x20" , await db.planes.count({ x: 20 }))
130
+
131
+ // db.planes.findOne({}, { select: { last_updated: "$max"}}).then(log)
132
+ }, CLOCK_DRIFT)
126
133
  }
127
134
  });
128
135
 
@@ -148,7 +155,7 @@ export default async function client_only(db: DBHandlerClient, auth: Auth, log:
148
155
  if(x20 === 100){
149
156
  // log(22)
150
157
  // console.timeEnd("test")
151
- log(Date.now() + ": sync end: Finished replication test. Inserting 100 rows then updating two times took: " + (Date.now() - start) + "ms")
158
+ log(Date.now() + ": sync end: Finished replication test. Inserting 100 rows then updating two times took: " + (Date.now() - start - CLOCK_DRIFT) + "ms")
152
159
  resolve(true)
153
160
  }
154
161
  });
@@ -194,6 +201,18 @@ export default async function client_only(db: DBHandlerClient, auth: Auth, log:
194
201
  { id: 1, public: 'public data' },
195
202
  { id: 2, public: 'public data' }
196
203
  ]);
204
+
205
+
206
+ const cols = await db.insert_rules.getColumns();
207
+ assert.equal(cols.filter(({ insert, update: u, select: s, delete: d }) => insert && !u && !s && !d).length, 3, "Validated getColumns failed")
208
+
209
+ /* Validated insert */
210
+ const expectB = await db.insert_rules.insert({ name: "a" }, { returning: "*" });
211
+ assert.deepStrictEqual(expectB, { name: "b" }, "Validated insert failed");
212
+
213
+ /* forced UUID insert */
214
+ const row: any = await db.uuid_text.insert({}, {returning: "*"});
215
+ assert.equal(row.id, 'c81089e1-c4c1-45d7-a73d-e2d613cb7c3e');
197
216
  });
198
217
 
199
218
  // await tryRun("Duplicate subscription", async () => {
@@ -221,16 +240,7 @@ export default async function client_only(db: DBHandlerClient, auth: Auth, log:
221
240
  // })
222
241
  // })
223
242
 
224
- const cols = await db.insert_rules.getColumns();
225
- assert.equal(cols.filter(({ insert, update: u, select: s, delete: d }) => insert && !u && !s && !d).length, 3, "Validated getColumns failed")
226
-
227
- /* Validated insert */
228
- const expectB = await db.insert_rules.insert({ name: "a" }, { returning: "*" });
229
- assert.deepStrictEqual(expectB, { name: "b" }, "Validated insert failed");
230
243
 
231
- /* forced UUID insert */
232
- const row: any = await db.uuid_text.insert({}, {returning: "*"});
233
- assert.equal(row.id, 'c81089e1-c4c1-45d7-a73d-e2d613cb7c3e')
234
244
 
235
245
  await testRealtime();
236
246
 
@@ -247,6 +257,29 @@ export default async function client_only(db: DBHandlerClient, auth: Auth, log:
247
257
  { id: 1, public: 'public data' },
248
258
  { id: 2, public: 'public data' }
249
259
  ]);
260
+
261
+
262
+ const dynamicCols = await db.uuid_text.getColumns(undefined, {
263
+ rule: "update",
264
+ filter: {
265
+ id: 'c81089e1-c4c1-45d7-a73d-e2d613cb7c3e'
266
+ },
267
+ data: {
268
+ id: "dwadwa"
269
+ }
270
+ });
271
+ assert.equal(dynamicCols.length, 1);
272
+ assert.equal(dynamicCols[0].name, "id");
273
+ const defaultCols = await db.uuid_text.getColumns(undefined, {
274
+ rule: "update",
275
+ filter: {
276
+ id: 'not matching'
277
+ },
278
+ data: {
279
+ id: "dwadwa"
280
+ }
281
+ });
282
+ throw defaultCols.map(c => c.name);
250
283
  }, log);
251
284
  }
252
285
 
@@ -87,7 +87,7 @@ async function isomorphic(db) {
87
87
  await tryRun("getColumns definition", async () => {
88
88
  const res = await db.tr2.getColumns("fr");
89
89
  // console.log(JSON.stringify(res, null, 2))
90
- assert_1.strict.deepStrictEqual(res, [
90
+ const expected = [
91
91
  {
92
92
  "label": "Id",
93
93
  "name": "id",
@@ -180,7 +180,10 @@ async function isomorphic(db) {
180
180
  "update": true,
181
181
  "delete": true
182
182
  }
183
- ]);
183
+ ];
184
+ assert_1.strict.deepStrictEqual(res, expected);
185
+ const resDynamic = await db.tr2.getColumns("fr", { rule: "update", filter: {}, data: { t2: "a" } });
186
+ assert_1.strict.deepStrictEqual(resDynamic, expected);
184
187
  });
185
188
  await tryRun("$unnest_words", async () => {
186
189
  const res = await db.various.find({}, { returnType: "values", select: { name: "$unnest_words" } });
@@ -73,9 +73,7 @@ export default async function isomorphic(db: Partial<DbHandler> | Partial<DBHand
73
73
  await tryRun("getColumns definition", async () => {
74
74
  const res = await db.tr2.getColumns("fr");
75
75
  // console.log(JSON.stringify(res, null, 2))
76
- assert.deepStrictEqual(
77
- res,
78
- [
76
+ const expected = [
79
77
  {
80
78
  "label": "Id",
81
79
  "name": "id",
@@ -168,7 +166,17 @@ export default async function isomorphic(db: Partial<DbHandler> | Partial<DBHand
168
166
  "update": true,
169
167
  "delete": true
170
168
  }
171
- ]
169
+ ];
170
+
171
+ assert.deepStrictEqual(
172
+ res,
173
+ expected
174
+ );
175
+
176
+ const resDynamic = await db.tr2.getColumns("fr", { rule: "update", filter: {}, data: { t2: "a" } });
177
+ assert.deepStrictEqual(
178
+ resDynamic,
179
+ expected
172
180
  );
173
181
  });
174
182
 
@@ -268,6 +268,15 @@ const dbConnection = {
268
268
  forcedData: {
269
269
  id: 'c81089e1-c4c1-45d7-a73d-e2d613cb7c3e'
270
270
  }
271
+ },
272
+ update: {
273
+ fields: [],
274
+ dynamicFields: [{
275
+ fields: { id: 1 },
276
+ filter: {
277
+ id: 'c81089e1-c4c1-45d7-a73d-e2d613cb7c3e'
278
+ }
279
+ }]
271
280
  }
272
281
  }
273
282
  };
@@ -290,6 +290,15 @@ const dbConnection = {
290
290
  forcedData: {
291
291
  id: 'c81089e1-c4c1-45d7-a73d-e2d613cb7c3e'
292
292
  }
293
+ },
294
+ update: {
295
+ fields: [],
296
+ dynamicFields: [{
297
+ fields: { id: 1 },
298
+ filter: {
299
+ id: 'c81089e1-c4c1-45d7-a73d-e2d613cb7c3e'
300
+ }
301
+ }]
293
302
  }
294
303
  }
295
304
  };
@@ -21,7 +21,7 @@
21
21
  },
22
22
  "../..": {
23
23
  "name": "prostgles-server",
24
- "version": "2.0.159",
24
+ "version": "2.0.162",
25
25
  "license": "MIT",
26
26
  "dependencies": {
27
27
  "@aws-sdk/client-s3": "^3.95.0",
@@ -29,7 +29,7 @@
29
29
  "bluebird": "^3.7.2",
30
30
  "file-type": "^16.5.3",
31
31
  "pg-promise": "^10.11.1",
32
- "prostgles-types": "^1.5.132",
32
+ "prostgles-types": "^1.5.134",
33
33
  "sharp": "^0.30.5"
34
34
  },
35
35
  "devDependencies": {
@@ -1371,7 +1371,7 @@
1371
1371
  "bluebird": "^3.7.2",
1372
1372
  "file-type": "^16.5.3",
1373
1373
  "pg-promise": "^10.11.1",
1374
- "prostgles-types": "^1.5.132",
1374
+ "prostgles-types": "^1.5.134",
1375
1375
  "sharp": "^0.30.5",
1376
1376
  "typescript": "^4.7.2"
1377
1377
  }