tsondb 0.14.0 → 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.
@@ -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
+ 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?: DisplayNameCustomizer<ObjectType<T>>;
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?: DisplayNameCustomizer<ObjectType<T>>;
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<ObjectType<T>>;
31
+ displayNameCustomizer?: DisplayNameCustomizer;
32
32
  isDeprecated?: boolean;
33
33
  }) => NestedEntityMapType<Name, T>;
34
34
  export { NestedEntityMapType as NestedEntityMap };
@@ -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
+ };
@@ -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"));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tsondb",
3
- "version": "0.14.0",
3
+ "version": "0.15.0",
4
4
  "description": "",
5
5
  "license": "ISC",
6
6
  "author": "Lukas Obermann",