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