@zero-server/orm 0.9.1 → 0.9.3
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/LICENSE +21 -21
- package/index.d.ts +1 -1
- package/index.js +35 -35
- package/lib/debug.js +372 -0
- package/lib/orm/adapters/json.js +290 -0
- package/lib/orm/adapters/memory.js +764 -0
- package/lib/orm/adapters/mongo.js +764 -0
- package/lib/orm/adapters/mysql.js +933 -0
- package/lib/orm/adapters/postgres.js +1144 -0
- package/lib/orm/adapters/redis.js +1534 -0
- package/lib/orm/adapters/sql-base.js +212 -0
- package/lib/orm/adapters/sqlite.js +858 -0
- package/lib/orm/audit.js +649 -0
- package/lib/orm/cache.js +394 -0
- package/lib/orm/geo.js +387 -0
- package/lib/orm/index.js +784 -0
- package/lib/orm/migrate.js +432 -0
- package/lib/orm/model.js +1706 -0
- package/lib/orm/plugin.js +375 -0
- package/lib/orm/procedures.js +836 -0
- package/lib/orm/profiler.js +233 -0
- package/lib/orm/query.js +1772 -0
- package/lib/orm/replicas.js +241 -0
- package/lib/orm/schema.js +307 -0
- package/lib/orm/search.js +380 -0
- package/lib/orm/seed/data/commerce.js +136 -0
- package/lib/orm/seed/data/internet.js +111 -0
- package/lib/orm/seed/data/locations.js +204 -0
- package/lib/orm/seed/data/names.js +338 -0
- package/lib/orm/seed/data/person.js +128 -0
- package/lib/orm/seed/data/phone.js +211 -0
- package/lib/orm/seed/data/words.js +134 -0
- package/lib/orm/seed/factory.js +178 -0
- package/lib/orm/seed/fake.js +1186 -0
- package/lib/orm/seed/index.js +18 -0
- package/lib/orm/seed/rng.js +71 -0
- package/lib/orm/seed/seeder.js +125 -0
- package/lib/orm/seed/unique.js +68 -0
- package/lib/orm/snapshot.js +366 -0
- package/lib/orm/tenancy.js +605 -0
- package/lib/orm/views.js +350 -0
- package/package.json +12 -2
- package/types/app.d.ts +223 -0
- package/types/auth.d.ts +520 -0
- package/types/body.d.ts +14 -0
- package/types/cli.d.ts +2 -0
- package/types/cluster.d.ts +75 -0
- package/types/env.d.ts +80 -0
- package/types/errors.d.ts +316 -0
- package/types/fetch.d.ts +43 -0
- package/types/grpc.d.ts +432 -0
- package/types/index.d.ts +384 -0
- package/types/lifecycle.d.ts +60 -0
- package/types/middleware.d.ts +320 -0
- package/types/observe.d.ts +304 -0
- package/types/orm.d.ts +1887 -0
- package/types/request.d.ts +109 -0
- package/types/response.d.ts +157 -0
- package/types/router.d.ts +78 -0
- package/types/sse.d.ts +78 -0
- package/types/websocket.d.ts +126 -0
package/lib/orm/index.js
ADDED
|
@@ -0,0 +1,784 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module orm
|
|
3
|
+
* @description ORM entry point. Provides the `Database` factory that creates
|
|
4
|
+
* a connection to a backing store, the base `Model` class, the
|
|
5
|
+
* `TYPES` enum, and schema helpers.
|
|
6
|
+
*
|
|
7
|
+
* Supported adapters (all optional "bring your own driver"):
|
|
8
|
+
* - `memory` — in-process (no driver needed)
|
|
9
|
+
* - `json` — JSON file persistence (no driver needed)
|
|
10
|
+
* - `sqlite` — requires `better-sqlite3`
|
|
11
|
+
* - `mysql` — requires `mysql2`
|
|
12
|
+
* - `postgres` — requires `pg`
|
|
13
|
+
* - `mongo` — requires `mongodb`
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* const { Database, Model, TYPES } = require('@zero-server/sdk');
|
|
17
|
+
*
|
|
18
|
+
* const db = Database.connect('memory');
|
|
19
|
+
*
|
|
20
|
+
* class User extends Model {
|
|
21
|
+
* static table = 'users';
|
|
22
|
+
* static schema = {
|
|
23
|
+
* id: { type: TYPES.INTEGER, primaryKey: true, autoIncrement: true },
|
|
24
|
+
* name: { type: TYPES.STRING, required: true },
|
|
25
|
+
* email: { type: TYPES.STRING, required: true, unique: true },
|
|
26
|
+
* };
|
|
27
|
+
* static timestamps = true;
|
|
28
|
+
* }
|
|
29
|
+
*
|
|
30
|
+
* db.register(User);
|
|
31
|
+
* await db.sync();
|
|
32
|
+
*
|
|
33
|
+
* const user = await User.create({ name: 'Alice', email: 'a@b.com' });
|
|
34
|
+
*/
|
|
35
|
+
const Model = require('./model');
|
|
36
|
+
const { TYPES, validate, validateValue, validateFKAction, validateCheck } = require('./schema');
|
|
37
|
+
const Query = require('./query');
|
|
38
|
+
|
|
39
|
+
// -- Adapter loaders (lazy) ------------------------------
|
|
40
|
+
|
|
41
|
+
const ADAPTERS = {
|
|
42
|
+
memory: () => require('./adapters/memory'),
|
|
43
|
+
json: () => require('./adapters/json'),
|
|
44
|
+
sqlite: () => require('./adapters/sqlite'),
|
|
45
|
+
mysql: () => require('./adapters/mysql'),
|
|
46
|
+
postgres: () => require('./adapters/postgres'),
|
|
47
|
+
mongo: () => require('./adapters/mongo'),
|
|
48
|
+
redis: () => require('./adapters/redis'),
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
// -- Database class --------------------------------------
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Validate adapter connection options and sanitize credentials.
|
|
55
|
+
* @private
|
|
56
|
+
* @param {string} type - Adapter type identifier.
|
|
57
|
+
* @param {object} options - Configuration options.
|
|
58
|
+
* @returns {object} sanitised options
|
|
59
|
+
*/
|
|
60
|
+
function _validateOptions(type, options)
|
|
61
|
+
{
|
|
62
|
+
const opts = { ...options };
|
|
63
|
+
|
|
64
|
+
// Adapters that take network credentials
|
|
65
|
+
if (type === 'mysql' || type === 'postgres')
|
|
66
|
+
{
|
|
67
|
+
if (opts.host !== undefined)
|
|
68
|
+
{
|
|
69
|
+
if (typeof opts.host !== 'string' || !opts.host.trim())
|
|
70
|
+
throw new Error(`${type}: "host" must be a non-empty string`);
|
|
71
|
+
opts.host = opts.host.trim();
|
|
72
|
+
}
|
|
73
|
+
if (opts.port !== undefined)
|
|
74
|
+
{
|
|
75
|
+
opts.port = Number(opts.port);
|
|
76
|
+
if (!Number.isInteger(opts.port) || opts.port < 1 || opts.port > 65535)
|
|
77
|
+
throw new Error(`${type}: "port" must be an integer 1-65535`);
|
|
78
|
+
}
|
|
79
|
+
if (opts.user !== undefined)
|
|
80
|
+
{
|
|
81
|
+
if (typeof opts.user !== 'string')
|
|
82
|
+
throw new Error(`${type}: "user" must be a string`);
|
|
83
|
+
}
|
|
84
|
+
if (opts.password !== undefined)
|
|
85
|
+
{
|
|
86
|
+
if (typeof opts.password !== 'string')
|
|
87
|
+
throw new Error(`${type}: "password" must be a string`);
|
|
88
|
+
}
|
|
89
|
+
if (opts.database !== undefined)
|
|
90
|
+
{
|
|
91
|
+
if (typeof opts.database !== 'string' || !opts.database.trim())
|
|
92
|
+
throw new Error(`${type}: "database" must be a non-empty string`);
|
|
93
|
+
opts.database = opts.database.trim();
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Mongo connection string
|
|
98
|
+
if (type === 'mongo')
|
|
99
|
+
{
|
|
100
|
+
if (opts.url !== undefined)
|
|
101
|
+
{
|
|
102
|
+
if (typeof opts.url !== 'string' || !opts.url.trim())
|
|
103
|
+
throw new Error('mongo: "url" must be a non-empty string');
|
|
104
|
+
opts.url = opts.url.trim();
|
|
105
|
+
}
|
|
106
|
+
if (opts.database !== undefined)
|
|
107
|
+
{
|
|
108
|
+
if (typeof opts.database !== 'string' || !opts.database.trim())
|
|
109
|
+
throw new Error('mongo: "database" must be a non-empty string');
|
|
110
|
+
opts.database = opts.database.trim();
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// SQLite validation
|
|
115
|
+
if (type === 'sqlite')
|
|
116
|
+
{
|
|
117
|
+
if (opts.filename !== undefined && typeof opts.filename !== 'string')
|
|
118
|
+
throw new Error('sqlite: "filename" must be a string');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Redis validation
|
|
122
|
+
if (type === 'redis')
|
|
123
|
+
{
|
|
124
|
+
if (opts.url !== undefined)
|
|
125
|
+
{
|
|
126
|
+
if (typeof opts.url !== 'string' || !opts.url.trim())
|
|
127
|
+
throw new Error('redis: "url" must be a non-empty string');
|
|
128
|
+
opts.url = opts.url.trim();
|
|
129
|
+
}
|
|
130
|
+
if (opts.host !== undefined)
|
|
131
|
+
{
|
|
132
|
+
if (typeof opts.host !== 'string' || !opts.host.trim())
|
|
133
|
+
throw new Error('redis: "host" must be a non-empty string');
|
|
134
|
+
opts.host = opts.host.trim();
|
|
135
|
+
}
|
|
136
|
+
if (opts.port !== undefined)
|
|
137
|
+
{
|
|
138
|
+
opts.port = Number(opts.port);
|
|
139
|
+
if (!Number.isInteger(opts.port) || opts.port < 1 || opts.port > 65535)
|
|
140
|
+
throw new Error('redis: "port" must be an integer 1-65535');
|
|
141
|
+
}
|
|
142
|
+
if (opts.password !== undefined)
|
|
143
|
+
{
|
|
144
|
+
if (typeof opts.password !== 'string')
|
|
145
|
+
throw new Error('redis: "password" must be a string');
|
|
146
|
+
}
|
|
147
|
+
if (opts.db !== undefined)
|
|
148
|
+
{
|
|
149
|
+
opts.db = Number(opts.db);
|
|
150
|
+
if (!Number.isInteger(opts.db) || opts.db < 0)
|
|
151
|
+
throw new Error('redis: "db" must be a non-negative integer');
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return opts;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
class Database
|
|
159
|
+
{
|
|
160
|
+
/**
|
|
161
|
+
* @constructor
|
|
162
|
+
* @param {object} adapter - Instantiated adapter.
|
|
163
|
+
*/
|
|
164
|
+
constructor(adapter)
|
|
165
|
+
{
|
|
166
|
+
/** @type {object} The underlying adapter instance. */
|
|
167
|
+
this.adapter = adapter;
|
|
168
|
+
|
|
169
|
+
/** @type {Map<string, typeof Model>} Registered model classes. */
|
|
170
|
+
this._models = new Map();
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Create a Database connection.
|
|
175
|
+
*
|
|
176
|
+
* @param {string} type - Adapter type: memory, json, sqlite, mysql, postgres, mongo.
|
|
177
|
+
* @param {object} [options] - Adapter-specific connection options.
|
|
178
|
+
* @param {string} [options.host] - Database host (mysql, postgres).
|
|
179
|
+
* @param {number} [options.port] - Database port (mysql, postgres).
|
|
180
|
+
* @param {string} [options.user] - Database user (mysql, postgres).
|
|
181
|
+
* @param {string} [options.password] - Database password (mysql, postgres).
|
|
182
|
+
* @param {string} [options.database] - Database name (mysql, postgres, mongo).
|
|
183
|
+
* @param {string} [options.url] - Connection URL (mongo, redis).
|
|
184
|
+
* @param {string} [options.filename] - Database file path (sqlite).
|
|
185
|
+
* @param {string} [options.directory] - Storage directory (json).
|
|
186
|
+
* @returns {Database} Connected database instance.
|
|
187
|
+
*
|
|
188
|
+
* @example
|
|
189
|
+
* const db = Database.connect('sqlite', { filename: './app.db' });
|
|
190
|
+
* const db = Database.connect('mysql', { host: '127.0.0.1', user: 'root', database: 'app' });
|
|
191
|
+
* const db = Database.connect('postgres', { host: '127.0.0.1', user: 'postgres', database: 'app' });
|
|
192
|
+
* const db = Database.connect('mongo', { url: 'mongodb://localhost:27017', database: 'app' });
|
|
193
|
+
* const db = Database.connect('json', { directory: './data' });
|
|
194
|
+
* const db = Database.connect('memory');
|
|
195
|
+
*/
|
|
196
|
+
static connect(type, options = {})
|
|
197
|
+
{
|
|
198
|
+
const loader = ADAPTERS[type];
|
|
199
|
+
if (!loader) throw new Error(`Unknown adapter "${type}". Supported: ${Object.keys(ADAPTERS).join(', ')}`);
|
|
200
|
+
|
|
201
|
+
const opts = _validateOptions(type, options);
|
|
202
|
+
const AdapterClass = loader();
|
|
203
|
+
const adapter = new AdapterClass(opts);
|
|
204
|
+
return new Database(adapter);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Register a Model class with this database.
|
|
209
|
+
* Binds the adapter to the model so all CRUD operations go through it.
|
|
210
|
+
*
|
|
211
|
+
* @param {typeof Model} ModelClass - Model class.
|
|
212
|
+
* @returns {Database} this (for chaining)
|
|
213
|
+
*/
|
|
214
|
+
register(ModelClass)
|
|
215
|
+
{
|
|
216
|
+
ModelClass._adapter = this.adapter;
|
|
217
|
+
this._models.set(ModelClass.table || ModelClass.name, ModelClass);
|
|
218
|
+
return this;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Register multiple Model classes at once.
|
|
223
|
+
*
|
|
224
|
+
* @param {...typeof Model} models - Array of Model classes.
|
|
225
|
+
* @returns {Database} this (for chaining)
|
|
226
|
+
*/
|
|
227
|
+
registerAll(...models)
|
|
228
|
+
{
|
|
229
|
+
for (const m of models) this.register(m);
|
|
230
|
+
return this;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Synchronise all registered models — create tables if they don't exist.
|
|
235
|
+
* Tables are ordered so referenced tables are created first (topological sort).
|
|
236
|
+
* @returns {Promise<void>}
|
|
237
|
+
*/
|
|
238
|
+
async sync()
|
|
239
|
+
{
|
|
240
|
+
const models = [...this._models.values()];
|
|
241
|
+
|
|
242
|
+
// Build dependency graph from schema references
|
|
243
|
+
const tableMap = new Map();
|
|
244
|
+
for (const M of models) tableMap.set(M.table || M.name, M);
|
|
245
|
+
|
|
246
|
+
const ordered = this._topoSort(models);
|
|
247
|
+
for (const ModelClass of ordered)
|
|
248
|
+
{
|
|
249
|
+
await ModelClass.sync();
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Topological sort of models by FK dependencies.
|
|
255
|
+
* Models that reference other models come after them.
|
|
256
|
+
* @param {Array<typeof Model>} models - Array of Model classes.
|
|
257
|
+
* @returns {Array<typeof Model>} Models ordered by foreign key dependencies.
|
|
258
|
+
* @private
|
|
259
|
+
*/
|
|
260
|
+
_topoSort(models)
|
|
261
|
+
{
|
|
262
|
+
const nameToModel = new Map();
|
|
263
|
+
for (const M of models) nameToModel.set(M.table || M.name, M);
|
|
264
|
+
|
|
265
|
+
const deps = new Map();
|
|
266
|
+
for (const M of models)
|
|
267
|
+
{
|
|
268
|
+
const tableName = M.table || M.name;
|
|
269
|
+
const references = new Set();
|
|
270
|
+
if (M.schema)
|
|
271
|
+
{
|
|
272
|
+
for (const def of Object.values(M.schema))
|
|
273
|
+
{
|
|
274
|
+
if (def.references && def.references.table && nameToModel.has(def.references.table))
|
|
275
|
+
{
|
|
276
|
+
references.add(def.references.table);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
deps.set(tableName, references);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const sorted = [];
|
|
284
|
+
const visited = new Set();
|
|
285
|
+
const visiting = new Set();
|
|
286
|
+
|
|
287
|
+
const visit = (tableName) =>
|
|
288
|
+
{
|
|
289
|
+
if (visited.has(tableName)) return;
|
|
290
|
+
if (visiting.has(tableName)) return; // circular ref, break cycle
|
|
291
|
+
visiting.add(tableName);
|
|
292
|
+
const references = deps.get(tableName) || new Set();
|
|
293
|
+
for (const ref of references) visit(ref);
|
|
294
|
+
visiting.delete(tableName);
|
|
295
|
+
visited.add(tableName);
|
|
296
|
+
sorted.push(nameToModel.get(tableName));
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
for (const M of models) visit(M.table || M.name);
|
|
300
|
+
return sorted;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Drop all registered model tables (in reverse order to respect FK deps).
|
|
305
|
+
* @returns {Promise<void>}
|
|
306
|
+
*/
|
|
307
|
+
async drop()
|
|
308
|
+
{
|
|
309
|
+
const models = [...this._models.values()].reverse();
|
|
310
|
+
for (const ModelClass of models)
|
|
311
|
+
{
|
|
312
|
+
await ModelClass.drop();
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Close the underlying connection / pool.
|
|
318
|
+
* @returns {Promise<void>}
|
|
319
|
+
*/
|
|
320
|
+
async close()
|
|
321
|
+
{
|
|
322
|
+
if (typeof this.adapter.close === 'function')
|
|
323
|
+
{
|
|
324
|
+
await this.adapter.close();
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Get a registered model by table name.
|
|
330
|
+
* @param {string} name - Name identifier.
|
|
331
|
+
* @returns {typeof Model|undefined} The registered Model class, or undefined.
|
|
332
|
+
*/
|
|
333
|
+
model(name)
|
|
334
|
+
{
|
|
335
|
+
return this._models.get(name);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Execute a callback within a database transaction.
|
|
340
|
+
* If the callback throws, the transaction is rolled back.
|
|
341
|
+
* If the callback returns normally, the transaction is committed.
|
|
342
|
+
*
|
|
343
|
+
* Note: Transaction support depends on the adapter.
|
|
344
|
+
* Memory and JSON adapters run the callback directly (no real transaction).
|
|
345
|
+
*
|
|
346
|
+
* @param {Function} fn - Async callback to execute within the transaction.
|
|
347
|
+
* @returns {Promise<*>} The return value of the callback.
|
|
348
|
+
*
|
|
349
|
+
* @example
|
|
350
|
+
* await db.transaction(async () => {
|
|
351
|
+
* await User.create({ name: 'Alice', email: 'a@b.com' });
|
|
352
|
+
* await Account.create({ userId: 1, balance: 100 });
|
|
353
|
+
* });
|
|
354
|
+
*/
|
|
355
|
+
async transaction(fn)
|
|
356
|
+
{
|
|
357
|
+
if (typeof this.adapter.beginTransaction === 'function')
|
|
358
|
+
{
|
|
359
|
+
await this.adapter.beginTransaction();
|
|
360
|
+
try
|
|
361
|
+
{
|
|
362
|
+
const result = await fn();
|
|
363
|
+
await this.adapter.commit();
|
|
364
|
+
return result;
|
|
365
|
+
}
|
|
366
|
+
catch (err)
|
|
367
|
+
{
|
|
368
|
+
await this.adapter.rollback();
|
|
369
|
+
throw err;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
// Adapters without transaction support run directly
|
|
373
|
+
return fn();
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// -- Migration / DDL Convenience ---------------------
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Add a column to an existing table.
|
|
380
|
+
* @param {string} table - Table name.
|
|
381
|
+
* @param {string} column - Column name.
|
|
382
|
+
* @param {object} definition - Column definition.
|
|
383
|
+
* @returns {Promise<void>}
|
|
384
|
+
*/
|
|
385
|
+
async addColumn(table, column, definition)
|
|
386
|
+
{
|
|
387
|
+
if (typeof this.adapter.addColumn !== 'function')
|
|
388
|
+
throw new Error(`Adapter does not support addColumn`);
|
|
389
|
+
return this.adapter.addColumn(table, column, definition);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Drop a column from a table.
|
|
394
|
+
* @param {string} table - Table name.
|
|
395
|
+
* @param {string} column - Column name.
|
|
396
|
+
* @returns {Promise<void>}
|
|
397
|
+
*/
|
|
398
|
+
async dropColumn(table, column)
|
|
399
|
+
{
|
|
400
|
+
if (typeof this.adapter.dropColumn !== 'function')
|
|
401
|
+
throw new Error(`Adapter does not support dropColumn`);
|
|
402
|
+
return this.adapter.dropColumn(table, column);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Rename a column.
|
|
407
|
+
* @param {string} table - Table name.
|
|
408
|
+
* @param {string} oldName - Current name.
|
|
409
|
+
* @param {string} newName - New name.
|
|
410
|
+
* @returns {Promise<void>}
|
|
411
|
+
*/
|
|
412
|
+
async renameColumn(table, oldName, newName)
|
|
413
|
+
{
|
|
414
|
+
if (typeof this.adapter.renameColumn !== 'function')
|
|
415
|
+
throw new Error(`Adapter does not support renameColumn`);
|
|
416
|
+
return this.adapter.renameColumn(table, oldName, newName);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* Rename a table.
|
|
421
|
+
* @param {string} oldName - Current name.
|
|
422
|
+
* @param {string} newName - New name.
|
|
423
|
+
* @returns {Promise<void>}
|
|
424
|
+
*/
|
|
425
|
+
async renameTable(oldName, newName)
|
|
426
|
+
{
|
|
427
|
+
if (typeof this.adapter.renameTable !== 'function')
|
|
428
|
+
throw new Error(`Adapter does not support renameTable`);
|
|
429
|
+
return this.adapter.renameTable(oldName, newName);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Create an index on a table.
|
|
434
|
+
* @param {string} table - Table name.
|
|
435
|
+
* @param {string|string[]} columns - Column name(s).
|
|
436
|
+
* @param {object} [options={}] - Index configuration.
|
|
437
|
+
* @param {string} [options.name] - Custom index name.
|
|
438
|
+
* @param {boolean} [options.unique] - Create a unique index.
|
|
439
|
+
* @returns {Promise<void>}
|
|
440
|
+
*/
|
|
441
|
+
async createIndex(table, columns, options = {})
|
|
442
|
+
{
|
|
443
|
+
if (typeof this.adapter.createIndex !== 'function')
|
|
444
|
+
throw new Error(`Adapter does not support createIndex`);
|
|
445
|
+
return this.adapter.createIndex(table, columns, options);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* Drop an index.
|
|
450
|
+
* @param {string} table - Table name.
|
|
451
|
+
* @param {string} name - Index name.
|
|
452
|
+
* @returns {Promise<void>}
|
|
453
|
+
*/
|
|
454
|
+
async dropIndex(table, name)
|
|
455
|
+
{
|
|
456
|
+
if (typeof this.adapter.dropIndex !== 'function')
|
|
457
|
+
throw new Error(`Adapter does not support dropIndex`);
|
|
458
|
+
return this.adapter.dropIndex(table, name);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* Check if a table exists.
|
|
463
|
+
* @param {string} table - Table name.
|
|
464
|
+
* @returns {Promise<boolean>} True if the table exists.
|
|
465
|
+
*/
|
|
466
|
+
async hasTable(table)
|
|
467
|
+
{
|
|
468
|
+
if (typeof this.adapter.hasTable !== 'function')
|
|
469
|
+
throw new Error(`Adapter does not support hasTable`);
|
|
470
|
+
return this.adapter.hasTable(table);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* Check if a column exists on a table.
|
|
475
|
+
* @param {string} table - Table name.
|
|
476
|
+
* @param {string} column - Column name.
|
|
477
|
+
* @returns {Promise<boolean>} True if the column exists.
|
|
478
|
+
*/
|
|
479
|
+
async hasColumn(table, column)
|
|
480
|
+
{
|
|
481
|
+
if (typeof this.adapter.hasColumn !== 'function')
|
|
482
|
+
throw new Error(`Adapter does not support hasColumn`);
|
|
483
|
+
return this.adapter.hasColumn(table, column);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* Get detailed column info for a table.
|
|
488
|
+
* @param {string} table - Table name.
|
|
489
|
+
* @returns {Promise<Array>} Column definition objects for the table.
|
|
490
|
+
*/
|
|
491
|
+
async describeTable(table)
|
|
492
|
+
{
|
|
493
|
+
if (typeof this.adapter.describeTable !== 'function')
|
|
494
|
+
throw new Error(`Adapter does not support describeTable`);
|
|
495
|
+
return this.adapter.describeTable(table);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/**
|
|
499
|
+
* Add a foreign key constraint (MySQL / PostgreSQL only).
|
|
500
|
+
* @param {string} table - Table name.
|
|
501
|
+
* @param {string} column - Column name.
|
|
502
|
+
* @param {string} refTable - Referenced table name.
|
|
503
|
+
* @param {string} refColumn - Referenced column name.
|
|
504
|
+
* @param {object} [options={}] - Foreign key constraint options.
|
|
505
|
+
* @param {string} [options.onDelete] - Referential action on delete (CASCADE, SET NULL, RESTRICT, etc.).
|
|
506
|
+
* @param {string} [options.onUpdate] - Referential action on update (CASCADE, SET NULL, RESTRICT, etc.).
|
|
507
|
+
* @param {string} [options.name] - Custom constraint name.
|
|
508
|
+
* @returns {Promise<void>}
|
|
509
|
+
*/
|
|
510
|
+
async addForeignKey(table, column, refTable, refColumn, options = {})
|
|
511
|
+
{
|
|
512
|
+
if (typeof this.adapter.addForeignKey !== 'function')
|
|
513
|
+
throw new Error(`Adapter does not support addForeignKey`);
|
|
514
|
+
return this.adapter.addForeignKey(table, column, refTable, refColumn, options);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/**
|
|
518
|
+
* Drop a foreign key constraint (MySQL / PostgreSQL only).
|
|
519
|
+
* @param {string} table - Table name.
|
|
520
|
+
* @param {string} constraintName - Constraint name.
|
|
521
|
+
* @returns {Promise<void>}
|
|
522
|
+
*/
|
|
523
|
+
async dropForeignKey(table, constraintName)
|
|
524
|
+
{
|
|
525
|
+
if (typeof this.adapter.dropForeignKey !== 'function')
|
|
526
|
+
throw new Error(`Adapter does not support dropForeignKey`);
|
|
527
|
+
return this.adapter.dropForeignKey(table, constraintName);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// -- Health Check & Retry ----------------------------
|
|
531
|
+
|
|
532
|
+
/**
|
|
533
|
+
* Ping the database to check connectivity.
|
|
534
|
+
* Works across all adapters.
|
|
535
|
+
*
|
|
536
|
+
* @returns {Promise<boolean>} True if healthy.
|
|
537
|
+
*
|
|
538
|
+
* @example
|
|
539
|
+
* const healthy = await db.ping();
|
|
540
|
+
* if (!healthy) console.error('Database is unreachable');
|
|
541
|
+
*/
|
|
542
|
+
async ping()
|
|
543
|
+
{
|
|
544
|
+
try
|
|
545
|
+
{
|
|
546
|
+
// Adapter-specific ping
|
|
547
|
+
if (typeof this.adapter.ping === 'function')
|
|
548
|
+
{
|
|
549
|
+
return await this.adapter.ping();
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// Memory/JSON adapters are always healthy
|
|
553
|
+
if (typeof this.adapter._tables !== 'undefined' ||
|
|
554
|
+
typeof this.adapter._getTable === 'function')
|
|
555
|
+
{
|
|
556
|
+
return true;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// Fallback: try a trivial query
|
|
560
|
+
if (typeof this.adapter.execute === 'function')
|
|
561
|
+
{
|
|
562
|
+
await this.adapter.execute({ action: 'count', table: '_ping_test', where: [] });
|
|
563
|
+
return true;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
return true;
|
|
567
|
+
}
|
|
568
|
+
catch (_)
|
|
569
|
+
{
|
|
570
|
+
return false;
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
/**
|
|
575
|
+
* Execute a function with automatic retry on failure.
|
|
576
|
+
* Uses exponential backoff with jitter.
|
|
577
|
+
*
|
|
578
|
+
* @param {Function} fn - Async function to execute.
|
|
579
|
+
* @param {object} [options] - Configuration options.
|
|
580
|
+
* @param {number} [options.retries=3] - Maximum retry attempts.
|
|
581
|
+
* @param {number} [options.delay=100] - Initial delay in ms.
|
|
582
|
+
* @param {number} [options.maxDelay=5000] - Maximum delay in ms.
|
|
583
|
+
* @param {number} [options.factor=2] - Backoff multiplier.
|
|
584
|
+
* @param {Function} [options.onRetry] - Callback on each retry: (err, attempt) => {}.
|
|
585
|
+
* @returns {Promise<*>} Result of fn().
|
|
586
|
+
*
|
|
587
|
+
* @example
|
|
588
|
+
* const users = await db.retry(async () => {
|
|
589
|
+
* return User.find({ active: true });
|
|
590
|
+
* }, { retries: 5, delay: 200 });
|
|
591
|
+
*/
|
|
592
|
+
async retry(fn, options = {})
|
|
593
|
+
{
|
|
594
|
+
const {
|
|
595
|
+
retries = 3,
|
|
596
|
+
delay = 100,
|
|
597
|
+
maxDelay = 5000,
|
|
598
|
+
factor = 2,
|
|
599
|
+
onRetry,
|
|
600
|
+
} = options;
|
|
601
|
+
|
|
602
|
+
let lastError;
|
|
603
|
+
for (let attempt = 0; attempt <= retries; attempt++)
|
|
604
|
+
{
|
|
605
|
+
try
|
|
606
|
+
{
|
|
607
|
+
return await fn();
|
|
608
|
+
}
|
|
609
|
+
catch (err)
|
|
610
|
+
{
|
|
611
|
+
lastError = err;
|
|
612
|
+
if (attempt >= retries) break;
|
|
613
|
+
|
|
614
|
+
const wait = Math.min(delay * Math.pow(factor, attempt), maxDelay);
|
|
615
|
+
// Add jitter (±25%)
|
|
616
|
+
const jitter = wait * (0.75 + Math.random() * 0.5);
|
|
617
|
+
|
|
618
|
+
if (typeof onRetry === 'function')
|
|
619
|
+
{
|
|
620
|
+
onRetry(err, attempt + 1);
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
await new Promise(resolve => setTimeout(resolve, jitter));
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
throw lastError;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// -- Profiling ---------------------------------------
|
|
630
|
+
|
|
631
|
+
/**
|
|
632
|
+
* Enable query profiling on this database.
|
|
633
|
+
* Attaches a QueryProfiler that tracks every query execution, detects
|
|
634
|
+
* slow queries, and flags potential N+1 patterns.
|
|
635
|
+
*
|
|
636
|
+
* @param {object} [options] - Configuration options.
|
|
637
|
+
* @param {number} [options.slowThreshold=100] - Duration (ms) above which a query is considered slow.
|
|
638
|
+
* @param {number} [options.maxHistory=1000] - Maximum recorded query entries.
|
|
639
|
+
* @param {Function} [options.onSlow] - Callback on slow query: (entry) => {}.
|
|
640
|
+
* @param {number} [options.n1Threshold=5] - Minimum rapid SELECTs to flag N+1.
|
|
641
|
+
* @param {number} [options.n1Window=100] - Time window (ms) for N+1 detection.
|
|
642
|
+
* @param {Function} [options.onN1] - Callback on N+1 detection.
|
|
643
|
+
* @returns {QueryProfiler} The attached profiler instance.
|
|
644
|
+
*
|
|
645
|
+
* @example
|
|
646
|
+
* const profiler = db.enableProfiling({ slowThreshold: 50 });
|
|
647
|
+
* // ... run queries ...
|
|
648
|
+
* console.log(profiler.metrics());
|
|
649
|
+
*/
|
|
650
|
+
enableProfiling(options = {})
|
|
651
|
+
{
|
|
652
|
+
const { QueryProfiler } = require('./profiler');
|
|
653
|
+
this._profiler = new QueryProfiler(options);
|
|
654
|
+
this.adapter._profiler = this._profiler;
|
|
655
|
+
return this._profiler;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
/**
|
|
659
|
+
* The attached profiler (if profiling is enabled).
|
|
660
|
+
* @type {QueryProfiler|null}
|
|
661
|
+
*/
|
|
662
|
+
get profiler() { return this._profiler || null; }
|
|
663
|
+
|
|
664
|
+
/**
|
|
665
|
+
* The attached replica manager (if configured).
|
|
666
|
+
* @type {ReplicaManager|null}
|
|
667
|
+
*/
|
|
668
|
+
get replicas() { return this._replicaManager || null; }
|
|
669
|
+
|
|
670
|
+
// -- Read Replicas -----------------------------------
|
|
671
|
+
|
|
672
|
+
/**
|
|
673
|
+
* Create a Database with read replica support.
|
|
674
|
+
* Automatically sets up a ReplicaManager with the primary and replica adapters.
|
|
675
|
+
*
|
|
676
|
+
* @param {string} type - Adapter type.
|
|
677
|
+
* @param {object} primaryOpts - Options for the primary adapter.
|
|
678
|
+
* @param {object[]} [replicaConfigs=[]] - Array of options for each replica adapter.
|
|
679
|
+
* @param {object} [options] - ReplicaManager options.
|
|
680
|
+
* @param {string} [options.strategy='round-robin'] - Selection strategy.
|
|
681
|
+
* @param {boolean} [options.stickyWrite=true] - Read from primary after a write.
|
|
682
|
+
* @param {number} [options.stickyWindow=1000] - Duration of sticky window (ms).
|
|
683
|
+
* @returns {Database} Connected database with replica routing.
|
|
684
|
+
*
|
|
685
|
+
* @example
|
|
686
|
+
* const db = Database.connectWithReplicas('postgres',
|
|
687
|
+
* { host: 'primary.db', database: 'app' },
|
|
688
|
+
* [
|
|
689
|
+
* { host: 'replica1.db', database: 'app' },
|
|
690
|
+
* { host: 'replica2.db', database: 'app' },
|
|
691
|
+
* ],
|
|
692
|
+
* { strategy: 'round-robin', stickyWindow: 2000 }
|
|
693
|
+
* );
|
|
694
|
+
*/
|
|
695
|
+
static connectWithReplicas(type, primaryOpts, replicaConfigs = [], options = {})
|
|
696
|
+
{
|
|
697
|
+
if (!Array.isArray(replicaConfigs))
|
|
698
|
+
{
|
|
699
|
+
throw new Error('replicaConfigs must be an array');
|
|
700
|
+
}
|
|
701
|
+
const { ReplicaManager } = require('./replicas');
|
|
702
|
+
const db = Database.connect(type, primaryOpts);
|
|
703
|
+
const manager = new ReplicaManager(options);
|
|
704
|
+
manager.setPrimary(db.adapter);
|
|
705
|
+
|
|
706
|
+
for (const opts of replicaConfigs)
|
|
707
|
+
{
|
|
708
|
+
const replicaDb = Database.connect(type, opts);
|
|
709
|
+
manager.addReplica(replicaDb.adapter);
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
db._replicaManager = manager;
|
|
713
|
+
db.adapter._replicaManager = manager;
|
|
714
|
+
return db;
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
// -- Lazy module loaders ---------------------------------
|
|
719
|
+
|
|
720
|
+
const { Migrator, defineMigration } = require('./migrate');
|
|
721
|
+
const { QueryCache } = require('./cache');
|
|
722
|
+
const { Seeder, SeederRunner, Factory, Fake } = require('./seed');
|
|
723
|
+
const { QueryProfiler } = require('./profiler');
|
|
724
|
+
const { ReplicaManager } = require('./replicas');
|
|
725
|
+
const { DatabaseView } = require('./views');
|
|
726
|
+
const { FullTextSearch } = require('./search');
|
|
727
|
+
const { GeoQuery, EARTH_RADIUS_KM, EARTH_RADIUS_MI } = require('./geo');
|
|
728
|
+
const { TenantManager } = require('./tenancy');
|
|
729
|
+
const { AuditLog } = require('./audit');
|
|
730
|
+
const { PluginManager } = require('./plugin');
|
|
731
|
+
const { StoredProcedure, StoredFunction, TriggerManager } = require('./procedures');
|
|
732
|
+
const {
|
|
733
|
+
buildSnapshot, loadSnapshot, saveSnapshot,
|
|
734
|
+
diffSnapshots, hasNoChanges, generateMigrationCode,
|
|
735
|
+
discoverModels, SNAPSHOT_FILE,
|
|
736
|
+
} = require('./snapshot');
|
|
737
|
+
|
|
738
|
+
// -- Exports ---------------------------------------------
|
|
739
|
+
|
|
740
|
+
module.exports = {
|
|
741
|
+
Database,
|
|
742
|
+
Model,
|
|
743
|
+
TYPES,
|
|
744
|
+
Query,
|
|
745
|
+
validate,
|
|
746
|
+
validateValue,
|
|
747
|
+
validateFKAction,
|
|
748
|
+
validateCheck,
|
|
749
|
+
// Migration framework
|
|
750
|
+
Migrator,
|
|
751
|
+
defineMigration,
|
|
752
|
+
// Schema snapshots (EF Core–style auto-migrations)
|
|
753
|
+
buildSnapshot,
|
|
754
|
+
loadSnapshot,
|
|
755
|
+
saveSnapshot,
|
|
756
|
+
diffSnapshots,
|
|
757
|
+
hasNoChanges,
|
|
758
|
+
generateMigrationCode,
|
|
759
|
+
discoverModels,
|
|
760
|
+
SNAPSHOT_FILE,
|
|
761
|
+
// Query caching
|
|
762
|
+
QueryCache,
|
|
763
|
+
// Seeder framework
|
|
764
|
+
Seeder,
|
|
765
|
+
SeederRunner,
|
|
766
|
+
Factory,
|
|
767
|
+
Fake,
|
|
768
|
+
// Performance & Scalability (Phase 2)
|
|
769
|
+
QueryProfiler,
|
|
770
|
+
ReplicaManager,
|
|
771
|
+
// Advanced ORM Features (Phase 3)
|
|
772
|
+
DatabaseView,
|
|
773
|
+
FullTextSearch,
|
|
774
|
+
GeoQuery,
|
|
775
|
+
EARTH_RADIUS_KM,
|
|
776
|
+
EARTH_RADIUS_MI,
|
|
777
|
+
// Enterprise Infrastructure (Phase 4)
|
|
778
|
+
TenantManager,
|
|
779
|
+
AuditLog,
|
|
780
|
+
PluginManager,
|
|
781
|
+
StoredProcedure,
|
|
782
|
+
StoredFunction,
|
|
783
|
+
TriggerManager,
|
|
784
|
+
};
|