@carbonorm/carbonnode 3.11.0 → 4.0.0
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/api/orm/builders/ConditionBuilder.d.ts +3 -0
- package/dist/api/orm/queries/PostQueryBuilder.d.ts +1 -1
- package/dist/api/restOrm.d.ts +4 -4
- package/dist/api/types/ormInterfaces.d.ts +32 -12
- package/dist/api/utils/cacheManager.d.ts +7 -8
- package/dist/index.cjs.js +267 -140
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.esm.js +263 -139
- package/dist/index.esm.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/cacheManager.test.ts +67 -0
- package/src/__tests__/expressServer.e2e.test.ts +104 -2
- package/src/__tests__/fixtures/c6.fixture.ts +2 -0
- package/src/__tests__/httpExecutorSingular.e2e.test.ts +35 -4
- package/src/__tests__/sakila-db/C6.js +1 -1
- package/src/__tests__/sakila-db/C6.ts +1 -1
- package/src/__tests__/sqlBuilders.test.ts +28 -0
- package/src/api/axiosInstance.ts +29 -0
- package/src/api/executors/HttpExecutor.ts +73 -97
- package/src/api/handlers/ExpressHandler.ts +30 -7
- package/src/api/orm/builders/ConditionBuilder.ts +54 -0
- package/src/api/orm/queries/PostQueryBuilder.ts +4 -2
- package/src/api/orm/queries/UpdateQueryBuilder.ts +3 -1
- package/src/api/types/ormInterfaces.ts +32 -18
- package/src/api/utils/cacheManager.ts +75 -34
- package/src/api/utils/testHelpers.ts +5 -3
- package/src/variables/isNode.ts +1 -8
package/package.json
CHANGED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { AxiosPromise } from "axios";
|
|
2
|
+
import { describe, it, expect, beforeEach } from "vitest";
|
|
3
|
+
import {
|
|
4
|
+
apiRequestCache,
|
|
5
|
+
checkCache,
|
|
6
|
+
clearCache,
|
|
7
|
+
setCache,
|
|
8
|
+
} from "../api/utils/cacheManager";
|
|
9
|
+
import { checkAllRequestsComplete } from "../api/utils/testHelpers";
|
|
10
|
+
|
|
11
|
+
describe("cacheManager with map storage", () => {
|
|
12
|
+
const requestData = { id: 1 } as const;
|
|
13
|
+
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
clearCache({ ignoreWarning: true });
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("stores and returns cached requests", () => {
|
|
19
|
+
const mockRequest = Promise.resolve({ data: { rest: [] } }) as AxiosPromise;
|
|
20
|
+
|
|
21
|
+
setCache("GET", "table", requestData, {
|
|
22
|
+
requestArgumentsSerialized: "serialized",
|
|
23
|
+
request: mockRequest,
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const cached = checkCache("GET", "table", { ...requestData });
|
|
27
|
+
expect(cached).toBe(mockRequest);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("clears cache entries", () => {
|
|
31
|
+
const mockRequest = Promise.resolve({ data: { rest: [] } }) as AxiosPromise;
|
|
32
|
+
|
|
33
|
+
setCache("GET", "table", requestData, {
|
|
34
|
+
requestArgumentsSerialized: "serialized",
|
|
35
|
+
request: mockRequest,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
clearCache({ ignoreWarning: true });
|
|
39
|
+
|
|
40
|
+
expect(apiRequestCache.size).toBe(0);
|
|
41
|
+
expect(checkCache("GET", "table", requestData)).toBe(false);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("reports pending and completed requests via helper", () => {
|
|
45
|
+
const originalDocument = (global as any).document;
|
|
46
|
+
(global as any).document = {};
|
|
47
|
+
|
|
48
|
+
setCache("GET", "table", requestData, {
|
|
49
|
+
requestArgumentsSerialized: "pending",
|
|
50
|
+
request: Promise.resolve({ data: null }) as AxiosPromise,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
expect(checkAllRequestsComplete()).toEqual(["pending"]);
|
|
54
|
+
|
|
55
|
+
setCache("GET", "table", requestData, {
|
|
56
|
+
requestArgumentsSerialized: "pending",
|
|
57
|
+
request: Promise.resolve({ data: null }) as AxiosPromise,
|
|
58
|
+
response: {} as any,
|
|
59
|
+
final: true,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
expect(checkAllRequestsComplete()).toBe(true);
|
|
63
|
+
|
|
64
|
+
(global as any).document = originalDocument;
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
@@ -2,7 +2,7 @@ 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, GLOBAL_REST_PARAMETERS} from "./sakila-db/C6.js";
|
|
5
|
+
import {Actor, C6, Film_Actor, GLOBAL_REST_PARAMETERS} from "./sakila-db/C6.js";
|
|
6
6
|
import {C6C} from "../api/C6Constants";
|
|
7
7
|
import createTestServer from "./fixtures/createTestServer";
|
|
8
8
|
|
|
@@ -23,7 +23,12 @@ beforeAll(async () => {
|
|
|
23
23
|
const {port} = server.address() as AddressInfo;
|
|
24
24
|
|
|
25
25
|
GLOBAL_REST_PARAMETERS.restURL = `http://127.0.0.1:${port}/rest/`;
|
|
26
|
-
|
|
26
|
+
const axiosClient = axios.create();
|
|
27
|
+
axiosClient.interceptors.response.use(
|
|
28
|
+
response => response,
|
|
29
|
+
error => Promise.reject(new Error(error?.message ?? 'Request failed')),
|
|
30
|
+
);
|
|
31
|
+
GLOBAL_REST_PARAMETERS.axios = axiosClient;
|
|
27
32
|
GLOBAL_REST_PARAMETERS.verbose = false;
|
|
28
33
|
// ensure HTTP executor is used
|
|
29
34
|
// @ts-ignore
|
|
@@ -93,6 +98,79 @@ describe("ExpressHandler e2e", () => {
|
|
|
93
98
|
expect(data?.rest.length).toBe(0);
|
|
94
99
|
});
|
|
95
100
|
|
|
101
|
+
it("stringifies plain object values in PUT updates", async () => {
|
|
102
|
+
const first_name = `Json${Date.now()}`;
|
|
103
|
+
const last_name = `User${Date.now()}`;
|
|
104
|
+
|
|
105
|
+
await Actor.Post({
|
|
106
|
+
first_name,
|
|
107
|
+
last_name,
|
|
108
|
+
} as any);
|
|
109
|
+
|
|
110
|
+
const payload = { greeting: "hello", flags: [1, true] };
|
|
111
|
+
|
|
112
|
+
let data = await Actor.Get({
|
|
113
|
+
[C6C.WHERE]: { [Actor.FIRST_NAME]: first_name, [Actor.LAST_NAME]: last_name },
|
|
114
|
+
[C6C.PAGINATION]: { [C6C.LIMIT]: 1 },
|
|
115
|
+
} as any);
|
|
116
|
+
|
|
117
|
+
const actorId = data?.rest?.[0]?.actor_id;
|
|
118
|
+
expect(actorId).toBeTruthy();
|
|
119
|
+
|
|
120
|
+
await Actor.Put({
|
|
121
|
+
[C6C.WHERE]: { [Actor.ACTOR_ID]: actorId },
|
|
122
|
+
[C6C.UPDATE]: { first_name: payload },
|
|
123
|
+
} as any);
|
|
124
|
+
|
|
125
|
+
data = await Actor.Get({
|
|
126
|
+
[C6C.WHERE]: { [Actor.ACTOR_ID]: actorId },
|
|
127
|
+
} as any);
|
|
128
|
+
|
|
129
|
+
expect(data?.rest?.[0]?.first_name).toBe(JSON.stringify(payload));
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("rejects operator-like objects in PUT updates", async () => {
|
|
133
|
+
const first_name = `Invalid${Date.now()}`;
|
|
134
|
+
const last_name = `User${Date.now()}`;
|
|
135
|
+
|
|
136
|
+
await Actor.Post({
|
|
137
|
+
first_name,
|
|
138
|
+
last_name,
|
|
139
|
+
} as any);
|
|
140
|
+
|
|
141
|
+
const data = await Actor.Get({
|
|
142
|
+
[C6C.WHERE]: { [Actor.FIRST_NAME]: first_name, [Actor.LAST_NAME]: last_name },
|
|
143
|
+
[C6C.PAGINATION]: { [C6C.LIMIT]: 1 },
|
|
144
|
+
} as any);
|
|
145
|
+
|
|
146
|
+
const actorId = data?.rest?.[0]?.actor_id;
|
|
147
|
+
expect(actorId).toBeTruthy();
|
|
148
|
+
|
|
149
|
+
const operatorLike = { [C6C.GREATER_THAN]: "oops" } as any;
|
|
150
|
+
|
|
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
|
+
try {
|
|
158
|
+
await Actor.Put({
|
|
159
|
+
[C6C.WHERE]: { [Actor.ACTOR_ID]: actorId },
|
|
160
|
+
[C6C.UPDATE]: { first_name: operatorLike },
|
|
161
|
+
} as any);
|
|
162
|
+
throw new Error('Expected PUT to reject for operator-like payload.');
|
|
163
|
+
} catch (error: any) {
|
|
164
|
+
const message = String(error?.message ?? error);
|
|
165
|
+
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
|
+
}
|
|
172
|
+
});
|
|
173
|
+
|
|
96
174
|
it("respects METHOD=GET override on POST", async () => {
|
|
97
175
|
const {restURL} = GLOBAL_REST_PARAMETERS;
|
|
98
176
|
const table = Actor.TABLE_NAME;
|
|
@@ -121,4 +199,28 @@ describe("ExpressHandler e2e", () => {
|
|
|
121
199
|
expect(response.status).toBe(200);
|
|
122
200
|
expect(response.data?.success).toBeTruthy();
|
|
123
201
|
});
|
|
202
|
+
|
|
203
|
+
it("allows composite keys when a URL primary is present", async () => {
|
|
204
|
+
const {restURL} = GLOBAL_REST_PARAMETERS;
|
|
205
|
+
const table = Film_Actor.TABLE_NAME;
|
|
206
|
+
|
|
207
|
+
const seed = await Film_Actor.Get({
|
|
208
|
+
[C6C.PAGINATION]: { [C6C.LIMIT]: 1 },
|
|
209
|
+
} as any);
|
|
210
|
+
|
|
211
|
+
const filmActor = seed.rest[0];
|
|
212
|
+
expect(filmActor).toBeTruthy();
|
|
213
|
+
|
|
214
|
+
const response = await axios.post(`${restURL}${table}/${filmActor.actor_id}?METHOD=GET`, {
|
|
215
|
+
[C6C.WHERE]: {
|
|
216
|
+
[Film_Actor.ACTOR_ID]: filmActor.actor_id,
|
|
217
|
+
[Film_Actor.FILM_ID]: filmActor.film_id,
|
|
218
|
+
},
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
expect(response.status).toBe(200);
|
|
222
|
+
expect(response.data?.rest).toHaveLength(1);
|
|
223
|
+
expect(response.data?.rest?.[0]?.actor_id).toBe(filmActor.actor_id);
|
|
224
|
+
expect(response.data?.rest?.[0]?.film_id).toBe(filmActor.film_id);
|
|
225
|
+
});
|
|
124
226
|
});
|
|
@@ -43,6 +43,7 @@ export function buildTestConfig() {
|
|
|
43
43
|
'actor.first_name': 'first_name',
|
|
44
44
|
'actor.last_name': 'last_name',
|
|
45
45
|
'actor.binarycol': 'binarycol',
|
|
46
|
+
'actor.json_data': 'json_data',
|
|
46
47
|
} as const;
|
|
47
48
|
|
|
48
49
|
const filmActorCols = {
|
|
@@ -62,6 +63,7 @@ export function buildTestConfig() {
|
|
|
62
63
|
|
|
63
64
|
// Special-case: mark binary column as BINARY to test conversion
|
|
64
65
|
C6.TABLES.actor.TYPE_VALIDATION['binarycol'].MYSQL_TYPE = 'BINARY(16)';
|
|
66
|
+
C6.TABLES.actor.TYPE_VALIDATION['json_data'].MYSQL_TYPE = 'JSON';
|
|
65
67
|
|
|
66
68
|
const baseConfig: iRest<any, any, any> = {
|
|
67
69
|
C6,
|
|
@@ -23,7 +23,12 @@ beforeAll(async () => {
|
|
|
23
23
|
const { port } = server.address() as AddressInfo;
|
|
24
24
|
|
|
25
25
|
GLOBAL_REST_PARAMETERS.restURL = `http://127.0.0.1:${port}/rest/`;
|
|
26
|
-
|
|
26
|
+
const axiosClient = axios.create();
|
|
27
|
+
axiosClient.interceptors.response.use(
|
|
28
|
+
response => response,
|
|
29
|
+
error => Promise.reject(new Error(error?.message ?? "Request failed")),
|
|
30
|
+
);
|
|
31
|
+
GLOBAL_REST_PARAMETERS.axios = axiosClient;
|
|
27
32
|
GLOBAL_REST_PARAMETERS.verbose = false;
|
|
28
33
|
// ensure HTTP executor is used
|
|
29
34
|
// @ts-ignore
|
|
@@ -41,14 +46,14 @@ describe("HttpExecutor singular e2e", () => {
|
|
|
41
46
|
const last_name = `User${Date.now()}`;
|
|
42
47
|
|
|
43
48
|
// POST
|
|
44
|
-
|
|
45
|
-
expect(postRes.affected).toBe(1);
|
|
49
|
+
await Actor.Post({ first_name, last_name } as any);
|
|
46
50
|
|
|
47
51
|
// Fetch inserted id using complex query
|
|
48
52
|
let data = await Actor.Get({
|
|
49
53
|
[C6C.WHERE]: { [Actor.FIRST_NAME]: first_name, [Actor.LAST_NAME]: last_name },
|
|
50
54
|
[C6C.PAGINATION]: { [C6C.LIMIT]: 1 },
|
|
51
|
-
}
|
|
55
|
+
});
|
|
56
|
+
|
|
52
57
|
expect(data.rest).toHaveLength(1);
|
|
53
58
|
const testId = data.rest[0].actor_id;
|
|
54
59
|
|
|
@@ -89,4 +94,30 @@ describe("HttpExecutor singular e2e", () => {
|
|
|
89
94
|
expect(Array.isArray(data.rest)).toBe(true);
|
|
90
95
|
expect(data.rest.length).toBe(0);
|
|
91
96
|
});
|
|
97
|
+
|
|
98
|
+
it("exposes next when pagination continues", async () => {
|
|
99
|
+
const data = await Actor.Get({
|
|
100
|
+
[C6C.PAGINATION]: { [C6C.LIMIT]: 2 },
|
|
101
|
+
} as any);
|
|
102
|
+
|
|
103
|
+
expect(Array.isArray(data.rest)).toBe(true);
|
|
104
|
+
expect(data.rest).toHaveLength(2);
|
|
105
|
+
expect(typeof data.next).toBe("function");
|
|
106
|
+
|
|
107
|
+
const nextPage = await data.next?.();
|
|
108
|
+
expect(nextPage?.rest).toBeDefined();
|
|
109
|
+
expect(Array.isArray(nextPage?.rest)).toBe(true);
|
|
110
|
+
expect(nextPage?.rest?.length).toBeGreaterThan(0);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("exposes limit 1 does not expose next", async () => {
|
|
114
|
+
const data = await Actor.Get({
|
|
115
|
+
[C6C.PAGINATION]: { [C6C.LIMIT]: 1 },
|
|
116
|
+
} as any);
|
|
117
|
+
|
|
118
|
+
expect(Array.isArray(data.rest)).toBe(true);
|
|
119
|
+
expect(data.rest).toHaveLength(1);
|
|
120
|
+
expect(typeof data.next).toBe("undefined");
|
|
121
|
+
|
|
122
|
+
});
|
|
92
123
|
});
|
|
@@ -1342,7 +1342,7 @@ export const TABLES = {
|
|
|
1342
1342
|
};
|
|
1343
1343
|
export const C6 = {
|
|
1344
1344
|
...C6Constants,
|
|
1345
|
-
C6VERSION: '
|
|
1345
|
+
C6VERSION: '4.0.0',
|
|
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
|
|
@@ -2009,7 +2009,7 @@ export type RestTableInterfaces = iActor
|
|
|
2009
2009
|
|
|
2010
2010
|
export const C6 : iC6Object<RestTableInterfaces> = {
|
|
2011
2011
|
...C6Constants,
|
|
2012
|
-
C6VERSION: '
|
|
2012
|
+
C6VERSION: '4.0.0',
|
|
2013
2013
|
IMPORT: async (tableName: string) : Promise<iDynamicApiImport> => {
|
|
2014
2014
|
|
|
2015
2015
|
tableName = tableName.toLowerCase();
|
|
@@ -61,6 +61,34 @@ describe('SQL Builders', () => {
|
|
|
61
61
|
expect(params).toEqual(['BOB', 'SMITH']);
|
|
62
62
|
});
|
|
63
63
|
|
|
64
|
+
it('stringifies plain object inserts for JSON columns', () => {
|
|
65
|
+
const config = buildTestConfig();
|
|
66
|
+
const payload = { hello: 'world', nested: { ok: true } };
|
|
67
|
+
const qb = new PostQueryBuilder(config as any, {
|
|
68
|
+
[C6C.INSERT]: {
|
|
69
|
+
'actor.json_data': payload,
|
|
70
|
+
},
|
|
71
|
+
} as any, false);
|
|
72
|
+
|
|
73
|
+
const { sql, params } = qb.build('actor');
|
|
74
|
+
|
|
75
|
+
expect(sql).toContain('`json_data`');
|
|
76
|
+
expect(params).toEqual([JSON.stringify(payload)]);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('throws on operator-shaped insert payloads', () => {
|
|
80
|
+
const config = buildTestConfig();
|
|
81
|
+
const qb = new PostQueryBuilder(config as any, {
|
|
82
|
+
[C6C.INSERT]: {
|
|
83
|
+
'actor.first_name': {
|
|
84
|
+
[C6C.GREATER_THAN]: 'ALICE',
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
} as any, false);
|
|
88
|
+
|
|
89
|
+
expect(() => qb.build('actor')).toThrowError(/requires two operands/);
|
|
90
|
+
});
|
|
91
|
+
|
|
64
92
|
it('builds UPDATE with WHERE and pagination', () => {
|
|
65
93
|
const config = buildTestConfig();
|
|
66
94
|
const qb = new UpdateQueryBuilder(config as any, {
|
package/src/api/axiosInstance.ts
CHANGED
|
@@ -151,7 +151,36 @@ axiosInstance.interceptors.request.use((config) => {
|
|
|
151
151
|
}
|
|
152
152
|
}
|
|
153
153
|
|
|
154
|
+
(config as any).__carbonStart = performance.now();
|
|
155
|
+
|
|
154
156
|
return config;
|
|
155
157
|
});
|
|
156
158
|
|
|
159
|
+
|
|
160
|
+
axiosInstance.interceptors.response.use(
|
|
161
|
+
(response) => {
|
|
162
|
+
const end = performance.now();
|
|
163
|
+
const start = (response.config as any).__carbonStart;
|
|
164
|
+
(response as any).__carbonTiming = {
|
|
165
|
+
start,
|
|
166
|
+
end,
|
|
167
|
+
duration: end - start,
|
|
168
|
+
};
|
|
169
|
+
return response;
|
|
170
|
+
},
|
|
171
|
+
(error) => {
|
|
172
|
+
if (error.config) {
|
|
173
|
+
const end = performance.now();
|
|
174
|
+
const start = (error.config as any).__carbonStart;
|
|
175
|
+
(error as any).__carbonTiming = {
|
|
176
|
+
start,
|
|
177
|
+
end,
|
|
178
|
+
duration: end - start,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
throw error;
|
|
182
|
+
}
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
|
|
157
186
|
export default axiosInstance
|
|
@@ -8,14 +8,13 @@ import {OrmGenerics} from "../types/ormGenerics";
|
|
|
8
8
|
import {
|
|
9
9
|
DELETE, DetermineResponseDataType,
|
|
10
10
|
GET,
|
|
11
|
-
iCacheAPI,
|
|
12
11
|
iConstraint,
|
|
13
|
-
|
|
12
|
+
C6RestResponse,
|
|
14
13
|
POST,
|
|
15
14
|
PUT, RequestQueryBody
|
|
16
15
|
} from "../types/ormInterfaces";
|
|
17
16
|
import {removeInvalidKeys, removePrefixIfExists, TestRestfulResponse} from "../utils/apiHelpers";
|
|
18
|
-
import {
|
|
17
|
+
import {checkCache, setCache, userCustomClearCache} from "../utils/cacheManager";
|
|
19
18
|
import {sortAndSerializeQueryObject} from "../utils/sortAndSerializeQueryObject";
|
|
20
19
|
import {Executor} from "./Executor";
|
|
21
20
|
import {toastOptions, toastOptionsDevs} from "variables/toastOptions";
|
|
@@ -27,8 +26,11 @@ export class HttpExecutor<
|
|
|
27
26
|
|
|
28
27
|
private isRestResponse<T extends Record<string, any>>(
|
|
29
28
|
r: AxiosResponse<any>
|
|
30
|
-
): r is AxiosResponse<
|
|
31
|
-
return !!r
|
|
29
|
+
): r is AxiosResponse<C6RestResponse<'GET', T>> {
|
|
30
|
+
return !!r
|
|
31
|
+
&& r.data != null
|
|
32
|
+
&& typeof r.data === 'object'
|
|
33
|
+
&& Array.isArray((r.data as C6RestResponse<'GET', T>).rest);
|
|
32
34
|
}
|
|
33
35
|
|
|
34
36
|
private stripTableNameFromKeys<T extends Record<string, any>>(obj: Partial<T> | undefined | null): Partial<T> {
|
|
@@ -182,20 +184,6 @@ export class HttpExecutor<
|
|
|
182
184
|
console.groupEnd()
|
|
183
185
|
}
|
|
184
186
|
|
|
185
|
-
// an undefined query would indicate queryCallback returned undefined,
|
|
186
|
-
// thus the request shouldn't fire as is in custom cache
|
|
187
|
-
if (undefined === this.request || null === this.request) {
|
|
188
|
-
|
|
189
|
-
if (isLocal()) {
|
|
190
|
-
console.groupCollapsed(`API: (${requestMethod}) (${tableName}) query undefined/null → returning null`)
|
|
191
|
-
console.log('request', this.request)
|
|
192
|
-
console.groupEnd()
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
return null;
|
|
196
|
-
|
|
197
|
-
}
|
|
198
|
-
|
|
199
187
|
let query = this.request;
|
|
200
188
|
|
|
201
189
|
// this is parameterless and could return itself with a new page number, or undefined if the end is reached
|
|
@@ -219,14 +207,6 @@ export class HttpExecutor<
|
|
|
219
207
|
console.groupEnd()
|
|
220
208
|
}
|
|
221
209
|
|
|
222
|
-
// The problem with creating cache keys with a stringified object is the order of keys matters and it's possible for the same query to be stringified differently.
|
|
223
|
-
// Here we ensure the key order will be identical between two of the same requests. https://stackoverflow.com/questions/5467129/sort-javascript-object-by-key
|
|
224
|
-
|
|
225
|
-
// literally impossible for query to be undefined or null here but the editor is too busy licking windows to understand that
|
|
226
|
-
let querySerialized: string = sortAndSerializeQueryObject(tables, query ?? {});
|
|
227
|
-
|
|
228
|
-
let cacheResult: iCacheAPI | undefined = apiRequestCache.find(cache => cache.requestArgumentsSerialized === querySerialized);
|
|
229
|
-
|
|
230
210
|
let cachingConfirmed = false;
|
|
231
211
|
|
|
232
212
|
// determine if we need to paginate.
|
|
@@ -253,52 +233,42 @@ export class HttpExecutor<
|
|
|
253
233
|
|
|
254
234
|
query[C6.PAGINATION][C6.LIMIT] = query[C6.PAGINATION][C6.LIMIT] || 100;
|
|
255
235
|
|
|
256
|
-
|
|
257
|
-
if (true === cacheResults) {
|
|
258
|
-
if (undefined !== cacheResult) {
|
|
259
|
-
do {
|
|
260
|
-
const cacheCheck = checkCache<ResponseDataType>(cacheResult, requestMethod, tableName, this.request);
|
|
261
|
-
if (false !== cacheCheck) {
|
|
262
|
-
return (await cacheCheck).data;
|
|
263
|
-
}
|
|
264
|
-
++query[C6.PAGINATION][C6.PAGE];
|
|
265
|
-
querySerialized = sortAndSerializeQueryObject(tables, query ?? {});
|
|
266
|
-
cacheResult = apiRequestCache.find(cache => cache.requestArgumentsSerialized === querySerialized)
|
|
267
|
-
} while (undefined !== cacheResult)
|
|
268
|
-
if (debug && isLocal()) {
|
|
269
|
-
toast.warning("DEVS: Request pages exhausted in cache; firing network.", toastOptionsDevs);
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
cachingConfirmed = true;
|
|
273
|
-
} else {
|
|
274
|
-
if (debug && isLocal()) toast.info("DEVS: Ignore cache was set to true.", toastOptionsDevs);
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
if (debug && isLocal()) {
|
|
278
|
-
toast.success("DEVS: Request not in cache." + (requestMethod === C6.GET ? " Page (" + query[C6.PAGINATION][C6.PAGE] + ")" : ''), toastOptionsDevs);
|
|
279
|
-
}
|
|
236
|
+
}
|
|
280
237
|
|
|
281
|
-
|
|
238
|
+
// The problem with creating cache keys with a stringified object is the order of keys matters and it's possible for the same query to be stringified differently.
|
|
239
|
+
// Here we ensure the key order will be identical between two of the same requests. https://stackoverflow.com/questions/5467129/sort-javascript-object-by-key
|
|
240
|
+
const cacheRequestData = JSON.parse(JSON.stringify(query ?? {})) as RequestQueryBody<
|
|
241
|
+
G['RequestMethod'],
|
|
242
|
+
G['RestTableInterface'],
|
|
243
|
+
G['CustomAndRequiredFields'],
|
|
244
|
+
G['RequestTableOverrides']
|
|
245
|
+
>;
|
|
282
246
|
|
|
283
|
-
|
|
284
|
-
|
|
247
|
+
// literally impossible for query to be undefined or null here but the editor is too busy licking windows to understand that
|
|
248
|
+
let querySerialized: string = sortAndSerializeQueryObject(tables, cacheRequestData ?? {});
|
|
285
249
|
|
|
286
|
-
|
|
250
|
+
let cachedRequest: AxiosPromise<ResponseDataType> | false = false;
|
|
287
251
|
|
|
288
|
-
|
|
252
|
+
if (cacheResults) {
|
|
253
|
+
cachedRequest = checkCache<ResponseDataType>(requestMethod, tableName, cacheRequestData);
|
|
254
|
+
}
|
|
289
255
|
|
|
290
|
-
|
|
291
|
-
|
|
256
|
+
if (cachedRequest) {
|
|
257
|
+
return (await cachedRequest).data;
|
|
258
|
+
}
|
|
292
259
|
|
|
260
|
+
if (cacheResults) {
|
|
293
261
|
cachingConfirmed = true;
|
|
294
|
-
|
|
262
|
+
} else if (debug && isLocal()) {
|
|
263
|
+
toast.info("DEVS: Ignore cache was set to true.", toastOptionsDevs);
|
|
264
|
+
}
|
|
295
265
|
|
|
266
|
+
if (cacheResults && debug && isLocal()) {
|
|
267
|
+
toast.success("DEVS: Request not in cache." + (requestMethod === C6.GET ? " Page (" + query[C6.PAGINATION][C6.PAGE] + ")" : ''), toastOptionsDevs);
|
|
296
268
|
}
|
|
297
269
|
|
|
298
270
|
let apiResponse: G['RestTableInterface'][G['PrimaryKey']] | string | boolean | number | undefined;
|
|
299
271
|
|
|
300
|
-
let returnGetNextPageFunction = false;
|
|
301
|
-
|
|
302
272
|
let restRequestUri: string = restURL + operatingTable + '/';
|
|
303
273
|
|
|
304
274
|
const needsConditionOrPrimaryCheck = (PUT === requestMethod || DELETE === requestMethod)
|
|
@@ -460,22 +430,30 @@ export class HttpExecutor<
|
|
|
460
430
|
|
|
461
431
|
|
|
462
432
|
if (cachingConfirmed) {
|
|
463
|
-
|
|
464
|
-
// push to cache so we do not repeat the request
|
|
465
|
-
apiRequestCache.push({
|
|
433
|
+
setCache<ResponseDataType>(requestMethod, tableName, cacheRequestData, {
|
|
466
434
|
requestArgumentsSerialized: querySerialized,
|
|
467
|
-
request: axiosActiveRequest
|
|
435
|
+
request: axiosActiveRequest,
|
|
468
436
|
});
|
|
469
|
-
|
|
470
437
|
}
|
|
471
438
|
|
|
472
439
|
// returning the promise with this then is important for tests. todo - we could make that optional.
|
|
473
440
|
// https://rapidapi.com/guides/axios-async-await
|
|
474
441
|
return axiosActiveRequest.then(async (response: AxiosResponse<ResponseDataType, any>): Promise<AxiosResponse<ResponseDataType, any>> => {
|
|
475
442
|
|
|
443
|
+
let hasNext: boolean | undefined;
|
|
444
|
+
|
|
476
445
|
// noinspection SuspiciousTypeOfGuard
|
|
477
446
|
if (typeof response.data === 'string') {
|
|
478
447
|
|
|
448
|
+
if (cachingConfirmed) {
|
|
449
|
+
setCache<ResponseDataType>(requestMethod, tableName, cacheRequestData, {
|
|
450
|
+
requestArgumentsSerialized: querySerialized,
|
|
451
|
+
request: axiosActiveRequest,
|
|
452
|
+
response,
|
|
453
|
+
final: true,
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
|
|
479
457
|
if (isTest()) {
|
|
480
458
|
|
|
481
459
|
console.trace()
|
|
@@ -489,15 +467,11 @@ export class HttpExecutor<
|
|
|
489
467
|
}
|
|
490
468
|
|
|
491
469
|
if (cachingConfirmed) {
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
// only cache get method requests
|
|
499
|
-
apiRequestCache[cacheIndex].response = response
|
|
500
|
-
|
|
470
|
+
setCache<ResponseDataType>(requestMethod, tableName, cacheRequestData, {
|
|
471
|
+
requestArgumentsSerialized: querySerialized,
|
|
472
|
+
request: axiosActiveRequest,
|
|
473
|
+
response,
|
|
474
|
+
});
|
|
501
475
|
}
|
|
502
476
|
|
|
503
477
|
this.runLifecycleHooks<"afterExecution">(
|
|
@@ -549,27 +523,30 @@ export class HttpExecutor<
|
|
|
549
523
|
callback();
|
|
550
524
|
}
|
|
551
525
|
|
|
552
|
-
if (C6.GET === requestMethod && this.isRestResponse
|
|
526
|
+
if (C6.GET === requestMethod && this.isRestResponse(response)) {
|
|
553
527
|
|
|
554
528
|
const responseData = response.data;
|
|
555
529
|
|
|
556
530
|
const pageLimit = query?.[C6.PAGINATION]?.[C6.LIMIT];
|
|
531
|
+
|
|
557
532
|
const got = responseData.rest.length;
|
|
558
|
-
|
|
533
|
+
hasNext = pageLimit !== 1 && got === pageLimit;
|
|
559
534
|
|
|
560
535
|
if (hasNext) {
|
|
561
|
-
responseData.next = apiRequest
|
|
536
|
+
responseData.next = apiRequest as () => Promise<
|
|
537
|
+
DetermineResponseDataType<'GET', G['RestTableInterface']>
|
|
538
|
+
>;
|
|
562
539
|
} else {
|
|
563
540
|
responseData.next = undefined; // short page => done
|
|
564
541
|
}
|
|
565
542
|
|
|
566
|
-
// If you keep this flag, make it reflect reality:
|
|
567
|
-
returnGetNextPageFunction = hasNext;
|
|
568
|
-
|
|
569
|
-
// and fix cache ‘final’ flag to match:
|
|
570
543
|
if (cachingConfirmed) {
|
|
571
|
-
|
|
572
|
-
|
|
544
|
+
setCache<ResponseDataType>(requestMethod, tableName, cacheRequestData, {
|
|
545
|
+
requestArgumentsSerialized: querySerialized,
|
|
546
|
+
request: axiosActiveRequest,
|
|
547
|
+
response,
|
|
548
|
+
final: !hasNext,
|
|
549
|
+
});
|
|
573
550
|
}
|
|
574
551
|
|
|
575
552
|
if ((this.config.verbose || debug) && isLocal()) {
|
|
@@ -580,11 +557,9 @@ export class HttpExecutor<
|
|
|
580
557
|
}
|
|
581
558
|
|
|
582
559
|
// next already set above based on hasNext; avoid duplicate, inverted logic
|
|
583
|
-
|
|
584
|
-
|
|
585
560
|
if (fetchDependencies
|
|
586
561
|
&& 'number' === typeof fetchDependencies
|
|
587
|
-
&& responseData.rest
|
|
562
|
+
&& responseData.rest?.length > 0) {
|
|
588
563
|
|
|
589
564
|
console.groupCollapsed('%c API: Fetch Dependencies segment (' + requestMethod + ' ' + tableName + ')'
|
|
590
565
|
+ (fetchDependencies & eFetchDependencies.CHILDREN ? ' | (CHILDREN|REFERENCED) ' : '')
|
|
@@ -827,6 +802,15 @@ export class HttpExecutor<
|
|
|
827
802
|
|
|
828
803
|
}
|
|
829
804
|
|
|
805
|
+
if (cachingConfirmed && hasNext === undefined) {
|
|
806
|
+
setCache<ResponseDataType>(requestMethod, tableName, cacheRequestData, {
|
|
807
|
+
requestArgumentsSerialized: querySerialized,
|
|
808
|
+
request: axiosActiveRequest,
|
|
809
|
+
response,
|
|
810
|
+
final: true,
|
|
811
|
+
});
|
|
812
|
+
}
|
|
813
|
+
|
|
830
814
|
if (debug && isLocal()) {
|
|
831
815
|
|
|
832
816
|
toast.success("DEVS: (" + requestMethod + ") request complete.", toastOptionsDevs);
|
|
@@ -841,25 +825,17 @@ export class HttpExecutor<
|
|
|
841
825
|
|
|
842
826
|
} catch (throwableError) {
|
|
843
827
|
|
|
844
|
-
if (isTest()) {
|
|
845
|
-
|
|
846
|
-
throw new Error(JSON.stringify(throwableError))
|
|
847
|
-
|
|
848
|
-
}
|
|
849
|
-
|
|
850
828
|
console.groupCollapsed('%c API: An error occurred in the try catch block. returning null!', 'color: #ff0000')
|
|
851
829
|
|
|
852
830
|
console.log('%c ' + requestMethod + ' ' + tableName, 'color: #A020F0')
|
|
853
831
|
|
|
854
|
-
console.
|
|
832
|
+
console.error(throwableError)
|
|
855
833
|
|
|
856
834
|
console.trace()
|
|
857
835
|
|
|
858
836
|
console.groupEnd()
|
|
859
837
|
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
return null;
|
|
838
|
+
throw new Error(JSON.stringify(throwableError))
|
|
863
839
|
|
|
864
840
|
}
|
|
865
841
|
|