tsondb 0.21.10 → 0.22.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.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
|
+
metaFile.version = await getTSONDBVersion();
|
|
171
|
+
if (metaFile.schemaVersion !== options.schema.version && options.migrationStages) {
|
|
172
|
+
debug("migrating database from schema version %s to %s", metaFile.schemaVersion, options.schema.version);
|
|
173
|
+
for (const stage of options.migrationStages) {
|
|
174
|
+
if (semver.valid(stage.toVersion) === null ||
|
|
175
|
+
semver.validRange(stage.fromVersion) === null) {
|
|
176
|
+
throw new Error(`Invalid semver version in migration stage from ${stage.fromVersion} to ${stage.toVersion}`);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
for (const stage of options.migrationStages) {
|
|
180
|
+
if (semver.satisfies(metaFile.schemaVersion, stage.fromVersion)) {
|
|
181
|
+
debug("applying migration stage from %s to %s", stage.fromVersion, stage.toVersion);
|
|
182
|
+
// oxlint-disable-next-line no-await-in-loop
|
|
183
|
+
await stage.willMigrate?.(db);
|
|
184
|
+
metaFile.schemaVersion = stage.toVersion;
|
|
185
|
+
// oxlint-disable-next-line no-await-in-loop
|
|
186
|
+
await writeMetaFile(options.dataRootPath, metaFile);
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
debug("skipping migration stage from %s to %s", stage.fromVersion, stage.toVersion);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
else {
|
|
194
|
+
debug("no migration needed, schema version is up to date");
|
|
195
|
+
}
|
|
196
|
+
return db;
|
|
148
197
|
}
|
|
149
198
|
/**
|
|
150
199
|
* Generates outputs based on the provided schema and output configurations.
|
|
@@ -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.
|
|
3
|
+
"version": "0.22.0",
|
|
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
|
},
|