tsondb 0.12.8 → 0.13.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 (31) hide show
  1. package/dist/src/bin/tsondb.js +11 -6
  2. package/dist/src/node/config.d.ts +15 -2
  3. package/dist/src/node/index.d.ts +10 -4
  4. package/dist/src/node/index.js +30 -7
  5. package/dist/src/node/renderers/ts/render.d.ts +10 -1
  6. package/dist/src/node/renderers/ts/render.js +56 -12
  7. package/dist/src/node/schema/Node.d.ts +6 -4
  8. package/dist/src/node/schema/Node.js +2 -1
  9. package/dist/src/node/schema/Schema.js +65 -0
  10. package/dist/src/node/schema/declarations/EntityDecl.d.ts +4 -0
  11. package/dist/src/node/schema/declarations/TypeAliasDecl.d.ts +2 -2
  12. package/dist/src/node/schema/types/generic/TranslationObjectType.js +25 -6
  13. package/dist/src/node/server/index.d.ts +3 -1
  14. package/dist/src/node/server/index.js +2 -2
  15. package/dist/src/node/server/init.d.ts +2 -1
  16. package/dist/src/node/server/init.js +2 -1
  17. package/dist/src/node/server/utils/instanceOperations.js +3 -3
  18. package/dist/src/node/utils/childInstances.d.ts +2 -1
  19. package/dist/src/node/utils/childInstances.js +6 -6
  20. package/dist/src/node/utils/databaseTransactions.js +5 -0
  21. package/dist/src/node/utils/instanceTransactionSteps.d.ts +3 -2
  22. package/dist/src/node/utils/instanceTransactionSteps.js +7 -7
  23. package/dist/src/node/utils/unique.d.ts +19 -0
  24. package/dist/src/node/utils/unique.js +94 -0
  25. package/dist/src/shared/schema/declarations/EntityDecl.d.ts +28 -0
  26. package/dist/src/shared/schema/declarations/EntityDecl.js +2 -0
  27. package/dist/src/shared/utils/array.d.ts +18 -0
  28. package/dist/src/shared/utils/array.js +32 -0
  29. package/dist/src/shared/utils/translation.d.ts +5 -0
  30. package/dist/src/shared/utils/translation.js +28 -2
  31. package/package.json +1 -1
@@ -15,6 +15,7 @@ import { pathToFileURL } from "node:url";
15
15
  import { parseArguments } from "simple-cli-args";
16
16
  import { validateConfigForFormatting, validateConfigForGeneration, validateConfigForServer, validateConfigForTesting, } from "../node/config.js";
17
17
  import { format, generateOutputs, serve, validate } from "../node/index.js";
18
+ import { omitUndefinedKeys } from "../shared/utils/object.js";
18
19
  const debug = Debug("tsondb:cli");
19
20
  const passedArguments = parseArguments({
20
21
  commands: {
@@ -25,7 +26,11 @@ const passedArguments = parseArguments({
25
26
  name: "check-referential-integrity",
26
27
  type: Boolean,
27
28
  },
28
- entities: {
29
+ checkTranslationParameters: {
30
+ name: "check-translation-parameters",
31
+ type: Boolean,
32
+ },
33
+ checkOnlyEntities: {
29
34
  name: "entities",
30
35
  alias: "e",
31
36
  multiple: true,
@@ -87,7 +92,7 @@ switch (passedArguments.command.name) {
87
92
  case "serve":
88
93
  debug(`running command: serve`);
89
94
  validateConfigForServer(config);
90
- await serve(config.schema, config.dataRootPath, config.defaultLocales, config.homeLayoutSections, config.serverOptions, config.customStylesheetPath);
95
+ await serve(config.schema, config.dataRootPath, config.defaultLocales, config.homeLayoutSections, config.serverOptions, config.validationOptions, config.customStylesheetPath);
91
96
  break;
92
97
  case "validate":
93
98
  debug(`running command: validate`);
@@ -95,13 +100,13 @@ switch (passedArguments.command.name) {
95
100
  if (passedArguments.command.options?.checkReferentialIntegrity !== undefined) {
96
101
  debug(`check referential integrity: ${passedArguments.command.options.checkReferentialIntegrity ? "yes" : "no"}`);
97
102
  }
98
- if (passedArguments.command.options?.entities !== undefined) {
99
- const entities = passedArguments.command.options.entities;
103
+ if (passedArguments.command.options?.checkOnlyEntities !== undefined) {
104
+ const entities = passedArguments.command.options.checkOnlyEntities;
100
105
  debug(`only check the following entities: ${entities.join(", ")}`);
101
106
  }
102
107
  await validate(config.schema, config.dataRootPath, {
103
- checkReferentialIntegrity: passedArguments.command.options?.checkReferentialIntegrity,
104
- checkOnlyEntities: passedArguments.command.options?.entities,
108
+ ...config.validationOptions,
109
+ ...omitUndefinedKeys(passedArguments.command.options ?? {}),
105
110
  });
106
111
  break;
107
112
  case "format":
@@ -1,4 +1,5 @@
1
1
  import type { Output } from "../shared/output.ts";
2
+ import type { ValidationOptions } from "./index.ts";
2
3
  import type { EntityDecl } from "./schema/index.ts";
3
4
  import type { Schema } from "./schema/Schema.ts";
4
5
  /**
@@ -12,6 +13,7 @@ export type Config = {
12
13
  dataRootPath?: string;
13
14
  homeLayoutSections?: HomeLayoutSection[];
14
15
  customStylesheetPath?: string;
16
+ validationOptions?: Partial<ValidationOptions>;
15
17
  };
16
18
  export type ServerOptions = {
17
19
  port: number;
@@ -43,8 +45,19 @@ export type ServerConfig = DataConfig & {
43
45
  serverOptions?: ServerOptions;
44
46
  defaultLocales: string[];
45
47
  homeLayoutSections?: HomeLayoutSection[];
48
+ validationOptions?: Partial<ValidationOptions>;
46
49
  customStylesheetPath?: string;
47
50
  };
51
+ /**
52
+ * The configuration type required for validating the contents of the database.
53
+ */
54
+ export type TestingConfig = DataConfig & {
55
+ validationOptions?: Partial<ValidationOptions>;
56
+ };
57
+ /**
58
+ * The configuration type required for formatting the contents of the database.
59
+ */
60
+ export type FormattingConfig = DataConfig;
48
61
  export declare const validateConfigForServer: (config: Config) => asserts config is ServerConfig;
49
- export declare const validateConfigForTesting: (config: Config) => asserts config is DataConfig;
50
- export declare const validateConfigForFormatting: (config: Config) => asserts config is DataConfig;
62
+ export declare const validateConfigForTesting: (config: Config) => asserts config is TestingConfig;
63
+ export declare const validateConfigForFormatting: (config: Config) => asserts config is FormattingConfig;
@@ -3,13 +3,19 @@ import type { HomeLayoutSection } from "./config.ts";
3
3
  import { type Schema } from "./schema/Schema.ts";
4
4
  import type { ServerOptions } from "./server/index.ts";
5
5
  export declare const generateOutputs: (schema: Schema, outputs: Output[]) => Promise<void>;
6
- type ValidationOptions = {
6
+ export type ValidationOptions = {
7
7
  checkReferentialIntegrity: boolean;
8
8
  checkOnlyEntities: string[];
9
+ checkTranslations?: {
10
+ format: "mf2";
11
+ /**
12
+ * If set to `true`, translation keys will be treated as message format strings and their parameters must match the ones in the values.
13
+ */
14
+ matchParametersInKeys?: boolean;
15
+ };
9
16
  };
10
17
  export declare const validate: (schema: Schema, dataRootPath: string, options?: Partial<ValidationOptions>) => Promise<void>;
11
18
  export declare const generateAndValidate: (schema: Schema, outputs: Output[], dataRootPath: string, validationOptions?: Partial<ValidationOptions>) => Promise<void>;
12
- export declare const serve: (schema: Schema, dataRootPath: string, defaultLocales: string[], homeLayoutSections?: HomeLayoutSection[], serverOptions?: Partial<ServerOptions>, customStylesheetPath?: string) => Promise<void>;
13
- export declare const generateValidateAndServe: (schema: Schema, outputs: Output[], dataRootPath: string, defaultLocales: string[], homeLayoutSections?: HomeLayoutSection[], serverOptions?: Partial<ServerOptions>, 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>;
14
21
  export declare const format: (schema: Schema, dataRootPath: string) => Promise<void>;
15
- export {};
@@ -2,14 +2,16 @@ import Debug from "debug";
2
2
  import { mkdir } from "fs/promises";
3
3
  import { join, sep } from "path";
4
4
  import { styleText } from "util";
5
+ import { isError } from "../shared/utils/result.js";
5
6
  import { parallelizeErrors } from "../shared/utils/validation.js";
6
7
  import { validateEntityDecl } from "./schema/declarations/EntityDecl.js";
7
- import { createValidators } from "./schema/Node.js";
8
+ import { createValidationContext } from "./schema/Node.js";
8
9
  import { getEntities } from "./schema/Schema.js";
9
10
  import { createServer } from "./server/index.js";
10
11
  import { asyncForEachInstanceInDatabaseInMemory, createDatabaseInMemory, getInstancesOfEntityFromDatabaseInMemory, } from "./utils/databaseInMemory.js";
11
12
  import { countErrors, getErrorMessageForDisplay, wrapErrorsIfAny } from "./utils/error.js";
12
13
  import { getFileNameForId, writeInstance } from "./utils/files.js";
14
+ import { checkUniqueConstraintsForAllEntities } from "./utils/unique.js";
13
15
  const debug = Debug("tsondb:jsapi");
14
16
  const prepareFolders = async (dataRootPath, entities) => {
15
17
  await mkdir(dataRootPath, { recursive: true });
@@ -30,12 +32,33 @@ const _validate = (dataRootPath, entities, databaseInMemory, options = {}) => {
30
32
  throw new Error(`Entity "${onlyEntity}" not found in schema`);
31
33
  }
32
34
  }
33
- const validationHelpers = createValidators(databaseInMemory, true, checkReferentialIntegrity);
35
+ const validationContext = createValidationContext(options, databaseInMemory, true, checkReferentialIntegrity);
36
+ debug("Checking structural integrity ...");
34
37
  const errors = (checkOnlyEntities.length > 0
35
38
  ? entities.filter(entity => checkOnlyEntities.includes(entity.name))
36
39
  : entities)
37
- .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(validationHelpers, [], entity, instance.content)))))
40
+ .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)))))
38
41
  .toSorted((a, b) => a.message.localeCompare(b.message));
42
+ if (errors.length > 0) {
43
+ debug(`${errors.length.toString()} structural integrity violation${errors.length === 1 ? "" : "s"} found`);
44
+ }
45
+ else {
46
+ debug("No structural integrity violations found");
47
+ }
48
+ if (errors.length === 0) {
49
+ debug("Checking unique constraints ...");
50
+ const constraintResult = checkUniqueConstraintsForAllEntities(databaseInMemory, entities);
51
+ if (isError(constraintResult)) {
52
+ debug(`${constraintResult.error.errors.length.toString()} unique constraint violation${constraintResult.error.errors.length === 1 ? "" : "s"} found`);
53
+ errors.push(constraintResult.error);
54
+ }
55
+ else {
56
+ debug("No unique constraint violations found");
57
+ }
58
+ }
59
+ else {
60
+ debug("Skipping unique constraint checks due to previous structural integrity errors");
61
+ }
39
62
  if (errors.length === 0) {
40
63
  debug("All entities are valid");
41
64
  }
@@ -57,7 +80,7 @@ export const generateAndValidate = async (schema, outputs, dataRootPath, validat
57
80
  const databaseInMemory = await createDatabaseInMemory(dataRootPath, entities);
58
81
  _validate(dataRootPath, entities, databaseInMemory, validationOptions);
59
82
  };
60
- export const serve = async (schema, dataRootPath, defaultLocales, homeLayoutSections, serverOptions, customStylesheetPath) => {
83
+ export const serve = async (schema, dataRootPath, defaultLocales, homeLayoutSections, serverOptions, validationOptions, customStylesheetPath) => {
61
84
  if (defaultLocales.length === 0) {
62
85
  throw new Error("At least one default locale must be specified to start the server.");
63
86
  }
@@ -66,15 +89,15 @@ export const serve = async (schema, dataRootPath, defaultLocales, homeLayoutSect
66
89
  debug("prepared folders");
67
90
  const databaseInMemory = await createDatabaseInMemory(dataRootPath, entities);
68
91
  debug("loaded instances");
69
- await createServer(schema, dataRootPath, databaseInMemory, defaultLocales, homeLayoutSections, serverOptions, customStylesheetPath);
92
+ await createServer(schema, dataRootPath, databaseInMemory, defaultLocales, homeLayoutSections, serverOptions, validationOptions, customStylesheetPath);
70
93
  };
71
- export const generateValidateAndServe = async (schema, outputs, dataRootPath, defaultLocales, homeLayoutSections, serverOptions, validationOptions) => {
94
+ export const generateValidateAndServe = async (schema, outputs, dataRootPath, defaultLocales, homeLayoutSections, serverOptions, validationOptions, customStylesheetPath) => {
72
95
  await generateOutputs(schema, outputs);
73
96
  const entities = getEntities(schema);
74
97
  await prepareFolders(dataRootPath, entities);
75
98
  const databaseInMemory = await createDatabaseInMemory(dataRootPath, entities);
76
99
  _validate(dataRootPath, entities, databaseInMemory, validationOptions);
77
- await createServer(schema, dataRootPath, databaseInMemory, defaultLocales, homeLayoutSections, serverOptions);
100
+ await createServer(schema, dataRootPath, databaseInMemory, defaultLocales, homeLayoutSections, serverOptions, validationOptions, customStylesheetPath);
78
101
  };
79
102
  export const format = async (schema, dataRootPath) => {
80
103
  const entities = getEntities(schema);
@@ -3,7 +3,16 @@ export type TypeScriptRendererOptions = {
3
3
  indentation: number;
4
4
  objectTypeKeyword: "interface" | "type";
5
5
  preserveFiles: boolean;
6
- generateEntityMapType: boolean;
6
+ generateHelpers: Partial<{
7
+ /**
8
+ * If `true` or a string, generates an object type with all names of the entities as the key and their corresponding generated type as their respective value. Uses `EntityMap` as a default name when using `true` or the string if set to a string.
9
+ */
10
+ entityMap: boolean | string;
11
+ /**
12
+ * If `true` or a string, generates an object type with all names of entities with parent references as the key and their corresponding generated type as well as their parent reference key as their respective value. Uses `ChildEntityMap` as a default name when using `true` or the string if set to a string.
13
+ */
14
+ childEntityMap: boolean | string;
15
+ }>;
7
16
  addIdentifierToEntities: boolean;
8
17
  /**
9
18
  * Infer translation parameter types from the message strings in a {@link TranslationObjectType TranslationObject} as branded types.
@@ -6,7 +6,7 @@ import { toCamelCase } from "../../../shared/utils/string.js";
6
6
  import { extractParameterTypeNamesFromMessage, mapParameterTypeNames, } from "../../../shared/utils/translation.js";
7
7
  import { assertExhaustive } from "../../../shared/utils/typeSafety.js";
8
8
  import { asDecl } from "../../schema/declarations/Declaration.js";
9
- import { addEphemeralUUIDToType, createEntityIdentifierTypeAsDecl, isEntityDecl, } from "../../schema/declarations/EntityDecl.js";
9
+ import { addEphemeralUUIDToType, createEntityIdentifierTypeAsDecl, isEntityDecl, isEntityDeclWithParentReference, } from "../../schema/declarations/EntityDecl.js";
10
10
  import { TypeAliasDecl } from "../../schema/declarations/TypeAliasDecl.js";
11
11
  import { flatMapAuxiliaryDecls, NodeKind } from "../../schema/Node.js";
12
12
  import { ArrayType } from "../../schema/types/generic/ArrayType.js";
@@ -20,7 +20,7 @@ const defaultOptions = {
20
20
  indentation: 2,
21
21
  objectTypeKeyword: "interface",
22
22
  preserveFiles: false,
23
- generateEntityMapType: false,
23
+ generateHelpers: {},
24
24
  addIdentifierToEntities: false,
25
25
  };
26
26
  const renderDocumentation = (comment, isDeprecated) => syntax `${comment === undefined
@@ -158,20 +158,63 @@ const renderImports = (currentUrl, imports) => {
158
158
  .join(EOL);
159
159
  return importsSyntax.length > 0 ? importsSyntax + EOL + EOL : "";
160
160
  };
161
- const renderEntityMapType = (options, declarations) => syntax `export type EntityMap = {${EOL}${indent(options.indentation, 1, combineSyntaxes(declarations
162
- .filter(isEntityDecl)
163
- .sort((a, b) => a.name.localeCompare(b.name))
164
- .map(decl => syntax `${decl.name}: ${decl.name}`), EOL))}${EOL}}${EOL + EOL}`;
161
+ const renderEntityMapType = (options, declarations) => options.generateHelpers.entityMap
162
+ ? `export type ${options.generateHelpers.entityMap === true ? "EntityMap" : options.generateHelpers.entityMap} = {${EOL}${prefixLines(getIndentation(options.indentation, 1), declarations
163
+ .filter(isEntityDecl)
164
+ .sort((a, b) => a.name.localeCompare(b.name))
165
+ .map(decl => `${decl.name}: ${decl.name}`)
166
+ .join(EOL))}${EOL}}${EOL + EOL}`
167
+ : "";
168
+ const renderChildEntityMapType = (options, declarations) => options.generateHelpers.childEntityMap
169
+ ? `export type ${options.generateHelpers.childEntityMap === true ? "ChildEntityMap" : options.generateHelpers.childEntityMap} = {${EOL}${prefixLines(getIndentation(options.indentation, 1), declarations
170
+ .filter(isEntityDecl)
171
+ .filter(isEntityDeclWithParentReference)
172
+ .sort((a, b) => a.name.localeCompare(b.name))
173
+ .map(decl => `${decl.name}: [${decl.name}, "${decl.parentReferenceKey}", ${decl.name}["${decl.parentReferenceKey}"]]`)
174
+ .join(EOL))}${EOL}}${EOL + EOL}`
175
+ : "";
165
176
  const renderStringableTranslationParameterType = (options) => options.inferTranslationParameters?.format === "mf2"
166
- ? "export type StringableTranslationParameter = {\n" +
177
+ ? "export type StringableTranslationParameter = {" +
178
+ EOL +
167
179
  prefixLines(getIndentation(options.indentation, 1), "toString(): string") +
168
- "\n}\n\n"
180
+ EOL +
181
+ "}" +
182
+ EOL +
183
+ EOL
169
184
  : "";
185
+ // const renderGetterConstruct = (
186
+ // config: GetterOptions | undefined,
187
+ // getterType: "all" | "byId",
188
+ // defaultReturnTypeBuilder: (entity: EntityDecl) => string,
189
+ // declarations: readonly Decl[],
190
+ // ) => {
191
+ // if (!config) {
192
+ // return ""
193
+ // }
194
+ // const { returnTypeBuilder = defaultReturnTypeBuilder } = config
195
+ // const getLine: (decl: EntityDecl) => string = decl =>
196
+ // `export type ${decl.name} = (${getterType === "byId" ? `id: ${decl.name}_ID` : ""}) => ${returnTypeBuilder(decl)}`
197
+ // return (declarations.filter(isEntityDecl).map(getLine).join(EOL), EOL + EOL)
198
+ // }
199
+ // const renderGetters = (
200
+ // config:
201
+ // | Partial<{
202
+ // byId: GetterOptions
203
+ // all: GetterOptions
204
+ // }>
205
+ // | undefined,
206
+ // declarations: readonly Decl[],
207
+ // ): string =>
208
+ // (
209
+ // [
210
+ // ["all", (entity: EntityDecl) => entity.name + "[]"],
211
+ // ["byId", (entity: EntityDecl) => entity.name + " | undefined"],
212
+ // ] as const
213
+ // )
214
+ // .map(key => renderGetterConstruct(config?.[key[0]], key[0], key[1], declarations))
215
+ // .join("")
170
216
  export const render = (options = defaultOptions, declarations) => {
171
217
  const finalOptions = { ...defaultOptions, ...options };
172
- const [_, entityMap] = finalOptions.generateEntityMapType
173
- ? renderEntityMapType(finalOptions, declarations)
174
- : emptyRenderResult;
175
218
  const [imports, content] = renderDeclarations(finalOptions, flatMapAuxiliaryDecls((parentNodes, node) => {
176
219
  if (isNestedEntityMapType(node)) {
177
220
  return TypeAliasDecl(asDecl(parentNodes[0])?.sourceUrl ?? "", {
@@ -185,7 +228,8 @@ export const render = (options = defaultOptions, declarations) => {
185
228
  }
186
229
  return undefined;
187
230
  }, declarations));
188
- return (entityMap +
231
+ return (renderEntityMapType(finalOptions, declarations) +
232
+ renderChildEntityMapType(finalOptions, declarations) +
189
233
  renderStringableTranslationParameterType(finalOptions) +
190
234
  (finalOptions.preserveFiles
191
235
  ? (declarations[0] === undefined ? "" : renderImports(declarations[0].sourceUrl, imports)) +
@@ -17,6 +17,7 @@ import type { SerializedReferenceIdentifierType } from "../../shared/schema/type
17
17
  import type { SerializedStringType } from "../../shared/schema/types/StringType.ts";
18
18
  import type { SerializedTranslationObjectType } from "../../shared/schema/types/TranslationObjectType.ts";
19
19
  import type { SerializedTypeArgumentType } from "../../shared/schema/types/TypeArgumentType.ts";
20
+ import type { ValidationOptions } from "../index.ts";
20
21
  import { type DatabaseInMemory } from "../utils/databaseInMemory.ts";
21
22
  import { type Decl, type IncludableDeclP } from "./declarations/Declaration.ts";
22
23
  import { type EntityDecl } from "./declarations/EntityDecl.ts";
@@ -58,16 +59,17 @@ export type IdentifierToCheck = {
58
59
  name: string;
59
60
  value: unknown;
60
61
  };
61
- export interface Validators {
62
+ export interface ValidationContext {
62
63
  useStyling: boolean;
63
64
  checkReferentialIntegrity: (identifier: IdentifierToCheck) => Error[];
65
+ checkTranslations?: ValidationOptions["checkTranslations"];
64
66
  }
65
- export declare const createValidators: (databaseInMemory: DatabaseInMemory, useStyling: boolean, checkReferentialIntegrity?: boolean) => Validators;
67
+ export declare const createValidationContext: (options: Partial<ValidationOptions>, databaseInMemory: DatabaseInMemory, useStyling: boolean, checkReferentialIntegrity?: boolean) => ValidationContext;
66
68
  export type Predicate<T extends Node> = (node: Node) => node is T;
67
69
  export type GetNestedDeclarations<T extends Node = Node> = (addedDecls: NestedDecl[], node: T, parentDecl: Decl | undefined) => NestedDecl[];
68
70
  export declare const getNestedDeclarations: GetNestedDeclarations;
69
- export type Validator<T extends Node = Node> = (helpers: Validators, inDecls: Decl[], node: T, value: unknown) => Error[];
70
- export type ValidatorOfParamDecl<T extends Node = Node> = (helpers: Validators, inDecls: Decl[], node: T, typeArgs: Type[], value: unknown) => Error[];
71
+ export type Validator<T extends Node = Node> = (helpers: ValidationContext, inDecls: Decl[], node: T, value: unknown) => Error[];
72
+ export type ValidatorOfParamDecl<T extends Node = Node> = (helpers: ValidationContext, inDecls: Decl[], node: T, typeArgs: Type[], value: unknown) => Error[];
71
73
  export declare const validateDecl: ValidatorOfParamDecl<Decl>;
72
74
  export declare const validateType: Validator<Type>;
73
75
  export type NodeWithResolvedTypeArguments<T extends Node | null> = T extends BooleanType | DateType | FloatType | IntegerType | StringType | ReferenceIdentifierType | ChildEntitiesType | TranslationObjectType ? T : T extends EntityDecl<infer N, infer P, infer FK> ? EntityDecl<N, {
@@ -81,7 +81,7 @@ export const flatMapAuxiliaryDecls = (callbackFn, declarations) => reduceNodes((
81
81
  });
82
82
  return declsWithCurrentDecl.concat(normalizedResult);
83
83
  }, declarations);
84
- export const createValidators = (databaseInMemory, useStyling, checkReferentialIntegrity = true) => ({
84
+ export const createValidationContext = (options, databaseInMemory, useStyling, checkReferentialIntegrity = true) => ({
85
85
  useStyling,
86
86
  checkReferentialIntegrity: checkReferentialIntegrity
87
87
  ? ({ name, value }) => getInstancesOfEntityFromDatabaseInMemory(databaseInMemory, name).some(instance => instance.id === value)
@@ -90,6 +90,7 @@ export const createValidators = (databaseInMemory, useStyling, checkReferentialI
90
90
  ReferenceError(`Invalid reference to instance of entity ${entity(`"${name}"`, useStyling)} with identifier ${json(value, useStyling)}`),
91
91
  ]
92
92
  : () => [],
93
+ checkTranslations: options.checkTranslations,
93
94
  });
94
95
  export const getNestedDeclarations = (addedDecls, node, parentDecl) => {
95
96
  switch (node.kind) {
@@ -1,4 +1,7 @@
1
1
  import Debug from "debug";
2
+ import { normalizeKeyPath, renderKeyPath, } from "../../shared/schema/declarations/EntityDecl.js";
3
+ import { anySame } from "../../shared/utils/array.js";
4
+ import { deepEqual } from "../../shared/utils/compare.js";
2
5
  import { assertExhaustive } from "../../shared/utils/typeSafety.js";
3
6
  import { getParameterNames, walkNodeTree } from "./declarations/Declaration.js";
4
7
  import { isEntityDecl } from "./declarations/EntityDecl.js";
@@ -215,6 +218,66 @@ const checkRecursiveGenericTypeAliasesAndEnumerationsAreOnlyParameterizedDirectl
215
218
  }, decl);
216
219
  }
217
220
  };
221
+ const getNodeAtKeyPath = (decl, objectType, keyPath, parent, parentPath = []) => {
222
+ const [key, ...keyPathRest] = normalizeKeyPath(keyPath);
223
+ if (key === undefined) {
224
+ return objectType;
225
+ }
226
+ const memberDecl = objectType.properties[key];
227
+ if (memberDecl === undefined) {
228
+ throw TypeError(`key "${key}"${parentPath.length === 0 ? "" : " in " + renderKeyPath(parentPath)} in unique constraint of entity "${decl.name}" does not exist in the entity`);
229
+ }
230
+ const value = memberDecl.type;
231
+ const actualValue = isIncludeIdentifierType(value) ? value.reference.type.value : value;
232
+ if (keyPathRest.length > 0) {
233
+ if (isObjectType(actualValue)) {
234
+ return getNodeAtKeyPath(decl, actualValue, keyPathRest, parent, [...parentPath, key]);
235
+ }
236
+ throw TypeError(`value at key "${key}"${parentPath.length === 0 ? "" : ' in "' + renderKeyPath(parentPath) + '"'}${parent ? " " + parent : ""} in unique constraint of entity "${decl.name}" does not contain an object type`);
237
+ }
238
+ return value;
239
+ };
240
+ const checkUniqueConstraintElement = (decl, element) => {
241
+ if ("keyPath" in element) {
242
+ getNodeAtKeyPath(decl, decl.type.value, element.keyPath);
243
+ if (element.keyPathFallback !== undefined) {
244
+ getNodeAtKeyPath(decl, decl.type.value, element.keyPathFallback);
245
+ }
246
+ }
247
+ else {
248
+ const entityMapType = getNodeAtKeyPath(decl, decl.type.value, element.entityMapKeyPath);
249
+ if (!isNestedEntityMapType(entityMapType)) {
250
+ throw TypeError(`value at key "${renderKeyPath(element.entityMapKeyPath)}" is not a nested entity map as required by the unique constraint of entity "${decl.name}"`);
251
+ }
252
+ const nestedType = entityMapType.type.value;
253
+ const actualType = isIncludeIdentifierType(nestedType)
254
+ ? nestedType.reference.type.value
255
+ : nestedType;
256
+ getNodeAtKeyPath(decl, actualType, element.keyPathInEntityMap, `in entity map "${renderKeyPath(element.entityMapKeyPath)}"`);
257
+ if (element.keyPathInEntityMapFallback !== undefined) {
258
+ getNodeAtKeyPath(decl, actualType, element.keyPathInEntityMapFallback, `in entity map "${renderKeyPath(element.entityMapKeyPath)}"`);
259
+ }
260
+ }
261
+ };
262
+ const checkUniqueConstraints = (declarations) => {
263
+ for (const decl of declarations) {
264
+ if (isEntityDecl(decl)) {
265
+ for (const constraint of decl.uniqueConstraints ?? []) {
266
+ if (Array.isArray(constraint)) {
267
+ for (const constraintPart of constraint) {
268
+ checkUniqueConstraintElement(decl, constraintPart);
269
+ }
270
+ if (anySame(constraint, deepEqual)) {
271
+ throw TypeError(`there are duplicate key descriptions in a combined constraint of entity "${decl.name}"`);
272
+ }
273
+ }
274
+ else {
275
+ checkUniqueConstraintElement(decl, constraint);
276
+ }
277
+ }
278
+ }
279
+ }
280
+ };
218
281
  const addDeclarations = (existingDecls, declsToAdd) => declsToAdd.reduce((accDecls, decl) => {
219
282
  if (!accDecls.includes(decl)) {
220
283
  return getNestedDeclarations(accDecls, decl.kind === "NestedEntity" ? decl.type : decl, undefined);
@@ -242,6 +305,8 @@ export const Schema = (declarations, localeEntity) => {
242
305
  checkChildEntityTypes(localeEntity, allDeclsWithoutNestedEntities);
243
306
  debug("checking generic recursive types ...");
244
307
  checkRecursiveGenericTypeAliasesAndEnumerationsAreOnlyParameterizedDirectlyInTypeAliases(allDeclsWithoutNestedEntities);
308
+ debug("checking unique constraints ...");
309
+ checkUniqueConstraints(allDeclsWithoutNestedEntities);
245
310
  debug("created schema, no integrity violations found");
246
311
  return {
247
312
  declarations: allDeclsWithoutNestedEntities,
@@ -1,3 +1,4 @@
1
+ import type { UniqueConstraints } from "../../../shared/schema/declarations/EntityDecl.ts";
1
2
  import { Lazy } from "../../../shared/utils/lazy.ts";
2
3
  import type { DisplayNameCustomizer } from "../../utils/displayName.ts";
3
4
  import type { GetNestedDeclarations, GetReferences, Predicate, Serialized, TypeArgumentsResolver, Validator } from "../Node.ts";
@@ -36,6 +37,7 @@ export interface EntityDecl<Name extends string = string, T extends TConstraint
36
37
  displayName?: GenericEntityDisplayName;
37
38
  displayNameCustomizer?: DisplayNameCustomizer<ObjectType<T>>;
38
39
  isDeprecated?: boolean;
40
+ uniqueConstraints?: UniqueConstraints;
39
41
  }
40
42
  export interface EntityDeclWithParentReference<Name extends string = string, T extends TConstraint = TConstraint, FK extends Extract<keyof T, string> = Extract<keyof T, string>> extends EntityDecl<Name, T, FK> {
41
43
  }
@@ -51,6 +53,7 @@ export declare const EntityDecl: {
51
53
  displayName?: EntityDisplayName<T>;
52
54
  displayNameCustomizer?: DisplayNameCustomizer<ObjectType<T>>;
53
55
  isDeprecated?: boolean;
56
+ uniqueConstraints?: UniqueConstraints;
54
57
  }): EntityDecl<Name, T, undefined>;
55
58
  <Name extends string, T extends TConstraint, FK extends Extract<keyof T, string>>(sourceUrl: string, options: {
56
59
  name: Name;
@@ -64,6 +67,7 @@ export declare const EntityDecl: {
64
67
  displayName?: EntityDisplayName<T>;
65
68
  displayNameCustomizer?: DisplayNameCustomizer<ObjectType<T>>;
66
69
  isDeprecated?: boolean;
70
+ uniqueConstraints?: UniqueConstraints;
67
71
  }): EntityDecl<Name, T, FK>;
68
72
  };
69
73
  export { EntityDecl as Entity };
@@ -1,5 +1,5 @@
1
1
  import { Lazy } from "../../../shared/utils/lazy.ts";
2
- import type { GetNestedDeclarations, GetReferences, Predicate, Serializer, TypeArgumentsResolver, Validators } from "../Node.ts";
2
+ import type { GetNestedDeclarations, GetReferences, Predicate, Serializer, TypeArgumentsResolver, ValidationContext } from "../Node.ts";
3
3
  import { NodeKind } from "../Node.ts";
4
4
  import type { TypeParameter } from "../TypeParameter.ts";
5
5
  import type { Type } from "../types/Type.ts";
@@ -26,7 +26,7 @@ export declare const TypeAliasDecl: <Name extends string, T extends Type>(source
26
26
  export { TypeAliasDecl as TypeAlias };
27
27
  export declare const isTypeAliasDecl: Predicate<TypeAliasDecl>;
28
28
  export declare const getNestedDeclarationsInTypeAliasDecl: GetNestedDeclarations<TypeAliasDecl>;
29
- export declare const validateTypeAliasDecl: <Params extends TypeParameter[]>(helpers: Validators, inDecls: Decl[], decl: TypeAliasDecl<string, Type, Params>, args: TypeArguments<Params>, value: unknown) => Error[];
29
+ export declare const validateTypeAliasDecl: <Params extends TypeParameter[]>(helpers: ValidationContext, inDecls: Decl[], decl: TypeAliasDecl<string, Type, Params>, args: TypeArguments<Params>, value: unknown) => Error[];
30
30
  export declare const resolveTypeArgumentsInTypeAliasDecl: TypeArgumentsResolver<TypeAliasDecl>;
31
31
  export declare const serializeTypeAliasDecl: Serializer<TypeAliasDecl>;
32
32
  export declare const getReferencesForTypeAliasDecl: GetReferences<TypeAliasDecl>;
@@ -1,4 +1,6 @@
1
+ import { MessageError, parseMessage, validate } from "messageformat";
1
2
  import { sortObjectKeys } from "../../../../shared/utils/object.js";
3
+ import { extendsParameterTypes, extractParameterTypeNamesFromMessage, } from "../../../../shared/utils/translation.js";
2
4
  import { parallelizeErrors } from "../../../../shared/utils/validation.js";
3
5
  import { validateUnknownKeys } from "../../../../shared/validation/object.js";
4
6
  import { wrapErrorsIfAny } from "../../../utils/error.js";
@@ -16,9 +18,9 @@ export const TranslationObjectType = (properties, options = {}) => {
16
18
  export { TranslationObjectType as TranslationObject };
17
19
  export const isTranslationObjectType = node => node.kind === NodeKind.TranslationObjectType;
18
20
  export const getNestedDeclarationsInTranslationObjectType = addedDecls => addedDecls;
19
- const validateRecursively = (helpers, allKeysAreRequired, type, value) => {
21
+ const validateRecursively = (context, allKeysAreRequired, type, value) => {
20
22
  if (typeof value !== "object" || value === null || Array.isArray(value)) {
21
- return [TypeError(`expected an object, but got ${json(value, helpers.useStyling)}`)];
23
+ return [TypeError(`expected an object, but got ${json(value, context.useStyling)}`)];
22
24
  }
23
25
  const expectedKeys = Object.keys(type).filter(key => type[key] !== undefined);
24
26
  return parallelizeErrors([
@@ -27,19 +29,36 @@ const validateRecursively = (helpers, allKeysAreRequired, type, value) => {
27
29
  const propType = type[key];
28
30
  const propValue = value[key];
29
31
  if (allKeysAreRequired && propValue === undefined) {
30
- return TypeError(`missing required translation ${keyColor(`"${key}"`, helpers.useStyling)}`);
32
+ return TypeError(`missing required translation ${keyColor(`"${key}"`, context.useStyling)}`);
31
33
  }
32
34
  if (propType === null && propValue !== undefined && typeof propValue !== "string") {
33
- return TypeError(`expected a string at translation key ${keyColor(`"${key}"`, helpers.useStyling)}, but got ${json(propValue, helpers.useStyling)}`);
35
+ return TypeError(`expected a string at translation key ${keyColor(`"${key}"`, context.useStyling)}, but got ${json(propValue, context.useStyling)}`);
34
36
  }
35
37
  if (propType === null &&
36
38
  typeof propValue === "string" &&
37
39
  allKeysAreRequired &&
38
40
  propValue.length === 0) {
39
- return TypeError(`expected a non-empty string at translation key ${keyColor(`"${key}"`, helpers.useStyling)}`);
41
+ return TypeError(`expected a non-empty string at translation key ${keyColor(`"${key}"`, context.useStyling)}`);
42
+ }
43
+ if (typeof propValue === "string" && context.checkTranslations) {
44
+ try {
45
+ validate(parseMessage(propValue));
46
+ }
47
+ catch (err) {
48
+ if (err instanceof MessageError) {
49
+ return TypeError(`invalid translation string at key ${keyColor(`"${key}"`, context.useStyling)}: ${err.message} in message ${json(propValue, context.useStyling)}`);
50
+ }
51
+ }
52
+ if (context.checkTranslations.matchParametersInKeys) {
53
+ const expectedParams = extractParameterTypeNamesFromMessage(key);
54
+ const actualParams = extractParameterTypeNamesFromMessage(propValue);
55
+ if (!extendsParameterTypes(expectedParams, actualParams)) {
56
+ return TypeError(`parameter types in translation string at key ${keyColor(`"${key}"`, context.useStyling)} do not match the expected parameter types. Expected: ${json(expectedParams, context.useStyling)} Actual: ${json(actualParams, context.useStyling)}`);
57
+ }
58
+ }
40
59
  }
41
60
  if (propType !== null && propValue !== undefined) {
42
- return wrapErrorsIfAny(`at translation object key ${keyColor(`"${key}"`, helpers.useStyling)}`, validateRecursively(helpers, allKeysAreRequired, propType, propValue));
61
+ return wrapErrorsIfAny(`at translation object key ${keyColor(`"${key}"`, context.useStyling)}`, validateRecursively(context, allKeysAreRequired, propType, propValue));
43
62
  }
44
63
  return undefined;
45
64
  }),
@@ -2,6 +2,7 @@ import type { SimpleGit } from "simple-git";
2
2
  import type { SerializedDecl } from "../../shared/schema/declarations/Declaration.ts";
3
3
  import type { InstanceContainer } from "../../shared/utils/instances.ts";
4
4
  import type { HomeLayoutSection } from "../config.ts";
5
+ import type { ValidationOptions } from "../index.ts";
5
6
  import type { Decl } from "../schema/declarations/Declaration.ts";
6
7
  import type { EntityDecl } from "../schema/declarations/EntityDecl.ts";
7
8
  import type { Schema } from "../schema/Schema.ts";
@@ -24,6 +25,7 @@ export interface TSONDBRequestLocals {
24
25
  defaultLocales: string[];
25
26
  locales: string[];
26
27
  homeLayoutSections?: HomeLayoutSection[];
28
+ validationOptions: Partial<ValidationOptions>;
27
29
  getInstanceById: GetInstanceById;
28
30
  setLocal: <K extends keyof Omit<TSONDBRequestLocals, "setLocal">>(key: K, value: TSONDBRequestLocals[K]) => void;
29
31
  }
@@ -37,4 +39,4 @@ declare global {
37
39
  }
38
40
  }
39
41
  }
40
- export declare const createServer: (schema: Schema, dataRootPath: string, databaseInMemory: DatabaseInMemory, defaultLocales: string[], homeLayoutSections?: HomeLayoutSection[], options?: Partial<ServerOptions>, customStylesheetPath?: string) => Promise<void>;
42
+ export declare const createServer: (schema: Schema, dataRootPath: string, databaseInMemory: DatabaseInMemory, defaultLocales: string[], homeLayoutSections?: HomeLayoutSection[], options?: Partial<ServerOptions>, validationOptions?: Partial<ValidationOptions>, customStylesheetPath?: string) => Promise<void>;
@@ -16,7 +16,7 @@ const staticNodeModule = (moduleName) => {
16
16
  }
17
17
  return express.static(dirname(pathToPackageJson));
18
18
  };
19
- export const createServer = async (schema, dataRootPath, databaseInMemory, defaultLocales, homeLayoutSections, options, customStylesheetPath) => {
19
+ export const createServer = async (schema, dataRootPath, databaseInMemory, defaultLocales, homeLayoutSections, options, validationOptions, customStylesheetPath) => {
20
20
  const { port } = { ...defaultOptions, ...options };
21
21
  const app = express();
22
22
  app.use(express.static(join(import.meta.dirname, "../../../../public")));
@@ -27,7 +27,7 @@ export const createServer = async (schema, dataRootPath, databaseInMemory, defau
27
27
  app.use("/js/client", express.static(join(import.meta.dirname, "../../../../dist/src/web")));
28
28
  app.use("/js/shared", express.static(join(import.meta.dirname, "../../../../dist/src/shared")));
29
29
  app.use(express.json());
30
- const requestLocals = await init(schema, dataRootPath, databaseInMemory, defaultLocales, homeLayoutSections);
30
+ const requestLocals = await init(schema, dataRootPath, databaseInMemory, defaultLocales, validationOptions, homeLayoutSections);
31
31
  app.use((req, _res, next) => {
32
32
  debug("%s %s", req.method, req.originalUrl);
33
33
  requestLocals.setLocal = (key, value) => {
@@ -1,6 +1,7 @@
1
1
  import type { HomeLayoutSection } from "../config.ts";
2
+ import type { ValidationOptions } from "../index.ts";
2
3
  import type { Schema } from "../schema/Schema.ts";
3
4
  import { type DatabaseInMemory } from "../utils/databaseInMemory.ts";
4
5
  import type { TSONDBRequestLocals } from "./index.ts";
5
- export declare const init: (schema: Schema, dataRootPath: string, databaseInMemory: DatabaseInMemory, defaultLocales: string[], homeLayoutSections?: HomeLayoutSection[]) => Promise<Omit<TSONDBRequestLocals, "setLocal">>;
6
+ export declare const init: (schema: Schema, dataRootPath: string, databaseInMemory: DatabaseInMemory, defaultLocales: string[], validationOptions?: Partial<ValidationOptions>, homeLayoutSections?: HomeLayoutSection[]) => Promise<Omit<TSONDBRequestLocals, "setLocal">>;
6
7
  export declare const reinit: (locals: TSONDBRequestLocals) => Promise<void>;
@@ -22,7 +22,7 @@ const getGit = async (dataRootPath) => {
22
22
  return { git };
23
23
  }
24
24
  };
25
- export const init = async (schema, dataRootPath, databaseInMemory, defaultLocales, homeLayoutSections) => {
25
+ export const init = async (schema, dataRootPath, databaseInMemory, defaultLocales, validationOptions = {}, homeLayoutSections) => {
26
26
  const { git, root: gitRoot, status: gitStatus } = await getGit(dataRootPath);
27
27
  const declarations = resolveTypeArgumentsInDecls(schema.declarations);
28
28
  debug("resolved type arguments in declarations");
@@ -61,6 +61,7 @@ export const init = async (schema, dataRootPath, databaseInMemory, defaultLocale
61
61
  defaultLocales,
62
62
  locales: defaultLocales,
63
63
  homeLayoutSections,
64
+ validationOptions,
64
65
  };
65
66
  return requestLocals;
66
67
  };
@@ -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.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, 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.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, 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.entitiesByName, undefined, undefined, locals.localeEntity, entityName, {
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, {
55
55
  id: instanceId,
56
56
  content: instanceContainer.content,
57
57
  childInstances: oldChildInstances,
@@ -1,5 +1,6 @@
1
1
  import type { GitFileStatus } from "../../shared/utils/git.ts";
2
2
  import type { InstanceContainer, InstanceContent } from "../../shared/utils/instances.ts";
3
+ import type { ValidationOptions } from "../index.ts";
3
4
  import type { EntityDecl, EntityDeclWithParentReference } from "../schema/declarations/EntityDecl.ts";
4
5
  import { type DatabaseInMemory } from "./databaseInMemory.ts";
5
6
  import type { TransactionResult } from "./databaseTransactions.ts";
@@ -29,6 +30,6 @@ export interface GenEntityTaggedInstanceContainerWithChildInstances<ID extends s
29
30
  }
30
31
  export declare const getChildInstancesFromEntity: (databaseInMemory: DatabaseInMemory, parentEntity: EntityDecl, parentId: string, childEntity: EntityDeclWithParentReference) => InstanceContainer[];
31
32
  export declare const getChildInstances: (databaseInMemory: DatabaseInMemory, parentEntity: EntityDecl, parentId: string, recursive?: boolean) => EntityTaggedInstanceContainerWithChildInstances[];
32
- export declare const saveInstanceTree: (entitiesByName: Record<string, EntityDecl>, parentEntityName: string | undefined, parentId: string | undefined, localeEntity: EntityDecl | undefined, entityName: string, oldInstance: EntityTaggedInstanceContainerWithChildInstances | undefined, newInstance: UnsafeEntityTaggedInstanceContainerWithChildInstances | undefined, customId: unknown, res: TransactionResult) => TransactionResult<{
33
+ export declare const saveInstanceTree: (validationOptions: Partial<ValidationOptions>, entitiesByName: Record<string, EntityDecl>, parentEntityName: string | undefined, parentId: string | undefined, localeEntity: EntityDecl | undefined, entityName: string, oldInstance: EntityTaggedInstanceContainerWithChildInstances | undefined, newInstance: UnsafeEntityTaggedInstanceContainerWithChildInstances | undefined, customId: unknown, res: TransactionResult) => TransactionResult<{
33
34
  instanceContainer: InstanceContainer;
34
35
  }>;
@@ -50,7 +50,7 @@ const prepareNewChildInstanceContent = (entity, parentEntityName, parentId, cont
50
50
  return ok(content);
51
51
  }
52
52
  };
53
- export const saveInstanceTree = (entitiesByName, parentEntityName, parentId, localeEntity, entityName, oldInstance, newInstance, customId, res) => {
53
+ export const saveInstanceTree = (validationOptions, entitiesByName, parentEntityName, parentId, localeEntity, entityName, oldInstance, newInstance, customId, res) => {
54
54
  if (isError(res)) {
55
55
  return res;
56
56
  }
@@ -65,7 +65,7 @@ export const saveInstanceTree = (entitiesByName, parentEntityName, parentId, loc
65
65
  }
66
66
  // delete all child instances recursively
67
67
  const deletedRes = deleteInstance(res, entity, oldInstance.id);
68
- return map(oldInstance.childInstances.reduce((resAcc, oldChildInstance) => saveInstanceTree(entitiesByName, oldInstance.entityName, oldInstance.id, localeEntity, oldChildInstance.entityName, oldChildInstance, undefined, undefined, resAcc), deletedRes), data => ({
68
+ return map(oldInstance.childInstances.reduce((resAcc, oldChildInstance) => saveInstanceTree(validationOptions, entitiesByName, oldInstance.entityName, oldInstance.id, localeEntity, oldChildInstance.entityName, oldChildInstance, undefined, undefined, resAcc), deletedRes), data => ({
69
69
  ...data,
70
70
  instanceContainer: { id: oldInstance.id, content: oldInstance.content },
71
71
  }));
@@ -78,8 +78,8 @@ export const saveInstanceTree = (entitiesByName, parentEntityName, parentId, loc
78
78
  return preparedContent;
79
79
  }
80
80
  const setRes = newInstance.id === undefined
81
- ? createInstance(res, localeEntity, entity, preparedContent.value, customId)
82
- : map(updateInstance(res, entity, newInstance.id, preparedContent.value), data => ({
81
+ ? createInstance(validationOptions, res, localeEntity, entity, preparedContent.value, customId)
82
+ : map(updateInstance(validationOptions, res, entity, newInstance.id, preparedContent.value), data => ({
83
83
  ...data,
84
84
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
85
85
  instanceId: newInstance.id,
@@ -91,8 +91,8 @@ export const saveInstanceTree = (entitiesByName, parentEntityName, parentId, loc
91
91
  const setResWithoutInfo = ok({ ...setRes.value, additionalInformation: undefined });
92
92
  return map(newInstance.childInstances
93
93
  .filter(newChildInstance => newChildInstance.id === undefined)
94
- .reduce((resAcc, newChildInstance) => saveInstanceTree(entitiesByName, newInstance.entityName, instanceId, localeEntity, newChildInstance.entityName, undefined, newChildInstance, undefined, resAcc), oldInstance
95
- ? oldInstance.childInstances.reduce((resAcc, oldChildInstance) => saveInstanceTree(entitiesByName, oldInstance.entityName, instanceId, localeEntity, oldChildInstance.entityName, oldChildInstance, newInstance.childInstances.find(ci => ci.id === oldChildInstance.id), undefined, resAcc), setResWithoutInfo)
94
+ .reduce((resAcc, newChildInstance) => saveInstanceTree(validationOptions, entitiesByName, newInstance.entityName, instanceId, localeEntity, newChildInstance.entityName, undefined, newChildInstance, undefined, resAcc), oldInstance
95
+ ? oldInstance.childInstances.reduce((resAcc, oldChildInstance) => saveInstanceTree(validationOptions, entitiesByName, oldInstance.entityName, instanceId, localeEntity, oldChildInstance.entityName, oldChildInstance, newInstance.childInstances.find(ci => ci.id === oldChildInstance.id), undefined, resAcc), setResWithoutInfo)
96
96
  : setResWithoutInfo), data => ({
97
97
  ...data,
98
98
  instanceContainer: { id: instanceId, content: preparedContent.value },
@@ -3,6 +3,7 @@ import { deleteInstanceInDatabaseInMemory, setInstanceInDatabaseInMemory, } from
3
3
  import { applyStepsToDisk } from "./databaseOnDisk.js";
4
4
  import { attachGitStatusToDatabaseInMemory } from "./git.js";
5
5
  import { updateReferencesToInstances } from "./references.js";
6
+ import { checkUniqueConstraintsForAllEntities } from "./unique.js";
6
7
  /**
7
8
  * Run a transaction on the database in memory, applying all changes to disk if successful.
8
9
  */
@@ -17,6 +18,10 @@ export const runDatabaseTransaction = async (root, git, entitiesByName, database
17
18
  return result;
18
19
  }
19
20
  const { db: newDb, refs: newRefs, steps, instanceContainer } = result.value;
21
+ const constraintResult = checkUniqueConstraintsForAllEntities(newDb, Object.values(entitiesByName));
22
+ if (isError(constraintResult)) {
23
+ return constraintResult;
24
+ }
20
25
  const diskResult = await applyStepsToDisk(root, steps);
21
26
  if (isError(diskResult)) {
22
27
  return diskResult;
@@ -1,9 +1,10 @@
1
1
  import type { InstanceContent } from "../../shared/utils/instances.ts";
2
+ import type { ValidationOptions } from "../index.ts";
2
3
  import type { EntityDecl } from "../schema/declarations/EntityDecl.ts";
3
4
  import { type TransactionResult } from "./databaseTransactions.ts";
4
5
  export declare const createNewId: () => `${string}-${string}-${string}-${string}-${string}`;
5
- export declare const createInstance: (res: TransactionResult, localeEntity: EntityDecl | undefined, entity: EntityDecl, instanceContent: InstanceContent, customId: unknown) => TransactionResult<{
6
+ export declare const createInstance: (validationOptions: Partial<ValidationOptions>, res: TransactionResult, localeEntity: EntityDecl | undefined, entity: EntityDecl, instanceContent: InstanceContent, customId: unknown) => TransactionResult<{
6
7
  instanceId: string;
7
8
  }>;
8
- export declare const updateInstance: (res: TransactionResult, entity: EntityDecl, instanceId: string, instanceContent: InstanceContent) => TransactionResult;
9
+ export declare const updateInstance: (validationOptions: Partial<ValidationOptions>, res: TransactionResult, entity: EntityDecl, instanceId: string, instanceContent: InstanceContent) => TransactionResult;
9
10
  export declare const deleteInstance: (res: TransactionResult, entity: EntityDecl, instanceId: string) => TransactionResult;
@@ -1,12 +1,12 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { error, map, ok, then } from "../../shared/utils/result.js";
3
- import { createValidators, validateEntityDecl } from "../schema/index.js";
3
+ import { createValidationContext, validateEntityDecl } from "../schema/index.js";
4
4
  import { getInstanceOfEntityFromDatabaseInMemory, } from "./databaseInMemory.js";
5
5
  import { deleteInstanceT, setInstanceT } from "./databaseTransactions.js";
6
6
  import { getErrorMessageForDisplay, HTTPError } from "./error.js";
7
7
  import { isReferencedByOtherInstances } from "./references.js";
8
8
  export const createNewId = () => randomUUID();
9
- const checkCreateInstancePossible = (localeEntity, databaseInMemory, entity, instanceContent, customId) => {
9
+ const checkCreateInstancePossible = (validationOptions, localeEntity, databaseInMemory, entity, instanceContent, customId) => {
10
10
  const newInstanceId = entity === localeEntity ? customId : createNewId();
11
11
  if (typeof newInstanceId !== "string") {
12
12
  return error(new HTTPError(400, `New identifier "${String(newInstanceId)}" is not a string`));
@@ -16,11 +16,11 @@ const checkCreateInstancePossible = (localeEntity, databaseInMemory, entity, ins
16
16
  undefined) {
17
17
  return error(new HTTPError(400, `Duplicate id "${newInstanceId}" for locale entity`));
18
18
  }
19
- return map(checkUpdateInstancePossible(databaseInMemory, entity, instanceContent), () => newInstanceId);
19
+ return map(checkUpdateInstancePossible(validationOptions, databaseInMemory, entity, instanceContent), () => newInstanceId);
20
20
  };
21
- export const createInstance = (res, localeEntity, entity, instanceContent, customId) => then(res, data => then(checkCreateInstancePossible(localeEntity, data.db, entity, instanceContent, customId), newInstanceId => map(setInstanceT(ok(data), entity, { id: newInstanceId, content: instanceContent }), data => ({ ...data, instanceId: newInstanceId }))));
22
- const checkUpdateInstancePossible = (databaseInMemory, entity, instanceContent) => {
23
- const validationErrors = validateEntityDecl(createValidators(databaseInMemory, false), [], entity, instanceContent);
21
+ export const createInstance = (validationOptions, res, localeEntity, entity, instanceContent, customId) => then(res, data => then(checkCreateInstancePossible(validationOptions, localeEntity, data.db, entity, instanceContent, customId), newInstanceId => map(setInstanceT(ok(data), entity, { id: newInstanceId, content: instanceContent }), data => ({ ...data, instanceId: newInstanceId }))));
22
+ const checkUpdateInstancePossible = (validationOptions, databaseInMemory, entity, instanceContent) => {
23
+ const validationErrors = validateEntityDecl(createValidationContext(validationOptions, databaseInMemory, false), [], entity, instanceContent);
24
24
  if (validationErrors.length > 0) {
25
25
  return error(new HTTPError(400, validationErrors.map(getErrorMessageForDisplay).join("\n\n")));
26
26
  }
@@ -28,7 +28,7 @@ const checkUpdateInstancePossible = (databaseInMemory, entity, instanceContent)
28
28
  return ok();
29
29
  }
30
30
  };
31
- export const updateInstance = (res, entity, instanceId, instanceContent) => then(res, data => then(checkUpdateInstancePossible(data.db, entity, instanceContent), () => setInstanceT(ok(data), entity, { id: instanceId, content: instanceContent })));
31
+ export const updateInstance = (validationOptions, res, entity, instanceId, instanceContent) => then(res, data => then(checkUpdateInstancePossible(validationOptions, data.db, entity, instanceContent), () => setInstanceT(ok(data), entity, { id: instanceId, content: instanceContent })));
32
32
  const checkDeleteInstancePossible = (referencesToInstances, instanceId) => {
33
33
  if (isReferencedByOtherInstances(referencesToInstances, instanceId)) {
34
34
  return error(new HTTPError(400, "Cannot delete instance that is referenced by other instances"));
@@ -0,0 +1,19 @@
1
+ import type { InstanceContainer } from "../../shared/utils/instances.ts";
2
+ import { type Result } from "../../shared/utils/result.ts";
3
+ import type { EntityDecl } from "../schema/index.ts";
4
+ import { type DatabaseInMemory } from "./databaseInMemory.ts";
5
+ /**
6
+ * Checks all unique constraints for the provided entity and its instances.
7
+ *
8
+ * Returns `Ok` when no violations have been found and an `Error` with an
9
+ * `AggregateError` if there are any violations of any unique constraint.
10
+ */
11
+ export declare const checkUniqueConstraintsForEntity: (entity: EntityDecl, instances: InstanceContainer[]) => Result<void, AggregateError>;
12
+ /**
13
+ * Checks all unique constraints for all provided entities and their instances.
14
+ *
15
+ * Returns `Ok` when no violations have been found and an `Error` with a list of
16
+ * `AggregateError`s for each entity if there are any violations of any unique
17
+ * constraint.
18
+ */
19
+ export declare const checkUniqueConstraintsForAllEntities: (db: DatabaseInMemory, entities: EntityDecl[]) => Result<void, AggregateError>;
@@ -0,0 +1,94 @@
1
+ import { normalizeKeyPath, renderKeyPath, } from "../../shared/schema/declarations/EntityDecl.js";
2
+ import { anySameIndices, flatCombine } from "../../shared/utils/array.js";
3
+ import { deepEqual } from "../../shared/utils/compare.js";
4
+ import { error, isError, mapError, ok } from "../../shared/utils/result.js";
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
9
+ ? renderKeyPath(elem.keyPath) +
10
+ (elem.keyPathFallback ? "|" + renderKeyPath(elem.keyPathFallback) : "")
11
+ : renderKeyPath(elem.entityMapKeyPath) +
12
+ "[...]." +
13
+ (elem.keyPathInEntityMapFallback
14
+ ? "(" +
15
+ renderKeyPath(elem.keyPathInEntityMap) +
16
+ "|" +
17
+ renderKeyPath(elem.keyPathInEntityMapFallback) +
18
+ ")"
19
+ : renderKeyPath(elem.keyPathInEntityMap)))
20
+ .join("+");
21
+ const unsafeGetValueAtKeyPath = (value, keyPath) => {
22
+ let acc = value;
23
+ for (const key of normalizeKeyPath(keyPath)) {
24
+ acc = acc[key];
25
+ }
26
+ return acc;
27
+ };
28
+ /**
29
+ * Checks all unique constraints for the provided entity and its instances.
30
+ *
31
+ * Returns `Ok` when no violations have been found and an `Error` with an
32
+ * `AggregateError` if there are any violations of any unique constraint.
33
+ */
34
+ export const checkUniqueConstraintsForEntity = (entity, instances) => {
35
+ const constraintErrors = [];
36
+ const constraints = entity.uniqueConstraints ?? [];
37
+ for (const [constraintIndex, constraint] of constraints.entries()) {
38
+ const normalizedConstraint = Array.isArray(constraint) ? constraint : [constraint];
39
+ // the index contains all instances as rows with multiple rows per instance for all possible combinations of nested entity map values
40
+ const index = instances.flatMap(({ id, content }) => flatCombine(normalizedConstraint.map(elem => {
41
+ if ("keyPath" in elem) {
42
+ return [
43
+ unsafeGetValueAtKeyPath(content, elem.keyPath) ??
44
+ (elem.keyPathFallback
45
+ ? unsafeGetValueAtKeyPath(content, elem.keyPathFallback)
46
+ : undefined),
47
+ ];
48
+ }
49
+ else {
50
+ return Object.entries(unsafeGetValueAtKeyPath(content, elem.entityMapKeyPath)).map(([nestedId, nestedContent]) => [
51
+ nestedId,
52
+ unsafeGetValueAtKeyPath(nestedContent, elem.keyPathInEntityMap) ??
53
+ (elem.keyPathInEntityMapFallback
54
+ ? unsafeGetValueAtKeyPath(nestedContent, elem.keyPathInEntityMapFallback)
55
+ : undefined),
56
+ ]);
57
+ }
58
+ })).map((row) => [id, row]));
59
+ const duplicates = anySameIndices(index, (a, b) => deepEqual(a[1], b[1]));
60
+ if (duplicates.length > 0) {
61
+ constraintErrors.push([
62
+ constraintIndex,
63
+ // 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])),
65
+ ]);
66
+ }
67
+ }
68
+ 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`)),
70
+ // 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}"`));
72
+ }
73
+ return ok();
74
+ };
75
+ /**
76
+ * Checks all unique constraints for all provided entities and their instances.
77
+ *
78
+ * Returns `Ok` when no violations have been found and an `Error` with a list of
79
+ * `AggregateError`s for each entity if there are any violations of any unique
80
+ * constraint.
81
+ */
82
+ export const checkUniqueConstraintsForAllEntities = (db, entities) => mapError(entities.reduce((acc, entity) => {
83
+ const resultForEntity = checkUniqueConstraintsForEntity(entity, getInstancesOfEntityFromDatabaseInMemory(db, entity.name));
84
+ if (isError(acc)) {
85
+ if (isError(resultForEntity)) {
86
+ return error([...acc.error, resultForEntity.error]);
87
+ }
88
+ 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"));
@@ -18,6 +18,33 @@ type TSerializedConstraint = Record<string, SerializedMemberDecl>;
18
18
  type SerializedPathTo<T extends TSerializedConstraint, R> = {
19
19
  [K in keyof T]: T[K] extends SerializedMemberDecl<infer V> ? V extends R ? K : R extends V ? string : T[K] extends SerializedObjectType<infer P> ? `${Extract<K, string>}.${SerializedPathTo<P, R>}` : never : never;
20
20
  }[Extract<keyof T, string>];
21
+ export type KeyPath = string | string[];
22
+ export declare const normalizeKeyPath: (keyPath: KeyPath) => string[];
23
+ export declare const renderKeyPath: (keyPath: KeyPath) => string;
24
+ /**
25
+ * A uniquing element can be the full value of a key or the value of a key in a nested entity map.
26
+ */
27
+ export type UniquingElement = {
28
+ keyPath: KeyPath;
29
+ keyPathFallback?: KeyPath;
30
+ } | {
31
+ entityMapKeyPath: KeyPath;
32
+ keyPathInEntityMap: KeyPath;
33
+ keyPathInEntityMapFallback?: KeyPath;
34
+ };
35
+ export type UniqueConstraint = UniquingElement | UniquingElement[];
36
+ /**
37
+ * A list of keys or key descriptions whose values need to be unique across all instances in the entity.
38
+ *
39
+ * One or more constraints may be provided. A nested array indicates that the combination of described values must be unique.
40
+ *
41
+ * @example
42
+ *
43
+ * ["name"] // all `name` keys must be unique across the entity
44
+ * ["name", "age"] // all `name` keys and all `age` keys must be unique across the entity
45
+ * [["name", "age"]] // the combination of `name` and `age` must be unique across the entity
46
+ */
47
+ export type UniqueConstraints = UniqueConstraint[];
21
48
  export interface SerializedEntityDecl<Name extends string = string, T extends TSerializedConstraint = TSerializedConstraint, FK extends Extract<keyof T, string> | undefined = Extract<keyof T, string> | undefined> extends SerializedBaseDecl<Name, []> {
22
49
  kind: NodeKind["EntityDecl"];
23
50
  namePlural: string;
@@ -29,6 +56,7 @@ export interface SerializedEntityDecl<Name extends string = string, T extends TS
29
56
  displayName?: GenericEntityDisplayName;
30
57
  displayNameCustomizer: boolean;
31
58
  isDeprecated?: boolean;
59
+ uniqueConstraints?: UniqueConstraints;
32
60
  }
33
61
  export declare const isSerializedEntityDecl: (node: SerializedNode) => node is SerializedEntityDecl;
34
62
  export declare const isSerializedEntityDeclWithParentReference: <Name extends string, T extends TSerializedConstraint, FK extends Extract<keyof T, string> | undefined>(decl: SerializedEntityDecl<Name, T, FK>) => decl is SerializedEntityDecl<Name, T, NonNullable<FK>>;
@@ -1,5 +1,7 @@
1
1
  import { NodeKind, resolveSerializedTypeArguments, } from "../Node.js";
2
2
  import { getReferencesForSerializedObjectType, } from "../types/ObjectType.js";
3
+ export const normalizeKeyPath = (keyPath) => Array.isArray(keyPath) ? keyPath : [keyPath];
4
+ export const renderKeyPath = (keyPath) => normalizeKeyPath(keyPath).join(".");
3
5
  export const isSerializedEntityDecl = (node) => node.kind === NodeKind.EntityDecl;
4
6
  export const isSerializedEntityDeclWithParentReference = (decl) => decl.parentReferenceKey !== undefined;
5
7
  export const isSerializedEntityDeclWithoutParentReference = (decl) => decl.parentReferenceKey === undefined;
@@ -18,6 +18,24 @@ export interface ArrayDiffResult<T> {
18
18
  removed: T[];
19
19
  }
20
20
  export declare const unique: <T>(arr: T[], equalityCheck?: (a: T, b: T) => boolean) => T[];
21
+ /**
22
+ * Checks if there are any duplicate elements in the array.
23
+ */
24
+ export declare const anySame: <T>(arr: T[], equalityCheck?: (a: T, b: T) => boolean) => boolean;
25
+ /**
26
+ * Checks if there are any duplicate elements in the array and returns an array
27
+ * of found duplicates where the values are the indices of these values.
28
+ */
29
+ export declare const anySameIndices: <T>(arr: T[], equalityCheck?: (a: T, b: T) => boolean) => number[][];
30
+ /**
31
+ * Returns the possibilities of all the combinations of nested array values.
32
+ *
33
+ * @example
34
+ *
35
+ * flatCombine([["a", "b"], ["c"]]) // [["a", "c"], ["b", "c"]]
36
+ * flatCombine([["a", "b"], ["c", "d"]]) // [["a", "c"], ["b", "c"], ["a", "d"], ["b", "d"]]
37
+ */
38
+ export declare const flatCombine: <T>(arr: T[][]) => T[][];
21
39
  /**
22
40
  * Moves an element from one position to another within the array.
23
41
  */
@@ -29,6 +29,38 @@ export const difference = (oldArr, newArr) => newArr.reduce((acc, item) => {
29
29
  return acc;
30
30
  }, { removed: oldArr, added: newArr });
31
31
  export const unique = (arr, equalityCheck = (a, b) => a === b) => arr.filter((item, index) => arr.findIndex(other => equalityCheck(item, other)) === index);
32
+ /**
33
+ * Checks if there are any duplicate elements in the array.
34
+ */
35
+ export const anySame = (arr, equalityCheck = (a, b) => a === b) => arr.some((item, index) => arr.findIndex(other => equalityCheck(item, other)) !== index);
36
+ /**
37
+ * Checks if there are any duplicate elements in the array and returns an array
38
+ * of found duplicates where the values are the indices of these values.
39
+ */
40
+ export const anySameIndices = (arr, equalityCheck = (a, b) => a === b) => arr.reduce((acc, item, index) => {
41
+ const firstIndex = arr.findIndex(other => equalityCheck(item, other));
42
+ if (firstIndex === index) {
43
+ return acc;
44
+ }
45
+ const accIndex = acc.findIndex(accElem => accElem[0] === firstIndex);
46
+ return accIndex === -1
47
+ ? [...acc, [firstIndex, index]]
48
+ : // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- The index must exist according to the findIndex above
49
+ acc.with(accIndex, [...acc[accIndex], index]);
50
+ }, []);
51
+ /**
52
+ * Returns the possibilities of all the combinations of nested array values.
53
+ *
54
+ * @example
55
+ *
56
+ * flatCombine([["a", "b"], ["c"]]) // [["a", "c"], ["b", "c"]]
57
+ * flatCombine([["a", "b"], ["c", "d"]]) // [["a", "c"], ["b", "c"], ["a", "d"], ["b", "d"]]
58
+ */
59
+ export const flatCombine = (arr) => arr.length === 0
60
+ ? []
61
+ : arr.slice(1).reduce((acc, elem) => elem.flatMap(elemInner => acc.map(accElem => [...accElem, elemInner])),
62
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- it is checked before if the array is empty
63
+ arr[0].map(elem => [elem]));
32
64
  /**
33
65
  * Moves an element from one position to another within the array.
34
66
  */
@@ -1,2 +1,7 @@
1
1
  export declare const extractParameterTypeNamesFromMessage: (message: string) => Record<string, string | null>;
2
2
  export declare const mapParameterTypeNames: <T>(typeMap: Record<string, string | null>, map: (typeName: string | null) => T) => Record<string, T>;
3
+ /**
4
+ * Checks whether one set of parameter types (`ext`) is a subset of (or equals)
5
+ * another (`base`).
6
+ */
7
+ export declare const extendsParameterTypes: (base: Record<string, string | null>, ext: Record<string, string | null>) => boolean;
@@ -26,6 +26,12 @@ const extractParameterFromDeclaration = (decl) => {
26
26
  }
27
27
  };
28
28
  const extractParametersFromDeclarations = (decls) => decls.reduce((acc, decl) => reduceMapAssoc(extractParameterFromDeclaration, acc, decl), {});
29
+ const ignoreLocalVariables = (decls, acc) => decls.reduce((acc, decl) => reduceADT({
30
+ local: (acc, localDecl) => {
31
+ const { [localDecl.name]: _, ...rest } = acc;
32
+ return rest;
33
+ },
34
+ }, acc, decl), acc);
29
35
  const reduceParametersFromPattern = (acc, pattern) => pattern.reduce((acc, element) => {
30
36
  if (typeof element === "string") {
31
37
  return acc;
@@ -46,9 +52,9 @@ export const extractParameterTypeNamesFromMessage = (message) => {
46
52
  const dataModel = parseMessage(message);
47
53
  switch (dataModel.type) {
48
54
  case "message":
49
- return reduceParametersFromPattern(extractParametersFromDeclarations(dataModel.declarations), dataModel.pattern);
55
+ return ignoreLocalVariables(dataModel.declarations, reduceParametersFromPattern(extractParametersFromDeclarations(dataModel.declarations), dataModel.pattern));
50
56
  case "select": {
51
- return dataModel.selectors.reduce((acc, variable) => mergeAssoc(acc, variable.name, null), dataModel.variants.reduce((acc, variant) => reduceParametersFromPattern(acc, variant.value), extractParametersFromDeclarations(dataModel.declarations)));
57
+ return ignoreLocalVariables(dataModel.declarations, dataModel.selectors.reduce((acc, variable) => mergeAssoc(acc, variable.name, null), dataModel.variants.reduce((acc, variant) => reduceParametersFromPattern(acc, variant.value), extractParametersFromDeclarations(dataModel.declarations))));
52
58
  }
53
59
  default:
54
60
  return assertExhaustive(dataModel);
@@ -64,3 +70,23 @@ export const extractParameterTypeNamesFromMessage = (message) => {
64
70
  }
65
71
  };
66
72
  export const mapParameterTypeNames = (typeMap, map) => Object.fromEntries(Object.entries(typeMap).map(([key, typeName]) => [key, map(typeName)]));
73
+ /**
74
+ * Checks whether one set of parameter types (`ext`) is a subset of (or equals)
75
+ * another (`base`).
76
+ */
77
+ export const extendsParameterTypes = (base, ext) => {
78
+ for (const [key, typeName] of Object.entries(ext)) {
79
+ if (typeName === null || typeof typeName === "string") {
80
+ const baseTypeName = base[key];
81
+ if (baseTypeName === undefined) {
82
+ // extra parameter not in base
83
+ return false;
84
+ }
85
+ if (typeof typeName === "string" && baseTypeName !== typeName) {
86
+ // type in extension is neither null (any) nor matches the base type
87
+ return false;
88
+ }
89
+ }
90
+ }
91
+ return true;
92
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tsondb",
3
- "version": "0.12.8",
3
+ "version": "0.13.0",
4
4
  "description": "",
5
5
  "license": "ISC",
6
6
  "author": "Lukas Obermann",