@carbonorm/carbonnode 6.0.12 → 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 (73) hide show
  1. package/dist/executors/SqlExecutor.d.ts +1 -0
  2. package/dist/handlers/ExpressHandler.d.ts +6 -14
  3. package/dist/index.cjs.js +164 -40
  4. package/dist/index.cjs.js.map +1 -1
  5. package/dist/index.esm.js +164 -40
  6. package/dist/index.esm.js.map +1 -1
  7. package/dist/types/ormInterfaces.d.ts +13 -3
  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__/sqlBuilders.test.ts +46 -0
  64. package/src/api/convertForRequestBody.ts +9 -2
  65. package/src/api/restRequest.ts +1 -0
  66. package/src/executors/HttpExecutor.ts +1 -1
  67. package/src/executors/SqlExecutor.ts +64 -2
  68. package/src/handlers/ExpressHandler.ts +50 -39
  69. package/src/orm/builders/ConditionBuilder.ts +43 -1
  70. package/src/orm/queries/PostQueryBuilder.ts +24 -12
  71. package/src/types/ormInterfaces.ts +15 -3
  72. package/src/utils/cacheManager.ts +3 -4
  73. package/src/utils/colorSql.ts +18 -0
@@ -1,4 +1,4 @@
1
- import type { AxiosInstance, AxiosPromise, AxiosResponse } from "axios";
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: AxiosPromise<ResponseDataType>;
98
- response?: AxiosResponse & {
107
+ request: Promise<iCacheResponse<ResponseDataType>>;
108
+ response?: iCacheResponse<ResponseDataType> & {
99
109
  __carbonTiming?: {
100
110
  start: number;
101
111
  end: number;
@@ -1,9 +1,8 @@
1
- import type { AxiosPromise } from "axios";
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): AxiosPromise<ResponseDataType> | false;
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@carbonorm/carbonnode",
3
- "version": "6.0.12",
3
+ "version": "6.0.13",
4
4
  "browser": "dist/index.umd.js",
5
5
  "repository": {
6
6
  "type": "git",
@@ -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 {Actor, C6, Film_Actor, GLOBAL_REST_PARAMETERS} from "./sakila-db/C6.js";
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
- GLOBAL_REST_PARAMETERS.restURL = `http://127.0.0.1:${port}/rest/`;
26
- const axiosClient = axios.create();
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 Actor.Get({
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 Actor.Get({});
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 Actor.Post({
97
+ await actorRequest("POST", {
66
98
  first_name,
67
99
  last_name,
68
100
  } as any);
69
101
 
70
- let data = await Actor.Get({
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 Actor.Put({
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 Actor.Get({
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 Actor.Delete({
121
+ await actorRequest("DELETE", {
90
122
  [C6C.WHERE]: { [Actor.ACTOR_ID]: testId },
91
123
  [C6C.DELETE]: true,
92
124
  } as any);
93
- data = await Actor.Get({
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 Actor.Post({
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 Actor.Get({
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 Actor.Put({
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 Actor.Get({
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 Actor.Post({
168
+ await actorRequest("POST", {
137
169
  first_name,
138
170
  last_name,
139
171
  } as any);
140
172
 
141
- const data = await Actor.Get({
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
- await Actor.Put({
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 Film_Actor.Get({
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 {ExpressHandler} from "../../handlers/ExpressHandler";
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
- app.all("/rest/:table", ExpressHandler({C6, mysqlPool, sqlAllowListPath}));
19
- app.all("/rest/:table/:primary", ExpressHandler({C6, mysqlPool, sqlAllowListPath}));
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 { Actor, C6, GLOBAL_REST_PARAMETERS } from "./sakila-db/C6.js";
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
- GLOBAL_REST_PARAMETERS.restURL = `http://127.0.0.1:${port}/rest/`;
26
- const axiosClient = axios.create();
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 => Promise.reject(new Error(error?.message ?? "Request failed")),
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
- // POST
49
- await Actor.Post({ first_name, last_name } as any);
50
-
51
- // Fetch inserted id using complex query
52
- let data = await Actor.Get({
53
- [C6C.WHERE]: { [Actor.FIRST_NAME]: first_name, [Actor.LAST_NAME]: last_name },
54
- [C6C.PAGINATION]: { [C6C.LIMIT]: 1 },
55
- });
56
-
57
- expect(data.rest).toHaveLength(1);
58
- const testId = data.rest[0].actor_id;
59
-
60
- // GET singular
61
- data = await Actor.Get({ actor_id: testId } as any);
62
- expect(data.rest).toHaveLength(1);
63
- expect(data.rest[0].actor_id).toBe(testId);
64
-
65
- // PUT singular
66
- await Actor.Put({ actor_id: testId, first_name: "Updated" } as any);
67
- data = await Actor.Get({ actor_id: testId, cacheResults: false } as any);
68
- expect(data.rest).toHaveLength(1);
69
- expect(data.rest[0].first_name).toBe("Updated");
70
-
71
- // PUT using fully qualified keys
72
- const updateStub = vi.fn();
73
- GLOBAL_REST_PARAMETERS.reactBootstrap = {
74
- updateRestfulObjectArrays: updateStub,
75
- deleteRestfulObjectArrays: vi.fn(),
76
- } as any;
77
- await Actor.Put({
78
- [Actor.ACTOR_ID]: testId,
79
- [Actor.FIRST_NAME]: "UpdatedFQ",
80
- } as any);
81
- expect(updateStub).toHaveBeenCalled();
82
- const args = updateStub.mock.calls[0][0];
83
- expect(args.dataOrCallback[0]).toHaveProperty("first_name", "UpdatedFQ");
84
- expect(args.dataOrCallback[0]).toHaveProperty("actor_id", testId);
85
- expect(args.dataOrCallback[0]).not.toHaveProperty(Actor.FIRST_NAME);
86
- GLOBAL_REST_PARAMETERS.reactBootstrap = undefined as any;
87
- data = await Actor.Get({ actor_id: testId, cacheResults: false } as any);
88
- expect(data.rest).toHaveLength(1);
89
- expect(data.rest[0].first_name).toBe("UpdatedFQ");
90
-
91
- // DELETE singular
92
- await Actor.Delete({ actor_id: testId } as any);
93
- data = await Actor.Get({ actor_id: testId, cacheResults: false } as any);
94
- expect(Array.isArray(data.rest)).toBe(true);
95
- expect(data.rest.length).toBe(0);
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 Actor.Get({
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 Actor.Get({
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.12',
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