@sprucelabs/data-stores 28.3.232 → 28.3.234
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/build/esm/stores/AbstractStore.d.ts +2 -2
- package/build/esm/stores/AbstractStore.js +1 -0
- package/build/esm/tests/databaseAssertUtil.d.ts +2 -0
- package/build/esm/tests/databaseAssertUtil.js +47 -0
- package/build/esm/types/query.types.d.ts +15 -4
- package/build/stores/AbstractStore.d.ts +2 -2
- package/build/stores/AbstractStore.js +1 -0
- package/build/tests/databaseAssertUtil.d.ts +2 -0
- package/build/tests/databaseAssertUtil.js +43 -0
- package/build/types/query.types.d.ts +15 -4
- package/package.json +12 -12
@@ -2,7 +2,7 @@ import { Schema, SchemaFieldNames, SchemaPartialValues, SchemaPublicFieldNames,
|
|
2
2
|
import { FindBatchOptions } from '../cursors/BatchCursor';
|
3
3
|
import AbstractMutexer from '../mutexers/AbstractMutexer';
|
4
4
|
import { Database } from '../types/database.types';
|
5
|
-
import { QueryBuilder, QueryOptions } from '../types/query.types';
|
5
|
+
import { QueryBuilder, QueryOptions, ValuesWithPaths } from '../types/query.types';
|
6
6
|
import { PrepareOptions, PrepareResults, SaveOperations, DataStore, DataStorePlugin } from '../types/stores.types';
|
7
7
|
export default abstract class AbstractStore<FullSchema extends Schema, CreateSchema extends Schema = FullSchema, UpdateSchema extends Schema = CreateSchema, DatabaseSchema extends Schema = FullSchema, PrimaryFieldName extends SchemaFieldNames<DatabaseSchema> | 'id' = 'id', DatabaseRecord = SchemaValues<DatabaseSchema>, QueryRecord = SchemaPartialValues<FullSchema>, FullRecord = SchemaValues<FullSchema>, CreateRecord = SchemaValues<CreateSchema>, UpdateRecord = SchemaValues<UpdateSchema> & SaveOperations> extends AbstractMutexer implements DataStore {
|
8
8
|
abstract readonly name: string;
|
@@ -49,7 +49,7 @@ export default abstract class AbstractStore<FullSchema extends Schema, CreateSch
|
|
49
49
|
upsertOne<IncludePrivateFields extends boolean = true, CreateEntityInstances extends boolean = false, F extends SchemaFieldNames<FullSchema> = SchemaFieldNames<FullSchema>, PF extends SchemaPublicFieldNames<FullSchema> = SchemaPublicFieldNames<FullSchema>>(query: QueryBuilder<QueryRecord>, updates: UpdateRecord & CreateRecord & {
|
50
50
|
id?: string;
|
51
51
|
}, options?: PrepareOptions<IncludePrivateFields, FullSchema, F>): Promise<Response<FullSchema, CreateEntityInstances, IncludePrivateFields, PF, F>>;
|
52
|
-
updateOne<IncludePrivateFields extends boolean = false, CreateEntityInstances extends boolean = false, F extends SchemaFieldNames<FullSchema> = SchemaFieldNames<FullSchema>, PF extends SchemaPublicFieldNames<FullSchema> = SchemaPublicFieldNames<FullSchema>>(query: QueryBuilder<QueryRecord>, updates: UpdateRecord
|
52
|
+
updateOne<IncludePrivateFields extends boolean = false, CreateEntityInstances extends boolean = false, F extends SchemaFieldNames<FullSchema> = SchemaFieldNames<FullSchema>, PF extends SchemaPublicFieldNames<FullSchema> = SchemaPublicFieldNames<FullSchema>>(query: QueryBuilder<QueryRecord>, updates: ValuesWithPaths<UpdateRecord>, options?: PrepareOptions<IncludePrivateFields, FullSchema, F>): Promise<Response<FullSchema, CreateEntityInstances, IncludePrivateFields, PF, F>>;
|
53
53
|
update(query: QueryBuilder<QueryRecord>, updates: UpdateRecord): Promise<number>;
|
54
54
|
private findOneAndUpdate;
|
55
55
|
private handleWillUpdateOnePlugins;
|
@@ -351,6 +351,7 @@ export default class AbstractStore extends AbstractMutexer {
|
|
351
351
|
? databaseRecord
|
352
352
|
: normalizeSchemaValues(this.databaseSchema, databaseRecord, {
|
353
353
|
shouldCreateEntityInstances: false,
|
354
|
+
shouldRetainDotSyntaxKeys: true,
|
354
355
|
fields: Object.keys(cleanedUpdates),
|
355
356
|
});
|
356
357
|
for (const { name, value } of ops) {
|
@@ -85,6 +85,8 @@ declare const databaseAssertUtil: {
|
|
85
85
|
_assertThrowsExpectedNotFoundOnUpdateOne(db: Database, query: Record<string, any>): Promise<void>;
|
86
86
|
_assert$orReturnsExpectedTotalRecords(db: Database, $or: Record<string, any>[], expected: number): Promise<void>;
|
87
87
|
_assertCanCreateMultiFieldIndex(connect: TestConnect, fields: string[]): Promise<void>;
|
88
|
+
canUpdateNestedField(connect: TestConnect): Promise<void>;
|
89
|
+
canUpsertNestedField(connect: TestConnect): Promise<void>;
|
88
90
|
assertHasLowerCaseToCamelCaseMappingEnabled(store: DataStore): void;
|
89
91
|
};
|
90
92
|
export default databaseAssertUtil;
|
@@ -1958,6 +1958,53 @@ const databaseAssertUtil = {
|
|
1958
1958
|
yield this.shutdown(db);
|
1959
1959
|
});
|
1960
1960
|
},
|
1961
|
+
canUpdateNestedField(connect) {
|
1962
|
+
return __awaiter(this, void 0, void 0, function* () {
|
1963
|
+
const db = yield connectToDabatase(connect);
|
1964
|
+
const locationId = generateId();
|
1965
|
+
const values = {
|
1966
|
+
name: 'first',
|
1967
|
+
target: {
|
1968
|
+
organizationId: generateId(),
|
1969
|
+
locationId,
|
1970
|
+
},
|
1971
|
+
};
|
1972
|
+
yield db.createOne(this.collectionName, values);
|
1973
|
+
const newOrganizationId = generateId();
|
1974
|
+
const updated = yield db.updateOne(this.collectionName, {}, {
|
1975
|
+
'target.organizationId': newOrganizationId,
|
1976
|
+
});
|
1977
|
+
assert.isEqual(updated.target.organizationId, newOrganizationId, 'Could not update nested field target.organizationId using key "target.organizationId"');
|
1978
|
+
const match = yield db.findOne(this.collectionName, { id: updated.id });
|
1979
|
+
assert.isEqualDeep(match.target, {
|
1980
|
+
organizationId: newOrganizationId,
|
1981
|
+
locationId,
|
1982
|
+
}, `Updating nested field lost existing key 'target.locationId'`);
|
1983
|
+
yield this.shutdown(db);
|
1984
|
+
});
|
1985
|
+
},
|
1986
|
+
canUpsertNestedField(connect) {
|
1987
|
+
return __awaiter(this, void 0, void 0, function* () {
|
1988
|
+
const db = yield connectToDabatase(connect);
|
1989
|
+
const locationId = generateId();
|
1990
|
+
const values = {
|
1991
|
+
name: 'first',
|
1992
|
+
target: {
|
1993
|
+
organizationId: generateId(),
|
1994
|
+
locationId,
|
1995
|
+
},
|
1996
|
+
};
|
1997
|
+
const record = yield db.createOne(this.collectionName, values);
|
1998
|
+
const newOrganizationId = generateId();
|
1999
|
+
yield db.upsertOne(this.collectionName, { id: record.id }, {
|
2000
|
+
'target.organizationId': newOrganizationId,
|
2001
|
+
});
|
2002
|
+
const match = yield db.findOne(this.collectionName, { id: record.id });
|
2003
|
+
assert.isEqual(match.target.organizationId, newOrganizationId, 'Could not find updated record with key "target.organizationId" set');
|
2004
|
+
assert.isEqual(match.target.locationId, locationId, `Upserting lost existing key 'target.locationId'`);
|
2005
|
+
yield this.shutdown(db);
|
2006
|
+
});
|
2007
|
+
},
|
1961
2008
|
assertHasLowerCaseToCamelCaseMappingEnabled(store) {
|
1962
2009
|
assert.isTrue(
|
1963
2010
|
//@ts-ignore
|
@@ -5,6 +5,21 @@ export type QueryBuilder<Query, Keys extends Paths<Query> = Paths<Query>> = {
|
|
5
5
|
} & {
|
6
6
|
id?: string | QuerySelector<string>;
|
7
7
|
} & RootQuerySelector<Query>;
|
8
|
+
export type ValuesWithPaths<Values, Keys extends Paths<Values> = Paths<Values>> = {
|
9
|
+
[K in RequiredKeys<Values, Keys>]: TypeAtPath<Values, K>;
|
10
|
+
} & {
|
11
|
+
[K in OptionalKeys<Values, Keys>]?: TypeAtPath<Values, K>;
|
12
|
+
};
|
13
|
+
type IsOptional<T, K extends keyof T> = {} extends Pick<T, K> ? true : false;
|
14
|
+
type IsPathOptional<T, P extends string> = P extends `${infer K}.${infer Rest}` ? K extends keyof T ? IsOptional<T, K> extends true ? true : IsPathOptional<T[K], Rest> : true : P extends keyof T ? IsOptional<T, P> : true;
|
15
|
+
type RequiredKeys<Values, Keys extends Paths<Values>> = {
|
16
|
+
[K in Keys]: IsPathOptional<Values, K> extends true ? never : K;
|
17
|
+
}[Keys];
|
18
|
+
type OptionalKeys<Values, Keys extends Paths<Values>> = Exclude<Keys, RequiredKeys<Values, Keys>>;
|
19
|
+
type Paths<T, D extends number = 3> = [D] extends [never] ? never : T extends object ? {
|
20
|
+
[K in keyof T]-?: K extends string | number ? `${K}` | Join<K, Paths<T[K], Prev[D]>> : never;
|
21
|
+
}[keyof T] : '';
|
22
|
+
type TypeAtPath<T, P extends string> = P extends `${infer K}.${infer Rest}` ? K extends keyof T ? TypeAtPath<T[K], Rest> : any : P extends keyof T ? T[P] : any;
|
8
23
|
export interface QuerySortField {
|
9
24
|
field: string;
|
10
25
|
direction: 'asc' | 'desc';
|
@@ -44,8 +59,4 @@ type Prev = [
|
|
44
59
|
20,
|
45
60
|
...0[]
|
46
61
|
];
|
47
|
-
type Paths<T, D extends number = 3> = [D] extends [never] ? never : T extends object ? {
|
48
|
-
[K in keyof T]-?: K extends string | number ? `${K}` | Join<K, Paths<T[K], Prev[D]>> : never;
|
49
|
-
}[keyof T] : '';
|
50
|
-
type TypeAtPath<T, P extends string> = P extends `${infer K}.${infer Rest}` ? K extends keyof T ? TypeAtPath<T[K], Rest> : any : P extends keyof T ? T[P] : any;
|
51
62
|
export {};
|
@@ -2,7 +2,7 @@ import { Schema, SchemaFieldNames, SchemaPartialValues, SchemaPublicFieldNames,
|
|
2
2
|
import { FindBatchOptions } from '../cursors/BatchCursor';
|
3
3
|
import AbstractMutexer from '../mutexers/AbstractMutexer';
|
4
4
|
import { Database } from '../types/database.types';
|
5
|
-
import { QueryBuilder, QueryOptions } from '../types/query.types';
|
5
|
+
import { QueryBuilder, QueryOptions, ValuesWithPaths } from '../types/query.types';
|
6
6
|
import { PrepareOptions, PrepareResults, SaveOperations, DataStore, DataStorePlugin } from '../types/stores.types';
|
7
7
|
export default abstract class AbstractStore<FullSchema extends Schema, CreateSchema extends Schema = FullSchema, UpdateSchema extends Schema = CreateSchema, DatabaseSchema extends Schema = FullSchema, PrimaryFieldName extends SchemaFieldNames<DatabaseSchema> | 'id' = 'id', DatabaseRecord = SchemaValues<DatabaseSchema>, QueryRecord = SchemaPartialValues<FullSchema>, FullRecord = SchemaValues<FullSchema>, CreateRecord = SchemaValues<CreateSchema>, UpdateRecord = SchemaValues<UpdateSchema> & SaveOperations> extends AbstractMutexer implements DataStore {
|
8
8
|
abstract readonly name: string;
|
@@ -49,7 +49,7 @@ export default abstract class AbstractStore<FullSchema extends Schema, CreateSch
|
|
49
49
|
upsertOne<IncludePrivateFields extends boolean = true, CreateEntityInstances extends boolean = false, F extends SchemaFieldNames<FullSchema> = SchemaFieldNames<FullSchema>, PF extends SchemaPublicFieldNames<FullSchema> = SchemaPublicFieldNames<FullSchema>>(query: QueryBuilder<QueryRecord>, updates: UpdateRecord & CreateRecord & {
|
50
50
|
id?: string;
|
51
51
|
}, options?: PrepareOptions<IncludePrivateFields, FullSchema, F>): Promise<Response<FullSchema, CreateEntityInstances, IncludePrivateFields, PF, F>>;
|
52
|
-
updateOne<IncludePrivateFields extends boolean = false, CreateEntityInstances extends boolean = false, F extends SchemaFieldNames<FullSchema> = SchemaFieldNames<FullSchema>, PF extends SchemaPublicFieldNames<FullSchema> = SchemaPublicFieldNames<FullSchema>>(query: QueryBuilder<QueryRecord>, updates: UpdateRecord
|
52
|
+
updateOne<IncludePrivateFields extends boolean = false, CreateEntityInstances extends boolean = false, F extends SchemaFieldNames<FullSchema> = SchemaFieldNames<FullSchema>, PF extends SchemaPublicFieldNames<FullSchema> = SchemaPublicFieldNames<FullSchema>>(query: QueryBuilder<QueryRecord>, updates: ValuesWithPaths<UpdateRecord>, options?: PrepareOptions<IncludePrivateFields, FullSchema, F>): Promise<Response<FullSchema, CreateEntityInstances, IncludePrivateFields, PF, F>>;
|
53
53
|
update(query: QueryBuilder<QueryRecord>, updates: UpdateRecord): Promise<number>;
|
54
54
|
private findOneAndUpdate;
|
55
55
|
private handleWillUpdateOnePlugins;
|
@@ -347,6 +347,7 @@ class AbstractStore extends AbstractMutexer_1.default {
|
|
347
347
|
? databaseRecord
|
348
348
|
: (0, schema_1.normalizeSchemaValues)(this.databaseSchema, databaseRecord, {
|
349
349
|
shouldCreateEntityInstances: false,
|
350
|
+
shouldRetainDotSyntaxKeys: true,
|
350
351
|
fields: Object.keys(cleanedUpdates),
|
351
352
|
});
|
352
353
|
for (const { name, value } of ops) {
|
@@ -85,6 +85,8 @@ declare const databaseAssertUtil: {
|
|
85
85
|
_assertThrowsExpectedNotFoundOnUpdateOne(db: Database, query: Record<string, any>): Promise<void>;
|
86
86
|
_assert$orReturnsExpectedTotalRecords(db: Database, $or: Record<string, any>[], expected: number): Promise<void>;
|
87
87
|
_assertCanCreateMultiFieldIndex(connect: TestConnect, fields: string[]): Promise<void>;
|
88
|
+
canUpdateNestedField(connect: TestConnect): Promise<void>;
|
89
|
+
canUpsertNestedField(connect: TestConnect): Promise<void>;
|
88
90
|
assertHasLowerCaseToCamelCaseMappingEnabled(store: DataStore): void;
|
89
91
|
};
|
90
92
|
export default databaseAssertUtil;
|
@@ -1798,6 +1798,49 @@ const databaseAssertUtil = {
|
|
1798
1798
|
test_utils_1.assert.isEqualDeep(indexes[0].fields.map((i) => i.toLowerCase()), fields.map((f) => f.toLowerCase()));
|
1799
1799
|
await this.shutdown(db);
|
1800
1800
|
},
|
1801
|
+
async canUpdateNestedField(connect) {
|
1802
|
+
const db = await connectToDabatase(connect);
|
1803
|
+
const locationId = (0, generateId_1.default)();
|
1804
|
+
const values = {
|
1805
|
+
name: 'first',
|
1806
|
+
target: {
|
1807
|
+
organizationId: (0, generateId_1.default)(),
|
1808
|
+
locationId,
|
1809
|
+
},
|
1810
|
+
};
|
1811
|
+
await db.createOne(this.collectionName, values);
|
1812
|
+
const newOrganizationId = (0, generateId_1.default)();
|
1813
|
+
const updated = await db.updateOne(this.collectionName, {}, {
|
1814
|
+
'target.organizationId': newOrganizationId,
|
1815
|
+
});
|
1816
|
+
test_utils_1.assert.isEqual(updated.target.organizationId, newOrganizationId, 'Could not update nested field target.organizationId using key "target.organizationId"');
|
1817
|
+
const match = await db.findOne(this.collectionName, { id: updated.id });
|
1818
|
+
test_utils_1.assert.isEqualDeep(match.target, {
|
1819
|
+
organizationId: newOrganizationId,
|
1820
|
+
locationId,
|
1821
|
+
}, `Updating nested field lost existing key 'target.locationId'`);
|
1822
|
+
await this.shutdown(db);
|
1823
|
+
},
|
1824
|
+
async canUpsertNestedField(connect) {
|
1825
|
+
const db = await connectToDabatase(connect);
|
1826
|
+
const locationId = (0, generateId_1.default)();
|
1827
|
+
const values = {
|
1828
|
+
name: 'first',
|
1829
|
+
target: {
|
1830
|
+
organizationId: (0, generateId_1.default)(),
|
1831
|
+
locationId,
|
1832
|
+
},
|
1833
|
+
};
|
1834
|
+
const record = await db.createOne(this.collectionName, values);
|
1835
|
+
const newOrganizationId = (0, generateId_1.default)();
|
1836
|
+
await db.upsertOne(this.collectionName, { id: record.id }, {
|
1837
|
+
'target.organizationId': newOrganizationId,
|
1838
|
+
});
|
1839
|
+
const match = await db.findOne(this.collectionName, { id: record.id });
|
1840
|
+
test_utils_1.assert.isEqual(match.target.organizationId, newOrganizationId, 'Could not find updated record with key "target.organizationId" set');
|
1841
|
+
test_utils_1.assert.isEqual(match.target.locationId, locationId, `Upserting lost existing key 'target.locationId'`);
|
1842
|
+
await this.shutdown(db);
|
1843
|
+
},
|
1801
1844
|
assertHasLowerCaseToCamelCaseMappingEnabled(store) {
|
1802
1845
|
test_utils_1.assert.isTrue(
|
1803
1846
|
//@ts-ignore
|
@@ -5,6 +5,21 @@ export type QueryBuilder<Query, Keys extends Paths<Query> = Paths<Query>> = {
|
|
5
5
|
} & {
|
6
6
|
id?: string | QuerySelector<string>;
|
7
7
|
} & RootQuerySelector<Query>;
|
8
|
+
export type ValuesWithPaths<Values, Keys extends Paths<Values> = Paths<Values>> = {
|
9
|
+
[K in RequiredKeys<Values, Keys>]: TypeAtPath<Values, K>;
|
10
|
+
} & {
|
11
|
+
[K in OptionalKeys<Values, Keys>]?: TypeAtPath<Values, K>;
|
12
|
+
};
|
13
|
+
type IsOptional<T, K extends keyof T> = {} extends Pick<T, K> ? true : false;
|
14
|
+
type IsPathOptional<T, P extends string> = P extends `${infer K}.${infer Rest}` ? K extends keyof T ? IsOptional<T, K> extends true ? true : IsPathOptional<T[K], Rest> : true : P extends keyof T ? IsOptional<T, P> : true;
|
15
|
+
type RequiredKeys<Values, Keys extends Paths<Values>> = {
|
16
|
+
[K in Keys]: IsPathOptional<Values, K> extends true ? never : K;
|
17
|
+
}[Keys];
|
18
|
+
type OptionalKeys<Values, Keys extends Paths<Values>> = Exclude<Keys, RequiredKeys<Values, Keys>>;
|
19
|
+
type Paths<T, D extends number = 3> = [D] extends [never] ? never : T extends object ? {
|
20
|
+
[K in keyof T]-?: K extends string | number ? `${K}` | Join<K, Paths<T[K], Prev[D]>> : never;
|
21
|
+
}[keyof T] : '';
|
22
|
+
type TypeAtPath<T, P extends string> = P extends `${infer K}.${infer Rest}` ? K extends keyof T ? TypeAtPath<T[K], Rest> : any : P extends keyof T ? T[P] : any;
|
8
23
|
export interface QuerySortField {
|
9
24
|
field: string;
|
10
25
|
direction: 'asc' | 'desc';
|
@@ -44,8 +59,4 @@ type Prev = [
|
|
44
59
|
20,
|
45
60
|
...0[]
|
46
61
|
];
|
47
|
-
type Paths<T, D extends number = 3> = [D] extends [never] ? never : T extends object ? {
|
48
|
-
[K in keyof T]-?: K extends string | number ? `${K}` | Join<K, Paths<T[K], Prev[D]>> : never;
|
49
|
-
}[keyof T] : '';
|
50
|
-
type TypeAtPath<T, P extends string> = P extends `${infer K}.${infer Rest}` ? K extends keyof T ? TypeAtPath<T[K], Rest> : any : P extends keyof T ? T[P] : any;
|
51
62
|
export {};
|
package/package.json
CHANGED
@@ -3,7 +3,7 @@
|
|
3
3
|
"publishConfig": {
|
4
4
|
"access": "public"
|
5
5
|
},
|
6
|
-
"version": "28.3.
|
6
|
+
"version": "28.3.234",
|
7
7
|
"files": [
|
8
8
|
"build/**/*",
|
9
9
|
"!build/__tests__",
|
@@ -65,31 +65,31 @@
|
|
65
65
|
},
|
66
66
|
"dependencies": {
|
67
67
|
"@seald-io/nedb": "^4.0.4",
|
68
|
-
"@sprucelabs/error": "^6.0.
|
68
|
+
"@sprucelabs/error": "^6.0.548",
|
69
69
|
"@sprucelabs/globby": "^2.0.499",
|
70
|
-
"@sprucelabs/schema": "^30.0.
|
71
|
-
"@sprucelabs/spruce-skill-utils": "^31.0.
|
70
|
+
"@sprucelabs/schema": "^30.0.577",
|
71
|
+
"@sprucelabs/spruce-skill-utils": "^31.0.641",
|
72
72
|
"just-clone": "^6.2.0",
|
73
73
|
"lodash": "^4.17.21",
|
74
74
|
"mongodb": "^6.11.0"
|
75
75
|
},
|
76
76
|
"devDependencies": {
|
77
|
-
"@sprucelabs/esm-postbuild": "^6.0.
|
78
|
-
"@sprucelabs/jest-json-reporter": "^8.0.
|
79
|
-
"@sprucelabs/resolve-path-aliases": "^2.0.
|
77
|
+
"@sprucelabs/esm-postbuild": "^6.0.528",
|
78
|
+
"@sprucelabs/jest-json-reporter": "^8.0.550",
|
79
|
+
"@sprucelabs/resolve-path-aliases": "^2.0.519",
|
80
80
|
"@sprucelabs/semantic-release": "^5.0.2",
|
81
|
-
"@sprucelabs/test": "^9.0.
|
82
|
-
"@sprucelabs/test-utils": "^5.1.
|
81
|
+
"@sprucelabs/test": "^9.0.62",
|
82
|
+
"@sprucelabs/test-utils": "^5.1.515",
|
83
83
|
"@types/lodash": "^4.17.13",
|
84
|
-
"@types/node": "^22.10.
|
84
|
+
"@types/node": "^22.10.1",
|
85
85
|
"chokidar-cli": "^3.0.0",
|
86
86
|
"concurrently": "^9.1.0",
|
87
87
|
"dotenv": "^16.4.5",
|
88
|
-
"eslint": "^9.
|
88
|
+
"eslint": "^9.16.0",
|
89
89
|
"eslint-config-spruce": "^11.2.26",
|
90
90
|
"jest": "^29.7.0",
|
91
91
|
"jest-circus": "^29.7.0",
|
92
|
-
"prettier": "^3.4.
|
92
|
+
"prettier": "^3.4.1",
|
93
93
|
"ts-node": "^10.9.2",
|
94
94
|
"tsc-watch": "^6.2.1",
|
95
95
|
"tsconfig-paths": "^4.2.0",
|