@incoqnito.io/bajadab 1.2.2 → 1.4.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/README.md +192 -174
- package/dist/base.d.ts +18 -13
- package/dist/base.js +2 -2
- package/dist/base.js.map +1 -1
- package/dist/basics.d.ts +30 -10
- package/dist/basics.js +466 -279
- package/dist/basics.js.map +1 -1
- package/dist/fileio.d.ts +7 -0
- package/dist/fileio.js +104 -22
- package/dist/fileio.js.map +1 -1
- package/package.json +5 -9
- package/src/main/base.ts +236 -231
- package/src/main/basics.ts +1135 -913
- package/src/main/fileio.ts +305 -207
package/src/main/fileio.ts
CHANGED
|
@@ -1,207 +1,305 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
const
|
|
21
|
-
const
|
|
22
|
-
const
|
|
23
|
-
const
|
|
24
|
-
const
|
|
25
|
-
const
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
await
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
1
|
+
import { isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
2
|
+
import { chmod, cp, mkdir, readdir, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
3
|
+
import { setTimeout } from "node:timers/promises";
|
|
4
|
+
import { uuidv7 } from "uuidv7";
|
|
5
|
+
|
|
6
|
+
import { IBCollectionConfig, IBDatabase, IBDatabaseConfig, IBFunctionRegistry, IBSnapshotOptions, } from "./base.js";
|
|
7
|
+
import { BCollection, BDatabase, DEFAULT_DATABASE_CONFIG, } from "./basics.js";
|
|
8
|
+
|
|
9
|
+
interface IStoredDatabase {
|
|
10
|
+
config: IBDatabaseConfig;
|
|
11
|
+
nameIDMap: { [k: string]: string; };
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface IStoredCollection {
|
|
15
|
+
collectionID: string;
|
|
16
|
+
collectionName: string;
|
|
17
|
+
collectionConfig: IBCollectionConfig;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const DB_META_FILE_NAME = "dbmeta.json";
|
|
21
|
+
const DB_COLLECTION_FOLDER_SUFFIX = "_data";
|
|
22
|
+
const DB_COLLECTION_META_FILE_SUFFIX = ".json";
|
|
23
|
+
const DB_COLLECTION_DATA_FILE = "data.json";
|
|
24
|
+
const RESTRICTED_FILE_MODE = 0o600;
|
|
25
|
+
const RESTRICTED_DIR_MODE = 0o700;
|
|
26
|
+
const SNAPSHOT_VERSION_PATTERN = /^\d{8}T\d{9}Z(_[A-Za-z0-9_-]+)?$/;
|
|
27
|
+
const SNAPSHOT_LABEL_PATTERN = /^[A-Za-z0-9_-]+$/;
|
|
28
|
+
const RENAME_RETRY_DELAYS_MS = [10, 25, 50, 100];
|
|
29
|
+
const SNAPSHOT_NAME_ATTEMPTS = 100;
|
|
30
|
+
|
|
31
|
+
enum FsErrorCode {
|
|
32
|
+
ENOENT = "ENOENT",
|
|
33
|
+
EPERM = "EPERM",
|
|
34
|
+
EBUSY = "EBUSY",
|
|
35
|
+
EACCES = "EACCES",
|
|
36
|
+
EEXIST = "EEXIST",
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const RETRYABLE_RENAME_ERRORS: ReadonlySet<string> =
|
|
40
|
+
new Set([FsErrorCode.EPERM, FsErrorCode.EBUSY, FsErrorCode.EACCES]);
|
|
41
|
+
|
|
42
|
+
export interface IPreparedWrite {
|
|
43
|
+
commit(): Promise<void>;
|
|
44
|
+
discard(): Promise<void>;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function isSameOrInside(path: string, dir: string): boolean {
|
|
48
|
+
const rel = relative(resolve(dir), resolve(path));
|
|
49
|
+
return !isAbsolute(rel) && rel !== ".." && !rel.startsWith(".." + sep);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
let lastSnapshotMs = 0;
|
|
53
|
+
|
|
54
|
+
// strictly increasing within the process, so consecutive snapshots never share a name
|
|
55
|
+
function nextSnapshotMs(): number {
|
|
56
|
+
lastSnapshotMs = Math.max(Date.now(), lastSnapshotMs + 1);
|
|
57
|
+
return lastSnapshotMs;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function snapshotVersionName(ms: number, label?: string): string {
|
|
61
|
+
const timestamp = new Date(ms).toISOString().replace(/[-:.]/g, "");
|
|
62
|
+
return label ? `${timestamp}_${label}` : timestamp;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// exclusive mkdir claims the name atomically, also against other processes using the same targetDir
|
|
66
|
+
async function createSnapshotVersionDir(targetDir: string, label?: string): Promise<string> {
|
|
67
|
+
for (let attempt = 1; ; attempt++) {
|
|
68
|
+
const versionName = snapshotVersionName(nextSnapshotMs(), label);
|
|
69
|
+
try {
|
|
70
|
+
await mkdir(join(targetDir, versionName), { mode: RESTRICTED_DIR_MODE });
|
|
71
|
+
return versionName;
|
|
72
|
+
}
|
|
73
|
+
catch (err: any) {
|
|
74
|
+
if (err.code !== FsErrorCode.EEXIST || attempt >= SNAPSHOT_NAME_ATTEMPTS) {
|
|
75
|
+
throw err;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// re-applies the restricted mode explicitly rather than relying on fs.cp() to have preserved it
|
|
82
|
+
async function restrictPermissions(path: string): Promise<void> {
|
|
83
|
+
const info = await stat(path);
|
|
84
|
+
if (info.isDirectory()) {
|
|
85
|
+
await chmod(path, RESTRICTED_DIR_MODE);
|
|
86
|
+
await Promise.all((await readdir(path)).map(entry => restrictPermissions(join(path, entry))));
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
await chmod(path, RESTRICTED_FILE_MODE);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function renameWithRetry(from: string, to: string): Promise<void> {
|
|
94
|
+
for (let attempt = 0; ; attempt++) {
|
|
95
|
+
try {
|
|
96
|
+
return await rename(from, to);
|
|
97
|
+
}
|
|
98
|
+
catch (err: any) {
|
|
99
|
+
if (attempt >= RENAME_RETRY_DELAYS_MS.length || !RETRYABLE_RENAME_ERRORS.has(err.code)) {
|
|
100
|
+
throw err;
|
|
101
|
+
}
|
|
102
|
+
await setTimeout(RENAME_RETRY_DELAYS_MS[attempt]);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function prepareAtomicWrite(targetPath: string, data: string): Promise<IPreparedWrite> {
|
|
108
|
+
const tempPath = `${targetPath}.${uuidv7()}.tmp`;
|
|
109
|
+
try {
|
|
110
|
+
await writeFile(tempPath, data, { encoding: "utf-8", mode: RESTRICTED_FILE_MODE });
|
|
111
|
+
}
|
|
112
|
+
catch (err) {
|
|
113
|
+
await rm(tempPath, { force: true });
|
|
114
|
+
throw err;
|
|
115
|
+
}
|
|
116
|
+
return {
|
|
117
|
+
commit: async () => {
|
|
118
|
+
try {
|
|
119
|
+
await renameWithRetry(tempPath, targetPath);
|
|
120
|
+
}
|
|
121
|
+
catch (err) {
|
|
122
|
+
await rm(tempPath, { force: true });
|
|
123
|
+
throw err;
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
discard: () => rm(tempPath, { force: true }),
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async function atomicWriteFile(targetPath: string, data: string): Promise<void> {
|
|
131
|
+
return (await prepareAtomicWrite(targetPath, data)).commit();
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export class IODatabase {
|
|
135
|
+
readonly basePath: string;
|
|
136
|
+
private readonly nameIDMap: Map<string, string>;
|
|
137
|
+
private databaseConfig: IBDatabaseConfig;
|
|
138
|
+
|
|
139
|
+
public constructor(basePath: string, databaseConfig: IBDatabaseConfig, nameIDMap?: Map<string, string>) {
|
|
140
|
+
this.basePath = basePath;
|
|
141
|
+
this.databaseConfig = databaseConfig;
|
|
142
|
+
this.nameIDMap = nameIDMap ?? new Map();
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
private async loadCollectionMeta(id: string, database: BDatabase): Promise<BCollection> {
|
|
146
|
+
const meta: IStoredCollection = JSON.parse(await readFile(join(this.basePath, id + DB_COLLECTION_META_FILE_SUFFIX), { encoding: "utf-8" }));
|
|
147
|
+
return new BCollection(database, id, meta.collectionName, meta.collectionConfig);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
public static async createOrLoad(basePath: string, functionRegistry?: IBFunctionRegistry): Promise<BDatabase> {
|
|
151
|
+
const metaPath = join(basePath, DB_META_FILE_NAME);
|
|
152
|
+
let data: string;
|
|
153
|
+
try {
|
|
154
|
+
data = await readFile(metaPath, { encoding: "utf-8" })
|
|
155
|
+
}
|
|
156
|
+
catch (err: any) {
|
|
157
|
+
if (err.code === FsErrorCode.ENOENT) {
|
|
158
|
+
const iodb = new IODatabase(basePath, DEFAULT_DATABASE_CONFIG);
|
|
159
|
+
return new BDatabase(iodb, DEFAULT_DATABASE_CONFIG, functionRegistry);
|
|
160
|
+
}
|
|
161
|
+
throw err;
|
|
162
|
+
}
|
|
163
|
+
const meta: IStoredDatabase = JSON.parse(data);
|
|
164
|
+
const nameIDMap = new Map(Object.entries(meta.nameIDMap));
|
|
165
|
+
const iodb = new IODatabase(basePath, meta.config, nameIDMap);
|
|
166
|
+
const database = new BDatabase(iodb, meta.config, functionRegistry);
|
|
167
|
+
const collections = await Promise.all(
|
|
168
|
+
nameIDMap.values()
|
|
169
|
+
.map(id => iodb.loadCollectionMeta(id, database))
|
|
170
|
+
.toArray());
|
|
171
|
+
collections.forEach(c => database.addCollection(c));
|
|
172
|
+
return database;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
public async flush(): Promise<void> {
|
|
176
|
+
const meta: IStoredDatabase = {
|
|
177
|
+
config: this.databaseConfig,
|
|
178
|
+
nameIDMap: Object.fromEntries(this.nameIDMap),
|
|
179
|
+
};
|
|
180
|
+
const data = JSON.stringify(meta);
|
|
181
|
+
await atomicWriteFile(join(this.basePath, DB_META_FILE_NAME), data);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
public async createCollection<T = any>(collection: BCollection<T>): Promise<BCollection<T>> {
|
|
185
|
+
const id = collection.collectionID;
|
|
186
|
+
const collectionPath = join(this.basePath, id + DB_COLLECTION_FOLDER_SUFFIX);
|
|
187
|
+
await mkdir(collectionPath, { recursive: true, mode: RESTRICTED_DIR_MODE });
|
|
188
|
+
const meta: IStoredCollection = {
|
|
189
|
+
collectionConfig: collection.collectionConfig,
|
|
190
|
+
collectionName: collection.collectionName,
|
|
191
|
+
collectionID: id,
|
|
192
|
+
};
|
|
193
|
+
const data = JSON.stringify(meta);
|
|
194
|
+
await atomicWriteFile(join(this.basePath, id + DB_COLLECTION_META_FILE_SUFFIX), data);
|
|
195
|
+
this.nameIDMap.set(collection.collectionName, id);
|
|
196
|
+
await this.flush();
|
|
197
|
+
return collection;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
public async deleteCollection(collectionName: string): Promise<void> {
|
|
201
|
+
const id = this.nameIDMap.get(collectionName);
|
|
202
|
+
if (!id) {
|
|
203
|
+
throw new Error(`no such collection ${collectionName}`);
|
|
204
|
+
}
|
|
205
|
+
const collectionPath = join(this.basePath, id + DB_COLLECTION_FOLDER_SUFFIX);
|
|
206
|
+
const metaPath = join(this.basePath, id + DB_COLLECTION_META_FILE_SUFFIX);
|
|
207
|
+
this.nameIDMap.delete(collectionName);
|
|
208
|
+
await this.flush();
|
|
209
|
+
await rm(collectionPath, { recursive: true, force: true });
|
|
210
|
+
await rm(metaPath, { force: true });
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
private async loadData(collectionID: string): Promise<any> {
|
|
214
|
+
try {
|
|
215
|
+
const data = await readFile(join(this.basePath, collectionID + DB_COLLECTION_FOLDER_SUFFIX, DB_COLLECTION_DATA_FILE), { encoding: "utf-8" });
|
|
216
|
+
return JSON.parse(data);
|
|
217
|
+
}
|
|
218
|
+
catch (err: any) {
|
|
219
|
+
if (err.code === FsErrorCode.ENOENT) {
|
|
220
|
+
return null;
|
|
221
|
+
}
|
|
222
|
+
throw err;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
public async loadArrayData<T>(collectionID: string): Promise<T[]> {
|
|
227
|
+
const data = await this.loadData(collectionID);
|
|
228
|
+
return data || [];
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
public async loadMapData<T>(collectionID: string): Promise<Map<string, T>> {
|
|
232
|
+
const data = await this.loadData(collectionID);
|
|
233
|
+
return data ? new Map(Object.entries(data)) : new Map();
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
private async prepareData(collectionID: string, data: any): Promise<IPreparedWrite> {
|
|
237
|
+
return prepareAtomicWrite(join(this.basePath, collectionID + DB_COLLECTION_FOLDER_SUFFIX, DB_COLLECTION_DATA_FILE), JSON.stringify(data));
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
private async writeData(collectionID: string, data: any): Promise<void> {
|
|
241
|
+
return (await this.prepareData(collectionID, data)).commit();
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
public async prepareArrayData<T = any>(collectionID: string, arrayData: T[]): Promise<IPreparedWrite> {
|
|
245
|
+
return this.prepareData(collectionID, arrayData);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
public async prepareMapData<T = any>(collectionID: string, mapData: Map<string, T>): Promise<IPreparedWrite> {
|
|
249
|
+
return this.prepareData(collectionID, Object.fromEntries(mapData));
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
public async writeArrayData<T = any>(collectionID: string, arrayData: T[]): Promise<void> {
|
|
253
|
+
return this.writeData(collectionID, arrayData);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
public async writeMapData<T = any>(collectionID: string, mapData: Map<string, T>): Promise<void> {
|
|
257
|
+
return this.writeData(collectionID, Object.fromEntries(mapData));
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
public async snapshot(targetDir: string, options?: IBSnapshotOptions): Promise<string> {
|
|
261
|
+
if (options?.keepLast !== undefined && options.keepLast < 0) {
|
|
262
|
+
throw new Error(`keepLast must not be negative, got ${options.keepLast}`);
|
|
263
|
+
}
|
|
264
|
+
if (options?.label !== undefined && !SNAPSHOT_LABEL_PATTERN.test(options.label)) {
|
|
265
|
+
throw new Error(`snapshot label "${options.label}" must only contain letters, digits, "-" or "_"`);
|
|
266
|
+
}
|
|
267
|
+
if (isSameOrInside(targetDir, this.basePath)) {
|
|
268
|
+
throw new Error(`snapshot target "${targetDir}" must not be inside the database directory "${this.basePath}"`);
|
|
269
|
+
}
|
|
270
|
+
await mkdir(targetDir, { recursive: true, mode: RESTRICTED_DIR_MODE });
|
|
271
|
+
const versionName = await createSnapshotVersionDir(targetDir, options?.label);
|
|
272
|
+
const versionPath = join(targetDir, versionName);
|
|
273
|
+
try {
|
|
274
|
+
await cp(this.basePath, versionPath, { recursive: true });
|
|
275
|
+
await restrictPermissions(versionPath);
|
|
276
|
+
}
|
|
277
|
+
catch (err) {
|
|
278
|
+
await rm(versionPath, { recursive: true, force: true });
|
|
279
|
+
throw err;
|
|
280
|
+
}
|
|
281
|
+
if (options?.keepLast !== undefined) {
|
|
282
|
+
await this.pruneSnapshots(targetDir, versionName, options.keepLast);
|
|
283
|
+
}
|
|
284
|
+
return versionPath;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// only ever deletes folders matching our own naming scheme, never anything else the caller keeps in targetDir
|
|
288
|
+
private async pruneSnapshots(targetDir: string, createdVersionName: string, keepLast: number): Promise<void> {
|
|
289
|
+
const entries = await readdir(targetDir, { withFileTypes: true });
|
|
290
|
+
const olderVersions = entries
|
|
291
|
+
.filter(e => e.isDirectory() && e.name !== createdVersionName && SNAPSHOT_VERSION_PATTERN.test(e.name))
|
|
292
|
+
.map(e => e.name)
|
|
293
|
+
.sort();
|
|
294
|
+
const toDelete = olderVersions.slice(0, Math.max(0, olderVersions.length - keepLast));
|
|
295
|
+
await Promise.all(toDelete.map(name => rm(join(targetDir, name), { recursive: true, force: true })));
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Opens the database at `basePath`, creating it if it doesn't exist yet.
|
|
301
|
+
* `functionRegistry` resolves any {@link IBCollectionConfig.getID}/`setID` keys used by its collections.
|
|
302
|
+
*/
|
|
303
|
+
export async function getDatabase(basePath: string, functionRegistry?: IBFunctionRegistry): Promise<IBDatabase> {
|
|
304
|
+
return IODatabase.createOrLoad(basePath, functionRegistry);
|
|
305
|
+
}
|