tsondb 0.12.9 → 0.13.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.
@@ -2,6 +2,7 @@ import Debug from "debug";
2
2
  import { mkdir } from "fs/promises";
3
3
  import { join, sep } from "path";
4
4
  import { styleText } from "util";
5
+ import { isError } from "../shared/utils/result.js";
5
6
  import { parallelizeErrors } from "../shared/utils/validation.js";
6
7
  import { validateEntityDecl } from "./schema/declarations/EntityDecl.js";
7
8
  import { createValidationContext } from "./schema/Node.js";
@@ -10,6 +11,7 @@ import { createServer } from "./server/index.js";
10
11
  import { asyncForEachInstanceInDatabaseInMemory, createDatabaseInMemory, getInstancesOfEntityFromDatabaseInMemory, } from "./utils/databaseInMemory.js";
11
12
  import { countErrors, getErrorMessageForDisplay, wrapErrorsIfAny } from "./utils/error.js";
12
13
  import { getFileNameForId, writeInstance } from "./utils/files.js";
14
+ import { checkUniqueConstraintsForAllEntities } from "./utils/unique.js";
13
15
  const debug = Debug("tsondb:jsapi");
14
16
  const prepareFolders = async (dataRootPath, entities) => {
15
17
  await mkdir(dataRootPath, { recursive: true });
@@ -31,11 +33,32 @@ const _validate = (dataRootPath, entities, databaseInMemory, options = {}) => {
31
33
  }
32
34
  }
33
35
  const validationContext = createValidationContext(options, databaseInMemory, true, checkReferentialIntegrity);
36
+ debug("Checking structural integrity ...");
34
37
  const errors = (checkOnlyEntities.length > 0
35
38
  ? entities.filter(entity => checkOnlyEntities.includes(entity.name))
36
39
  : entities)
37
40
  .flatMap(entity => parallelizeErrors(getInstancesOfEntityFromDatabaseInMemory(databaseInMemory, entity.name).map(instance => wrapErrorsIfAny(`in file ${styleText("white", `"${dataRootPath}${sep}${styleText("bold", join(entity.name, getFileNameForId(instance.id)))}"`)}`, validateEntityDecl(validationContext, [], entity, instance.content)))))
38
41
  .toSorted((a, b) => a.message.localeCompare(b.message));
42
+ if (errors.length > 0) {
43
+ debug(`${errors.length.toString()} structural integrity violation${errors.length === 1 ? "" : "s"} found`);
44
+ }
45
+ else {
46
+ debug("No structural integrity violations found");
47
+ }
48
+ if (errors.length === 0) {
49
+ debug("Checking unique constraints ...");
50
+ const constraintResult = checkUniqueConstraintsForAllEntities(databaseInMemory, entities);
51
+ if (isError(constraintResult)) {
52
+ debug(`${constraintResult.error.errors.length.toString()} unique constraint violation${constraintResult.error.errors.length === 1 ? "" : "s"} found`);
53
+ errors.push(constraintResult.error);
54
+ }
55
+ else {
56
+ debug("No unique constraint violations found");
57
+ }
58
+ }
59
+ else {
60
+ debug("Skipping unique constraint checks due to previous structural integrity errors");
61
+ }
39
62
  if (errors.length === 0) {
40
63
  debug("All entities are valid");
41
64
  }
@@ -3,7 +3,16 @@ export type TypeScriptRendererOptions = {
3
3
  indentation: number;
4
4
  objectTypeKeyword: "interface" | "type";
5
5
  preserveFiles: boolean;
6
- generateEntityMapType: boolean;
6
+ generateHelpers: Partial<{
7
+ /**
8
+ * If `true` or a string, generates an object type with all names of the entities as the key and their corresponding generated type as their respective value. Uses `EntityMap` as a default name when using `true` or the string if set to a string.
9
+ */
10
+ entityMap: boolean | string;
11
+ /**
12
+ * If `true` or a string, generates an object type with all names of entities with parent references as the key and their corresponding generated type as well as their parent reference key as their respective value. Uses `ChildEntityMap` as a default name when using `true` or the string if set to a string.
13
+ */
14
+ childEntityMap: boolean | string;
15
+ }>;
7
16
  addIdentifierToEntities: boolean;
8
17
  /**
9
18
  * Infer translation parameter types from the message strings in a {@link TranslationObjectType TranslationObject} as branded types.
@@ -6,7 +6,7 @@ import { toCamelCase } from "../../../shared/utils/string.js";
6
6
  import { extractParameterTypeNamesFromMessage, mapParameterTypeNames, } from "../../../shared/utils/translation.js";
7
7
  import { assertExhaustive } from "../../../shared/utils/typeSafety.js";
8
8
  import { asDecl } from "../../schema/declarations/Declaration.js";
9
- import { addEphemeralUUIDToType, createEntityIdentifierTypeAsDecl, isEntityDecl, } from "../../schema/declarations/EntityDecl.js";
9
+ import { addEphemeralUUIDToType, createEntityIdentifierTypeAsDecl, isEntityDecl, isEntityDeclWithParentReference, } from "../../schema/declarations/EntityDecl.js";
10
10
  import { TypeAliasDecl } from "../../schema/declarations/TypeAliasDecl.js";
11
11
  import { flatMapAuxiliaryDecls, NodeKind } from "../../schema/Node.js";
12
12
  import { ArrayType } from "../../schema/types/generic/ArrayType.js";
@@ -20,7 +20,7 @@ const defaultOptions = {
20
20
  indentation: 2,
21
21
  objectTypeKeyword: "interface",
22
22
  preserveFiles: false,
23
- generateEntityMapType: false,
23
+ generateHelpers: {},
24
24
  addIdentifierToEntities: false,
25
25
  };
26
26
  const renderDocumentation = (comment, isDeprecated) => syntax `${comment === undefined
@@ -158,20 +158,63 @@ const renderImports = (currentUrl, imports) => {
158
158
  .join(EOL);
159
159
  return importsSyntax.length > 0 ? importsSyntax + EOL + EOL : "";
160
160
  };
161
- const renderEntityMapType = (options, declarations) => syntax `export type EntityMap = {${EOL}${indent(options.indentation, 1, combineSyntaxes(declarations
162
- .filter(isEntityDecl)
163
- .sort((a, b) => a.name.localeCompare(b.name))
164
- .map(decl => syntax `${decl.name}: ${decl.name}`), EOL))}${EOL}}${EOL + EOL}`;
161
+ const renderEntityMapType = (options, declarations) => options.generateHelpers.entityMap
162
+ ? `export type ${options.generateHelpers.entityMap === true ? "EntityMap" : options.generateHelpers.entityMap} = {${EOL}${prefixLines(getIndentation(options.indentation, 1), declarations
163
+ .filter(isEntityDecl)
164
+ .sort((a, b) => a.name.localeCompare(b.name))
165
+ .map(decl => `${decl.name}: ${decl.name}`)
166
+ .join(EOL))}${EOL}}${EOL + EOL}`
167
+ : "";
168
+ const renderChildEntityMapType = (options, declarations) => options.generateHelpers.childEntityMap
169
+ ? `export type ${options.generateHelpers.childEntityMap === true ? "ChildEntityMap" : options.generateHelpers.childEntityMap} = {${EOL}${prefixLines(getIndentation(options.indentation, 1), declarations
170
+ .filter(isEntityDecl)
171
+ .filter(isEntityDeclWithParentReference)
172
+ .sort((a, b) => a.name.localeCompare(b.name))
173
+ .map(decl => `${decl.name}: [${decl.name}, "${decl.parentReferenceKey}", ${decl.name}["${decl.parentReferenceKey}"]]`)
174
+ .join(EOL))}${EOL}}${EOL + EOL}`
175
+ : "";
165
176
  const renderStringableTranslationParameterType = (options) => options.inferTranslationParameters?.format === "mf2"
166
- ? "export type StringableTranslationParameter = {\n" +
177
+ ? "export type StringableTranslationParameter = {" +
178
+ EOL +
167
179
  prefixLines(getIndentation(options.indentation, 1), "toString(): string") +
168
- "\n}\n\n"
180
+ EOL +
181
+ "}" +
182
+ EOL +
183
+ EOL
169
184
  : "";
185
+ // const renderGetterConstruct = (
186
+ // config: GetterOptions | undefined,
187
+ // getterType: "all" | "byId",
188
+ // defaultReturnTypeBuilder: (entity: EntityDecl) => string,
189
+ // declarations: readonly Decl[],
190
+ // ) => {
191
+ // if (!config) {
192
+ // return ""
193
+ // }
194
+ // const { returnTypeBuilder = defaultReturnTypeBuilder } = config
195
+ // const getLine: (decl: EntityDecl) => string = decl =>
196
+ // `export type ${decl.name} = (${getterType === "byId" ? `id: ${decl.name}_ID` : ""}) => ${returnTypeBuilder(decl)}`
197
+ // return (declarations.filter(isEntityDecl).map(getLine).join(EOL), EOL + EOL)
198
+ // }
199
+ // const renderGetters = (
200
+ // config:
201
+ // | Partial<{
202
+ // byId: GetterOptions
203
+ // all: GetterOptions
204
+ // }>
205
+ // | undefined,
206
+ // declarations: readonly Decl[],
207
+ // ): string =>
208
+ // (
209
+ // [
210
+ // ["all", (entity: EntityDecl) => entity.name + "[]"],
211
+ // ["byId", (entity: EntityDecl) => entity.name + " | undefined"],
212
+ // ] as const
213
+ // )
214
+ // .map(key => renderGetterConstruct(config?.[key[0]], key[0], key[1], declarations))
215
+ // .join("")
170
216
  export const render = (options = defaultOptions, declarations) => {
171
217
  const finalOptions = { ...defaultOptions, ...options };
172
- const [_, entityMap] = finalOptions.generateEntityMapType
173
- ? renderEntityMapType(finalOptions, declarations)
174
- : emptyRenderResult;
175
218
  const [imports, content] = renderDeclarations(finalOptions, flatMapAuxiliaryDecls((parentNodes, node) => {
176
219
  if (isNestedEntityMapType(node)) {
177
220
  return TypeAliasDecl(asDecl(parentNodes[0])?.sourceUrl ?? "", {
@@ -185,7 +228,8 @@ export const render = (options = defaultOptions, declarations) => {
185
228
  }
186
229
  return undefined;
187
230
  }, declarations));
188
- return (entityMap +
231
+ return (renderEntityMapType(finalOptions, declarations) +
232
+ renderChildEntityMapType(finalOptions, declarations) +
189
233
  renderStringableTranslationParameterType(finalOptions) +
190
234
  (finalOptions.preserveFiles
191
235
  ? (declarations[0] === undefined ? "" : renderImports(declarations[0].sourceUrl, imports)) +
@@ -1,4 +1,7 @@
1
1
  import Debug from "debug";
2
+ import { normalizeKeyPath, renderKeyPath, } from "../../shared/schema/declarations/EntityDecl.js";
3
+ import { anySame } from "../../shared/utils/array.js";
4
+ import { deepEqual } from "../../shared/utils/compare.js";
2
5
  import { assertExhaustive } from "../../shared/utils/typeSafety.js";
3
6
  import { getParameterNames, walkNodeTree } from "./declarations/Declaration.js";
4
7
  import { isEntityDecl } from "./declarations/EntityDecl.js";
@@ -215,6 +218,66 @@ const checkRecursiveGenericTypeAliasesAndEnumerationsAreOnlyParameterizedDirectl
215
218
  }, decl);
216
219
  }
217
220
  };
221
+ const getNodeAtKeyPath = (decl, objectType, keyPath, parent, parentPath = []) => {
222
+ const [key, ...keyPathRest] = normalizeKeyPath(keyPath);
223
+ if (key === undefined) {
224
+ return objectType;
225
+ }
226
+ const memberDecl = objectType.properties[key];
227
+ if (memberDecl === undefined) {
228
+ throw TypeError(`key "${key}"${parentPath.length === 0 ? "" : " in " + renderKeyPath(parentPath)} in unique constraint of entity "${decl.name}" does not exist in the entity`);
229
+ }
230
+ const value = memberDecl.type;
231
+ const actualValue = isIncludeIdentifierType(value) ? value.reference.type.value : value;
232
+ if (keyPathRest.length > 0) {
233
+ if (isObjectType(actualValue)) {
234
+ return getNodeAtKeyPath(decl, actualValue, keyPathRest, parent, [...parentPath, key]);
235
+ }
236
+ throw TypeError(`value at key "${key}"${parentPath.length === 0 ? "" : ' in "' + renderKeyPath(parentPath) + '"'}${parent ? " " + parent : ""} in unique constraint of entity "${decl.name}" does not contain an object type`);
237
+ }
238
+ return value;
239
+ };
240
+ const checkUniqueConstraintElement = (decl, element) => {
241
+ if ("keyPath" in element) {
242
+ getNodeAtKeyPath(decl, decl.type.value, element.keyPath);
243
+ if (element.keyPathFallback !== undefined) {
244
+ getNodeAtKeyPath(decl, decl.type.value, element.keyPathFallback);
245
+ }
246
+ }
247
+ else {
248
+ const entityMapType = getNodeAtKeyPath(decl, decl.type.value, element.entityMapKeyPath);
249
+ if (!isNestedEntityMapType(entityMapType)) {
250
+ throw TypeError(`value at key "${renderKeyPath(element.entityMapKeyPath)}" is not a nested entity map as required by the unique constraint of entity "${decl.name}"`);
251
+ }
252
+ const nestedType = entityMapType.type.value;
253
+ const actualType = isIncludeIdentifierType(nestedType)
254
+ ? nestedType.reference.type.value
255
+ : nestedType;
256
+ getNodeAtKeyPath(decl, actualType, element.keyPathInEntityMap, `in entity map "${renderKeyPath(element.entityMapKeyPath)}"`);
257
+ if (element.keyPathInEntityMapFallback !== undefined) {
258
+ getNodeAtKeyPath(decl, actualType, element.keyPathInEntityMapFallback, `in entity map "${renderKeyPath(element.entityMapKeyPath)}"`);
259
+ }
260
+ }
261
+ };
262
+ const checkUniqueConstraints = (declarations) => {
263
+ for (const decl of declarations) {
264
+ if (isEntityDecl(decl)) {
265
+ for (const constraint of decl.uniqueConstraints ?? []) {
266
+ if (Array.isArray(constraint)) {
267
+ for (const constraintPart of constraint) {
268
+ checkUniqueConstraintElement(decl, constraintPart);
269
+ }
270
+ if (anySame(constraint, deepEqual)) {
271
+ throw TypeError(`there are duplicate key descriptions in a combined constraint of entity "${decl.name}"`);
272
+ }
273
+ }
274
+ else {
275
+ checkUniqueConstraintElement(decl, constraint);
276
+ }
277
+ }
278
+ }
279
+ }
280
+ };
218
281
  const addDeclarations = (existingDecls, declsToAdd) => declsToAdd.reduce((accDecls, decl) => {
219
282
  if (!accDecls.includes(decl)) {
220
283
  return getNestedDeclarations(accDecls, decl.kind === "NestedEntity" ? decl.type : decl, undefined);
@@ -242,6 +305,8 @@ export const Schema = (declarations, localeEntity) => {
242
305
  checkChildEntityTypes(localeEntity, allDeclsWithoutNestedEntities);
243
306
  debug("checking generic recursive types ...");
244
307
  checkRecursiveGenericTypeAliasesAndEnumerationsAreOnlyParameterizedDirectlyInTypeAliases(allDeclsWithoutNestedEntities);
308
+ debug("checking unique constraints ...");
309
+ checkUniqueConstraints(allDeclsWithoutNestedEntities);
245
310
  debug("created schema, no integrity violations found");
246
311
  return {
247
312
  declarations: allDeclsWithoutNestedEntities,
@@ -1,3 +1,4 @@
1
+ import type { UniqueConstraints } from "../../../shared/schema/declarations/EntityDecl.ts";
1
2
  import { Lazy } from "../../../shared/utils/lazy.ts";
2
3
  import type { DisplayNameCustomizer } from "../../utils/displayName.ts";
3
4
  import type { GetNestedDeclarations, GetReferences, Predicate, Serialized, TypeArgumentsResolver, Validator } from "../Node.ts";
@@ -36,6 +37,7 @@ export interface EntityDecl<Name extends string = string, T extends TConstraint
36
37
  displayName?: GenericEntityDisplayName;
37
38
  displayNameCustomizer?: DisplayNameCustomizer<ObjectType<T>>;
38
39
  isDeprecated?: boolean;
40
+ uniqueConstraints?: UniqueConstraints;
39
41
  }
40
42
  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> {
41
43
  }
@@ -51,6 +53,7 @@ export declare const EntityDecl: {
51
53
  displayName?: EntityDisplayName<T>;
52
54
  displayNameCustomizer?: DisplayNameCustomizer<ObjectType<T>>;
53
55
  isDeprecated?: boolean;
56
+ uniqueConstraints?: UniqueConstraints;
54
57
  }): EntityDecl<Name, T, undefined>;
55
58
  <Name extends string, T extends TConstraint, FK extends Extract<keyof T, string>>(sourceUrl: string, options: {
56
59
  name: Name;
@@ -64,6 +67,7 @@ export declare const EntityDecl: {
64
67
  displayName?: EntityDisplayName<T>;
65
68
  displayNameCustomizer?: DisplayNameCustomizer<ObjectType<T>>;
66
69
  isDeprecated?: boolean;
70
+ uniqueConstraints?: UniqueConstraints;
67
71
  }): EntityDecl<Name, T, FK>;
68
72
  };
69
73
  export { EntityDecl as Entity };
@@ -3,6 +3,7 @@ import { deleteInstanceInDatabaseInMemory, setInstanceInDatabaseInMemory, } from
3
3
  import { applyStepsToDisk } from "./databaseOnDisk.js";
4
4
  import { attachGitStatusToDatabaseInMemory } from "./git.js";
5
5
  import { updateReferencesToInstances } from "./references.js";
6
+ import { checkUniqueConstraintsForAllEntities } from "./unique.js";
6
7
  /**
7
8
  * Run a transaction on the database in memory, applying all changes to disk if successful.
8
9
  */
@@ -17,6 +18,10 @@ export const runDatabaseTransaction = async (root, git, entitiesByName, database
17
18
  return result;
18
19
  }
19
20
  const { db: newDb, refs: newRefs, steps, instanceContainer } = result.value;
21
+ const constraintResult = checkUniqueConstraintsForAllEntities(newDb, Object.values(entitiesByName));
22
+ if (isError(constraintResult)) {
23
+ return constraintResult;
24
+ }
20
25
  const diskResult = await applyStepsToDisk(root, steps);
21
26
  if (isError(diskResult)) {
22
27
  return diskResult;
@@ -0,0 +1,19 @@
1
+ import type { InstanceContainer } from "../../shared/utils/instances.ts";
2
+ import { type Result } from "../../shared/utils/result.ts";
3
+ import type { EntityDecl } from "../schema/index.ts";
4
+ import { type DatabaseInMemory } from "./databaseInMemory.ts";
5
+ /**
6
+ * Checks all unique constraints for the provided entity and its instances.
7
+ *
8
+ * Returns `Ok` when no violations have been found and an `Error` with an
9
+ * `AggregateError` if there are any violations of any unique constraint.
10
+ */
11
+ export declare const checkUniqueConstraintsForEntity: (entity: EntityDecl, instances: InstanceContainer[]) => Result<void, AggregateError>;
12
+ /**
13
+ * Checks all unique constraints for all provided entities and their instances.
14
+ *
15
+ * Returns `Ok` when no violations have been found and an `Error` with a list of
16
+ * `AggregateError`s for each entity if there are any violations of any unique
17
+ * constraint.
18
+ */
19
+ export declare const checkUniqueConstraintsForAllEntities: (db: DatabaseInMemory, entities: EntityDecl[]) => Result<void, AggregateError>;
@@ -0,0 +1,94 @@
1
+ import { normalizeKeyPath, renderKeyPath, } from "../../shared/schema/declarations/EntityDecl.js";
2
+ import { anySameIndices, flatCombine } from "../../shared/utils/array.js";
3
+ import { deepEqual } from "../../shared/utils/compare.js";
4
+ import { error, isError, mapError, ok } from "../../shared/utils/result.js";
5
+ import { getInstancesOfEntityFromDatabaseInMemory, } from "./databaseInMemory.js";
6
+ const listFormatter = new Intl.ListFormat("en-US", { type: "conjunction" });
7
+ const printUniqueConstraint = (constraint) => (Array.isArray(constraint) ? constraint : [constraint])
8
+ .map(elem => "keyPath" in elem
9
+ ? renderKeyPath(elem.keyPath) +
10
+ (elem.keyPathFallback ? "|" + renderKeyPath(elem.keyPathFallback) : "")
11
+ : renderKeyPath(elem.entityMapKeyPath) +
12
+ "[...]." +
13
+ (elem.keyPathInEntityMapFallback
14
+ ? "(" +
15
+ renderKeyPath(elem.keyPathInEntityMap) +
16
+ "|" +
17
+ renderKeyPath(elem.keyPathInEntityMapFallback) +
18
+ ")"
19
+ : renderKeyPath(elem.keyPathInEntityMap)))
20
+ .join("+");
21
+ const unsafeGetValueAtKeyPath = (value, keyPath) => {
22
+ let acc = value;
23
+ for (const key of normalizeKeyPath(keyPath)) {
24
+ acc = acc[key];
25
+ }
26
+ return acc;
27
+ };
28
+ /**
29
+ * Checks all unique constraints for the provided entity and its instances.
30
+ *
31
+ * Returns `Ok` when no violations have been found and an `Error` with an
32
+ * `AggregateError` if there are any violations of any unique constraint.
33
+ */
34
+ export const checkUniqueConstraintsForEntity = (entity, instances) => {
35
+ const constraintErrors = [];
36
+ const constraints = entity.uniqueConstraints ?? [];
37
+ for (const [constraintIndex, constraint] of constraints.entries()) {
38
+ const normalizedConstraint = Array.isArray(constraint) ? constraint : [constraint];
39
+ // the index contains all instances as rows with multiple rows per instance for all possible combinations of nested entity map values
40
+ const index = instances.flatMap(({ id, content }) => flatCombine(normalizedConstraint.map(elem => {
41
+ if ("keyPath" in elem) {
42
+ return [
43
+ unsafeGetValueAtKeyPath(content, elem.keyPath) ??
44
+ (elem.keyPathFallback
45
+ ? unsafeGetValueAtKeyPath(content, elem.keyPathFallback)
46
+ : undefined),
47
+ ];
48
+ }
49
+ else {
50
+ return Object.entries(unsafeGetValueAtKeyPath(content, elem.entityMapKeyPath)).map(([nestedId, nestedContent]) => [
51
+ nestedId,
52
+ unsafeGetValueAtKeyPath(nestedContent, elem.keyPathInEntityMap) ??
53
+ (elem.keyPathInEntityMapFallback
54
+ ? unsafeGetValueAtKeyPath(nestedContent, elem.keyPathInEntityMapFallback)
55
+ : undefined),
56
+ ]);
57
+ }
58
+ })).map((row) => [id, row]));
59
+ const duplicates = anySameIndices(index, (a, b) => deepEqual(a[1], b[1]));
60
+ if (duplicates.length > 0) {
61
+ constraintErrors.push([
62
+ constraintIndex,
63
+ // 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][0])),
65
+ ]);
66
+ }
67
+ }
68
+ if (constraintErrors.length > 0) {
69
+ return error(new AggregateError(constraintErrors.map(([constraintIndex, constraintErrors]) => new AggregateError(constraintErrors.map(error => new Error(`instances ${listFormatter.format(error)} contain duplicate values`)),
70
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- constraint must be present
71
+ `in unique constraint ${printUniqueConstraint(constraints[constraintIndex])}`)), `in entity "${entity.name}"`));
72
+ }
73
+ return ok();
74
+ };
75
+ /**
76
+ * Checks all unique constraints for all provided entities and their instances.
77
+ *
78
+ * Returns `Ok` when no violations have been found and an `Error` with a list of
79
+ * `AggregateError`s for each entity if there are any violations of any unique
80
+ * constraint.
81
+ */
82
+ export const checkUniqueConstraintsForAllEntities = (db, entities) => mapError(entities.reduce((acc, entity) => {
83
+ const resultForEntity = checkUniqueConstraintsForEntity(entity, getInstancesOfEntityFromDatabaseInMemory(db, entity.name));
84
+ if (isError(acc)) {
85
+ if (isError(resultForEntity)) {
86
+ return error([...acc.error, resultForEntity.error]);
87
+ }
88
+ return acc;
89
+ }
90
+ if (isError(resultForEntity)) {
91
+ return error([resultForEntity.error]);
92
+ }
93
+ return acc;
94
+ }, ok()), errors => new AggregateError(errors, "at least one unique constraint has been violated"));
@@ -18,6 +18,33 @@ type TSerializedConstraint = Record<string, SerializedMemberDecl>;
18
18
  type SerializedPathTo<T extends TSerializedConstraint, R> = {
19
19
  [K in keyof T]: T[K] extends SerializedMemberDecl<infer V> ? V extends R ? K : R extends V ? string : T[K] extends SerializedObjectType<infer P> ? `${Extract<K, string>}.${SerializedPathTo<P, R>}` : never : never;
20
20
  }[Extract<keyof T, string>];
21
+ export type KeyPath = string | string[];
22
+ export declare const normalizeKeyPath: (keyPath: KeyPath) => string[];
23
+ export declare const renderKeyPath: (keyPath: KeyPath) => string;
24
+ /**
25
+ * A uniquing element can be the full value of a key or the value of a key in a nested entity map.
26
+ */
27
+ export type UniquingElement = {
28
+ keyPath: KeyPath;
29
+ keyPathFallback?: KeyPath;
30
+ } | {
31
+ entityMapKeyPath: KeyPath;
32
+ keyPathInEntityMap: KeyPath;
33
+ keyPathInEntityMapFallback?: KeyPath;
34
+ };
35
+ export type UniqueConstraint = UniquingElement | UniquingElement[];
36
+ /**
37
+ * A list of keys or key descriptions whose values need to be unique across all instances in the entity.
38
+ *
39
+ * One or more constraints may be provided. A nested array indicates that the combination of described values must be unique.
40
+ *
41
+ * @example
42
+ *
43
+ * ["name"] // all `name` keys must be unique across the entity
44
+ * ["name", "age"] // all `name` keys and all `age` keys must be unique across the entity
45
+ * [["name", "age"]] // the combination of `name` and `age` must be unique across the entity
46
+ */
47
+ export type UniqueConstraints = UniqueConstraint[];
21
48
  export interface SerializedEntityDecl<Name extends string = string, T extends TSerializedConstraint = TSerializedConstraint, FK extends Extract<keyof T, string> | undefined = Extract<keyof T, string> | undefined> extends SerializedBaseDecl<Name, []> {
22
49
  kind: NodeKind["EntityDecl"];
23
50
  namePlural: string;
@@ -29,6 +56,7 @@ export interface SerializedEntityDecl<Name extends string = string, T extends TS
29
56
  displayName?: GenericEntityDisplayName;
30
57
  displayNameCustomizer: boolean;
31
58
  isDeprecated?: boolean;
59
+ uniqueConstraints?: UniqueConstraints;
32
60
  }
33
61
  export declare const isSerializedEntityDecl: (node: SerializedNode) => node is SerializedEntityDecl;
34
62
  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>>;
@@ -1,5 +1,7 @@
1
1
  import { NodeKind, resolveSerializedTypeArguments, } from "../Node.js";
2
2
  import { getReferencesForSerializedObjectType, } from "../types/ObjectType.js";
3
+ export const normalizeKeyPath = (keyPath) => Array.isArray(keyPath) ? keyPath : [keyPath];
4
+ export const renderKeyPath = (keyPath) => normalizeKeyPath(keyPath).join(".");
3
5
  export const isSerializedEntityDecl = (node) => node.kind === NodeKind.EntityDecl;
4
6
  export const isSerializedEntityDeclWithParentReference = (decl) => decl.parentReferenceKey !== undefined;
5
7
  export const isSerializedEntityDeclWithoutParentReference = (decl) => decl.parentReferenceKey === undefined;
@@ -18,6 +18,24 @@ export interface ArrayDiffResult<T> {
18
18
  removed: T[];
19
19
  }
20
20
  export declare const unique: <T>(arr: T[], equalityCheck?: (a: T, b: T) => boolean) => T[];
21
+ /**
22
+ * Checks if there are any duplicate elements in the array.
23
+ */
24
+ export declare const anySame: <T>(arr: T[], equalityCheck?: (a: T, b: T) => boolean) => boolean;
25
+ /**
26
+ * Checks if there are any duplicate elements in the array and returns an array
27
+ * of found duplicates where the values are the indices of these values.
28
+ */
29
+ export declare const anySameIndices: <T>(arr: T[], equalityCheck?: (a: T, b: T) => boolean) => number[][];
30
+ /**
31
+ * Returns the possibilities of all the combinations of nested array values.
32
+ *
33
+ * @example
34
+ *
35
+ * flatCombine([["a", "b"], ["c"]]) // [["a", "c"], ["b", "c"]]
36
+ * flatCombine([["a", "b"], ["c", "d"]]) // [["a", "c"], ["b", "c"], ["a", "d"], ["b", "d"]]
37
+ */
38
+ export declare const flatCombine: <T>(arr: T[][]) => T[][];
21
39
  /**
22
40
  * Moves an element from one position to another within the array.
23
41
  */
@@ -29,6 +29,38 @@ export const difference = (oldArr, newArr) => newArr.reduce((acc, item) => {
29
29
  return acc;
30
30
  }, { removed: oldArr, added: newArr });
31
31
  export const unique = (arr, equalityCheck = (a, b) => a === b) => arr.filter((item, index) => arr.findIndex(other => equalityCheck(item, other)) === index);
32
+ /**
33
+ * Checks if there are any duplicate elements in the array.
34
+ */
35
+ export const anySame = (arr, equalityCheck = (a, b) => a === b) => arr.some((item, index) => arr.findIndex(other => equalityCheck(item, other)) !== index);
36
+ /**
37
+ * Checks if there are any duplicate elements in the array and returns an array
38
+ * of found duplicates where the values are the indices of these values.
39
+ */
40
+ export const anySameIndices = (arr, equalityCheck = (a, b) => a === b) => arr.reduce((acc, item, index) => {
41
+ const firstIndex = arr.findIndex(other => equalityCheck(item, other));
42
+ if (firstIndex === index) {
43
+ return acc;
44
+ }
45
+ const accIndex = acc.findIndex(accElem => accElem[0] === firstIndex);
46
+ return accIndex === -1
47
+ ? [...acc, [firstIndex, index]]
48
+ : // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- The index must exist according to the findIndex above
49
+ acc.with(accIndex, [...acc[accIndex], index]);
50
+ }, []);
51
+ /**
52
+ * Returns the possibilities of all the combinations of nested array values.
53
+ *
54
+ * @example
55
+ *
56
+ * flatCombine([["a", "b"], ["c"]]) // [["a", "c"], ["b", "c"]]
57
+ * flatCombine([["a", "b"], ["c", "d"]]) // [["a", "c"], ["b", "c"], ["a", "d"], ["b", "d"]]
58
+ */
59
+ export const flatCombine = (arr) => arr.length === 0
60
+ ? []
61
+ : arr.slice(1).reduce((acc, elem) => elem.flatMap(elemInner => acc.map(accElem => [...accElem, elemInner])),
62
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- it is checked before if the array is empty
63
+ arr[0].map(elem => [elem]));
32
64
  /**
33
65
  * Moves an element from one position to another within the array.
34
66
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tsondb",
3
- "version": "0.12.9",
3
+ "version": "0.13.0",
4
4
  "description": "",
5
5
  "license": "ISC",
6
6
  "author": "Lukas Obermann",