@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.
Files changed (60) hide show
  1. package/dist/executors/SqlExecutor.d.ts +5 -0
  2. package/dist/handlers/ExpressHandler.d.ts +10 -3
  3. package/dist/index.cjs.js +170 -78
  4. package/dist/index.cjs.js.map +1 -1
  5. package/dist/index.esm.js +170 -79
  6. package/dist/index.esm.js.map +1 -1
  7. package/dist/types/ormInterfaces.d.ts +12 -2
  8. package/package.json +2 -1
  9. package/src/__tests__/sakila-db/C6.js +1 -1
  10. package/src/__tests__/sakila-db/C6.mysqldump.json +1 -1
  11. package/src/__tests__/sakila-db/C6.mysqldump.sql +1 -1
  12. package/src/__tests__/sakila-db/C6.ts +1 -1
  13. package/src/__tests__/sakila-db/sqlResponses/C6.actor.post.json +3 -3
  14. package/src/__tests__/sakila-db/sqlResponses/C6.actor.post.latest.json +3 -3
  15. package/src/__tests__/sakila-db/sqlResponses/C6.actor.put.json +1 -1
  16. package/src/__tests__/sakila-db/sqlResponses/C6.actor.put.lookup.json +3 -3
  17. package/src/__tests__/sakila-db/sqlResponses/C6.address.post.json +5 -5
  18. package/src/__tests__/sakila-db/sqlResponses/C6.address.post.latest.json +5 -5
  19. package/src/__tests__/sakila-db/sqlResponses/C6.address.put.json +1 -1
  20. package/src/__tests__/sakila-db/sqlResponses/C6.address.put.lookup.json +5 -5
  21. package/src/__tests__/sakila-db/sqlResponses/C6.category.post.json +2 -2
  22. package/src/__tests__/sakila-db/sqlResponses/C6.category.post.latest.json +2 -2
  23. package/src/__tests__/sakila-db/sqlResponses/C6.category.put.json +1 -1
  24. package/src/__tests__/sakila-db/sqlResponses/C6.category.put.lookup.json +2 -2
  25. package/src/__tests__/sakila-db/sqlResponses/C6.city.post.json +2 -2
  26. package/src/__tests__/sakila-db/sqlResponses/C6.city.post.latest.json +2 -2
  27. package/src/__tests__/sakila-db/sqlResponses/C6.city.put.json +1 -1
  28. package/src/__tests__/sakila-db/sqlResponses/C6.city.put.lookup.json +2 -2
  29. package/src/__tests__/sakila-db/sqlResponses/C6.country.post.json +2 -2
  30. package/src/__tests__/sakila-db/sqlResponses/C6.country.post.latest.json +2 -2
  31. package/src/__tests__/sakila-db/sqlResponses/C6.country.put.json +1 -1
  32. package/src/__tests__/sakila-db/sqlResponses/C6.country.put.lookup.json +2 -2
  33. package/src/__tests__/sakila-db/sqlResponses/C6.customer.post.json +5 -5
  34. package/src/__tests__/sakila-db/sqlResponses/C6.customer.post.latest.json +5 -5
  35. package/src/__tests__/sakila-db/sqlResponses/C6.customer.put.json +1 -1
  36. package/src/__tests__/sakila-db/sqlResponses/C6.customer.put.lookup.json +5 -5
  37. package/src/__tests__/sakila-db/sqlResponses/C6.film.post.json +2 -2
  38. package/src/__tests__/sakila-db/sqlResponses/C6.film.post.latest.json +2 -2
  39. package/src/__tests__/sakila-db/sqlResponses/C6.film.put.json +1 -1
  40. package/src/__tests__/sakila-db/sqlResponses/C6.film.put.lookup.json +2 -2
  41. package/src/__tests__/sakila-db/sqlResponses/C6.inventory.post.json +1 -1
  42. package/src/__tests__/sakila-db/sqlResponses/C6.inventory.post.latest.json +1 -1
  43. package/src/__tests__/sakila-db/sqlResponses/C6.inventory.put.json +1 -1
  44. package/src/__tests__/sakila-db/sqlResponses/C6.inventory.put.lookup.json +1 -1
  45. package/src/__tests__/sakila-db/sqlResponses/C6.language.post.json +2 -2
  46. package/src/__tests__/sakila-db/sqlResponses/C6.language.post.latest.json +2 -2
  47. package/src/__tests__/sakila-db/sqlResponses/C6.language.put.json +1 -1
  48. package/src/__tests__/sakila-db/sqlResponses/C6.language.put.lookup.json +2 -2
  49. package/src/__tests__/sakila-db/sqlResponses/C6.payment.post.json +2 -2
  50. package/src/__tests__/sakila-db/sqlResponses/C6.payment.post.latest.json +2 -2
  51. package/src/__tests__/sakila-db/sqlResponses/C6.payment.put.lookup.json +2 -2
  52. package/src/__tests__/sakila-db/sqlResponses/C6.rental.post.json +3 -3
  53. package/src/__tests__/sakila-db/sqlResponses/C6.rental.post.latest.json +3 -3
  54. package/src/__tests__/sakila-db/sqlResponses/C6.rental.put.json +1 -1
  55. package/src/__tests__/sakila-db/sqlResponses/C6.rental.put.lookup.json +3 -3
  56. package/src/__tests__/sakila.generated.test.ts +11 -3
  57. package/src/__tests__/sqlExecutorLifecycleHooks.test.ts +122 -0
  58. package/src/executors/SqlExecutor.ts +190 -49
  59. package/src/handlers/ExpressHandler.ts +22 -7
  60. 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 (_sql: string, _values?: any[]) => {
18
- // Return a result set shaped like mysql2/promise: [rows, fields]
19
- return [[{ ok: true }], []];
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 as any,
31
- this.request as any,
32
- this.config.restModel as any,
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: (rest as any).rest,
79
+ dataOrCallback: restRows,
61
80
  stateKey: this.config.restModel.TABLE_NAME,
62
- uniqueObjectId: this.config.restModel.PRIMARY_SHORT as any,
81
+ uniqueObjectId:
82
+ this.config.restModel.PRIMARY_SHORT as unknown as (keyof G['RestTableInterface'])[],
63
83
  });
64
84
  }
65
- return rest as DetermineResponseDataType<G['RequestMethod'], G['RestTableInterface']>;
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
- return result as DetermineResponseDataType<G['RequestMethod'], G['RestTableInterface']>;
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
- return result as DetermineResponseDataType<G['RequestMethod'], G['RestTableInterface']>;
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
- return result as DetermineResponseDataType<G['RequestMethod'], G['RestTableInterface']>;
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 = (this.config.restModel as any).PRIMARY ?? [];
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
- let builder: SelectQueryBuilder<G> | UpdateQueryBuilder<G> | DeleteQueryBuilder<G> | PostQueryBuilder<G>;
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 'GET':
463
- builder = new SelectQueryBuilder(this.config, this.request);
464
- break;
465
- case 'PUT':
466
- builder = new UpdateQueryBuilder(this.config, this.request);
467
- break;
468
- case 'DELETE':
469
- builder = new DeleteQueryBuilder(this.config, this.request);
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
- const QueryResult = builder.build(TABLE_NAME);
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
- QueryResult,
523
+ queryResult,
487
524
  );
488
525
 
489
- const formatted = this.formatSQLWithParams(QueryResult.sql, QueryResult.params);
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(QueryResult.sql, QueryResult.params);
536
+ const [sql, values] = toUnnamed(queryResult.sql, queryResult.params);
537
+ return { sql, values };
538
+ }
500
539
 
501
- await this.validateSqlAllowList(sql);
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
- return await this.withConnection(async (conn) => {
504
- const [result] = await conn.query<any>(sql, values);
553
+ logWithLevel(
554
+ LogLevel.DEBUG,
555
+ logContext,
556
+ console.log,
557
+ `[SQL EXECUTOR] ✏️ Rows affected:`,
558
+ result.affectedRows,
559
+ );
505
560
 
506
- if (method === 'GET') {
507
- return {
508
- rest: result.map(this.serialize),
509
- sql: { sql, values }
510
- } as DetermineResponseDataType<G['RequestMethod'], G['RestTableInterface']>;
511
- } else {
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] ✏️ Rows affected:`,
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: AxiosResponse<DetermineResponseDataType<G['RequestMethod'], G['RestTableInterface']>>;
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: AxiosResponse<DetermineResponseDataType<G['RequestMethod'], G['RestTableInterface']>>;
284
+ response: iRestLifecycleResponse<G>;
271
285
  }) => void | Promise<void>;
272
286
  };
273
287
  };