@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.
@@ -25,13 +25,21 @@ export class HttpExecutor<
25
25
  >
26
26
  extends Executor<G> {
27
27
 
28
- private stripTableNameFromKeys(obj: Record<string, any>) {
29
- const columns = (this.config.restModel as any).COLUMNS || {};
30
- return Object.keys(obj || {}).reduce((acc: Record<string, any>, key) => {
31
- const shortKey = columns[key] || key.split('.').pop();
32
- acc[shortKey] = obj[key];
33
- return acc;
34
- }, {} as Record<string, any>);
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 (1 !== this.config.restModel.PRIMARY_SHORT.length) {
73
-
74
- console.error("C6 received unexpected result's given the primary key length");
75
-
76
- } else {
77
-
78
- const pk = this.config.restModel.PRIMARY_SHORT[0];
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<G['RestTableInterface']>({
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<G['RestTableInterface']>({
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<G['RestTableInterface']>({
98
- ...this.stripTableNameFromKeys(request as Record<string, any>),
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 G['RestTableInterface'])[]
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 (null !== clearCache || undefined !== clearCache) {
171
-
172
- userCustomClearCache[tables + requestMethod] = clearCache;
173
-
175
+ if (clearCache != null) {
176
+ userCustomClearCache.push(clearCache);
174
177
  }
175
178
 
176
- console.groupCollapsed('%c API: (' + requestMethod + ') Request for (' + tableName + ')', 'color: #0c0')
177
-
178
- console.log('request', this.request)
179
-
180
- console.groupEnd()
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
- console.groupCollapsed('%c API: (' + requestMethod + ') Request Query for (' + tableName + ') undefined, returning null (will not fire)!', 'color: #c00')
187
-
188
- console.log('request', this.request)
189
-
190
- console.log('%c Returning (undefined|null) for a query would indicate a custom cache hit (outside API.tsx), thus the request should not fire.', 'color: #c00')
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('Request on table (' + tableName + ') is firing for page (' + query[C6.PAGINATION][C6.PAGE] + '), please wait!')
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
- if (null === query
370
- || undefined === query
371
- || undefined === query?.[C6.WHERE]
372
- || (true === Array.isArray(query[C6.WHERE])
373
- || query[C6.WHERE].length === 0)
374
- || (Object.keys(query?.[C6.WHERE]).length === 0)
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
- console.log('query', query, 'primaryKey', primaryKey)
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
- console.log('query', query)
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
- console.log('query', query)
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
- console.groupCollapsed('%c API: (' + requestMethod + ') Request Query for (' + operatingTable + ') is about to fire, will return with promise!', 'color: #A020F0')
443
-
444
- console.log(this.request)
445
-
446
- console.log('%c If this is the first request for this datatype; thus the value being set is currently undefined, please remember to update the state to null.', 'color: #A020F0')
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
- console.groupCollapsed('%c API: Response (' + requestMethod + ' ' + tableName + ') returned length (' + responseData.rest?.length + ') of possible (' + query?.[C6.PAGINATION]?.[C6.LIMIT] + ') limit!', 'color: #0c0')
554
+ const responseData = response.data;
617
555
 
618
- console.log('%c ' + requestMethod + ' ' + tableName, 'color: #0c0')
556
+ const pageLimit = query?.[C6.PAGINATION]?.[C6.LIMIT];
557
+ const got = responseData.rest.length;
558
+ const hasNext = pageLimit !== 1 && got === pageLimit;
619
559
 
620
- console.log('%c Request Data (note you may see the success and/or error prompt):', 'color: #0c0', this.request)
621
-
622
- console.log('%c Response Data:', 'color: #0c0', responseData.rest)
560
+ if (hasNext) {
561
+ responseData.next = apiRequest; // there might be more
562
+ } else {
563
+ responseData.next = undefined; // short page => done
564
+ }
623
565
 
624
- console.log('%c Will return get next page function:' + (returnGetNextPageFunction ? '' : ' (Will not return with explicit limit 1 set)'), 'color: #0c0', true === returnGetNextPageFunction)
566
+ // If you keep this flag, make it reflect reality:
567
+ returnGetNextPageFunction = hasNext;
625
568
 
626
- console.trace();
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
- if (false === returnGetNextPageFunction) {
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 method = req.method.toUpperCase() as iRestMethods;
14
+ const incomingMethod = req.method.toUpperCase() as iRestMethods;
15
15
  const table = req.params.table;
16
16
  const primary = req.params.primary;
17
- const payload = method === 'GET' ? req.query : req.body;
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";