@carbonorm/carbonnode 6.0.10 → 6.0.13

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 (75) hide show
  1. package/dist/executors/SqlExecutor.d.ts +6 -0
  2. package/dist/handlers/ExpressHandler.d.ts +8 -9
  3. package/dist/index.cjs.js +324 -108
  4. package/dist/index.cjs.js.map +1 -1
  5. package/dist/index.esm.js +324 -109
  6. package/dist/index.esm.js.map +1 -1
  7. package/dist/types/ormInterfaces.d.ts +25 -5
  8. package/dist/utils/cacheManager.d.ts +2 -3
  9. package/package.json +1 -1
  10. package/src/__tests__/convertForRequestBody.test.ts +58 -0
  11. package/src/__tests__/expressServer.e2e.test.ts +62 -38
  12. package/src/__tests__/fixtures/createTestServer.ts +7 -3
  13. package/src/__tests__/httpExecutorSingular.e2e.test.ts +97 -60
  14. package/src/__tests__/logSql.test.ts +13 -0
  15. package/src/__tests__/sakila-db/C6.js +1 -1
  16. package/src/__tests__/sakila-db/C6.mysqldump.json +1 -1
  17. package/src/__tests__/sakila-db/C6.mysqldump.sql +1 -1
  18. package/src/__tests__/sakila-db/C6.sqlAllowList.json +11 -11
  19. package/src/__tests__/sakila-db/C6.ts +1 -1
  20. package/src/__tests__/sakila-db/sqlResponses/C6.actor.post.json +4 -4
  21. package/src/__tests__/sakila-db/sqlResponses/C6.actor.post.latest.json +3 -3
  22. package/src/__tests__/sakila-db/sqlResponses/C6.actor.put.json +1 -1
  23. package/src/__tests__/sakila-db/sqlResponses/C6.actor.put.lookup.json +3 -3
  24. package/src/__tests__/sakila-db/sqlResponses/C6.address.post.json +6 -6
  25. package/src/__tests__/sakila-db/sqlResponses/C6.address.post.latest.json +5 -5
  26. package/src/__tests__/sakila-db/sqlResponses/C6.address.put.json +1 -1
  27. package/src/__tests__/sakila-db/sqlResponses/C6.address.put.lookup.json +5 -5
  28. package/src/__tests__/sakila-db/sqlResponses/C6.category.post.json +3 -3
  29. package/src/__tests__/sakila-db/sqlResponses/C6.category.post.latest.json +2 -2
  30. package/src/__tests__/sakila-db/sqlResponses/C6.category.put.json +1 -1
  31. package/src/__tests__/sakila-db/sqlResponses/C6.category.put.lookup.json +2 -2
  32. package/src/__tests__/sakila-db/sqlResponses/C6.city.post.json +3 -3
  33. package/src/__tests__/sakila-db/sqlResponses/C6.city.post.latest.json +2 -2
  34. package/src/__tests__/sakila-db/sqlResponses/C6.city.put.json +1 -1
  35. package/src/__tests__/sakila-db/sqlResponses/C6.city.put.lookup.json +2 -2
  36. package/src/__tests__/sakila-db/sqlResponses/C6.country.post.json +3 -3
  37. package/src/__tests__/sakila-db/sqlResponses/C6.country.post.latest.json +2 -2
  38. package/src/__tests__/sakila-db/sqlResponses/C6.country.put.json +1 -1
  39. package/src/__tests__/sakila-db/sqlResponses/C6.country.put.lookup.json +2 -2
  40. package/src/__tests__/sakila-db/sqlResponses/C6.customer.post.json +6 -6
  41. package/src/__tests__/sakila-db/sqlResponses/C6.customer.post.latest.json +5 -5
  42. package/src/__tests__/sakila-db/sqlResponses/C6.customer.put.json +1 -1
  43. package/src/__tests__/sakila-db/sqlResponses/C6.customer.put.lookup.json +5 -5
  44. package/src/__tests__/sakila-db/sqlResponses/C6.film.post.json +3 -3
  45. package/src/__tests__/sakila-db/sqlResponses/C6.film.post.latest.json +2 -2
  46. package/src/__tests__/sakila-db/sqlResponses/C6.film.put.json +1 -1
  47. package/src/__tests__/sakila-db/sqlResponses/C6.film.put.lookup.json +2 -2
  48. package/src/__tests__/sakila-db/sqlResponses/C6.inventory.post.json +2 -2
  49. package/src/__tests__/sakila-db/sqlResponses/C6.inventory.post.latest.json +1 -1
  50. package/src/__tests__/sakila-db/sqlResponses/C6.inventory.put.json +1 -1
  51. package/src/__tests__/sakila-db/sqlResponses/C6.inventory.put.lookup.json +1 -1
  52. package/src/__tests__/sakila-db/sqlResponses/C6.language.post.json +3 -3
  53. package/src/__tests__/sakila-db/sqlResponses/C6.language.post.latest.json +2 -2
  54. package/src/__tests__/sakila-db/sqlResponses/C6.language.put.json +1 -1
  55. package/src/__tests__/sakila-db/sqlResponses/C6.language.put.lookup.json +2 -2
  56. package/src/__tests__/sakila-db/sqlResponses/C6.payment.post.json +3 -3
  57. package/src/__tests__/sakila-db/sqlResponses/C6.payment.post.latest.json +2 -2
  58. package/src/__tests__/sakila-db/sqlResponses/C6.payment.put.lookup.json +2 -2
  59. package/src/__tests__/sakila-db/sqlResponses/C6.rental.post.json +4 -4
  60. package/src/__tests__/sakila-db/sqlResponses/C6.rental.post.latest.json +3 -3
  61. package/src/__tests__/sakila-db/sqlResponses/C6.rental.put.json +1 -1
  62. package/src/__tests__/sakila-db/sqlResponses/C6.rental.put.lookup.json +3 -3
  63. package/src/__tests__/sakila.generated.test.ts +11 -3
  64. package/src/__tests__/sqlBuilders.test.ts +46 -0
  65. package/src/__tests__/sqlExecutorLifecycleHooks.test.ts +122 -0
  66. package/src/api/convertForRequestBody.ts +9 -2
  67. package/src/api/restRequest.ts +1 -0
  68. package/src/executors/HttpExecutor.ts +1 -1
  69. package/src/executors/SqlExecutor.ts +252 -49
  70. package/src/handlers/ExpressHandler.ts +50 -24
  71. package/src/orm/builders/ConditionBuilder.ts +43 -1
  72. package/src/orm/queries/PostQueryBuilder.ts +24 -12
  73. package/src/types/ormInterfaces.ts +31 -5
  74. package/src/utils/cacheManager.ts +3 -4
  75. package/src/utils/colorSql.ts +18 -0
@@ -3,14 +3,14 @@
3
3
  "insertId": 16050,
4
4
  "rest": [],
5
5
  "sql": {
6
- "sql": "INSERT INTO `rental` (\n `rental_date`, `inventory_id`, `customer_id`, `return_date`, `staff_id`, `last_update`\n ) VALUES (\n ?, ?, ?, ?, ?, ?\n )",
6
+ "sql": "INSERT INTO `rental` (\n `rental_date`, `inventory_id`, `customer_id`, `return_date`, `staff_id`, `last_update`\n ) VALUES\n (?, ?, ?, ?, ?, ?)",
7
7
  "values": [
8
- "2026-02-06 05:58:33",
8
+ "2026-02-08 19:33:00",
9
9
  1,
10
10
  1,
11
- "2026-02-06 05:58:33",
11
+ "2026-02-08 19:33:00",
12
12
  1,
13
- "2026-02-06 05:58:33"
13
+ "2026-02-08 19:33:00"
14
14
  ]
15
15
  }
16
16
  }
@@ -2,12 +2,12 @@
2
2
  "rest": [
3
3
  {
4
4
  "rental_id": 16050,
5
- "rental_date": "2026-02-06T05:58:33.000Z",
5
+ "rental_date": "2026-02-08T19:33:00.000Z",
6
6
  "inventory_id": 1,
7
7
  "customer_id": 1,
8
- "return_date": "2026-02-06T05:58:33.000Z",
8
+ "return_date": "2026-02-08T19:33:00.000Z",
9
9
  "staff_id": 1,
10
- "last_update": "2026-02-06T05:58:33.000Z"
10
+ "last_update": "2026-02-08T19:33:00.000Z"
11
11
  }
12
12
  ],
13
13
  "sql": {
@@ -5,7 +5,7 @@
5
5
  "sql": {
6
6
  "sql": "UPDATE `rental` SET `rental_date` = ? WHERE (rental.rental_id) = ?",
7
7
  "values": [
8
- "2026-02-06 05:58:33",
8
+ "2026-02-08 19:33:00",
9
9
  16050
10
10
  ]
11
11
  }
@@ -2,12 +2,12 @@
2
2
  "rest": [
3
3
  {
4
4
  "rental_id": 16050,
5
- "rental_date": "2026-02-06T05:58:33.000Z",
5
+ "rental_date": "2026-02-08T19:33:00.000Z",
6
6
  "inventory_id": 1,
7
7
  "customer_id": 1,
8
- "return_date": "2026-02-06T05:58:33.000Z",
8
+ "return_date": "2026-02-08T19:33:00.000Z",
9
9
  "staff_id": 1,
10
- "last_update": "2026-02-06T05:58:33.000Z"
10
+ "last_update": "2026-02-08T19:33:00.000Z"
11
11
  }
12
12
  ],
13
13
  "sql": {
@@ -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
 
@@ -96,6 +96,52 @@ describe('SQL Builders', () => {
96
96
  expect(params).toEqual([JSON.stringify(payload)]);
97
97
  });
98
98
 
99
+ it('builds multi-row INSERT from dataInsertMultipleRows', () => {
100
+ const config = buildTestConfig();
101
+ const qb = new PostQueryBuilder(config as any, {
102
+ dataInsertMultipleRows: [
103
+ {
104
+ 'actor.first_name': 'ALICE',
105
+ 'actor.last_name': 'ONE',
106
+ },
107
+ {
108
+ 'actor.first_name': 'BOB',
109
+ 'actor.last_name': 'TWO',
110
+ },
111
+ ],
112
+ } as any, false);
113
+
114
+ const { sql, params } = qb.build('actor');
115
+
116
+ expect(sql).toContain('INSERT INTO `actor`');
117
+ expect(sql).toContain('`first_name`, `last_name`');
118
+ expect(sql).toContain(') VALUES');
119
+ expect(sql).toContain('),');
120
+ expect(params).toEqual(['ALICE', 'ONE', 'BOB', 'TWO']);
121
+ });
122
+
123
+ it('builds multi-row INSERT from direct array request syntax', () => {
124
+ const config = buildTestConfig();
125
+ const qb = new PostQueryBuilder(config as any, [
126
+ {
127
+ 'actor.first_name': 'ALICE',
128
+ 'actor.last_name': 'ONE',
129
+ },
130
+ {
131
+ 'actor.first_name': 'BOB',
132
+ 'actor.last_name': 'TWO',
133
+ },
134
+ ] as any, false);
135
+
136
+ const { sql, params } = qb.build('actor');
137
+
138
+ expect(sql).toContain('INSERT INTO `actor`');
139
+ expect(sql).toContain('`first_name`, `last_name`');
140
+ expect(sql).toContain(') VALUES');
141
+ expect(sql).toContain('),');
142
+ expect(params).toEqual(['ALICE', 'ONE', 'BOB', 'TWO']);
143
+ });
144
+
99
145
  it('stringifies dotted-key JSON payloads for JSON columns on UPDATE', () => {
100
146
  const config = buildTestConfig();
101
147
  const payload = { 'section1.preparedBy': 'Prepared by Assessorly, Co.' };
@@ -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
+ });
@@ -11,7 +11,11 @@ export default function <
11
11
  restfulObject: RequestQueryBody<RequestMethod, RestTableInterface, CustomAndRequiredFields, RequestTableOverrides>,
12
12
  tableName: string | string[],
13
13
  C6: iC6Object,
14
- regexErrorHandler: (message: string) => void = alert
14
+ regexErrorHandler: (message: string) => void = (message: string) => {
15
+ if (typeof globalThis !== "undefined" && typeof (globalThis as any).alert === "function") {
16
+ (globalThis as any).alert(message);
17
+ }
18
+ }
15
19
  ) {
16
20
  const payload: Record<string, any> = {};
17
21
  const tableNames = Array.isArray(tableName) ? tableName : [tableName];
@@ -43,7 +47,8 @@ export default function <
43
47
  C6Constants.DELETE,
44
48
  C6Constants.WHERE,
45
49
  C6Constants.JOIN,
46
- C6Constants.PAGINATION
50
+ C6Constants.PAGINATION,
51
+ "cacheResults",
47
52
  ].includes(value)) {
48
53
  const val = restfulObject[value];
49
54
  if (Array.isArray(val)) {
@@ -52,6 +57,8 @@ export default function <
52
57
  payload[value] = Object.keys(val)
53
58
  .sort()
54
59
  .reduce((acc, key) => ({ ...acc, [key]: val[key] }), {});
60
+ } else {
61
+ payload[value] = val;
55
62
  }
56
63
  continue;
57
64
  }
@@ -34,6 +34,7 @@ export default function restRequest<
34
34
  const config = typeof configX === "function" ? configX() : configX;
35
35
 
36
36
  applyLogLevelDefaults(config, request);
37
+
37
38
  const logContext = getLogContext(config, request);
38
39
 
39
40
  if (!config.mysqlPool && !config.axios) {
@@ -257,7 +257,7 @@ export class HttpExecutor<
257
257
  // literally impossible for query to be undefined or null here but the editor is too busy licking windows to understand that
258
258
  let querySerialized: string = sortAndSerializeQueryObject(tables, cacheRequestData ?? {});
259
259
 
260
- let cachedRequest: AxiosPromise<ResponseDataType> | false = false;
260
+ let cachedRequest: Promise<{ data: ResponseDataType }> | false = false;
261
261
 
262
262
  if (cacheResults) {
263
263
  cachedRequest = checkCache<ResponseDataType>(requestMethod, tableName, cacheRequestData);