@carbonorm/carbonnode 6.0.9 → 6.0.12
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/dist/executors/SqlExecutor.d.ts +5 -0
- package/dist/handlers/ExpressHandler.d.ts +10 -3
- package/dist/index.cjs.js +170 -78
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.esm.js +170 -79
- package/dist/index.esm.js.map +1 -1
- package/dist/types/ormInterfaces.d.ts +12 -2
- package/package.json +2 -1
- package/src/__tests__/sakila-db/C6.js +1 -1
- package/src/__tests__/sakila-db/C6.mysqldump.json +1 -1
- package/src/__tests__/sakila-db/C6.mysqldump.sql +1 -1
- package/src/__tests__/sakila-db/C6.ts +1 -1
- package/src/__tests__/sakila-db/sqlResponses/C6.actor.post.json +3 -3
- package/src/__tests__/sakila-db/sqlResponses/C6.actor.post.latest.json +3 -3
- package/src/__tests__/sakila-db/sqlResponses/C6.actor.put.json +1 -1
- package/src/__tests__/sakila-db/sqlResponses/C6.actor.put.lookup.json +3 -3
- package/src/__tests__/sakila-db/sqlResponses/C6.address.post.json +5 -5
- package/src/__tests__/sakila-db/sqlResponses/C6.address.post.latest.json +5 -5
- package/src/__tests__/sakila-db/sqlResponses/C6.address.put.json +1 -1
- package/src/__tests__/sakila-db/sqlResponses/C6.address.put.lookup.json +5 -5
- package/src/__tests__/sakila-db/sqlResponses/C6.category.post.json +2 -2
- package/src/__tests__/sakila-db/sqlResponses/C6.category.post.latest.json +2 -2
- package/src/__tests__/sakila-db/sqlResponses/C6.category.put.json +1 -1
- package/src/__tests__/sakila-db/sqlResponses/C6.category.put.lookup.json +2 -2
- package/src/__tests__/sakila-db/sqlResponses/C6.city.post.json +2 -2
- package/src/__tests__/sakila-db/sqlResponses/C6.city.post.latest.json +2 -2
- package/src/__tests__/sakila-db/sqlResponses/C6.city.put.json +1 -1
- package/src/__tests__/sakila-db/sqlResponses/C6.city.put.lookup.json +2 -2
- package/src/__tests__/sakila-db/sqlResponses/C6.country.post.json +2 -2
- package/src/__tests__/sakila-db/sqlResponses/C6.country.post.latest.json +2 -2
- package/src/__tests__/sakila-db/sqlResponses/C6.country.put.json +1 -1
- package/src/__tests__/sakila-db/sqlResponses/C6.country.put.lookup.json +2 -2
- package/src/__tests__/sakila-db/sqlResponses/C6.customer.post.json +5 -5
- package/src/__tests__/sakila-db/sqlResponses/C6.customer.post.latest.json +5 -5
- package/src/__tests__/sakila-db/sqlResponses/C6.customer.put.json +1 -1
- package/src/__tests__/sakila-db/sqlResponses/C6.customer.put.lookup.json +5 -5
- package/src/__tests__/sakila-db/sqlResponses/C6.film.post.json +2 -2
- package/src/__tests__/sakila-db/sqlResponses/C6.film.post.latest.json +2 -2
- package/src/__tests__/sakila-db/sqlResponses/C6.film.put.json +1 -1
- package/src/__tests__/sakila-db/sqlResponses/C6.film.put.lookup.json +2 -2
- package/src/__tests__/sakila-db/sqlResponses/C6.inventory.post.json +1 -1
- package/src/__tests__/sakila-db/sqlResponses/C6.inventory.post.latest.json +1 -1
- package/src/__tests__/sakila-db/sqlResponses/C6.inventory.put.json +1 -1
- package/src/__tests__/sakila-db/sqlResponses/C6.inventory.put.lookup.json +1 -1
- package/src/__tests__/sakila-db/sqlResponses/C6.language.post.json +2 -2
- package/src/__tests__/sakila-db/sqlResponses/C6.language.post.latest.json +2 -2
- package/src/__tests__/sakila-db/sqlResponses/C6.language.put.json +1 -1
- package/src/__tests__/sakila-db/sqlResponses/C6.language.put.lookup.json +2 -2
- package/src/__tests__/sakila-db/sqlResponses/C6.payment.post.json +2 -2
- package/src/__tests__/sakila-db/sqlResponses/C6.payment.post.latest.json +2 -2
- package/src/__tests__/sakila-db/sqlResponses/C6.payment.put.lookup.json +2 -2
- package/src/__tests__/sakila-db/sqlResponses/C6.rental.post.json +3 -3
- package/src/__tests__/sakila-db/sqlResponses/C6.rental.post.latest.json +3 -3
- package/src/__tests__/sakila-db/sqlResponses/C6.rental.put.json +1 -1
- package/src/__tests__/sakila-db/sqlResponses/C6.rental.put.lookup.json +3 -3
- package/src/__tests__/sakila.generated.test.ts +11 -3
- package/src/__tests__/sqlExecutorLifecycleHooks.test.ts +122 -0
- package/src/executors/SqlExecutor.ts +190 -49
- package/src/handlers/ExpressHandler.ts +22 -7
- package/src/types/ormInterfaces.ts +16 -2
|
@@ -14,10 +14,18 @@ describe('sakila-db generated C6 bindings', () => {
|
|
|
14
14
|
beforeAll(() => {
|
|
15
15
|
// Provide a mocked MySQL pool so SqlExecutor path is used without a real DB
|
|
16
16
|
const mockConn = {
|
|
17
|
-
query: vi.fn().mockImplementation(async (
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
query: vi.fn().mockImplementation(async (sql: string, _values?: any[]) => {
|
|
18
|
+
const statement = sql.trim().toUpperCase();
|
|
19
|
+
if (statement.startsWith("SELECT")) {
|
|
20
|
+
// Return a result set shaped like mysql2/promise: [rows, fields]
|
|
21
|
+
return [[{ ok: true }], []];
|
|
22
|
+
}
|
|
23
|
+
// Return a write result for POST/PUT/DELETE shaped like mysql2/promise
|
|
24
|
+
return [{ affectedRows: 1, insertId: 9999 }, []];
|
|
20
25
|
}),
|
|
26
|
+
beginTransaction: vi.fn().mockResolvedValue(undefined),
|
|
27
|
+
commit: vi.fn().mockResolvedValue(undefined),
|
|
28
|
+
rollback: vi.fn().mockResolvedValue(undefined),
|
|
21
29
|
release: vi.fn()
|
|
22
30
|
};
|
|
23
31
|
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { SqlExecutor } from '../executors/SqlExecutor';
|
|
3
|
+
import { PostQueryBuilder } from '../orm/queries/PostQueryBuilder';
|
|
4
|
+
|
|
5
|
+
describe('SqlExecutor lifecycle hooks', () => {
|
|
6
|
+
it('runs beforeExecution/afterExecution around query and afterCommit after commit', async () => {
|
|
7
|
+
const hookOrder: string[] = [];
|
|
8
|
+
let afterExecutionArgs: any;
|
|
9
|
+
let afterCommitArgs: any;
|
|
10
|
+
|
|
11
|
+
const conn: any = {
|
|
12
|
+
beginTransaction: vi.fn(async () => {
|
|
13
|
+
hookOrder.push('begin');
|
|
14
|
+
}),
|
|
15
|
+
query: vi.fn(async () => {
|
|
16
|
+
hookOrder.push('query');
|
|
17
|
+
return [{ affectedRows: 1, insertId: 42 }, []];
|
|
18
|
+
}),
|
|
19
|
+
commit: vi.fn(async () => {
|
|
20
|
+
hookOrder.push('commit');
|
|
21
|
+
}),
|
|
22
|
+
rollback: vi.fn(async () => {
|
|
23
|
+
hookOrder.push('rollback');
|
|
24
|
+
}),
|
|
25
|
+
release: vi.fn(),
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const config: any = {
|
|
29
|
+
requestMethod: 'POST',
|
|
30
|
+
mysqlPool: {
|
|
31
|
+
getConnection: vi.fn(async () => conn),
|
|
32
|
+
},
|
|
33
|
+
websocketBroadcast: vi.fn(async () => {
|
|
34
|
+
hookOrder.push('broadcast');
|
|
35
|
+
}),
|
|
36
|
+
C6: {
|
|
37
|
+
PREFIX: '',
|
|
38
|
+
},
|
|
39
|
+
restModel: {
|
|
40
|
+
TABLE_NAME: 'widgets',
|
|
41
|
+
PRIMARY: ['widgets.id'],
|
|
42
|
+
PRIMARY_SHORT: ['id'],
|
|
43
|
+
COLUMNS: {
|
|
44
|
+
'widgets.id': 'id',
|
|
45
|
+
'widgets.name': 'name',
|
|
46
|
+
},
|
|
47
|
+
LIFECYCLE_HOOKS: {
|
|
48
|
+
GET: {},
|
|
49
|
+
POST: {
|
|
50
|
+
beforeProcessing: {
|
|
51
|
+
first: async ({ request }: any) => {
|
|
52
|
+
hookOrder.push('beforeProcessing');
|
|
53
|
+
request.seed = 'from-before-processing';
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
beforeExecution: {
|
|
57
|
+
second: async ({ request }: any) => {
|
|
58
|
+
hookOrder.push('beforeExecution');
|
|
59
|
+
request.stage = 'from-before-execution';
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
afterExecution: {
|
|
63
|
+
third: async (args: any) => {
|
|
64
|
+
hookOrder.push('afterExecution');
|
|
65
|
+
afterExecutionArgs = args;
|
|
66
|
+
expect(conn.commit).toHaveBeenCalledTimes(0);
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
afterCommit: {
|
|
70
|
+
fourth: async (args: any) => {
|
|
71
|
+
hookOrder.push('afterCommit');
|
|
72
|
+
afterCommitArgs = args;
|
|
73
|
+
expect(conn.commit).toHaveBeenCalledTimes(1);
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
PUT: {},
|
|
78
|
+
DELETE: {},
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const request: any = { name: 'example' };
|
|
84
|
+
|
|
85
|
+
vi.spyOn(PostQueryBuilder.prototype as any, 'build').mockReturnValue({
|
|
86
|
+
sql: 'INSERT INTO widgets (name) VALUES (:name)',
|
|
87
|
+
params: { name: 'example' },
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const executor = new SqlExecutor<any>(config, request);
|
|
91
|
+
|
|
92
|
+
const result = await executor.execute();
|
|
93
|
+
|
|
94
|
+
expect(result).toMatchObject({
|
|
95
|
+
affected: 1,
|
|
96
|
+
insertId: 42,
|
|
97
|
+
});
|
|
98
|
+
expect(hookOrder).toEqual([
|
|
99
|
+
'beforeProcessing',
|
|
100
|
+
'begin',
|
|
101
|
+
'beforeExecution',
|
|
102
|
+
'query',
|
|
103
|
+
'afterExecution',
|
|
104
|
+
'commit',
|
|
105
|
+
'afterCommit',
|
|
106
|
+
'broadcast',
|
|
107
|
+
]);
|
|
108
|
+
|
|
109
|
+
expect((executor as any).request.seed).toBe('from-before-processing');
|
|
110
|
+
expect((executor as any).request.stage).toBe('from-before-execution');
|
|
111
|
+
|
|
112
|
+
expect(afterExecutionArgs.response.data.success).toBe(true);
|
|
113
|
+
expect(afterExecutionArgs.response.data.insertId).toBe(42);
|
|
114
|
+
expect(afterExecutionArgs.response.data.affected).toBe(1);
|
|
115
|
+
|
|
116
|
+
expect(afterCommitArgs.response.data.success).toBe(true);
|
|
117
|
+
expect(afterCommitArgs.response.data.insertId).toBe(42);
|
|
118
|
+
expect(afterCommitArgs.response.data.affected).toBe(1);
|
|
119
|
+
|
|
120
|
+
expect(conn.rollback).not.toHaveBeenCalled();
|
|
121
|
+
});
|
|
122
|
+
});
|
|
@@ -6,6 +6,9 @@ import { OrmGenerics } from "../types/ormGenerics";
|
|
|
6
6
|
import { C6Constants as C6C } from "../constants/C6Constants";
|
|
7
7
|
import {
|
|
8
8
|
DetermineResponseDataType,
|
|
9
|
+
iRestLifecycleResponse,
|
|
10
|
+
iRestMethods,
|
|
11
|
+
iRestSqlExecutionContext,
|
|
9
12
|
iRestWebsocketPayload,
|
|
10
13
|
} from "../types/ormInterfaces";
|
|
11
14
|
import namedPlaceholders from 'named-placeholders';
|
|
@@ -24,12 +27,20 @@ export class SqlExecutor<
|
|
|
24
27
|
const { TABLE_NAME } = this.config.restModel;
|
|
25
28
|
const method = this.config.requestMethod;
|
|
26
29
|
|
|
30
|
+
await this.runLifecycleHooks<"beforeProcessing">(
|
|
31
|
+
"beforeProcessing",
|
|
32
|
+
{
|
|
33
|
+
config: this.config,
|
|
34
|
+
request: this.request,
|
|
35
|
+
},
|
|
36
|
+
);
|
|
37
|
+
|
|
27
38
|
// Normalize singular T-shaped requests into complex ORM shape (GET/PUT/DELETE)
|
|
28
39
|
try {
|
|
29
40
|
this.request = normalizeSingularRequest(
|
|
30
|
-
method
|
|
31
|
-
this.request
|
|
32
|
-
this.config.restModel
|
|
41
|
+
method,
|
|
42
|
+
this.request,
|
|
43
|
+
this.config.restModel,
|
|
33
44
|
undefined
|
|
34
45
|
) as typeof this.request;
|
|
35
46
|
} catch (e) {
|
|
@@ -52,40 +63,55 @@ export class SqlExecutor<
|
|
|
52
63
|
this.request,
|
|
53
64
|
);
|
|
54
65
|
|
|
66
|
+
let response:
|
|
67
|
+
| DetermineResponseDataType<G['RequestMethod'], G['RestTableInterface']>;
|
|
68
|
+
|
|
55
69
|
switch (method) {
|
|
56
70
|
case 'GET': {
|
|
57
71
|
const rest = await this.runQuery();
|
|
58
72
|
if (this.config.reactBootstrap) {
|
|
73
|
+
const getResponse =
|
|
74
|
+
rest as unknown as DetermineResponseDataType<'GET', G['RestTableInterface']>;
|
|
75
|
+
const restRows = Array.isArray(getResponse.rest)
|
|
76
|
+
? getResponse.rest
|
|
77
|
+
: [getResponse.rest];
|
|
59
78
|
this.config.reactBootstrap.updateRestfulObjectArrays({
|
|
60
|
-
dataOrCallback:
|
|
79
|
+
dataOrCallback: restRows,
|
|
61
80
|
stateKey: this.config.restModel.TABLE_NAME,
|
|
62
|
-
uniqueObjectId:
|
|
81
|
+
uniqueObjectId:
|
|
82
|
+
this.config.restModel.PRIMARY_SHORT as unknown as (keyof G['RestTableInterface'])[],
|
|
63
83
|
});
|
|
64
84
|
}
|
|
65
|
-
|
|
85
|
+
response = rest as DetermineResponseDataType<G['RequestMethod'], G['RestTableInterface']>;
|
|
86
|
+
break;
|
|
66
87
|
}
|
|
67
88
|
|
|
68
89
|
case 'POST': {
|
|
69
90
|
const result = await this.runQuery();
|
|
70
91
|
await this.broadcastWebsocketIfConfigured(result);
|
|
71
|
-
|
|
92
|
+
response = result as DetermineResponseDataType<G['RequestMethod'], G['RestTableInterface']>;
|
|
93
|
+
break;
|
|
72
94
|
}
|
|
73
95
|
|
|
74
96
|
case 'PUT': {
|
|
75
97
|
const result = await this.runQuery();
|
|
76
98
|
await this.broadcastWebsocketIfConfigured(result);
|
|
77
|
-
|
|
99
|
+
response = result as DetermineResponseDataType<G['RequestMethod'], G['RestTableInterface']>;
|
|
100
|
+
break;
|
|
78
101
|
}
|
|
79
102
|
|
|
80
103
|
case 'DELETE': {
|
|
81
104
|
const result = await this.runQuery();
|
|
82
105
|
await this.broadcastWebsocketIfConfigured(result);
|
|
83
|
-
|
|
106
|
+
response = result as DetermineResponseDataType<G['RequestMethod'], G['RestTableInterface']>;
|
|
107
|
+
break;
|
|
84
108
|
}
|
|
85
109
|
|
|
86
110
|
default:
|
|
87
111
|
throw new Error(`Unsupported request method: ${method}`);
|
|
88
112
|
}
|
|
113
|
+
|
|
114
|
+
return response;
|
|
89
115
|
}
|
|
90
116
|
|
|
91
117
|
private async withConnection<T>(cb: (conn: PoolConnection) => Promise<T>): Promise<T> {
|
|
@@ -222,7 +248,7 @@ export class SqlExecutor<
|
|
|
222
248
|
const sources = [request, (where && typeof where === "object" && !Array.isArray(where)) ? where : undefined];
|
|
223
249
|
const columns = this.config.restModel.COLUMNS as Record<string, string>;
|
|
224
250
|
const primaryShorts = this.config.restModel.PRIMARY_SHORT ?? [];
|
|
225
|
-
const primaryFulls =
|
|
251
|
+
const primaryFulls = this.config.restModel.PRIMARY ?? [];
|
|
226
252
|
const pkValues: Record<string, any> = {};
|
|
227
253
|
|
|
228
254
|
for (const pkShort of primaryShorts) {
|
|
@@ -272,7 +298,7 @@ export class SqlExecutor<
|
|
|
272
298
|
|
|
273
299
|
for (const pk of pkShorts) {
|
|
274
300
|
if (pk in row) {
|
|
275
|
-
pkValues[pk] = (row as any)[pk];
|
|
301
|
+
pkValues[pk] = (row as Record<string, any>)[pk];
|
|
276
302
|
continue;
|
|
277
303
|
}
|
|
278
304
|
|
|
@@ -281,7 +307,7 @@ export class SqlExecutor<
|
|
|
281
307
|
);
|
|
282
308
|
|
|
283
309
|
if (fullKey && fullKey in row) {
|
|
284
|
-
pkValues[pk] = (row as any)[fullKey];
|
|
310
|
+
pkValues[pk] = (row as Record<string, any>)[fullKey];
|
|
285
311
|
}
|
|
286
312
|
}
|
|
287
313
|
|
|
@@ -454,39 +480,50 @@ export class SqlExecutor<
|
|
|
454
480
|
}
|
|
455
481
|
}
|
|
456
482
|
async runQuery(): Promise<DetermineResponseDataType<G['RequestMethod'], G['RestTableInterface']>> {
|
|
457
|
-
const { TABLE_NAME } = this.config.restModel;
|
|
458
483
|
const method = this.config.requestMethod;
|
|
459
|
-
|
|
484
|
+
const tableName = this.config.restModel.TABLE_NAME;
|
|
485
|
+
const logContext = getLogContext(this.config, this.request);
|
|
486
|
+
const sqlExecution = this.buildSqlExecutionContext(method, tableName, logContext);
|
|
460
487
|
|
|
488
|
+
return await this.withConnection(async (conn) =>
|
|
489
|
+
this.executeQueryWithLifecycle(conn, method, sqlExecution, logContext),
|
|
490
|
+
);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
private getQueryBuilder(
|
|
494
|
+
method: iRestMethods,
|
|
495
|
+
): SelectQueryBuilder<G> | UpdateQueryBuilder<G> | DeleteQueryBuilder<G> | PostQueryBuilder<G> {
|
|
461
496
|
switch (method) {
|
|
462
|
-
case
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
case
|
|
469
|
-
|
|
470
|
-
break;
|
|
471
|
-
case 'POST':
|
|
472
|
-
builder = new PostQueryBuilder(this.config, this.request);
|
|
473
|
-
break;
|
|
497
|
+
case C6C.GET:
|
|
498
|
+
return new SelectQueryBuilder(this.config, this.request);
|
|
499
|
+
case C6C.PUT:
|
|
500
|
+
return new UpdateQueryBuilder(this.config, this.request);
|
|
501
|
+
case C6C.DELETE:
|
|
502
|
+
return new DeleteQueryBuilder(this.config, this.request);
|
|
503
|
+
case C6C.POST:
|
|
504
|
+
return new PostQueryBuilder(this.config, this.request);
|
|
474
505
|
default:
|
|
475
506
|
throw new Error(`Unsupported query method: ${method}`);
|
|
476
507
|
}
|
|
508
|
+
}
|
|
477
509
|
|
|
478
|
-
|
|
510
|
+
private buildSqlExecutionContext(
|
|
511
|
+
method: iRestMethods,
|
|
512
|
+
tableName: string,
|
|
513
|
+
logContext: ReturnType<typeof getLogContext>,
|
|
514
|
+
): iRestSqlExecutionContext {
|
|
515
|
+
const builder = this.getQueryBuilder(method);
|
|
516
|
+
const queryResult = builder.build(tableName);
|
|
479
517
|
|
|
480
|
-
const logContext = getLogContext(this.config, this.request);
|
|
481
518
|
logWithLevel(
|
|
482
519
|
LogLevel.DEBUG,
|
|
483
520
|
logContext,
|
|
484
521
|
console.log,
|
|
485
522
|
`[SQL EXECUTOR] 🧠 Generated ${method.toUpperCase()} SQL:`,
|
|
486
|
-
|
|
523
|
+
queryResult,
|
|
487
524
|
);
|
|
488
525
|
|
|
489
|
-
const formatted = this.formatSQLWithParams(
|
|
526
|
+
const formatted = this.formatSQLWithParams(queryResult.sql, queryResult.params);
|
|
490
527
|
logWithLevel(
|
|
491
528
|
LogLevel.DEBUG,
|
|
492
529
|
logContext,
|
|
@@ -496,34 +533,138 @@ export class SqlExecutor<
|
|
|
496
533
|
);
|
|
497
534
|
|
|
498
535
|
const toUnnamed = namedPlaceholders();
|
|
499
|
-
const [sql, values] = toUnnamed(
|
|
536
|
+
const [sql, values] = toUnnamed(queryResult.sql, queryResult.params);
|
|
537
|
+
return { sql, values };
|
|
538
|
+
}
|
|
500
539
|
|
|
501
|
-
|
|
540
|
+
private createResponseFromQueryResult(
|
|
541
|
+
method: iRestMethods,
|
|
542
|
+
result: any,
|
|
543
|
+
sqlExecution: iRestSqlExecutionContext,
|
|
544
|
+
logContext: ReturnType<typeof getLogContext>,
|
|
545
|
+
): DetermineResponseDataType<G['RequestMethod'], G['RestTableInterface']> {
|
|
546
|
+
if (method === C6C.GET) {
|
|
547
|
+
return {
|
|
548
|
+
rest: result.map(this.serialize),
|
|
549
|
+
sql: { sql: sqlExecution.sql, values: sqlExecution.values },
|
|
550
|
+
} as DetermineResponseDataType<G['RequestMethod'], G['RestTableInterface']>;
|
|
551
|
+
}
|
|
502
552
|
|
|
503
|
-
|
|
504
|
-
|
|
553
|
+
logWithLevel(
|
|
554
|
+
LogLevel.DEBUG,
|
|
555
|
+
logContext,
|
|
556
|
+
console.log,
|
|
557
|
+
`[SQL EXECUTOR] ✏️ Rows affected:`,
|
|
558
|
+
result.affectedRows,
|
|
559
|
+
);
|
|
505
560
|
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
561
|
+
return {
|
|
562
|
+
affected: result.affectedRows as number,
|
|
563
|
+
insertId: result.insertId as number,
|
|
564
|
+
rest: [],
|
|
565
|
+
sql: { sql: sqlExecution.sql, values: sqlExecution.values },
|
|
566
|
+
} as DetermineResponseDataType<G['RequestMethod'], G['RestTableInterface']>;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
private createLifecycleHookResponse(
|
|
570
|
+
response: DetermineResponseDataType<G['RequestMethod'], G['RestTableInterface']>,
|
|
571
|
+
): iRestLifecycleResponse<G> {
|
|
572
|
+
const data = Object.assign({ success: true }, response);
|
|
573
|
+
return { data };
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
private async executeQueryWithLifecycle(
|
|
577
|
+
conn: PoolConnection,
|
|
578
|
+
method: iRestMethods,
|
|
579
|
+
sqlExecution: iRestSqlExecutionContext,
|
|
580
|
+
logContext: ReturnType<typeof getLogContext>,
|
|
581
|
+
): Promise<DetermineResponseDataType<G['RequestMethod'], G['RestTableInterface']>> {
|
|
582
|
+
const useTransaction = method !== C6C.GET;
|
|
583
|
+
let committed = false;
|
|
584
|
+
|
|
585
|
+
try {
|
|
586
|
+
if (useTransaction) {
|
|
587
|
+
logWithLevel(
|
|
588
|
+
LogLevel.DEBUG,
|
|
589
|
+
logContext,
|
|
590
|
+
console.log,
|
|
591
|
+
`[SQL EXECUTOR] 🧾 Beginning transaction`,
|
|
592
|
+
);
|
|
593
|
+
await conn.beginTransaction();
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
await this.validateSqlAllowList(sqlExecution.sql);
|
|
597
|
+
|
|
598
|
+
await this.runLifecycleHooks<"beforeExecution">(
|
|
599
|
+
"beforeExecution",
|
|
600
|
+
{
|
|
601
|
+
config: this.config,
|
|
602
|
+
request: this.request,
|
|
603
|
+
sqlExecution,
|
|
604
|
+
},
|
|
605
|
+
);
|
|
606
|
+
const [result] = await conn.query<any>(sqlExecution.sql, sqlExecution.values);
|
|
607
|
+
|
|
608
|
+
const response = this.createResponseFromQueryResult(
|
|
609
|
+
method,
|
|
610
|
+
result,
|
|
611
|
+
sqlExecution,
|
|
612
|
+
logContext,
|
|
613
|
+
);
|
|
614
|
+
const hookResponse = this.createLifecycleHookResponse(response);
|
|
615
|
+
|
|
616
|
+
await this.runLifecycleHooks<"afterExecution">(
|
|
617
|
+
"afterExecution",
|
|
618
|
+
{
|
|
619
|
+
config: this.config,
|
|
620
|
+
request: this.request,
|
|
621
|
+
response: hookResponse,
|
|
622
|
+
},
|
|
623
|
+
);
|
|
624
|
+
|
|
625
|
+
if (useTransaction) {
|
|
626
|
+
await conn.commit();
|
|
627
|
+
committed = true;
|
|
512
628
|
logWithLevel(
|
|
513
629
|
LogLevel.DEBUG,
|
|
514
630
|
logContext,
|
|
515
631
|
console.log,
|
|
516
|
-
`[SQL EXECUTOR]
|
|
517
|
-
result.affectedRows,
|
|
632
|
+
`[SQL EXECUTOR] 🧾 Transaction committed`,
|
|
518
633
|
);
|
|
519
|
-
return {
|
|
520
|
-
affected: result.affectedRows as number,
|
|
521
|
-
insertId: result.insertId as number,
|
|
522
|
-
rest: [], // TODO - remove rest empty array from non-GET responses?
|
|
523
|
-
sql: { sql, values }
|
|
524
|
-
} as DetermineResponseDataType<G['RequestMethod'], G['RestTableInterface']>;
|
|
525
634
|
}
|
|
526
|
-
|
|
635
|
+
|
|
636
|
+
await this.runLifecycleHooks<"afterCommit">(
|
|
637
|
+
"afterCommit",
|
|
638
|
+
{
|
|
639
|
+
config: this.config,
|
|
640
|
+
request: this.request,
|
|
641
|
+
response: hookResponse,
|
|
642
|
+
},
|
|
643
|
+
);
|
|
644
|
+
|
|
645
|
+
return response;
|
|
646
|
+
} catch (err) {
|
|
647
|
+
if (useTransaction && !committed) {
|
|
648
|
+
try {
|
|
649
|
+
await conn.rollback();
|
|
650
|
+
logWithLevel(
|
|
651
|
+
LogLevel.WARN,
|
|
652
|
+
logContext,
|
|
653
|
+
console.warn,
|
|
654
|
+
`[SQL EXECUTOR] 🧾 Transaction rolled back`,
|
|
655
|
+
);
|
|
656
|
+
} catch (rollbackErr) {
|
|
657
|
+
logWithLevel(
|
|
658
|
+
LogLevel.ERROR,
|
|
659
|
+
logContext,
|
|
660
|
+
console.error,
|
|
661
|
+
`[SQL EXECUTOR] Rollback failed`,
|
|
662
|
+
rollbackErr,
|
|
663
|
+
);
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
throw err;
|
|
667
|
+
}
|
|
527
668
|
}
|
|
528
669
|
|
|
529
670
|
private async validateSqlAllowList(sql: string): Promise<void> {
|
|
@@ -1,10 +1,30 @@
|
|
|
1
|
-
import type {Request, Response, NextFunction} from "express";
|
|
1
|
+
import type {Request, Response, NextFunction, Router} from "express";
|
|
2
2
|
import type {Pool} from "mysql2/promise";
|
|
3
3
|
import {C6C} from "../constants/C6Constants";
|
|
4
4
|
import restRequest from "../api/restRequest";
|
|
5
5
|
import type {iC6Object, iRestMethods, tWebsocketBroadcast} from "../types/ormInterfaces";
|
|
6
6
|
import {LogLevel, logWithLevel} from "../utils/logLevel";
|
|
7
7
|
|
|
8
|
+
type iExpressHandlerConfig = {
|
|
9
|
+
C6: iC6Object;
|
|
10
|
+
mysqlPool: Pool;
|
|
11
|
+
sqlAllowListPath?: string;
|
|
12
|
+
websocketBroadcast?: tWebsocketBroadcast;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
type iRestExpressRequestConfig = iExpressHandlerConfig & {
|
|
16
|
+
router: Pick<Router, "all">;
|
|
17
|
+
routePath?: string;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export function restExpressRequest({
|
|
21
|
+
router,
|
|
22
|
+
routePath = "/rest/:table{/:primary}",
|
|
23
|
+
...handlerConfig
|
|
24
|
+
}: iRestExpressRequestConfig) {
|
|
25
|
+
router.all(routePath, ExpressHandler(handlerConfig));
|
|
26
|
+
}
|
|
27
|
+
|
|
8
28
|
|
|
9
29
|
// TODO - WE MUST make this a generic - optional, but helpful
|
|
10
30
|
// note sure how it would help anyone actually...
|
|
@@ -13,12 +33,7 @@ export function ExpressHandler({
|
|
|
13
33
|
mysqlPool,
|
|
14
34
|
sqlAllowListPath,
|
|
15
35
|
websocketBroadcast,
|
|
16
|
-
}: {
|
|
17
|
-
C6: iC6Object;
|
|
18
|
-
mysqlPool: Pool;
|
|
19
|
-
sqlAllowListPath?: string;
|
|
20
|
-
websocketBroadcast?: tWebsocketBroadcast;
|
|
21
|
-
}) {
|
|
36
|
+
}: iExpressHandlerConfig) {
|
|
22
37
|
|
|
23
38
|
return async (req: Request, res: Response, next: NextFunction) => {
|
|
24
39
|
try {
|
|
@@ -172,6 +172,19 @@ export type DetermineResponseDataType<
|
|
|
172
172
|
? iDeleteC6RestResponse<RestTableInterface>
|
|
173
173
|
: never);
|
|
174
174
|
|
|
175
|
+
export type iRestSqlExecutionContext = {
|
|
176
|
+
sql: string;
|
|
177
|
+
values: any[];
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
export type iRestLifecycleResponse<G extends OrmGenerics> =
|
|
181
|
+
AxiosResponse<DetermineResponseDataType<G['RequestMethod'], G['RestTableInterface']>>
|
|
182
|
+
| {
|
|
183
|
+
data: {
|
|
184
|
+
success: boolean;
|
|
185
|
+
} & DetermineResponseDataType<G['RequestMethod'], G['RestTableInterface']>;
|
|
186
|
+
};
|
|
187
|
+
|
|
175
188
|
|
|
176
189
|
export type iRestWebsocketPayload = {
|
|
177
190
|
REST: {
|
|
@@ -254,20 +267,21 @@ export type iRestReactiveLifecycle<G extends OrmGenerics> = {
|
|
|
254
267
|
[key: string]: (args: {
|
|
255
268
|
config: iRest<G['RestShortTableName'], G['RestTableInterface'], G['PrimaryKey']>;
|
|
256
269
|
request: RequestQueryBody<G['RequestMethod'], G['RestTableInterface'], G['CustomAndRequiredFields'], G['RequestTableOverrides']>;
|
|
270
|
+
sqlExecution?: iRestSqlExecutionContext;
|
|
257
271
|
}) => void | Promise<void>;
|
|
258
272
|
};
|
|
259
273
|
afterExecution?: {
|
|
260
274
|
[key: string]: (args: {
|
|
261
275
|
config: iRest<G['RestShortTableName'], G['RestTableInterface'], G['PrimaryKey']>;
|
|
262
276
|
request: RequestQueryBody<G['RequestMethod'], G['RestTableInterface'], G['CustomAndRequiredFields'], G['RequestTableOverrides']>;
|
|
263
|
-
response:
|
|
277
|
+
response: iRestLifecycleResponse<G>;
|
|
264
278
|
}) => void | Promise<void>;
|
|
265
279
|
};
|
|
266
280
|
afterCommit?: {
|
|
267
281
|
[key: string]: (args: {
|
|
268
282
|
config: iRest<G['RestShortTableName'], G['RestTableInterface'], G['PrimaryKey']>;
|
|
269
283
|
request: RequestQueryBody<G['RequestMethod'], G['RestTableInterface'], G['CustomAndRequiredFields'], G['RequestTableOverrides']>;
|
|
270
|
-
response:
|
|
284
|
+
response: iRestLifecycleResponse<G>;
|
|
271
285
|
}) => void | Promise<void>;
|
|
272
286
|
};
|
|
273
287
|
};
|