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
@@ -33,6 +33,27 @@ const checkDeleteInstancePossible = (referencesToInstances, instanceId) => {
33
33
  throw new HTTPError(400, "Cannot delete instance that is referenced by other instances");
34
34
  }
35
35
  };
36
+ /**
37
+ * @throws {HTTPError}
38
+ */
39
+ const checkCreateSingletonInstancePossible = (validate, databaseInMemory, entity, instanceContent) => {
40
+ if (databaseInMemory.hasInstanceOfSingletonEntity(entity.name)) {
41
+ throw new HTTPError(400, `Duplicate instance for singleton entity`);
42
+ }
43
+ checkUpdateSingletonInstancePossible(validate, entity, instanceContent);
44
+ };
45
+ /**
46
+ * @throws {HTTPError}
47
+ */
48
+ const checkUpdateSingletonInstancePossible = (validate, entity, instanceContent) => {
49
+ const validationErrors = validate(entity, instanceContent);
50
+ if (validationErrors.length > 0) {
51
+ throw new HTTPError(400, validationErrors.map(getErrorMessageForDisplay).join("\n\n"));
52
+ }
53
+ };
54
+ const checkDeleteSingletonInstancePossible = () => {
55
+ // cannot be referenced and does not have any integration into other areas, so no checks needed
56
+ };
36
57
  export class Transaction {
37
58
  #values;
38
59
  constructor(values) {
@@ -115,6 +136,78 @@ export class Transaction {
115
136
  { id: instanceId, content: oldInstance },
116
137
  ];
117
138
  }
139
+ createSingletonInstance(entity, instanceContent) {
140
+ const { data, steps, referencesToInstances, getEntity, validate } = this.#values;
141
+ checkCreateSingletonInstancePossible(validate, data, entity, instanceContent);
142
+ const [updatedDb] = data.setInstanceContainerOfSingletonEntity(entity.name, {
143
+ content: instanceContent,
144
+ });
145
+ const updatedRefs = updateReferencesToInstances(getEntity, referencesToInstances, entity.name, entity.name, undefined, instanceContent);
146
+ const step = {
147
+ kind: "createSingleton",
148
+ entity,
149
+ instanceContent,
150
+ };
151
+ return [
152
+ new Transaction({
153
+ ...this.#values,
154
+ data: updatedDb,
155
+ steps: [...steps, step],
156
+ referencesToInstances: updatedRefs,
157
+ }),
158
+ { content: instanceContent },
159
+ ];
160
+ }
161
+ updateSingletonInstance(entity, instanceContent) {
162
+ const { data, steps, referencesToInstances, getEntity, validate } = this.#values;
163
+ checkUpdateSingletonInstancePossible(validate, entity, instanceContent);
164
+ const [updatedDb, oldInstance] = data.setInstanceContainerOfSingletonEntity(entity.name, {
165
+ content: instanceContent,
166
+ });
167
+ if (oldInstance === undefined) {
168
+ throw new HTTPError(400, `Singleton instance of entity "${entity.name}" did not yet exist`);
169
+ }
170
+ const updatedRefs = updateReferencesToInstances(getEntity, referencesToInstances, entity.name, entity.name, oldInstance, instanceContent);
171
+ const step = {
172
+ kind: "updateSingleton",
173
+ entity,
174
+ instanceContent,
175
+ oldInstance,
176
+ };
177
+ return [
178
+ new Transaction({
179
+ ...this.#values,
180
+ data: updatedDb,
181
+ steps: [...steps, step],
182
+ referencesToInstances: updatedRefs,
183
+ }),
184
+ { content: instanceContent },
185
+ ];
186
+ }
187
+ deleteSingletonInstance(entity) {
188
+ const { data, steps, referencesToInstances, getEntity } = this.#values;
189
+ checkDeleteSingletonInstancePossible();
190
+ const [updatedDb, oldInstance] = data.deleteInstanceContainerOfSingletonEntity(entity.name);
191
+ if (oldInstance === undefined) {
192
+ // instance did not exist
193
+ throw new Error("Instance did not exist");
194
+ }
195
+ const updatedRefs = updateReferencesToInstances(getEntity, referencesToInstances, entity.name, entity.name, oldInstance, undefined);
196
+ const step = {
197
+ kind: "deleteSingleton",
198
+ entity,
199
+ oldInstance,
200
+ };
201
+ return [
202
+ new Transaction({
203
+ ...this.#values,
204
+ data: updatedDb,
205
+ steps: [...steps, step],
206
+ referencesToInstances: updatedRefs,
207
+ }),
208
+ { content: oldInstance },
209
+ ];
210
+ }
118
211
  /**
119
212
  * @package
120
213
  */
@@ -1,16 +1,17 @@
1
1
  import { type Result } from "@elyukai/utils/result";
2
2
  import type { InstanceContent } from "../../shared/utils/instances.ts";
3
- import type { DefaultTSONDBTypes, EntityName } from "../index.ts";
4
- import { type EntityDecl } from "../schema/dsl/index.ts";
5
- import type { AnyChildEntityMap, AnyEntityMap, GetAllChildInstanceContainersForParent, GetAllInstanceContainers, GetAllInstances, GetDisplayName, GetDisplayNameAndId, GetEntityByName, GetInstanceById, GetInstanceOverviewOfEntityById, RegisteredChildEntityMap, RegisteredEntity, RegisteredEntityMap, RegisteredEnumOrTypeAlias } from "../schema/generatedTypeHelpers.ts";
3
+ import type { DefaultTSONDBTypes, EntityName, SingletonEntityName } from "../index.ts";
4
+ import { type EntityDecl, type SingletonEntityDecl } from "../schema/dsl/index.ts";
5
+ import type { AnyChildEntityMap, AnyEntityMap, AnySingletonEntityMap, GetAllChildInstanceContainersForParent, GetAllInstanceContainers, GetAllInstances, GetDisplayName, GetDisplayNameAndId, GetEntityByName, GetInstanceById, GetInstanceOverviewOfEntityById, GetSingletonInstance, RegisteredChildEntityMap, RegisteredEntity, RegisteredEntityMap, RegisteredEnumOrTypeAlias, RegisteredSingletonEntityMap } from "../schema/generatedTypeHelpers.ts";
6
6
  import type { DatabaseInMemory } from "./databaseInMemory.ts";
7
- export type CustomConstraintHelpers<EM extends AnyEntityMap = RegisteredEntityMap, CEM extends AnyChildEntityMap = RegisteredChildEntityMap> = {
7
+ export type CustomConstraintHelpers<EM extends AnyEntityMap = RegisteredEntityMap, CEM extends AnyChildEntityMap = RegisteredChildEntityMap, SEM extends AnySingletonEntityMap = RegisteredSingletonEntityMap> = {
8
8
  getInstanceById: GetInstanceById<EM>;
9
9
  getAllInstances: GetAllInstances<EM>;
10
10
  getAllInstanceContainers: GetAllInstanceContainers<EM>;
11
11
  getAllChildInstancesForParent: GetAllChildInstanceContainersForParent<CEM>;
12
12
  getDisplayName: GetDisplayName<EM>;
13
13
  getDisplayNameAndId: GetDisplayNameAndId<EM>;
14
+ getSingletonInstance: GetSingletonInstance<SEM>;
14
15
  };
15
16
  /**
16
17
  * A constraint that can be defined on an entity to enforce custom validation logic.
@@ -71,4 +72,4 @@ export type TypedNestedCustomConstraint<Name extends string, EM extends AnyEntit
71
72
  * `AggregateError`s for each entity if there are any violations of any custom
72
73
  * constraint.
73
74
  */
74
- export declare const checkCustomConstraintsForAllEntities: <T extends DefaultTSONDBTypes>(getDisplayName: GetDisplayName<T["entityMap"]>, getDisplayNameAndId: GetDisplayNameAndId<T["entityMap"]>, getInstanceOverviewOfEntityById: GetInstanceOverviewOfEntityById<T["entityMap"]>, getEntityByName: GetEntityByName<T["entityMap"]>, data: DatabaseInMemory<T["entityMap"]>, entities: EntityDecl<EntityName<T>>[]) => Result<void, AggregateError>;
75
+ export declare const checkCustomConstraintsForAllEntities: <T extends DefaultTSONDBTypes>(getDisplayName: GetDisplayName<T["entityMap"]>, getDisplayNameAndId: GetDisplayNameAndId<T["entityMap"]>, getInstanceOverviewOfEntityById: GetInstanceOverviewOfEntityById<T["entityMap"]>, getEntityByName: GetEntityByName<T["entityMap"]>, data: DatabaseInMemory<T["entityMap"], T["singletonEntityMap"]>, entities: (EntityDecl<EntityName<T>> | SingletonEntityDecl<SingletonEntityName<T>>)[]) => Result<void, AggregateError>;
@@ -1,6 +1,37 @@
1
1
  import { error, isError, mapError, ok } from "@elyukai/utils/result";
2
+ import { NodeKind } from "../../shared/schema/Node.js";
2
3
  import {} from "../schema/dsl/index.js";
3
- import { checkCustomConstraints } from "../schema/treeOperations/customConstraints.js";
4
+ import { checkCustomConstraintsInEntityDecl, checkCustomConstraintsInSingletonEntityDecl, } from "../schema/treeOperations/customConstraints.js";
5
+ const collectErrorsForEntity = (data, entity, getInstanceOverviewOfEntityById, helpers) => {
6
+ switch (entity.kind) {
7
+ case NodeKind.EntityDecl:
8
+ return data
9
+ .getAllInstanceContainersOfEntity(entity.name)
10
+ .map((instance) => [
11
+ instance,
12
+ checkCustomConstraintsInEntityDecl(entity, [instance.id, instance.content], helpers),
13
+ ])
14
+ .filter(([, violations]) => violations.length > 0)
15
+ .map(([instance, violations]) => {
16
+ const instanceOverview = getInstanceOverviewOfEntityById(entity.name, instance.id);
17
+ const name = instanceOverview
18
+ ? `"${instanceOverview.displayName}" (${instance.id})`
19
+ : instance.id;
20
+ return new AggregateError(violations.map(violation => new Error(violation)), `in instance ${name}`);
21
+ });
22
+ case NodeKind.SingletonEntityDecl: {
23
+ const instance = data.getSingletonInstanceContainerOfEntity(entity.name);
24
+ if (instance) {
25
+ return checkCustomConstraintsInSingletonEntityDecl(entity, instance.content, helpers);
26
+ }
27
+ else {
28
+ return [];
29
+ }
30
+ }
31
+ default:
32
+ return [];
33
+ }
34
+ };
4
35
  /**
5
36
  * Checks all custom constraints for all provided entities and their instances.
6
37
  *
@@ -16,22 +47,10 @@ export const checkCustomConstraintsForAllEntities = (getDisplayName, getDisplayN
16
47
  getAllChildInstancesForParent: data.getAllChildInstanceContainersForParent.bind(data, getEntityByName),
17
48
  getDisplayName,
18
49
  getDisplayNameAndId,
50
+ getSingletonInstance: data.getSingletonInstanceOfEntity.bind(data),
19
51
  };
20
52
  return mapError(entities.reduce((acc, entity) => {
21
- const errors = data
22
- .getAllInstanceContainersOfEntity(entity.name)
23
- .map((instance) => [
24
- instance,
25
- checkCustomConstraints(entity, [instance.id, instance.content], helpers),
26
- ])
27
- .filter(([, violations]) => violations.length > 0)
28
- .map(([instance, violations]) => {
29
- const instanceOverview = getInstanceOverviewOfEntityById(entity.name, instance.id);
30
- const name = instanceOverview
31
- ? `"${instanceOverview.displayName}" (${instance.id})`
32
- : instance.id;
33
- return new AggregateError(violations.map(violation => new Error(violation)), `in instance ${name}`);
34
- });
53
+ const errors = collectErrorsForEntity(data, entity, getInstanceOverviewOfEntityById, helpers);
35
54
  const aggregate = errors.length > 0 ? new AggregateError(errors, `in entity "${entity.name}"`) : undefined;
36
55
  if (isError(acc)) {
37
56
  if (aggregate) {
@@ -1,25 +1,37 @@
1
1
  import { Dictionary } from "@elyukai/utils/dictionary";
2
- import type { InstanceContainer, InstanceContent } from "../../shared/utils/instances.ts";
3
- import type { EntityDecl } from "../schema/dsl/index.ts";
4
- import { type AnyChildEntityMap, type AnyEntityMap, type GetEntityByName, type IdArgsVariant, type RegisteredEntityMap } from "../schema/generatedTypeHelpers.ts";
5
- export declare class DatabaseInMemory<EM extends AnyEntityMap = RegisteredEntityMap> {
2
+ import type { InstanceContainer, InstanceContent, SingletonInstanceContainer } from "../../shared/utils/instances.ts";
3
+ import type { EntityDecl, SingletonEntityDecl } from "../schema/dsl/index.ts";
4
+ import { type AnyChildEntityMap, type AnyEntityMap, type AnySingletonEntityMap, type GetEntityByName, type IdArgsVariant, type RegisteredEntityMap, type RegisteredSingletonEntityMap } from "../schema/generatedTypeHelpers.ts";
5
+ export declare class DatabaseInMemory<EM extends AnyEntityMap = RegisteredEntityMap, SEM extends AnySingletonEntityMap = RegisteredSingletonEntityMap> {
6
6
  #private;
7
7
  private constructor();
8
- static load<EM extends AnyEntityMap = RegisteredEntityMap>(dataRoot: string, entities: readonly EntityDecl<Extract<keyof EM, string>>[]): Promise<DatabaseInMemory<EM>>;
8
+ static load<EM extends AnyEntityMap = RegisteredEntityMap, SEM extends AnySingletonEntityMap = RegisteredSingletonEntityMap>(dataRoot: string, entities: readonly EntityDecl<Extract<keyof EM, string>>[], singletonEntities: readonly SingletonEntityDecl<Extract<keyof SEM, string>>[]): Promise<DatabaseInMemory<EM, SEM>>;
9
9
  getAllInstanceContainersOfEntity<E extends Extract<keyof EM, string>>(entityName: E): InstanceContainer<EM[E]>[];
10
10
  getAllInstancesOfEntity<E extends Extract<keyof EM, string>>(entityName: E): EM[E][];
11
11
  getAllInstances(): [entityName: Extract<keyof EM, string>, instances: InstanceContainer[]][];
12
+ getAllSingletonInstances(): [
13
+ entityName: Extract<keyof SEM, string>,
14
+ instance: SingletonInstanceContainer
15
+ ][];
12
16
  getInstanceContainerOfEntityById<E extends Extract<keyof EM, string>>(...args: IdArgsVariant<EM, E>): InstanceContainer<EM[E]> | undefined;
13
17
  getInstanceOfEntityById<E extends Extract<keyof EM, string>>(...args: IdArgsVariant<EM, E>): EM[E] | undefined;
18
+ getSingletonInstanceContainerOfEntity<E extends Extract<keyof SEM, string>>(entityName: E): SingletonInstanceContainer<SEM[E]> | undefined;
19
+ getSingletonInstanceOfEntity<E extends Extract<keyof SEM, string>>(entityName: E): SEM[E] | undefined;
14
20
  hasInstanceOfEntityById<E extends Extract<keyof EM, string>>(...args: IdArgsVariant<EM, E>): boolean;
21
+ hasInstanceOfSingletonEntity(entityName: Extract<keyof SEM, string>): boolean;
15
22
  forEachInstance(fn: (entityName: Extract<keyof EM, string>, instance: InstanceContainer) => Promise<void>, async: true): Promise<void>;
16
23
  forEachInstance(fn: (entityName: Extract<keyof EM, string>, instance: InstanceContainer) => void, async?: false): void;
24
+ forEachSingletonInstance(fn: (entityName: Extract<keyof SEM, string>, instance: SingletonInstanceContainer) => Promise<void>, async: true): Promise<void>;
25
+ forEachSingletonInstance(fn: (entityName: Extract<keyof SEM, string>, instance: SingletonInstanceContainer) => void, async?: false): void;
17
26
  countInstancesOfEntity(entityName: Extract<keyof EM, string>): number;
18
27
  get totalSize(): number;
19
28
  getAllChildInstanceContainersForParent: <CEM extends AnyChildEntityMap, CE extends Extract<keyof CEM, string>>(getEntityByName: GetEntityByName<EM>, childEntityName: CE, parentId: CEM[CE][2]) => InstanceContainer<CEM[CE][0]>[];
20
29
  getAllChildInstancesForParent: <CEM extends AnyChildEntityMap, CE extends Extract<keyof CEM, string>>(getEntityByName: GetEntityByName<EM>, childEntityName: CE, parentId: CEM[CE][2]) => CEM[CE][0][];
21
- setInstanceContainerOfEntityById(entityName: Extract<keyof EM, string>, instance: InstanceContainer): [DatabaseInMemory<EM>, oldInstance: InstanceContent | undefined];
22
- deleteInstanceContainerOfEntityById(entityName: Extract<keyof EM, string>, instanceId: string): [DatabaseInMemory<EM>, oldInstance: InstanceContent | undefined];
23
- map(fn: (instances: Dictionary<InstanceContainer>, entityName: Extract<keyof EM, string>) => Dictionary<InstanceContainer>): DatabaseInMemory<EM>;
30
+ setInstanceContainerOfEntityById(entityName: Extract<keyof EM, string>, instance: InstanceContainer): [DatabaseInMemory<EM, SEM>, oldInstance: InstanceContent | undefined];
31
+ setInstanceContainerOfSingletonEntity(entityName: Extract<keyof SEM, string>, instance: SingletonInstanceContainer): [DatabaseInMemory<EM, SEM>, oldInstance: InstanceContent | undefined];
32
+ deleteInstanceContainerOfEntityById(entityName: Extract<keyof EM, string>, instanceId: string): [DatabaseInMemory<EM, SEM>, oldInstance: InstanceContent | undefined];
33
+ deleteInstanceContainerOfSingletonEntity(entityName: Extract<keyof SEM, string>): [DatabaseInMemory<EM, SEM>, oldInstance: InstanceContent | undefined];
34
+ map(fn: (instances: Dictionary<InstanceContainer>, entityName: Extract<keyof EM, string>) => Dictionary<InstanceContainer>): DatabaseInMemory<EM, SEM>;
35
+ mapSingletons(fn: (instance: SingletonInstanceContainer, entityName: Extract<keyof SEM, string>) => SingletonInstanceContainer): DatabaseInMemory<EM, SEM>;
24
36
  getEntityNameOfInstanceId(id: string): Extract<keyof EM, string> | undefined;
25
37
  }
@@ -3,19 +3,22 @@ import { Dictionary } from "@elyukai/utils/dictionary";
3
3
  import { deepEqual } from "@elyukai/utils/equality";
4
4
  import { Lazy } from "@elyukai/utils/lazy";
5
5
  import child_process from "node:child_process";
6
- import { readdir, readFile } from "node:fs/promises";
6
+ import { readdir, readFile, stat } from "node:fs/promises";
7
7
  import { basename, extname, join } from "node:path";
8
8
  import { platform } from "node:process";
9
9
  import { promisify } from "node:util";
10
10
  import { normalizedIdArgs, } from "../schema/generatedTypeHelpers.js";
11
+ import { getPathToSingletonInstance, readSingletonInstance } from "./files.js";
11
12
  const exec = promisify(child_process.exec);
12
13
  const ulimit = platform === "win32" ? 2048 : Number.parseInt((await exec("ulimit -n")).stdout);
13
14
  export class DatabaseInMemory {
14
15
  #data;
15
- constructor(data) {
16
+ #singletonData;
17
+ constructor(data, singletonData) {
16
18
  this.#data = data;
19
+ this.#singletonData = singletonData;
17
20
  }
18
- static async load(dataRoot, entities) {
21
+ static async load(dataRoot, entities, singletonEntities) {
19
22
  return new DatabaseInMemory(Dictionary.fromEntries(await mapAsync(entities, async (entity) => {
20
23
  const entityDir = join(dataRoot, entity.name);
21
24
  const instanceFileNames = await readdir(entityDir);
@@ -38,7 +41,21 @@ export class DatabaseInMemory {
38
41
  }, ulimit);
39
42
  const instancesById = Dictionary.fromEntries(instances);
40
43
  return [entity.name, instancesById];
41
- }, 1)));
44
+ }, 1)), Dictionary.fromEntries((await mapAsync(singletonEntities, async (singletonEntity) => {
45
+ const pathToSingletonFile = getPathToSingletonInstance(dataRoot, singletonEntity.name);
46
+ try {
47
+ await stat(pathToSingletonFile);
48
+ return [
49
+ singletonEntity.name,
50
+ {
51
+ content: JSON.parse(await readSingletonInstance(dataRoot, singletonEntity)),
52
+ },
53
+ ];
54
+ }
55
+ catch {
56
+ return [singletonEntity.name, undefined];
57
+ }
58
+ }, 1)).filter((pair) => pair[1] !== undefined)));
42
59
  }
43
60
  getAllInstanceContainersOfEntity(entityName) {
44
61
  return (this.#data.getMap(entityName, instances => instances.values()) ?? []);
@@ -49,6 +66,9 @@ export class DatabaseInMemory {
49
66
  getAllInstances() {
50
67
  return this.#data.entries().map(([entityName, instances]) => [entityName, instances.values()]);
51
68
  }
69
+ getAllSingletonInstances() {
70
+ return this.#singletonData.entries();
71
+ }
52
72
  getInstanceContainerOfEntityById(...args) {
53
73
  const { entityName, id } = normalizedIdArgs(args);
54
74
  return this.#data.getMap(entityName, instances => instances.get(id));
@@ -56,10 +76,19 @@ export class DatabaseInMemory {
56
76
  getInstanceOfEntityById(...args) {
57
77
  return this.getInstanceContainerOfEntityById(...args)?.content;
58
78
  }
79
+ getSingletonInstanceContainerOfEntity(entityName) {
80
+ return this.#singletonData.get(entityName);
81
+ }
82
+ getSingletonInstanceOfEntity(entityName) {
83
+ return this.getSingletonInstanceContainerOfEntity(entityName)?.content;
84
+ }
59
85
  hasInstanceOfEntityById(...args) {
60
86
  const { entityName, id } = normalizedIdArgs(args);
61
87
  return this.#data.getMap(entityName, instances => instances.has(id)) ?? false;
62
88
  }
89
+ hasInstanceOfSingletonEntity(entityName) {
90
+ return this.#singletonData.get(entityName) !== undefined;
91
+ }
63
92
  forEachInstance(...args) {
64
93
  const [fn, async] = args;
65
94
  if (async) {
@@ -73,10 +102,21 @@ export class DatabaseInMemory {
73
102
  });
74
103
  }
75
104
  }
105
+ forEachSingletonInstance(...args) {
106
+ const [fn, async] = args;
107
+ if (async) {
108
+ return this.#singletonData.forEach((instance, entityName) => fn(entityName, instance), true);
109
+ }
110
+ else {
111
+ this.#singletonData.forEach((instance, entityName) => {
112
+ fn(entityName, instance);
113
+ });
114
+ }
115
+ }
76
116
  countInstancesOfEntity(entityName) {
77
117
  return this.#data.getMap(entityName, instances => instances.size) ?? 0;
78
118
  }
79
- #totalSize = Lazy.of(() => this.#data.reduce((total, instances) => total + instances.size, 0));
119
+ #totalSize = Lazy.of(() => this.#data.reduce((total, instances) => total + instances.size, 0) + this.#singletonData.size);
80
120
  get totalSize() {
81
121
  return this.#totalSize.value;
82
122
  }
@@ -94,8 +134,14 @@ export class DatabaseInMemory {
94
134
  setInstanceContainerOfEntityById(entityName, instance) {
95
135
  const instances = this.#data.get(entityName) ?? Dictionary.empty;
96
136
  return [
97
- new DatabaseInMemory(this.#data.set(entityName, instances.set(instance.id, instance))),
98
- instances.get(instance.id),
137
+ new DatabaseInMemory(this.#data.set(entityName, instances.set(instance.id, instance)), this.#singletonData),
138
+ instances.get(instance.id)?.content,
139
+ ];
140
+ }
141
+ setInstanceContainerOfSingletonEntity(entityName, instance) {
142
+ return [
143
+ new DatabaseInMemory(this.#data, this.#singletonData.set(entityName, instance)),
144
+ this.#singletonData.get(entityName)?.content,
99
145
  ];
100
146
  }
101
147
  deleteInstanceContainerOfEntityById(entityName, instanceId) {
@@ -109,13 +155,25 @@ export class DatabaseInMemory {
109
155
  }
110
156
  const remainingInstances = instances.remove(instanceId);
111
157
  return remainingInstances.size === 0 ? undefined : remainingInstances;
112
- })),
158
+ }), this.#singletonData),
159
+ oldInstance.content,
160
+ ]
161
+ : [this, undefined];
162
+ }
163
+ deleteInstanceContainerOfSingletonEntity(entityName) {
164
+ const oldInstance = this.#singletonData.get(entityName);
165
+ return oldInstance
166
+ ? [
167
+ new DatabaseInMemory(this.#data, this.#singletonData.remove(entityName)),
113
168
  oldInstance.content,
114
169
  ]
115
170
  : [this, undefined];
116
171
  }
117
172
  map(fn) {
118
- return new DatabaseInMemory(this.#data.map(fn));
173
+ return new DatabaseInMemory(this.#data.map(fn), this.#singletonData);
174
+ }
175
+ mapSingletons(fn) {
176
+ return new DatabaseInMemory(this.#data, this.#singletonData.map(fn));
119
177
  }
120
178
  getEntityNameOfInstanceId(id) {
121
179
  return this.#data.findKey(instances => instances.has(id));
@@ -1,28 +1,12 @@
1
- import { error, isError, ok } from "@elyukai/utils/result";
1
+ import { error, ok } from "@elyukai/utils/result";
2
2
  import * as DatabaseFilesystem from "./files.js";
3
- const setInstanceOnDisk = async (root, entity, instanceId, instanceContent, formatterOptions) => {
4
- try {
5
- await DatabaseFilesystem.writeInstance(root, entity, instanceId, instanceContent, formatterOptions);
6
- return ok();
7
- }
8
- catch (e) {
9
- return error(e);
10
- }
11
- };
12
- const deleteInstanceOnDisk = async (root, entityName, instanceId) => {
13
- try {
14
- await DatabaseFilesystem.deleteInstance(root, entityName, instanceId);
15
- return ok();
16
- }
17
- catch (e) {
18
- return error(e);
19
- }
20
- };
21
3
  const rollbackChanges = async (root, steps, formatterOptions) => {
22
4
  for (const step of steps) {
23
- const res = await runReverseStepAction(root, step, formatterOptions);
24
- if (isError(res)) {
25
- return res;
5
+ try {
6
+ await runReverseStepAction(root, step, formatterOptions);
7
+ }
8
+ catch (e) {
9
+ return error(e);
26
10
  }
27
11
  }
28
12
  return ok();
@@ -31,29 +15,42 @@ const runStepAction = (root, step, formatterOptions) => {
31
15
  switch (step.kind) {
32
16
  case "create":
33
17
  case "update":
34
- return setInstanceOnDisk(root, step.entity, step.instanceId, step.instanceContent, formatterOptions);
18
+ return DatabaseFilesystem.writeInstance(root, step.entity, step.instanceId, step.instanceContent, formatterOptions);
35
19
  case "delete":
36
- return deleteInstanceOnDisk(root, step.entity.name, step.instanceId);
20
+ return DatabaseFilesystem.deleteInstance(root, step.entity.name, step.instanceId);
21
+ case "createSingleton":
22
+ case "updateSingleton":
23
+ return DatabaseFilesystem.writeSingletonInstance(root, step.entity, step.instanceContent, formatterOptions);
24
+ case "deleteSingleton":
25
+ return DatabaseFilesystem.deleteSingletonInstance(root, step.entity.name);
37
26
  }
38
27
  };
39
28
  const runReverseStepAction = (root, step, formatterOptions) => {
40
29
  switch (step.kind) {
41
30
  case "create":
42
- return deleteInstanceOnDisk(root, step.entity.name, step.instanceId);
31
+ return DatabaseFilesystem.deleteInstance(root, step.entity.name, step.instanceId);
43
32
  case "update":
44
- return setInstanceOnDisk(root, step.entity, step.instanceId, step.oldInstance, formatterOptions);
33
+ return DatabaseFilesystem.writeInstance(root, step.entity, step.instanceId, step.oldInstance, formatterOptions);
45
34
  case "delete":
46
- return setInstanceOnDisk(root, step.entity, step.instanceId, step.oldInstance, formatterOptions);
35
+ return DatabaseFilesystem.writeInstance(root, step.entity, step.instanceId, step.oldInstance, formatterOptions);
36
+ case "createSingleton":
37
+ return DatabaseFilesystem.deleteSingletonInstance(root, step.entity.name);
38
+ case "updateSingleton":
39
+ return DatabaseFilesystem.writeSingletonInstance(root, step.entity, step.oldInstance, formatterOptions);
40
+ case "deleteSingleton":
41
+ return DatabaseFilesystem.writeSingletonInstance(root, step.entity, step.oldInstance, formatterOptions);
47
42
  }
48
43
  };
49
44
  export const applyStepsToDisk = async (root, steps, formatterOptions) => {
50
45
  for (let i = 0; i < steps.length; i++) {
51
46
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
52
47
  const step = steps[i];
53
- const res = await runStepAction(root, step, formatterOptions);
54
- if (isError(res)) {
48
+ try {
49
+ await runStepAction(root, step, formatterOptions);
50
+ }
51
+ catch (e) {
55
52
  await rollbackChanges(root, steps.slice(0, i), formatterOptions);
56
- return res;
53
+ return error(e);
57
54
  }
58
55
  }
59
56
  return ok();
@@ -1,9 +1,14 @@
1
1
  import type { InstanceContent } from "../../shared/utils/instances.ts";
2
2
  import type { FormatterOptions } from "../config.ts";
3
- import { type EntityDecl } from "../schema/dsl/index.ts";
3
+ import { type EntityDecl, type SingletonEntityDecl } from "../schema/dsl/index.ts";
4
4
  export declare const getFileNameForId: (id: string) => string;
5
+ export declare const getSingletonFileName: (entityName: string) => string;
5
6
  export declare const getPathToInstance: (dataRoot: string, entityName: string, id: string) => string;
7
+ export declare const getPathToSingletonInstance: (dataRoot: string, entityName: string) => string;
6
8
  export declare const readInstance: (dataRoot: string, entity: EntityDecl, id: string) => Promise<string>;
9
+ export declare const readSingletonInstance: (dataRoot: string, entity: SingletonEntityDecl) => Promise<string>;
7
10
  export declare const writeInstance: (dataRoot: string, entity: EntityDecl, id: string, instance: InstanceContent, formatterOptions: Partial<FormatterOptions> | undefined) => Promise<void>;
11
+ export declare const writeSingletonInstance: (dataRoot: string, entity: SingletonEntityDecl, instance: InstanceContent, formatterOptions: Partial<FormatterOptions> | undefined) => Promise<void>;
8
12
  export declare const deleteInstance: (dataRoot: string, entityName: string, id: string) => Promise<void>;
9
- export declare const formatInstance: (entity: EntityDecl, instanceContent: InstanceContent, options: Partial<FormatterOptions> | undefined) => string;
13
+ export declare const deleteSingletonInstance: (dataRoot: string, entityName: string) => Promise<void>;
14
+ export declare const formatInstance: (entity: EntityDecl | SingletonEntityDecl, instanceContent: InstanceContent, options: Partial<FormatterOptions> | undefined) => string;
@@ -3,12 +3,21 @@ import { join } from "node:path";
3
3
  import {} from "../schema/dsl/index.js";
4
4
  import { formatValue } from "../schema/treeOperations/format.js";
5
5
  export const getFileNameForId = (id) => `${id}.json`;
6
+ export const getSingletonFileName = (entityName) => `${entityName}.json`;
6
7
  export const getPathToInstance = (dataRoot, entityName, id) => join(dataRoot, entityName, getFileNameForId(id));
8
+ export const getPathToSingletonInstance = (dataRoot, entityName) => join(dataRoot, getSingletonFileName(entityName));
7
9
  export const readInstance = (dataRoot, entity, id) => readFile(getPathToInstance(dataRoot, entity.name, id), {
8
10
  encoding: "utf-8",
9
11
  });
12
+ export const readSingletonInstance = (dataRoot, entity) => readFile(getPathToSingletonInstance(dataRoot, entity.name), {
13
+ encoding: "utf-8",
14
+ });
10
15
  export const writeInstance = (dataRoot, entity, id, instance, formatterOptions) => writeFile(getPathToInstance(dataRoot, entity.name, id), formatInstance(entity, instance, formatterOptions), {
11
16
  encoding: "utf-8",
12
17
  });
18
+ export const writeSingletonInstance = (dataRoot, entity, instance, formatterOptions) => writeFile(getPathToSingletonInstance(dataRoot, entity.name), formatInstance(entity, instance, formatterOptions), {
19
+ encoding: "utf-8",
20
+ });
13
21
  export const deleteInstance = (dataRoot, entityName, id) => rm(getPathToInstance(dataRoot, entityName, id));
22
+ export const deleteSingletonInstance = (dataRoot, entityName) => rm(getPathToSingletonInstance(dataRoot, entityName));
14
23
  export const formatInstance = (entity, instanceContent, options) => JSON.stringify(formatValue(entity.type.value, instanceContent, {}, options), undefined, 2) + "\n";
@@ -4,6 +4,8 @@ import type { AnyEntityMap, GetEntityByName, RegisteredEntityMap } from "../sche
4
4
  import { type DatabaseInMemory } from "./databaseInMemory.ts";
5
5
  /**
6
6
  * A mapping from instance IDs to the list of instance IDs that reference them.
7
+ *
8
+ * For single instance entities, the instance ID is the entity name.
7
9
  */
8
10
  export type ReferencesToInstances = {
9
11
  [instanceId: string]: string[];
@@ -2,6 +2,7 @@ import { removeAt } from "@elyukai/utils/array/modify";
2
2
  import { difference } from "@elyukai/utils/array/sets";
3
3
  import { isOk } from "@elyukai/utils/result";
4
4
  import { resolve } from "node:path";
5
+ import { getReferencesForSerializedSingletonEntityDecl, } from "../../shared/schema/declarations/SingletonEntityDecl.js";
5
6
  import { getReferences } from "../schema/treeOperations/references.js";
6
7
  import {} from "./databaseInMemory.js";
7
8
  import { WorkerPool } from "./workers.js";
@@ -52,7 +53,11 @@ export const getReferencesToInstances = async (databaseInMemory, serializedDecla
52
53
  }
53
54
  })));
54
55
  await pool.close();
55
- const results = separateResults.reduce(mergeReferences, {});
56
+ const results = separateResults
57
+ .concat(databaseInMemory
58
+ .getAllSingletonInstances()
59
+ .reduce((acc, [entityName, instance]) => addReferences(acc, getReferencesForSerializedSingletonEntityDecl(serializedDeclarationsByName, serializedDeclarationsByName[entityName], instance.content), entityName), {}))
60
+ .reduce(mergeReferences, {});
56
61
  return results;
57
62
  };
58
63
  export const updateReferencesToInstances = (getEntity, referencesToInstances, entityName, instanceId, oldInstance, newInstance) => {
@@ -1,6 +1,8 @@
1
1
  import type { SerializedDecl } from "./schema/declarations/Declaration.ts";
2
- import type { CreatedEntityTaggedInstanceContainerWithChildInstances, EntityTaggedInstanceContainerWithChildInstances, UpdatedEntityTaggedInstanceContainerWithChildInstances } from "./utils/childInstances.ts";
3
- import type { InstanceContainer, InstanceContainerOverview } from "./utils/instances.ts";
2
+ import type { SerializedEntityDecl } from "./schema/declarations/EntityDecl.ts";
3
+ import type { SerializedSingletonEntityDecl } from "./schema/declarations/SingletonEntityDecl.ts";
4
+ import type { CreatedEntityTaggedInstanceContainerWithChildInstances, EntityTaggedInstanceContainerWithChildInstances, EntityTaggedSingletonInstanceContainer, UpdatedEntityTaggedInstanceContainerWithChildInstances } from "./utils/childInstances.ts";
5
+ import type { InstanceContainer, InstanceContainerOverview, SingletonInstanceContainer } from "./utils/instances.ts";
4
6
  export type WebConfig = {
5
7
  localeEntityName: string | undefined;
6
8
  defaultLocales: string[];
@@ -12,17 +14,36 @@ export type WebConfig = {
12
14
  };
13
15
  export type GetWebConfigResponseBody = WebConfig;
14
16
  export interface GetAllDeclarationsResponseBody<D extends SerializedDecl = SerializedDecl> {
17
+ declarations: {
18
+ declaration: D;
19
+ }[];
20
+ localeEntity?: string;
21
+ }
22
+ export interface GetAllEntityDeclarationsResponseBody<D extends SerializedEntityDecl = SerializedEntityDecl> {
15
23
  declarations: {
16
24
  declaration: D;
17
25
  instanceCount: number;
18
26
  }[];
19
27
  localeEntity?: string;
20
28
  }
29
+ export interface GetAllSingletonEntityDeclarationsResponseBody<D extends SerializedSingletonEntityDecl = SerializedSingletonEntityDecl> {
30
+ declarations: {
31
+ declaration: D;
32
+ hasInstance: boolean;
33
+ }[];
34
+ }
21
35
  export interface GetDeclarationResponseBody<D extends SerializedDecl = SerializedDecl> {
22
36
  declaration: D;
37
+ }
38
+ export interface GetEntityDeclarationResponseBody<D extends SerializedEntityDecl = SerializedEntityDecl> {
39
+ declaration: D;
23
40
  instanceCount: number;
24
41
  isLocaleEntity: boolean;
25
42
  }
43
+ export interface GetSingletonEntityDeclarationResponseBody<D extends SerializedSingletonEntityDecl = SerializedSingletonEntityDecl> {
44
+ declaration: D;
45
+ hasInstance: boolean;
46
+ }
26
47
  export interface GetAllInstancesOfEntityResponseBody {
27
48
  instances: InstanceContainerOverview[];
28
49
  isLocaleEntity: boolean;
@@ -52,6 +73,24 @@ export interface DeleteInstanceOfEntityResponseBody {
52
73
  instance: InstanceContainer;
53
74
  isLocaleEntity: boolean;
54
75
  }
76
+ export interface CreateSingletonInstanceOfEntityRequestBody {
77
+ instance: EntityTaggedSingletonInstanceContainer;
78
+ }
79
+ export interface CreateSingletonInstanceOfEntityResponseBody {
80
+ instance: SingletonInstanceContainer;
81
+ }
82
+ export interface GetSingletonInstanceOfEntityResponseBody {
83
+ instance: SingletonInstanceContainer;
84
+ }
85
+ export interface UpdateSingletonInstanceOfEntityRequestBody {
86
+ instance: EntityTaggedSingletonInstanceContainer;
87
+ }
88
+ export interface UpdateSingletonInstanceOfEntityResponseBody {
89
+ instance: SingletonInstanceContainer;
90
+ }
91
+ export interface DeleteSingletonInstanceOfEntityResponseBody {
92
+ instance: SingletonInstanceContainer;
93
+ }
55
94
  export interface GetAllInstancesResponseBody {
56
95
  instances: {
57
96
  [entity: string]: {