@itrocks/mysql-maintainer 0.0.17 → 0.1.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/README.md CHANGED
@@ -7,3 +7,268 @@
7
7
  # mysql-maintainer
8
8
 
9
9
  Reactively maintains database structure by updating schema and retrying on MySQL errors.
10
+
11
+ *This documentation was written by an artificial intelligence and may contain errors or approximations.
12
+ It has not yet been fully reviewed by a human. If anything seems unclear or incomplete,
13
+ please feel free to contact the author of this package.*
14
+
15
+ ## Installation
16
+
17
+ ```bash
18
+ npm i @itrocks/mysql-maintainer
19
+ ```
20
+
21
+ ## Usage
22
+
23
+ `@itrocks/mysql-maintainer` provides two main entry points:
24
+
25
+ - the `Mysql` class, a drop‑in replacement for `@itrocks/mysql` that
26
+ automatically keeps your MySQL/MariaDB schema in sync with your
27
+ TypeScript models while you work with data;
28
+ - the `MysqlMaintainer` class, a lower‑level helper you can call
29
+ directly when you want to create or update tables yourself.
30
+
31
+ In both cases, the package reacts to common MySQL errors
32
+ (`ER_BAD_FIELD_ERROR`, `ER_CANNOT_ADD_FOREIGN`, `ER_NO_SUCH_TABLE`) and
33
+ updates the database structure, then transparently retries the failed
34
+ query.
35
+
36
+ ### Minimal example: automatic schema maintenance
37
+
38
+ ```ts
39
+ import { Mysql } from '@itrocks/mysql-maintainer'
40
+ import { Entity } from '@itrocks/storage'
41
+
42
+ class User extends Entity<User>
43
+ {
44
+ name = ''
45
+ email = ''
46
+ }
47
+
48
+ async function main() {
49
+ const mysql = new Mysql({
50
+ // Same connection options as @itrocks/mysql / mariadb
51
+ host: 'localhost',
52
+ user: 'root',
53
+ database: 'demo',
54
+ })
55
+
56
+ // On first call, mysql-maintainer will create or update the
57
+ // corresponding table based on the User class definition, then retry
58
+ // the insert.
59
+ await mysql.insert(new User().assign({ name: 'Alice', email: 'a@example.com' }))
60
+ }
61
+ ```
62
+
63
+ You can continue using the `Mysql` instance as you would use
64
+ `@itrocks/mysql`: methods such as `read`, `search`, `update`, and
65
+ collection helpers are forwarded to the base implementation, while
66
+ `mysql-maintainer` keeps track of the involved entities and adjusts the
67
+ schema when needed.
68
+
69
+ ### Example: maintaining join tables on collection access
70
+
71
+ When you read many‑to‑many relationships defined as collections on your
72
+ entities, `Mysql` automatically creates or updates the implicit join
73
+ table.
74
+
75
+ ```ts
76
+ import { Mysql } from '@itrocks/mysql-maintainer'
77
+ import { Entity } from '@itrocks/storage'
78
+
79
+ class Tag extends Entity<Tag>
80
+ {
81
+ name = ''
82
+ }
83
+
84
+ class Article extends Entity<Article>
85
+ {
86
+ title = ''
87
+
88
+ // Many-to-many with Tag; the framework will map this to a join table
89
+ tags: Tag[] = []
90
+ }
91
+
92
+ async function loadArticleTags(id: number) {
93
+ const mysql = new Mysql({ database: 'demo' })
94
+
95
+ const article = await mysql.read(Article, id)
96
+
97
+ // If the join table does not exist yet or is out of date, it will be
98
+ // created/updated before the query is retried.
99
+ const tags = await mysql.readCollection(article, 'tags')
100
+ return tags
101
+ }
102
+ ```
103
+
104
+ ### Example: using MysqlMaintainer directly
105
+
106
+ If you want to control when schema updates happen (for instance during a
107
+ deployment step or a migration command), you can work with
108
+ `MysqlMaintainer` and a `mariadb` connection.
109
+
110
+ ```ts
111
+ import { createConnection } from 'mariadb'
112
+ import { MysqlMaintainer } from '@itrocks/mysql-maintainer'
113
+ import { Entity } from '@itrocks/storage'
114
+
115
+ class Product extends Entity<Product>
116
+ {
117
+ name = ''
118
+ price = 0
119
+ }
120
+
121
+ async function ensureSchema() {
122
+ const connection = await createConnection({ database: 'demo' })
123
+ const schemaManager = new MysqlMaintainer(connection)
124
+
125
+ // Create or update the table corresponding to Product
126
+ await schemaManager.updateTable(Product)
127
+
128
+ await connection.end()
129
+ }
130
+ ```
131
+
132
+ ## API
133
+
134
+ ### `class Mysql extends @itrocks/mysql.Mysql`
135
+
136
+ Enhanced MySQL storage class that automatically tracks the context of
137
+ operations and lets `MysqlMaintainer` react to schema‑related errors.
138
+
139
+ Aside from the behavior described below, it exposes the same public API
140
+ as `@itrocks/mysql.Mysql`.
141
+
142
+ #### Properties
143
+
144
+ - `connection?: ContextualConnection` – the underlying `mariadb`
145
+ connection wrapped with additional context information. You normally
146
+ do not interact with it directly.
147
+
148
+ #### Methods
149
+
150
+ All methods have the same signature and return types as in
151
+ `@itrocks/mysql.Mysql`; the extra behavior is that each call pushes the
152
+ relevant entity type or instance to an internal context stack before the
153
+ query, and pops it afterwards.
154
+
155
+ - `async connect(): Promise<ContextualConnection>`
156
+
157
+ Opens the connection and installs the contextual behavior so that
158
+ errors can trigger schema maintenance.
159
+
160
+ - `async insert<T extends object>(object: T): Promise<Entity<T>>`
161
+
162
+ Inserts a new row for `object`. If a “table does not exist” or
163
+ “column not found / foreign key failed” error occurs, the
164
+ corresponding table (and, for collections, the join table) is created
165
+ or updated and the query is retried.
166
+
167
+ - `async update<T extends object>(object: Entity<T>): Promise<Entity<T>>`
168
+
169
+ Updates an existing row; schema maintenance/automatic retry applies in
170
+ the same way as for `insert`.
171
+
172
+ - `async deleteId<T extends object>(type: ObjectOrType<T>, id: any, property?: KeyOf<Entity<T>>): Promise<void>`
173
+
174
+ Deletes a row by its identifier; the entity `type` is used as context
175
+ for potential schema updates.
176
+
177
+ - `async deleteRelatedId<T extends Entity>(object: T, property: KeyOf<T>, id: Identifier): Promise<void>`
178
+
179
+ Deletes a related entity (for example from a collection association).
180
+
181
+ - `async read<T extends object>(type: Type<T>, id: Identifier): Promise<Entity<T> | undefined>`
182
+
183
+ Reads a single entity instance by identifier.
184
+
185
+ - `async readMultiple<T extends object>(type: Type<T>, ids: Identifier[]): Promise<Entity<T>[]>`
186
+
187
+ Reads several entities by their identifiers.
188
+
189
+ - `async readCollection<T extends object, PT extends object>(object: Entity<T>, property: KeyOf<T>, type?: Type<PT>): Promise<Entity<PT>[]>`
190
+
191
+ Reads a collection relationship (for example `article.tags`). The
192
+ context includes both the owning object and the related type so that
193
+ the implicit join table can be created or updated if necessary.
194
+
195
+ - `async readCollectionIds<T extends object, PT extends object>(object: Entity<T>, property: KeyOf<T>, type?: Type<PT>): Promise<Identifier[]>`
196
+
197
+ Same as `readCollection`, but only retrieves identifiers.
198
+
199
+ - `async search<T extends object>(type: Type<T>, search?: SearchType<T>, options?: Options): Promise<Entity<T>[]>`
200
+
201
+ Searches entities; the entity `type` becomes the context for schema
202
+ maintenance.
203
+
204
+ ### `class MysqlMaintainer`
205
+
206
+ Low‑level utility that knows how to translate your TypeScript entity
207
+ definitions (via reflection and schema packages) into MySQL tables and
208
+ how to react to common schema‑related errors.
209
+
210
+ #### Constructor
211
+
212
+ ```ts
213
+ const maintainer = new MysqlMaintainer(connection)
214
+ ```
215
+
216
+ - `connection` – a `mariadb` `Connection` used to inspect and modify the
217
+ database schema.
218
+
219
+ #### Methods
220
+
221
+ - `async createImplicitTable(type1: ObjectOrType, type2: ObjectOrType): Promise<boolean>`
222
+
223
+ Creates a join table between the two entity types, with a composite
224
+ primary key and cascading foreign keys. Returns `true` once the table
225
+ has been created.
226
+
227
+ - `async createTable(type: Type): Promise<boolean>`
228
+
229
+ Creates a table corresponding to the given entity `type`. Throws if no
230
+ table name can be resolved for the type.
231
+
232
+ - `implicitTableName(type1: ObjectOrType, type2: ObjectOrType): string`
233
+
234
+ Returns the name of the join table that would be used for a
235
+ many‑to‑many association between `type1` and `type2`. Throws if one of
236
+ the types is not mapped to a table.
237
+
238
+ - `async manageError(error: SqlError, context: Context, sql: string | QueryOptions, values: any[]): Promise<boolean>`
239
+
240
+ Examines the MySQL error and, when it corresponds to a managed error
241
+ code, updates the tables for the provided `context` (entity type,
242
+ instance, or pair `[owner, relatedType]`). Returns `true` if the error
243
+ has been handled and the caller should retry the query, `false`
244
+ otherwise.
245
+
246
+ - `async updateContextTables(context: Context, tableName?: string): Promise<boolean>`
247
+
248
+ Internal helper that inspects the database to see whether the table
249
+ for each context entity already exists. If not, it calls
250
+ `createTable`; otherwise it calls `updateTable`. When two contexts are
251
+ provided and their implicit join table matches `tableName`, the join
252
+ table is also created.
253
+
254
+ - `async updateTable(type: Type): Promise<boolean>`
255
+
256
+ Computes the difference between the current MySQL table and the table
257
+ that should exist according to the entity `type`, then applies the
258
+ required SQL changes. Returns `true` when a change has been applied,
259
+ `false` when the schema was already up to date.
260
+
261
+ ## Typical use cases
262
+
263
+ - Rapid prototyping: evolve your TypeScript entity models and let
264
+ `mysql-maintainer` adjust the database schema on the fly while you
265
+ insert, update, or query data.
266
+ - Developer workstations and test environments where automatic schema
267
+ updates remove the need for manual migrations.
268
+ - Applications with many small deployments where you prefer incremental
269
+ schema evolution handled by the application itself.
270
+ - Projects using the `@itrocks/storage` / `@itrocks/mysql` stack that
271
+ want schema maintenance without writing migration scripts.
272
+ - Custom migration tools or CLI scripts built on top of
273
+ `MysqlMaintainer` to validate and update schema before a new version
274
+ of the application is started.
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.Contextual = void 0;
4
4
  const mariadb_1 = require("mariadb");
5
5
  const mysql_maintainer_1 = require("./mysql-maintainer");
6
+ const mysql_maintainer_2 = require("./mysql-maintainer");
6
7
  const MANAGED_ERROR_CODES = ['ER_BAD_FIELD_ERROR', 'ER_CANNOT_ADD_FOREIGN', 'ER_NO_SUCH_TABLE'];
7
8
  class Contextual {
8
9
  contexts = [];
@@ -15,16 +16,20 @@ class Contextual {
15
16
  }
16
17
  async query(sql, values) {
17
18
  try {
19
+ if (mysql_maintainer_1.DEBUG)
20
+ console.log(sql, values);
18
21
  return await this.superQuery(sql, values);
19
22
  }
20
23
  catch (error) {
21
24
  if (!(error instanceof mariadb_1.SqlError)
22
25
  || !error.code
23
26
  || !MANAGED_ERROR_CODES.includes(error.code)) {
27
+ if (mysql_maintainer_1.DEBUG)
28
+ console.log('MAINTAINER: throw', error);
24
29
  throw error;
25
30
  }
26
31
  // @ts-ignore query applies to a Connection
27
- if (await new mysql_maintainer_1.MysqlMaintainer(this).manageError(error, this.contexts[this.contexts.length - 1], sql, values)) {
32
+ if (await new mysql_maintainer_2.MysqlMaintainer(this).manageError(error, this.contexts[this.contexts.length - 1], sql, values)) {
28
33
  return this.query(sql, values);
29
34
  }
30
35
  throw error;
@@ -5,6 +5,7 @@ import { QueryOptions } from 'mariadb';
5
5
  import { SqlError } from 'mariadb';
6
6
  import { Context } from './contextual-connection';
7
7
  export { Mysql } from './mysql';
8
+ export declare const DEBUG = false;
8
9
  export declare class MysqlMaintainer {
9
10
  connection: Connection;
10
11
  constructor(connection: Connection);
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.MysqlMaintainer = exports.Mysql = void 0;
3
+ exports.MysqlMaintainer = exports.DEBUG = exports.Mysql = void 0;
4
4
  const class_type_1 = require("@itrocks/class-type");
5
5
  const mysql_1 = require("@itrocks/mysql");
6
6
  const mysql_to_schema_1 = require("@itrocks/mysql-to-schema");
@@ -11,6 +11,7 @@ const schema_to_mysql_1 = require("@itrocks/schema-to-mysql");
11
11
  const store_1 = require("@itrocks/store");
12
12
  var mysql_2 = require("./mysql");
13
13
  Object.defineProperty(exports, "Mysql", { enumerable: true, get: function () { return mysql_2.Mysql; } });
14
+ exports.DEBUG = false;
14
15
  const DELETION = false;
15
16
  class MysqlMaintainer {
16
17
  connection;
@@ -51,6 +52,8 @@ CONSTRAINT \`${joinTable}.${table2}_id\` FOREIGN KEY (${table2}_id) REFERENCES \
51
52
  return (0, mysql_1.joinTableName)(table1, table2);
52
53
  }
53
54
  async manageError(error, context, sql, values) {
55
+ if (exports.DEBUG)
56
+ console.log('MAINTAINER: manageError', error);
54
57
  switch (error.code) {
55
58
  case 'ER_BAD_FIELD_ERROR':
56
59
  case 'ER_CANNOT_ADD_FOREIGN':
@@ -1,4 +1,5 @@
1
1
  import { SqlError } from 'mariadb';
2
+ import { DEBUG } from './mysql-maintainer';
2
3
  import { MysqlMaintainer } from './mysql-maintainer';
3
4
  const MANAGED_ERROR_CODES = ['ER_BAD_FIELD_ERROR', 'ER_CANNOT_ADD_FOREIGN', 'ER_NO_SUCH_TABLE'];
4
5
  export class Contextual {
@@ -12,12 +13,16 @@ export class Contextual {
12
13
  }
13
14
  async query(sql, values) {
14
15
  try {
16
+ if (DEBUG)
17
+ console.log(sql, values);
15
18
  return await this.superQuery(sql, values);
16
19
  }
17
20
  catch (error) {
18
21
  if (!(error instanceof SqlError)
19
22
  || !error.code
20
23
  || !MANAGED_ERROR_CODES.includes(error.code)) {
24
+ if (DEBUG)
25
+ console.log('MAINTAINER: throw', error);
21
26
  throw error;
22
27
  }
23
28
  // @ts-ignore query applies to a Connection
@@ -5,6 +5,7 @@ import { QueryOptions } from 'mariadb';
5
5
  import { SqlError } from 'mariadb';
6
6
  import { Context } from './contextual-connection';
7
7
  export { Mysql } from './mysql';
8
+ export declare const DEBUG = false;
8
9
  export declare class MysqlMaintainer {
9
10
  connection: Connection;
10
11
  constructor(connection: Connection);
@@ -7,6 +7,7 @@ import { SchemaDiffMysql } from '@itrocks/schema-diff-mysql';
7
7
  import { SchemaToMysql } from '@itrocks/schema-to-mysql';
8
8
  import { storeOf } from '@itrocks/store';
9
9
  export { Mysql } from './mysql';
10
+ export const DEBUG = false;
10
11
  const DELETION = false;
11
12
  export class MysqlMaintainer {
12
13
  connection;
@@ -47,6 +48,8 @@ CONSTRAINT \`${joinTable}.${table2}_id\` FOREIGN KEY (${table2}_id) REFERENCES \
47
48
  return joinTableName(table1, table2);
48
49
  }
49
50
  async manageError(error, context, sql, values) {
51
+ if (DEBUG)
52
+ console.log('MAINTAINER: manageError', error);
50
53
  switch (error.code) {
51
54
  case 'ER_BAD_FIELD_ERROR':
52
55
  case 'ER_CANNOT_ADD_FOREIGN':
package/package.json CHANGED
@@ -37,10 +37,13 @@
37
37
  "automatic",
38
38
  "automation",
39
39
  "backend",
40
+ "database",
40
41
  "it.rocks",
41
- "maintain",
42
- "maintainer",
43
- "mysql"
42
+ "mariadb",
43
+ "migration",
44
+ "mysql",
45
+ "schema",
46
+ "synchronization"
44
47
  ],
45
48
  "license": "LGPL-3.0-or-later",
46
49
  "name": "@itrocks/mysql-maintainer",
@@ -54,5 +57,5 @@
54
57
  "build:esm": "tsc -p tsconfig.esm.json"
55
58
  },
56
59
  "types": "./esm/mysql-maintainer.d.ts",
57
- "version": "0.0.17"
60
+ "version": "0.1.0"
58
61
  }