@based/db 0.0.39 → 0.0.41

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.
Files changed (31) hide show
  1. package/dist/lib/darwin_aarch64/include/selva/db.h +3 -2
  2. package/dist/lib/darwin_aarch64/libdeflate.dylib +0 -0
  3. package/dist/lib/darwin_aarch64/libjemalloc_selva.2.dylib +0 -0
  4. package/dist/lib/darwin_aarch64/libnode-v20.node +0 -0
  5. package/dist/lib/darwin_aarch64/libnode-v21.node +0 -0
  6. package/dist/lib/darwin_aarch64/libnode-v22.node +0 -0
  7. package/dist/lib/darwin_aarch64/libnode-v23.node +0 -0
  8. package/dist/lib/darwin_aarch64/libselva.dylib +0 -0
  9. package/dist/lib/linux_aarch64/include/selva/db.h +3 -2
  10. package/dist/lib/linux_aarch64/libnode-v20.node +0 -0
  11. package/dist/lib/linux_aarch64/libnode-v21.node +0 -0
  12. package/dist/lib/linux_aarch64/libnode-v22.node +0 -0
  13. package/dist/lib/linux_aarch64/libnode-v23.node +0 -0
  14. package/dist/lib/linux_aarch64/libselva.so +0 -0
  15. package/dist/lib/linux_x86_64/include/selva/db.h +3 -2
  16. package/dist/lib/linux_x86_64/libnode-v20.node +0 -0
  17. package/dist/lib/linux_x86_64/libnode-v21.node +0 -0
  18. package/dist/lib/linux_x86_64/libnode-v22.node +0 -0
  19. package/dist/lib/linux_x86_64/libnode-v23.node +0 -0
  20. package/dist/lib/linux_x86_64/libselva.so +0 -0
  21. package/dist/src/client/query/BasedDbQuery.d.ts +1 -0
  22. package/dist/src/client/query/BasedDbQuery.js +6 -1
  23. package/dist/src/client/query/aggregates/aggregation.js +18 -4
  24. package/dist/src/client/query/aggregates/types.d.ts +3 -2
  25. package/dist/src/client/query/display.js +33 -4
  26. package/dist/src/client/query/read/read.js +22 -10
  27. package/dist/src/client/query/validation.d.ts +3 -0
  28. package/dist/src/client/query/validation.js +11 -1
  29. package/dist/src/index.d.ts +1 -0
  30. package/dist/src/index.js +1 -0
  31. package/package.json +3 -3
@@ -5,6 +5,7 @@
5
5
  #pragma once
6
6
 
7
7
  #include <assert.h>
8
+ #include <stdint.h>
8
9
  #include <sys/types.h>
9
10
  #include "selva/_export.h"
10
11
  #include "selva/types.h"
@@ -32,7 +33,7 @@ void selva_db_destroy(struct SelvaDb *db) __attribute__((nonnull));
32
33
  * @param type must not exist before.
33
34
  */
34
35
  SELVA_EXPORT
35
- int selva_db_create_type(struct SelvaDb *db, node_type_t type, const char *schema_buf, size_t schema_len) __attribute__((nonnull));
36
+ int selva_db_create_type(struct SelvaDb *db, node_type_t type, const uint8_t *schema_buf, size_t schema_len) __attribute__((nonnull));
36
37
 
37
38
  /**
38
39
  * Save the common/shared data of the database.
@@ -406,7 +407,7 @@ SELVA_EXPORT
406
407
  const struct SelvaAlias *selva_get_next_alias(const struct SelvaAlias *alias);
407
408
 
408
409
  SELVA_EXPORT
409
- const char *selva_get_alias_name(const struct SelvaAlias *alias, size_t *len) __attribute__((nonnull(1), pure));
410
+ const char *selva_get_alias_name(const struct SelvaAlias *alias, size_t *len) __attribute__((nonnull, pure));
410
411
 
411
412
  SELVA_EXPORT
412
413
  struct SelvaAliases *selva_get_aliases(struct SelvaTypeEntry *type, field_t field);
@@ -5,6 +5,7 @@
5
5
  #pragma once
6
6
 
7
7
  #include <assert.h>
8
+ #include <stdint.h>
8
9
  #include <sys/types.h>
9
10
  #include "selva/_export.h"
10
11
  #include "selva/types.h"
@@ -32,7 +33,7 @@ void selva_db_destroy(struct SelvaDb *db) __attribute__((nonnull));
32
33
  * @param type must not exist before.
33
34
  */
34
35
  SELVA_EXPORT
35
- int selva_db_create_type(struct SelvaDb *db, node_type_t type, const char *schema_buf, size_t schema_len) __attribute__((nonnull));
36
+ int selva_db_create_type(struct SelvaDb *db, node_type_t type, const uint8_t *schema_buf, size_t schema_len) __attribute__((nonnull));
36
37
 
37
38
  /**
38
39
  * Save the common/shared data of the database.
@@ -406,7 +407,7 @@ SELVA_EXPORT
406
407
  const struct SelvaAlias *selva_get_next_alias(const struct SelvaAlias *alias);
407
408
 
408
409
  SELVA_EXPORT
409
- const char *selva_get_alias_name(const struct SelvaAlias *alias, size_t *len) __attribute__((nonnull(1), pure));
410
+ const char *selva_get_alias_name(const struct SelvaAlias *alias, size_t *len) __attribute__((nonnull, pure));
410
411
 
411
412
  SELVA_EXPORT
412
413
  struct SelvaAliases *selva_get_aliases(struct SelvaTypeEntry *type, field_t field);
Binary file
@@ -5,6 +5,7 @@
5
5
  #pragma once
6
6
 
7
7
  #include <assert.h>
8
+ #include <stdint.h>
8
9
  #include <sys/types.h>
9
10
  #include "selva/_export.h"
10
11
  #include "selva/types.h"
@@ -32,7 +33,7 @@ void selva_db_destroy(struct SelvaDb *db) __attribute__((nonnull));
32
33
  * @param type must not exist before.
33
34
  */
34
35
  SELVA_EXPORT
35
- int selva_db_create_type(struct SelvaDb *db, node_type_t type, const char *schema_buf, size_t schema_len) __attribute__((nonnull));
36
+ int selva_db_create_type(struct SelvaDb *db, node_type_t type, const uint8_t *schema_buf, size_t schema_len) __attribute__((nonnull));
36
37
 
37
38
  /**
38
39
  * Save the common/shared data of the database.
@@ -406,7 +407,7 @@ SELVA_EXPORT
406
407
  const struct SelvaAlias *selva_get_next_alias(const struct SelvaAlias *alias);
407
408
 
408
409
  SELVA_EXPORT
409
- const char *selva_get_alias_name(const struct SelvaAlias *alias, size_t *len) __attribute__((nonnull(1), pure));
410
+ const char *selva_get_alias_name(const struct SelvaAlias *alias, size_t *len) __attribute__((nonnull, pure));
410
411
 
411
412
  SELVA_EXPORT
412
413
  struct SelvaAliases *selva_get_aliases(struct SelvaTypeEntry *type, field_t field);
Binary file
@@ -18,6 +18,7 @@ export declare class QueryBranch<T> {
18
18
  search(query: string, ...fields: Search[]): T;
19
19
  search(query: ArrayBufferView, field: string, opts?: Omit<FilterOpts, 'lowerCase'>): T;
20
20
  groupBy(field: string): T;
21
+ count(field?: string): T;
21
22
  sum(...fields: (string | string[])[]): T;
22
23
  or(fn: FilterBranchFn): T;
23
24
  or(field: string, operator?: Operator | boolean, value?: any, opts?: FilterOpts): T;
@@ -100,7 +100,12 @@ export class QueryBranch {
100
100
  // @ts-ignore
101
101
  return this;
102
102
  }
103
- // x
103
+ count(field = '$count') {
104
+ const p = field.split('.');
105
+ addAggregate(2 /* AggregateType.COUNT */, this.def, p);
106
+ // @ts-ignore
107
+ return this;
108
+ }
104
109
  sum(...fields) {
105
110
  addAggregate(1 /* AggregateType.SUM */, this.def, fields);
106
111
  // @ts-ignore
@@ -1,4 +1,6 @@
1
1
  import { writeUint16 } from '@saulx/utils';
2
+ import { UINT32 } from '@based/schema/def';
3
+ import { aggregationFieldDoesNotExist } from '../validation.js';
2
4
  export const aggregateToBuffer = (aggregates) => {
3
5
  const aggBuffer = new Uint8Array(aggregates.size);
4
6
  let i = 0;
@@ -15,7 +17,7 @@ export const aggregateToBuffer = (aggregates) => {
15
17
  i += 2;
16
18
  }
17
19
  else {
18
- aggBuffer[i] = 1 /* GroupBy.NONE */;
20
+ aggBuffer[i] = 0 /* GroupBy.NONE */;
19
21
  i += 1;
20
22
  }
21
23
  writeUint16(aggBuffer, aggregates.totalResultsPos, i);
@@ -55,7 +57,7 @@ const ensureAggregate = (def) => {
55
57
  export const groupBy = (def, field) => {
56
58
  const fieldDef = def.schema.props[field];
57
59
  if (!fieldDef) {
58
- throw new Error(`Field for agg:groupBy does not exists "${field}" make better error later...`);
60
+ aggregationFieldDoesNotExist(def, field);
59
61
  }
60
62
  ensureAggregate(def);
61
63
  if (!def.aggregate.groupBy) {
@@ -71,9 +73,21 @@ export const addAggregate = (type, def, fields) => {
71
73
  addAggregate(type, def, field);
72
74
  }
73
75
  else {
74
- const fieldDef = def.schema.props[field];
76
+ const fieldDef = type === 2 /* AggregateType.COUNT */
77
+ ? {
78
+ prop: 255,
79
+ path: [field],
80
+ __isPropDef: true,
81
+ len: 4,
82
+ start: 0,
83
+ typeIndex: UINT32,
84
+ separate: true,
85
+ validation: () => true,
86
+ default: 0,
87
+ }
88
+ : def.schema.props[field];
75
89
  if (!fieldDef) {
76
- throw new Error(`Field for agg does not exists ${field} make better error later...`);
90
+ aggregationFieldDoesNotExist(def, field);
77
91
  }
78
92
  if (!aggregates.get(fieldDef.prop)) {
79
93
  aggregates.set(fieldDef.prop, []);
@@ -1,7 +1,8 @@
1
1
  export declare const enum AggregateType {
2
- SUM = 1
2
+ SUM = 1,
3
+ COUNT = 2
3
4
  }
4
5
  export declare const enum GroupBy {
5
- NONE = 1,
6
+ NONE = 0,
6
7
  HAS_GROUP = 255
7
8
  }
@@ -122,7 +122,7 @@ const inspectObject = (object, q, path, level, isLast, isFirst, isObject, depth)
122
122
  let def;
123
123
  def = q.props[key];
124
124
  let v = object[k];
125
- const isEdge = k[0] === '$';
125
+ let isEdge = k[0] === '$';
126
126
  if (k === '$searchScore') {
127
127
  edges.push({ k, v, def: { typeIndex: NUMBER } });
128
128
  }
@@ -130,6 +130,10 @@ const inspectObject = (object, q, path, level, isLast, isFirst, isObject, depth)
130
130
  if (q.edges?.props?.[k]) {
131
131
  edges.push({ k, v, def: q.edges?.props?.[k] });
132
132
  }
133
+ else {
134
+ str += prefixBody + `${k}: `;
135
+ isEdge = false;
136
+ }
133
137
  }
134
138
  else {
135
139
  str += prefixBody + `${k}: `;
@@ -145,7 +149,20 @@ const inspectObject = (object, q, path, level, isLast, isFirst, isObject, depth)
145
149
  str += ',\n';
146
150
  }
147
151
  else if (!def) {
148
- str += inspectObject(v, q, key, level + 2, false, false, true, depth) + '';
152
+ if (typeof v === 'number') {
153
+ if (q.aggregate) {
154
+ str += picocolors.blue(v);
155
+ str += picocolors.italic(picocolors.dim(` ${key.indexOf('count') >= 0 ? ' count' : ' sum'}`));
156
+ str += ',\n';
157
+ }
158
+ else {
159
+ str += picocolors.blue(v) + '\n';
160
+ }
161
+ }
162
+ else {
163
+ str +=
164
+ inspectObject(v, q, key, level + 2, false, false, true, depth) + '';
165
+ }
149
166
  }
150
167
  else if ('__isPropDef' in def) {
151
168
  if (def.typeIndex === REFERENCES) {
@@ -194,7 +211,15 @@ const inspectObject = (object, q, path, level, isLast, isFirst, isObject, depth)
194
211
  }
195
212
  else {
196
213
  if (typeof v === 'number') {
197
- str += picocolors.blue(v);
214
+ if (q.aggregate) {
215
+ str += picocolors.blue(v);
216
+ str += picocolors.italic(picocolors.dim(` ${key.indexOf('count') >= 0 ? ' count' : ' sum'}`));
217
+ }
218
+ // str += ',\n'
219
+ // str += picocolors.blue(v)
220
+ }
221
+ else if (typeof v === 'object' && v) {
222
+ inspectObject(v, q, key, level + 2, false, false, true, depth) + '';
198
223
  }
199
224
  else {
200
225
  str += v;
@@ -243,7 +268,7 @@ export const inspectData = (q, def, level, top, depth, hasId = false) => {
243
268
  const prefix = top ? ' ' : '';
244
269
  let str;
245
270
  let i = 0;
246
- if (hasId) {
271
+ if (hasId || def.aggregate) {
247
272
  str = prefix;
248
273
  level = level + 1;
249
274
  }
@@ -254,6 +279,10 @@ export const inspectData = (q, def, level, top, depth, hasId = false) => {
254
279
  else {
255
280
  str = prefix + '[';
256
281
  }
282
+ if (def.aggregate) {
283
+ str += inspectObject(q.toObject(), def, '', level + 1, i === max - 1, i === 0, false, depth);
284
+ return str;
285
+ }
257
286
  for (const x of q) {
258
287
  str += inspectObject(x, def, '', level + 1, i === max - 1, i === 0, false, depth);
259
288
  i++;
@@ -3,9 +3,7 @@ import { QueryDefType } from '../types.js';
3
3
  import { read, readUtf8 } from '../../string.js';
4
4
  import { DECODER, readDoubleLE, readFloatLE, readInt16, readInt32, readUint16, readUint32, setByPath, } from '@saulx/utils';
5
5
  import { inverseLangMap } from '@based/schema';
6
- import { READ_EDGE, READ_ID, READ_REFERENCE, READ_REFERENCES,
7
- // AggFlag,
8
- } from '../types.js';
6
+ import { READ_EDGE, READ_ID, READ_REFERENCE, READ_REFERENCES, } from '../types.js';
9
7
  const addField = (p, value, item, defaultOnly = false, lang = 0) => {
10
8
  let i = p.__isEdge === true ? 1 : 0;
11
9
  // TODO OPTMIZE
@@ -229,12 +227,12 @@ export const readAllFields = (q, result, offset, end, item, id) => {
229
227
  const t = edgeDef.typeIndex;
230
228
  if (t === JSON) {
231
229
  const size = readUint32(result, i);
232
- addField(edgeDef, global.JSON.parse(readUtf8(result, i + 6, size + i)), item);
230
+ addField(edgeDef, global.JSON.parse(read(result, i + 4, size, true)), item);
233
231
  i += size + 4;
234
232
  }
235
233
  else if (t === BINARY) {
236
234
  const size = readUint32(result, i);
237
- addField(edgeDef, result.subarray(i + 6, size + i), item);
235
+ addField(edgeDef, result.subarray(i + 6, size + i + 4), item);
238
236
  i += size + 4;
239
237
  }
240
238
  else if (t === STRING || t === ALIAS || t === ALIASES) {
@@ -304,13 +302,13 @@ export const readAllFields = (q, result, offset, end, item, id) => {
304
302
  else if (prop.typeIndex === JSON) {
305
303
  q.include.propsRead[index] = id;
306
304
  const size = readUint32(result, i);
307
- addField(prop, global.JSON.parse(readUtf8(result, i + 6, size - 6)), item);
305
+ addField(prop, global.JSON.parse(read(result, i + 4, size, true)), item);
308
306
  i += size + 4;
309
307
  }
310
308
  else if (prop.typeIndex === BINARY) {
311
309
  q.include.propsRead[index] = id;
312
310
  const size = readUint32(result, i);
313
- addField(prop, result.subarray(i + 6, i + size), item);
311
+ addField(prop, result.subarray(i + 6, i + size + 4), item);
314
312
  i += size + 4;
315
313
  }
316
314
  else if (prop.typeIndex === STRING) {
@@ -332,10 +330,10 @@ export const readAllFields = (q, result, offset, end, item, id) => {
332
330
  else {
333
331
  if (q.lang != 0) {
334
332
  q.include.propsRead[index] = id;
335
- addField(prop, read(result, i + 4, size, false), item);
333
+ addField(prop, read(result, i + 4, size, true), item);
336
334
  }
337
335
  else {
338
- addField(prop, read(result, i + 4, size, false), item, false, result[i + 4]);
336
+ addField(prop, read(result, i + 4, size, true), item, false, result[i + 4]);
339
337
  }
340
338
  }
341
339
  i += size + 4;
@@ -372,11 +370,25 @@ let cnt = 0;
372
370
  export const resultToObject = (q, result, end, offset = 0) => {
373
371
  if (q.aggregate) {
374
372
  const results = {};
373
+ // range for numbers
375
374
  if (q.aggregate.groupBy) {
376
375
  // key size = 2 for now... not perfect...
377
376
  let i = 0;
378
377
  while (i < result.byteLength - 4) {
379
- const key = DECODER.decode(result.subarray(i, i + 2));
378
+ // if group = 0
379
+ // add extra thing for the keys maybe?
380
+ let key = '';
381
+ if (result[i] == 0) {
382
+ if (q.aggregate.groupBy.default) {
383
+ key = q.aggregate.groupBy.default;
384
+ }
385
+ else {
386
+ key = `$undefined`;
387
+ }
388
+ }
389
+ else {
390
+ key = DECODER.decode(result.subarray(i, i + 2));
391
+ }
380
392
  i += 2;
381
393
  const resultKey = (results[key] = {});
382
394
  for (const aggregatesArray of q.aggregate.aggregates.values()) {
@@ -30,6 +30,7 @@ export declare const ERR_SEARCH_ENOENT = 21;
30
30
  export declare const ERR_SEARCH_TYPE = 22;
31
31
  export declare const ERR_SEARCH_INCORRECT_VALUE = 23;
32
32
  export declare const ERR_SORT_LANG = 24;
33
+ export declare const ERR_AGG_ENOENT = 25;
33
34
  declare const messages: {
34
35
  1: (p: any) => string;
35
36
  2: (p: any) => string;
@@ -55,6 +56,7 @@ declare const messages: {
55
56
  22: (p: any) => string;
56
57
  23: (p: any) => string;
57
58
  24: (p: any) => string;
59
+ 25: (p: any) => string;
58
60
  };
59
61
  export type ErrorCode = keyof typeof messages;
60
62
  export declare const searchIncorrecQueryValue: (def: QueryDef, payload: any) => void;
@@ -82,4 +84,5 @@ export declare const EMPTY_ALIAS_PROP_DEF: PropDef;
82
84
  export declare const ERROR_STRING: PropDef;
83
85
  export declare const ERROR_VECTOR: PropDef;
84
86
  export declare const EMPTY_SCHEMA_DEF: SchemaTypeDef;
87
+ export declare const aggregationFieldDoesNotExist: (def: QueryDef, field: string) => void;
85
88
  export {};
@@ -28,6 +28,7 @@ export const ERR_SEARCH_ENOENT = 21;
28
28
  export const ERR_SEARCH_TYPE = 22;
29
29
  export const ERR_SEARCH_INCORRECT_VALUE = 23;
30
30
  export const ERR_SORT_LANG = 24;
31
+ export const ERR_AGG_ENOENT = 25;
31
32
  const messages = {
32
33
  [ERR_TARGET_INVAL_TYPE]: (p) => `Type "${p}" does not exist`,
33
34
  [ERR_TARGET_INVAL_ALIAS]: (p) => {
@@ -59,6 +60,7 @@ const messages = {
59
60
  [ERR_SEARCH_TYPE]: (p) => `Search: incorrect type "${p.path.join('.')}"`,
60
61
  [ERR_SEARCH_INCORRECT_VALUE]: (p) => `Search: incorrect query on field "${safeStringify(p)}"`,
61
62
  [ERR_SORT_LANG]: (p) => `Sort: invalid lang`,
63
+ [ERR_AGG_ENOENT]: (p) => `Field \"${p}\" in the aggregate function is invalid or unreacheable.`,
62
64
  };
63
65
  export const searchIncorrecQueryValue = (def, payload) => {
64
66
  def.errors.push({ code: ERR_SEARCH_INCORRECT_VALUE, payload });
@@ -397,7 +399,8 @@ export const handleErrors = (def) => {
397
399
  }
398
400
  }
399
401
  const err = new Error(`Query\n`);
400
- err.stack = name;
402
+ err.message = name;
403
+ err.stack = '';
401
404
  throw err;
402
405
  }
403
406
  };
@@ -441,4 +444,11 @@ export const EMPTY_SCHEMA_DEF = {
441
444
  idUint8: new Uint8Array([0, 0]),
442
445
  mainEmptyAllZeroes: true,
443
446
  };
447
+ export const aggregationFieldDoesNotExist = (def, field) => {
448
+ def.errors.push({
449
+ code: ERR_AGG_ENOENT,
450
+ payload: field,
451
+ });
452
+ handleErrors(def);
453
+ };
444
454
  //# sourceMappingURL=validation.js.map
@@ -13,6 +13,7 @@ export * from './utils.js';
13
13
  export * from './client/query/query.js';
14
14
  export * from './client/query/BasedDbQuery.js';
15
15
  export * from './client/query/BasedIterable.js';
16
+ export * from './server/save.js';
16
17
  export declare class BasedDb {
17
18
  #private;
18
19
  client: DbClient;
package/dist/src/index.js CHANGED
@@ -16,6 +16,7 @@ export * from './utils.js';
16
16
  export * from './client/query/query.js';
17
17
  export * from './client/query/BasedDbQuery.js';
18
18
  export * from './client/query/BasedIterable.js';
19
+ export * from './server/save.js';
19
20
  export class BasedDb {
20
21
  client;
21
22
  server;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@based/db",
3
- "version": "0.0.39",
3
+ "version": "0.0.41",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "./dist/src/index.js",
@@ -23,7 +23,7 @@
23
23
  "test-fast-linux_aarch64": "podman run --rm -v \"$PWD/../..:/usr/src/based-db\" based-db-clibs-build-linux_aarch64 sh -c '\\. \"/usr/local/nvm/nvm.sh\"; cd /usr/src/based-db/packages/db; npm run test-fast'",
24
24
  "test-fast-linux_aarch64-gdb": "podman run --rm -v \"$PWD/../..:/usr/src/based-db\" based-db-clibs-build-linux_aarch64 sh -c '\\. \"/usr/local/nvm/nvm.sh\"; cd /usr/src/based-db/packages/db; LOCPATH=../locale/locale-x86_64-gnu/locale gdb -ex run --args node ./scripts/test.js'",
25
25
  "test-fast-linux_aarch64-valgrind": "podman run --rm -v \"$PWD/../..:/usr/src/based-db\" based-db-clibs-build-linux_aarch64 sh -c '\\. \"/usr/local/nvm/nvm.sh\"; cd /usr/src/based-db/packages/db; LOCPATH=../locale/locale-aarch64-gnu/locale valgrind --leak-check=full node ./scripts/test.js references:update2'",
26
- "test-zig": "npm run build-zig -- debug && tsc && npm run test-fast",
26
+ "test-zig": "npm run build-zig && tsc && npm run test-fast",
27
27
  "test-zig-debug": "npm run build-zig -- debug && tsc && LOCPATH=../locale/locale-x86_64-gnu/locale ./scripts/lldb-node ./scripts/test.js",
28
28
  "test-ts": "tsc && node ./scripts/test.js",
29
29
  "perf": "npm run build && node benchmarks/references.js && node benchmarks/transfermarkt/transfermarkt-based.js"
@@ -38,7 +38,7 @@
38
38
  "basedDbNative.cjs"
39
39
  ],
40
40
  "dependencies": {
41
- "@based/schema": "5.0.0-alpha.14",
41
+ "@based/schema": "5.0.0-alpha.15",
42
42
  "@saulx/hash": "^3.0.0",
43
43
  "@saulx/utils": "^6.6.0",
44
44
  "exit-hook": "^4.0.0",