tsondb 0.19.3 → 0.19.6

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.
@@ -13,6 +13,7 @@ import { access, constants } from "node:fs/promises";
13
13
  import { join } from "node:path";
14
14
  import { cwd } from "node:process";
15
15
  import { pathToFileURL } from "node:url";
16
+ import { styleText } from "node:util";
16
17
  import { parseArguments } from "simple-cli-args";
17
18
  import { validateConfigForData, validateConfigForGeneration, validateConfigForServer, validateConfigForTesting, } from "../node/config.js";
18
19
  import { TSONDB } from "../node/index.js";
@@ -48,6 +49,25 @@ const passedArguments = parseArguments({
48
49
  format: {
49
50
  name: "format",
50
51
  },
52
+ name: {
53
+ name: "name",
54
+ options: {
55
+ id: {
56
+ name: "id",
57
+ type: String,
58
+ },
59
+ },
60
+ },
61
+ list: {
62
+ name: "list",
63
+ options: {
64
+ entity: {
65
+ name: "entity",
66
+ alias: "e",
67
+ type: String,
68
+ },
69
+ },
70
+ },
51
71
  },
52
72
  });
53
73
  // import the config
@@ -84,6 +104,10 @@ if (config === undefined) {
84
104
  if (passedArguments.command === undefined) {
85
105
  throw new Error("No command has been specified. Possible commands are: generate, serve, validate.");
86
106
  }
107
+ const createDB = (config, validationOptions, skipReferenceCache) => TSONDB.create({
108
+ ...config,
109
+ validationOptions,
110
+ }, skipReferenceCache);
87
111
  if (passedArguments.command.name === "generate") {
88
112
  debug(`running command: generate`);
89
113
  validateConfigForGeneration(config);
@@ -91,21 +115,14 @@ if (passedArguments.command.name === "generate") {
91
115
  }
92
116
  else {
93
117
  validateConfigForData(config);
94
- const db = await TSONDB.create({
95
- ...config,
96
- validationOptions: passedArguments.command.name === "validate"
97
- ? {
98
- ...config.validationOptions,
99
- ...omitUndefinedKeys(passedArguments.command.options ?? {}),
100
- }
101
- : config.validationOptions,
102
- });
103
118
  switch (passedArguments.command.name) {
104
- case "serve":
119
+ case "serve": {
105
120
  debug(`running command: serve`);
106
121
  validateConfigForServer(config);
122
+ const db = await createDB(config, config.validationOptions, false);
107
123
  await createServer(db, config.homeLayoutSections, config.serverOptions, config.validationOptions, config.customStylesheetPath);
108
124
  break;
125
+ }
109
126
  case "validate": {
110
127
  debug(`running command: validate`);
111
128
  validateConfigForTesting(config);
@@ -116,15 +133,57 @@ else {
116
133
  const entities = passedArguments.command.options.checkOnlyEntities;
117
134
  debug(`only check the following entities: ${entities.join(", ")}`);
118
135
  }
136
+ const db = await createDB(config, {
137
+ ...config.validationOptions,
138
+ ...omitUndefinedKeys(passedArguments.command.options ?? {}),
139
+ }, false);
119
140
  const result = db.validate();
120
141
  if (!result) {
121
142
  process.exitCode = 1;
122
143
  }
123
144
  break;
124
145
  }
125
- case "format":
146
+ case "format": {
126
147
  debug(`running command: format`);
148
+ const db = await createDB(config, config.validationOptions, true);
127
149
  await db.format();
128
150
  break;
151
+ }
152
+ case "name": {
153
+ debug(`running command: name`);
154
+ const id = passedArguments.command.options?.id;
155
+ if (id === undefined) {
156
+ throw new Error("No ID specified for name command.");
157
+ }
158
+ const db = await createDB(config, config.validationOptions, true);
159
+ const entityName = db.getEntityNameOfInstanceId(id);
160
+ if (entityName === undefined) {
161
+ throw new Error("An instance with the specified ID does not exist in the database.");
162
+ }
163
+ const displayName = db.getDisplayName(entityName, id);
164
+ console.log(displayName ?? styleText(["underline"], "no display name available"));
165
+ break;
166
+ }
167
+ case "list": {
168
+ debug(`running command: list`);
169
+ const entity = passedArguments.command.options?.entity;
170
+ if (entity === undefined) {
171
+ throw new Error("No entity specified for list command.");
172
+ }
173
+ const db = await createDB(config, config.validationOptions, true);
174
+ if (!db.schema.isEntityName(entity)) {
175
+ throw new Error("An entity with the specified name does not exist in the database.");
176
+ }
177
+ const instances = db.getAllInstanceOverviewsOfEntity(entity);
178
+ if (instances.length === 0) {
179
+ console.log(styleText(["italic"], "no instances found"));
180
+ break;
181
+ }
182
+ console.log(styleText(["bold"], "UUID Display Name"));
183
+ for (const instance of instances) {
184
+ console.log(`${styleText(["yellow"], instance.id)} ${instance.displayName}`);
185
+ }
186
+ break;
187
+ }
129
188
  }
130
189
  }
@@ -1,13 +1,13 @@
1
1
  import type { Output } from "../shared/output.ts";
2
- import type { ValidationOptions } from "./index.ts";
2
+ import type { DefaultTSONDBTypes, ValidationOptions } from "./index.ts";
3
3
  import type { EntityDecl } from "./schema/dsl/index.ts";
4
4
  import type { Schema } from "./schema/index.ts";
5
5
  /**
6
6
  * The main configuration type for TSONDB.
7
7
  */
8
- export type Config = {
8
+ export type Config<T extends DefaultTSONDBTypes = DefaultTSONDBTypes> = {
9
9
  serverOptions?: ServerOptions;
10
- schema: Schema;
10
+ schema: Schema<T>;
11
11
  outputs?: Output[];
12
12
  /**
13
13
  * Default locales for the server/editor and locales used for testing via CLI.
@@ -83,8 +83,11 @@ export declare class TSONDB<T extends DefaultTSONDBTypes = DefaultTSONDBTypes> {
83
83
  private constructor();
84
84
  /**
85
85
  * Creates a new TSONDB instance with the specified options.
86
+ *
87
+ * @param options The options for creating the TSONDB instance.
88
+ * @param skipReferenceCache If set to `true`, the reference cache will not be created during initialization. This can speed up the initialization process, but referential integrity checks will not work correctly until the cache is created. This should only be set to `true` if you plan to call `sync()` immediately after initialization.
86
89
  */
87
- static create<Types extends DefaultTSONDBTypes = DefaultTSONDBTypes>(options: TSONDBOptions<Types>): Promise<TSONDB<Types>>;
90
+ static create<Types extends DefaultTSONDBTypes = DefaultTSONDBTypes>(options: TSONDBOptions<Types>, skipReferenceCache?: boolean): Promise<TSONDB<Types>>;
88
91
  /**
89
92
  * Generates outputs based on the provided schema and output configurations.
90
93
  */
@@ -218,6 +221,10 @@ export declare class TSONDB<T extends DefaultTSONDBTypes = DefaultTSONDBTypes> {
218
221
  queryLowerCase: string;
219
222
  }) => number;
220
223
  }>): [string, InstanceContainerOverview][];
224
+ /**
225
+ * Retrieves the name of the entity for a given instance ID. If no instance with the provided ID is found, `undefined` is returned.
226
+ */
227
+ getEntityNameOfInstanceId(id: string): string | undefined;
221
228
  /**
222
229
  * Compresses the entire database into a single JSON file at the specified path.
223
230
  *
@@ -21,7 +21,7 @@ import { checkCustomConstraintsForAllEntities } from "./utils/customConstraints.
21
21
  import { DatabaseInMemory } from "./utils/databaseInMemory.js";
22
22
  import { applyStepsToDisk } from "./utils/databaseOnDisk.js";
23
23
  import { getAllInstanceOverviewsByEntityName, getDisplayNameFromEntityInstance, getInstanceOverview, getInstanceOverviewsByEntityName, } from "./utils/displayName.js";
24
- import { countError, countErrors, getErrorMessageForDisplay, wrapErrorsIfAny, } from "./utils/error.js";
24
+ import { countError, countErrors, getErrorMessageForDisplay, HTTPError, wrapErrorsIfAny, } from "./utils/error.js";
25
25
  import { getFileNameForId, writeInstance } from "./utils/files.js";
26
26
  import { attachGitStatusToDatabaseInMemory } from "./utils/git.js";
27
27
  import { getReferencesToInstances, isReferencedByOtherInstances, } from "./utils/references.js";
@@ -55,7 +55,7 @@ const getGit = async (dataRootPath) => {
55
55
  return { git };
56
56
  }
57
57
  };
58
- const initData = async (dataRootPath, schema, locales, git, gitStatus) => {
58
+ const initData = async (dataRootPath, schema, locales, git, gitStatus, skipReferenceCache) => {
59
59
  debug("loading database into memory ...");
60
60
  let data = await DatabaseInMemory.load(dataRootPath, schema.entities);
61
61
  debug("done");
@@ -65,9 +65,16 @@ const initData = async (dataRootPath, schema, locales, git, gitStatus) => {
65
65
  throw new Error("All provided locales must exist in the database.");
66
66
  }
67
67
  const serializedDeclarationsByName = Object.fromEntries(schema.resolvedDeclarations.map(decl => [decl.name, serializeNode(decl)]));
68
- debug("creating references cache ...");
69
- const referencesToInstances = await getReferencesToInstances(data, serializedDeclarationsByName);
70
- debug("done");
68
+ let referencesToInstances;
69
+ if (!skipReferenceCache) {
70
+ debug("creating references cache ...");
71
+ referencesToInstances = await getReferencesToInstances(data, serializedDeclarationsByName);
72
+ debug("done");
73
+ }
74
+ else {
75
+ debug("skipping references cache creation");
76
+ referencesToInstances = {};
77
+ }
71
78
  if (git) {
72
79
  const status = gitStatus ?? (await git.client.status());
73
80
  data = attachGitStatusToDatabaseInMemory(data, dataRootPath, git.root, status);
@@ -87,6 +94,7 @@ export class TSONDB {
87
94
  #referencesToInstances;
88
95
  #validationOptions;
89
96
  #gitWrapper;
97
+ #locked = false;
90
98
  constructor(options) {
91
99
  this.#dataRootPath = options.dataRootPath;
92
100
  this.#data = options.data;
@@ -103,8 +111,11 @@ export class TSONDB {
103
111
  }
104
112
  /**
105
113
  * Creates a new TSONDB instance with the specified options.
114
+ *
115
+ * @param options The options for creating the TSONDB instance.
116
+ * @param skipReferenceCache If set to `true`, the reference cache will not be created during initialization. This can speed up the initialization process, but referential integrity checks will not work correctly until the cache is created. This should only be set to `true` if you plan to call `sync()` immediately after initialization.
106
117
  */
107
- static async create(options) {
118
+ static async create(options, skipReferenceCache = false) {
108
119
  const entities = options.schema.entities;
109
120
  debug("using data root path: %s", options.dataRootPath);
110
121
  await prepareFolders(options.dataRootPath, entities);
@@ -121,7 +132,7 @@ export class TSONDB {
121
132
  throw new Error("Schema does not contain a locale entity, locales cannot be provided.");
122
133
  }
123
134
  const git = gitRoot ? { client: gitClient, root: gitRoot } : undefined;
124
- const { data, referencesToInstances } = await initData(options.dataRootPath, options.schema, options.locales ?? [], git, gitStatus);
135
+ const { data, referencesToInstances } = await initData(options.dataRootPath, options.schema, options.locales ?? [], git, gitStatus, skipReferenceCache);
125
136
  return new TSONDB({
126
137
  ...options,
127
138
  data,
@@ -266,7 +277,7 @@ export class TSONDB {
266
277
  * Reloads the data from disk into memory.
267
278
  */
268
279
  async sync() {
269
- const { data, referencesToInstances } = await initData(this.#dataRootPath, this.#schema, this.#locales, this.#git);
280
+ const { data, referencesToInstances } = await initData(this.#dataRootPath, this.#schema, this.#locales, this.#git, undefined, false);
270
281
  this.#data = data;
271
282
  this.#referencesToInstances = referencesToInstances;
272
283
  }
@@ -277,35 +288,46 @@ export class TSONDB {
277
288
  * @throws {Error} when the transaction could not be completed
278
289
  */
279
290
  async runTransaction(fn) {
280
- const getEntity = this.#schema.getEntity.bind(this.#schema);
281
- const [txn, res] = fn(new Transaction({
282
- data: this.#data,
283
- getEntity,
284
- referencesToInstances: this.#referencesToInstances,
285
- validate: this.#strucurallyValidateInstance.bind(this),
286
- localeEntity: this.#schema.localeEntity,
287
- steps: [],
288
- }));
289
- const txtResult = txn.getResult();
290
- const { data: newData, referencesToInstances: newRefs, steps } = txtResult;
291
- const errors = this.#validate(newData, true);
292
- if (errors.length > 0) {
293
- throw new AggregateError(errors, "Validation errors occurred");
294
- }
295
- const diskResult = await applyStepsToDisk(this.#dataRootPath, steps);
296
- if (isError(diskResult)) {
297
- throw diskResult.error;
291
+ if (this.#locked) {
292
+ throw new HTTPError(503, "Another transaction is currently running. Transactions cannot be run concurrently.");
298
293
  }
299
- if (this.#git) {
300
- const status = await this.#git.client.status();
301
- const newDbWithUpdatedGit = attachGitStatusToDatabaseInMemory(newData, this.#dataRootPath, this.#git.root, status);
302
- this.#data = newDbWithUpdatedGit;
294
+ this.#locked = true;
295
+ try {
296
+ const getEntity = this.#schema.getEntity.bind(this.#schema);
297
+ const [txn, res] = fn(new Transaction({
298
+ data: this.#data,
299
+ getEntity,
300
+ referencesToInstances: this.#referencesToInstances,
301
+ validate: this.#strucurallyValidateInstance.bind(this),
302
+ localeEntity: this.#schema.localeEntity,
303
+ steps: [],
304
+ }));
305
+ const txtResult = txn.getResult();
306
+ const { data: newData, referencesToInstances: newRefs, steps } = txtResult;
307
+ const errors = this.#validate(newData, true);
308
+ if (errors.length > 0) {
309
+ throw new AggregateError(errors, "Validation errors occurred");
310
+ }
311
+ const diskResult = await applyStepsToDisk(this.#dataRootPath, steps);
312
+ if (isError(diskResult)) {
313
+ throw diskResult.error;
314
+ }
315
+ if (this.#git) {
316
+ const status = await this.#git.client.status();
317
+ const newDbWithUpdatedGit = attachGitStatusToDatabaseInMemory(newData, this.#dataRootPath, this.#git.root, status);
318
+ this.#data = newDbWithUpdatedGit;
319
+ }
320
+ else {
321
+ this.#data = newData;
322
+ }
323
+ this.#referencesToInstances = newRefs;
324
+ this.#locked = false;
325
+ return res;
303
326
  }
304
- else {
305
- this.#data = newData;
327
+ catch (error) {
328
+ this.#locked = false;
329
+ throw error;
306
330
  }
307
- this.#referencesToInstances = newRefs;
308
- return res;
309
331
  }
310
332
  /**
311
333
  * Creates a new instance in the database.
@@ -495,6 +517,12 @@ export class TSONDB {
495
517
  })
496
518
  .toSorted(reduceCompare(on(item => item[1].relevance, compareNumber), (a, b) => a[1].displayName.localeCompare(b[1].displayName, a[1].displayNameLocaleId)));
497
519
  }
520
+ /**
521
+ * Retrieves the name of the entity for a given instance ID. If no instance with the provided ID is found, `undefined` is returned.
522
+ */
523
+ getEntityNameOfInstanceId(id) {
524
+ return this.#data.getEntityNameOfInstanceId(id);
525
+ }
498
526
  /**
499
527
  * Compresses the entire database into a single JSON file at the specified path.
500
528
  *
@@ -21,4 +21,5 @@ export declare class DatabaseInMemory<EM extends AnyEntityMap = RegisteredEntity
21
21
  setInstanceContainerOfEntityById(entityName: Extract<keyof EM, string>, instance: InstanceContainer): [DatabaseInMemory<EM>, oldInstance: InstanceContent | undefined];
22
22
  deleteInstanceContainerOfEntityById(entityName: Extract<keyof EM, string>, instanceId: string): [DatabaseInMemory<EM>, oldInstance: InstanceContent | undefined];
23
23
  map(fn: (instances: Dictionary<InstanceContainer>, entityName: Extract<keyof EM, string>) => Dictionary<InstanceContainer>): DatabaseInMemory<EM>;
24
+ getEntityNameOfInstanceId(id: string): Extract<keyof EM, string> | undefined;
24
25
  }
@@ -110,4 +110,7 @@ export class DatabaseInMemory {
110
110
  map(fn) {
111
111
  return new DatabaseInMemory(this.#data.map(fn));
112
112
  }
113
+ getEntityNameOfInstanceId(id) {
114
+ return this.#data.findKey(instances => instances.has(id));
115
+ }
113
116
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tsondb",
3
- "version": "0.19.3",
3
+ "version": "0.19.6",
4
4
  "description": "",
5
5
  "license": "ISC",
6
6
  "author": "Lukas Obermann",
@@ -48,7 +48,7 @@
48
48
  "typescript-eslint": "^8.54.0"
49
49
  },
50
50
  "dependencies": {
51
- "@elyukai/utils": "^0.2.0",
51
+ "@elyukai/utils": "^0.2.1",
52
52
  "@preact/signals": "^2.7.0",
53
53
  "debug": "^4.4.3",
54
54
  "express": "^5.2.1",