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.
- package/.claude/settings.local.json +16 -0
- package/LICENSE +674 -0
- package/README.md +655 -309
- package/RELATIONSHIPS.md +1013 -0
- package/TODO_rest-api-plugin-extras.js +142 -0
- package/connectors_ideas/express-plugin.js +120 -0
- package/connectors_ideas/fastify.js +219 -0
- package/connectors_ideas/http-plugin.js +138 -0
- package/connectors_ideas/koa-plugin.js +113 -0
- package/docs/fileUploads.md +722 -0
- package/docs/mini-guides/file-uploads-complete.md +159 -0
- package/examples/.gitkeep +0 -0
- package/examples/express-example.js +262 -0
- package/examples/express-plugin.js +366 -0
- package/examples/file-upload-example.js +205 -0
- package/examples/filename-strategies.js +140 -0
- package/examples/knex-basic.js +66 -0
- package/index.js +31 -56
- package/lib/payload-validators.js +685 -0
- package/lib/rest-api-errors.js +58 -0
- package/package.json +41 -14
- package/payloadValdators.js +686 -0
- package/plugins/core/connectors/express-plugin.js +449 -0
- package/plugins/core/connectors/http-plugin.js +450 -0
- package/plugins/core/file-handling-plugin.js +338 -0
- package/plugins/core/lib/busboy-detector.js +167 -0
- package/plugins/core/lib/formidable-detector.js +130 -0
- package/plugins/core/rest-api-knex-plugin.js +200 -0
- package/plugins/core/rest-api-plugin.js +1419 -0
- package/plugins/storage/local-storage.js +218 -0
- package/plugins/storage/s3-storage.js +125 -0
- package/test-examples/package-lock.json +906 -0
- package/test-examples/package.json +15 -0
- package/test-examples/test-basic-api.js +11 -0
- package/test-examples/test-books-resource.js +116 -0
- package/test-examples/test-complete-file-upload.js +140 -0
- package/test-examples/test-debug.js +28 -0
- package/test-examples/test-express-plugin.js +177 -0
- package/test-examples/test-express-simple.js +28 -0
- package/test-examples/test-express-with-import.js +71 -0
- package/test-examples/test-file-upload.js +132 -0
- package/test-examples/test-http-plugin.js +159 -0
- package/test-examples/test-programmatic-api.js +188 -0
- package/test-examples/test-rest-api-plugin.js +13 -0
- package/TODO.todo +0 -113
- package/api.js +0 -628
- package/docs/API.md +0 -1306
- package/docs/FIXES_APPLIED.md +0 -59
- package/docs/GUIDES.md +0 -1792
- package/docs/RESOURCES_IN_EXPRESS.md +0 -456
- package/docs/example-versioning.js +0 -168
- package/docs/example.js +0 -195
- package/plugins/API guide.md +0 -364
- package/plugins/api-registry.js +0 -142
- package/plugins/api-versioning.md +0 -135
- package/plugins/http.js +0 -273
- package/plugins/logging.js +0 -255
- package/plugins/memory.js +0 -182
- package/plugins/mysql.js +0 -753
- package/plugins/openapi.js +0 -401
- package/plugins/positioning.js +0 -220
- package/plugins/security.js +0 -261
- package/plugins/validation.js +0 -173
- package/plugins/versioning.js +0 -288
- package/resource-helper.js +0 -79
- package/schema.js +0 -481
- 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
|
-
}
|