@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.
- package/dist/executors/SqlExecutor.d.ts +6 -0
- package/dist/handlers/ExpressHandler.d.ts +8 -9
- package/dist/index.cjs.js +324 -108
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.esm.js +324 -109
- package/dist/index.esm.js.map +1 -1
- package/dist/types/ormInterfaces.d.ts +25 -5
- package/dist/utils/cacheManager.d.ts +2 -3
- package/package.json +1 -1
- package/src/__tests__/convertForRequestBody.test.ts +58 -0
- package/src/__tests__/expressServer.e2e.test.ts +62 -38
- package/src/__tests__/fixtures/createTestServer.ts +7 -3
- package/src/__tests__/httpExecutorSingular.e2e.test.ts +97 -60
- package/src/__tests__/logSql.test.ts +13 -0
- 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.sqlAllowList.json +11 -11
- package/src/__tests__/sakila-db/C6.ts +1 -1
- package/src/__tests__/sakila-db/sqlResponses/C6.actor.post.json +4 -4
- 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 +6 -6
- 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 +3 -3
- 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 +3 -3
- 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 +3 -3
- 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 +6 -6
- 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 +3 -3
- 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 +2 -2
- 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 +3 -3
- 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 +3 -3
- 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 +4 -4
- 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__/sqlBuilders.test.ts +46 -0
- package/src/__tests__/sqlExecutorLifecycleHooks.test.ts +122 -0
- package/src/api/convertForRequestBody.ts +9 -2
- package/src/api/restRequest.ts +1 -0
- package/src/executors/HttpExecutor.ts +1 -1
- package/src/executors/SqlExecutor.ts +252 -49
- package/src/handlers/ExpressHandler.ts +50 -24
- package/src/orm/builders/ConditionBuilder.ts +43 -1
- package/src/orm/queries/PostQueryBuilder.ts +24 -12
- package/src/types/ormInterfaces.ts +31 -5
- package/src/utils/cacheManager.ts +3 -4
- 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
|
|
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-
|
|
8
|
+
"2026-02-08 19:33:00",
|
|
9
9
|
1,
|
|
10
10
|
1,
|
|
11
|
-
"2026-02-
|
|
11
|
+
"2026-02-08 19:33:00",
|
|
12
12
|
1,
|
|
13
|
-
"2026-02-
|
|
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-
|
|
5
|
+
"rental_date": "2026-02-08T19:33:00.000Z",
|
|
6
6
|
"inventory_id": 1,
|
|
7
7
|
"customer_id": 1,
|
|
8
|
-
"return_date": "2026-02-
|
|
8
|
+
"return_date": "2026-02-08T19:33:00.000Z",
|
|
9
9
|
"staff_id": 1,
|
|
10
|
-
"last_update": "2026-02-
|
|
10
|
+
"last_update": "2026-02-08T19:33:00.000Z"
|
|
11
11
|
}
|
|
12
12
|
],
|
|
13
13
|
"sql": {
|
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
"rest": [
|
|
3
3
|
{
|
|
4
4
|
"rental_id": 16050,
|
|
5
|
-
"rental_date": "2026-02-
|
|
5
|
+
"rental_date": "2026-02-08T19:33:00.000Z",
|
|
6
6
|
"inventory_id": 1,
|
|
7
7
|
"customer_id": 1,
|
|
8
|
-
"return_date": "2026-02-
|
|
8
|
+
"return_date": "2026-02-08T19:33:00.000Z",
|
|
9
9
|
"staff_id": 1,
|
|
10
|
-
"last_update": "2026-02-
|
|
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 (
|
|
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
|
|
|
@@ -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 =
|
|
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
|
}
|
package/src/api/restRequest.ts
CHANGED
|
@@ -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:
|
|
260
|
+
let cachedRequest: Promise<{ data: ResponseDataType }> | false = false;
|
|
261
261
|
|
|
262
262
|
if (cacheResults) {
|
|
263
263
|
cachedRequest = checkCache<ResponseDataType>(requestMethod, tableName, cacheRequestData);
|