tsondb 0.21.1 → 0.21.2

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 (51) hide show
  1. package/dist/src/node/config.d.ts +2 -2
  2. package/dist/src/node/git.d.ts +4 -4
  3. package/dist/src/node/index.d.ts +39 -4
  4. package/dist/src/node/index.js +112 -11
  5. package/dist/src/node/renderers/jsonschema/render.js +7 -0
  6. package/dist/src/node/renderers/ts/render.d.ts +4 -0
  7. package/dist/src/node/renderers/ts/render.js +6 -0
  8. package/dist/src/node/schema/dsl/declarations/Decl.d.ts +3 -2
  9. package/dist/src/node/schema/dsl/declarations/Decl.js +2 -1
  10. package/dist/src/node/schema/dsl/declarations/SingletonEntityDecl.d.ts +35 -0
  11. package/dist/src/node/schema/dsl/declarations/SingletonEntityDecl.js +23 -0
  12. package/dist/src/node/schema/dsl/index.d.ts +1 -0
  13. package/dist/src/node/schema/dsl/index.js +1 -0
  14. package/dist/src/node/schema/generatedTypeHelpers.d.ts +18 -0
  15. package/dist/src/node/schema/guards.d.ts +1 -0
  16. package/dist/src/node/schema/guards.js +1 -0
  17. package/dist/src/node/schema/helpers.js +1 -0
  18. package/dist/src/node/schema/index.d.ts +12 -1
  19. package/dist/src/node/schema/index.js +44 -0
  20. package/dist/src/node/schema/treeOperations/customConstraints.d.ts +2 -1
  21. package/dist/src/node/schema/treeOperations/customConstraints.js +10 -10
  22. package/dist/src/node/schema/treeOperations/implicitSchema.js +1 -0
  23. package/dist/src/node/schema/treeOperations/references.js +1 -0
  24. package/dist/src/node/schema/treeOperations/serialization.d.ts +4 -1
  25. package/dist/src/node/schema/treeOperations/serialization.js +8 -0
  26. package/dist/src/node/schema/treeOperations/typeResolution.d.ts +4 -1
  27. package/dist/src/node/schema/treeOperations/typeResolution.js +6 -0
  28. package/dist/src/node/schema/treeOperations/validation.js +2 -0
  29. package/dist/src/node/server/api/declarations.js +165 -23
  30. package/dist/src/node/transaction.d.ts +28 -11
  31. package/dist/src/node/transaction.js +93 -0
  32. package/dist/src/node/utils/customConstraints.d.ts +6 -5
  33. package/dist/src/node/utils/customConstraints.js +34 -15
  34. package/dist/src/node/utils/databaseInMemory.d.ts +20 -8
  35. package/dist/src/node/utils/databaseInMemory.js +67 -9
  36. package/dist/src/node/utils/databaseOnDisk.js +27 -30
  37. package/dist/src/node/utils/files.d.ts +7 -2
  38. package/dist/src/node/utils/files.js +9 -0
  39. package/dist/src/node/utils/references.d.ts +2 -0
  40. package/dist/src/node/utils/references.js +6 -1
  41. package/dist/src/shared/api.d.ts +41 -2
  42. package/dist/src/shared/schema/Node.d.ts +5 -1
  43. package/dist/src/shared/schema/Node.js +6 -0
  44. package/dist/src/shared/schema/declarations/Declaration.d.ts +3 -2
  45. package/dist/src/shared/schema/declarations/SingletonEntityDecl.d.ts +18 -0
  46. package/dist/src/shared/schema/declarations/SingletonEntityDecl.js +8 -0
  47. package/dist/src/shared/utils/childInstances.d.ts +4 -0
  48. package/dist/src/shared/utils/instances.d.ts +4 -0
  49. package/dist/src/web/web.js +573 -130
  50. package/package.json +1 -1
  51. package/public/css/styles.css +1 -0
@@ -1,7 +1,7 @@
1
1
  import type { FormatterOptions as MarkdownFormatterOptions } from "@elyukai/markdown/format";
2
2
  import type { DefaultTSONDBTypes, ValidationOptions } from "./index.ts";
3
3
  import type { Output } from "./output.ts";
4
- import type { EntityDecl } from "./schema/dsl/index.ts";
4
+ import type { EntityDecl, SingletonEntityDecl } from "./schema/dsl/index.ts";
5
5
  import type { Schema } from "./schema/index.ts";
6
6
  /**
7
7
  * The main configuration type for TSONDB.
@@ -33,7 +33,7 @@ export type ServerOptions = {
33
33
  export type HomeLayoutSection = {
34
34
  title: string;
35
35
  comment?: string;
36
- entities: EntityDecl[];
36
+ entities: (EntityDecl | SingletonEntityDecl)[];
37
37
  };
38
38
  /**
39
39
  * The configuration type required for generation commands.
@@ -1,6 +1,6 @@
1
1
  import type { SimpleGit } from "simple-git";
2
2
  import type { InstanceContainerOverview } from "../shared/utils/instances.ts";
3
- import type { DefaultTSONDBTypes, EntityName, TSONDB } from "./index.ts";
3
+ import type { DefaultTSONDBTypes, StoringEntityName, TSONDB } from "./index.ts";
4
4
  import type { DatabaseInMemory } from "./utils/databaseInMemory.ts";
5
5
  export declare class Git<T extends DefaultTSONDBTypes = DefaultTSONDBTypes> {
6
6
  #private;
@@ -36,7 +36,7 @@ export declare class Git<T extends DefaultTSONDBTypes = DefaultTSONDBTypes> {
36
36
  *
37
37
  * @throws {HTTPError} when no git repository is found
38
38
  */
39
- stage(entity?: EntityName<T>, id?: string): Promise<void>;
39
+ stage(entity?: StoringEntityName<T>, id?: string): Promise<void>;
40
40
  /**
41
41
  * Removes files from the git staging area.
42
42
  *
@@ -46,7 +46,7 @@ export declare class Git<T extends DefaultTSONDBTypes = DefaultTSONDBTypes> {
46
46
  *
47
47
  * @throws {HTTPError} when no git repository is found
48
48
  */
49
- unstage(entity?: EntityName<T>, id?: string): Promise<void>;
49
+ unstage(entity?: StoringEntityName<T>, id?: string): Promise<void>;
50
50
  /**
51
51
  * Restores files in the git repository to their last committed state.
52
52
  *
@@ -56,7 +56,7 @@ export declare class Git<T extends DefaultTSONDBTypes = DefaultTSONDBTypes> {
56
56
  *
57
57
  * @throws {HTTPError} when no git repository is found
58
58
  */
59
- restore(entity?: EntityName<T>, id?: string): Promise<void>;
59
+ restore(entity?: StoringEntityName<T>, id?: string): Promise<void>;
60
60
  /**
61
61
  * Commits staged changes to the git repository with the provided commit message.
62
62
  *
@@ -1,23 +1,27 @@
1
- import type { InstanceContainer, InstanceContainerOverview } from "../shared/utils/instances.ts";
1
+ import type { InstanceContainer, InstanceContainerOverview, SingletonInstanceContainer } from "../shared/utils/instances.ts";
2
2
  import type { FormatterOptions } from "./config.ts";
3
3
  import { Git } from "./git.js";
4
4
  import type { Output } from "./output.ts";
5
5
  import { type EntityDecl } from "./schema/dsl/index.ts";
6
- import type { AnyChildEntityMap, AnyEntityMap, AnyEnumMap, AnyTypeAliasMap } from "./schema/generatedTypeHelpers.ts";
6
+ import type { AnyChildEntityMap, AnyEntityMap, AnyEnumMap, AnySingletonEntityMap, AnyTypeAliasMap } from "./schema/generatedTypeHelpers.ts";
7
7
  import { type IdArgsVariant } from "./schema/generatedTypeHelpers.ts";
8
8
  import type { Schema } from "./schema/index.ts";
9
9
  import { Transaction } from "./transaction.ts";
10
10
  export interface DefaultTSONDBTypes {
11
11
  entityMap: AnyEntityMap;
12
+ singletonEntityMap: AnySingletonEntityMap;
12
13
  childEntityMap: AnyChildEntityMap;
13
14
  enumMap: AnyEnumMap;
14
15
  typeAliasMap: AnyTypeAliasMap;
15
16
  }
16
17
  export type Entity<T extends DefaultTSONDBTypes, E extends EntityName<T>> = T["entityMap"][E];
17
18
  export type EntityName<T extends DefaultTSONDBTypes> = Extract<keyof T["entityMap"], string>;
19
+ export type SingletonEntity<T extends DefaultTSONDBTypes, E extends SingletonEntityName<T>> = T["singletonEntityMap"][E];
20
+ export type SingletonEntityName<T extends DefaultTSONDBTypes> = Extract<keyof T["singletonEntityMap"], string>;
18
21
  export type EnumName<T extends DefaultTSONDBTypes> = Extract<keyof T["enumMap"], string>;
19
22
  export type TypeAliasName<T extends DefaultTSONDBTypes> = Extract<keyof T["typeAliasMap"], string>;
20
- export type DeclarationName<T extends DefaultTSONDBTypes> = EntityName<T> | EnumName<T> | TypeAliasName<T>;
23
+ export type StoringEntityName<T extends DefaultTSONDBTypes> = EntityName<T> | SingletonEntityName<T>;
24
+ export type DeclarationName<T extends DefaultTSONDBTypes> = EntityName<T> | SingletonEntityName<T> | EnumName<T> | TypeAliasName<T>;
21
25
  export type ChildEntity<T extends DefaultTSONDBTypes, E extends ChildEntityName<T>> = T["childEntityMap"][E][0];
22
26
  export type ChildEntityConfig<T extends DefaultTSONDBTypes, E extends ChildEntityName<T>> = T["childEntityMap"][E];
23
27
  export type ChildEntityName<T extends DefaultTSONDBTypes> = Extract<keyof T["childEntityMap"], string>;
@@ -148,7 +152,7 @@ export declare class TSONDB<T extends DefaultTSONDBTypes = DefaultTSONDBTypes> {
148
152
  * The provided function is called with a `Transaction` object, which can be used to create, update, or delete instances. If an error is thrown during the execution of the function, the transaction is rolled back and no changes are made to the database.
149
153
  * @throws {Error} when the transaction could not be completed
150
154
  */
151
- runTransaction<R>(fn: (transaction: Transaction<T["entityMap"]>) => [Transaction<T["entityMap"]>, R]): Promise<R>;
155
+ runTransaction<R>(fn: (transaction: Transaction<T["entityMap"], T["singletonEntityMap"]>) => [Transaction<T["entityMap"], T["singletonEntityMap"]>, R]): Promise<R>;
152
156
  /**
153
157
  * Creates a new instance in the database.
154
158
  *
@@ -168,6 +172,25 @@ export declare class TSONDB<T extends DefaultTSONDBTypes = DefaultTSONDBTypes> {
168
172
  * @throws {Error} when the instance could not be deleted
169
173
  */
170
174
  deleteInstance<E extends EntityName<T>>(entityName: E, id: string): Promise<Entity<T, E>>;
175
+ /**
176
+ * Creates a new singleton instance in the database.
177
+ *
178
+ * If the instance is of the locale entity, a locale identifier must be provided as `id`.
179
+ * @throws {Error} when the instance could not be created
180
+ */
181
+ createSingletonInstance<E extends SingletonEntityName<T>>(entityName: E, content: SingletonEntity<T, E>): Promise<SingletonInstanceContainer<SingletonEntity<T, E>>>;
182
+ /**
183
+ * Updates a singleton instance in the database.
184
+ *
185
+ * @throws {Error} when the instance could not be updated
186
+ */
187
+ updateSingletonInstance<E extends SingletonEntityName<T>>(entityName: E, content: SingletonEntity<T, E>): Promise<SingletonInstanceContainer<SingletonEntity<T, E>>>;
188
+ /**
189
+ * Deletes a singleton instance in the database.
190
+ *
191
+ * @throws {Error} when the instance could not be deleted
192
+ */
193
+ deleteSingletonInstance<E extends SingletonEntityName<T>>(entityName: E): Promise<SingletonEntity<T, E>>;
171
194
  /**
172
195
  * Retrieves an instance of the specified entity by its identifier.
173
196
  */
@@ -192,10 +215,22 @@ export declare class TSONDB<T extends DefaultTSONDBTypes = DefaultTSONDBTypes> {
192
215
  * Retrieves all instance overviews of the specified entity.
193
216
  */
194
217
  getAllInstanceOverviewsOfEntity(entity: EntityDecl<EntityName<T>> | EntityName<T>): InstanceContainerOverview[];
218
+ /**
219
+ * Retrieves an instance of the specified entity by its identifier.
220
+ */
221
+ getSingletonInstanceOfEntity<E extends SingletonEntityName<T>>(entityName: E): T["singletonEntityMap"][E] | undefined;
222
+ /**
223
+ * Retrieves the instance container of the specified entity by its identifier.
224
+ */
225
+ getSingletonInstanceContainerOfEntity<E extends SingletonEntityName<T>>(entityName: E): SingletonInstanceContainer<T["singletonEntityMap"][E]> | undefined;
195
226
  /**
196
227
  * Counts the number of instances registered for the specified entity.
197
228
  */
198
229
  countInstancesOfEntity(entityName: EntityName<T>): number;
230
+ /**
231
+ * Returns whether the singleton entity has an instance.
232
+ */
233
+ hasInstanceOfSingletonEntity(entityName: SingletonEntityName<T>): boolean;
199
234
  /**
200
235
  * Retrieves overviews for all instances in the database, grouped by entity name.
201
236
  */
@@ -1,19 +1,22 @@
1
1
  import { on } from "@elyukai/utils/function";
2
2
  import { Lazy } from "@elyukai/utils/lazy";
3
+ import { nullableToArray } from "@elyukai/utils/nullable";
3
4
  import { compareNumber, reduceCompare } from "@elyukai/utils/ordering";
4
5
  import { isError } from "@elyukai/utils/result";
6
+ import { assertExhaustive } from "@elyukai/utils/typeSafety";
5
7
  import Debug from "debug";
6
8
  import { mkdir, writeFile } from "node:fs/promises";
7
9
  import { join, sep } from "node:path";
8
10
  import { stderr } from "node:process";
9
11
  import { styleText } from "node:util";
10
12
  import { simpleGit } from "simple-git";
13
+ import { NodeKind } from "../shared/schema/Node.js";
11
14
  import { parallelizeErrors } from "../shared/utils/validation.js";
12
15
  import { Git } from "./git.js";
13
16
  import { getDisplayName, getDisplayNameWithId } from "./schema/detached.js";
14
17
  import {} from "./schema/dsl/index.js";
15
18
  import {} from "./schema/generatedTypeHelpers.js";
16
- import { isEntityDeclWithParentReference } from "./schema/guards.js";
19
+ import { isEntityDecl, isEntityDeclWithParentReference } from "./schema/guards.js";
17
20
  import { serializeNode } from "./schema/treeOperations/serialization.js";
18
21
  import { createEntityNameValidator, createReferenceValidator, validateDeclReferentialIntegrity, validateDeclStructuralIntegrity, } from "./schema/treeOperations/validation.js";
19
22
  import { Transaction } from "./transaction.js";
@@ -22,7 +25,7 @@ import { DatabaseInMemory } from "./utils/databaseInMemory.js";
22
25
  import { applyStepsToDisk } from "./utils/databaseOnDisk.js";
23
26
  import { getAllInstanceOverviewsByEntityName, getDisplayNameFromEntityInstance, getInstanceOverview, getInstanceOverviewsByEntityName, } from "./utils/displayName.js";
24
27
  import { countError, countErrors, getErrorMessageForDisplay, HTTPError, wrapErrorsIfAny, } from "./utils/error.js";
25
- import { formatInstance, getFileNameForId, readInstance, writeInstance } from "./utils/files.js";
28
+ import { formatInstance, getFileNameForId, getSingletonFileName, readInstance, readSingletonInstance, writeInstance, writeSingletonInstance, } from "./utils/files.js";
26
29
  import { attachGitStatusToDatabaseInMemory } from "./utils/git.js";
27
30
  import { getReferencesToInstances, isReferencedByOtherInstances, } from "./utils/references.js";
28
31
  import { checkUniqueConstraintsForAllEntities } from "./utils/unique.js";
@@ -64,7 +67,7 @@ const checkLocales = (schema, data, locales) => {
64
67
  };
65
68
  const initData = async (dataRootPath, schema, locales, git, gitStatus, skipReferenceCache) => {
66
69
  debug("loading database into memory ...");
67
- let data = await DatabaseInMemory.load(dataRootPath, schema.entities);
70
+ let data = await DatabaseInMemory.load(dataRootPath, schema.entities, schema.singletonEntities);
68
71
  debug("done");
69
72
  checkLocales(schema, data, locales);
70
73
  const serializedDeclarationsByName = Object.fromEntries(schema.resolvedDeclarations.map(decl => [decl.name, serializeNode(decl)]));
@@ -172,7 +175,7 @@ export class TSONDB {
172
175
  #validate(data, options = {}) {
173
176
  const { checkReferentialIntegrity, checkOnlyEntities } = this.#validationOptions;
174
177
  const { skipStructuralValidation = false, logToConsole = true, changedEntities: changedEntityNames, } = options;
175
- const entities = this.#schema.entities;
178
+ const entities = [...this.#schema.entities, ...this.#schema.singletonEntities];
176
179
  const getEntity = this.#schema.getEntity.bind(this.#schema);
177
180
  for (const onlyEntity of checkOnlyEntities) {
178
181
  if (!entities.find(entity => entity.name === onlyEntity)) {
@@ -193,9 +196,23 @@ export class TSONDB {
193
196
  if (!skipStructuralValidation) {
194
197
  debug("Checking structural integrity ...");
195
198
  const structureErrors = onlyEntities
196
- .flatMap(entity => parallelizeErrors(data
197
- .getAllInstanceContainersOfEntity(entity.name)
198
- .map(instance => wrapErrorsIfAny(`in file ${styleText("white", `"${this.#dataRootPath}${sep}${styleText("bold", join(entity.name, getFileNameForId(instance.id)))}"`)}`, validateDeclStructuralIntegrity(validationContext, [], entity, [], instance.content)))))
199
+ .flatMap(entity => {
200
+ switch (entity.kind) {
201
+ case NodeKind.EntityDecl:
202
+ return parallelizeErrors(data
203
+ .getAllInstanceContainersOfEntity(entity.name)
204
+ .map(instance => wrapErrorsIfAny(`in file ${styleText("white", `"${this.#dataRootPath}${sep}${styleText("bold", join(entity.name, getFileNameForId(instance.id)))}"`)}`, validateDeclStructuralIntegrity(validationContext, [], entity, [], instance.content))));
205
+ case NodeKind.SingletonEntityDecl: {
206
+ const instance = data.getSingletonInstanceContainerOfEntity(entity.name);
207
+ if (instance) {
208
+ return nullableToArray(wrapErrorsIfAny(`in file ${styleText("white", `"${this.#dataRootPath}${sep}${styleText("bold", getSingletonFileName(entity.name))}"`)}`, validateDeclStructuralIntegrity(validationContext, [], entity, [], instance.content)));
209
+ }
210
+ return [];
211
+ }
212
+ default:
213
+ return assertExhaustive(entity);
214
+ }
215
+ })
199
216
  .toSorted((a, b) => a.message.localeCompare(b.message));
200
217
  if (structureErrors.length > 0) {
201
218
  const errorCount = countErrors(structureErrors);
@@ -213,9 +230,23 @@ export class TSONDB {
213
230
  const entityNameValidator = createEntityNameValidator(this.#schema.isEntityName.bind(this.#schema), true);
214
231
  const referenceValidator = createReferenceValidator(this.#schema.isEntityName.bind(this.#schema), data, true);
215
232
  const referenceErrors = onlyEntities
216
- .flatMap(entity => parallelizeErrors(data
217
- .getAllInstanceContainersOfEntity(entity.name)
218
- .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)))))
233
+ .flatMap(entity => {
234
+ switch (entity.kind) {
235
+ case NodeKind.EntityDecl:
236
+ return parallelizeErrors(data
237
+ .getAllInstanceContainersOfEntity(entity.name)
238
+ .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))));
239
+ case NodeKind.SingletonEntityDecl: {
240
+ const instance = data.getSingletonInstanceContainerOfEntity(entity.name);
241
+ if (instance) {
242
+ return nullableToArray(wrapErrorsIfAny(`in file ${styleText("white", `"${this.#dataRootPath}${sep}${styleText("bold", getSingletonFileName(entity.name))}"`)}`, validateDeclReferentialIntegrity(validationContext, entityNameValidator, referenceValidator, [], entity, [], instance.content)));
243
+ }
244
+ return [];
245
+ }
246
+ default:
247
+ return assertExhaustive(entity);
248
+ }
249
+ })
219
250
  .toSorted((a, b) => a.message.localeCompare(b.message));
220
251
  if (referenceErrors.length > 0) {
221
252
  const errorCount = countErrors(referenceErrors);
@@ -232,7 +263,7 @@ export class TSONDB {
232
263
  }
233
264
  debug("Checking unique constraints ...");
234
265
  const instanceOverviewsByEntityName = getAllInstanceOverviewsByEntityName(getEntity, data, this.#locales);
235
- const uniqueConstraintResult = checkUniqueConstraintsForAllEntities(data, changedEntities ?? onlyEntities, instanceOverviewsByEntityName);
266
+ const uniqueConstraintResult = checkUniqueConstraintsForAllEntities(data, (changedEntities ?? onlyEntities).filter(e => isEntityDecl(e)), instanceOverviewsByEntityName);
236
267
  if (isError(uniqueConstraintResult)) {
237
268
  const errorCount = countError(uniqueConstraintResult.error);
238
269
  debug(`${errorCount.toString()} unique constraint violation${errorCount === 1 ? "" : "s"} found`);
@@ -286,6 +317,11 @@ export class TSONDB {
286
317
  const entity = this.#schema.getEntity(entityName);
287
318
  await writeInstance(this.#dataRootPath, entity, instance.id, instance.content, this.#formatterOptions);
288
319
  }, true);
320
+ await this.#data.forEachSingletonInstance(async (entityName, instance) => {
321
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
322
+ const entity = this.#schema.getSingletonEntity(entityName);
323
+ await writeSingletonInstance(this.#dataRootPath, entity, instance.content, this.#formatterOptions);
324
+ }, true);
289
325
  debug("All data is formatted");
290
326
  }
291
327
  /**
@@ -306,6 +342,17 @@ export class TSONDB {
306
342
  }
307
343
  total++;
308
344
  }, true);
345
+ await this.#data.forEachSingletonInstance(async (entityName, instance) => {
346
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
347
+ const entity = this.#schema.getSingletonEntity(entityName);
348
+ const instanceOnDisk = await readSingletonInstance(this.#dataRootPath, entity);
349
+ const currentFormatted = formatInstance(entity, instance.content, this.#formatterOptions);
350
+ if (instanceOnDisk !== currentFormatted) {
351
+ console.log(`Singleton instance of entity ${styleText("cyan", entityName)} is not formatted correctly.`);
352
+ counter++;
353
+ }
354
+ total++;
355
+ }, true);
309
356
  if (counter === 0) {
310
357
  console.log(styleText("green", `All ${total.toFixed()} instance${total === 1 ? "" : "s"} are formatted correctly`));
311
358
  return true;
@@ -342,9 +389,11 @@ export class TSONDB {
342
389
  this.#locked = true;
343
390
  try {
344
391
  const getEntity = this.#schema.getEntity.bind(this.#schema);
392
+ const getSingletonEntity = this.#schema.getSingletonEntity.bind(this.#schema);
345
393
  const [txn, res] = fn(new Transaction({
346
394
  data: this.#data,
347
395
  getEntity,
396
+ getSingletonEntity,
348
397
  referencesToInstances: this.#referencesToInstances,
349
398
  validate: this.#strucurallyValidateInstance.bind(this),
350
399
  localeEntity: this.#schema.localeEntity,
@@ -426,6 +475,40 @@ export class TSONDB {
426
475
  txn.deleteInstance(this.#schema.getEntity(entityName), id));
427
476
  return res.content;
428
477
  }
478
+ /**
479
+ * Creates a new singleton instance in the database.
480
+ *
481
+ * If the instance is of the locale entity, a locale identifier must be provided as `id`.
482
+ * @throws {Error} when the instance could not be created
483
+ */
484
+ async createSingletonInstance(entityName, content) {
485
+ const res = await this.runTransaction(txn =>
486
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
487
+ txn.createSingletonInstance(this.#schema.getSingletonEntity(entityName), content));
488
+ return res;
489
+ }
490
+ /**
491
+ * Updates a singleton instance in the database.
492
+ *
493
+ * @throws {Error} when the instance could not be updated
494
+ */
495
+ async updateSingletonInstance(entityName, content) {
496
+ const res = await this.runTransaction(txn =>
497
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
498
+ txn.updateSingletonInstance(this.#schema.getSingletonEntity(entityName), content));
499
+ return res;
500
+ }
501
+ /**
502
+ * Deletes a singleton instance in the database.
503
+ *
504
+ * @throws {Error} when the instance could not be deleted
505
+ */
506
+ async deleteSingletonInstance(entityName) {
507
+ const res = await this.runTransaction(txn =>
508
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
509
+ txn.deleteSingletonInstance(this.#schema.getSingletonEntity(entityName)));
510
+ return res.content;
511
+ }
429
512
  /**
430
513
  * Retrieves an instance of the specified entity by its identifier.
431
514
  */
@@ -466,12 +549,30 @@ export class TSONDB {
466
549
  }
467
550
  return getInstanceOverviewsByEntityName(this.getInstanceContainerOfEntityById.bind(this), this.getAllChildInstanceContainersForParent.bind(this), this.#schema.getEntity.bind(this.#schema), normEntity, this.getAllInstanceContainersOfEntity(normEntity.name), this.#locales);
468
551
  }
552
+ /**
553
+ * Retrieves an instance of the specified entity by its identifier.
554
+ */
555
+ getSingletonInstanceOfEntity(entityName) {
556
+ return this.#data.getSingletonInstanceOfEntity(entityName);
557
+ }
558
+ /**
559
+ * Retrieves the instance container of the specified entity by its identifier.
560
+ */
561
+ getSingletonInstanceContainerOfEntity(entityName) {
562
+ return this.#data.getSingletonInstanceContainerOfEntity(entityName);
563
+ }
469
564
  /**
470
565
  * Counts the number of instances registered for the specified entity.
471
566
  */
472
567
  countInstancesOfEntity(entityName) {
473
568
  return this.#data.countInstancesOfEntity(entityName);
474
569
  }
570
+ /**
571
+ * Returns whether the singleton entity has an instance.
572
+ */
573
+ hasInstanceOfSingletonEntity(entityName) {
574
+ return this.#data.hasInstanceOfSingletonEntity(entityName);
575
+ }
475
576
  /**
476
577
  * Retrieves overviews for all instances in the database, grouped by entity name.
477
578
  */
@@ -156,6 +156,11 @@ const renderEntityDecl = (options, decl) => ({
156
156
  deprecated: decl.isDeprecated,
157
157
  ...renderType(options, addEphemeralUUIDToType(decl)),
158
158
  });
159
+ const renderSingletonEntityDecl = (options, decl) => ({
160
+ description: decl.comment,
161
+ deprecated: decl.isDeprecated,
162
+ ...renderType(options, decl.type.value),
163
+ });
159
164
  const renderEnumDecl = (options, decl) => ({
160
165
  description: decl.comment,
161
166
  deprecated: decl.isDeprecated,
@@ -170,6 +175,8 @@ const renderDecl = (options, decl) => {
170
175
  switch (decl.kind) {
171
176
  case NodeKind.EntityDecl:
172
177
  return renderEntityDecl({ ...options, parentDecl: decl }, decl);
178
+ case NodeKind.SingletonEntityDecl:
179
+ return renderSingletonEntityDecl({ ...options, parentDecl: decl }, decl);
173
180
  case NodeKind.EnumDecl:
174
181
  return renderEnumDecl({ ...options, parentDecl: decl }, decl);
175
182
  case NodeKind.TypeAliasDecl:
@@ -8,6 +8,10 @@ export type TypeScriptRendererOptions = {
8
8
  * If `true` or a string, generates an object type with all names of the entities as the key and their corresponding generated type as their respective value. Uses `EntityMap` as a default name when using `true` or the string if set to a string.
9
9
  */
10
10
  entityMap: boolean | string;
11
+ /**
12
+ * If `true` or a string, generates an object type with all names of the singleton entities as the key and their corresponding generated type as their respective value. Uses `SingletonEntityMap` as a default name when using `true` or the string if set to a string.
13
+ */
14
+ singletonEntityMap: boolean | string;
11
15
  /**
12
16
  * If `true` or a string, generates an object type with all names of entities with parent references as the key and their corresponding generated type as well as their parent reference key as their respective value. Uses `ChildEntityMap` as a default name when using `true` or the string if set to a string.
13
17
  */
@@ -9,6 +9,7 @@ import { extractParameterTypeNamesFromMessage, mapParameterTypeNames, } from "..
9
9
  import { asDecl } from "../../schema/dsl/declarations/Decl.js";
10
10
  import { addEphemeralUUIDToType, createEntityIdentifierTypeAsDecl, isEntityDecl, isEntityDeclWithParentReference, } from "../../schema/dsl/declarations/EntityDecl.js";
11
11
  import { isEnumDecl } from "../../schema/dsl/declarations/EnumDecl.js";
12
+ import { isSingletonEntityDecl, } from "../../schema/dsl/declarations/SingletonEntityDecl.js";
12
13
  import { isTypeAliasDecl, TypeAliasDecl } from "../../schema/dsl/declarations/TypeAliasDecl.js";
13
14
  import { isNestedEntityMapType } from "../../schema/dsl/types/NestedEntityMapType.js";
14
15
  import { isObjectType } from "../../schema/dsl/types/ObjectType.js";
@@ -148,6 +149,7 @@ const renderType = (options, type) => {
148
149
  }
149
150
  };
150
151
  const renderEntityDecl = (options, decl) => syntax `${renderDocumentation(decl.comment, decl.isDeprecated)}export ${options.objectTypeKeyword} ${decl.name} ${options.objectTypeKeyword === "type" ? "= " : ""}${renderType(options, options.addIdentifierToEntities ? addEphemeralUUIDToType(decl) : decl.type.value)}`;
152
+ const renderSingletonEntityDecl = (options, decl) => syntax `${renderDocumentation(decl.comment, decl.isDeprecated)}export ${options.objectTypeKeyword} ${decl.name} ${options.objectTypeKeyword === "type" ? "= " : ""}${renderType(options, decl.type.value)}`;
151
153
  const renderEnumDecl = (options, decl) => syntax `${renderDocumentation(decl.comment, decl.isDeprecated)}export type ${decl.name}${renderTypeParameters(options, decl.parameters)} =${renderEnumType(options, decl.type.value)}`;
152
154
  const renderTypeAliasDecl = (options, decl) => isObjectType(decl.type.value)
153
155
  ? syntax `${renderDocumentation(decl.comment, decl.isDeprecated)}export ${options.objectTypeKeyword} ${decl.name}${renderTypeParameters(options, decl.parameters)} ${options.objectTypeKeyword === "type" ? "= " : ""}${renderType(options, decl.type.value)}`
@@ -156,6 +158,8 @@ const renderDecl = (options, decl) => {
156
158
  switch (decl.kind) {
157
159
  case NodeKind.EntityDecl:
158
160
  return renderEntityDecl(options, decl);
161
+ case NodeKind.SingletonEntityDecl:
162
+ return renderSingletonEntityDecl(options, decl);
159
163
  case NodeKind.EnumDecl:
160
164
  return renderEnumDecl(options, decl);
161
165
  case NodeKind.TypeAliasDecl:
@@ -193,6 +197,7 @@ const renderMapHelperType = (options, declarations, key, defaultName, filterFn,
193
197
  : "";
194
198
  };
195
199
  const renderEntityMapType = (options, declarations) => renderMapHelperType(options, declarations, "entityMap", "EntityMap", isEntityDecl);
200
+ const renderSingletonEntityMapType = (options, declarations) => renderMapHelperType(options, declarations, "singletonEntityMap", "SingletonEntityMap", isSingletonEntityDecl);
196
201
  const renderChildEntityMapType = (options, declarations) => renderMapHelperType(options, declarations, "childEntityMap", "ChildEntityMap", decl => isEntityDecl(decl) && isEntityDeclWithParentReference(decl), decl => `${decl.name}: [${decl.name}, "${decl.parentReferenceKey}", ${decl.name}["${decl.parentReferenceKey}"]]`);
197
202
  const renderEnumMapType = (options, declarations) => renderMapHelperType(options, declarations, "enumMap", "EnumMap", isEnumDecl);
198
203
  const renderTypeAliasMapType = (options, declarations) => renderMapHelperType(options, declarations, "typeAliasMap", "TypeAliasMap", isTypeAliasDecl);
@@ -255,6 +260,7 @@ export const render = (options = defaultOptions, declarations) => {
255
260
  return undefined;
256
261
  }, declarations));
257
262
  return (renderEntityMapType(finalOptions, declarations) +
263
+ renderSingletonEntityMapType(finalOptions, declarations) +
258
264
  renderChildEntityMapType(finalOptions, declarations) +
259
265
  renderEnumMapType(finalOptions, declarations) +
260
266
  renderTypeAliasMapType(finalOptions, declarations) +
@@ -5,14 +5,15 @@ import type { EnumCaseDecl } from "../types/EnumType.ts";
5
5
  import type { Type } from "../types/Type.ts";
6
6
  import type { EntityDecl } from "./EntityDecl.ts";
7
7
  import type { EnumDecl } from "./EnumDecl.ts";
8
+ import { type SingletonEntityDecl } from "./SingletonEntityDecl.ts";
8
9
  import type { TypeAliasDecl } from "./TypeAliasDecl.ts";
9
10
  export type TypeArguments<Params extends TypeParameter[]> = {
10
11
  [K in keyof Params]: Params[K] extends TypeParameter<string, infer T> ? T : Type;
11
12
  };
12
13
  export declare const getParameterNames: (decl: Decl) => string[];
13
14
  export declare const getTypeArgumentsRecord: <Params extends TypeParameter[]>(decl: DeclP<Params>, args: TypeArguments<Params>) => Record<string, Type>;
14
- export type Decl<Name extends string = string> = EntityDecl<Name> | EnumDecl<Name> | TypeAliasDecl<Name>;
15
- export type DeclP<Params extends TypeParameter[] = TypeParameter[]> = EntityDecl | EnumDecl<string, Record<string, EnumCaseDecl>, Params> | TypeAliasDecl<string, Type, Params>;
15
+ export type Decl<Name extends string = string> = EntityDecl<Name> | SingletonEntityDecl<Name> | EnumDecl<Name> | TypeAliasDecl<Name>;
16
+ export type DeclP<Params extends TypeParameter[] = TypeParameter[]> = EntityDecl | SingletonEntityDecl | EnumDecl<string, Record<string, EnumCaseDecl>, Params> | TypeAliasDecl<string, Type, Params>;
16
17
  export type IncludableDeclP<Params extends TypeParameter[] = TypeParameter[]> = EnumDecl<string, Record<string, EnumCaseDecl>, Params> | TypeAliasDecl<string, Type, Params>;
17
18
  export type SecondaryDecl = EnumDecl | TypeAliasDecl;
18
19
  export declare const isDecl: (node: Node) => node is Decl;
@@ -2,12 +2,13 @@ import { walkTypeNodeTree } from "../../helpers.js";
2
2
  import { resolveTypeArguments } from "../../treeOperations/typeResolution.js";
3
3
  import { isEntityDecl } from "./EntityDecl.js";
4
4
  import { isEnumDecl } from "./EnumDecl.js";
5
+ import { isSingletonEntityDecl } from "./SingletonEntityDecl.js";
5
6
  import { isTypeAliasDecl } from "./TypeAliasDecl.js";
6
7
  export const getParameterNames = (decl) => decl.parameters.map(param => param.name);
7
8
  export const getTypeArgumentsRecord = (decl, args) => Object.fromEntries(
8
9
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
9
10
  args.slice(0, decl.parameters.length).map((arg, i) => [decl.parameters[i].name, arg]));
10
- export const isDecl = (node) => isEntityDecl(node) || isEnumDecl(node) || isTypeAliasDecl(node);
11
+ export const isDecl = (node) => isEntityDecl(node) || isSingletonEntityDecl(node) || isEnumDecl(node) || isTypeAliasDecl(node);
11
12
  export const asDecl = (node) => node && isDecl(node) ? node : undefined;
12
13
  const declNamePattern = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
13
14
  export const validateDeclName = (name) => {
@@ -0,0 +1,35 @@
1
+ import { Lazy } from "@elyukai/utils/lazy";
2
+ import { NodeKind } from "../../../../shared/schema/Node.ts";
3
+ import type { SortOrder } from "../../../../shared/schema/utils/sortOrder.ts";
4
+ import type { CustomConstraint, TypedCustomConstraint } from "../../../utils/customConstraints.ts";
5
+ import type { Node } from "../index.ts";
6
+ import type { MemberDecl, ObjectType } from "../types/ObjectType.ts";
7
+ import type { BaseDecl } from "./Decl.ts";
8
+ type TConstraint = Record<string, MemberDecl>;
9
+ export interface SingletonEntityDecl<Name extends string = string, T extends TConstraint = TConstraint> extends BaseDecl<Name, []> {
10
+ kind: NodeKind["SingletonEntityDecl"];
11
+ /**
12
+ * Changes the appearance of the entity’s name in singular form.
13
+ */
14
+ displayName?: string;
15
+ type: Lazy<ObjectType<T>>;
16
+ isDeprecated?: boolean;
17
+ customConstraints?: CustomConstraint;
18
+ /**
19
+ * The order in which instances of an entity are sorted in the editor. This affects entity details pages and reference options.
20
+ */
21
+ sortOrder?: SortOrder;
22
+ }
23
+ export declare const SingletonEntityDecl: <Name extends string, T extends TConstraint>(sourceUrl: string, options: {
24
+ name: Name;
25
+ /**
26
+ * Changes the appearance of the entity’s name in singular form.
27
+ */
28
+ displayName?: string;
29
+ comment?: string;
30
+ type: () => ObjectType<T>;
31
+ isDeprecated?: boolean;
32
+ customConstraints?: TypedCustomConstraint<Name>;
33
+ }) => SingletonEntityDecl<Name, T>;
34
+ export { SingletonEntityDecl as SingletonEntity };
35
+ export declare const isSingletonEntityDecl: (node: Node) => node is SingletonEntityDecl;
@@ -0,0 +1,23 @@
1
+ import { Lazy } from "@elyukai/utils/lazy";
2
+ import { NodeKind } from "../../../../shared/schema/Node.js";
3
+ import { validateDeclName } from "./Decl.js";
4
+ export const SingletonEntityDecl = (sourceUrl, options) => {
5
+ validateDeclName(options.name);
6
+ return {
7
+ ...options,
8
+ kind: NodeKind.SingletonEntityDecl,
9
+ sourceUrl,
10
+ parameters: [],
11
+ type: Lazy.of(() => {
12
+ const type = options.type();
13
+ Object.keys(type.properties).forEach(key => {
14
+ if (key === "id") {
15
+ throw new TypeError(`Invalid object key "${key}" for entity "${options.name}". The key "id" is reserved for the entity identifier.`);
16
+ }
17
+ });
18
+ return type;
19
+ }),
20
+ };
21
+ };
22
+ export { SingletonEntityDecl as SingletonEntity };
23
+ export const isSingletonEntityDecl = (node) => node.kind === NodeKind.SingletonEntityDecl;
@@ -5,6 +5,7 @@ export { Param, TypeParameter } from "./TypeParameter.ts";
5
5
  export { type Decl, type DeclP, type IncludableDeclP, type SecondaryDecl, } from "./declarations/Decl.ts";
6
6
  export { Entity, EntityDecl, type EntityDeclWithParentReference, type EntityDisplayName, } from "./declarations/EntityDecl.ts";
7
7
  export { Enum, EnumDecl, GenEnum, GenEnumDecl } from "./declarations/EnumDecl.ts";
8
+ export { SingletonEntity, SingletonEntityDecl } from "./declarations/SingletonEntityDecl.ts";
8
9
  export { GenTypeAlias, GenTypeAliasDecl, TypeAlias, TypeAliasDecl, } from "./declarations/TypeAliasDecl.ts";
9
10
  export { Array, ArrayType } from "./types/ArrayType.ts";
10
11
  export { Boolean, BooleanType } from "./types/BooleanType.ts";
@@ -2,6 +2,7 @@ export { Param, TypeParameter } from "./TypeParameter.js";
2
2
  export {} from "./declarations/Decl.js";
3
3
  export { Entity, EntityDecl, } from "./declarations/EntityDecl.js";
4
4
  export { Enum, EnumDecl, GenEnum, GenEnumDecl } from "./declarations/EnumDecl.js";
5
+ export { SingletonEntity, SingletonEntityDecl } from "./declarations/SingletonEntityDecl.js";
5
6
  export { GenTypeAlias, GenTypeAliasDecl, TypeAlias, TypeAliasDecl, } from "./declarations/TypeAliasDecl.js";
6
7
  export { Array, ArrayType } from "./types/ArrayType.js";
7
8
  export { Boolean, BooleanType } from "./types/BooleanType.js";
@@ -8,6 +8,7 @@ import type { DisplayNameResult } from "../../shared/utils/displayName.ts";
8
8
  import type { InstanceContainer, InstanceContainerOverview, InstanceContent } from "../../shared/utils/instances.ts";
9
9
  import type { DeclarationName, DefaultTSONDBTypes } from "../index.ts";
10
10
  import type { EntityDecl } from "./dsl/declarations/EntityDecl.ts";
11
+ import type { SingletonEntityDecl } from "./dsl/declarations/SingletonEntityDecl.ts";
11
12
  export interface Register {
12
13
  }
13
14
  export type AnyEntityMap = Record<string, InstanceContent>;
@@ -19,6 +20,15 @@ export type RegisteredEntity<Name extends string, T = Register> = T extends {
19
20
  [K in Name]: InstanceContent;
20
21
  };
21
22
  } ? T["entityMap"][Name] : InstanceContent;
23
+ export type AnySingletonEntityMap = Record<string, InstanceContent>;
24
+ export type RegisteredSingletonEntityMap<T = Register> = T extends {
25
+ singletonEntityMap: AnySingletonEntityMap;
26
+ } ? T["singletonEntityMap"] : AnySingletonEntityMap;
27
+ export type RegisteredSingletonEntity<Name extends string, T = Register> = T extends {
28
+ singletonEntityMap: {
29
+ [K in Name]: InstanceContent;
30
+ };
31
+ } ? T["singletonEntityMap"][Name] : InstanceContent;
22
32
  export type AnyChildEntityMap = Record<string, [
23
33
  content: InstanceContent,
24
34
  parentKey: string,
@@ -133,6 +143,14 @@ export type GetDisplayNameAndId<T extends AnyEntityMap = RegisteredEntityMap> =
133
143
  * A function that retrieves an entity declaration by its name.
134
144
  */
135
145
  export type GetEntityByName<T extends AnyEntityMap = RegisteredEntityMap> = <E extends Extract<keyof T, string> = Extract<keyof T, string>>(name: E) => EntityDecl<E> | undefined;
146
+ /**
147
+ * A function that retrieves an entity declaration by its name.
148
+ */
149
+ export type GetSingletonEntityByName<T extends AnySingletonEntityMap = RegisteredSingletonEntityMap> = <E extends Extract<keyof T, string> = Extract<keyof T, string>>(name: E) => SingletonEntityDecl<E> | undefined;
150
+ /**
151
+ * A function that retrieves the singleton instance by its entity if available.
152
+ */
153
+ export type GetSingletonInstance<T extends AnySingletonEntityMap = RegisteredSingletonEntityMap> = <E extends Extract<keyof T, string> = Extract<keyof T, string>>(entity: E) => T[E] | undefined;
136
154
  /**
137
155
  * A type guard function that checks if a given string is a valid declaration name.
138
156
  */
@@ -1,6 +1,7 @@
1
1
  export { isDecl, isDeclWithoutTypeParameters } from "./dsl/declarations/Decl.ts";
2
2
  export { isEntityDecl, isEntityDeclWithParentReference } from "./dsl/declarations/EntityDecl.ts";
3
3
  export { isEnumDecl } from "./dsl/declarations/EnumDecl.ts";
4
+ export { isSingletonEntityDecl } from "./dsl/declarations/SingletonEntityDecl.ts";
4
5
  export { isTypeAliasDecl } from "./dsl/declarations/TypeAliasDecl.ts";
5
6
  export { isArrayType } from "./dsl/types/ArrayType.ts";
6
7
  export { isBooleanType } from "./dsl/types/BooleanType.ts";
@@ -1,6 +1,7 @@
1
1
  export { isDecl, isDeclWithoutTypeParameters } from "./dsl/declarations/Decl.js";
2
2
  export { isEntityDecl, isEntityDeclWithParentReference } from "./dsl/declarations/EntityDecl.js";
3
3
  export { isEnumDecl } from "./dsl/declarations/EnumDecl.js";
4
+ export { isSingletonEntityDecl } from "./dsl/declarations/SingletonEntityDecl.js";
4
5
  export { isTypeAliasDecl } from "./dsl/declarations/TypeAliasDecl.js";
5
6
  export { isArrayType } from "./dsl/types/ArrayType.js";
6
7
  export { isBooleanType } from "./dsl/types/BooleanType.js";
@@ -12,6 +12,7 @@ export const reduceNodes = (reducer, nodes, options = {}) => {
12
12
  const reduceNodeTree = (parentNodes, node, collectedResults, reducedDecls) => {
13
13
  switch (node.kind) {
14
14
  case NodeKind.EntityDecl:
15
+ case NodeKind.SingletonEntityDecl:
15
16
  case NodeKind.EnumDecl:
16
17
  case NodeKind.TypeAliasDecl:
17
18
  return reduceNodeTree([node], node.type.value, reducer(parentNodes, node, collectedResults), [...reducedDecls, node]);