@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
|
@@ -0,0 +1,836 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module orm/procedures
|
|
3
|
+
* @description Stored procedures, functions, and trigger management for the ORM.
|
|
4
|
+
* Provides a cross-adapter API for defining, creating, executing,
|
|
5
|
+
* and dropping stored procedures, functions, and triggers.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* const { StoredProcedure, StoredFunction, TriggerManager } = require('@zero-server/sdk');
|
|
9
|
+
*
|
|
10
|
+
* // Define a procedure
|
|
11
|
+
* const proc = new StoredProcedure('update_balance', {
|
|
12
|
+
* params: [
|
|
13
|
+
* { name: 'user_id', type: 'INTEGER' },
|
|
14
|
+
* { name: 'amount', type: 'DECIMAL' },
|
|
15
|
+
* ],
|
|
16
|
+
* body: `UPDATE accounts SET balance = balance + amount WHERE id = user_id;`,
|
|
17
|
+
* });
|
|
18
|
+
*
|
|
19
|
+
* await proc.create(db);
|
|
20
|
+
* await proc.execute(db, [1, 50.00]);
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
const log = require('../debug')('zero:orm:procedures');
|
|
24
|
+
|
|
25
|
+
// -- StoredProcedure -----------------------------------------
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Represents a stored procedure.
|
|
29
|
+
* Generates adapter-appropriate DDL and executes the procedure.
|
|
30
|
+
*/
|
|
31
|
+
class StoredProcedure
|
|
32
|
+
{
|
|
33
|
+
/**
|
|
34
|
+
* @constructor
|
|
35
|
+
* @param {string} name - Procedure name.
|
|
36
|
+
* @param {object} options - Procedure definition.
|
|
37
|
+
* @param {Array<{name: string, type: string, direction?: string}>} [options.params=[]] - Parameters.
|
|
38
|
+
* @param {string} options.body - Procedure body (SQL).
|
|
39
|
+
* @param {string} [options.language='sql'] - Language (sql, plpgsql, javascript).
|
|
40
|
+
* @param {object} [options.options] - Adapter-specific options.
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* const proc = new StoredProcedure('calculate_tax', {
|
|
44
|
+
* params: [{ name: 'subtotal', type: 'DECIMAL' }],
|
|
45
|
+
* body: 'UPDATE cart SET tax = subtotal * 0.08;',
|
|
46
|
+
* });
|
|
47
|
+
*/
|
|
48
|
+
constructor(name, options = {})
|
|
49
|
+
{
|
|
50
|
+
if (!name || typeof name !== 'string')
|
|
51
|
+
{
|
|
52
|
+
throw new Error('StoredProcedure requires a non-empty string name');
|
|
53
|
+
}
|
|
54
|
+
if (!options.body || typeof options.body !== 'string')
|
|
55
|
+
{
|
|
56
|
+
throw new Error('StoredProcedure requires a "body" string');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Sanitize name — only allow identifiers
|
|
60
|
+
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name))
|
|
61
|
+
{
|
|
62
|
+
throw new Error(`Invalid procedure name: "${name}"`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** @type {string} */
|
|
66
|
+
this.name = name;
|
|
67
|
+
|
|
68
|
+
/** @type {Array<{name: string, type: string, direction?: string}>} */
|
|
69
|
+
this.params = options.params || [];
|
|
70
|
+
|
|
71
|
+
/** @type {string} */
|
|
72
|
+
this.body = options.body;
|
|
73
|
+
|
|
74
|
+
/** @type {string} */
|
|
75
|
+
this.language = options.language || 'sql';
|
|
76
|
+
|
|
77
|
+
/** @type {object} */
|
|
78
|
+
this.adapterOptions = options.options || {};
|
|
79
|
+
|
|
80
|
+
// Validate param names
|
|
81
|
+
for (const p of this.params)
|
|
82
|
+
{
|
|
83
|
+
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(p.name))
|
|
84
|
+
{
|
|
85
|
+
throw new Error(`Invalid parameter name: "${p.name}"`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Create the stored procedure in the database.
|
|
92
|
+
*
|
|
93
|
+
* @param {import('./index').Database} db - Database instance.
|
|
94
|
+
* @returns {Promise<void>}
|
|
95
|
+
*
|
|
96
|
+
* @example
|
|
97
|
+
* await proc.create(db);
|
|
98
|
+
*/
|
|
99
|
+
async create(db)
|
|
100
|
+
{
|
|
101
|
+
const adapter = db.adapter;
|
|
102
|
+
const adapterType = this._detectAdapter(adapter);
|
|
103
|
+
|
|
104
|
+
if (typeof adapter.createProcedure === 'function')
|
|
105
|
+
{
|
|
106
|
+
return adapter.createProcedure(this.name, this.params, this.body, {
|
|
107
|
+
language: this.language,
|
|
108
|
+
...this.adapterOptions,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const sql = this._buildCreateSQL(adapterType);
|
|
113
|
+
|
|
114
|
+
if (typeof adapter.execute !== 'function')
|
|
115
|
+
{
|
|
116
|
+
throw new Error('StoredProcedure requires a SQL adapter');
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
await adapter.execute({ raw: sql });
|
|
120
|
+
log('procedure created', this.name);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Drop the stored procedure.
|
|
125
|
+
*
|
|
126
|
+
* @param {import('./index').Database} db - Database instance.
|
|
127
|
+
* @param {object} [options] - Drop options.
|
|
128
|
+
* @param {boolean} [options.ifExists=true] - Add IF EXISTS clause.
|
|
129
|
+
* @returns {Promise<void>}
|
|
130
|
+
*
|
|
131
|
+
* @example
|
|
132
|
+
* await proc.drop(db);
|
|
133
|
+
*/
|
|
134
|
+
async drop(db, options = {})
|
|
135
|
+
{
|
|
136
|
+
const adapter = db.adapter;
|
|
137
|
+
const ifExists = options.ifExists !== false;
|
|
138
|
+
const adapterType = this._detectAdapter(adapter);
|
|
139
|
+
|
|
140
|
+
if (typeof adapter.dropProcedure === 'function')
|
|
141
|
+
{
|
|
142
|
+
return adapter.dropProcedure(this.name, options);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (typeof adapter.execute !== 'function')
|
|
146
|
+
{
|
|
147
|
+
throw new Error('StoredProcedure requires a SQL adapter');
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
let sql;
|
|
151
|
+
if (adapterType === 'mysql')
|
|
152
|
+
{
|
|
153
|
+
sql = `DROP PROCEDURE ${ifExists ? 'IF EXISTS ' : ''}\`${this.name}\``;
|
|
154
|
+
}
|
|
155
|
+
else if (adapterType === 'postgres')
|
|
156
|
+
{
|
|
157
|
+
const paramTypes = this.params.map(p => p.type).join(', ');
|
|
158
|
+
sql = `DROP PROCEDURE ${ifExists ? 'IF EXISTS ' : ''}"${this.name}"(${paramTypes})`;
|
|
159
|
+
}
|
|
160
|
+
else
|
|
161
|
+
{
|
|
162
|
+
sql = `DROP PROCEDURE ${ifExists ? 'IF EXISTS ' : ''}"${this.name}"`;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
await adapter.execute({ raw: sql });
|
|
166
|
+
log('procedure dropped', this.name);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Execute the stored procedure with arguments.
|
|
171
|
+
*
|
|
172
|
+
* @param {import('./index').Database} db - Database instance.
|
|
173
|
+
* @param {Array} [args=[]] - Procedure arguments (positional).
|
|
174
|
+
* @returns {Promise<*>} Result from the database.
|
|
175
|
+
*
|
|
176
|
+
* @example
|
|
177
|
+
* const result = await proc.execute(db, [1, 50.00]);
|
|
178
|
+
*/
|
|
179
|
+
async execute(db, args = [])
|
|
180
|
+
{
|
|
181
|
+
const adapter = db.adapter;
|
|
182
|
+
const adapterType = this._detectAdapter(adapter);
|
|
183
|
+
|
|
184
|
+
if (typeof adapter.callProcedure === 'function')
|
|
185
|
+
{
|
|
186
|
+
return adapter.callProcedure(this.name, args);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (typeof adapter.execute !== 'function')
|
|
190
|
+
{
|
|
191
|
+
throw new Error('StoredProcedure requires a SQL adapter');
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
let sql;
|
|
195
|
+
const placeholders = args.map((_, i) =>
|
|
196
|
+
adapterType === 'postgres' ? `$${i + 1}` : '?'
|
|
197
|
+
).join(', ');
|
|
198
|
+
|
|
199
|
+
if (adapterType === 'mysql')
|
|
200
|
+
{
|
|
201
|
+
sql = `CALL \`${this.name}\`(${placeholders})`;
|
|
202
|
+
}
|
|
203
|
+
else if (adapterType === 'postgres')
|
|
204
|
+
{
|
|
205
|
+
sql = `CALL "${this.name}"(${placeholders})`;
|
|
206
|
+
}
|
|
207
|
+
else
|
|
208
|
+
{
|
|
209
|
+
// SQLite doesn't support stored procedures natively
|
|
210
|
+
throw new Error('Stored procedures are not supported by this adapter');
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return adapter.execute({ raw: sql, params: args });
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Check if the procedure exists.
|
|
218
|
+
*
|
|
219
|
+
* @param {import('./index').Database} db - Database instance.
|
|
220
|
+
* @returns {Promise<boolean>}
|
|
221
|
+
*/
|
|
222
|
+
async exists(db)
|
|
223
|
+
{
|
|
224
|
+
const adapter = db.adapter;
|
|
225
|
+
const adapterType = this._detectAdapter(adapter);
|
|
226
|
+
|
|
227
|
+
if (typeof adapter.execute !== 'function') return false;
|
|
228
|
+
|
|
229
|
+
try
|
|
230
|
+
{
|
|
231
|
+
let sql, params;
|
|
232
|
+
if (adapterType === 'mysql')
|
|
233
|
+
{
|
|
234
|
+
sql = `SELECT COUNT(*) as cnt FROM information_schema.ROUTINES WHERE ROUTINE_TYPE = 'PROCEDURE' AND ROUTINE_NAME = ?`;
|
|
235
|
+
params = [this.name];
|
|
236
|
+
}
|
|
237
|
+
else if (adapterType === 'postgres')
|
|
238
|
+
{
|
|
239
|
+
sql = `SELECT COUNT(*) as cnt FROM information_schema.routines WHERE routine_type = 'PROCEDURE' AND routine_name = $1`;
|
|
240
|
+
params = [this.name];
|
|
241
|
+
}
|
|
242
|
+
else
|
|
243
|
+
{
|
|
244
|
+
return false;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const result = await adapter.execute({ raw: sql, params });
|
|
248
|
+
if (Array.isArray(result) && result[0])
|
|
249
|
+
{
|
|
250
|
+
return (result[0].cnt || 0) > 0;
|
|
251
|
+
}
|
|
252
|
+
return false;
|
|
253
|
+
}
|
|
254
|
+
catch (_)
|
|
255
|
+
{
|
|
256
|
+
return false;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// -- Internal ----------------------------------------
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* @private
|
|
264
|
+
*/
|
|
265
|
+
_detectAdapter(adapter)
|
|
266
|
+
{
|
|
267
|
+
const name = (adapter.constructor.name || '').toLowerCase();
|
|
268
|
+
if (name.includes('mysql')) return 'mysql';
|
|
269
|
+
if (name.includes('postgres') || name.includes('pg')) return 'postgres';
|
|
270
|
+
if (name.includes('sqlite')) return 'sqlite';
|
|
271
|
+
return 'unknown';
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* @private
|
|
276
|
+
*/
|
|
277
|
+
_buildCreateSQL(adapterType)
|
|
278
|
+
{
|
|
279
|
+
if (adapterType === 'mysql')
|
|
280
|
+
{
|
|
281
|
+
const params = this.params.map(p =>
|
|
282
|
+
{
|
|
283
|
+
const dir = (p.direction || 'IN').toUpperCase();
|
|
284
|
+
return `${dir} \`${p.name}\` ${p.type}`;
|
|
285
|
+
}).join(', ');
|
|
286
|
+
|
|
287
|
+
return `CREATE PROCEDURE \`${this.name}\`(${params})\nBEGIN\n${this.body}\nEND`;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (adapterType === 'postgres')
|
|
291
|
+
{
|
|
292
|
+
const params = this.params.map(p =>
|
|
293
|
+
{
|
|
294
|
+
const dir = (p.direction || 'IN').toUpperCase();
|
|
295
|
+
return `${dir} "${p.name}" ${p.type}`;
|
|
296
|
+
}).join(', ');
|
|
297
|
+
|
|
298
|
+
const lang = this.language === 'sql' ? 'SQL' : this.language.toUpperCase();
|
|
299
|
+
return `CREATE OR REPLACE PROCEDURE "${this.name}"(${params})\nLANGUAGE ${lang}\nAS $$\n${this.body}\n$$`;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
throw new Error(`Stored procedures not supported for adapter: ${adapterType}`);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// -- StoredFunction ------------------------------------------
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Represents a database function.
|
|
310
|
+
* Similar to StoredProcedure but returns a value.
|
|
311
|
+
*/
|
|
312
|
+
class StoredFunction
|
|
313
|
+
{
|
|
314
|
+
/**
|
|
315
|
+
* @constructor
|
|
316
|
+
* @param {string} name - Function name.
|
|
317
|
+
* @param {object} options - Function definition.
|
|
318
|
+
* @param {Array<{name: string, type: string}>} [options.params=[]] - Parameters.
|
|
319
|
+
* @param {string} options.returns - Return type (e.g. 'INTEGER', 'TEXT').
|
|
320
|
+
* @param {string} options.body - Function body (SQL).
|
|
321
|
+
* @param {string} [options.language='sql'] - Language.
|
|
322
|
+
* @param {boolean} [options.deterministic=false] - Whether function is deterministic (MySQL).
|
|
323
|
+
* @param {string} [options.volatility] - PostgreSQL volatility (STABLE, VOLATILE, IMMUTABLE).
|
|
324
|
+
*
|
|
325
|
+
* @example
|
|
326
|
+
* const fn = new StoredFunction('calculate_tax', {
|
|
327
|
+
* params: [{ name: 'amount', type: 'DECIMAL' }],
|
|
328
|
+
* returns: 'DECIMAL',
|
|
329
|
+
* body: 'RETURN amount * 0.08;',
|
|
330
|
+
* });
|
|
331
|
+
*/
|
|
332
|
+
constructor(name, options = {})
|
|
333
|
+
{
|
|
334
|
+
if (!name || typeof name !== 'string')
|
|
335
|
+
{
|
|
336
|
+
throw new Error('StoredFunction requires a non-empty string name');
|
|
337
|
+
}
|
|
338
|
+
if (!options.body || typeof options.body !== 'string')
|
|
339
|
+
{
|
|
340
|
+
throw new Error('StoredFunction requires a "body" string');
|
|
341
|
+
}
|
|
342
|
+
if (!options.returns || typeof options.returns !== 'string')
|
|
343
|
+
{
|
|
344
|
+
throw new Error('StoredFunction requires a "returns" type string');
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name))
|
|
348
|
+
{
|
|
349
|
+
throw new Error(`Invalid function name: "${name}"`);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/** @type {string} */
|
|
353
|
+
this.name = name;
|
|
354
|
+
|
|
355
|
+
/** @type {Array<{name: string, type: string}>} */
|
|
356
|
+
this.params = options.params || [];
|
|
357
|
+
|
|
358
|
+
/** @type {string} */
|
|
359
|
+
this.returns = options.returns;
|
|
360
|
+
|
|
361
|
+
/** @type {string} */
|
|
362
|
+
this.body = options.body;
|
|
363
|
+
|
|
364
|
+
/** @type {string} */
|
|
365
|
+
this.language = options.language || 'sql';
|
|
366
|
+
|
|
367
|
+
/** @type {boolean} */
|
|
368
|
+
this.deterministic = options.deterministic === true;
|
|
369
|
+
|
|
370
|
+
/** @type {string|null} */
|
|
371
|
+
this.volatility = options.volatility || null;
|
|
372
|
+
|
|
373
|
+
// Validate param names
|
|
374
|
+
for (const p of this.params)
|
|
375
|
+
{
|
|
376
|
+
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(p.name))
|
|
377
|
+
{
|
|
378
|
+
throw new Error(`Invalid parameter name: "${p.name}"`);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Create the function in the database.
|
|
385
|
+
*
|
|
386
|
+
* @param {import('./index').Database} db - Database instance.
|
|
387
|
+
* @returns {Promise<void>}
|
|
388
|
+
*
|
|
389
|
+
* @example
|
|
390
|
+
* await fn.create(db);
|
|
391
|
+
*/
|
|
392
|
+
async create(db)
|
|
393
|
+
{
|
|
394
|
+
const adapter = db.adapter;
|
|
395
|
+
const adapterType = this._detectAdapter(adapter);
|
|
396
|
+
|
|
397
|
+
if (typeof adapter.createFunction === 'function')
|
|
398
|
+
{
|
|
399
|
+
return adapter.createFunction(this.name, this.params, this.returns, this.body, {
|
|
400
|
+
language: this.language,
|
|
401
|
+
deterministic: this.deterministic,
|
|
402
|
+
volatility: this.volatility,
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
const sql = this._buildCreateSQL(adapterType);
|
|
407
|
+
|
|
408
|
+
if (typeof adapter.execute !== 'function')
|
|
409
|
+
{
|
|
410
|
+
throw new Error('StoredFunction requires a SQL adapter');
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
await adapter.execute({ raw: sql });
|
|
414
|
+
log('function created', this.name);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Drop the function.
|
|
419
|
+
*
|
|
420
|
+
* @param {import('./index').Database} db - Database instance.
|
|
421
|
+
* @param {object} [options] - Drop options.
|
|
422
|
+
* @param {boolean} [options.ifExists=true] - Add IF EXISTS clause.
|
|
423
|
+
* @returns {Promise<void>}
|
|
424
|
+
*/
|
|
425
|
+
async drop(db, options = {})
|
|
426
|
+
{
|
|
427
|
+
const adapter = db.adapter;
|
|
428
|
+
const ifExists = options.ifExists !== false;
|
|
429
|
+
const adapterType = this._detectAdapter(adapter);
|
|
430
|
+
|
|
431
|
+
if (typeof adapter.dropFunction === 'function')
|
|
432
|
+
{
|
|
433
|
+
return adapter.dropFunction(this.name, options);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
if (typeof adapter.execute !== 'function')
|
|
437
|
+
{
|
|
438
|
+
throw new Error('StoredFunction requires a SQL adapter');
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
let sql;
|
|
442
|
+
if (adapterType === 'mysql')
|
|
443
|
+
{
|
|
444
|
+
sql = `DROP FUNCTION ${ifExists ? 'IF EXISTS ' : ''}\`${this.name}\``;
|
|
445
|
+
}
|
|
446
|
+
else if (adapterType === 'postgres')
|
|
447
|
+
{
|
|
448
|
+
const paramTypes = this.params.map(p => p.type).join(', ');
|
|
449
|
+
sql = `DROP FUNCTION ${ifExists ? 'IF EXISTS ' : ''}"${this.name}"(${paramTypes})`;
|
|
450
|
+
}
|
|
451
|
+
else
|
|
452
|
+
{
|
|
453
|
+
sql = `DROP FUNCTION ${ifExists ? 'IF EXISTS ' : ''}"${this.name}"`;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
await adapter.execute({ raw: sql });
|
|
457
|
+
log('function dropped', this.name);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* Call the function and return its result.
|
|
462
|
+
*
|
|
463
|
+
* @param {import('./index').Database} db - Database instance.
|
|
464
|
+
* @param {Array} [args=[]] - Function arguments.
|
|
465
|
+
* @returns {Promise<*>} Function return value.
|
|
466
|
+
*
|
|
467
|
+
* @example
|
|
468
|
+
* const tax = await fn.call(db, [100.00]);
|
|
469
|
+
*/
|
|
470
|
+
async call(db, args = [])
|
|
471
|
+
{
|
|
472
|
+
const adapter = db.adapter;
|
|
473
|
+
const adapterType = this._detectAdapter(adapter);
|
|
474
|
+
|
|
475
|
+
if (typeof adapter.callFunction === 'function')
|
|
476
|
+
{
|
|
477
|
+
return adapter.callFunction(this.name, args);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
if (typeof adapter.execute !== 'function')
|
|
481
|
+
{
|
|
482
|
+
throw new Error('StoredFunction requires a SQL adapter');
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
const placeholders = args.map((_, i) =>
|
|
486
|
+
adapterType === 'postgres' ? `$${i + 1}` : '?'
|
|
487
|
+
).join(', ');
|
|
488
|
+
|
|
489
|
+
const sql = adapterType === 'mysql'
|
|
490
|
+
? `SELECT \`${this.name}\`(${placeholders}) AS result`
|
|
491
|
+
: `SELECT "${this.name}"(${placeholders}) AS result`;
|
|
492
|
+
|
|
493
|
+
const result = await adapter.execute({ raw: sql, params: args });
|
|
494
|
+
if (Array.isArray(result) && result[0])
|
|
495
|
+
{
|
|
496
|
+
return result[0].result;
|
|
497
|
+
}
|
|
498
|
+
return result;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* Check if the function exists.
|
|
503
|
+
*
|
|
504
|
+
* @param {import('./index').Database} db - Database instance.
|
|
505
|
+
* @returns {Promise<boolean>}
|
|
506
|
+
*/
|
|
507
|
+
async exists(db)
|
|
508
|
+
{
|
|
509
|
+
const adapter = db.adapter;
|
|
510
|
+
const adapterType = this._detectAdapter(adapter);
|
|
511
|
+
|
|
512
|
+
if (typeof adapter.execute !== 'function') return false;
|
|
513
|
+
|
|
514
|
+
try
|
|
515
|
+
{
|
|
516
|
+
let sql, params;
|
|
517
|
+
if (adapterType === 'mysql')
|
|
518
|
+
{
|
|
519
|
+
sql = `SELECT COUNT(*) as cnt FROM information_schema.ROUTINES WHERE ROUTINE_TYPE = 'FUNCTION' AND ROUTINE_NAME = ?`;
|
|
520
|
+
params = [this.name];
|
|
521
|
+
}
|
|
522
|
+
else if (adapterType === 'postgres')
|
|
523
|
+
{
|
|
524
|
+
sql = `SELECT COUNT(*) as cnt FROM information_schema.routines WHERE routine_type = 'FUNCTION' AND routine_name = $1`;
|
|
525
|
+
params = [this.name];
|
|
526
|
+
}
|
|
527
|
+
else
|
|
528
|
+
{
|
|
529
|
+
return false;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
const result = await adapter.execute({ raw: sql, params });
|
|
533
|
+
if (Array.isArray(result) && result[0])
|
|
534
|
+
{
|
|
535
|
+
return (result[0].cnt || 0) > 0;
|
|
536
|
+
}
|
|
537
|
+
return false;
|
|
538
|
+
}
|
|
539
|
+
catch (_)
|
|
540
|
+
{
|
|
541
|
+
return false;
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
/**
|
|
546
|
+
* @private
|
|
547
|
+
*/
|
|
548
|
+
_detectAdapter(adapter)
|
|
549
|
+
{
|
|
550
|
+
const name = (adapter.constructor.name || '').toLowerCase();
|
|
551
|
+
if (name.includes('mysql')) return 'mysql';
|
|
552
|
+
if (name.includes('postgres') || name.includes('pg')) return 'postgres';
|
|
553
|
+
if (name.includes('sqlite')) return 'sqlite';
|
|
554
|
+
return 'unknown';
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
/**
|
|
558
|
+
* @private
|
|
559
|
+
*/
|
|
560
|
+
_buildCreateSQL(adapterType)
|
|
561
|
+
{
|
|
562
|
+
if (adapterType === 'mysql')
|
|
563
|
+
{
|
|
564
|
+
const params = this.params.map(p => `\`${p.name}\` ${p.type}`).join(', ');
|
|
565
|
+
const det = this.deterministic ? '\nDETERMINISTIC' : '';
|
|
566
|
+
return `CREATE FUNCTION \`${this.name}\`(${params})\nRETURNS ${this.returns}${det}\nBEGIN\n${this.body}\nEND`;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
if (adapterType === 'postgres')
|
|
570
|
+
{
|
|
571
|
+
const params = this.params.map(p => `"${p.name}" ${p.type}`).join(', ');
|
|
572
|
+
const lang = this.language === 'sql' ? 'SQL' : this.language.toUpperCase();
|
|
573
|
+
const vol = this.volatility ? `\n${this.volatility.toUpperCase()}` : '';
|
|
574
|
+
return `CREATE OR REPLACE FUNCTION "${this.name}"(${params})\nRETURNS ${this.returns}\nLANGUAGE ${lang}${vol}\nAS $$\n${this.body}\n$$`;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
throw new Error(`Stored functions not supported for adapter: ${adapterType}`);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
// -- TriggerManager ------------------------------------------
|
|
582
|
+
|
|
583
|
+
/**
|
|
584
|
+
* Database trigger management.
|
|
585
|
+
* Define, create, drop, and list triggers.
|
|
586
|
+
*/
|
|
587
|
+
class TriggerManager
|
|
588
|
+
{
|
|
589
|
+
/**
|
|
590
|
+
* @constructor
|
|
591
|
+
* @param {import('./index').Database} db - Database instance.
|
|
592
|
+
*
|
|
593
|
+
* @example
|
|
594
|
+
* const triggers = new TriggerManager(db);
|
|
595
|
+
*/
|
|
596
|
+
constructor(db)
|
|
597
|
+
{
|
|
598
|
+
if (!db) throw new Error('TriggerManager requires a Database instance');
|
|
599
|
+
|
|
600
|
+
/** @type {import('./index').Database} */
|
|
601
|
+
this.db = db;
|
|
602
|
+
|
|
603
|
+
/** @type {Map<string, object>} Registered trigger definitions. */
|
|
604
|
+
this._triggers = new Map();
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
/**
|
|
608
|
+
* Define a trigger.
|
|
609
|
+
*
|
|
610
|
+
* @param {string} name - Trigger name.
|
|
611
|
+
* @param {object} options - Trigger definition.
|
|
612
|
+
* @param {string} options.table - Table the trigger is on.
|
|
613
|
+
* @param {string} options.timing - 'BEFORE' or 'AFTER'.
|
|
614
|
+
* @param {string} options.event - 'INSERT', 'UPDATE', or 'DELETE'.
|
|
615
|
+
* @param {string} options.body - Trigger body (SQL).
|
|
616
|
+
* @param {string} [options.forEach='ROW'] - 'ROW' or 'STATEMENT'.
|
|
617
|
+
* @param {string} [options.when] - Optional WHEN condition.
|
|
618
|
+
* @returns {TriggerManager} this (for chaining)
|
|
619
|
+
*
|
|
620
|
+
* @example
|
|
621
|
+
* triggers.define('trg_audit_users', {
|
|
622
|
+
* table: 'users',
|
|
623
|
+
* timing: 'AFTER',
|
|
624
|
+
* event: 'UPDATE',
|
|
625
|
+
* body: 'INSERT INTO audit_log(table_name, record_id, action) VALUES ("users", NEW.id, "update");',
|
|
626
|
+
* });
|
|
627
|
+
*/
|
|
628
|
+
define(name, options = {})
|
|
629
|
+
{
|
|
630
|
+
if (!name || typeof name !== 'string')
|
|
631
|
+
{
|
|
632
|
+
throw new Error('Trigger name must be a non-empty string');
|
|
633
|
+
}
|
|
634
|
+
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name))
|
|
635
|
+
{
|
|
636
|
+
throw new Error(`Invalid trigger name: "${name}"`);
|
|
637
|
+
}
|
|
638
|
+
if (!options.table || typeof options.table !== 'string')
|
|
639
|
+
{
|
|
640
|
+
throw new Error('Trigger requires a "table" string');
|
|
641
|
+
}
|
|
642
|
+
if (!options.timing || !['BEFORE', 'AFTER', 'INSTEAD OF'].includes(options.timing.toUpperCase()))
|
|
643
|
+
{
|
|
644
|
+
throw new Error('Trigger timing must be BEFORE, AFTER, or INSTEAD OF');
|
|
645
|
+
}
|
|
646
|
+
if (!options.event || !['INSERT', 'UPDATE', 'DELETE'].includes(options.event.toUpperCase()))
|
|
647
|
+
{
|
|
648
|
+
throw new Error('Trigger event must be INSERT, UPDATE, or DELETE');
|
|
649
|
+
}
|
|
650
|
+
if (!options.body || typeof options.body !== 'string')
|
|
651
|
+
{
|
|
652
|
+
throw new Error('Trigger requires a "body" string');
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
this._triggers.set(name, {
|
|
656
|
+
name,
|
|
657
|
+
table: options.table,
|
|
658
|
+
timing: options.timing.toUpperCase(),
|
|
659
|
+
event: options.event.toUpperCase(),
|
|
660
|
+
body: options.body,
|
|
661
|
+
forEach: (options.forEach || 'ROW').toUpperCase(),
|
|
662
|
+
when: options.when || null,
|
|
663
|
+
});
|
|
664
|
+
|
|
665
|
+
log('trigger defined', name);
|
|
666
|
+
return this;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
/**
|
|
670
|
+
* Create a trigger in the database.
|
|
671
|
+
*
|
|
672
|
+
* @param {string} name - Trigger name (must be previously defined).
|
|
673
|
+
* @returns {Promise<void>}
|
|
674
|
+
*
|
|
675
|
+
* @example
|
|
676
|
+
* await triggers.create('trg_audit_users');
|
|
677
|
+
*/
|
|
678
|
+
async create(name)
|
|
679
|
+
{
|
|
680
|
+
const def = this._triggers.get(name);
|
|
681
|
+
if (!def)
|
|
682
|
+
{
|
|
683
|
+
throw new Error(`Trigger "${name}" is not defined. Call define() first.`);
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
const adapter = this.db.adapter;
|
|
687
|
+
const adapterType = this._detectAdapter(adapter);
|
|
688
|
+
|
|
689
|
+
if (typeof adapter.createTrigger === 'function')
|
|
690
|
+
{
|
|
691
|
+
return adapter.createTrigger(def);
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
if (typeof adapter.execute !== 'function')
|
|
695
|
+
{
|
|
696
|
+
throw new Error('TriggerManager requires a SQL adapter');
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
const sql = this._buildCreateSQL(def, adapterType);
|
|
700
|
+
await adapter.execute({ raw: sql });
|
|
701
|
+
log('trigger created', name);
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
/**
|
|
705
|
+
* Create all defined triggers.
|
|
706
|
+
*
|
|
707
|
+
* @returns {Promise<string[]>} Names of created triggers.
|
|
708
|
+
*/
|
|
709
|
+
async createAll()
|
|
710
|
+
{
|
|
711
|
+
const names = [];
|
|
712
|
+
for (const name of this._triggers.keys())
|
|
713
|
+
{
|
|
714
|
+
await this.create(name);
|
|
715
|
+
names.push(name);
|
|
716
|
+
}
|
|
717
|
+
return names;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
/**
|
|
721
|
+
* Drop a trigger.
|
|
722
|
+
*
|
|
723
|
+
* @param {string} name - Trigger name.
|
|
724
|
+
* @param {object} [options] - Drop options.
|
|
725
|
+
* @param {string} [options.table] - Table name (required for MySQL).
|
|
726
|
+
* @param {boolean} [options.ifExists=true] - Add IF EXISTS clause.
|
|
727
|
+
* @returns {Promise<void>}
|
|
728
|
+
*/
|
|
729
|
+
async drop(name, options = {})
|
|
730
|
+
{
|
|
731
|
+
const adapter = this.db.adapter;
|
|
732
|
+
const ifExists = options.ifExists !== false;
|
|
733
|
+
const adapterType = this._detectAdapter(adapter);
|
|
734
|
+
const def = this._triggers.get(name);
|
|
735
|
+
const table = options.table || (def && def.table);
|
|
736
|
+
|
|
737
|
+
if (typeof adapter.dropTrigger === 'function')
|
|
738
|
+
{
|
|
739
|
+
return adapter.dropTrigger(name, options);
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
if (typeof adapter.execute !== 'function')
|
|
743
|
+
{
|
|
744
|
+
throw new Error('TriggerManager requires a SQL adapter');
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
let sql;
|
|
748
|
+
if (adapterType === 'mysql')
|
|
749
|
+
{
|
|
750
|
+
sql = `DROP TRIGGER ${ifExists ? 'IF EXISTS ' : ''}\`${name}\``;
|
|
751
|
+
}
|
|
752
|
+
else if (adapterType === 'postgres')
|
|
753
|
+
{
|
|
754
|
+
if (!table) throw new Error('PostgreSQL requires table name to drop trigger');
|
|
755
|
+
sql = `DROP TRIGGER ${ifExists ? 'IF EXISTS ' : ''}"${name}" ON "${table}"`;
|
|
756
|
+
}
|
|
757
|
+
else if (adapterType === 'sqlite')
|
|
758
|
+
{
|
|
759
|
+
sql = `DROP TRIGGER ${ifExists ? 'IF EXISTS ' : ''}"${name}"`;
|
|
760
|
+
}
|
|
761
|
+
else
|
|
762
|
+
{
|
|
763
|
+
sql = `DROP TRIGGER ${ifExists ? 'IF EXISTS ' : ''}"${name}"`;
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
await adapter.execute({ raw: sql });
|
|
767
|
+
this._triggers.delete(name);
|
|
768
|
+
log('trigger dropped', name);
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
/**
|
|
772
|
+
* List all defined trigger names.
|
|
773
|
+
*
|
|
774
|
+
* @returns {string[]}
|
|
775
|
+
*/
|
|
776
|
+
list()
|
|
777
|
+
{
|
|
778
|
+
return [...this._triggers.keys()];
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
/**
|
|
782
|
+
* Get a trigger definition by name.
|
|
783
|
+
*
|
|
784
|
+
* @param {string} name - Trigger name.
|
|
785
|
+
* @returns {object|undefined}
|
|
786
|
+
*/
|
|
787
|
+
get(name)
|
|
788
|
+
{
|
|
789
|
+
return this._triggers.get(name);
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
/**
|
|
793
|
+
* @private
|
|
794
|
+
*/
|
|
795
|
+
_detectAdapter(adapter)
|
|
796
|
+
{
|
|
797
|
+
const name = (adapter.constructor.name || '').toLowerCase();
|
|
798
|
+
if (name.includes('mysql')) return 'mysql';
|
|
799
|
+
if (name.includes('postgres') || name.includes('pg')) return 'postgres';
|
|
800
|
+
if (name.includes('sqlite')) return 'sqlite';
|
|
801
|
+
return 'unknown';
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
/**
|
|
805
|
+
* @private
|
|
806
|
+
*/
|
|
807
|
+
_buildCreateSQL(def, adapterType)
|
|
808
|
+
{
|
|
809
|
+
if (adapterType === 'mysql')
|
|
810
|
+
{
|
|
811
|
+
const when = def.when ? `\n WHEN (${def.when})` : '';
|
|
812
|
+
return `CREATE TRIGGER \`${def.name}\` ${def.timing} ${def.event}\nON \`${def.table}\` FOR EACH ${def.forEach}${when}\nBEGIN\n${def.body}\nEND`;
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
if (adapterType === 'postgres')
|
|
816
|
+
{
|
|
817
|
+
// PostgreSQL triggers need a function
|
|
818
|
+
const funcName = `${def.name}_fn`;
|
|
819
|
+
const when = def.when ? `\n WHEN (${def.when})` : '';
|
|
820
|
+
return (
|
|
821
|
+
`CREATE OR REPLACE FUNCTION "${funcName}"() RETURNS TRIGGER AS $$\nBEGIN\n${def.body}\nRETURN NEW;\nEND;\n$$ LANGUAGE plpgsql;\n\n` +
|
|
822
|
+
`CREATE TRIGGER "${def.name}" ${def.timing} ${def.event}\nON "${def.table}" FOR EACH ${def.forEach}${when}\nEXECUTE FUNCTION "${funcName}"()`
|
|
823
|
+
);
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
if (adapterType === 'sqlite')
|
|
827
|
+
{
|
|
828
|
+
const when = def.when ? `\n WHEN ${def.when}` : '';
|
|
829
|
+
return `CREATE TRIGGER IF NOT EXISTS "${def.name}" ${def.timing} ${def.event}\nON "${def.table}" FOR EACH ${def.forEach}${when}\nBEGIN\n${def.body}\nEND`;
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
throw new Error(`Triggers not supported for adapter: ${adapterType}`);
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
module.exports = { StoredProcedure, StoredFunction, TriggerManager };
|