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.
@@ -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.
@@ -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 prepareFolders = async (dataRootPath, entities) => {
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
- return new TSONDB({
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
  }
@@ -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)[0];
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)[0];
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.10",
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.8",
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
  },