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.
@@ -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 constraintResult = checkUniqueConstraintsForAllEntities(databaseInMemory, Object.fromEntries(entities.map(entity => [entity.name, entity])), locales);
53
- if (isError(constraintResult)) {
54
- const errorCount = countError(constraintResult.error);
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(constraintResult.error);
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 entities are valid"));
82
+ console.log(styleText("green", "All instances are valid"));
69
83
  return true;
70
84
  }
71
85
  else {
@@ -6,3 +6,4 @@ export interface Schema {
6
6
  }
7
7
  export declare const Schema: (declarations: Decl[], localeEntity?: EntityDecl) => Schema;
8
8
  export declare const getEntities: (schema: Schema) => EntityDecl[];
9
+ export type * from "./externalTypes.ts";
@@ -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 { DisplayNameCustomizer } from "../../utils/displayName.ts";
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<ObjectType<T>>;
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?: DisplayNameCustomizer<ObjectType<T>>;
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?: DisplayNameCustomizer<ObjectType<T>>;
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<ObjectType<T>>;
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 constraintResult = checkUniqueConstraintsForAllEntities(newDb, entitiesByName, locales);
22
- if (isError(constraintResult)) {
23
- return constraintResult;
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 { AsDeepType, Type } from "../schema/types/Type.ts";
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<T extends Type> = (params: {
11
- instance: AsDeepType<T>;
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>, locales: string[]) => Result<void, AggregateError>;
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, locales) => {
95
- const instanceOverviewsByEntityName = getAllInstanceOverviewsByEntityName(entitiesByName, db, locales);
96
- return mapError(Object.values(entitiesByName).reduce((acc, entity) => {
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
- }, ok()), errors => new AggregateError(errors.toSorted((a, b) => a.message.localeCompare(b.message)), "at least one unique constraint has been violated"));
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, "."] }), _jsx("button", { class: "destructive", onClick: () => {
43
- onChildRemove(i);
44
- }, disabled: disabled, children: "Delete Item" })] }), _jsx(TypeInput, { ...props, type: childEntity.type, value: item.content, parentKey: childEntity.parentReferenceKey, onChange: newItem => {
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tsondb",
3
- "version": "0.14.0",
3
+ "version": "0.16.0",
4
4
  "description": "",
5
5
  "license": "ISC",
6
6
  "author": "Lukas Obermann",
@@ -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;