@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
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AxiosInstance,
|
|
1
|
+
import type { AxiosInstance, AxiosResponse } from "axios";
|
|
2
2
|
import type { Pool } from "mysql2/promise";
|
|
3
3
|
import { eFetchDependencies } from "./dynamicFetching";
|
|
4
4
|
import { Modify } from "./modifyTypes";
|
|
@@ -92,10 +92,20 @@ export type RequestQueryBody<Method extends iRestMethods, T extends {
|
|
|
92
92
|
} = {}, Overrides extends {
|
|
93
93
|
[key: string]: any;
|
|
94
94
|
} = {}> = Method extends 'GET' | 'PUT' | 'DELETE' ? iAPI<RequestGetPutDeleteBody<Modify<T, Overrides> & Custom>> : iAPI<RequestPostBody<Modify<T, Overrides> & Custom>>;
|
|
95
|
+
export interface iCacheRequestConfig {
|
|
96
|
+
method?: string;
|
|
97
|
+
url?: string;
|
|
98
|
+
headers?: any;
|
|
99
|
+
}
|
|
100
|
+
export interface iCacheResponse<ResponseDataType = any> {
|
|
101
|
+
data: ResponseDataType;
|
|
102
|
+
config?: iCacheRequestConfig;
|
|
103
|
+
[key: string]: any;
|
|
104
|
+
}
|
|
95
105
|
export interface iCacheAPI<ResponseDataType = any> {
|
|
96
106
|
requestArgumentsSerialized: string;
|
|
97
|
-
request:
|
|
98
|
-
response?:
|
|
107
|
+
request: Promise<iCacheResponse<ResponseDataType>>;
|
|
108
|
+
response?: iCacheResponse<ResponseDataType> & {
|
|
99
109
|
__carbonTiming?: {
|
|
100
110
|
start: number;
|
|
101
111
|
end: number;
|
|
@@ -146,6 +156,15 @@ export interface iGetC6RestResponse<ResponseDataType extends {
|
|
|
146
156
|
export type DetermineResponseDataType<Method extends iRestMethods, RestTableInterface extends {
|
|
147
157
|
[key: string]: any;
|
|
148
158
|
}, ResponseDataOverrides = {}> = (Method extends 'POST' ? iPostC6RestResponse<RestTableInterface> : Method extends 'GET' ? iGetC6RestResponse<RestTableInterface, ResponseDataOverrides> : Method extends 'PUT' ? iPutC6RestResponse<RestTableInterface> : Method extends 'DELETE' ? iDeleteC6RestResponse<RestTableInterface> : never);
|
|
159
|
+
export type iRestSqlExecutionContext = {
|
|
160
|
+
sql: string;
|
|
161
|
+
values: any[];
|
|
162
|
+
};
|
|
163
|
+
export type iRestLifecycleResponse<G extends OrmGenerics> = AxiosResponse<DetermineResponseDataType<G['RequestMethod'], G['RestTableInterface']>> | {
|
|
164
|
+
data: {
|
|
165
|
+
success: boolean;
|
|
166
|
+
} & DetermineResponseDataType<G['RequestMethod'], G['RestTableInterface']>;
|
|
167
|
+
};
|
|
149
168
|
export type iRestWebsocketPayload = {
|
|
150
169
|
REST: {
|
|
151
170
|
TABLE_NAME: string;
|
|
@@ -217,20 +236,21 @@ export type iRestReactiveLifecycle<G extends OrmGenerics> = {
|
|
|
217
236
|
[key: string]: (args: {
|
|
218
237
|
config: iRest<G['RestShortTableName'], G['RestTableInterface'], G['PrimaryKey']>;
|
|
219
238
|
request: RequestQueryBody<G['RequestMethod'], G['RestTableInterface'], G['CustomAndRequiredFields'], G['RequestTableOverrides']>;
|
|
239
|
+
sqlExecution?: iRestSqlExecutionContext;
|
|
220
240
|
}) => void | Promise<void>;
|
|
221
241
|
};
|
|
222
242
|
afterExecution?: {
|
|
223
243
|
[key: string]: (args: {
|
|
224
244
|
config: iRest<G['RestShortTableName'], G['RestTableInterface'], G['PrimaryKey']>;
|
|
225
245
|
request: RequestQueryBody<G['RequestMethod'], G['RestTableInterface'], G['CustomAndRequiredFields'], G['RequestTableOverrides']>;
|
|
226
|
-
response:
|
|
246
|
+
response: iRestLifecycleResponse<G>;
|
|
227
247
|
}) => void | Promise<void>;
|
|
228
248
|
};
|
|
229
249
|
afterCommit?: {
|
|
230
250
|
[key: string]: (args: {
|
|
231
251
|
config: iRest<G['RestShortTableName'], G['RestTableInterface'], G['PrimaryKey']>;
|
|
232
252
|
request: RequestQueryBody<G['RequestMethod'], G['RestTableInterface'], G['CustomAndRequiredFields'], G['RequestTableOverrides']>;
|
|
233
|
-
response:
|
|
253
|
+
response: iRestLifecycleResponse<G>;
|
|
234
254
|
}) => void | Promise<void>;
|
|
235
255
|
};
|
|
236
256
|
};
|
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import type { iCacheAPI } from "../types/ormInterfaces";
|
|
1
|
+
import type { iCacheAPI, iCacheResponse } from "../types/ormInterfaces";
|
|
3
2
|
export declare const apiRequestCache: Map<string, iCacheAPI<any>>;
|
|
4
3
|
export declare const userCustomClearCache: (() => void)[];
|
|
5
4
|
export declare function clearCache(props?: {
|
|
6
5
|
ignoreWarning?: boolean;
|
|
7
6
|
}): void;
|
|
8
|
-
export declare function checkCache<ResponseDataType = any>(method: string, tableName: string | string[], requestData: any):
|
|
7
|
+
export declare function checkCache<ResponseDataType = any>(method: string, tableName: string | string[], requestData: any): Promise<iCacheResponse<ResponseDataType>> | false;
|
|
9
8
|
export declare function setCache<ResponseDataType = any>(method: string, tableName: string | string[], requestData: any, cacheEntry: iCacheAPI<ResponseDataType>): void;
|
package/package.json
CHANGED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import convertForRequestBody from "../api/convertForRequestBody";
|
|
3
|
+
import { C6Constants as C6C } from "../constants/C6Constants";
|
|
4
|
+
|
|
5
|
+
const C6 = {
|
|
6
|
+
TABLES: {
|
|
7
|
+
actor: {
|
|
8
|
+
TABLE_NAME: "actor",
|
|
9
|
+
ACTOR_ID: "actor.actor_id",
|
|
10
|
+
FIRST_NAME: "actor.first_name",
|
|
11
|
+
LAST_NAME: "actor.last_name",
|
|
12
|
+
COLUMNS: {
|
|
13
|
+
"actor.actor_id": "actor_id",
|
|
14
|
+
"actor.first_name": "first_name",
|
|
15
|
+
"actor.last_name": "last_name",
|
|
16
|
+
},
|
|
17
|
+
REGEX_VALIDATION: {},
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
} as any;
|
|
21
|
+
|
|
22
|
+
describe("convertForRequestBody", () => {
|
|
23
|
+
it("preserves primitive control keys and maps shorthand columns", () => {
|
|
24
|
+
const payload = convertForRequestBody(
|
|
25
|
+
{
|
|
26
|
+
actor_id: 5,
|
|
27
|
+
[C6C.DELETE]: true,
|
|
28
|
+
cacheResults: false,
|
|
29
|
+
} as any,
|
|
30
|
+
"actor",
|
|
31
|
+
C6,
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
expect(payload).toMatchObject({
|
|
35
|
+
"actor.actor_id": 5,
|
|
36
|
+
[C6C.DELETE]: true,
|
|
37
|
+
cacheResults: false,
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("normalizes object control keys deterministically", () => {
|
|
42
|
+
const payload = convertForRequestBody(
|
|
43
|
+
{
|
|
44
|
+
[C6C.WHERE]: {
|
|
45
|
+
"actor.last_name": "B",
|
|
46
|
+
"actor.first_name": "A",
|
|
47
|
+
},
|
|
48
|
+
} as any,
|
|
49
|
+
"actor",
|
|
50
|
+
C6,
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
expect(Object.keys(payload[C6C.WHERE])).toEqual([
|
|
54
|
+
"actor.first_name",
|
|
55
|
+
"actor.last_name",
|
|
56
|
+
]);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
@@ -2,12 +2,49 @@ import mysql from "mysql2/promise";
|
|
|
2
2
|
import axios from "axios";
|
|
3
3
|
import { AddressInfo } from "net";
|
|
4
4
|
import {describe, it, expect, beforeAll, afterAll} from "vitest";
|
|
5
|
-
import {
|
|
5
|
+
import { restOrm } from "@carbonorm/carbonnode";
|
|
6
|
+
import {Actor, C6, Film_Actor} from "./sakila-db/C6.js";
|
|
6
7
|
import {C6C} from "../constants/C6Constants";
|
|
7
8
|
import createTestServer from "./fixtures/createTestServer";
|
|
8
9
|
|
|
9
10
|
let pool: mysql.Pool;
|
|
10
11
|
let server: any;
|
|
12
|
+
let restURL: string;
|
|
13
|
+
let axiosClient: ReturnType<typeof axios.create>;
|
|
14
|
+
const actorHttp = restOrm<any>(() => ({
|
|
15
|
+
C6,
|
|
16
|
+
restModel: C6.TABLES.actor,
|
|
17
|
+
restURL,
|
|
18
|
+
axios: axiosClient,
|
|
19
|
+
verbose: false,
|
|
20
|
+
}));
|
|
21
|
+
const filmActorHttp = restOrm<any>(() => ({
|
|
22
|
+
C6,
|
|
23
|
+
restModel: C6.TABLES.film_actor,
|
|
24
|
+
restURL,
|
|
25
|
+
axios: axiosClient,
|
|
26
|
+
verbose: false,
|
|
27
|
+
}));
|
|
28
|
+
|
|
29
|
+
const actorRequest = async (
|
|
30
|
+
method: "GET" | "POST" | "PUT" | "DELETE",
|
|
31
|
+
request: any
|
|
32
|
+
) => {
|
|
33
|
+
if (method === "GET") return actorHttp.Get(request as any);
|
|
34
|
+
if (method === "POST") return actorHttp.Post(request as any);
|
|
35
|
+
if (method === "PUT") return actorHttp.Put(request as any);
|
|
36
|
+
return actorHttp.Delete(request as any);
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const filmActorRequest = async (
|
|
40
|
+
method: "GET" | "POST" | "PUT" | "DELETE",
|
|
41
|
+
request: any
|
|
42
|
+
) => {
|
|
43
|
+
if (method === "GET") return filmActorHttp.Get(request as any);
|
|
44
|
+
if (method === "POST") return filmActorHttp.Post(request as any);
|
|
45
|
+
if (method === "PUT") return filmActorHttp.Put(request as any);
|
|
46
|
+
return filmActorHttp.Delete(request as any);
|
|
47
|
+
};
|
|
11
48
|
|
|
12
49
|
beforeAll(async () => {
|
|
13
50
|
pool = mysql.createPool({
|
|
@@ -22,17 +59,12 @@ beforeAll(async () => {
|
|
|
22
59
|
await new Promise(resolve => server.on('listening', resolve));
|
|
23
60
|
const {port} = server.address() as AddressInfo;
|
|
24
61
|
|
|
25
|
-
|
|
26
|
-
|
|
62
|
+
restURL = `http://127.0.0.1:${port}/rest/`;
|
|
63
|
+
axiosClient = axios.create();
|
|
27
64
|
axiosClient.interceptors.response.use(
|
|
28
65
|
response => response,
|
|
29
66
|
error => Promise.reject(new Error(error?.message ?? 'Request failed')),
|
|
30
67
|
);
|
|
31
|
-
GLOBAL_REST_PARAMETERS.axios = axiosClient;
|
|
32
|
-
GLOBAL_REST_PARAMETERS.verbose = false;
|
|
33
|
-
// ensure HTTP executor is used
|
|
34
|
-
// @ts-ignore
|
|
35
|
-
delete GLOBAL_REST_PARAMETERS.mysqlPool;
|
|
36
68
|
});
|
|
37
69
|
|
|
38
70
|
afterAll(async () => {
|
|
@@ -42,7 +74,7 @@ afterAll(async () => {
|
|
|
42
74
|
|
|
43
75
|
describe("ExpressHandler e2e", () => {
|
|
44
76
|
it("handles GET requests", async () => {
|
|
45
|
-
const data = await
|
|
77
|
+
const data = await actorRequest("GET", {
|
|
46
78
|
[C6C.PAGINATION]: {
|
|
47
79
|
[C6C.LIMIT]: 1
|
|
48
80
|
},
|
|
@@ -53,7 +85,7 @@ describe("ExpressHandler e2e", () => {
|
|
|
53
85
|
|
|
54
86
|
|
|
55
87
|
it("handles empty get requests", async () => {
|
|
56
|
-
const data = await
|
|
88
|
+
const data = await actorRequest("GET", {});
|
|
57
89
|
expect(Array.isArray(data?.rest)).toBe(true);
|
|
58
90
|
expect(data?.rest?.length).toBeGreaterThan(0);
|
|
59
91
|
});
|
|
@@ -62,12 +94,12 @@ describe("ExpressHandler e2e", () => {
|
|
|
62
94
|
const first_name = `Test${Date.now()}`;
|
|
63
95
|
const last_name = `User${Date.now()}`;
|
|
64
96
|
|
|
65
|
-
await
|
|
97
|
+
await actorRequest("POST", {
|
|
66
98
|
first_name,
|
|
67
99
|
last_name,
|
|
68
100
|
} as any);
|
|
69
101
|
|
|
70
|
-
let data = await
|
|
102
|
+
let data = await actorRequest("GET", {
|
|
71
103
|
[C6C.WHERE]: { [Actor.FIRST_NAME]: first_name, [Actor.LAST_NAME]: last_name },
|
|
72
104
|
[C6C.PAGINATION]: { [C6C.LIMIT]: 1 },
|
|
73
105
|
} as any);
|
|
@@ -75,22 +107,22 @@ describe("ExpressHandler e2e", () => {
|
|
|
75
107
|
expect(data?.rest).toHaveLength(1);
|
|
76
108
|
const testId = data?.rest[0].actor_id;
|
|
77
109
|
|
|
78
|
-
await
|
|
110
|
+
await actorRequest("PUT", {
|
|
79
111
|
[C6C.WHERE]: { [Actor.ACTOR_ID]: testId },
|
|
80
112
|
[C6C.UPDATE]: { first_name: "Updated" },
|
|
81
113
|
} as any);
|
|
82
114
|
|
|
83
|
-
data = await
|
|
115
|
+
data = await actorRequest("GET", {
|
|
84
116
|
[C6C.WHERE]: { [Actor.ACTOR_ID]: testId },
|
|
85
117
|
} as any);
|
|
86
118
|
expect(data?.rest).toHaveLength(1);
|
|
87
119
|
expect(data?.rest[0].first_name).toBe("Updated");
|
|
88
120
|
|
|
89
|
-
await
|
|
121
|
+
await actorRequest("DELETE", {
|
|
90
122
|
[C6C.WHERE]: { [Actor.ACTOR_ID]: testId },
|
|
91
123
|
[C6C.DELETE]: true,
|
|
92
124
|
} as any);
|
|
93
|
-
data = await
|
|
125
|
+
data = await actorRequest("GET", {
|
|
94
126
|
[C6C.WHERE]: { [Actor.ACTOR_ID]: testId },
|
|
95
127
|
cacheResults: false,
|
|
96
128
|
} as any);
|
|
@@ -102,14 +134,14 @@ describe("ExpressHandler e2e", () => {
|
|
|
102
134
|
const first_name = `Json${Date.now()}`;
|
|
103
135
|
const last_name = `User${Date.now()}`;
|
|
104
136
|
|
|
105
|
-
await
|
|
137
|
+
await actorRequest("POST", {
|
|
106
138
|
first_name,
|
|
107
139
|
last_name,
|
|
108
140
|
} as any);
|
|
109
141
|
|
|
110
142
|
const payload = { greeting: "hello", flags: [1, true] };
|
|
111
143
|
|
|
112
|
-
let data = await
|
|
144
|
+
let data = await actorRequest("GET", {
|
|
113
145
|
[C6C.WHERE]: { [Actor.FIRST_NAME]: first_name, [Actor.LAST_NAME]: last_name },
|
|
114
146
|
[C6C.PAGINATION]: { [C6C.LIMIT]: 1 },
|
|
115
147
|
} as any);
|
|
@@ -117,12 +149,12 @@ describe("ExpressHandler e2e", () => {
|
|
|
117
149
|
const actorId = data?.rest?.[0]?.actor_id;
|
|
118
150
|
expect(actorId).toBeTruthy();
|
|
119
151
|
|
|
120
|
-
await
|
|
152
|
+
await actorRequest("PUT", {
|
|
121
153
|
[C6C.WHERE]: { [Actor.ACTOR_ID]: actorId },
|
|
122
154
|
[C6C.UPDATE]: { first_name: payload },
|
|
123
155
|
} as any);
|
|
124
156
|
|
|
125
|
-
data = await
|
|
157
|
+
data = await actorRequest("GET", {
|
|
126
158
|
[C6C.WHERE]: { [Actor.ACTOR_ID]: actorId },
|
|
127
159
|
} as any);
|
|
128
160
|
|
|
@@ -133,12 +165,12 @@ describe("ExpressHandler e2e", () => {
|
|
|
133
165
|
const first_name = `Invalid${Date.now()}`;
|
|
134
166
|
const last_name = `User${Date.now()}`;
|
|
135
167
|
|
|
136
|
-
await
|
|
168
|
+
await actorRequest("POST", {
|
|
137
169
|
first_name,
|
|
138
170
|
last_name,
|
|
139
171
|
} as any);
|
|
140
172
|
|
|
141
|
-
const data = await
|
|
173
|
+
const data = await actorRequest("GET", {
|
|
142
174
|
[C6C.WHERE]: { [Actor.FIRST_NAME]: first_name, [Actor.LAST_NAME]: last_name },
|
|
143
175
|
[C6C.PAGINATION]: { [C6C.LIMIT]: 1 },
|
|
144
176
|
} as any);
|
|
@@ -148,14 +180,14 @@ describe("ExpressHandler e2e", () => {
|
|
|
148
180
|
|
|
149
181
|
const operatorLike = { [C6C.GREATER_THAN]: "oops" } as any;
|
|
150
182
|
|
|
151
|
-
const prevRestUrl = GLOBAL_REST_PARAMETERS.restURL;
|
|
152
|
-
const prevAxios = GLOBAL_REST_PARAMETERS.axios;
|
|
153
|
-
GLOBAL_REST_PARAMETERS.mysqlPool = pool as any;
|
|
154
|
-
delete (GLOBAL_REST_PARAMETERS as any).restURL;
|
|
155
|
-
delete (GLOBAL_REST_PARAMETERS as any).axios;
|
|
156
|
-
|
|
157
183
|
try {
|
|
158
|
-
|
|
184
|
+
const actorSql = restOrm<any>(() => ({
|
|
185
|
+
C6,
|
|
186
|
+
restModel: C6.TABLES.actor,
|
|
187
|
+
mysqlPool: pool,
|
|
188
|
+
verbose: false,
|
|
189
|
+
}));
|
|
190
|
+
await actorSql.Put({
|
|
159
191
|
[C6C.WHERE]: { [Actor.ACTOR_ID]: actorId },
|
|
160
192
|
[C6C.UPDATE]: { first_name: operatorLike },
|
|
161
193
|
} as any);
|
|
@@ -163,16 +195,10 @@ describe("ExpressHandler e2e", () => {
|
|
|
163
195
|
} catch (error: any) {
|
|
164
196
|
const message = String(error?.message ?? error);
|
|
165
197
|
expect(message).toMatch(/operand/i);
|
|
166
|
-
} finally {
|
|
167
|
-
GLOBAL_REST_PARAMETERS.restURL = prevRestUrl;
|
|
168
|
-
GLOBAL_REST_PARAMETERS.axios = prevAxios;
|
|
169
|
-
// @ts-ignore
|
|
170
|
-
delete GLOBAL_REST_PARAMETERS.mysqlPool;
|
|
171
198
|
}
|
|
172
199
|
});
|
|
173
200
|
|
|
174
201
|
it("respects METHOD=GET override on POST", async () => {
|
|
175
|
-
const {restURL} = GLOBAL_REST_PARAMETERS;
|
|
176
202
|
const table = Actor.TABLE_NAME;
|
|
177
203
|
|
|
178
204
|
const response = await axios.post(`${restURL}${table}?METHOD=GET`, {
|
|
@@ -185,7 +211,6 @@ describe("ExpressHandler e2e", () => {
|
|
|
185
211
|
});
|
|
186
212
|
|
|
187
213
|
it("ignores unsupported METHOD overrides", async () => {
|
|
188
|
-
const {restURL} = GLOBAL_REST_PARAMETERS;
|
|
189
214
|
const table = Actor.TABLE_NAME;
|
|
190
215
|
|
|
191
216
|
const first_name = `Override${Date.now()}`;
|
|
@@ -201,10 +226,9 @@ describe("ExpressHandler e2e", () => {
|
|
|
201
226
|
});
|
|
202
227
|
|
|
203
228
|
it("allows composite keys when a URL primary is present", async () => {
|
|
204
|
-
const {restURL} = GLOBAL_REST_PARAMETERS;
|
|
205
229
|
const table = Film_Actor.TABLE_NAME;
|
|
206
230
|
|
|
207
|
-
const seed = await
|
|
231
|
+
const seed = await filmActorRequest("GET", {
|
|
208
232
|
[C6C.PAGINATION]: { [C6C.LIMIT]: 1 },
|
|
209
233
|
} as any);
|
|
210
234
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import express, {Express} from "express";
|
|
2
2
|
import {Pool} from "mysql2/promise";
|
|
3
3
|
import {iC6Object} from "../../types/ormInterfaces";
|
|
4
|
-
import {
|
|
4
|
+
import {restExpressRequest} from "../../handlers/ExpressHandler";
|
|
5
5
|
|
|
6
6
|
export function createTestServer({
|
|
7
7
|
C6,
|
|
@@ -15,8 +15,12 @@ export function createTestServer({
|
|
|
15
15
|
const app = express();
|
|
16
16
|
app.set('query parser', 'extended');
|
|
17
17
|
app.use(express.json());
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
restExpressRequest({
|
|
19
|
+
router: app,
|
|
20
|
+
C6,
|
|
21
|
+
mysqlPool,
|
|
22
|
+
sqlAllowListPath,
|
|
23
|
+
});
|
|
20
24
|
return app;
|
|
21
25
|
}
|
|
22
26
|
|
|
@@ -2,12 +2,32 @@ import mysql from "mysql2/promise";
|
|
|
2
2
|
import axios from "axios";
|
|
3
3
|
import { AddressInfo } from "net";
|
|
4
4
|
import { describe, it, expect, beforeAll, afterAll, vi } from "vitest";
|
|
5
|
-
import {
|
|
5
|
+
import { restOrm } from "@carbonorm/carbonnode";
|
|
6
|
+
import { Actor, C6 } from "./sakila-db/C6.js";
|
|
6
7
|
import { C6C } from "../constants/C6Constants";
|
|
7
8
|
import createTestServer from "./fixtures/createTestServer";
|
|
8
9
|
|
|
9
10
|
let pool: mysql.Pool;
|
|
10
11
|
let server: any;
|
|
12
|
+
let restURL: string;
|
|
13
|
+
let axiosClient: ReturnType<typeof axios.create>;
|
|
14
|
+
const actorHttp = restOrm<any>(() => ({
|
|
15
|
+
C6,
|
|
16
|
+
restModel: C6.TABLES.actor,
|
|
17
|
+
restURL,
|
|
18
|
+
axios: axiosClient,
|
|
19
|
+
verbose: false,
|
|
20
|
+
}));
|
|
21
|
+
|
|
22
|
+
const actorRequest = async (
|
|
23
|
+
method: "GET" | "POST" | "PUT" | "DELETE",
|
|
24
|
+
request: any
|
|
25
|
+
) => {
|
|
26
|
+
if (method === "GET") return actorHttp.Get(request as any);
|
|
27
|
+
if (method === "POST") return actorHttp.Post(request as any);
|
|
28
|
+
if (method === "PUT") return actorHttp.Put(request as any);
|
|
29
|
+
return actorHttp.Delete(request as any);
|
|
30
|
+
};
|
|
11
31
|
|
|
12
32
|
beforeAll(async () => {
|
|
13
33
|
pool = mysql.createPool({
|
|
@@ -22,17 +42,20 @@ beforeAll(async () => {
|
|
|
22
42
|
await new Promise(resolve => server.on("listening", resolve));
|
|
23
43
|
const { port } = server.address() as AddressInfo;
|
|
24
44
|
|
|
25
|
-
|
|
26
|
-
|
|
45
|
+
restURL = `http://127.0.0.1:${port}/rest/`;
|
|
46
|
+
axiosClient = axios.create();
|
|
27
47
|
axiosClient.interceptors.response.use(
|
|
28
48
|
response => response,
|
|
29
|
-
error =>
|
|
49
|
+
error => {
|
|
50
|
+
const serverError = error?.response?.data?.error;
|
|
51
|
+
const message =
|
|
52
|
+
serverError?.message
|
|
53
|
+
?? (typeof serverError === "string" ? serverError : undefined)
|
|
54
|
+
?? error?.message
|
|
55
|
+
?? "Request failed";
|
|
56
|
+
return Promise.reject(new Error(message));
|
|
57
|
+
},
|
|
30
58
|
);
|
|
31
|
-
GLOBAL_REST_PARAMETERS.axios = axiosClient;
|
|
32
|
-
GLOBAL_REST_PARAMETERS.verbose = false;
|
|
33
|
-
// ensure HTTP executor is used
|
|
34
|
-
// @ts-ignore
|
|
35
|
-
delete GLOBAL_REST_PARAMETERS.mysqlPool;
|
|
36
59
|
});
|
|
37
60
|
|
|
38
61
|
afterAll(async () => {
|
|
@@ -44,59 +67,73 @@ describe("HttpExecutor singular e2e", () => {
|
|
|
44
67
|
it("handles CRUD with singular objects", async () => {
|
|
45
68
|
const first_name = `Test${Date.now()}`;
|
|
46
69
|
const last_name = `User${Date.now()}`;
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
70
|
+
let step = "POST";
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
// POST
|
|
74
|
+
await actorRequest("POST", { first_name, last_name } as any);
|
|
75
|
+
|
|
76
|
+
step = "GET-complex";
|
|
77
|
+
// Fetch inserted id using complex query
|
|
78
|
+
let data = await actorRequest("GET", {
|
|
79
|
+
[C6C.WHERE]: { [Actor.FIRST_NAME]: first_name, [Actor.LAST_NAME]: last_name },
|
|
80
|
+
[C6C.PAGINATION]: { [C6C.LIMIT]: 1 },
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
expect(data.rest).toHaveLength(1);
|
|
84
|
+
const testId = data.rest[0].actor_id;
|
|
85
|
+
|
|
86
|
+
step = "GET-singular";
|
|
87
|
+
// GET singular
|
|
88
|
+
data = await actorRequest("GET", { actor_id: testId } as any);
|
|
89
|
+
expect(data.rest).toHaveLength(1);
|
|
90
|
+
expect(data.rest[0].actor_id).toBe(testId);
|
|
91
|
+
|
|
92
|
+
step = "PUT-singular";
|
|
93
|
+
// PUT singular
|
|
94
|
+
await actorRequest("PUT", { actor_id: testId, first_name: "Updated" } as any);
|
|
95
|
+
data = await actorRequest("GET", { actor_id: testId, cacheResults: false } as any);
|
|
96
|
+
expect(data.rest).toHaveLength(1);
|
|
97
|
+
expect(data.rest[0].first_name).toBe("Updated");
|
|
98
|
+
|
|
99
|
+
step = "PUT-fq-react";
|
|
100
|
+
// PUT using fully qualified keys
|
|
101
|
+
const updateStub = vi.fn();
|
|
102
|
+
const reactBootstrap = {
|
|
103
|
+
updateRestfulObjectArrays: updateStub,
|
|
104
|
+
deleteRestfulObjectArrays: vi.fn(),
|
|
105
|
+
} as any;
|
|
106
|
+
const actorHttpWithReact = restOrm<any>(() => ({
|
|
107
|
+
C6,
|
|
108
|
+
restModel: C6.TABLES.actor,
|
|
109
|
+
restURL,
|
|
110
|
+
axios: axiosClient,
|
|
111
|
+
verbose: false,
|
|
112
|
+
reactBootstrap,
|
|
113
|
+
}));
|
|
114
|
+
await actorHttpWithReact.Put({
|
|
115
|
+
[Actor.ACTOR_ID]: testId,
|
|
116
|
+
[Actor.FIRST_NAME]: "UpdatedFQ",
|
|
117
|
+
} as any);
|
|
118
|
+
expect(updateStub).toHaveBeenCalled();
|
|
119
|
+
const args = updateStub.mock.calls[0][0];
|
|
120
|
+
expect(args.dataOrCallback[0]).toHaveProperty("first_name", "UpdatedFQ");
|
|
121
|
+
expect(args.dataOrCallback[0]).toHaveProperty("actor_id", testId);
|
|
122
|
+
expect(args.dataOrCallback[0]).not.toHaveProperty(Actor.FIRST_NAME);
|
|
123
|
+
|
|
124
|
+
step = "DELETE-singular";
|
|
125
|
+
// DELETE singular
|
|
126
|
+
await actorRequest("DELETE", { actor_id: testId } as any);
|
|
127
|
+
data = await actorRequest("GET", { actor_id: testId, cacheResults: false } as any);
|
|
128
|
+
expect(Array.isArray(data.rest)).toBe(true);
|
|
129
|
+
expect(data.rest.length).toBe(0);
|
|
130
|
+
} catch (error: any) {
|
|
131
|
+
throw new Error(`Failed at step ${step}: ${String(error?.message ?? error)}`);
|
|
132
|
+
}
|
|
96
133
|
});
|
|
97
134
|
|
|
98
135
|
it("exposes next when pagination continues", async () => {
|
|
99
|
-
const data = await
|
|
136
|
+
const data = await actorRequest("GET", {
|
|
100
137
|
[C6C.PAGINATION]: { [C6C.LIMIT]: 2 },
|
|
101
138
|
} as any);
|
|
102
139
|
|
|
@@ -111,7 +148,7 @@ describe("HttpExecutor singular e2e", () => {
|
|
|
111
148
|
});
|
|
112
149
|
|
|
113
150
|
it("exposes limit 1 does not expose next", async () => {
|
|
114
|
-
const data = await
|
|
151
|
+
const data = await actorRequest("GET", {
|
|
115
152
|
[C6C.PAGINATION]: { [C6C.LIMIT]: 1 },
|
|
116
153
|
} as any);
|
|
117
154
|
|
|
@@ -67,4 +67,17 @@ describe("colorSql", () => {
|
|
|
67
67
|
expect(colored).toContain("\x1b[94mUPDATE\x1b[0m");
|
|
68
68
|
expect(colored).toContain("\x1b[94mDUPLICATE\x1b[0m");
|
|
69
69
|
});
|
|
70
|
+
|
|
71
|
+
it("collapses repeated multi-row value groups", () => {
|
|
72
|
+
const sql = `INSERT INTO \`valuation_report_comparables\` (a,b,c) VALUES
|
|
73
|
+
(?, ?, ?),
|
|
74
|
+
(?, ?, ?),
|
|
75
|
+
(?, ?, ?),
|
|
76
|
+
(?, ?, ?)`;
|
|
77
|
+
const colored = colorSql(sql);
|
|
78
|
+
const plain = stripAnsi(colored);
|
|
79
|
+
|
|
80
|
+
expect(plain).toContain("(? ×3) ×4");
|
|
81
|
+
expect(plain).not.toContain("(? ×3),");
|
|
82
|
+
});
|
|
70
83
|
});
|
|
@@ -1342,7 +1342,7 @@ export const TABLES = {
|
|
|
1342
1342
|
};
|
|
1343
1343
|
export const C6 = {
|
|
1344
1344
|
...C6Constants,
|
|
1345
|
-
C6VERSION: '6.0.
|
|
1345
|
+
C6VERSION: '6.0.13',
|
|
1346
1346
|
IMPORT: async (tableName) => {
|
|
1347
1347
|
tableName = tableName.toLowerCase();
|
|
1348
1348
|
// if tableName is not a key in the TABLES object then throw an error
|