tsondb 0.22.0 → 0.23.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.
@@ -1,61 +1,21 @@
1
- import { exists } from "@elyukai/utils/async/node";
2
- import { on } from "@elyukai/utils/function";
3
1
  import { Lazy } from "@elyukai/utils/lazy";
4
- import { nullableToArray } from "@elyukai/utils/nullable";
5
- import { compareNumber, reduceCompare } from "@elyukai/utils/ordering";
6
2
  import { isError } from "@elyukai/utils/result";
7
- import { assertExhaustive } from "@elyukai/utils/typeSafety";
8
3
  import Debug from "debug";
9
- import { mkdir, readFile, writeFile } from "node:fs/promises";
10
- import { findPackageJSON } from "node:module";
11
- import { join, sep } from "node:path";
12
- import { stderr } from "node:process";
13
- import { styleText } from "node:util";
4
+ import { writeFile } from "node:fs/promises";
14
5
  import semver from "semver";
15
6
  import { simpleGit } from "simple-git";
16
- import { NodeKind } from "../shared/schema/Node.js";
17
- import { countError, countErrors, getErrorMessageForDisplay, HTTPError, wrapErrorsIfAny, } from "../shared/utils/error.js";
18
- import { parallelizeErrors } from "../shared/utils/validation.js";
7
+ import { HTTPError } from "../shared/utils/error.js";
19
8
  import { Git } from "./git.js";
20
- import { getDisplayName, getDisplayNameWithId } from "./schema/detached.js";
21
- import { isEntityDecl, isEntityDeclWithParentReference } from "./schema/guards.js";
22
- import { serializeNode } from "./schema/treeOperations/serialization.js";
23
- import { createEntityNameValidator, createReferenceValidator, validateDeclReferentialIntegrity, validateDeclStructuralIntegrity, } from "./schema/treeOperations/validation.js";
9
+ import { ReadonlyTSONDB } from "./readonly.js";
10
+ import { validateDeclStructuralIntegrity, } from "./schema/treeOperations/validation.js";
11
+ import { createServer } from "./server/index.js";
12
+ import { createCompressedDatabaseFile, getTSONDBVersion, initData, prepareFolders, validate, writeMetaFile, } from "./shared.js";
24
13
  import { Transaction } from "./transaction.js";
25
- import { checkCustomConstraintsForAllEntities } from "./utils/customConstraints.js";
26
14
  import { DatabaseInMemory } from "./utils/databaseInMemory.js";
27
15
  import { applyStepsToDisk } from "./utils/databaseOnDisk.js";
28
- import { getAllInstanceOverviewsByEntityName, getDisplayNameFromEntityInstance, getInstanceOverview, getInstanceOverviewsByEntityName, } from "./utils/displayName.js";
29
- import { formatInstance, getFileNameForId, getSingletonFileName, readInstance, readSingletonInstance, writeInstance, writeSingletonInstance, } from "./utils/files.js";
16
+ import { writeInstance, writeSingletonInstance } from "./utils/files.js";
30
17
  import { attachGitStatusToDatabaseInMemory } from "./utils/git.js";
31
- import { getReferencesToInstances, isReferencedByOtherInstances, } from "./utils/references.js";
32
- import { checkUniqueConstraintsForAllEntities } from "./utils/unique.js";
33
- const debug = Debug("tsondb:class");
34
- const getMetaFilePath = (dataRootPath) => join(dataRootPath, ".meta.json");
35
- const getTSONDBVersion = async () => {
36
- const packageJsonPath = findPackageJSON(".", import.meta.url);
37
- if (!packageJsonPath) {
38
- throw new Error("Could not find package.json for TSONDB");
39
- }
40
- const packageJson = JSON.parse(await readFile(packageJsonPath, "utf-8"));
41
- return packageJson.version;
42
- };
43
- const readMetaFile = async (dataRootPath) => JSON.parse(await readFile(getMetaFilePath(dataRootPath), "utf-8"));
44
- const writeMetaFile = async (dataRootPath, meta) => writeFile(getMetaFilePath(dataRootPath), JSON.stringify(meta, null, 2), "utf-8");
45
- const prepareFolders = async (schemaVersion, dataRootPath, entities) => {
46
- await mkdir(dataRootPath, { recursive: true });
47
- await Promise.all(entities.map(entity => mkdir(join(dataRootPath, entity.name), { recursive: true })));
48
- if (!(await exists(getMetaFilePath(dataRootPath)))) {
49
- const newMetaFile = {
50
- version: await getTSONDBVersion(),
51
- schemaVersion,
52
- };
53
- await writeMetaFile(dataRootPath, newMetaFile);
54
- }
55
- };
56
- const generateOutputs = async (schema, outputs) => {
57
- await Promise.all(outputs.map(output => output.run(schema)));
58
- };
18
+ const debug = Debug("tsondb:main");
59
19
  const getGit = async (dataRootPath) => {
60
20
  const git = simpleGit({ baseDir: dataRootPath });
61
21
  if (await git.checkIsRepo()) {
@@ -72,62 +32,20 @@ const getGit = async (dataRootPath) => {
72
32
  return { git };
73
33
  }
74
34
  };
75
- const checkLocales = (schema, data, locales) => {
76
- const { localeEntity } = schema;
77
- if (localeEntity &&
78
- !locales.every(locale => data.hasInstanceOfEntityById(localeEntity.name, locale))) {
79
- throw new Error("All provided locales must exist in the database.");
80
- }
81
- };
82
- const initData = async (dataRootPath, schema, locales, git, gitStatus, skipReferenceCache) => {
83
- debug("loading database into memory ...");
84
- let data = await DatabaseInMemory.load(dataRootPath, schema.entities, schema.singletonEntities);
85
- debug("done");
86
- checkLocales(schema, data, locales);
87
- const serializedDeclarationsByName = Object.fromEntries(schema.resolvedDeclarations.map(decl => [decl.name, serializeNode(decl)]));
88
- let referencesToInstances = {};
89
- if (!skipReferenceCache) {
90
- debug("creating references cache ...");
91
- referencesToInstances = await getReferencesToInstances(data, serializedDeclarationsByName);
92
- debug("done");
93
- }
94
- else {
95
- debug("skipping references cache creation");
96
- }
97
- if (git) {
98
- const status = gitStatus ?? (await git.client.status());
99
- data = attachGitStatusToDatabaseInMemory(data, dataRootPath, git.root, status);
100
- debug("attached git status to instances");
101
- }
102
- return { data, referencesToInstances };
103
- };
104
35
  /**
105
36
  * The main class for managing a typed JSON database.
106
37
  */
107
- export class TSONDB {
108
- #data;
109
- #dataRootPath;
110
- #schema;
111
- #locales;
38
+ export class TSONDB extends ReadonlyTSONDB {
112
39
  #git;
113
- #referencesToInstances;
114
- #validationOptions;
115
- #formatterOptions;
116
40
  #gitWrapper;
117
41
  #locked = false;
118
42
  #hooks = {};
119
43
  constructor(options) {
120
- this.#dataRootPath = options.dataRootPath;
121
- this.#data = options.data;
122
- this.#schema = options.schema;
123
- this.#locales = options.locales ?? [];
44
+ super(options);
124
45
  this.#git = options.git;
125
- this.#referencesToInstances = options.referencesToInstances;
126
- this.#validationOptions = options.validationOptions;
127
- this.#formatterOptions = options.formatterOptions;
128
46
  this.#gitWrapper = Lazy.of(() => this.#git
129
- ? new Git(this, this.#git.client, this.#git.root, this.#dataRootPath, () => this.#data, (data) => {
130
- this.#data = data;
47
+ ? new Git(this, this.#git.client, this.#git.root, this.dataRootPath, () => this._data, (data) => {
48
+ this._data = data;
131
49
  })
132
50
  : undefined);
133
51
  }
@@ -154,7 +72,7 @@ export class TSONDB {
154
72
  throw new Error("Schema does not contain a locale entity, locales cannot be provided.");
155
73
  }
156
74
  const git = gitRoot ? { client: gitClient, root: gitRoot } : undefined;
157
- const { data, referencesToInstances } = await initData(options.dataRootPath, options.schema, options.locales ?? [], git, gitStatus, skipReferenceCache);
75
+ const { data, referencesToInstances, meta } = await initData(options.dataRootPath, options.schema, options.locales ?? [], git, gitStatus, skipReferenceCache, false);
158
76
  const db = new TSONDB({
159
77
  ...options,
160
78
  data,
@@ -166,10 +84,9 @@ export class TSONDB {
166
84
  ...options.validationOptions,
167
85
  },
168
86
  });
169
- const metaFile = await readMetaFile(options.dataRootPath);
170
- metaFile.version = await getTSONDBVersion();
171
- if (metaFile.schemaVersion !== options.schema.version && options.migrationStages) {
172
- debug("migrating database from schema version %s to %s", metaFile.schemaVersion, options.schema.version);
87
+ meta.version = await getTSONDBVersion();
88
+ if (meta.schemaVersion !== options.schema.version && options.migrationStages) {
89
+ debug("migrating database from schema version %s to %s", meta.schemaVersion, options.schema.version);
173
90
  for (const stage of options.migrationStages) {
174
91
  if (semver.valid(stage.toVersion) === null ||
175
92
  semver.validRange(stage.fromVersion) === null) {
@@ -177,13 +94,13 @@ export class TSONDB {
177
94
  }
178
95
  }
179
96
  for (const stage of options.migrationStages) {
180
- if (semver.satisfies(metaFile.schemaVersion, stage.fromVersion)) {
97
+ if (semver.satisfies(meta.schemaVersion, stage.fromVersion)) {
181
98
  debug("applying migration stage from %s to %s", stage.fromVersion, stage.toVersion);
182
99
  // oxlint-disable-next-line no-await-in-loop
183
100
  await stage.willMigrate?.(db);
184
- metaFile.schemaVersion = stage.toVersion;
101
+ meta.schemaVersion = stage.toVersion;
185
102
  // oxlint-disable-next-line no-await-in-loop
186
- await writeMetaFile(options.dataRootPath, metaFile);
103
+ await writeMetaFile(options.dataRootPath, meta);
187
104
  }
188
105
  else {
189
106
  debug("skipping migration stage from %s to %s", stage.fromVersion, stage.toVersion);
@@ -195,239 +112,39 @@ export class TSONDB {
195
112
  }
196
113
  return db;
197
114
  }
198
- /**
199
- * Generates outputs based on the provided schema and output configurations.
200
- */
201
- static async generateOutputs(options) {
202
- await generateOutputs(options.schema, options.outputs);
203
- }
204
- /**
205
- * Generates outputs based on the current schema and the provided output configurations.
206
- */
207
- async generateOutputs(outputs) {
208
- await generateOutputs(this.#schema, outputs);
209
- }
210
115
  #strucurallyValidateInstance(entity, instanceContent) {
211
116
  const validationContext = {
212
- validationOptions: this.#validationOptions,
117
+ validationOptions: this._validationOptions,
213
118
  useStyling: true,
214
119
  };
215
120
  return validateDeclStructuralIntegrity(validationContext, [], entity, [], instanceContent);
216
121
  }
217
- #validate(data, options = {}) {
218
- const { checkReferentialIntegrity, checkOnlyEntities } = this.#validationOptions;
219
- const { skipStructuralValidation = false, logToConsole = true, changedEntities: changedEntityNames, } = options;
220
- const entities = [...this.#schema.entities, ...this.#schema.singletonEntities];
221
- const getEntity = this.#schema.getEntity.bind(this.#schema);
222
- for (const onlyEntity of checkOnlyEntities) {
223
- if (!entities.find(entity => entity.name === onlyEntity)) {
224
- throw new Error(`Entity "${onlyEntity}" not found in schema`);
225
- }
226
- }
227
- const onlyEntities = checkOnlyEntities.length > 0
228
- ? entities.filter(entity => checkOnlyEntities.includes(entity.name))
229
- : entities;
230
- const changedEntities = changedEntityNames
231
- ? entities.filter(entity => changedEntityNames.includes(entity.name))
232
- : undefined;
233
- const validationContext = {
234
- validationOptions: this.#validationOptions,
235
- useStyling: true,
236
- };
237
- const errors = [];
238
- const summary = [];
239
- const debugAndAddToSummary = (message) => {
240
- debug(message);
241
- summary.push(message);
242
- };
243
- if (!skipStructuralValidation) {
244
- debug("Checking structural integrity ...");
245
- const structureErrors = onlyEntities
246
- .flatMap(entity => {
247
- switch (entity.kind) {
248
- case NodeKind.EntityDecl:
249
- return parallelizeErrors(data
250
- .getAllInstanceContainersOfEntity(entity.name)
251
- .map(instance => wrapErrorsIfAny(`in file ${styleText("white", `"${this.#dataRootPath}${sep}${styleText("bold", join(entity.name, getFileNameForId(instance.id)))}"`)}`, validateDeclStructuralIntegrity(validationContext, [], entity, [], instance.content))));
252
- case NodeKind.SingletonEntityDecl: {
253
- const instance = data.getSingletonInstanceContainerOfEntity(entity.name);
254
- if (instance) {
255
- return nullableToArray(wrapErrorsIfAny(`in file ${styleText("white", `"${this.#dataRootPath}${sep}${styleText("bold", getSingletonFileName(entity.name))}"`)}`, validateDeclStructuralIntegrity(validationContext, [], entity, [], instance.content)));
256
- }
257
- return [];
258
- }
259
- default:
260
- return assertExhaustive(entity);
261
- }
262
- })
263
- .toSorted((a, b) => a.message.localeCompare(b.message));
264
- if (structureErrors.length > 0) {
265
- const errorCount = countErrors(structureErrors);
266
- debugAndAddToSummary(`${errorCount.toString()} structural integrity violation${errorCount === 1 ? "" : "s"} found`);
267
- errors.push(...structureErrors);
268
- }
269
- else {
270
- debug("No structural integrity violations found");
271
- }
272
- }
273
- if (errors.length === 0) {
274
- if (!skipStructuralValidation) {
275
- if (checkReferentialIntegrity) {
276
- debug("Checking referential integrity ...");
277
- const entityNameValidator = createEntityNameValidator(this.#schema.isEntityName.bind(this.#schema), true);
278
- const referenceValidator = createReferenceValidator(this.#schema.isEntityName.bind(this.#schema), data, true);
279
- const referenceErrors = onlyEntities
280
- .flatMap(entity => {
281
- switch (entity.kind) {
282
- case NodeKind.EntityDecl:
283
- return parallelizeErrors(data
284
- .getAllInstanceContainersOfEntity(entity.name)
285
- .map(instance => wrapErrorsIfAny(`in file ${styleText("white", `"${this.#dataRootPath}${sep}${styleText("bold", join(entity.name, getFileNameForId(instance.id)))}"`)}`, validateDeclReferentialIntegrity(validationContext, entityNameValidator, referenceValidator, [], entity, [], instance.content))));
286
- case NodeKind.SingletonEntityDecl: {
287
- const instance = data.getSingletonInstanceContainerOfEntity(entity.name);
288
- if (instance) {
289
- return nullableToArray(wrapErrorsIfAny(`in file ${styleText("white", `"${this.#dataRootPath}${sep}${styleText("bold", getSingletonFileName(entity.name))}"`)}`, validateDeclReferentialIntegrity(validationContext, entityNameValidator, referenceValidator, [], entity, [], instance.content)));
290
- }
291
- return [];
292
- }
293
- default:
294
- return assertExhaustive(entity);
295
- }
296
- })
297
- .toSorted((a, b) => a.message.localeCompare(b.message));
298
- if (referenceErrors.length > 0) {
299
- const errorCount = countErrors(referenceErrors);
300
- debugAndAddToSummary(`${errorCount.toString()} referential integrity violation${errorCount === 1 ? "" : "s"} found`);
301
- errors.push(...referenceErrors);
302
- }
303
- else {
304
- debug("No referential integrity violations found");
305
- }
306
- }
307
- else {
308
- debug("Disabled referential integrity checks, skipping them");
309
- }
310
- }
311
- debug("Checking unique constraints ...");
312
- const instanceOverviewsByEntityName = getAllInstanceOverviewsByEntityName(getEntity, data, this.#locales);
313
- const uniqueConstraintResult = checkUniqueConstraintsForAllEntities(data, (changedEntities ?? onlyEntities).filter(e => isEntityDecl(e)), instanceOverviewsByEntityName);
314
- if (isError(uniqueConstraintResult)) {
315
- const errorCount = countError(uniqueConstraintResult.error);
316
- debugAndAddToSummary(`${errorCount.toString()} unique constraint violation${errorCount === 1 ? "" : "s"} found`);
317
- errors.push(uniqueConstraintResult.error);
318
- }
319
- else {
320
- debug("No unique constraint violations found");
321
- }
322
- debug("Checking custom constraints ...");
323
- const customConstraintResult = checkCustomConstraintsForAllEntities((...args) => getDisplayName(getEntity, this.#locales, data, ...args), (...args) => getDisplayNameWithId(getEntity, this.#locales, data, ...args), (...args) => getInstanceOverview(data.getInstanceContainerOfEntityById.bind(data), data.getAllChildInstanceContainersForParent.bind(data, getEntity), getEntity, this.#locales, args), getEntity, data, entities);
324
- if (isError(customConstraintResult)) {
325
- const errorCount = countError(customConstraintResult.error);
326
- debugAndAddToSummary(`${errorCount.toString()} custom constraint violation${errorCount === 1 ? "" : "s"} found`);
327
- errors.push(customConstraintResult.error);
328
- }
329
- else {
330
- debug("No custom constraint violations found");
331
- }
332
- }
333
- else {
334
- debugAndAddToSummary("Skipping further checks due to previous structural integrity errors");
335
- }
336
- if (logToConsole) {
337
- console.log(`${data.totalSize.toString()} instance${data.totalSize === 1 ? "" : "s"} checked`);
338
- if (errors.length === 0) {
339
- console.log(styleText("green", "All instances are valid"));
340
- }
341
- else {
342
- const errorCount = countErrors(errors);
343
- console.error(styleText("red", `${errors.map(err => getErrorMessageForDisplay(err)).join("\n\n")}\n`, {
344
- stream: stderr,
345
- }));
346
- console.error(styleText(["red", "bold"], "Summary", { stream: stderr }));
347
- console.error(styleText("red", [
348
- `${errorCount.toString()} validation error${errorCount === 1 ? "" : "s"} found`,
349
- ...summary,
350
- ].join("\n"), { stream: stderr }));
351
- }
352
- }
353
- return errors;
354
- }
355
- /**
356
- * Validates the data in the database.
357
- *
358
- * Returns `true` if the data is valid, `false` otherwise.
359
- */
360
- validate() {
361
- debug("Validating database ...");
362
- return this.#validate(this.#data).length === 0;
122
+ #validate(data, options) {
123
+ return validate({
124
+ data,
125
+ dataRootPath: this.dataRootPath,
126
+ locales: this.locales,
127
+ schema: this.schema,
128
+ validationOptions: this._validationOptions,
129
+ }, options);
363
130
  }
364
131
  /**
365
132
  * Formats the data on disk according to the current in-memory representation.
366
133
  */
367
134
  async format() {
368
135
  debug("Formatting database ...");
369
- await this.#data.forEachInstance(async (entityName, instance) => {
136
+ await this._data.forEachInstance(async (entityName, instance) => {
370
137
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
371
- const entity = this.#schema.getEntity(entityName);
372
- await writeInstance(this.#dataRootPath, entity, instance.id, instance.content, this.#formatterOptions);
138
+ const entity = this.schema.getEntity(entityName);
139
+ await writeInstance(this.dataRootPath, entity, instance.id, instance.content, this._formatterOptions);
373
140
  }, true);
374
- await this.#data.forEachSingletonInstance(async (entityName, instance) => {
141
+ await this._data.forEachSingletonInstance(async (entityName, instance) => {
375
142
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
376
- const entity = this.#schema.getSingletonEntity(entityName);
377
- await writeSingletonInstance(this.#dataRootPath, entity, instance.content, this.#formatterOptions);
143
+ const entity = this.schema.getSingletonEntity(entityName);
144
+ await writeSingletonInstance(this.dataRootPath, entity, instance.content, this._formatterOptions);
378
145
  }, true);
379
146
  debug("All data is formatted");
380
147
  }
381
- /**
382
- * Checks if the data on disk is formatted according to the current in-memory representation.
383
- */
384
- async checkFormat() {
385
- debug("Check database formatting ...");
386
- let counter = 0;
387
- let total = 0;
388
- await this.#data.forEachInstance(async (entityName, instance) => {
389
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
390
- const entity = this.#schema.getEntity(entityName);
391
- const instanceOnDisk = await readInstance(this.#dataRootPath, entity, instance.id);
392
- const currentFormatted = formatInstance(entity, instance.content, this.#formatterOptions);
393
- if (instanceOnDisk !== currentFormatted) {
394
- console.log(`Instance ${styleText("yellow", instance.id)} of entity ${styleText("cyan", entityName)} is not formatted correctly.`);
395
- counter++;
396
- }
397
- total++;
398
- }, true);
399
- await this.#data.forEachSingletonInstance(async (entityName, instance) => {
400
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
401
- const entity = this.#schema.getSingletonEntity(entityName);
402
- const instanceOnDisk = await readSingletonInstance(this.#dataRootPath, entity);
403
- const currentFormatted = formatInstance(entity, instance.content, this.#formatterOptions);
404
- if (instanceOnDisk !== currentFormatted) {
405
- console.log(`Singleton instance of entity ${styleText("cyan", entityName)} is not formatted correctly.`);
406
- counter++;
407
- }
408
- total++;
409
- }, true);
410
- if (counter === 0) {
411
- console.log(styleText("green", `All ${total.toFixed()} instance${total === 1 ? "" : "s"} are formatted correctly`));
412
- return true;
413
- }
414
- else {
415
- console.error(styleText("red", `${counter.toFixed()} instance${counter === 1 ? "" : "s"} of ${total.toFixed()} instance${total === 1 ? "" : "s"} are not formatted correctly`, {
416
- stream: stderr,
417
- }));
418
- return false;
419
- }
420
- }
421
- /**
422
- * Reloads the data from disk into memory.
423
- */
424
- async sync() {
425
- debug("Syncing with disk ...");
426
- const { data, referencesToInstances } = await initData(this.#dataRootPath, this.#schema, this.#locales, this.#git, undefined, false);
427
- this.#data = data;
428
- this.#referencesToInstances = referencesToInstances;
429
- debug("Synced with disk");
430
- }
431
148
  /**
432
149
  * Runs a transaction on the database.
433
150
  *
@@ -443,15 +160,15 @@ export class TSONDB {
443
160
  this.#hooks["transaction:before"]?.forEach(hook => hook(this));
444
161
  this.#locked = true;
445
162
  try {
446
- const getEntity = this.#schema.getEntity.bind(this.#schema);
447
- const getSingletonEntity = this.#schema.getSingletonEntity.bind(this.#schema);
163
+ const getEntity = this.schema.getEntity.bind(this.schema);
164
+ const getSingletonEntity = this.schema.getSingletonEntity.bind(this.schema);
448
165
  const [txn, res] = fn(new Transaction({
449
- data: this.#data,
166
+ data: this._data,
450
167
  getEntity,
451
168
  getSingletonEntity,
452
- referencesToInstances: this.#referencesToInstances,
169
+ referencesToInstances: this._referencesToInstances,
453
170
  validate: this.#strucurallyValidateInstance.bind(this),
454
- localeEntity: this.#schema.localeEntity,
171
+ localeEntity: this.schema.localeEntity,
455
172
  steps: [],
456
173
  }));
457
174
  const txtResult = txn.getResult();
@@ -467,7 +184,7 @@ export class TSONDB {
467
184
  throw new AggregateError(errors, "Validation errors occurred");
468
185
  }
469
186
  debug("Applying changes to disk ...");
470
- const diskResult = await applyStepsToDisk(this.#dataRootPath, steps, this.#formatterOptions);
187
+ const diskResult = await applyStepsToDisk(this.dataRootPath, steps, this._formatterOptions);
471
188
  if (isError(diskResult)) {
472
189
  debug("Error applying changes to disk: %s", diskResult.error.message);
473
190
  throw diskResult.error;
@@ -478,15 +195,15 @@ export class TSONDB {
478
195
  if (this.#git) {
479
196
  debug("Updating git status ...");
480
197
  const status = await this.#git.client.status();
481
- const newDbWithUpdatedGit = attachGitStatusToDatabaseInMemory(newData, this.#dataRootPath, this.#git.root, status);
198
+ const newDbWithUpdatedGit = attachGitStatusToDatabaseInMemory(newData, this.dataRootPath, this.#git.root, status);
482
199
  debug("Git status updated");
483
- this.#data = newDbWithUpdatedGit;
200
+ this._data = newDbWithUpdatedGit;
484
201
  }
485
202
  else {
486
203
  debug("No git repository, skipping git status update");
487
- this.#data = newData;
204
+ this._data = newData;
488
205
  }
489
- this.#referencesToInstances = newRefs;
206
+ this._referencesToInstances = newRefs;
490
207
  this.#locked = false;
491
208
  debug("Transaction successful, released lock");
492
209
  this.#hooks["transaction:after"]?.forEach(hook => hook(this));
@@ -506,7 +223,7 @@ export class TSONDB {
506
223
  async createInstance(entityName, content, id) {
507
224
  const res = await this.runTransaction(txn =>
508
225
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
509
- txn.createInstance(this.#schema.getEntity(entityName), content, id));
226
+ txn.createInstance(this.schema.getEntity(entityName), content, id));
510
227
  return res;
511
228
  }
512
229
  /**
@@ -517,7 +234,7 @@ export class TSONDB {
517
234
  async updateInstance(entityName, id, content) {
518
235
  const res = await this.runTransaction(txn =>
519
236
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
520
- txn.updateInstance(this.#schema.getEntity(entityName), id, content));
237
+ txn.updateInstance(this.schema.getEntity(entityName), id, content));
521
238
  return res;
522
239
  }
523
240
  /**
@@ -528,7 +245,7 @@ export class TSONDB {
528
245
  async deleteInstance(entityName, id) {
529
246
  const res = await this.runTransaction(txn =>
530
247
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
531
- txn.deleteInstance(this.#schema.getEntity(entityName), id));
248
+ txn.deleteInstance(this.schema.getEntity(entityName), id));
532
249
  return res.content;
533
250
  }
534
251
  /**
@@ -540,7 +257,7 @@ export class TSONDB {
540
257
  async createSingletonInstance(entityName, content) {
541
258
  const res = await this.runTransaction(txn =>
542
259
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
543
- txn.createSingletonInstance(this.#schema.getSingletonEntity(entityName), content));
260
+ txn.createSingletonInstance(this.schema.getSingletonEntity(entityName), content));
544
261
  return res;
545
262
  }
546
263
  /**
@@ -551,7 +268,7 @@ export class TSONDB {
551
268
  async updateSingletonInstance(entityName, content) {
552
269
  const res = await this.runTransaction(txn =>
553
270
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
554
- txn.updateSingletonInstance(this.#schema.getSingletonEntity(entityName), content));
271
+ txn.updateSingletonInstance(this.schema.getSingletonEntity(entityName), content));
555
272
  return res;
556
273
  }
557
274
  /**
@@ -562,7 +279,7 @@ export class TSONDB {
562
279
  async deleteSingletonInstance(entityName) {
563
280
  const res = await this.runTransaction(txn =>
564
281
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
565
- txn.deleteSingletonInstance(this.#schema.getSingletonEntity(entityName)));
282
+ txn.deleteSingletonInstance(this.schema.getSingletonEntity(entityName)));
566
283
  return res.content;
567
284
  }
568
285
  /**
@@ -576,7 +293,7 @@ export class TSONDB {
576
293
  return this.runTransaction(txn => [
577
294
  txn.reorderAllInstancesOfEntity(
578
295
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
579
- typeof entityName === "string" ? this.#schema.getEntity(entityName) : entityName, newOrder),
296
+ typeof entityName === "string" ? this.schema.getEntity(entityName) : entityName, newOrder),
580
297
  undefined,
581
298
  ]);
582
299
  }
@@ -590,124 +307,7 @@ export class TSONDB {
590
307
  async reorderInstanceOfEntity(entityName, instanceId, newIndex) {
591
308
  return this.runTransaction(txn => txn.reorderInstanceOfEntity(
592
309
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
593
- typeof entityName === "string" ? this.#schema.getEntity(entityName) : entityName, instanceId, newIndex));
594
- }
595
- /**
596
- * Retrieves an instance of the specified entity by its identifier.
597
- */
598
- getInstanceOfEntityById(...args) {
599
- return this.#data.getInstanceContainerOfEntityById(...args)?.content;
600
- }
601
- /**
602
- * Retrieves the instance container of the specified entity by its identifier.
603
- */
604
- getInstanceContainerOfEntityById(...args) {
605
- return this.#data.getInstanceContainerOfEntityById(...args);
606
- }
607
- /**
608
- * Retrieves the overview of an instance of the specified entity by its identifier.
609
- */
610
- getInstanceOverviewOfEntityById(...args) {
611
- return getInstanceOverview(this.getInstanceContainerOfEntityById.bind(this), this.getAllChildInstanceContainersForParent.bind(this), this.#schema.getEntity.bind(this.#schema), this.#locales, args);
612
- }
613
- /**
614
- * Retrieves all instances of the specified entity.
615
- */
616
- getAllInstancesOfEntity(entityName) {
617
- return this.#data.getAllInstanceContainersOfEntity(entityName).map(ic => ic.content);
618
- }
619
- /**
620
- * Retrieves all instance containers of the specified entity.
621
- */
622
- getAllInstanceContainersOfEntity(entityName) {
623
- return this.#data.getAllInstanceContainersOfEntity(entityName);
624
- }
625
- /**
626
- * Retrieves all instance overviews of the specified entity.
627
- */
628
- getAllInstanceOverviewsOfEntity(entity) {
629
- const normEntity = typeof entity === "object" ? entity : this.#schema.getEntity(entity);
630
- if (!normEntity) {
631
- return [];
632
- }
633
- return getInstanceOverviewsByEntityName(this.getInstanceContainerOfEntityById.bind(this), this.getAllChildInstanceContainersForParent.bind(this), this.#schema.getEntity.bind(this.#schema), normEntity, this.getAllInstanceContainersOfEntity(normEntity.name), this.#locales);
634
- }
635
- /**
636
- * Retrieves an instance of the specified entity by its identifier.
637
- */
638
- getSingletonInstanceOfEntity(entityName) {
639
- return this.#data.getSingletonInstanceOfEntity(entityName);
640
- }
641
- /**
642
- * Retrieves the instance container of the specified entity by its identifier.
643
- */
644
- getSingletonInstanceContainerOfEntity(entityName) {
645
- return this.#data.getSingletonInstanceContainerOfEntity(entityName);
646
- }
647
- /**
648
- * Counts the number of instances registered for the specified entity.
649
- */
650
- countInstancesOfEntity(entityName) {
651
- return this.#data.countInstancesOfEntity(entityName);
652
- }
653
- /**
654
- * Returns whether the singleton entity has an instance.
655
- */
656
- hasInstanceOfSingletonEntity(entityName) {
657
- return this.#data.hasInstanceOfSingletonEntity(entityName);
658
- }
659
- /**
660
- * Retrieves overviews for all instances in the database, grouped by entity name.
661
- */
662
- getAllInstanceOverviews() {
663
- return getAllInstanceOverviewsByEntityName(this.#schema.getEntity.bind(this.#schema), this.#data, this.#locales);
664
- }
665
- /**
666
- * Retrieves all instance containers of the specified child entity for a given parent instance.
667
- */
668
- getAllChildInstanceContainersForParent(childEntityName, parentId) {
669
- return this.#data.getAllChildInstanceContainersForParent(this.#schema.getEntity.bind(this.#schema), childEntityName, parentId);
670
- }
671
- /**
672
- * Displays the name of an entity instance including its ID. If no display name is found, `undefined` is returned.
673
- */
674
- getDisplayName(...args) {
675
- return getDisplayName(this.#schema.getEntity.bind(this.#schema), this.#locales, this.#data, ...args);
676
- }
677
- /**
678
- * Displays the name of an entity instance including its ID. If no display name is found, only the ID is returned.
679
- */
680
- getDisplayNameWithId(...args) {
681
- return getDisplayNameWithId(this.#schema.getEntity.bind(this.#schema), this.#locales, this.#data, ...args);
682
- }
683
- /**
684
- * Checks if a given instance is referenced by other instances in the database.
685
- *
686
- * If `otherInstancesToDelete` is provided, those instances will be excluded from the check. This can be useful when planning to delete multiple instances at once and wanting to ignore references from those instances.
687
- */
688
- isReferencedByOtherInstances(instance, otherInstancesToDelete) {
689
- return isReferencedByOtherInstances(this.#referencesToInstances, instance, otherInstancesToDelete);
690
- }
691
- /**
692
- * The configured locales for this TSONDB instance.
693
- */
694
- get locales() {
695
- return this.#locales;
696
- }
697
- /**
698
- * Sets the locales for this TSONDB instance.
699
- *
700
- * The provided locales must exist in the database if a locale entity is defined in the schema.
701
- */
702
- setLocales(locales) {
703
- checkLocales(this.#schema, this.#data, locales);
704
- this.#locales = locales;
705
- }
706
- /**
707
- * The loaded schema for this TSONDB instance.
708
- */
709
- get schema() {
710
- return this.#schema;
310
+ typeof entityName === "string" ? this.schema.getEntity(entityName) : entityName, instanceId, newIndex));
711
311
  }
712
312
  /**
713
313
  * The Git wrapper for this TSONDB instance, if a git repository is connected.
@@ -721,73 +321,6 @@ export class TSONDB {
721
321
  isGitRepo() {
722
322
  return this.#git !== undefined;
723
323
  }
724
- /**
725
- * Searches for instances matching the provided query in their ID or display name.
726
- *
727
- * The results are sorted by relevance. A custom relevance score function can be provided.
728
- */
729
- search(query, options = {}) {
730
- if (!query || query.trim().length === 0) {
731
- return [];
732
- }
733
- const { getRelevanceScore = ({ id, displayName }) => {
734
- const searchableName = displayName.toLowerCase();
735
- return id.startsWith(query) || searchableName.startsWith(lowerCaseQuery)
736
- ? 1
737
- : id.includes(query) || searchableName.includes(lowerCaseQuery)
738
- ? 0.5
739
- : 0;
740
- }, } = options;
741
- const lowerCaseQuery = query.toLowerCase();
742
- return this.#data
743
- .getAllInstances()
744
- .flatMap(([entityName, instances]) => {
745
- const entity = this.#schema.getEntity(entityName);
746
- if (entity && isEntityDeclWithParentReference(entity)) {
747
- return [];
748
- }
749
- return instances
750
- .map((instance) => {
751
- const { name, localeId } = getDisplayNameFromEntityInstance(
752
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
753
- entity, instance, this.#schema.getEntity.bind(this.#schema), this.getInstanceContainerOfEntityById.bind(this), this.getAllChildInstanceContainersForParent.bind(this), this.#locales);
754
- return [
755
- entityName,
756
- {
757
- id: instance.id,
758
- displayName: name,
759
- displayNameLocaleId: localeId,
760
- relevance: getRelevanceScore({
761
- entityName,
762
- id: instance.id,
763
- content: instance.content,
764
- displayName: name,
765
- displayNameLocaleId: localeId,
766
- query,
767
- queryLowerCase: lowerCaseQuery,
768
- }),
769
- },
770
- ];
771
- })
772
- .filter(instance => instance[1].relevance > 0);
773
- })
774
- .toSorted(reduceCompare(on(item => item[1].relevance, compareNumber), (a, b) => a[1].displayName.localeCompare(b[1].displayName, a[1].displayNameLocaleId)));
775
- }
776
- /**
777
- * Retrieves the name of the entity for a given instance ID. If no instance with the provided ID is found, `undefined` is returned.
778
- */
779
- getEntityNameOfInstanceId(id) {
780
- return this.#data.getEntityNameOfInstanceId(id);
781
- }
782
- /**
783
- * Compresses the entire database into a single JSON file at the specified path.
784
- *
785
- * Instead of storing each instance in its own file, this method serializes the entire database into a single JSON file. This can be useful for backup, transfer or archiving purposes. The resulting file contains all instances organized by entity names. No spaces or indentation are used in the JSON output to minimize file size.
786
- * @param path The file path where the compressed database will be saved.
787
- */
788
- async compressToSingleFile(path) {
789
- await writeFile(path, JSON.stringify(this.#data), "utf-8");
790
- }
791
324
  /**
792
325
  * Registers a new hook for the specified trigger. The callback will be invoked whenever the trigger is activated.
793
326
  */
@@ -807,5 +340,17 @@ export class TSONDB {
807
340
  }
808
341
  }
809
342
  }
343
+ /**
344
+ * Compresses the entire database into a single JSON file at the specified path.
345
+ *
346
+ * Instead of storing each instance in its own file, this method serializes the entire database into a single JSON file. This can be useful for backup, transfer or archiving purposes. The resulting file contains all instances organized by entity names. No spaces or indentation are used in the JSON output to minimize file size.
347
+ * @param path The file path where the compressed database will be saved.
348
+ * @param indentation Optional. If provided, the JSON output will be formatted with the specified number of spaces for indentation. If not provided, the output will be minified without any extra spaces.
349
+ */
350
+ async compressToSingleFile(path, indentation) {
351
+ await writeFile(path, JSON.stringify(createCompressedDatabaseFile(this._data, this.schema.version), undefined, indentation), "utf-8");
352
+ }
353
+ serve(config, callback) {
354
+ createServer(this, { serverOptions: config, validationOptions: this._validationOptions }, callback);
355
+ }
810
356
  }
811
- export { createServer } from "./server/index.js";