tsondb 0.21.8 → 0.21.11
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.d.ts
CHANGED
|
@@ -50,6 +50,10 @@ export interface TSONDBOptions<T extends DefaultTSONDBTypes = DefaultTSONDBTypes
|
|
|
50
50
|
* Options for formatting the data in the database.
|
|
51
51
|
*/
|
|
52
52
|
formatterOptions?: Partial<FormatterOptions>;
|
|
53
|
+
/**
|
|
54
|
+
* If set, the database will be migrated through the specified migration stages before being used.
|
|
55
|
+
*/
|
|
56
|
+
migrationStages?: MigrationStage<T>[];
|
|
53
57
|
}
|
|
54
58
|
/**
|
|
55
59
|
* Options for validating the data in the database.
|
|
@@ -103,6 +107,11 @@ export interface TranslationValidationOptions {
|
|
|
103
107
|
*/
|
|
104
108
|
matchParametersInKeys?: boolean;
|
|
105
109
|
}
|
|
110
|
+
export interface MigrationStage<T extends DefaultTSONDBTypes = DefaultTSONDBTypes> {
|
|
111
|
+
fromVersion: string;
|
|
112
|
+
toVersion: string;
|
|
113
|
+
willMigrate?(context: TSONDB<T>): Promise<void>;
|
|
114
|
+
}
|
|
106
115
|
export type HookTrigger = "transaction:before" | "transaction:after";
|
|
107
116
|
/**
|
|
108
117
|
* The main class for managing a typed JSON database.
|
package/dist/src/node/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { exists } from "@elyukai/utils/async/node";
|
|
1
2
|
import { on } from "@elyukai/utils/function";
|
|
2
3
|
import { Lazy } from "@elyukai/utils/lazy";
|
|
3
4
|
import { nullableToArray } from "@elyukai/utils/nullable";
|
|
@@ -5,10 +6,12 @@ import { compareNumber, reduceCompare } from "@elyukai/utils/ordering";
|
|
|
5
6
|
import { isError } from "@elyukai/utils/result";
|
|
6
7
|
import { assertExhaustive } from "@elyukai/utils/typeSafety";
|
|
7
8
|
import Debug from "debug";
|
|
8
|
-
import { mkdir, writeFile } from "node:fs/promises";
|
|
9
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
10
|
+
import { findPackageJSON } from "node:module";
|
|
9
11
|
import { join, sep } from "node:path";
|
|
10
12
|
import { stderr } from "node:process";
|
|
11
13
|
import { styleText } from "node:util";
|
|
14
|
+
import semver from "semver";
|
|
12
15
|
import { simpleGit } from "simple-git";
|
|
13
16
|
import { NodeKind } from "../shared/schema/Node.js";
|
|
14
17
|
import { countError, countErrors, getErrorMessageForDisplay, HTTPError, wrapErrorsIfAny, } from "../shared/utils/error.js";
|
|
@@ -28,9 +31,27 @@ import { attachGitStatusToDatabaseInMemory } from "./utils/git.js";
|
|
|
28
31
|
import { getReferencesToInstances, isReferencedByOtherInstances, } from "./utils/references.js";
|
|
29
32
|
import { checkUniqueConstraintsForAllEntities } from "./utils/unique.js";
|
|
30
33
|
const debug = Debug("tsondb:class");
|
|
31
|
-
const
|
|
34
|
+
const getMetaFilePath = (dataRootPath) => join(dataRootPath, ".meta.json");
|
|
35
|
+
const getTSONDBVersion = async () => {
|
|
36
|
+
const packageJsonPath = findPackageJSON(".", import.meta.url);
|
|
37
|
+
if (!packageJsonPath) {
|
|
38
|
+
throw new Error("Could not find package.json for TSONDB");
|
|
39
|
+
}
|
|
40
|
+
const packageJson = JSON.parse(await readFile(packageJsonPath, "utf-8"));
|
|
41
|
+
return packageJson.version;
|
|
42
|
+
};
|
|
43
|
+
const readMetaFile = async (dataRootPath) => JSON.parse(await readFile(getMetaFilePath(dataRootPath), "utf-8"));
|
|
44
|
+
const writeMetaFile = async (dataRootPath, meta) => writeFile(getMetaFilePath(dataRootPath), JSON.stringify(meta, null, 2), "utf-8");
|
|
45
|
+
const prepareFolders = async (schemaVersion, dataRootPath, entities) => {
|
|
32
46
|
await mkdir(dataRootPath, { recursive: true });
|
|
33
47
|
await Promise.all(entities.map(entity => mkdir(join(dataRootPath, entity.name), { recursive: true })));
|
|
48
|
+
if (!(await exists(getMetaFilePath(dataRootPath)))) {
|
|
49
|
+
const newMetaFile = {
|
|
50
|
+
version: await getTSONDBVersion(),
|
|
51
|
+
schemaVersion,
|
|
52
|
+
};
|
|
53
|
+
await writeMetaFile(dataRootPath, newMetaFile);
|
|
54
|
+
}
|
|
34
55
|
};
|
|
35
56
|
const generateOutputs = async (schema, outputs) => {
|
|
36
57
|
await Promise.all(outputs.map(output => output.run(schema)));
|
|
@@ -119,7 +140,7 @@ export class TSONDB {
|
|
|
119
140
|
static async create(options, skipReferenceCache = false) {
|
|
120
141
|
const { entities } = options.schema;
|
|
121
142
|
debug("using data root path: %s", options.dataRootPath);
|
|
122
|
-
await prepareFolders(options.dataRootPath, entities);
|
|
143
|
+
await prepareFolders(options.schema.version, options.dataRootPath, entities);
|
|
123
144
|
debug("prepared folders");
|
|
124
145
|
const { git: gitClient, root: gitRoot, status: gitStatus } = await getGit(options.dataRootPath);
|
|
125
146
|
if (gitRoot) {
|
|
@@ -134,7 +155,7 @@ export class TSONDB {
|
|
|
134
155
|
}
|
|
135
156
|
const git = gitRoot ? { client: gitClient, root: gitRoot } : undefined;
|
|
136
157
|
const { data, referencesToInstances } = await initData(options.dataRootPath, options.schema, options.locales ?? [], git, gitStatus, skipReferenceCache);
|
|
137
|
-
|
|
158
|
+
const db = new TSONDB({
|
|
138
159
|
...options,
|
|
139
160
|
data,
|
|
140
161
|
referencesToInstances,
|
|
@@ -145,6 +166,34 @@ export class TSONDB {
|
|
|
145
166
|
...options.validationOptions,
|
|
146
167
|
},
|
|
147
168
|
});
|
|
169
|
+
const metaFile = await readMetaFile(options.dataRootPath);
|
|
170
|
+
if (metaFile.schemaVersion !== options.schema.version && options.migrationStages) {
|
|
171
|
+
debug("migrating database from schema version %s to %s", metaFile.schemaVersion, options.schema.version);
|
|
172
|
+
for (const stage of options.migrationStages) {
|
|
173
|
+
if (semver.valid(stage.toVersion) === null ||
|
|
174
|
+
semver.validRange(stage.fromVersion) === null) {
|
|
175
|
+
throw new Error(`Invalid semver version in migration stage from ${stage.fromVersion} to ${stage.toVersion}`);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
for (const stage of options.migrationStages) {
|
|
179
|
+
if (semver.satisfies(metaFile.schemaVersion, stage.fromVersion)) {
|
|
180
|
+
debug("applying migration stage from %s to %s", stage.fromVersion, stage.toVersion);
|
|
181
|
+
// oxlint-disable-next-line no-await-in-loop
|
|
182
|
+
await stage.willMigrate?.(db);
|
|
183
|
+
metaFile.schemaVersion = stage.toVersion;
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
debug("skipping migration stage from %s to %s", stage.fromVersion, stage.toVersion);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
debug("no migration needed, schema version is up to date");
|
|
192
|
+
}
|
|
193
|
+
metaFile.version = await getTSONDBVersion();
|
|
194
|
+
metaFile.schemaVersion = options.schema.version;
|
|
195
|
+
await writeMetaFile(options.dataRootPath, metaFile);
|
|
196
|
+
return db;
|
|
148
197
|
}
|
|
149
198
|
/**
|
|
150
199
|
* Generates outputs based on the provided schema and output configurations.
|
|
@@ -186,6 +235,11 @@ export class TSONDB {
|
|
|
186
235
|
useStyling: true,
|
|
187
236
|
};
|
|
188
237
|
const errors = [];
|
|
238
|
+
const summary = [];
|
|
239
|
+
const debugAndAddToSummary = (message) => {
|
|
240
|
+
debug(message);
|
|
241
|
+
summary.push(message);
|
|
242
|
+
};
|
|
189
243
|
if (!skipStructuralValidation) {
|
|
190
244
|
debug("Checking structural integrity ...");
|
|
191
245
|
const structureErrors = onlyEntities
|
|
@@ -209,7 +263,7 @@ export class TSONDB {
|
|
|
209
263
|
.toSorted((a, b) => a.message.localeCompare(b.message));
|
|
210
264
|
if (structureErrors.length > 0) {
|
|
211
265
|
const errorCount = countErrors(structureErrors);
|
|
212
|
-
|
|
266
|
+
debugAndAddToSummary(`${errorCount.toString()} structural integrity violation${errorCount === 1 ? "" : "s"} found`);
|
|
213
267
|
errors.push(...structureErrors);
|
|
214
268
|
}
|
|
215
269
|
else {
|
|
@@ -243,7 +297,7 @@ export class TSONDB {
|
|
|
243
297
|
.toSorted((a, b) => a.message.localeCompare(b.message));
|
|
244
298
|
if (referenceErrors.length > 0) {
|
|
245
299
|
const errorCount = countErrors(referenceErrors);
|
|
246
|
-
|
|
300
|
+
debugAndAddToSummary(`${errorCount.toString()} referential integrity violation${errorCount === 1 ? "" : "s"} found`);
|
|
247
301
|
errors.push(...referenceErrors);
|
|
248
302
|
}
|
|
249
303
|
else {
|
|
@@ -259,7 +313,7 @@ export class TSONDB {
|
|
|
259
313
|
const uniqueConstraintResult = checkUniqueConstraintsForAllEntities(data, (changedEntities ?? onlyEntities).filter(e => isEntityDecl(e)), instanceOverviewsByEntityName);
|
|
260
314
|
if (isError(uniqueConstraintResult)) {
|
|
261
315
|
const errorCount = countError(uniqueConstraintResult.error);
|
|
262
|
-
|
|
316
|
+
debugAndAddToSummary(`${errorCount.toString()} unique constraint violation${errorCount === 1 ? "" : "s"} found`);
|
|
263
317
|
errors.push(uniqueConstraintResult.error);
|
|
264
318
|
}
|
|
265
319
|
else {
|
|
@@ -269,7 +323,7 @@ export class TSONDB {
|
|
|
269
323
|
const customConstraintResult = checkCustomConstraintsForAllEntities((...args) => getDisplayName(getEntity, this.#locales, data, ...args), (...args) => getDisplayNameWithId(getEntity, this.#locales, data, ...args), (...args) => getInstanceOverview(data.getInstanceContainerOfEntityById.bind(data), data.getAllChildInstanceContainersForParent.bind(data, getEntity), getEntity, this.#locales, args), getEntity, data, entities);
|
|
270
324
|
if (isError(customConstraintResult)) {
|
|
271
325
|
const errorCount = countError(customConstraintResult.error);
|
|
272
|
-
|
|
326
|
+
debugAndAddToSummary(`${errorCount.toString()} custom constraint violation${errorCount === 1 ? "" : "s"} found`);
|
|
273
327
|
errors.push(customConstraintResult.error);
|
|
274
328
|
}
|
|
275
329
|
else {
|
|
@@ -277,7 +331,7 @@ export class TSONDB {
|
|
|
277
331
|
}
|
|
278
332
|
}
|
|
279
333
|
else {
|
|
280
|
-
|
|
334
|
+
debugAndAddToSummary("Skipping further checks due to previous structural integrity errors");
|
|
281
335
|
}
|
|
282
336
|
if (logToConsole) {
|
|
283
337
|
console.log(`${data.totalSize.toString()} instance${data.totalSize === 1 ? "" : "s"} checked`);
|
|
@@ -286,7 +340,14 @@ export class TSONDB {
|
|
|
286
340
|
}
|
|
287
341
|
else {
|
|
288
342
|
const errorCount = countErrors(errors);
|
|
289
|
-
console.error(styleText("red", `${
|
|
343
|
+
console.error(styleText("red", `${errors.map(err => getErrorMessageForDisplay(err)).join("\n\n")}\n`, {
|
|
344
|
+
stream: stderr,
|
|
345
|
+
}));
|
|
346
|
+
console.error(styleText(["red", "bold"], "Summary", { stream: stderr }));
|
|
347
|
+
console.error(styleText("red", [
|
|
348
|
+
`${errorCount.toString()} validation error${errorCount === 1 ? "" : "s"} found`,
|
|
349
|
+
...summary,
|
|
350
|
+
].join("\n"), { stream: stderr }));
|
|
290
351
|
}
|
|
291
352
|
}
|
|
292
353
|
return errors;
|
|
@@ -35,6 +35,8 @@ export const reduceNodes = (reducer, nodes, options = {}) => {
|
|
|
35
35
|
else {
|
|
36
36
|
return { results: reducer(parentNodes, node, collectedResults), reducedDecls };
|
|
37
37
|
}
|
|
38
|
+
case NodeKind.NestedEntityMapType:
|
|
39
|
+
return reduceNodeTree([...parentNodes, node], node.type.value, reducer(parentNodes, node, collectedResults), reducedDecls);
|
|
38
40
|
case NodeKind.BooleanType:
|
|
39
41
|
case NodeKind.DateType:
|
|
40
42
|
case NodeKind.FloatType:
|
|
@@ -42,7 +44,6 @@ export const reduceNodes = (reducer, nodes, options = {}) => {
|
|
|
42
44
|
case NodeKind.StringType:
|
|
43
45
|
case NodeKind.TypeArgumentType:
|
|
44
46
|
case NodeKind.ReferenceIdentifierType:
|
|
45
|
-
case NodeKind.NestedEntityMapType:
|
|
46
47
|
case NodeKind.TypeParameter:
|
|
47
48
|
case NodeKind.TranslationObjectType:
|
|
48
49
|
return { results: reducer(parentNodes, node, collectedResults), reducedDecls };
|
|
@@ -4,7 +4,8 @@ import { type EntityDecl } from "./dsl/declarations/EntityDecl.ts";
|
|
|
4
4
|
import type { SingletonEntityDecl } from "./dsl/index.ts";
|
|
5
5
|
export declare class Schema<T extends DefaultTSONDBTypes = DefaultTSONDBTypes> {
|
|
6
6
|
#private;
|
|
7
|
-
constructor(declarations: Decl[], localeEntity?: EntityDecl);
|
|
7
|
+
constructor(version: string, declarations: Decl[], localeEntity?: EntityDecl);
|
|
8
|
+
get version(): string;
|
|
8
9
|
get entities(): EntityDecl<EntityName<T>>[];
|
|
9
10
|
get resolvedEntities(): EntityDecl<EntityName<T>>[];
|
|
10
11
|
getEntity<E extends EntityName<T>>(name: E): EntityDecl<E> | undefined;
|
|
@@ -3,6 +3,7 @@ import { Dictionary } from "@elyukai/utils/dictionary";
|
|
|
3
3
|
import { deepEqual } from "@elyukai/utils/equality";
|
|
4
4
|
import { assertExhaustive, trySafe } from "@elyukai/utils/typeSafety";
|
|
5
5
|
import Debug from "debug";
|
|
6
|
+
import semver from "semver";
|
|
6
7
|
import { NodeKind } from "../../shared/schema/Node.js";
|
|
7
8
|
import { renderKeyPath } from "../../shared/schema/utils/keyPath.js";
|
|
8
9
|
import { getParameterNames, resolveTypeArgumentsInDecls, walkNodeTree, } from "./dsl/declarations/Decl.js";
|
|
@@ -22,6 +23,7 @@ import { getNestedDeclarations } from "./treeOperations/implicitSchema.js";
|
|
|
22
23
|
const debug = Debug("tsondb:schema");
|
|
23
24
|
// const RESERVED_DECLARATION_IDENTIFIER = ["EntityMap", "StringableTranslationParameter"]
|
|
24
25
|
export class Schema {
|
|
26
|
+
#version;
|
|
25
27
|
#declarations;
|
|
26
28
|
#entities;
|
|
27
29
|
#singletonEntities;
|
|
@@ -31,7 +33,10 @@ export class Schema {
|
|
|
31
33
|
#localeEntity;
|
|
32
34
|
#declarationMap;
|
|
33
35
|
#resolvedDeclarationMap;
|
|
34
|
-
constructor(declarations, localeEntity) {
|
|
36
|
+
constructor(version, declarations, localeEntity) {
|
|
37
|
+
if (semver.valid(version) === null) {
|
|
38
|
+
throw new TypeError(`Invalid schema version "${version}". Must be a valid semver string.`);
|
|
39
|
+
}
|
|
35
40
|
debug("creating schema from %d declarations", declarations.length);
|
|
36
41
|
debug("collecting nested declarations ...");
|
|
37
42
|
const allDecls = addDeclarations([], localeEntity ? declarations.concat(localeEntity) : declarations);
|
|
@@ -56,6 +61,7 @@ export class Schema {
|
|
|
56
61
|
checkUniqueConstraints(allDeclsWithoutNestedEntities);
|
|
57
62
|
debug("checking sort orders ...");
|
|
58
63
|
checkSortOrders(allDeclsWithoutNestedEntities);
|
|
64
|
+
this.#version = version;
|
|
59
65
|
this.#localeEntity = localeEntity;
|
|
60
66
|
this.#declarations = allDeclsWithoutNestedEntities;
|
|
61
67
|
this.#resolvedDeclarations = resolveTypeArgumentsInDecls(allDeclsWithoutNestedEntities);
|
|
@@ -67,6 +73,9 @@ export class Schema {
|
|
|
67
73
|
this.#resolvedDeclarationMap = Dictionary.fromEntries(this.#resolvedDeclarations.map(decl => [decl.name, decl]));
|
|
68
74
|
debug("created schema, no integrity violations found");
|
|
69
75
|
}
|
|
76
|
+
get version() {
|
|
77
|
+
return this.#version;
|
|
78
|
+
}
|
|
70
79
|
get entities() {
|
|
71
80
|
return this.#entities;
|
|
72
81
|
}
|
package/dist/src/web/web.js
CHANGED
|
@@ -2468,10 +2468,10 @@ const useMappedAPIResource = (apiFn, mapFn, ...args)=>{
|
|
|
2468
2468
|
setNewData
|
|
2469
2469
|
];
|
|
2470
2470
|
};
|
|
2471
|
-
const letterOrDigit = /[a-zA-Z0-9]
|
|
2472
|
-
const uppercase = /[A-Z]
|
|
2473
|
-
const lowerCaseOrDigit = /[a-z0-9]
|
|
2474
|
-
const string_separator = /[^a-z0-9]
|
|
2471
|
+
const letterOrDigit = /[a-zA-Z0-9]/u;
|
|
2472
|
+
const uppercase = /[A-Z]/u;
|
|
2473
|
+
const lowerCaseOrDigit = /[a-z0-9]/u;
|
|
2474
|
+
const string_separator = /[^a-z0-9]/u;
|
|
2475
2475
|
const lastChar = (str)=>str[str.length - 1];
|
|
2476
2476
|
const lastElement = (arr)=>arr[arr.length - 1];
|
|
2477
2477
|
const isAllUppercase = (str)=>str === str.toUpperCase();
|
|
@@ -3035,8 +3035,8 @@ class Parser {
|
|
|
3035
3035
|
];
|
|
3036
3036
|
});
|
|
3037
3037
|
}
|
|
3038
|
-
static space = Parser.regex(/^\s*/);
|
|
3039
|
-
static hspace = Parser.regex(/^[^\S\r\n]*/);
|
|
3038
|
+
static space = Parser.regex(/^\s*/u);
|
|
3039
|
+
static hspace = Parser.regex(/^[^\S\r\n]*/u);
|
|
3040
3040
|
token() {
|
|
3041
3041
|
return this.then((result)=>Parser.space.then(()=>Parser.of(result)));
|
|
3042
3042
|
}
|
|
@@ -3054,7 +3054,7 @@ class Parser {
|
|
|
3054
3054
|
}
|
|
3055
3055
|
static lookahead(parser) {
|
|
3056
3056
|
return new Parser((syntax)=>{
|
|
3057
|
-
const result = parser.parse(syntax)
|
|
3057
|
+
const [result] = parser.parse(syntax);
|
|
3058
3058
|
return void 0 !== result ? [
|
|
3059
3059
|
[
|
|
3060
3060
|
result[0],
|
|
@@ -3065,7 +3065,7 @@ class Parser {
|
|
|
3065
3065
|
}
|
|
3066
3066
|
static negativeLookahead(parser) {
|
|
3067
3067
|
return new Parser((syntax)=>{
|
|
3068
|
-
const result = parser.parse(syntax)
|
|
3068
|
+
const [result] = parser.parse(syntax);
|
|
3069
3069
|
return void 0 === result ? [
|
|
3070
3070
|
[
|
|
3071
3071
|
void 0,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tsondb",
|
|
3
|
-
"version": "0.21.
|
|
3
|
+
"version": "0.21.11",
|
|
4
4
|
"description": "",
|
|
5
5
|
"license": "ISC",
|
|
6
6
|
"author": "Lukas Obermann",
|
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
"@types/debug": "^4.1.13",
|
|
41
41
|
"@types/express": "^5.0.6",
|
|
42
42
|
"@types/node": "^26.6.4",
|
|
43
|
+
"@types/semver": "^7.8.0",
|
|
43
44
|
"@typescript/native": "npm:typescript@^7.0.2",
|
|
44
45
|
"commit-and-tag-version": "^13.2.1",
|
|
45
46
|
"tsx": "^4.23.15",
|
|
@@ -48,7 +49,7 @@
|
|
|
48
49
|
"dependencies": {
|
|
49
50
|
"@elyukai/markdown": "^0.4.0",
|
|
50
51
|
"@elyukai/oxc-config": "^0.1.2",
|
|
51
|
-
"@elyukai/utils": "^0.3.
|
|
52
|
+
"@elyukai/utils": "^0.3.11",
|
|
52
53
|
"@preact/signals": "^2.11.3",
|
|
53
54
|
"debug": "^4.4.3",
|
|
54
55
|
"express": "^5.2.1",
|
|
@@ -58,6 +59,7 @@
|
|
|
58
59
|
"oxlint-tsgolint": "^7.0.2003",
|
|
59
60
|
"preact": "^11.0.0",
|
|
60
61
|
"preact-iso": "^2.12.2",
|
|
62
|
+
"semver": "^7.8.5",
|
|
61
63
|
"simple-cli-args": "^0.1.3",
|
|
62
64
|
"simple-git": "^4.0.2"
|
|
63
65
|
},
|