tsondb 0.13.1 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/README.md +7 -0
  2. package/dist/src/bin/tsondb.js +2 -2
  3. package/dist/src/node/config.d.ts +6 -2
  4. package/dist/src/node/config.js +5 -2
  5. package/dist/src/node/index.d.ts +4 -4
  6. package/dist/src/node/index.js +24 -18
  7. package/dist/src/node/server/api/declarations.js +4 -3
  8. package/dist/src/node/server/api/git.js +4 -3
  9. package/dist/src/node/server/api/instances.js +2 -19
  10. package/dist/src/node/server/api/search.js +4 -3
  11. package/dist/src/node/server/index.d.ts +0 -6
  12. package/dist/src/node/server/init.js +1 -12
  13. package/dist/src/node/server/utils/childInstances.d.ts +3 -2
  14. package/dist/src/node/server/utils/childInstances.js +4 -4
  15. package/dist/src/node/server/utils/instanceOperations.js +3 -3
  16. package/dist/src/node/utils/databaseInMemory.d.ts +10 -1
  17. package/dist/src/node/utils/databaseInMemory.js +14 -2
  18. package/dist/src/node/utils/databaseTransactions.d.ts +1 -1
  19. package/dist/src/node/utils/databaseTransactions.js +2 -2
  20. package/dist/src/node/utils/displayName.d.ts +4 -3
  21. package/dist/src/node/utils/displayName.js +22 -0
  22. package/dist/src/node/utils/error.d.ts +1 -1
  23. package/dist/src/node/utils/error.js +8 -4
  24. package/dist/src/node/utils/unique.d.ts +7 -3
  25. package/dist/src/node/utils/unique.js +33 -18
  26. package/dist/src/shared/api.d.ts +1 -1
  27. package/dist/src/shared/utils/dictionary.d.ts +1 -0
  28. package/dist/src/shared/utils/dictionary.js +1 -0
  29. package/dist/src/shared/utils/instances.d.ts +2 -2
  30. package/dist/src/web/components/typeInputs/NestedEntityMapTypeInput.js +3 -3
  31. package/dist/src/web/components/typeInputs/ReferenceIdentifierTypeInput.js +1 -1
  32. package/package.json +1 -1
package/README.md CHANGED
@@ -48,6 +48,13 @@ export default {
48
48
 
49
49
  See the following sections for details on how the different parts come together.
50
50
 
51
+ ## Terms
52
+
53
+ The following list will define common terms used in TSONDB.
54
+
55
+ - **Entity:** The type of group of values, e.g. employees.
56
+ - **Instance:** A singular group of values of an *entity*, e.g. Jane Doe.
57
+
51
58
  ## Define a Schema
52
59
 
53
60
  You define a schema by declarations that consist of types.
@@ -92,7 +92,7 @@ switch (passedArguments.command.name) {
92
92
  case "serve":
93
93
  debug(`running command: serve`);
94
94
  validateConfigForServer(config);
95
- await serve(config.schema, config.dataRootPath, config.defaultLocales, config.homeLayoutSections, config.serverOptions, config.validationOptions, config.customStylesheetPath);
95
+ await serve(config.schema, config.dataRootPath, config.locales, config.homeLayoutSections, config.serverOptions, config.validationOptions, config.customStylesheetPath);
96
96
  break;
97
97
  case "validate":
98
98
  debug(`running command: validate`);
@@ -104,7 +104,7 @@ switch (passedArguments.command.name) {
104
104
  const entities = passedArguments.command.options.checkOnlyEntities;
105
105
  debug(`only check the following entities: ${entities.join(", ")}`);
106
106
  }
107
- await validate(config.schema, config.dataRootPath, {
107
+ await validate(config.schema, config.dataRootPath, config.locales, {
108
108
  ...config.validationOptions,
109
109
  ...omitUndefinedKeys(passedArguments.command.options ?? {}),
110
110
  });
@@ -9,7 +9,10 @@ export type Config = {
9
9
  serverOptions?: ServerOptions;
10
10
  schema: Schema;
11
11
  outputs?: Output[];
12
- defaultLocales?: string[];
12
+ /**
13
+ * Default locales for the server/editor and locales used for testing via CLI.
14
+ */
15
+ locales?: string[];
13
16
  dataRootPath?: string;
14
17
  homeLayoutSections?: HomeLayoutSection[];
15
18
  customStylesheetPath?: string;
@@ -43,7 +46,7 @@ export type DataConfig = {
43
46
  */
44
47
  export type ServerConfig = DataConfig & {
45
48
  serverOptions?: ServerOptions;
46
- defaultLocales: string[];
49
+ locales: string[];
47
50
  homeLayoutSections?: HomeLayoutSection[];
48
51
  validationOptions?: Partial<ValidationOptions>;
49
52
  customStylesheetPath?: string;
@@ -52,6 +55,7 @@ export type ServerConfig = DataConfig & {
52
55
  * The configuration type required for validating the contents of the database.
53
56
  */
54
57
  export type TestingConfig = DataConfig & {
58
+ locales: string[];
55
59
  validationOptions?: Partial<ValidationOptions>;
56
60
  };
57
61
  /**
@@ -4,14 +4,17 @@ export const validateConfigForGeneration = config => {
4
4
  }
5
5
  };
6
6
  export const validateConfigForServer = config => {
7
- if ((config.defaultLocales?.length ?? 0) === 0) {
8
- throw new Error("At least one default locale must be specified in the config.");
7
+ if ((config.locales?.length ?? 0) === 0) {
8
+ throw new Error("At least one locale must be specified in the config.");
9
9
  }
10
10
  if (config.dataRootPath === undefined) {
11
11
  throw new Error("A data root path must be specified in the config.");
12
12
  }
13
13
  };
14
14
  export const validateConfigForTesting = config => {
15
+ if ((config.locales?.length ?? 0) === 0) {
16
+ throw new Error("At least one locale must be specified in the config.");
17
+ }
15
18
  if (config.dataRootPath === undefined) {
16
19
  throw new Error("A data root path must be specified in the config.");
17
20
  }
@@ -14,8 +14,8 @@ export type ValidationOptions = {
14
14
  matchParametersInKeys?: boolean;
15
15
  };
16
16
  };
17
- export declare const validate: (schema: Schema, dataRootPath: string, options?: Partial<ValidationOptions>) => Promise<void>;
18
- export declare const generateAndValidate: (schema: Schema, outputs: Output[], dataRootPath: string, validationOptions?: Partial<ValidationOptions>) => Promise<void>;
19
- export declare const serve: (schema: Schema, dataRootPath: string, defaultLocales: string[], homeLayoutSections?: HomeLayoutSection[], serverOptions?: Partial<ServerOptions>, validationOptions?: Partial<ValidationOptions>, customStylesheetPath?: string) => Promise<void>;
20
- export declare const generateValidateAndServe: (schema: Schema, outputs: Output[], dataRootPath: string, defaultLocales: string[], homeLayoutSections?: HomeLayoutSection[], serverOptions?: Partial<ServerOptions>, validationOptions?: Partial<ValidationOptions>, customStylesheetPath?: string) => Promise<void>;
17
+ export declare const validate: (schema: Schema, dataRootPath: string, locales: string[], options?: Partial<ValidationOptions>) => Promise<void>;
18
+ export declare const generateAndValidate: (schema: Schema, outputs: Output[], dataRootPath: string, locales: string[], validationOptions?: Partial<ValidationOptions>) => Promise<void>;
19
+ export declare const serve: (schema: Schema, dataRootPath: string, locales: string[], homeLayoutSections?: HomeLayoutSection[], serverOptions?: Partial<ServerOptions>, validationOptions?: Partial<ValidationOptions>, customStylesheetPath?: string) => Promise<void>;
20
+ export declare const generateValidateAndServe: (schema: Schema, outputs: Output[], dataRootPath: string, locales: string[], homeLayoutSections?: HomeLayoutSection[], serverOptions?: Partial<ServerOptions>, validationOptions?: Partial<ValidationOptions>, customStylesheetPath?: string) => Promise<void>;
21
21
  export declare const format: (schema: Schema, dataRootPath: string) => Promise<void>;
@@ -1,6 +1,7 @@
1
1
  import Debug from "debug";
2
2
  import { mkdir } from "fs/promises";
3
3
  import { join, sep } from "path";
4
+ import { stderr } from "process";
4
5
  import { styleText } from "util";
5
6
  import { isError } from "../shared/utils/result.js";
6
7
  import { parallelizeErrors } from "../shared/utils/validation.js";
@@ -8,8 +9,8 @@ import { validateEntityDecl } from "./schema/declarations/EntityDecl.js";
8
9
  import { createValidationContext } from "./schema/Node.js";
9
10
  import { getEntities } from "./schema/Schema.js";
10
11
  import { createServer } from "./server/index.js";
11
- import { asyncForEachInstanceInDatabaseInMemory, createDatabaseInMemory, getInstancesOfEntityFromDatabaseInMemory, } from "./utils/databaseInMemory.js";
12
- import { getErrorMessageForDisplay, wrapErrorsIfAny } from "./utils/error.js";
12
+ import { asyncForEachInstanceInDatabaseInMemory, countInstancesInDatabaseInMemory, createDatabaseInMemory, getInstancesOfEntityFromDatabaseInMemory, } from "./utils/databaseInMemory.js";
13
+ import { countError, countErrors, getErrorMessageForDisplay, wrapErrorsIfAny, } from "./utils/error.js";
13
14
  import { getFileNameForId, writeInstance } from "./utils/files.js";
14
15
  import { checkUniqueConstraintsForAllEntities } from "./utils/unique.js";
15
16
  const debug = Debug("tsondb:jsapi");
@@ -25,7 +26,7 @@ export const generateOutputs = async (schema, outputs) => {
25
26
  await output.run(schema);
26
27
  }
27
28
  };
28
- const _validate = (dataRootPath, entities, databaseInMemory, options = {}) => {
29
+ const _validate = (dataRootPath, entities, databaseInMemory, locales, options = {}) => {
29
30
  const { checkReferentialIntegrity = true, checkOnlyEntities = [] } = options;
30
31
  for (const onlyEntity of checkOnlyEntities) {
31
32
  if (!entities.find(entity => entity.name === onlyEntity)) {
@@ -40,16 +41,18 @@ const _validate = (dataRootPath, entities, databaseInMemory, options = {}) => {
40
41
  .flatMap(entity => parallelizeErrors(getInstancesOfEntityFromDatabaseInMemory(databaseInMemory, entity.name).map(instance => wrapErrorsIfAny(`in file ${styleText("white", `"${dataRootPath}${sep}${styleText("bold", join(entity.name, getFileNameForId(instance.id)))}"`)}`, validateEntityDecl(validationContext, [], entity, instance.content)))))
41
42
  .toSorted((a, b) => a.message.localeCompare(b.message));
42
43
  if (errors.length > 0) {
43
- debug(`${errors.length.toString()} structural integrity violation${errors.length === 1 ? "" : "s"} found`);
44
+ const errorCount = countErrors(errors);
45
+ debug(`${errorCount.toString()} structural integrity violation${errorCount === 1 ? "" : "s"} found`);
44
46
  }
45
47
  else {
46
48
  debug("No structural integrity violations found");
47
49
  }
48
50
  if (errors.length === 0) {
49
51
  debug("Checking unique constraints ...");
50
- const constraintResult = checkUniqueConstraintsForAllEntities(databaseInMemory, entities);
52
+ const constraintResult = checkUniqueConstraintsForAllEntities(databaseInMemory, Object.fromEntries(entities.map(entity => [entity.name, entity])), locales);
51
53
  if (isError(constraintResult)) {
52
- debug(`${constraintResult.error.errors.length.toString()} unique constraint violation${constraintResult.error.errors.length === 1 ? "" : "s"} found`);
54
+ const errorCount = countError(constraintResult.error);
55
+ debug(`${errorCount.toString()} unique constraint violation${errorCount === 1 ? "" : "s"} found`);
53
56
  errors.push(constraintResult.error);
54
57
  }
55
58
  else {
@@ -59,31 +62,34 @@ const _validate = (dataRootPath, entities, databaseInMemory, options = {}) => {
59
62
  else {
60
63
  debug("Skipping unique constraint checks due to previous structural integrity errors");
61
64
  }
65
+ const totalInstanceCount = countInstancesInDatabaseInMemory(databaseInMemory);
66
+ console.log(`${totalInstanceCount.toString()} instance${totalInstanceCount === 1 ? "" : "s"} checked`);
62
67
  if (errors.length === 0) {
63
- console.log("All entities are valid");
68
+ console.log(styleText("green", "All entities are valid"));
64
69
  return true;
65
70
  }
66
71
  else {
67
- console.error(`${errors.length.toString()} validation error${errors.length === 1 ? "" : "s"} found\n\n${errors.map(err => getErrorMessageForDisplay(err)).join("\n\n")}`);
72
+ const errorCount = countErrors(errors);
73
+ console.error(styleText("red", `${errorCount.toString()} validation error${errorCount === 1 ? "" : "s"} found\n\n${errors.map(err => getErrorMessageForDisplay(err)).join("\n\n")}`, { stream: stderr }));
68
74
  process.exitCode = 1;
69
75
  return false;
70
76
  }
71
77
  };
72
- export const validate = async (schema, dataRootPath, options) => {
78
+ export const validate = async (schema, dataRootPath, locales, options) => {
73
79
  const entities = getEntities(schema);
74
80
  await prepareFolders(dataRootPath, entities);
75
81
  const databaseInMemory = await createDatabaseInMemory(dataRootPath, entities);
76
- _validate(dataRootPath, entities, databaseInMemory, options);
82
+ _validate(dataRootPath, entities, databaseInMemory, locales, options);
77
83
  };
78
- export const generateAndValidate = async (schema, outputs, dataRootPath, validationOptions) => {
84
+ export const generateAndValidate = async (schema, outputs, dataRootPath, locales, validationOptions) => {
79
85
  await generateOutputs(schema, outputs);
80
86
  const entities = getEntities(schema);
81
87
  await prepareFolders(dataRootPath, entities);
82
88
  const databaseInMemory = await createDatabaseInMemory(dataRootPath, entities);
83
- _validate(dataRootPath, entities, databaseInMemory, validationOptions);
89
+ _validate(dataRootPath, entities, databaseInMemory, locales, validationOptions);
84
90
  };
85
- export const serve = async (schema, dataRootPath, defaultLocales, homeLayoutSections, serverOptions, validationOptions, customStylesheetPath) => {
86
- if (defaultLocales.length === 0) {
91
+ export const serve = async (schema, dataRootPath, locales, homeLayoutSections, serverOptions, validationOptions, customStylesheetPath) => {
92
+ if (locales.length === 0) {
87
93
  throw new Error("At least one default locale must be specified to start the server.");
88
94
  }
89
95
  const entities = getEntities(schema);
@@ -91,16 +97,16 @@ export const serve = async (schema, dataRootPath, defaultLocales, homeLayoutSect
91
97
  debug("prepared folders");
92
98
  const databaseInMemory = await createDatabaseInMemory(dataRootPath, entities);
93
99
  debug("loaded instances");
94
- await createServer(schema, dataRootPath, databaseInMemory, defaultLocales, homeLayoutSections, serverOptions, validationOptions, customStylesheetPath);
100
+ await createServer(schema, dataRootPath, databaseInMemory, locales, homeLayoutSections, serverOptions, validationOptions, customStylesheetPath);
95
101
  };
96
- export const generateValidateAndServe = async (schema, outputs, dataRootPath, defaultLocales, homeLayoutSections, serverOptions, validationOptions, customStylesheetPath) => {
102
+ export const generateValidateAndServe = async (schema, outputs, dataRootPath, locales, homeLayoutSections, serverOptions, validationOptions, customStylesheetPath) => {
97
103
  await generateOutputs(schema, outputs);
98
104
  const entities = getEntities(schema);
99
105
  await prepareFolders(dataRootPath, entities);
100
106
  const databaseInMemory = await createDatabaseInMemory(dataRootPath, entities);
101
- const isValid = _validate(dataRootPath, entities, databaseInMemory, validationOptions);
107
+ const isValid = _validate(dataRootPath, entities, databaseInMemory, locales, validationOptions);
102
108
  if (isValid) {
103
- await createServer(schema, dataRootPath, databaseInMemory, defaultLocales, homeLayoutSections, serverOptions, validationOptions, customStylesheetPath);
109
+ await createServer(schema, dataRootPath, databaseInMemory, locales, homeLayoutSections, serverOptions, validationOptions, customStylesheetPath);
104
110
  }
105
111
  else {
106
112
  console.error("Not starting server due to invalid database");
@@ -7,7 +7,7 @@ import { isEnumDecl } from "../../schema/declarations/EnumDecl.js";
7
7
  import { isTypeAliasDecl } from "../../schema/declarations/TypeAliasDecl.js";
8
8
  import { serializeNode } from "../../schema/index.js";
9
9
  import { getChildInstances } from "../../utils/childInstances.js";
10
- import { countInstancesOfEntityInDatabaseInMemory, getInstanceOfEntityFromDatabaseInMemory, getInstancesOfEntityFromDatabaseInMemory, } from "../../utils/databaseInMemory.js";
10
+ import { countInstancesOfEntityInDatabaseInMemory, createInstanceFromDatabaseInMemoryGetter, getInstanceOfEntityFromDatabaseInMemory, getInstancesOfEntityFromDatabaseInMemory, } from "../../utils/databaseInMemory.js";
11
11
  import { HTTPError } from "../../utils/error.js";
12
12
  import { createChildInstancesForInstanceIdGetter } from "../utils/childInstances.js";
13
13
  import { createInstance, deleteInstance, updateInstance } from "../utils/instanceOperations.js";
@@ -64,10 +64,11 @@ declarationsApi.get("/:name/instances", (req, res) => {
64
64
  res.status(400).send(`Declaration "${decl.name}" is not an entity`);
65
65
  return;
66
66
  }
67
- const getChildInstancesForInstanceId = createChildInstancesForInstanceIdGetter(req);
67
+ const getInstanceById = createInstanceFromDatabaseInMemoryGetter(req.databaseInMemory, req.entitiesByName);
68
+ const getChildInstancesForInstanceId = createChildInstancesForInstanceIdGetter(req.entitiesByName, req.databaseInMemory);
68
69
  const body = {
69
70
  instances: getInstancesOfEntityFromDatabaseInMemory(req.databaseInMemory, decl.name)
70
- .map(instanceContainer => getInstanceContainerOverview(decl, instanceContainer, req.getInstanceById, getChildInstancesForInstanceId, req.locales))
71
+ .map(instanceContainer => getInstanceContainerOverview(decl, instanceContainer, getInstanceById, getChildInstancesForInstanceId, req.locales))
71
72
  .toSorted((a, b) => a.displayName.localeCompare(b.displayName, undefined, { numeric: true })),
72
73
  isLocaleEntity: decl === req.localeEntity,
73
74
  };
@@ -3,7 +3,7 @@ import express, {} from "express";
3
3
  import { join } from "node:path";
4
4
  import { hasFileChanges, splitBranchName } from "../../../shared/utils/git.js";
5
5
  import { getInstanceContainerOverview } from "../../../shared/utils/instances.js";
6
- import { getGroupedInstancesFromDatabaseInMemory } from "../../utils/databaseInMemory.js";
6
+ import { createInstanceFromDatabaseInMemoryGetter, getGroupedInstancesFromDatabaseInMemory, } from "../../utils/databaseInMemory.js";
7
7
  import { attachGitStatusToDatabaseInMemory } from "../../utils/git.js";
8
8
  import { reinit } from "../init.js";
9
9
  import { createChildInstancesForInstanceIdGetter } from "../utils/childInstances.js";
@@ -28,7 +28,8 @@ gitApi.get("/status", async (req, res) => {
28
28
  req.setLocal("databaseInMemory", attachGitStatusToDatabaseInMemory(req.databaseInMemory, req.dataRoot,
29
29
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
30
30
  req.gitRoot, status));
31
- const getChildInstancesForInstanceId = createChildInstancesForInstanceIdGetter(req);
31
+ const getInstanceById = createInstanceFromDatabaseInMemoryGetter(req.databaseInMemory, req.entitiesByName);
32
+ const getChildInstancesForInstanceId = createChildInstancesForInstanceIdGetter(req.entitiesByName, req.databaseInMemory);
32
33
  const body = {
33
34
  currentBranch: status.current,
34
35
  trackingBranch: status.tracking,
@@ -40,7 +41,7 @@ gitApi.get("/status", async (req, res) => {
40
41
  .filter(instance => hasFileChanges(instance.gitStatus))
41
42
  .map(instance => getInstanceContainerOverview(
42
43
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
43
- req.entitiesByName[entityName], instance, req.getInstanceById, getChildInstancesForInstanceId, req.locales)),
44
+ req.entitiesByName[entityName], instance, getInstanceById, getChildInstancesForInstanceId, req.locales)),
44
45
  ])),
45
46
  latestCommit: await req.git.revparse(["HEAD"]),
46
47
  };
@@ -1,8 +1,6 @@
1
1
  import Debug from "debug";
2
2
  import express from "express";
3
- import { getGroupedInstancesFromDatabaseInMemory } from "../../utils/databaseInMemory.js";
4
- import { getDisplayNameFromEntityInstance } from "../../utils/displayName.js";
5
- import { createChildInstancesForInstanceIdGetter } from "../utils/childInstances.js";
3
+ import { getAllInstanceOverviewsByEntityName } from "../../utils/displayName.js";
6
4
  const debug = Debug("tsondb:server:api:instances");
7
5
  export const instancesApi = express.Router();
8
6
  instancesApi.use((req, _res, next) => {
@@ -10,23 +8,8 @@ instancesApi.use((req, _res, next) => {
10
8
  next();
11
9
  });
12
10
  instancesApi.get("/", (req, res) => {
13
- const getChildInstancesForInstanceId = createChildInstancesForInstanceIdGetter(req);
14
11
  const body = {
15
- instances: Object.fromEntries(getGroupedInstancesFromDatabaseInMemory(req.databaseInMemory).map(([entityName, instances]) => [
16
- entityName,
17
- instances
18
- .map(instance => {
19
- const { name, localeId } = getDisplayNameFromEntityInstance(
20
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
21
- req.entitiesByName[entityName], instance, req.getInstanceById, getChildInstancesForInstanceId, req.locales);
22
- return {
23
- id: instance.id,
24
- name,
25
- displayNameLocaleId: localeId,
26
- };
27
- })
28
- .toSorted((a, b) => a.name.localeCompare(b.name, a.displayNameLocaleId)),
29
- ])),
12
+ instances: getAllInstanceOverviewsByEntityName(req.entitiesByName, req.databaseInMemory, req.locales),
30
13
  };
31
14
  res.json(body);
32
15
  });
@@ -1,7 +1,7 @@
1
1
  import Debug from "debug";
2
2
  import express from "express";
3
3
  import { isEntityDeclWithParentReference } from "../../schema/index.js";
4
- import { getGroupedInstancesFromDatabaseInMemory } from "../../utils/databaseInMemory.js";
4
+ import { createInstanceFromDatabaseInMemoryGetter, getGroupedInstancesFromDatabaseInMemory, } from "../../utils/databaseInMemory.js";
5
5
  import { getDisplayNameFromEntityInstance } from "../../utils/displayName.js";
6
6
  import { createChildInstancesForInstanceIdGetter } from "../utils/childInstances.js";
7
7
  import { getQueryParamString } from "../utils/query.js";
@@ -10,7 +10,8 @@ export const searchApi = express.Router();
10
10
  searchApi.get("/", (req, res) => {
11
11
  const query = getQueryParamString(req.query, "q")?.toLowerCase() ?? "";
12
12
  debug('search for items containing "%s"', query);
13
- const getChildInstancesForInstanceId = createChildInstancesForInstanceIdGetter(req);
13
+ const getInstanceById = createInstanceFromDatabaseInMemoryGetter(req.databaseInMemory, req.entitiesByName);
14
+ const getChildInstancesForInstanceId = createChildInstancesForInstanceIdGetter(req.entitiesByName, req.databaseInMemory);
14
15
  const body = {
15
16
  query,
16
17
  results: query.length === 0
@@ -25,7 +26,7 @@ searchApi.get("/", (req, res) => {
25
26
  .map((instance) => {
26
27
  const { name, localeId } = getDisplayNameFromEntityInstance(
27
28
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
28
- req.entitiesByName[entityName], instance, req.getInstanceById, getChildInstancesForInstanceId, req.locales);
29
+ req.entitiesByName[entityName], instance, getInstanceById, getChildInstancesForInstanceId, req.locales);
29
30
  const searchableName = name.toLowerCase();
30
31
  return [
31
32
  entityName,
@@ -1,6 +1,5 @@
1
1
  import type { SimpleGit } from "simple-git";
2
2
  import type { SerializedDecl } from "../../shared/schema/declarations/Declaration.ts";
3
- import type { InstanceContainer } from "../../shared/utils/instances.ts";
4
3
  import type { HomeLayoutSection } from "../config.ts";
5
4
  import type { ValidationOptions } from "../index.ts";
6
5
  import type { Decl } from "../schema/declarations/Declaration.ts";
@@ -26,13 +25,8 @@ export interface TSONDBRequestLocals {
26
25
  locales: string[];
27
26
  homeLayoutSections?: HomeLayoutSection[];
28
27
  validationOptions: Partial<ValidationOptions>;
29
- getInstanceById: GetInstanceById;
30
28
  setLocal: <K extends keyof Omit<TSONDBRequestLocals, "setLocal">>(key: K, value: TSONDBRequestLocals[K]) => void;
31
29
  }
32
- export type GetInstanceById = (id: string) => {
33
- entity: EntityDecl;
34
- instance: InstanceContainer;
35
- } | undefined;
36
30
  declare global {
37
31
  namespace Express {
38
32
  interface Request extends TSONDBRequestLocals {
@@ -2,7 +2,7 @@ import Debug from "debug";
2
2
  import { simpleGit } from "simple-git";
3
3
  import { isEntityDecl } from "../schema/declarations/EntityDecl.js";
4
4
  import { resolveTypeArgumentsInDecls, serializeNode } from "../schema/index.js";
5
- import { createDatabaseInMemory, getInstanceFromDatabaseInMemory, } from "../utils/databaseInMemory.js";
5
+ import { createDatabaseInMemory } from "../utils/databaseInMemory.js";
6
6
  import { attachGitStatusToDatabaseInMemory } from "../utils/git.js";
7
7
  import { getReferencesToInstances } from "../utils/references.js";
8
8
  const debug = Debug("tsondb:server:init");
@@ -36,16 +36,6 @@ export const init = async (schema, dataRootPath, databaseInMemory, defaultLocale
36
36
  databaseInMemory = attachGitStatusToDatabaseInMemory(databaseInMemory, dataRootPath, gitRoot, gitStatus);
37
37
  debug("retrieved git status to instances");
38
38
  }
39
- const getInstanceById = id => {
40
- const res = getInstanceFromDatabaseInMemory(requestLocals.databaseInMemory, id);
41
- if (res) {
42
- const [entityName, instance] = res;
43
- if (requestLocals.entitiesByName[entityName]) {
44
- return { entity: requestLocals.entitiesByName[entityName], instance };
45
- }
46
- }
47
- return undefined;
48
- };
49
39
  const requestLocals = {
50
40
  git: git,
51
41
  gitRoot: gitRoot,
@@ -56,7 +46,6 @@ export const init = async (schema, dataRootPath, databaseInMemory, defaultLocale
56
46
  entitiesByName: entitiesByName,
57
47
  serializedDeclarationsByName,
58
48
  localeEntity: schema.localeEntity,
59
- getInstanceById,
60
49
  referencesToInstances,
61
50
  defaultLocales,
62
51
  locales: defaultLocales,
@@ -1,3 +1,4 @@
1
+ import { type EntityDecl } from "../../schema/index.ts";
2
+ import type { DatabaseInMemory } from "../../utils/databaseInMemory.ts";
1
3
  import type { GetChildInstancesForInstanceId } from "../../utils/displayName.ts";
2
- import type { TSONDBRequestLocals } from "../index.ts";
3
- export declare const createChildInstancesForInstanceIdGetter: (locals: TSONDBRequestLocals) => GetChildInstancesForInstanceId;
4
+ export declare const createChildInstancesForInstanceIdGetter: (entitiesByName: Record<string, EntityDecl>, databaseInMemory: DatabaseInMemory) => GetChildInstancesForInstanceId;
@@ -1,12 +1,12 @@
1
1
  import { isEntityDeclWithParentReference } from "../../schema/index.js";
2
2
  import { getChildInstancesFromEntity } from "../../utils/childInstances.js";
3
- export const createChildInstancesForInstanceIdGetter = (locals) => (parentEntityName, parentId, childEntityName) => {
4
- const parentEntity = locals.entitiesByName[parentEntityName];
5
- const childEntity = locals.entitiesByName[childEntityName];
3
+ export const createChildInstancesForInstanceIdGetter = (entitiesByName, databaseInMemory) => (parentEntityName, parentId, childEntityName) => {
4
+ const parentEntity = entitiesByName[parentEntityName];
5
+ const childEntity = entitiesByName[childEntityName];
6
6
  if (parentEntity === undefined ||
7
7
  childEntity === undefined ||
8
8
  !isEntityDeclWithParentReference(childEntity)) {
9
9
  return [];
10
10
  }
11
- return getChildInstancesFromEntity(locals.databaseInMemory, parentEntity, parentId, childEntity);
11
+ return getChildInstancesFromEntity(databaseInMemory, parentEntity, parentId, childEntity);
12
12
  };
@@ -8,7 +8,7 @@ export const createInstance = async (locals, instance, idQueryParam) => {
8
8
  if (entity === undefined) {
9
9
  return error(new HTTPError(400, "Entity not found"));
10
10
  }
11
- const databaseTransactionResult = await runDatabaseTransaction(locals.dataRoot, locals.gitRoot ? locals.git : undefined, locals.entitiesByName, locals.databaseInMemory, locals.referencesToInstances, res => saveInstanceTree(locals.validationOptions, locals.entitiesByName, undefined, undefined, locals.localeEntity, instance.entityName, undefined, instance, idQueryParam, res));
11
+ const databaseTransactionResult = await runDatabaseTransaction(locals.dataRoot, locals.gitRoot ? locals.git : undefined, locals.entitiesByName, locals.databaseInMemory, locals.referencesToInstances, locals.locales, res => saveInstanceTree(locals.validationOptions, locals.entitiesByName, undefined, undefined, locals.localeEntity, instance.entityName, undefined, instance, idQueryParam, res));
12
12
  if (isError(databaseTransactionResult)) {
13
13
  return databaseTransactionResult;
14
14
  }
@@ -27,7 +27,7 @@ export const updateInstance = async (locals, instance) => {
27
27
  return error(new HTTPError(400, "Entity not found"));
28
28
  }
29
29
  const oldChildInstances = getChildInstances(locals.databaseInMemory, entity, instance.id, true);
30
- const databaseTransactionResult = await runDatabaseTransaction(locals.dataRoot, locals.gitRoot ? locals.git : undefined, locals.entitiesByName, locals.databaseInMemory, locals.referencesToInstances, res => saveInstanceTree(locals.validationOptions, locals.entitiesByName, undefined, undefined, locals.localeEntity, instance.entityName, {
30
+ const databaseTransactionResult = await runDatabaseTransaction(locals.dataRoot, locals.gitRoot ? locals.git : undefined, locals.entitiesByName, locals.databaseInMemory, locals.referencesToInstances, locals.locales, res => saveInstanceTree(locals.validationOptions, locals.entitiesByName, undefined, undefined, locals.localeEntity, instance.entityName, {
31
31
  id: instance.id,
32
32
  content: instanceContainer.content,
33
33
  childInstances: oldChildInstances,
@@ -51,7 +51,7 @@ export const deleteInstance = async (locals, entityName, instanceId) => {
51
51
  return error(new HTTPError(400, "Entity not found"));
52
52
  }
53
53
  const oldChildInstances = getChildInstances(locals.databaseInMemory, entity, instanceId, true);
54
- const databaseTransactionResult = await runDatabaseTransaction(locals.dataRoot, locals.gitRoot ? locals.git : undefined, locals.entitiesByName, locals.databaseInMemory, locals.referencesToInstances, res => saveInstanceTree(locals.validationOptions, locals.entitiesByName, undefined, undefined, locals.localeEntity, entityName, {
54
+ const databaseTransactionResult = await runDatabaseTransaction(locals.dataRoot, locals.gitRoot ? locals.git : undefined, locals.entitiesByName, locals.databaseInMemory, locals.referencesToInstances, locals.locales, res => saveInstanceTree(locals.validationOptions, locals.entitiesByName, undefined, undefined, locals.localeEntity, entityName, {
55
55
  id: instanceId,
56
56
  content: instanceContainer.content,
57
57
  childInstances: oldChildInstances,
@@ -4,7 +4,15 @@ import type { EntityDecl } from "../schema/index.ts";
4
4
  export type DatabaseInMemory = Dictionary<InstancesInMemory>;
5
5
  export type InstancesInMemory = Dictionary<InstanceContainer>;
6
6
  export declare const emptyDatabaseInMemory: DatabaseInMemory;
7
- export declare const getInstanceFromDatabaseInMemory: (db: DatabaseInMemory, instanceId: string) => [entityName: string, InstanceContainer] | undefined;
7
+ export declare const getInstanceFromDatabaseInMemory: (db: DatabaseInMemory, instanceId: string) => {
8
+ entityName: string;
9
+ instance: InstanceContainer;
10
+ } | undefined;
11
+ export type InstanceFromDatabaseInMemoryGetter = (instanceId: string) => {
12
+ entity: EntityDecl;
13
+ instance: InstanceContainer;
14
+ } | undefined;
15
+ export declare const createInstanceFromDatabaseInMemoryGetter: (db: DatabaseInMemory, entitiesByName: Record<string, EntityDecl>) => InstanceFromDatabaseInMemoryGetter;
8
16
  export declare const getInstanceOfEntityFromDatabaseInMemory: (db: DatabaseInMemory, entityName: string, instanceId: string) => InstanceContainer | undefined;
9
17
  export declare const getInstancesOfEntityFromDatabaseInMemory: (db: DatabaseInMemory, entityName: string) => InstanceContainer[];
10
18
  export declare const getGroupedInstancesFromDatabaseInMemory: (db: DatabaseInMemory) => [entityName: string, InstanceContainer[]][];
@@ -12,6 +20,7 @@ export declare const forEachInstanceOfEntityInDatabaseInMemory: (db: DatabaseInM
12
20
  export declare const asyncForEachInstanceOfEntityInDatabaseInMemory: (db: DatabaseInMemory, entityName: string, fn: (instance: InstanceContainer) => Promise<void>) => Promise<void>;
13
21
  export declare const forEachInstanceInDatabaseInMemory: (db: DatabaseInMemory, fn: (entityName: string, instance: InstanceContainer) => void) => void;
14
22
  export declare const asyncForEachInstanceInDatabaseInMemory: (db: DatabaseInMemory, fn: (entityName: string, instance: InstanceContainer) => Promise<void>) => Promise<void>;
23
+ export declare const countInstancesInDatabaseInMemory: (db: DatabaseInMemory) => number;
15
24
  export declare const countInstancesOfEntityInDatabaseInMemory: (db: DatabaseInMemory, entityName: string) => number;
16
25
  export declare const createDatabaseInMemory: (dataRoot: string, entities: readonly EntityDecl[]) => Promise<DatabaseInMemory>;
17
26
  export declare const setInstanceInDatabaseInMemory: (db: DatabaseInMemory, entityName: string, instance: InstanceContainer) => [DatabaseInMemory, oldInstance: InstanceContent | undefined];
@@ -4,15 +4,26 @@ import { basename, extname, join } from "node:path";
4
4
  import { platform } from "node:process";
5
5
  import { promisify } from "node:util";
6
6
  import { mapAsync } from "../../shared/utils/async.js";
7
- import { emptyD, forEachAsyncD, forEachD, fromEntriesD, getD, getMapD, hasD, mapFirstD, removeD, setD, sizeD, toEntriesD, toValuesD, } from "../../shared/utils/dictionary.js";
7
+ import { emptyD, forEachAsyncD, forEachD, fromEntriesD, getD, getMapD, hasD, mapFirstD, reduceD, removeD, setD, sizeD, toEntriesD, toValuesD, } from "../../shared/utils/dictionary.js";
8
8
  export const emptyDatabaseInMemory = emptyD;
9
9
  export const getInstanceFromDatabaseInMemory = (db, instanceId) => mapFirstD(db, (instances, entityName) => {
10
10
  const instance = getD(instances, instanceId);
11
11
  if (instance) {
12
- return [entityName, instance];
12
+ return { entityName, instance };
13
13
  }
14
14
  return undefined;
15
15
  });
16
+ export const createInstanceFromDatabaseInMemoryGetter = (db, entitiesByName) => instanceId => {
17
+ const res = getInstanceFromDatabaseInMemory(db, instanceId);
18
+ if (res) {
19
+ const { entityName, instance } = res;
20
+ const entity = entitiesByName[entityName];
21
+ if (entity) {
22
+ return { entity, instance };
23
+ }
24
+ }
25
+ return undefined;
26
+ };
16
27
  export const getInstanceOfEntityFromDatabaseInMemory = (db, entityName, instanceId) => getMapD(db, entityName, instances => getD(instances, instanceId));
17
28
  export const getInstancesOfEntityFromDatabaseInMemory = (db, entityName) => getMapD(db, entityName, toValuesD) ?? [];
18
29
  export const getGroupedInstancesFromDatabaseInMemory = (db) => toEntriesD(db).map(([entityName, instances]) => [entityName, Object.values(instances[0])]);
@@ -34,6 +45,7 @@ export const forEachInstanceInDatabaseInMemory = (db, fn) => {
34
45
  });
35
46
  };
36
47
  export const asyncForEachInstanceInDatabaseInMemory = async (db, fn) => forEachAsyncD(db, (instances, entityName) => forEachAsyncD(instances, instance => fn(entityName, instance)));
48
+ export const countInstancesInDatabaseInMemory = (db) => reduceD(db, (sum, instances) => sum + sizeD(instances), 0);
37
49
  export const countInstancesOfEntityInDatabaseInMemory = (db, entityName) => getMapD(db, entityName, sizeD) ?? 0;
38
50
  const exec = promisify(child_process.exec);
39
51
  const ulimit = platform === "win32" ? 2048 : Number.parseInt((await exec("ulimit -n")).stdout);
@@ -25,7 +25,7 @@ export type TransactionResult<A extends object = object> = Result<{
25
25
  /**
26
26
  * Run a transaction on the database in memory, applying all changes to disk if successful.
27
27
  */
28
- export declare const runDatabaseTransaction: (root: string, git: SimpleGit | undefined, entitiesByName: Record<string, EntityDecl>, database: DatabaseInMemory, references: ReferencesToInstances, transactionFn: (db: TransactionResult) => TransactionResult<{
28
+ export declare const runDatabaseTransaction: (root: string, git: SimpleGit | undefined, entitiesByName: Record<string, EntityDecl>, database: DatabaseInMemory, references: ReferencesToInstances, locales: string[], transactionFn: (db: TransactionResult) => TransactionResult<{
29
29
  instanceContainer: InstanceContainer;
30
30
  }>) => Promise<Result<{
31
31
  db: DatabaseInMemory;
@@ -7,7 +7,7 @@ import { checkUniqueConstraintsForAllEntities } from "./unique.js";
7
7
  /**
8
8
  * Run a transaction on the database in memory, applying all changes to disk if successful.
9
9
  */
10
- export const runDatabaseTransaction = async (root, git, entitiesByName, database, references, transactionFn) => {
10
+ export const runDatabaseTransaction = async (root, git, entitiesByName, database, references, locales, transactionFn) => {
11
11
  const result = transactionFn(ok({
12
12
  db: database,
13
13
  entitiesByName,
@@ -18,7 +18,7 @@ export const runDatabaseTransaction = async (root, git, entitiesByName, database
18
18
  return result;
19
19
  }
20
20
  const { db: newDb, refs: newRefs, steps, instanceContainer } = result.value;
21
- const constraintResult = checkUniqueConstraintsForAllEntities(newDb, Object.values(entitiesByName));
21
+ const constraintResult = checkUniqueConstraintsForAllEntities(newDb, entitiesByName, locales);
22
22
  if (isError(constraintResult)) {
23
23
  return constraintResult;
24
24
  }
@@ -1,8 +1,8 @@
1
- import type { GetInstanceById } from "../../node/server/index.ts";
2
1
  import { type DisplayNameResult } from "../../shared/utils/displayName.ts";
3
- import type { InstanceContainer, InstanceContent } from "../../shared/utils/instances.ts";
2
+ import type { InstanceContainer, InstanceContainerOverview, InstanceContent } from "../../shared/utils/instances.ts";
4
3
  import { type EntityDecl } from "../schema/declarations/EntityDecl.ts";
5
4
  import type { AsDeepType, Type } from "../schema/types/Type.ts";
5
+ import { type DatabaseInMemory, type InstanceFromDatabaseInMemoryGetter } from "./databaseInMemory.ts";
6
6
  export type GetChildInstancesForInstanceId = (parentEntityName: string, parentId: string, childEntityName: string) => {
7
7
  id: string;
8
8
  content: InstanceContent;
@@ -17,4 +17,5 @@ export type DisplayNameCustomizer<T extends Type> = (params: {
17
17
  getDisplayNameForInstanceId: (id: string) => DisplayNameResult | undefined;
18
18
  getChildInstancesForInstanceId: GetChildInstancesForInstanceId;
19
19
  }) => DisplayNameResult;
20
- export declare const getDisplayNameFromEntityInstance: (entity: EntityDecl, instanceContainer: InstanceContainer, getInstanceById: GetInstanceById, getChildInstancesForInstanceId: GetChildInstancesForInstanceId, locales: string[], defaultName?: string, useCustomizer?: boolean) => DisplayNameResult;
20
+ export declare const getDisplayNameFromEntityInstance: (entity: EntityDecl, instanceContainer: InstanceContainer, getInstanceById: InstanceFromDatabaseInMemoryGetter, getChildInstancesForInstanceId: GetChildInstancesForInstanceId, locales: string[], defaultName?: string, useCustomizer?: boolean) => DisplayNameResult;
21
+ export declare const getAllInstanceOverviewsByEntityName: (entitiesByName: Record<string, EntityDecl>, databaseInMemory: DatabaseInMemory, locales: string[]) => Record<string, InstanceContainerOverview[]>;
@@ -1,5 +1,7 @@
1
1
  import { getSerializedDisplayNameFromEntityInstance, } from "../../shared/utils/displayName.js";
2
2
  import { serializeEntityDecl } from "../schema/declarations/EntityDecl.js";
3
+ import { createChildInstancesForInstanceIdGetter } from "../server/utils/childInstances.js";
4
+ import { createInstanceFromDatabaseInMemoryGetter, getGroupedInstancesFromDatabaseInMemory, } from "./databaseInMemory.js";
3
5
  export const getDisplayNameFromEntityInstance = (entity, instanceContainer, getInstanceById, getChildInstancesForInstanceId, locales, defaultName = "", useCustomizer = true) => {
4
6
  if (useCustomizer && entity.displayNameCustomizer) {
5
7
  const calculatedName = getDisplayNameFromEntityInstance(entity, instanceContainer, getInstanceById, getChildInstancesForInstanceId, locales, defaultName, false);
@@ -28,3 +30,23 @@ export const getDisplayNameFromEntityInstance = (entity, instanceContainer, getI
28
30
  return getSerializedDisplayNameFromEntityInstance(serializeEntityDecl(entity), instanceContainer.content, defaultName, locales);
29
31
  }
30
32
  };
33
+ export const getAllInstanceOverviewsByEntityName = (entitiesByName, databaseInMemory, locales) => {
34
+ const getInstanceById = createInstanceFromDatabaseInMemoryGetter(databaseInMemory, entitiesByName);
35
+ const getChildInstancesForInstanceId = createChildInstancesForInstanceIdGetter(entitiesByName, databaseInMemory);
36
+ return Object.fromEntries(getGroupedInstancesFromDatabaseInMemory(databaseInMemory).map(([entityName, instances]) => [
37
+ entityName,
38
+ instances
39
+ .map((instance) => {
40
+ const { name, localeId } = getDisplayNameFromEntityInstance(
41
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
42
+ entitiesByName[entityName], instance, getInstanceById, getChildInstancesForInstanceId, locales);
43
+ return {
44
+ id: instance.id,
45
+ displayName: name,
46
+ displayNameLocaleId: localeId,
47
+ gitStatus: instance.gitStatus,
48
+ };
49
+ })
50
+ .toSorted((a, b) => a.displayName.localeCompare(b.displayName, a.displayNameLocaleId)),
51
+ ]));
52
+ };
@@ -1,4 +1,4 @@
1
- export declare const getErrorMessageForDisplay: (error: Error) => string;
1
+ export declare const getErrorMessageForDisplay: (error: Error, indentation?: number) => string;
2
2
  export declare const countError: (error: Error) => number;
3
3
  export declare const countErrors: (errors: Error[]) => number;
4
4
  export declare const wrapErrorsIfAny: (message: string, errors: Error[]) => AggregateError | undefined;
@@ -1,13 +1,17 @@
1
1
  import { applyIndentation } from "./render.js";
2
- export const getErrorMessageForDisplay = (error) => {
2
+ import { UniqueConstraintError } from "./unique.js";
3
+ export const getErrorMessageForDisplay = (error, indentation = 2) => {
3
4
  if (error instanceof AggregateError) {
4
5
  return `${error.message}\n${applyIndentation(1, error.errors
5
6
  .filter(subError => subError instanceof Error)
6
- .map(subError => getErrorMessageForDisplay(subError))
7
- .join("\n"), 2)}`;
7
+ .map(subError => getErrorMessageForDisplay(subError, indentation))
8
+ .join("\n"), indentation)}`;
8
9
  }
9
10
  else if (error.cause instanceof Error) {
10
- return `${error.message}\n${applyIndentation(1, getErrorMessageForDisplay(error.cause), 2)}`;
11
+ return `${error.message}\n${applyIndentation(1, getErrorMessageForDisplay(error.cause, indentation), indentation)}`;
12
+ }
13
+ else if (error instanceof UniqueConstraintError) {
14
+ return `${error.message}\n${applyIndentation(1, error.parts.join("\n"), indentation)}`;
11
15
  }
12
16
  else {
13
17
  return error.message;
@@ -1,14 +1,18 @@
1
- import type { InstanceContainer } from "../../shared/utils/instances.ts";
1
+ import type { InstanceContainer, InstanceContainerOverview } from "../../shared/utils/instances.ts";
2
2
  import { type Result } from "../../shared/utils/result.ts";
3
3
  import type { EntityDecl } from "../schema/index.ts";
4
4
  import { type DatabaseInMemory } from "./databaseInMemory.ts";
5
+ export declare class UniqueConstraintError extends Error {
6
+ readonly parts: string[];
7
+ constructor(message: string, parts: string[]);
8
+ }
5
9
  /**
6
10
  * Checks all unique constraints for the provided entity and its instances.
7
11
  *
8
12
  * Returns `Ok` when no violations have been found and an `Error` with an
9
13
  * `AggregateError` if there are any violations of any unique constraint.
10
14
  */
11
- export declare const checkUniqueConstraintsForEntity: (entity: EntityDecl, instances: InstanceContainer[]) => Result<void, AggregateError>;
15
+ export declare const checkUniqueConstraintsForEntity: (entity: EntityDecl, instances: InstanceContainer[], instanceOverviews: InstanceContainerOverview[]) => Result<void, AggregateError>;
12
16
  /**
13
17
  * Checks all unique constraints for all provided entities and their instances.
14
18
  *
@@ -16,4 +20,4 @@ export declare const checkUniqueConstraintsForEntity: (entity: EntityDecl, insta
16
20
  * `AggregateError`s for each entity if there are any violations of any unique
17
21
  * constraint.
18
22
  */
19
- export declare const checkUniqueConstraintsForAllEntities: (db: DatabaseInMemory, entities: EntityDecl[]) => Result<void, AggregateError>;
23
+ export declare const checkUniqueConstraintsForAllEntities: (db: DatabaseInMemory, entitiesByName: Record<string, EntityDecl>, locales: string[]) => Result<void, AggregateError>;
@@ -3,13 +3,22 @@ import { anySameIndices, flatCombine } from "../../shared/utils/array.js";
3
3
  import { deepEqual } from "../../shared/utils/compare.js";
4
4
  import { error, isError, mapError, ok } from "../../shared/utils/result.js";
5
5
  import { getInstancesOfEntityFromDatabaseInMemory, } from "./databaseInMemory.js";
6
- const listFormatter = new Intl.ListFormat("en-US", { type: "conjunction" });
7
- const printUniqueConstraint = (constraint) => (Array.isArray(constraint) ? constraint : [constraint])
8
- .map(elem => "keyPath" in elem
6
+ import { getAllInstanceOverviewsByEntityName } from "./displayName.js";
7
+ export class UniqueConstraintError extends Error {
8
+ parts;
9
+ constructor(message, parts) {
10
+ super(message);
11
+ this.parts = parts;
12
+ }
13
+ }
14
+ const printUniqueConstraint = (constraint, values) => (Array.isArray(constraint) ? constraint : [constraint])
15
+ .map((elem, i) => "keyPath" in elem
9
16
  ? renderKeyPath(elem.keyPath) +
10
17
  (elem.keyPathFallback ? "|" + renderKeyPath(elem.keyPathFallback) : "")
11
18
  : renderKeyPath(elem.entityMapKeyPath) +
12
- "[...]." +
19
+ "[" +
20
+ (Array.isArray(values[i]) ? values[i][0] : "...") +
21
+ "]." +
13
22
  (elem.keyPathInEntityMapFallback
14
23
  ? "(" +
15
24
  renderKeyPath(elem.keyPathInEntityMap) +
@@ -31,7 +40,7 @@ const unsafeGetValueAtKeyPath = (value, keyPath) => {
31
40
  * Returns `Ok` when no violations have been found and an `Error` with an
32
41
  * `AggregateError` if there are any violations of any unique constraint.
33
42
  */
34
- export const checkUniqueConstraintsForEntity = (entity, instances) => {
43
+ export const checkUniqueConstraintsForEntity = (entity, instances, instanceOverviews) => {
35
44
  const constraintErrors = [];
36
45
  const constraints = entity.uniqueConstraints ?? [];
37
46
  for (const [constraintIndex, constraint] of constraints.entries()) {
@@ -61,14 +70,17 @@ export const checkUniqueConstraintsForEntity = (entity, instances) => {
61
70
  constraintErrors.push([
62
71
  constraintIndex,
63
72
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- Indices returned by anySameIndices must exist
64
- duplicates.map(duplicateSet => duplicateSet.map(rowIndex => index[rowIndex][0])),
73
+ duplicates.map(duplicateSet => duplicateSet.map(rowIndex => index[rowIndex])),
65
74
  ]);
66
75
  }
67
76
  }
68
77
  if (constraintErrors.length > 0) {
69
- return error(new AggregateError(constraintErrors.map(([constraintIndex, constraintErrors]) => new AggregateError(constraintErrors.map(error => new Error(`instances ${listFormatter.format(error)} contain duplicate values`)),
78
+ return error(new AggregateError(constraintErrors.flatMap(([constraintIndex, constraintErrors]) => constraintErrors.map(error => new UniqueConstraintError(
70
79
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- constraint must be present
71
- `in unique constraint ${printUniqueConstraint(constraints[constraintIndex])}`)), `in entity "${entity.name}"`));
80
+ `for unique constraint ${printUniqueConstraint(constraints[constraintIndex], error[0][1])}:`, error.map(row => {
81
+ const instanceOverview = instanceOverviews.find(o => o.id === row[0]);
82
+ return instanceOverview ? `"${instanceOverview.displayName}" (${row[0]})` : row[0];
83
+ })))), `in entity "${entity.name}"`));
72
84
  }
73
85
  return ok();
74
86
  };
@@ -79,16 +91,19 @@ export const checkUniqueConstraintsForEntity = (entity, instances) => {
79
91
  * `AggregateError`s for each entity if there are any violations of any unique
80
92
  * constraint.
81
93
  */
82
- export const checkUniqueConstraintsForAllEntities = (db, entities) => mapError(entities.reduce((acc, entity) => {
83
- const resultForEntity = checkUniqueConstraintsForEntity(entity, getInstancesOfEntityFromDatabaseInMemory(db, entity.name));
84
- if (isError(acc)) {
94
+ export const checkUniqueConstraintsForAllEntities = (db, entitiesByName, locales) => {
95
+ const instanceOverviewsByEntityName = getAllInstanceOverviewsByEntityName(entitiesByName, db, locales);
96
+ return mapError(Object.values(entitiesByName).reduce((acc, entity) => {
97
+ const resultForEntity = checkUniqueConstraintsForEntity(entity, getInstancesOfEntityFromDatabaseInMemory(db, entity.name), instanceOverviewsByEntityName[entity.name] ?? []);
98
+ if (isError(acc)) {
99
+ if (isError(resultForEntity)) {
100
+ return error([...acc.error, resultForEntity.error]);
101
+ }
102
+ return acc;
103
+ }
85
104
  if (isError(resultForEntity)) {
86
- return error([...acc.error, resultForEntity.error]);
105
+ return error([resultForEntity.error]);
87
106
  }
88
107
  return acc;
89
- }
90
- if (isError(resultForEntity)) {
91
- return error([resultForEntity.error]);
92
- }
93
- return acc;
94
- }, ok()), errors => new AggregateError(errors, "at least one unique constraint has been violated"));
108
+ }, ok()), errors => new AggregateError(errors.toSorted((a, b) => a.message.localeCompare(b.message)), "at least one unique constraint has been violated"));
109
+ };
@@ -48,7 +48,7 @@ export interface GetAllInstancesResponseBody {
48
48
  instances: {
49
49
  [entity: string]: {
50
50
  id: string;
51
- name: string;
51
+ displayName: string;
52
52
  displayNameLocaleId?: string;
53
53
  }[];
54
54
  };
@@ -15,3 +15,4 @@ export declare const modifyD: <T>(dict: Dictionary<T>, key: string, modifyFn: (c
15
15
  export declare const findD: <T>(dict: Dictionary<T>, predicate: (value: T, key: string) => boolean) => T | undefined;
16
16
  export declare const mapFirstD: <T, U>(dict: Dictionary<T>, mapFn: (value: T, key: string) => U | undefined) => U | undefined;
17
17
  export declare const mapD: <T, U>(dict: Dictionary<T>, mapFn: (value: T, key: string) => U) => Dictionary<U>;
18
+ export declare const reduceD: <T, U>(dict: Dictionary<T>, reducer: (acc: U, value: T, key: string) => U, initialValue: U) => U;
@@ -74,3 +74,4 @@ export const mapD = (dict, mapFn) => {
74
74
  }
75
75
  return [newRecord, dict[1]];
76
76
  };
77
+ export const reduceD = (dict, reducer, initialValue) => Object.entries(dict[0]).reduce((acc, [key, item]) => reducer(acc, item, key), initialValue);
@@ -1,5 +1,5 @@
1
1
  import type { EntityDecl } from "../../node/schema/index.ts";
2
- import type { GetInstanceById } from "../../node/server/index.ts";
2
+ import type { InstanceFromDatabaseInMemoryGetter } from "../../node/utils/databaseInMemory.ts";
3
3
  import { type GetChildInstancesForInstanceId } from "../../node/utils/displayName.ts";
4
4
  import type { GitFileStatus } from "./git.ts";
5
5
  export type InstanceContent = object;
@@ -14,4 +14,4 @@ export interface InstanceContainerOverview {
14
14
  displayName: string;
15
15
  displayNameLocaleId?: string;
16
16
  }
17
- export declare const getInstanceContainerOverview: (entity: EntityDecl, instanceContainer: InstanceContainer, getInstanceById: GetInstanceById, getChildInstancesForInstanceId: GetChildInstancesForInstanceId, locales: string[]) => InstanceContainerOverview;
17
+ export declare const getInstanceContainerOverview: (entity: EntityDecl, instanceContainer: InstanceContainer, getInstanceById: InstanceFromDatabaseInMemoryGetter, getChildInstancesForInstanceId: GetChildInstancesForInstanceId, locales: string[]) => InstanceContainerOverview;
@@ -16,11 +16,11 @@ export const NestedEntityMapTypeInput = props => {
16
16
  const secondaryInstances = (instanceNamesByEntity[type.secondaryEntity] ?? [])
17
17
  .slice()
18
18
  .filter(instance => !existingKeys.includes(instance.id))
19
- .sort((a, b) => a.name.localeCompare(b.name, undefined, { numeric: true }));
19
+ .sort((a, b) => a.displayName.localeCompare(b.displayName, undefined, { numeric: true }));
20
20
  const isLocaleEntity = checkIsLocaleEntity(type.secondaryEntity);
21
21
  return (_jsxs("div", { class: "field field--container field--nestedentitymap", children: [existingKeys.length > 0 && (_jsx("ul", { children: Object.entries(value).map(([key, item]) => {
22
22
  const name = instanceNamesByEntity[type.secondaryEntity]?.find(instance => instance.id === key)
23
- ?.name ?? key;
23
+ ?.displayName ?? key;
24
24
  return (_jsxs("li", { class: "container-item dict-item", ...(isLocaleEntity ? { lang: key } : {}), children: [_jsxs("div", { className: "container-item-header", children: [_jsx("div", { className: "container-item-title", children: _jsxs("span", { children: [_jsx("strong", { children: name }), " ", _jsx("span", { className: "id", children: key })] }) }), _jsx("div", { className: "btns", children: _jsxs("button", { class: "destructive", onClick: () => {
25
25
  const newObj = { ...value };
26
26
  // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
@@ -31,7 +31,7 @@ export const NestedEntityMapTypeInput = props => {
31
31
  } })] }, key));
32
32
  }) })), _jsxs("div", { class: "add-item-container", children: [_jsxs(Select, { value: newKey, onInput: event => {
33
33
  setNewKey(event.currentTarget.value);
34
- }, disabled: disabled || secondaryInstances.length === 0, children: [secondaryInstances.length === 0 ? (_jsx("option", { value: "", disabled: true, children: "No instances available" })) : (_jsx("option", { value: "", disabled: true, children: "No selected instance" })), secondaryInstances.map(instance => (_jsx("option", { value: instance.id, children: instance.name }, instance.id)))] }), _jsxs("button", { onClick: () => {
34
+ }, disabled: disabled || secondaryInstances.length === 0, children: [secondaryInstances.length === 0 ? (_jsx("option", { value: "", disabled: true, children: "No instances available" })) : (_jsx("option", { value: "", disabled: true, children: "No selected instance" })), secondaryInstances.map(instance => (_jsx("option", { value: instance.id, children: instance.displayName }, instance.id)))] }), _jsxs("button", { onClick: () => {
35
35
  onChange(sortObjectKeysAlphabetically({
36
36
  ...value,
37
37
  [newKey]: createTypeSkeleton(getDeclFromDeclName, type.type),
@@ -9,5 +9,5 @@ export const ReferenceIdentifierTypeInput = ({ type, value, instanceNamesByEntit
9
9
  const instances = instanceNamesByEntity[type.entity] ?? [];
10
10
  return (_jsxs("div", { class: "field", children: [_jsxs(Select, { value: value, onInput: event => {
11
11
  onChange(event.currentTarget.value);
12
- }, disabled: disabled || instances.length === 0, "aria-invalid": !value, children: [instances.length === 0 ? (_jsx("option", { value: "", disabled: true, children: "No instances available" })) : (_jsx("option", { value: "", disabled: true, children: "No selected instance" })), instances.map(instance => (_jsx("option", { value: instance.id, children: instance.name }, instance.id)))] }), _jsx(ValidationErrors, { disabled: disabled, errors: !value ? [ReferenceError("no reference provided")] : [] })] }));
12
+ }, disabled: disabled || instances.length === 0, "aria-invalid": !value, children: [instances.length === 0 ? (_jsx("option", { value: "", disabled: true, children: "No instances available" })) : (_jsx("option", { value: "", disabled: true, children: "No selected instance" })), instances.map(instance => (_jsx("option", { value: instance.id, children: instance.displayName }, instance.id)))] }), _jsx(ValidationErrors, { disabled: disabled, errors: !value ? [ReferenceError("no reference provided")] : [] })] }));
13
13
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tsondb",
3
- "version": "0.13.1",
3
+ "version": "0.14.0",
4
4
  "description": "",
5
5
  "license": "ISC",
6
6
  "author": "Lukas Obermann",