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.
- package/dist/src/node/index.js +23 -0
- package/dist/src/node/renderers/ts/render.d.ts +10 -1
- package/dist/src/node/renderers/ts/render.js +56 -12
- package/dist/src/node/schema/Schema.js +65 -0
- package/dist/src/node/schema/declarations/EntityDecl.d.ts +4 -0
- package/dist/src/node/utils/databaseTransactions.js +5 -0
- package/dist/src/node/utils/unique.d.ts +19 -0
- package/dist/src/node/utils/unique.js +94 -0
- package/dist/src/shared/schema/declarations/EntityDecl.d.ts +28 -0
- package/dist/src/shared/schema/declarations/EntityDecl.js +2 -0
- package/dist/src/shared/utils/array.d.ts +18 -0
- package/dist/src/shared/utils/array.js +32 -0
- package/package.json +1 -1
package/dist/src/node/index.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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) =>
|
|
162
|
-
.
|
|
163
|
-
|
|
164
|
-
|
|
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 = {
|
|
177
|
+
? "export type StringableTranslationParameter = {" +
|
|
178
|
+
EOL +
|
|
167
179
|
prefixLines(getIndentation(options.indentation, 1), "toString(): string") +
|
|
168
|
-
|
|
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 (
|
|
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
|
*/
|