tsondb 0.12.7 → 0.12.9

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.
@@ -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 {};
@@ -4,7 +4,7 @@ import { join, sep } from "path";
4
4
  import { styleText } from "util";
5
5
  import { parallelizeErrors } from "../shared/utils/validation.js";
6
6
  import { validateEntityDecl } from "./schema/declarations/EntityDecl.js";
7
- import { createValidators } from "./schema/Node.js";
7
+ import { createValidationContext } from "./schema/Node.js";
8
8
  import { getEntities } from "./schema/Schema.js";
9
9
  import { createServer } from "./server/index.js";
10
10
  import { asyncForEachInstanceInDatabaseInMemory, createDatabaseInMemory, getInstancesOfEntityFromDatabaseInMemory, } from "./utils/databaseInMemory.js";
@@ -30,11 +30,11 @@ const _validate = (dataRootPath, entities, databaseInMemory, options = {}) => {
30
30
  throw new Error(`Entity "${onlyEntity}" not found in schema`);
31
31
  }
32
32
  }
33
- const validationHelpers = createValidators(databaseInMemory, true, checkReferentialIntegrity);
33
+ const validationContext = createValidationContext(options, databaseInMemory, true, checkReferentialIntegrity);
34
34
  const errors = (checkOnlyEntities.length > 0
35
35
  ? entities.filter(entity => checkOnlyEntities.includes(entity.name))
36
36
  : 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)))))
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(validationContext, [], entity, instance.content)))))
38
38
  .toSorted((a, b) => a.message.localeCompare(b.message));
39
39
  if (errors.length === 0) {
40
40
  debug("All entities are valid");
@@ -57,7 +57,7 @@ export const generateAndValidate = async (schema, outputs, dataRootPath, validat
57
57
  const databaseInMemory = await createDatabaseInMemory(dataRootPath, entities);
58
58
  _validate(dataRootPath, entities, databaseInMemory, validationOptions);
59
59
  };
60
- export const serve = async (schema, dataRootPath, defaultLocales, homeLayoutSections, serverOptions, customStylesheetPath) => {
60
+ export const serve = async (schema, dataRootPath, defaultLocales, homeLayoutSections, serverOptions, validationOptions, customStylesheetPath) => {
61
61
  if (defaultLocales.length === 0) {
62
62
  throw new Error("At least one default locale must be specified to start the server.");
63
63
  }
@@ -66,15 +66,15 @@ export const serve = async (schema, dataRootPath, defaultLocales, homeLayoutSect
66
66
  debug("prepared folders");
67
67
  const databaseInMemory = await createDatabaseInMemory(dataRootPath, entities);
68
68
  debug("loaded instances");
69
- await createServer(schema, dataRootPath, databaseInMemory, defaultLocales, homeLayoutSections, serverOptions, customStylesheetPath);
69
+ await createServer(schema, dataRootPath, databaseInMemory, defaultLocales, homeLayoutSections, serverOptions, validationOptions, customStylesheetPath);
70
70
  };
71
- export const generateValidateAndServe = async (schema, outputs, dataRootPath, defaultLocales, homeLayoutSections, serverOptions, validationOptions) => {
71
+ export const generateValidateAndServe = async (schema, outputs, dataRootPath, defaultLocales, homeLayoutSections, serverOptions, validationOptions, customStylesheetPath) => {
72
72
  await generateOutputs(schema, outputs);
73
73
  const entities = getEntities(schema);
74
74
  await prepareFolders(dataRootPath, entities);
75
75
  const databaseInMemory = await createDatabaseInMemory(dataRootPath, entities);
76
76
  _validate(dataRootPath, entities, databaseInMemory, validationOptions);
77
- await createServer(schema, dataRootPath, databaseInMemory, defaultLocales, homeLayoutSections, serverOptions);
77
+ await createServer(schema, dataRootPath, databaseInMemory, defaultLocales, homeLayoutSections, serverOptions, validationOptions, customStylesheetPath);
78
78
  };
79
79
  export const format = async (schema, dataRootPath) => {
80
80
  const entities = getEntities(schema);
@@ -5,5 +5,11 @@ export type TypeScriptRendererOptions = {
5
5
  preserveFiles: boolean;
6
6
  generateEntityMapType: boolean;
7
7
  addIdentifierToEntities: boolean;
8
+ /**
9
+ * Infer translation parameter types from the message strings in a {@link TranslationObjectType TranslationObject} as branded types.
10
+ */
11
+ inferTranslationParameters?: {
12
+ format: "mf2";
13
+ };
8
14
  };
9
15
  export declare const render: (options: Partial<TypeScriptRendererOptions> | undefined, declarations: readonly Decl[]) => string;
@@ -3,6 +3,7 @@ import { dirname, relative } from "node:path";
3
3
  import { discriminatorKey } from "../../../shared/enum.js";
4
4
  import { unique } from "../../../shared/utils/array.js";
5
5
  import { toCamelCase } from "../../../shared/utils/string.js";
6
+ import { extractParameterTypeNamesFromMessage, mapParameterTypeNames, } from "../../../shared/utils/translation.js";
6
7
  import { assertExhaustive } from "../../../shared/utils/typeSafety.js";
7
8
  import { asDecl } from "../../schema/declarations/Declaration.js";
8
9
  import { addEphemeralUUIDToType, createEntityIdentifierTypeAsDecl, isEntityDecl, } from "../../schema/declarations/EntityDecl.js";
@@ -14,7 +15,7 @@ import { getTypeOfKey } from "../../schema/types/generic/TranslationObjectType.j
14
15
  import { isNestedEntityMapType } from "../../schema/types/references/NestedEntityMapType.js";
15
16
  import { ReferenceIdentifierType } from "../../schema/types/references/ReferenceIdentifierType.js";
16
17
  import { ensureSpecialDirStart } from "../../utils/path.js";
17
- import { combineSyntaxes, emptyRenderResult, indent, prefixLines, syntax, } from "../../utils/render.js";
18
+ import { combineSyntaxes, emptyRenderResult, getIndentation, indent, prefixLines, syntax, } from "../../utils/render.js";
18
19
  const defaultOptions = {
19
20
  indentation: 2,
20
21
  objectTypeKeyword: "interface",
@@ -62,8 +63,34 @@ const renderEnumType = (options, type) => combineSyntaxes(Object.entries(type.va
62
63
  ? ""
63
64
  : syntax `${EOL}${caseName}: ${renderType(options, caseDef.type)}`}`)}${EOL}}`)));
64
65
  const renderChildEntitiesType = (options, type) => renderType(options, ArrayType(ReferenceIdentifierType(type.entity), { uniqueItems: true }));
66
+ const mapTypeNameToType = (typeName) => {
67
+ switch (typeName) {
68
+ case "string":
69
+ return "string";
70
+ case "number":
71
+ case "integer":
72
+ return "number";
73
+ case "date":
74
+ case "datetime":
75
+ case "time":
76
+ return "Date";
77
+ case null:
78
+ default:
79
+ return "StringableTranslationParameter";
80
+ }
81
+ };
82
+ const renderTranslationParameterBrand = (options, params) => {
83
+ if (options.inferTranslationParameters === undefined) {
84
+ return emptyRenderResult;
85
+ }
86
+ const entries = Object.entries(params);
87
+ if (entries.length === 0) {
88
+ return emptyRenderResult;
89
+ }
90
+ return syntax ` & { __params: { ${entries.map(([name, type]) => `"${name}": ${type}`).join("; ")} } }`;
91
+ };
65
92
  const renderTranslationObjectType = (options, type) => {
66
- return wrapAsObject(options, combineSyntaxes(Object.entries(type.properties).map(([name, config]) => syntax `"${name.replace('"', '\\"')}"${type.allKeysAreRequired ? "" : "?"}: ${renderType(options, getTypeOfKey(config, type))}`), EOL));
93
+ return wrapAsObject(options, combineSyntaxes(Object.entries(type.properties).map(([name, config]) => syntax `"${name.replace('"', '\\"')}"${type.allKeysAreRequired ? "" : "?"}: ${renderType(options, getTypeOfKey(config, type))}${renderTranslationParameterBrand(options, mapParameterTypeNames(extractParameterTypeNamesFromMessage(name), mapTypeNameToType))}`), EOL));
67
94
  };
68
95
  const renderType = (options, type) => {
69
96
  switch (type.kind) {
@@ -135,6 +162,11 @@ const renderEntityMapType = (options, declarations) => syntax `export type Entit
135
162
  .filter(isEntityDecl)
136
163
  .sort((a, b) => a.name.localeCompare(b.name))
137
164
  .map(decl => syntax `${decl.name}: ${decl.name}`), EOL))}${EOL}}${EOL + EOL}`;
165
+ const renderStringableTranslationParameterType = (options) => options.inferTranslationParameters?.format === "mf2"
166
+ ? "export type StringableTranslationParameter = {\n" +
167
+ prefixLines(getIndentation(options.indentation, 1), "toString(): string") +
168
+ "\n}\n\n"
169
+ : "";
138
170
  export const render = (options = defaultOptions, declarations) => {
139
171
  const finalOptions = { ...defaultOptions, ...options };
140
172
  const [_, entityMap] = finalOptions.generateEntityMapType
@@ -154,6 +186,7 @@ export const render = (options = defaultOptions, declarations) => {
154
186
  return undefined;
155
187
  }, declarations));
156
188
  return (entityMap +
189
+ renderStringableTranslationParameterType(finalOptions) +
157
190
  (finalOptions.preserveFiles
158
191
  ? (declarations[0] === undefined ? "" : renderImports(declarations[0].sourceUrl, imports)) +
159
192
  content
@@ -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) {
@@ -20,6 +20,13 @@ const checkDuplicateIdentifier = (existingDecls, decl) => {
20
20
  throw new Error(`Duplicate declaration name "${decl.name}" in "${decl.sourceUrl}" and "${existingDeclWithSameName.sourceUrl}". Make sure declaration names are globally unique.`);
21
21
  }
22
22
  };
23
+ // const checkReservedIdentifier = (decl: NestedDecl) => {
24
+ // if (RESERVED_DECLARATION_IDENTIFIER.includes(decl.name)) {
25
+ // throw new Error(
26
+ // `Declaration "${decl.name}" in "${decl.sourceUrl}" uses a reserved identifier name.`,
27
+ // )
28
+ // }
29
+ // }
23
30
  const checkParameterNamesShadowing = (decls) => {
24
31
  for (const decl of decls) {
25
32
  for (const param of getParameterNames(decl)) {
@@ -219,9 +226,10 @@ export const Schema = (declarations, localeEntity) => {
219
226
  debug("collecting nested declarations ...");
220
227
  const allDecls = addDeclarations([], localeEntity ? declarations.concat(localeEntity) : declarations);
221
228
  debug("found %d nested declarations", allDecls.length);
222
- debug("checking for duplicate identifiers ...");
229
+ debug("checking for duplicate identifiers ..."); // debug("checking for duplicate or reserved identifiers ...")
223
230
  allDecls.forEach((decl, declIndex) => {
224
231
  checkDuplicateIdentifier(allDecls.slice(0, declIndex), decl);
232
+ // checkReservedIdentifier(decl)
225
233
  });
226
234
  const allDeclsWithoutNestedEntities = allDecls.filter(decl => decl.kind !== "NestedEntity");
227
235
  debug("checking name shadowing ...");
@@ -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 },
@@ -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"));
@@ -7,4 +7,5 @@ export type RenderResult = [imports: {
7
7
  export declare const emptyRenderResult: RenderResult;
8
8
  export declare const combineSyntaxes: (syntaxes: (RenderResult | string | undefined)[], separator?: string) => RenderResult;
9
9
  export declare const syntax: (strings: TemplateStringsArray, ...values: (RenderResult | string | undefined)[]) => RenderResult;
10
+ export declare const getIndentation: (spaces: number, indentLevel: number) => string;
10
11
  export declare const indent: (spaces: number, indentLevel: number, text: RenderResult) => RenderResult;
@@ -28,7 +28,8 @@ export const syntax = (strings, ...values) => strings.reduce((acc, str, i) => {
28
28
  return [acc[0], acc[1] + str.replace(/\n/g, EOL)];
29
29
  }
30
30
  }, emptyRenderResult);
31
+ export const getIndentation = (spaces, indentLevel) => " ".repeat(spaces * indentLevel);
31
32
  export const indent = (spaces, indentLevel, text) => [
32
33
  text[0],
33
- prefixLines(" ".repeat(spaces * indentLevel), text[1]),
34
+ prefixLines(getIndentation(spaces, indentLevel), text[1]),
34
35
  ];
@@ -0,0 +1,7 @@
1
+ export declare const extractParameterTypeNamesFromMessage: (message: string) => Record<string, string | null>;
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;
@@ -0,0 +1,92 @@
1
+ import { MessageError, parseMessage } from "messageformat";
2
+ import { assertExhaustive } from "./typeSafety.js";
3
+ const mergeAssoc = (acc, key, value) => {
4
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- if there is a previous value and the current result is null, keep the more specific one
5
+ return { ...acc, [key]: value ?? acc[key] };
6
+ };
7
+ const reduceMapAssoc = (map, acc, item) => {
8
+ const result = map(item);
9
+ if (result) {
10
+ return mergeAssoc(acc, result[0], result[1]);
11
+ }
12
+ return acc;
13
+ };
14
+ const reduceADT = (cases, acc, item) => {
15
+ const caseFn = cases[item.type];
16
+ return caseFn?.(acc, item) ?? acc;
17
+ };
18
+ const extractParameterFromDeclaration = (decl) => {
19
+ switch (decl.type) {
20
+ case "input":
21
+ return [decl.name, decl.value.functionRef?.name ?? null];
22
+ case "local":
23
+ return undefined;
24
+ default:
25
+ return assertExhaustive(decl);
26
+ }
27
+ };
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);
35
+ const reduceParametersFromPattern = (acc, pattern) => pattern.reduce((acc, element) => {
36
+ if (typeof element === "string") {
37
+ return acc;
38
+ }
39
+ return reduceADT({
40
+ expression: (acc, element) => {
41
+ if (!element.arg) {
42
+ return acc;
43
+ }
44
+ return reduceADT({
45
+ variable: (acc, variable) => mergeAssoc(acc, variable.name, element.functionRef?.name ?? null),
46
+ }, acc, element.arg);
47
+ },
48
+ }, acc, element);
49
+ }, acc);
50
+ export const extractParameterTypeNamesFromMessage = (message) => {
51
+ try {
52
+ const dataModel = parseMessage(message);
53
+ switch (dataModel.type) {
54
+ case "message":
55
+ return ignoreLocalVariables(dataModel.declarations, reduceParametersFromPattern(extractParametersFromDeclarations(dataModel.declarations), dataModel.pattern));
56
+ case "select": {
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))));
58
+ }
59
+ default:
60
+ return assertExhaustive(dataModel);
61
+ }
62
+ }
63
+ catch (error) {
64
+ if (error instanceof MessageError) {
65
+ throw new MessageError(error.message, message);
66
+ }
67
+ else {
68
+ throw error;
69
+ }
70
+ }
71
+ };
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.7",
3
+ "version": "0.12.9",
4
4
  "description": "",
5
5
  "license": "ISC",
6
6
  "author": "Lukas Obermann",
@@ -50,6 +50,7 @@
50
50
  "@preact/signals": "^2.5.1",
51
51
  "debug": "^4.4.3",
52
52
  "express": "^5.1.0",
53
+ "messageformat": "^4.0.0",
53
54
  "preact": "^10.27.2",
54
55
  "preact-iso": "^2.11.0",
55
56
  "simple-cli-args": "^0.1.3",