json-rest-api 1.0.0 → 1.0.2

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.
Files changed (67) hide show
  1. package/.claude/settings.local.json +16 -0
  2. package/LICENSE +674 -0
  3. package/README.md +655 -309
  4. package/RELATIONSHIPS.md +1013 -0
  5. package/TODO_rest-api-plugin-extras.js +142 -0
  6. package/connectors_ideas/express-plugin.js +120 -0
  7. package/connectors_ideas/fastify.js +219 -0
  8. package/connectors_ideas/http-plugin.js +138 -0
  9. package/connectors_ideas/koa-plugin.js +113 -0
  10. package/docs/fileUploads.md +722 -0
  11. package/docs/mini-guides/file-uploads-complete.md +159 -0
  12. package/examples/.gitkeep +0 -0
  13. package/examples/express-example.js +262 -0
  14. package/examples/express-plugin.js +366 -0
  15. package/examples/file-upload-example.js +205 -0
  16. package/examples/filename-strategies.js +140 -0
  17. package/examples/knex-basic.js +66 -0
  18. package/index.js +31 -56
  19. package/lib/payload-validators.js +685 -0
  20. package/lib/rest-api-errors.js +58 -0
  21. package/package.json +41 -14
  22. package/payloadValdators.js +686 -0
  23. package/plugins/core/connectors/express-plugin.js +449 -0
  24. package/plugins/core/connectors/http-plugin.js +450 -0
  25. package/plugins/core/file-handling-plugin.js +338 -0
  26. package/plugins/core/lib/busboy-detector.js +167 -0
  27. package/plugins/core/lib/formidable-detector.js +130 -0
  28. package/plugins/core/rest-api-knex-plugin.js +200 -0
  29. package/plugins/core/rest-api-plugin.js +1419 -0
  30. package/plugins/storage/local-storage.js +218 -0
  31. package/plugins/storage/s3-storage.js +125 -0
  32. package/test-examples/package-lock.json +906 -0
  33. package/test-examples/package.json +15 -0
  34. package/test-examples/test-basic-api.js +11 -0
  35. package/test-examples/test-books-resource.js +116 -0
  36. package/test-examples/test-complete-file-upload.js +140 -0
  37. package/test-examples/test-debug.js +28 -0
  38. package/test-examples/test-express-plugin.js +177 -0
  39. package/test-examples/test-express-simple.js +28 -0
  40. package/test-examples/test-express-with-import.js +71 -0
  41. package/test-examples/test-file-upload.js +132 -0
  42. package/test-examples/test-http-plugin.js +159 -0
  43. package/test-examples/test-programmatic-api.js +188 -0
  44. package/test-examples/test-rest-api-plugin.js +13 -0
  45. package/TODO.todo +0 -113
  46. package/api.js +0 -628
  47. package/docs/API.md +0 -1306
  48. package/docs/FIXES_APPLIED.md +0 -59
  49. package/docs/GUIDES.md +0 -1792
  50. package/docs/RESOURCES_IN_EXPRESS.md +0 -456
  51. package/docs/example-versioning.js +0 -168
  52. package/docs/example.js +0 -195
  53. package/plugins/API guide.md +0 -364
  54. package/plugins/api-registry.js +0 -142
  55. package/plugins/api-versioning.md +0 -135
  56. package/plugins/http.js +0 -273
  57. package/plugins/logging.js +0 -255
  58. package/plugins/memory.js +0 -182
  59. package/plugins/mysql.js +0 -753
  60. package/plugins/openapi.js +0 -401
  61. package/plugins/positioning.js +0 -220
  62. package/plugins/security.js +0 -261
  63. package/plugins/validation.js +0 -173
  64. package/plugins/versioning.js +0 -288
  65. package/resource-helper.js +0 -79
  66. package/schema.js +0 -481
  67. package/test-basic.js +0 -107
package/plugins/mysql.js DELETED
@@ -1,753 +0,0 @@
1
- import mysql from 'mysql2/promise';
2
-
3
- /**
4
- * MySQL storage plugin for JSON REST API with full feature parity
5
- */
6
- export const MySQLPlugin = {
7
- install(api, options = {}) {
8
- // Initialize connection pools
9
- api.mysqlPools = new Map();
10
- api.mysqlConfig = options;
11
-
12
- // Create connection pools
13
- if (options.connections) {
14
- for (const conn of options.connections) {
15
- const pool = mysql.createPool(conn.config);
16
- api.mysqlPools.set(conn.name || 'default', {
17
- pool,
18
- options: conn.options || {}
19
- });
20
- }
21
- } else if (options.connection) {
22
- // Single connection config
23
- const pool = mysql.createPool(options.connection);
24
- api.mysqlPools.set('default', { pool, options: {} });
25
- }
26
-
27
- // Helper to get current connection
28
- api.getConnection = (connectionName = 'default') => {
29
- const conn = api.mysqlPools.get(connectionName);
30
- if (!conn) {
31
- throw new Error(`Connection '${connectionName}' not found`);
32
- }
33
- return conn;
34
- };
35
-
36
- // Add hooks for advanced querying
37
- api.hook('beforeQuery', async (context) => {
38
- const { options } = context;
39
- if (options.queryFieldsAndJoins && typeof options.queryFieldsAndJoins === 'function') {
40
- const customQuery = await options.queryFieldsAndJoins(context);
41
- if (customQuery) {
42
- context.customQuery = customQuery;
43
- }
44
- }
45
- });
46
-
47
- api.hook('beforeUpdate', async (context) => {
48
- const { options } = context;
49
- if (options.updateConditionsAndArgs && typeof options.updateConditionsAndArgs === 'function') {
50
- const customConditions = await options.updateConditionsAndArgs(context);
51
- if (customConditions) {
52
- context.customConditions = customConditions;
53
- }
54
- }
55
- });
56
-
57
- api.hook('beforeDelete', async (context) => {
58
- const { options } = context;
59
- if (options.deleteConditionsAndArgs && typeof options.deleteConditionsAndArgs === 'function') {
60
- const customConditions = await options.deleteConditionsAndArgs(context);
61
- if (customConditions) {
62
- context.customConditions = customConditions;
63
- }
64
- }
65
- });
66
-
67
- // Implement CRUD operations
68
- api.implement('get', async (context) => {
69
- const { id, options } = context;
70
- const { pool } = api.getConnection(options.connection);
71
- const table = options.table || options.type;
72
- const idProperty = options.idProperty || api.options.idProperty;
73
-
74
- const [rows] = await pool.query(
75
- `SELECT * FROM ?? WHERE ?? = ?`,
76
- [table, idProperty, id]
77
- );
78
-
79
- return rows[0] || null;
80
- });
81
-
82
- api.implement('query', async (context) => {
83
- const { params, options } = context;
84
- const { pool } = api.getConnection(options.connection);
85
- const table = options.table || options.type;
86
-
87
- // Check for custom query from queryFieldsAndJoins hook
88
- if (context.customQuery) {
89
- const { sql, params: customParams, countSql } = context.customQuery;
90
-
91
- // Execute count query
92
- const [countResult] = await pool.query(countSql || sql.replace(/SELECT .* FROM/, 'SELECT COUNT(*) as total FROM'), customParams);
93
- const total = countResult[0].total;
94
-
95
- // Execute main query
96
- const [rows] = await pool.query(sql, customParams);
97
-
98
- const pageSize = Number(params.page?.size) || 10;
99
- const pageNumber = Number(params.page?.number) || 1;
100
-
101
- return {
102
- results: rows,
103
- meta: {
104
- total,
105
- pageSize,
106
- pageNumber,
107
- totalPages: Math.ceil(total / pageSize)
108
- }
109
- };
110
- }
111
-
112
- // Default query building
113
- let query = `SELECT * FROM ??`;
114
- const queryParams = [table];
115
- const conditions = [];
116
-
117
- // Build filter conditions with support for operators
118
- if (params.filter) {
119
- for (const [key, value] of Object.entries(params.filter)) {
120
- if (value === null) {
121
- conditions.push(`?? IS NULL`);
122
- queryParams.push(key);
123
- } else if (typeof value === 'object' && value !== null) {
124
- // Support for operators like $gt, $lt, $like, etc.
125
- for (const [op, val] of Object.entries(value)) {
126
- switch (op) {
127
- case '$gt':
128
- conditions.push(`?? > ?`);
129
- queryParams.push(key, val);
130
- break;
131
- case '$gte':
132
- conditions.push(`?? >= ?`);
133
- queryParams.push(key, val);
134
- break;
135
- case '$lt':
136
- conditions.push(`?? < ?`);
137
- queryParams.push(key, val);
138
- break;
139
- case '$lte':
140
- conditions.push(`?? <= ?`);
141
- queryParams.push(key, val);
142
- break;
143
- case '$ne':
144
- conditions.push(`?? != ?`);
145
- queryParams.push(key, val);
146
- break;
147
- case '$like':
148
- conditions.push(`?? LIKE ?`);
149
- queryParams.push(key, val);
150
- break;
151
- case '$in':
152
- if (Array.isArray(val) && val.length > 0) {
153
- conditions.push(`?? IN (${val.map(() => '?').join(',')})`);
154
- queryParams.push(key, ...val);
155
- }
156
- break;
157
- case '$nin':
158
- if (Array.isArray(val) && val.length > 0) {
159
- conditions.push(`?? NOT IN (${val.map(() => '?').join(',')})`);
160
- queryParams.push(key, ...val);
161
- }
162
- break;
163
- }
164
- }
165
- } else {
166
- conditions.push(`?? = ?`);
167
- queryParams.push(key, value);
168
- }
169
- }
170
- }
171
-
172
- // Build search conditions
173
- if (params.search && options.searchFields) {
174
- const searchConditions = options.searchFields.map(field => {
175
- queryParams.push(field, `%${params.search}%`);
176
- return `?? LIKE ?`;
177
- });
178
- if (searchConditions.length > 0) {
179
- conditions.push(`(${searchConditions.join(' OR ')})`);
180
- }
181
- }
182
-
183
- // Add WHERE clause
184
- if (conditions.length > 0) {
185
- query += ` WHERE ${conditions.join(' AND ')}`;
186
- }
187
-
188
- // Count total rows
189
- const countQuery = query.replace('SELECT *', 'SELECT COUNT(*) as total');
190
- const [countResult] = await pool.query(countQuery, queryParams);
191
- const total = countResult[0].total;
192
-
193
- // Add sorting with multiple field support
194
- if (params.sort) {
195
- const orderBy = params.sort.split(',').map(field => {
196
- const desc = field.startsWith('-');
197
- const cleanField = field.replace(/^-/, '');
198
- queryParams.push(cleanField);
199
- return `?? ${desc ? 'DESC' : 'ASC'}`;
200
- });
201
- query += ` ORDER BY ${orderBy.join(', ')}`;
202
- }
203
-
204
- // Add pagination
205
- const pageSize = Number(params.page?.size) || 10;
206
- const pageNumber = Number(params.page?.number) || 1;
207
- const offset = (pageNumber - 1) * pageSize;
208
-
209
- query += ` LIMIT ? OFFSET ?`;
210
- queryParams.push(pageSize, offset);
211
-
212
- // Execute query
213
- const [rows] = await pool.query(query, queryParams);
214
-
215
- return {
216
- results: rows,
217
- meta: {
218
- total,
219
- pageSize,
220
- pageNumber,
221
- totalPages: Math.ceil(total / pageSize)
222
- }
223
- };
224
- });
225
-
226
- api.implement('insert', async (context) => {
227
- const { data, options } = context;
228
- const { pool } = api.getConnection(options.connection);
229
- const table = options.table || options.type;
230
- const idProperty = options.idProperty || api.options.idProperty;
231
-
232
- // Handle positioning if enabled
233
- if (options.positionField && data[options.beforeIdField] !== undefined) {
234
- await handleMySQLPositioning(pool, table, data, options);
235
- }
236
-
237
- // Prepare insert data
238
- const insertData = { ...data };
239
- delete insertData[options.beforeIdField]; // Remove virtual field
240
-
241
- // Build insert query
242
- const fields = Object.keys(insertData);
243
- const placeholders = fields.map(() => '?').join(', ');
244
- const values = fields.map(field => insertData[field]);
245
-
246
- const [result] = await pool.query(
247
- `INSERT INTO ?? (${fields.map(() => '??').join(', ')}) VALUES (${placeholders})`,
248
- [table, ...fields, ...values]
249
- );
250
-
251
- // Return the inserted record
252
- if (result.insertId) {
253
- insertData[idProperty] = result.insertId;
254
- }
255
-
256
- return insertData;
257
- });
258
-
259
- api.implement('update', async (context) => {
260
- const { id, data, options } = context;
261
- const { pool } = api.getConnection(options.connection);
262
- const table = options.table || options.type;
263
- const idProperty = options.idProperty || api.options.idProperty;
264
-
265
- // Handle custom conditions from updateConditionsAndArgs hook
266
- let whereClause = `?? = ?`;
267
- let whereParams = [idProperty, id];
268
-
269
- if (context.customConditions) {
270
- const { conditions, params } = context.customConditions;
271
- whereClause = conditions;
272
- whereParams = params;
273
- }
274
-
275
- // Handle positioning if enabled
276
- if (options.positionField && data[options.beforeIdField] !== undefined) {
277
- await handleMySQLPositioning(pool, table, { ...data, [idProperty]: id }, options);
278
- }
279
-
280
- // Prepare update data
281
- const updateData = { ...data };
282
- delete updateData[options.beforeIdField]; // Remove virtual field
283
- delete updateData[idProperty]; // Don't update ID
284
-
285
- // Build update query
286
- const fields = Object.keys(updateData);
287
- if (fields.length === 0) {
288
- // Nothing to update
289
- return context.result || { [idProperty]: id };
290
- }
291
-
292
- const setClause = fields.map(() => '?? = ?').join(', ');
293
- const values = [];
294
- fields.forEach(field => {
295
- values.push(field, updateData[field]);
296
- });
297
-
298
- await pool.query(
299
- `UPDATE ?? SET ${setClause} WHERE ${whereClause}`,
300
- [table, ...values, ...whereParams]
301
- );
302
-
303
- // Fetch and return updated record
304
- const [rows] = await pool.query(
305
- `SELECT * FROM ?? WHERE ?? = ?`,
306
- [table, idProperty, id]
307
- );
308
-
309
- return rows[0] || null;
310
- });
311
-
312
- api.implement('delete', async (context) => {
313
- const { id, options } = context;
314
- const { pool } = api.getConnection(options.connection);
315
- const table = options.table || options.type;
316
- const idProperty = options.idProperty || api.options.idProperty;
317
-
318
- // Handle custom conditions from deleteConditionsAndArgs hook
319
- let whereClause = `?? = ?`;
320
- let whereParams = [idProperty, id];
321
-
322
- if (context.customConditions) {
323
- const { conditions, params } = context.customConditions;
324
- whereClause = conditions;
325
- whereParams = params;
326
- }
327
-
328
- await pool.query(
329
- `DELETE FROM ?? WHERE ${whereClause}`,
330
- [table, ...whereParams]
331
- );
332
- });
333
-
334
- // Add schema sync functionality
335
- api.syncSchema = async (schema, table, options = {}) => {
336
- try {
337
- const { pool } = api.getConnection(options.connection);
338
- // Merge options with API options
339
- const syncOptions = {
340
- idProperty: api.options.idProperty || 'id',
341
- positionField: api.options.positionField,
342
- dbExtraIndexes: api.options.dbExtraIndexes || [],
343
- stores: api.resources, // For foreign key references
344
- ...options
345
- };
346
- await syncMySQLSchema(pool, schema, table, syncOptions);
347
- } catch (error) {
348
- console.error(`Failed to sync schema for table ${table}:`, error);
349
- throw new Error(`Schema sync failed: ${error.message}`);
350
- }
351
- };
352
- }
353
- };
354
-
355
- /**
356
- * Handle MySQL record positioning with filtering support
357
- */
358
- async function handleMySQLPositioning(pool, table, record, options) {
359
- const { positionField, beforeIdField, idProperty, positionFilter } = options;
360
- const beforeId = record[beforeIdField];
361
-
362
- // Build filter conditions for positioning
363
- let filterCondition = '';
364
- const filterParams = [];
365
-
366
- if (positionFilter && typeof positionFilter === 'object') {
367
- const conditions = [];
368
- for (const [key, value] of Object.entries(positionFilter)) {
369
- conditions.push(`?? = ?`);
370
- filterParams.push(key, value);
371
- }
372
- if (conditions.length > 0) {
373
- filterCondition = ` AND ${conditions.join(' AND ')}`;
374
- }
375
- }
376
-
377
- if (beforeId === null) {
378
- // Place at end
379
- let maxQuery = `SELECT MAX(??) as maxPosition FROM ??`;
380
- const maxParams = [positionField, table];
381
-
382
- if (filterCondition) {
383
- maxQuery += ` WHERE 1=1 ${filterCondition}`;
384
- maxParams.push(...filterParams);
385
- }
386
-
387
- const [maxResult] = await pool.query(maxQuery, maxParams);
388
- record[positionField] = (maxResult[0].maxPosition || 0) + 1;
389
- } else if (beforeId !== undefined) {
390
- // Get position of the record to place before
391
- const [beforeResult] = await pool.query(
392
- `SELECT ?? FROM ?? WHERE ?? = ?`,
393
- [positionField, table, idProperty, beforeId]
394
- );
395
-
396
- if (beforeResult[0]) {
397
- const position = beforeResult[0][positionField];
398
-
399
- // Shift positions with filter support
400
- let shiftQuery = `UPDATE ?? SET ?? = ?? + 1 WHERE ?? >= ?`;
401
- const shiftParams = [table, positionField, positionField, positionField, position];
402
-
403
- if (filterCondition) {
404
- shiftQuery += filterCondition;
405
- shiftParams.push(...filterParams);
406
- }
407
-
408
- await pool.query(shiftQuery, shiftParams);
409
-
410
- record[positionField] = position;
411
- }
412
- }
413
- }
414
-
415
- /**
416
- * Sync MySQL schema with comprehensive features
417
- */
418
- async function syncMySQLSchema(pool, schema, table, options = {}) {
419
- const idProperty = options.idProperty || 'id';
420
- const positionField = options.positionField;
421
- const dbExtraIndexes = options.dbExtraIndexes || [];
422
-
423
- // Check if table exists
424
- const [tables] = await pool.query(
425
- `SHOW TABLES LIKE ?`,
426
- [table]
427
- );
428
-
429
- const tableAlreadyExists = tables.length > 0;
430
-
431
- // Create table with dummy column if it doesn't exist
432
- if (!tableAlreadyExists) {
433
- await pool.query(`CREATE TABLE \`${table}\` (__dummy__ INT(1))`);
434
- }
435
-
436
- // Get comprehensive table information
437
- const [columns] = await pool.query(
438
- `SELECT * FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ?`,
439
- [table]
440
- );
441
-
442
- const [indexes] = await pool.query(`SHOW INDEX FROM \`${table}\``);
443
-
444
- const [constraints] = await pool.query(
445
- `SELECT * FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS WHERE CONSTRAINT_SCHEMA = DATABASE() AND TABLE_NAME = ?`,
446
- [table]
447
- );
448
-
449
- // Create columns hash for easy lookup
450
- const columnsHash = columns.reduce((map, column) => {
451
- map[column.COLUMN_NAME] = column;
452
- return map;
453
- }, {});
454
-
455
- const primaryKeyColumn = columns.find(el => el.COLUMN_KEY === 'PRI');
456
-
457
- // Convert schema to array format for easier processing
458
- const schemaFieldsAsArray = Object.keys(schema.structure).map(k => ({
459
- ...schema.structure[k],
460
- name: k
461
- }));
462
-
463
- // Determine auto-increment field
464
- let autoIncrementField = schemaFieldsAsArray.find(el => el.autoIncrement);
465
- if (!autoIncrementField) {
466
- autoIncrementField = schemaFieldsAsArray.find(el => el.name === idProperty);
467
- }
468
- const autoIncrementFieldName = autoIncrementField ? autoIncrementField.name : idProperty;
469
-
470
- // Handle primary key changes if needed
471
- if (primaryKeyColumn) {
472
- await maybeChangePrimaryKey(pool, table, primaryKeyColumn, idProperty, schemaFieldsAsArray);
473
- }
474
-
475
- // Process columns
476
- const dbIndexes = [];
477
- const foreignEndpoints = [];
478
-
479
- for (let i = 0; i < schemaFieldsAsArray.length; i++) {
480
- const field = schemaFieldsAsArray[i];
481
- const creatingNewColumn = !columnsHash[field.name];
482
-
483
- await processColumn(
484
- pool, table, field, i, schemaFieldsAsArray,
485
- creatingNewColumn, idProperty, autoIncrementFieldName,
486
- columnsHash, dbIndexes, foreignEndpoints, positionField
487
- );
488
- }
489
-
490
- // Add extra indexes
491
- if (dbExtraIndexes.length > 0) {
492
- for (const ei of dbExtraIndexes) {
493
- dbIndexes.push({
494
- column: ei.column,
495
- unique: ei.unique,
496
- name: ei.name
497
- });
498
- }
499
- }
500
-
501
- // Remove dummy column if it exists
502
- if (columnsHash.__dummy__) {
503
- await pool.query(`ALTER TABLE \`${table}\` DROP COLUMN \`__dummy__\``);
504
- }
505
-
506
- // Create indexes
507
- await createIndexes(pool, table, dbIndexes, indexes);
508
-
509
- // Create foreign key constraints
510
- await createForeignKeyConstraints(pool, table, foreignEndpoints, constraints, options);
511
- }
512
-
513
- /**
514
- * Get MySQL column definition from schema field
515
- */
516
- function getMySQLColumnDefinition(fieldName, definition, options = {}) {
517
- let sqlType;
518
- let length = 256;
519
-
520
- // Allow custom DB type override
521
- if (definition.dbType) {
522
- sqlType = definition.dbType;
523
- } else {
524
- switch (definition.type) {
525
- case 'id':
526
- sqlType = 'INT';
527
- break;
528
- case 'number':
529
- if (definition.float) sqlType = 'FLOAT';
530
- else if (definition.currency) sqlType = 'NUMERIC(10,2)';
531
- else if (definition.longInt) sqlType = 'BIGINT';
532
- else sqlType = 'INT';
533
- break;
534
- case 'string':
535
- if (definition.length) length = definition.length;
536
- if (definition.max) length = definition.max;
537
- if (definition.asText || definition.text) sqlType = 'TEXT';
538
- else sqlType = `VARCHAR(${length})`;
539
- break;
540
- case 'boolean':
541
- sqlType = 'TINYINT';
542
- break;
543
- case 'timestamp':
544
- sqlType = 'BIGINT';
545
- break;
546
- case 'date':
547
- sqlType = 'DATE';
548
- break;
549
- case 'dateTime':
550
- sqlType = 'DATETIME';
551
- break;
552
- case 'blob':
553
- if (definition.length) length = definition.length;
554
- sqlType = `BLOB`;
555
- break;
556
- case 'object':
557
- case 'array':
558
- case 'serialize':
559
- sqlType = 'JSON';
560
- break;
561
- default:
562
- if (definition.type) {
563
- throw new Error(`${definition.type} not converted automatically. Use dbType instead`);
564
- }
565
- sqlType = 'VARCHAR(255)';
566
- }
567
- }
568
-
569
- // NULL clause - support both 'required' and 'canBeNull'
570
- const nullable = definition.required || !definition.canBeNull ? 'NOT NULL' : 'NULL';
571
-
572
- // Default value, giving priority to dbDefault
573
- let defaultValue = '';
574
- if (typeof definition.dbDefault !== 'undefined') {
575
- defaultValue = ` DEFAULT ${mysql.escape(definition.dbDefault)}`;
576
- } else if (typeof definition.default !== 'undefined') {
577
- defaultValue = ` DEFAULT ${mysql.escape(definition.default)}`;
578
- }
579
-
580
- // AUTO_INCREMENT clause
581
- const autoIncrement = (definition.autoIncrement || fieldName === options.idProperty) && !options.skipAutoIncrement ? 'AUTO_INCREMENT' : '';
582
-
583
- return `\`${fieldName}\` ${sqlType} ${nullable}${defaultValue} ${autoIncrement}`.trim();
584
- }
585
-
586
- /**
587
- * Process a single column during schema sync
588
- */
589
- async function processColumn(
590
- pool, table, field, index, schemaFieldsAsArray,
591
- creatingNewColumn, idProperty, autoIncrementFieldName,
592
- columnsHash, dbIndexes, foreignEndpoints, positionField
593
- ) {
594
- const changeOrAddStatement = creatingNewColumn ? 'ADD COLUMN' : `CHANGE \`${field.name}\``;
595
-
596
- const def = getMySQLColumnDefinition(field.name, field, {
597
- idProperty,
598
- skipAutoIncrement: autoIncrementFieldName !== field.name
599
- });
600
-
601
- // Handle primary key for new columns
602
- const maybePrimaryKey = (creatingNewColumn && field.name === idProperty) ? 'PRIMARY KEY' : '';
603
-
604
- // Handle column ordering
605
- const maybeAfter = index > 0 ? `AFTER \`${schemaFieldsAsArray[index - 1].name}\`` : '';
606
-
607
- const sqlQuery = `ALTER TABLE \`${table}\` ${changeOrAddStatement} ${def} ${maybePrimaryKey} ${maybeAfter}`;
608
- await pool.query(sqlQuery);
609
-
610
- // Collect index information
611
- if (field.dbIndex || field.searchable || field.name === positionField) {
612
- if (columnsHash[field.name] || creatingNewColumn) {
613
- if (field.name !== idProperty) {
614
- dbIndexes.push({
615
- column: field.name,
616
- unique: field.dbUnique,
617
- name: field.dbIndexName || `jrs_${field.name}`
618
- });
619
- }
620
- }
621
- }
622
-
623
- // Collect foreign key information
624
- if (field.foreignEndpoint) {
625
- foreignEndpoints.push({
626
- sourceField: field.name,
627
- ...field.foreignEndpoint
628
- });
629
- }
630
- }
631
-
632
- /**
633
- * Handle primary key changes during schema sync
634
- */
635
- async function maybeChangePrimaryKey(pool, table, primaryKeyColumn, idProperty, schemaFieldsAsArray) {
636
- // If primary key hasn't changed, don't do anything
637
- if (primaryKeyColumn && primaryKeyColumn.COLUMN_NAME === idProperty) {
638
- return;
639
- }
640
-
641
- const oldPrimaryKeyColumnName = primaryKeyColumn.COLUMN_NAME;
642
-
643
- // Remove AUTO_INCREMENT from old primary key if present
644
- if (primaryKeyColumn.EXTRA === 'auto_increment') {
645
- await pool.query('SET foreign_key_checks = 0');
646
- const pkc = primaryKeyColumn;
647
- const defWithoutAutoIncrement = `\`${pkc.COLUMN_NAME}\` ${pkc.COLUMN_TYPE} ${pkc.IS_NULLABLE === 'YES' ? 'NULL' : 'NOT NULL'} ${pkc.COLUMN_DEFAULT !== null ? 'DEFAULT ' + mysql.escape(pkc.COLUMN_DEFAULT) : ''}`;
648
- await pool.query(`ALTER TABLE \`${table}\` CHANGE \`${oldPrimaryKeyColumnName}\` ${defWithoutAutoIncrement}`);
649
- await pool.query('SET foreign_key_checks = 1');
650
- }
651
-
652
- // Ensure old primary key has an index
653
- const [indexIsThere] = await pool.query(
654
- `SHOW INDEX FROM \`${table}\` WHERE Key_name <> 'PRIMARY' AND Seq_in_index = 1 AND Column_name = ?`,
655
- [oldPrimaryKeyColumnName]
656
- );
657
-
658
- if (indexIsThere.length === 0) {
659
- const field = schemaFieldsAsArray.find(def => def.name === oldPrimaryKeyColumnName);
660
- const dbIndex = field?.dbIndex || `jrs_${oldPrimaryKeyColumnName}`;
661
- await pool.query(`ALTER TABLE \`${table}\` ADD INDEX \`${dbIndex}\`(\`${oldPrimaryKeyColumnName}\`)`);
662
- }
663
-
664
- // Drop old primary key and add new one
665
- await pool.query(`ALTER TABLE \`${table}\` DROP PRIMARY KEY, ADD PRIMARY KEY (\`${idProperty}\`)`);
666
- return true;
667
- }
668
-
669
- /**
670
- * Create indexes during schema sync
671
- */
672
- async function createIndexes(pool, table, dbIndexes, existingIndexes) {
673
- for (const dbi of dbIndexes) {
674
- // Handle multiple columns
675
- let columns;
676
- if (!Array.isArray(dbi.column)) {
677
- columns = `\`${dbi.column}\``;
678
- } else {
679
- columns = dbi.column.map(c => `\`${c}\``).join(',');
680
- }
681
-
682
- // Generate index name if not provided
683
- let indexName = dbi.name;
684
- if (!indexName) {
685
- if (!Array.isArray(dbi.column)) {
686
- indexName = `jrs_${dbi.column}`;
687
- } else {
688
- indexName = 'jrs_' + dbi.column.join('_');
689
- }
690
- }
691
-
692
- // Skip if index already exists
693
- if (existingIndexes.find(i => i.Key_name === indexName)) {
694
- continue;
695
- }
696
-
697
- const sqlQuery = `ALTER TABLE \`${table}\` ADD ${dbi.unique ? 'UNIQUE' : ''} INDEX \`${indexName}\` (${columns})`;
698
- await pool.query(sqlQuery);
699
- }
700
- }
701
-
702
- /**
703
- * Create foreign key constraints during schema sync
704
- */
705
- async function createForeignKeyConstraints(pool, table, foreignEndpoints, existingConstraints, options) {
706
- const stores = options.stores || {};
707
-
708
- for (const fe of foreignEndpoints) {
709
- let foreignTable;
710
- let foreignField;
711
- let constraintName;
712
-
713
- // Determine foreign table
714
- if (fe.foreignTable) {
715
- foreignTable = fe.foreignTable;
716
- } else if (fe.endpointName && stores[fe.endpointName]) {
717
- foreignTable = stores[fe.endpointName].table;
718
- } else {
719
- throw new Error('Cannot find the foreign table for field: ' + fe.sourceField);
720
- }
721
-
722
- // Determine foreign field
723
- if (fe.foreignField) {
724
- foreignField = fe.foreignField;
725
- } else if (fe.endpointName && stores[fe.endpointName]) {
726
- foreignField = stores[fe.endpointName].idProperty || 'id';
727
- } else {
728
- foreignField = 'id';
729
- }
730
-
731
- // Generate constraint name
732
- if (fe.constraintName) {
733
- constraintName = fe.constraintName;
734
- } else {
735
- constraintName = `jra_${fe.sourceField}_to_${foreignTable}_${foreignField}`;
736
- }
737
-
738
- // Skip if constraint already exists
739
- if (existingConstraints.find(c => c.CONSTRAINT_NAME === constraintName)) {
740
- continue;
741
- }
742
-
743
- const sqlQuery = `
744
- ALTER TABLE \`${table}\`
745
- ADD CONSTRAINT \`${constraintName}\`
746
- FOREIGN KEY (\`${fe.sourceField}\`)
747
- REFERENCES \`${foreignTable}\` (\`${foreignField}\`)
748
- ON DELETE NO ACTION
749
- ON UPDATE NO ACTION`;
750
-
751
- await pool.query(sqlQuery);
752
- }
753
- }