tsondb 0.14.0 → 0.16.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/dist/src/node/index.js +19 -5
- 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 +3 -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 +12 -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/utils/customConstraints.d.ts +50 -0
- package/dist/src/node/utils/customConstraints.js +67 -0
- package/dist/src/node/utils/databaseTransactions.js +10 -3
- package/dist/src/node/utils/displayName.d.ts +13 -3
- package/dist/src/node/utils/displayName.js +0 -1
- package/dist/src/node/utils/unique.d.ts +1 -1
- package/dist/src/node/utils/unique.js +10 -14
- package/dist/src/shared/schema/declarations/EntityDecl.d.ts +1 -0
- package/dist/src/web/components/typeInputs/ChildEntitiesTypeInput.js +22 -3
- package/package.json +1 -1
- package/public/css/styles.css +7 -0
package/dist/src/node/index.js
CHANGED
|
@@ -9,7 +9,9 @@ import { validateEntityDecl } from "./schema/declarations/EntityDecl.js";
|
|
|
9
9
|
import { createValidationContext } from "./schema/Node.js";
|
|
10
10
|
import { getEntities } from "./schema/Schema.js";
|
|
11
11
|
import { createServer } from "./server/index.js";
|
|
12
|
+
import { checkCustomConstraintsForAllEntities } from "./utils/customConstraints.js";
|
|
12
13
|
import { asyncForEachInstanceInDatabaseInMemory, countInstancesInDatabaseInMemory, createDatabaseInMemory, getInstancesOfEntityFromDatabaseInMemory, } from "./utils/databaseInMemory.js";
|
|
14
|
+
import { getAllInstanceOverviewsByEntityName } from "./utils/displayName.js";
|
|
13
15
|
import { countError, countErrors, getErrorMessageForDisplay, wrapErrorsIfAny, } from "./utils/error.js";
|
|
14
16
|
import { getFileNameForId, writeInstance } from "./utils/files.js";
|
|
15
17
|
import { checkUniqueConstraintsForAllEntities } from "./utils/unique.js";
|
|
@@ -49,15 +51,27 @@ const _validate = (dataRootPath, entities, databaseInMemory, locales, options =
|
|
|
49
51
|
}
|
|
50
52
|
if (errors.length === 0) {
|
|
51
53
|
debug("Checking unique constraints ...");
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
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);
|
|
55
59
|
debug(`${errorCount.toString()} unique constraint violation${errorCount === 1 ? "" : "s"} found`);
|
|
56
|
-
errors.push(
|
|
60
|
+
errors.push(uniqueConstraintResult.error);
|
|
57
61
|
}
|
|
58
62
|
else {
|
|
59
63
|
debug("No unique constraint violations found");
|
|
60
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
|
+
}
|
|
61
75
|
}
|
|
62
76
|
else {
|
|
63
77
|
debug("Skipping unique constraint checks due to previous structural integrity errors");
|
|
@@ -65,7 +79,7 @@ const _validate = (dataRootPath, entities, databaseInMemory, locales, options =
|
|
|
65
79
|
const totalInstanceCount = countInstancesInDatabaseInMemory(databaseInMemory);
|
|
66
80
|
console.log(`${totalInstanceCount.toString()} instance${totalInstanceCount === 1 ? "" : "s"} checked`);
|
|
67
81
|
if (errors.length === 0) {
|
|
68
|
-
console.log(styleText("green", "All
|
|
82
|
+
console.log(styleText("green", "All instances are valid"));
|
|
69
83
|
return true;
|
|
70
84
|
}
|
|
71
85
|
else {
|
|
@@ -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
|
+
customConstraints?: 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
|
+
customConstraints?: 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
|
+
customConstraints?: 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
|
+
customConstraints: options.customConstraints, // ignore contravariance of registered entity type
|
|
11
13
|
kind: NodeKind.EntityDecl,
|
|
12
14
|
sourceUrl,
|
|
13
15
|
parameters: [],
|
|
@@ -56,5 +58,6 @@ export const serializeEntityDecl = ((type) => ({
|
|
|
56
58
|
? null
|
|
57
59
|
: type.displayName,
|
|
58
60
|
displayNameCustomizer: type.displayNameCustomizer !== undefined,
|
|
61
|
+
customConstraints: type.customConstraints !== undefined,
|
|
59
62
|
}));
|
|
60
63
|
export const getReferencesForEntityDecl = (decl, value, inDecl) => getReferencesForObjectType(decl.type.value, value, [...inDecl, decl]);
|
|
@@ -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,12 @@
|
|
|
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][];
|
|
5
|
+
/**
|
|
6
|
+
* Displays the name of an entity instance including its ID. If no display name is found, `undefined` is returned.
|
|
7
|
+
*/
|
|
8
|
+
export type GetDisplayName = (entity: keyof RegisteredEntityMap, id: string) => string | undefined;
|
|
9
|
+
/**
|
|
10
|
+
* Displays the name of an entity instance including its ID. If no display name is found, only the ID is returned.
|
|
11
|
+
*/
|
|
12
|
+
export type GetDisplayNameWithId = (entity: keyof RegisteredEntityMap, id: string) => string;
|
|
@@ -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 };
|
|
@@ -0,0 +1,50 @@
|
|
|
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, GetDisplayName, GetDisplayNameWithId, 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
|
+
getAllInstances: GetAllInstances;
|
|
21
|
+
getAllChildInstancesForParent: GetAllChildInstancesForParent;
|
|
22
|
+
getDisplayName: GetDisplayName;
|
|
23
|
+
getDisplayNameWithId: GetDisplayNameWithId;
|
|
24
|
+
}) => string[];
|
|
25
|
+
/**
|
|
26
|
+
* A constraint that can be defined on an entity to enforce custom validation logic.
|
|
27
|
+
*
|
|
28
|
+
* The constraint function receives the instance to validate and helper functions
|
|
29
|
+
* to retrieve other instances from the database.
|
|
30
|
+
*
|
|
31
|
+
* It should return an array of strings describing the parts of the constraint
|
|
32
|
+
* that were violated. If the array is empty, the instance is considered valid.
|
|
33
|
+
*/
|
|
34
|
+
export type TypedCustomConstraint<Name extends string> = (params: {
|
|
35
|
+
instanceId: string;
|
|
36
|
+
instanceContent: RegisteredEntity<Name>;
|
|
37
|
+
getInstanceById: GetInstanceById;
|
|
38
|
+
getAllInstances: GetAllInstances;
|
|
39
|
+
getAllChildInstancesForParent: GetAllChildInstancesForParent;
|
|
40
|
+
getDisplayName: GetDisplayName;
|
|
41
|
+
getDisplayNameWithId: GetDisplayNameWithId;
|
|
42
|
+
}) => string[];
|
|
43
|
+
/**
|
|
44
|
+
* Checks all custom constraints for all provided entities and their instances.
|
|
45
|
+
*
|
|
46
|
+
* Returns `Ok` when no violations have been found and an `Error` with a list of
|
|
47
|
+
* `AggregateError`s for each entity if there are any violations of any custom
|
|
48
|
+
* constraint.
|
|
49
|
+
*/
|
|
50
|
+
export declare const checkCustomConstraintsForAllEntities: (db: DatabaseInMemory, entitiesByName: Record<string, EntityDecl>, instanceOverviewsByEntityName: Record<string, InstanceContainerOverview[]>) => Result<void, AggregateError>;
|
|
@@ -0,0 +1,67 @@
|
|
|
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 getInstanceById = (entityName, id) => getInstanceOfEntityFromDatabaseInMemory(db, entityName, id)?.content;
|
|
13
|
+
const getAllInstances = entityName => getInstancesOfEntityFromDatabaseInMemory(db, entityName).map(i => i.content);
|
|
14
|
+
const getAllChildInstancesForParent = (entityName, parentId) => {
|
|
15
|
+
const entity = entitiesByName[entityName];
|
|
16
|
+
if (!entity || !entity.parentReferenceKey) {
|
|
17
|
+
return [];
|
|
18
|
+
}
|
|
19
|
+
const parentKey = entity.parentReferenceKey;
|
|
20
|
+
return getInstancesOfEntityFromDatabaseInMemory(db, entityName)
|
|
21
|
+
.filter(instance => deepEqual(instance.content[parentKey], parentId))
|
|
22
|
+
.map(i => i.content);
|
|
23
|
+
};
|
|
24
|
+
const getDisplayName = (entityName, id) => instanceOverviewsByEntityName[entityName]?.find(o => o.id === id)?.displayName;
|
|
25
|
+
const getDisplayNameWithId = (entityName, id) => {
|
|
26
|
+
const displayName = getDisplayName(entityName, id);
|
|
27
|
+
return displayName ? `"${displayName}" (${id})` : id;
|
|
28
|
+
};
|
|
29
|
+
return mapError(Object.values(entitiesByName).reduce((acc, entity) => {
|
|
30
|
+
const constraintFn = entity.customConstraints;
|
|
31
|
+
if (!constraintFn) {
|
|
32
|
+
return acc;
|
|
33
|
+
}
|
|
34
|
+
const errors = getInstancesOfEntityFromDatabaseInMemory(db, entity.name)
|
|
35
|
+
.map((instance) => [
|
|
36
|
+
instance,
|
|
37
|
+
constraintFn({
|
|
38
|
+
getInstanceById,
|
|
39
|
+
getAllInstances,
|
|
40
|
+
getAllChildInstancesForParent,
|
|
41
|
+
getDisplayName,
|
|
42
|
+
getDisplayNameWithId,
|
|
43
|
+
instanceId: instance.id,
|
|
44
|
+
instanceContent: instance.content,
|
|
45
|
+
}),
|
|
46
|
+
])
|
|
47
|
+
.filter(([, violations]) => violations.length > 0)
|
|
48
|
+
.map(([instance, violations]) => {
|
|
49
|
+
const instanceOverview = instanceOverviewsByEntityName[entity.name]?.find(o => o.id === instance.id);
|
|
50
|
+
const name = instanceOverview
|
|
51
|
+
? `"${instanceOverview.displayName}" (${instance.id})`
|
|
52
|
+
: instance.id;
|
|
53
|
+
return new AggregateError(violations.map(violation => new Error(violation)), `in instance ${name}`);
|
|
54
|
+
});
|
|
55
|
+
const aggregate = errors.length > 0 ? new AggregateError(errors, `in entity "${entity.name}"`) : undefined;
|
|
56
|
+
if (isError(acc)) {
|
|
57
|
+
if (aggregate) {
|
|
58
|
+
return error([...acc.error, aggregate]);
|
|
59
|
+
}
|
|
60
|
+
return acc;
|
|
61
|
+
}
|
|
62
|
+
if (aggregate) {
|
|
63
|
+
return error([aggregate]);
|
|
64
|
+
}
|
|
65
|
+
return acc;
|
|
66
|
+
}, ok()), errors => new AggregateError(errors.toSorted((a, b) => a.message.localeCompare(b.message)), "at least one custom constraint has been violated"));
|
|
67
|
+
};
|
|
@@ -1,6 +1,8 @@
|
|
|
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";
|
|
@@ -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,24 @@
|
|
|
1
1
|
import { type DisplayNameResult } from "../../shared/utils/displayName.ts";
|
|
2
2
|
import type { InstanceContainer, InstanceContainerOverview, InstanceContent } from "../../shared/utils/instances.ts";
|
|
3
3
|
import { type EntityDecl } from "../schema/declarations/EntityDecl.ts";
|
|
4
|
-
import type {
|
|
4
|
+
import type { RegisteredEntity } from "../schema/externalTypes.ts";
|
|
5
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
|
+
instanceId: string;
|
|
13
|
+
instanceDisplayName: string;
|
|
14
|
+
instanceDisplayNameLocaleId: string | undefined;
|
|
15
|
+
locales: string[];
|
|
16
|
+
getInstanceById: (id: string) => InstanceContent | undefined;
|
|
17
|
+
getDisplayNameForInstanceId: (id: string) => DisplayNameResult | undefined;
|
|
18
|
+
getChildInstancesForInstanceId: GetChildInstancesForInstanceId;
|
|
19
|
+
}) => DisplayNameResult;
|
|
20
|
+
export type TypedDisplayNameCustomizer<Name extends string> = (params: {
|
|
21
|
+
instance: RegisteredEntity<Name>;
|
|
12
22
|
instanceId: string;
|
|
13
23
|
instanceDisplayName: string;
|
|
14
24
|
instanceDisplayNameLocaleId: string | undefined;
|
|
@@ -6,7 +6,6 @@ export const getDisplayNameFromEntityInstance = (entity, instanceContainer, getI
|
|
|
6
6
|
if (useCustomizer && entity.displayNameCustomizer) {
|
|
7
7
|
const calculatedName = getDisplayNameFromEntityInstance(entity, instanceContainer, getInstanceById, getChildInstancesForInstanceId, locales, defaultName, false);
|
|
8
8
|
return entity.displayNameCustomizer({
|
|
9
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment -- otherwise type instiatiation too deep
|
|
10
9
|
instance: instanceContainer.content,
|
|
11
10
|
instanceId: instanceContainer.id,
|
|
12
11
|
instanceDisplayName: calculatedName.name,
|
|
@@ -20,4 +20,4 @@ export declare const checkUniqueConstraintsForEntity: (entity: EntityDecl, insta
|
|
|
20
20
|
* `AggregateError`s for each entity if there are any violations of any unique
|
|
21
21
|
* constraint.
|
|
22
22
|
*/
|
|
23
|
-
export declare const checkUniqueConstraintsForAllEntities: (db: DatabaseInMemory, entitiesByName: Record<string, EntityDecl>,
|
|
23
|
+
export declare const checkUniqueConstraintsForAllEntities: (db: DatabaseInMemory, entitiesByName: Record<string, EntityDecl>, instanceOverviewsByEntityName: Record<string, InstanceContainerOverview[]>) => Result<void, AggregateError>;
|
|
@@ -3,7 +3,6 @@ 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
|
-
import { getAllInstanceOverviewsByEntityName } from "./displayName.js";
|
|
7
6
|
export class UniqueConstraintError extends Error {
|
|
8
7
|
parts;
|
|
9
8
|
constructor(message, parts) {
|
|
@@ -91,19 +90,16 @@ export const checkUniqueConstraintsForEntity = (entity, instances, instanceOverv
|
|
|
91
90
|
* `AggregateError`s for each entity if there are any violations of any unique
|
|
92
91
|
* constraint.
|
|
93
92
|
*/
|
|
94
|
-
export const checkUniqueConstraintsForAllEntities = (db, entitiesByName,
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
const resultForEntity = checkUniqueConstraintsForEntity(entity, getInstancesOfEntityFromDatabaseInMemory(db, entity.name), instanceOverviewsByEntityName[entity.name] ?? []);
|
|
98
|
-
if (isError(acc)) {
|
|
99
|
-
if (isError(resultForEntity)) {
|
|
100
|
-
return error([...acc.error, resultForEntity.error]);
|
|
101
|
-
}
|
|
102
|
-
return acc;
|
|
103
|
-
}
|
|
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] ?? []);
|
|
95
|
+
if (isError(acc)) {
|
|
104
96
|
if (isError(resultForEntity)) {
|
|
105
|
-
return error([resultForEntity.error]);
|
|
97
|
+
return error([...acc.error, resultForEntity.error]);
|
|
106
98
|
}
|
|
107
99
|
return acc;
|
|
108
|
-
}
|
|
109
|
-
|
|
100
|
+
}
|
|
101
|
+
if (isError(resultForEntity)) {
|
|
102
|
+
return error([resultForEntity.error]);
|
|
103
|
+
}
|
|
104
|
+
return acc;
|
|
105
|
+
}, ok()), errors => new AggregateError(errors.toSorted((a, b) => a.message.localeCompare(b.message)), "at least one unique constraint has been violated"));
|
|
@@ -57,6 +57,7 @@ export interface SerializedEntityDecl<Name extends string = string, T extends TS
|
|
|
57
57
|
displayNameCustomizer: boolean;
|
|
58
58
|
isDeprecated?: boolean;
|
|
59
59
|
uniqueConstraints?: UniqueConstraints;
|
|
60
|
+
customConstraints: boolean;
|
|
60
61
|
}
|
|
61
62
|
export declare const isSerializedEntityDecl: (node: SerializedNode) => node is SerializedEntityDecl;
|
|
62
63
|
export declare const isSerializedEntityDeclWithParentReference: <Name extends string, T extends TSerializedConstraint, FK extends Extract<keyof T, string> | undefined>(decl: SerializedEntityDecl<Name, T, FK>) => decl is SerializedEntityDecl<Name, T, NonNullable<FK>>;
|
|
@@ -30,6 +30,23 @@ export const ChildEntitiesTypeInput = props => {
|
|
|
30
30
|
{ entityName, childInstances: [], id: undefined, content: value },
|
|
31
31
|
]);
|
|
32
32
|
}, [setChildInstances]);
|
|
33
|
+
const onChildDuplicate = useCallback((index) => {
|
|
34
|
+
const setChildInstancesAsNew = (childInstances) => childInstances.map(ci => ({
|
|
35
|
+
...ci,
|
|
36
|
+
childInstances: setChildInstancesAsNew(ci.childInstances),
|
|
37
|
+
id: undefined,
|
|
38
|
+
}));
|
|
39
|
+
setChildInstances(old => old[index]
|
|
40
|
+
? [
|
|
41
|
+
...old,
|
|
42
|
+
{
|
|
43
|
+
...old[index],
|
|
44
|
+
childInstances: setChildInstancesAsNew(old[index].childInstances),
|
|
45
|
+
id: undefined,
|
|
46
|
+
},
|
|
47
|
+
]
|
|
48
|
+
: old);
|
|
49
|
+
}, [setChildInstances]);
|
|
33
50
|
const onChildRemove = useCallback((index) => {
|
|
34
51
|
setChildInstances(old => removeAt(old, index));
|
|
35
52
|
}, [setChildInstances]);
|
|
@@ -39,9 +56,11 @@ export const ChildEntitiesTypeInput = props => {
|
|
|
39
56
|
if (path === undefined) {
|
|
40
57
|
return _jsx("div", { role: "alert", children: "A child entities type cannot be the root type of a document." });
|
|
41
58
|
}
|
|
42
|
-
return (_jsxs("div", { class: "field field--container field--array", children: [childInstancesForEntity.length > 0 ? (_jsx("ol", { children: childInstancesForEntity.map(([item, originalIndex], i) => (_jsxs("li", { class: "container-item array-item", children: [_jsxs("div", { className: "container-item-header", children: [_jsxs("div", { className: "container-item-title", children: [i + 1, "."] }),
|
|
43
|
-
|
|
44
|
-
|
|
59
|
+
return (_jsxs("div", { class: "field field--container field--array", children: [childInstancesForEntity.length > 0 ? (_jsx("ol", { children: childInstancesForEntity.map(([item, originalIndex], i) => (_jsxs("li", { class: "container-item array-item", children: [_jsxs("div", { className: "container-item-header", children: [_jsxs("div", { className: "container-item-title", children: [i + 1, "."] }), _jsxs("div", { class: "container-item-actions", children: [_jsxs("button", { onClick: () => {
|
|
60
|
+
onChildDuplicate(i);
|
|
61
|
+
}, disabled: disabled, children: ["Duplicate Item #", i + 1] }), _jsxs("button", { class: "destructive", onClick: () => {
|
|
62
|
+
onChildRemove(i);
|
|
63
|
+
}, disabled: disabled, children: ["Delete Item #", i + 1] })] })] }), _jsx(TypeInput, { ...props, type: childEntity.type, value: item.content, parentKey: childEntity.parentReferenceKey, onChange: newItem => {
|
|
45
64
|
onChildChange(originalIndex, newItem); // guaranteed to be an object because of the ObjectType in the child entity
|
|
46
65
|
}, childInstances: item.childInstances, setChildInstances: newChildInstances => {
|
|
47
66
|
onGrandChildrenChange(originalIndex, newChildInstances);
|
package/package.json
CHANGED
package/public/css/styles.css
CHANGED
|
@@ -856,6 +856,13 @@ form > .field--container {
|
|
|
856
856
|
margin: 0.125rem 0 0;
|
|
857
857
|
}
|
|
858
858
|
|
|
859
|
+
.container-item-actions {
|
|
860
|
+
flex: none;
|
|
861
|
+
display: flex;
|
|
862
|
+
gap: 0.5rem;
|
|
863
|
+
margin-left: 1rem;
|
|
864
|
+
}
|
|
865
|
+
|
|
859
866
|
.add-item-container {
|
|
860
867
|
display: flex;
|
|
861
868
|
margin-top: 1rem;
|