@carbonorm/carbonnode 3.7.23 → 3.8.1
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/axiosInstance.d.ts +1 -2
- package/dist/api/executors/HttpExecutor.d.ts +1 -0
- package/dist/index.cjs.js +162 -120
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.esm.js +162 -120
- package/dist/index.esm.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/expressServer.e2e.test.ts +29 -0
- package/src/__tests__/sakila-db/C6.js +1 -1
- package/src/__tests__/sakila-db/C6.ts +1 -1
- package/src/api/axiosInstance.ts +27 -2
- package/src/api/executors/HttpExecutor.ts +98 -160
- package/src/api/handlers/ExpressHandler.ts +19 -2
- package/src/index.ts +0 -1
|
@@ -25,13 +25,21 @@ export class HttpExecutor<
|
|
|
25
25
|
>
|
|
26
26
|
extends Executor<G> {
|
|
27
27
|
|
|
28
|
-
private
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
28
|
+
private isRestResponse<T extends Record<string, any>>(
|
|
29
|
+
r: AxiosResponse<any>
|
|
30
|
+
): r is AxiosResponse<iGetC6RestResponse<T>> {
|
|
31
|
+
return !!r && r.data != null && typeof r.data === 'object' && 'rest' in r.data;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
private stripTableNameFromKeys<T extends Record<string, any>>(obj: Partial<T> | undefined | null): Partial<T> {
|
|
35
|
+
const columns = this.config.restModel.COLUMNS as Record<string, string>;
|
|
36
|
+
const source: Record<string, any> = (obj ?? {}) as Record<string, any>;
|
|
37
|
+
const out: Partial<T> = {} as Partial<T>;
|
|
38
|
+
for (const [key, value] of Object.entries(source)) {
|
|
39
|
+
const short = columns[key] ?? (key.includes('.') ? key.split('.').pop()! : key);
|
|
40
|
+
(out as any)[short] = value;
|
|
41
|
+
}
|
|
42
|
+
return out;
|
|
35
43
|
}
|
|
36
44
|
|
|
37
45
|
public putState(
|
|
@@ -68,39 +76,36 @@ export class HttpExecutor<
|
|
|
68
76
|
>,
|
|
69
77
|
callback: () => void
|
|
70
78
|
) {
|
|
79
|
+
type RT = G['RestTableInterface'];
|
|
80
|
+
type PK = G['PrimaryKey'];
|
|
71
81
|
|
|
72
|
-
if (
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
// TODO - should overrides be handled differently? Why override: (react/php), driver missmatches, aux data..
|
|
81
|
-
// @ts-ignore - this is technically a correct error, but we allow it anyway...
|
|
82
|
-
request[pk] = response.data?.created as RestTableInterface[PrimaryKey]
|
|
83
|
-
|
|
82
|
+
if (this.config.restModel.PRIMARY_SHORT.length === 1) {
|
|
83
|
+
const pk = this.config.restModel.PRIMARY_SHORT[0] as PK;
|
|
84
|
+
try {
|
|
85
|
+
(request as unknown as Record<PK, RT[PK]>)[pk] = (response.data as any)?.created as RT[PK];
|
|
86
|
+
} catch {/* best-effort */}
|
|
87
|
+
} else if (isLocal()) {
|
|
88
|
+
console.error("C6 received unexpected results given the primary key length");
|
|
84
89
|
}
|
|
85
90
|
|
|
86
|
-
this.config.reactBootstrap?.updateRestfulObjectArrays<
|
|
91
|
+
this.config.reactBootstrap?.updateRestfulObjectArrays<RT>({
|
|
87
92
|
callback,
|
|
88
93
|
dataOrCallback: undefined !== request.dataInsertMultipleRows
|
|
89
94
|
? request.dataInsertMultipleRows.map((row, index) => {
|
|
90
|
-
const normalizedRow = this.stripTableNameFromKeys(row);
|
|
91
|
-
return removeInvalidKeys<
|
|
95
|
+
const normalizedRow = this.stripTableNameFromKeys<RT>(row as Partial<RT>);
|
|
96
|
+
return removeInvalidKeys<RT>({
|
|
92
97
|
...normalizedRow,
|
|
93
|
-
...(index === 0 ? response?.data?.rest : {}),
|
|
98
|
+
...(index === 0 ? (response?.data as any)?.rest : {}),
|
|
94
99
|
}, this.config.C6.TABLES)
|
|
95
100
|
})
|
|
96
101
|
: [
|
|
97
|
-
removeInvalidKeys<
|
|
98
|
-
...this.stripTableNameFromKeys(request as
|
|
99
|
-
...response?.data?.rest,
|
|
102
|
+
removeInvalidKeys<RT>({
|
|
103
|
+
...this.stripTableNameFromKeys<RT>(request as unknown as Partial<RT>),
|
|
104
|
+
...(response?.data as any)?.rest,
|
|
100
105
|
}, this.config.C6.TABLES)
|
|
101
106
|
],
|
|
102
107
|
stateKey: this.config.restModel.TABLE_NAME,
|
|
103
|
-
uniqueObjectId: this.config.restModel.PRIMARY_SHORT as (keyof
|
|
108
|
+
uniqueObjectId: this.config.restModel.PRIMARY_SHORT as (keyof RT)[]
|
|
104
109
|
})
|
|
105
110
|
}
|
|
106
111
|
|
|
@@ -167,31 +172,25 @@ export class HttpExecutor<
|
|
|
167
172
|
throw Error('Bad request method passed to getApi')
|
|
168
173
|
}
|
|
169
174
|
|
|
170
|
-
if (
|
|
171
|
-
|
|
172
|
-
userCustomClearCache[tables + requestMethod] = clearCache;
|
|
173
|
-
|
|
175
|
+
if (clearCache != null) {
|
|
176
|
+
userCustomClearCache.push(clearCache);
|
|
174
177
|
}
|
|
175
178
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
179
|
+
if (isLocal() && (this.config.verbose || (this.request as any)?.debug)) {
|
|
180
|
+
console.groupCollapsed('%c API:', 'color: #0c0', `(${requestMethod}) Request for (${tableName})`)
|
|
181
|
+
console.log('request', this.request)
|
|
182
|
+
console.groupEnd()
|
|
183
|
+
}
|
|
181
184
|
|
|
182
185
|
// an undefined query would indicate queryCallback returned undefined,
|
|
183
186
|
// thus the request shouldn't fire as is in custom cache
|
|
184
187
|
if (undefined === this.request || null === this.request) {
|
|
185
188
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
console.trace();
|
|
193
|
-
|
|
194
|
-
console.groupEnd()
|
|
189
|
+
if (isLocal()) {
|
|
190
|
+
console.groupCollapsed(`API: (${requestMethod}) (${tableName}) query undefined/null → returning null`)
|
|
191
|
+
console.log('request', this.request)
|
|
192
|
+
console.groupEnd()
|
|
193
|
+
}
|
|
195
194
|
|
|
196
195
|
return null;
|
|
197
196
|
|
|
@@ -199,20 +198,6 @@ export class HttpExecutor<
|
|
|
199
198
|
|
|
200
199
|
let query = this.request;
|
|
201
200
|
|
|
202
|
-
if (C6.GET === requestMethod) {
|
|
203
|
-
|
|
204
|
-
if (undefined === query[C6.PAGINATION]) {
|
|
205
|
-
|
|
206
|
-
query[C6.PAGINATION] = {}
|
|
207
|
-
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
query[C6.PAGINATION][C6.PAGE] = query[C6.PAGINATION][C6.PAGE] || 1;
|
|
211
|
-
|
|
212
|
-
query[C6.PAGINATION][C6.LIMIT] = query[C6.PAGINATION][C6.LIMIT] || 100;
|
|
213
|
-
|
|
214
|
-
}
|
|
215
|
-
|
|
216
201
|
// this is parameterless and could return itself with a new page number, or undefined if the end is reached
|
|
217
202
|
const apiRequest = async (): Promise<DetermineResponseDataType<G['RequestMethod'], G['RestTableInterface']>> => {
|
|
218
203
|
|
|
@@ -227,16 +212,11 @@ export class HttpExecutor<
|
|
|
227
212
|
|
|
228
213
|
if (C6.GET === requestMethod
|
|
229
214
|
&& undefined !== query?.[C6.PAGINATION]?.[C6.PAGE]
|
|
230
|
-
&& 1 !== query[C6.PAGINATION][C6.PAGE]
|
|
231
|
-
|
|
232
|
-
console.groupCollapsed(
|
|
233
|
-
|
|
234
|
-
console.log('Request Data (note you may see the success and/or error prompt):', this.request)
|
|
235
|
-
|
|
236
|
-
console.trace();
|
|
237
|
-
|
|
215
|
+
&& 1 !== query[C6.PAGINATION][C6.PAGE]
|
|
216
|
+
&& isLocal()) {
|
|
217
|
+
console.groupCollapsed(`Request (${tableName}) page (${query[C6.PAGINATION][C6.PAGE]})`)
|
|
218
|
+
console.log('request', this.request)
|
|
238
219
|
console.groupEnd()
|
|
239
|
-
|
|
240
220
|
}
|
|
241
221
|
|
|
242
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.
|
|
@@ -275,57 +255,27 @@ export class HttpExecutor<
|
|
|
275
255
|
|
|
276
256
|
// this will evaluate true most the time
|
|
277
257
|
if (true === cacheResults) {
|
|
278
|
-
|
|
279
|
-
// just find the next, non-fetched, page and return a function to request it
|
|
280
|
-
if (undefined !== cacheResult) { // we will return in this loop
|
|
281
|
-
|
|
258
|
+
if (undefined !== cacheResult) {
|
|
282
259
|
do {
|
|
283
|
-
|
|
284
260
|
const cacheCheck = checkCache<ResponseDataType>(cacheResult, requestMethod, tableName, this.request);
|
|
285
|
-
|
|
286
261
|
if (false !== cacheCheck) {
|
|
287
|
-
|
|
288
262
|
return (await cacheCheck).data;
|
|
289
|
-
|
|
290
263
|
}
|
|
291
|
-
|
|
292
|
-
// this line incrementing page is why we return recursively
|
|
293
264
|
++query[C6.PAGINATION][C6.PAGE];
|
|
294
|
-
|
|
295
|
-
// this json stringify is to capture the new page number
|
|
296
265
|
querySerialized = sortAndSerializeQueryObject(tables, query ?? {});
|
|
297
|
-
|
|
298
266
|
cacheResult = apiRequestCache.find(cache => cache.requestArgumentsSerialized === querySerialized)
|
|
299
|
-
|
|
300
267
|
} while (undefined !== cacheResult)
|
|
301
|
-
|
|
302
268
|
if (debug && isLocal()) {
|
|
303
|
-
|
|
304
|
-
toast.warning("DEVS: Request in cache. (" + apiRequestCache.findIndex(cache => cache.requestArgumentsSerialized === querySerialized) + "). Returning function to request page (" + query[C6.PAGINATION][C6.PAGE] + ")", toastOptionsDevs);
|
|
305
|
-
|
|
269
|
+
toast.warning("DEVS: Request pages exhausted in cache; firing network.", toastOptionsDevs);
|
|
306
270
|
}
|
|
307
|
-
|
|
308
|
-
// @ts-ignore - this is an incorrect warning on TS, it's well typed
|
|
309
|
-
return apiRequest;
|
|
310
|
-
|
|
311
271
|
}
|
|
312
|
-
|
|
313
272
|
cachingConfirmed = true;
|
|
314
|
-
|
|
315
273
|
} else {
|
|
316
|
-
|
|
317
|
-
if (debug && isLocal()) {
|
|
318
|
-
|
|
319
|
-
toast.info("DEVS: Ignore cache was set to true.", toastOptionsDevs);
|
|
320
|
-
|
|
321
|
-
}
|
|
322
|
-
|
|
274
|
+
if (debug && isLocal()) toast.info("DEVS: Ignore cache was set to true.", toastOptionsDevs);
|
|
323
275
|
}
|
|
324
276
|
|
|
325
277
|
if (debug && isLocal()) {
|
|
326
|
-
|
|
327
|
-
toast.success("DEVS: Request not in cache." + (requestMethod === C6.GET ? "Page (" + query[C6.PAGINATION][C6.PAGE] + ")." : '') + " Logging cache 2 console.", toastOptionsDevs);
|
|
328
|
-
|
|
278
|
+
toast.success("DEVS: Request not in cache." + (requestMethod === C6.GET ? " Page (" + query[C6.PAGINATION][C6.PAGE] + ")" : ''), toastOptionsDevs);
|
|
329
279
|
}
|
|
330
280
|
|
|
331
281
|
} else if (cacheResults) { // if we are not getting, we are updating, deleting, or inserting
|
|
@@ -366,13 +316,13 @@ export class HttpExecutor<
|
|
|
366
316
|
|
|
367
317
|
if (undefined === primaryKey) {
|
|
368
318
|
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
) {
|
|
319
|
+
const whereVal = query?.[C6.WHERE];
|
|
320
|
+
const whereIsEmpty =
|
|
321
|
+
whereVal == null ||
|
|
322
|
+
(Array.isArray(whereVal) && whereVal.length === 0) ||
|
|
323
|
+
(typeof whereVal === 'object' && !Array.isArray(whereVal) && Object.keys(whereVal).length === 0);
|
|
324
|
+
|
|
325
|
+
if (whereIsEmpty) {
|
|
376
326
|
|
|
377
327
|
console.error(query)
|
|
378
328
|
|
|
@@ -423,33 +373,33 @@ export class HttpExecutor<
|
|
|
423
373
|
|
|
424
374
|
restRequestUri += primaryVal + '/'
|
|
425
375
|
|
|
426
|
-
|
|
376
|
+
if (isLocal() && (this.config.verbose || (this.request as any)?.debug)) {
|
|
377
|
+
console.log('query', query, 'primaryKey', primaryKey)
|
|
378
|
+
}
|
|
427
379
|
|
|
428
380
|
} else {
|
|
429
381
|
|
|
430
|
-
|
|
382
|
+
if (isLocal() && (this.config.verbose || (this.request as any)?.debug)) {
|
|
383
|
+
console.log('query', query)
|
|
384
|
+
}
|
|
431
385
|
|
|
432
386
|
}
|
|
433
387
|
|
|
434
388
|
} else {
|
|
435
389
|
|
|
436
|
-
|
|
390
|
+
if (isLocal() && (this.config.verbose || (this.request as any)?.debug)) {
|
|
391
|
+
console.log('query', query)
|
|
392
|
+
}
|
|
437
393
|
|
|
438
394
|
}
|
|
439
395
|
|
|
440
396
|
try {
|
|
441
397
|
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
console.log('%c Remember undefined indicated the request has not fired, null indicates the request is firing, an empty array would signal no data was returned for the sql stmt.', 'color: #A020F0')
|
|
449
|
-
|
|
450
|
-
console.trace()
|
|
451
|
-
|
|
452
|
-
console.groupEnd()
|
|
398
|
+
if (isLocal() && (this.config.verbose || (this.request as any)?.debug)) {
|
|
399
|
+
console.groupCollapsed('%c API:', 'color: #A020F0', `(${requestMethod}) (${operatingTable}) firing`)
|
|
400
|
+
console.log(this.request)
|
|
401
|
+
console.groupEnd()
|
|
402
|
+
}
|
|
453
403
|
|
|
454
404
|
this.runLifecycleHooks<"beforeExecution">(
|
|
455
405
|
"beforeExecution", {
|
|
@@ -557,19 +507,14 @@ export class HttpExecutor<
|
|
|
557
507
|
response
|
|
558
508
|
})
|
|
559
509
|
|
|
560
|
-
// todo - this feels dumb now, but i digress
|
|
561
510
|
apiResponse = TestRestfulResponse(response, success, error)
|
|
562
511
|
|
|
563
512
|
if (false === apiResponse) {
|
|
564
|
-
|
|
565
513
|
if (debug && isLocal()) {
|
|
566
|
-
|
|
567
|
-
toast.warning("DEVS: TestRestfulResponse returned false for (" + operatingTable + ").", toastOptionsDevs);
|
|
568
|
-
|
|
514
|
+
toast.warning("DEVS: TestRestfulResponse returned false.", toastOptionsDevs);
|
|
569
515
|
}
|
|
570
|
-
|
|
571
|
-
return response;
|
|
572
|
-
|
|
516
|
+
// Force a null payload so the final .then(response => response.data) yields null
|
|
517
|
+
return Promise.resolve({ ...response, data: null as unknown as ResponseDataType });
|
|
573
518
|
}
|
|
574
519
|
|
|
575
520
|
const callback = () => this.runLifecycleHooks<"afterCommit">(
|
|
@@ -604,44 +549,37 @@ export class HttpExecutor<
|
|
|
604
549
|
callback();
|
|
605
550
|
}
|
|
606
551
|
|
|
607
|
-
if (C6.GET === requestMethod) {
|
|
608
|
-
|
|
609
|
-
const responseData = response.data as iGetC6RestResponse<any>;
|
|
610
|
-
|
|
611
|
-
returnGetNextPageFunction = 1 !== query?.[C6.PAGINATION]?.[C6.LIMIT] &&
|
|
612
|
-
query?.[C6.PAGINATION]?.[C6.LIMIT] === responseData.rest.length
|
|
613
|
-
|
|
614
|
-
if (false === isTest() || this.config.verbose) {
|
|
552
|
+
if (C6.GET === requestMethod && this.isRestResponse<any>(response)) {
|
|
615
553
|
|
|
616
|
-
|
|
554
|
+
const responseData = response.data;
|
|
617
555
|
|
|
618
|
-
|
|
556
|
+
const pageLimit = query?.[C6.PAGINATION]?.[C6.LIMIT];
|
|
557
|
+
const got = responseData.rest.length;
|
|
558
|
+
const hasNext = pageLimit !== 1 && got === pageLimit;
|
|
619
559
|
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
560
|
+
if (hasNext) {
|
|
561
|
+
responseData.next = apiRequest; // there might be more
|
|
562
|
+
} else {
|
|
563
|
+
responseData.next = undefined; // short page => done
|
|
564
|
+
}
|
|
623
565
|
|
|
624
|
-
|
|
566
|
+
// If you keep this flag, make it reflect reality:
|
|
567
|
+
returnGetNextPageFunction = hasNext;
|
|
625
568
|
|
|
626
|
-
|
|
569
|
+
// and fix cache ‘final’ flag to match:
|
|
570
|
+
if (cachingConfirmed) {
|
|
571
|
+
const cacheIndex = apiRequestCache.findIndex(c => c.requestArgumentsSerialized === querySerialized);
|
|
572
|
+
apiRequestCache[cacheIndex].final = !hasNext;
|
|
573
|
+
}
|
|
627
574
|
|
|
575
|
+
if ((this.config.verbose || debug) && isLocal()) {
|
|
576
|
+
console.groupCollapsed(`API: Response (${requestMethod} ${tableName}) len (${responseData.rest?.length}) of (${query?.[C6.PAGINATION]?.[C6.LIMIT]})`)
|
|
577
|
+
console.log('request', this.request)
|
|
578
|
+
console.log('response.rest', responseData.rest)
|
|
628
579
|
console.groupEnd()
|
|
629
|
-
|
|
630
580
|
}
|
|
631
581
|
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
responseData.next = apiRequest
|
|
635
|
-
|
|
636
|
-
} else {
|
|
637
|
-
|
|
638
|
-
responseData.next = undefined;
|
|
639
|
-
|
|
640
|
-
if (true === debug
|
|
641
|
-
&& isLocal()) {
|
|
642
|
-
toast.success("DEVS: Response returned length (" + responseData.rest?.length + ") less than limit (" + query?.[C6.PAGINATION]?.[C6.LIMIT] + ").", toastOptionsDevs);
|
|
643
|
-
}
|
|
644
|
-
}
|
|
582
|
+
// next already set above based on hasNext; avoid duplicate, inverted logic
|
|
645
583
|
|
|
646
584
|
|
|
647
585
|
if (fetchDependencies
|
|
@@ -11,10 +11,27 @@ export function ExpressHandler({C6, mysqlPool}: { C6: iC6Object, mysqlPool: Pool
|
|
|
11
11
|
|
|
12
12
|
return async (req: Request, res: Response, next: NextFunction) => {
|
|
13
13
|
try {
|
|
14
|
-
const
|
|
14
|
+
const incomingMethod = req.method.toUpperCase() as iRestMethods;
|
|
15
15
|
const table = req.params.table;
|
|
16
16
|
const primary = req.params.primary;
|
|
17
|
-
|
|
17
|
+
// Support Axios interceptor promoting large GETs to POST with ?METHOD=GET
|
|
18
|
+
const methodOverrideRaw = (req.query?.METHOD ?? req.query?.method) as unknown;
|
|
19
|
+
const methodOverride = typeof methodOverrideRaw === 'string' ? methodOverrideRaw.toUpperCase() : undefined;
|
|
20
|
+
|
|
21
|
+
const treatAsGet = incomingMethod === 'POST' && methodOverride === 'GET';
|
|
22
|
+
|
|
23
|
+
const method: iRestMethods = treatAsGet ? 'GET' : incomingMethod;
|
|
24
|
+
const payload: any = treatAsGet ? { ...(req.body as any) } : (method === 'GET' ? req.query : req.body);
|
|
25
|
+
|
|
26
|
+
// Remove transport-only METHOD flag so it never leaks into ORM parsing
|
|
27
|
+
if (treatAsGet && 'METHOD' in payload) {
|
|
28
|
+
try { delete (payload as any).METHOD } catch { /* noop */ }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Warn for unsupported overrides but continue normally
|
|
32
|
+
if (incomingMethod !== 'GET' && methodOverride && methodOverride !== 'GET') {
|
|
33
|
+
console.warn(`Ignoring unsupported METHOD override: ${methodOverride}`);
|
|
34
|
+
}
|
|
18
35
|
|
|
19
36
|
if (!(table in C6.TABLES)) {
|
|
20
37
|
res.status(400).json({error: `Invalid table: ${table}`});
|
package/src/index.ts
CHANGED
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
export * from "./api/C6Constants";
|
|
6
|
-
export { default as axiosInstance } from "./api/axiosInstance";
|
|
7
6
|
export * from "./api/axiosInstance";
|
|
8
7
|
export { default as convertForRequestBody } from "./api/convertForRequestBody";
|
|
9
8
|
export * from "./api/convertForRequestBody";
|