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.
- package/dist/src/bin/tsondb.js +11 -6
- package/dist/src/node/config.d.ts +15 -2
- package/dist/src/node/index.d.ts +10 -4
- package/dist/src/node/index.js +7 -7
- package/dist/src/node/renderers/ts/render.d.ts +6 -0
- package/dist/src/node/renderers/ts/render.js +35 -2
- package/dist/src/node/schema/Node.d.ts +6 -4
- package/dist/src/node/schema/Node.js +2 -1
- package/dist/src/node/schema/Schema.js +9 -1
- package/dist/src/node/schema/declarations/TypeAliasDecl.d.ts +2 -2
- package/dist/src/node/schema/types/generic/TranslationObjectType.js +25 -6
- package/dist/src/node/server/index.d.ts +3 -1
- package/dist/src/node/server/index.js +2 -2
- package/dist/src/node/server/init.d.ts +2 -1
- package/dist/src/node/server/init.js +2 -1
- package/dist/src/node/server/utils/instanceOperations.js +3 -3
- package/dist/src/node/utils/childInstances.d.ts +2 -1
- package/dist/src/node/utils/childInstances.js +6 -6
- package/dist/src/node/utils/instanceTransactionSteps.d.ts +3 -2
- package/dist/src/node/utils/instanceTransactionSteps.js +7 -7
- package/dist/src/node/utils/render.d.ts +1 -0
- package/dist/src/node/utils/render.js +2 -1
- package/dist/src/shared/utils/translation.d.ts +7 -0
- package/dist/src/shared/utils/translation.js +92 -0
- package/package.json +2 -1
package/dist/src/bin/tsondb.js
CHANGED
|
@@ -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
|
-
|
|
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?.
|
|
99
|
-
const entities = passedArguments.command.options.
|
|
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
|
-
|
|
104
|
-
|
|
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
|
|
50
|
-
export declare const validateConfigForFormatting: (config: Config) => asserts config is
|
|
62
|
+
export declare const validateConfigForTesting: (config: Config) => asserts config is TestingConfig;
|
|
63
|
+
export declare const validateConfigForFormatting: (config: Config) => asserts config is FormattingConfig;
|
package/dist/src/node/index.d.ts
CHANGED
|
@@ -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
|
|
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 {};
|
package/dist/src/node/index.js
CHANGED
|
@@ -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 {
|
|
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
|
|
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(
|
|
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
|
|
62
|
+
export interface ValidationContext {
|
|
62
63
|
useStyling: boolean;
|
|
63
64
|
checkReferentialIntegrity: (identifier: IdentifierToCheck) => Error[];
|
|
65
|
+
checkTranslations?: ValidationOptions["checkTranslations"];
|
|
64
66
|
}
|
|
65
|
-
export declare const
|
|
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:
|
|
70
|
-
export type ValidatorOfParamDecl<T extends Node = Node> = (helpers:
|
|
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
|
|
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,
|
|
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:
|
|
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 = (
|
|
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,
|
|
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}"`,
|
|
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}"`,
|
|
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}"`,
|
|
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}"`,
|
|
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 {
|
|
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(
|
|
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(
|
|
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.
|
|
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",
|