@carbonorm/carbonnode 3.0.1 → 3.0.3

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.
@@ -6,43 +6,140 @@ import isVerbose from "../../variables/isVerbose";
6
6
  import convertForRequestBody from "../convertForRequestBody";
7
7
  import {eFetchDependencies} from "../types/dynamicFetching";
8
8
  import {Modify} from "../types/modifyTypes";
9
- import {apiReturn, DELETE, GET, iCacheAPI, iConstraint, iGetC6RestResponse, POST, PUT, RequestQueryBody} from "../types/ormInterfaces";
10
- import {removePrefixIfExists, TestRestfulResponse} from "../utils/apiHelpers";
9
+ import {
10
+ apiReturn,
11
+ DELETE, DetermineResponseDataType,
12
+ GET, iAPI,
13
+ iCacheAPI,
14
+ iConstraint,
15
+ iGetC6RestResponse,
16
+ iRestMethods,
17
+ POST,
18
+ PUT,
19
+ RequestQueryBody
20
+ } from "../types/ormInterfaces";
21
+ import {removeInvalidKeys, removePrefixIfExists, TestRestfulResponse} from "../utils/apiHelpers";
11
22
  import {apiRequestCache, checkCache, userCustomClearCache} from "../utils/cacheManager";
12
23
  import {sortAndSerializeQueryObject} from "../utils/sortAndSerializeQueryObject";
13
24
  import {Executor} from "./Executor";
14
- import {toastOptions, toastOptionsDevs } from "variables/toastOptions";
25
+ import {toastOptions, toastOptionsDevs} from "variables/toastOptions";
15
26
 
16
27
  export class HttpExecutor<
17
- CustomAndRequiredFields extends { [key: string]: any }, // CustomAndRequiredFields
18
- RestTableInterfaces extends { [key: string]: any }, // RestTableInterfaces
19
- RequestTableOverrides = { [key in keyof RestTableInterfaces]: any }, // RequestTableOverrides
20
- ResponseDataType = any, // ResponseDataType
21
- RestShortTableNames extends string = "" // RestShortTableNames
28
+ RequestMethod extends iRestMethods,
29
+ RestShortTableName extends string = any,
30
+ RestTableInterface extends { [key: string]: any } = any,
31
+ PrimaryKey extends Extract<keyof RestTableInterface, string> = Extract<keyof RestTableInterface, string>,
32
+ CustomAndRequiredFields extends { [key: string]: any } = any,
33
+ RequestTableOverrides extends { [key in keyof RestTableInterface]: any } = { [key in keyof RestTableInterface]: any }
22
34
  >
23
35
  extends Executor<
36
+ RequestMethod,
37
+ RestShortTableName,
38
+ RestTableInterface,
39
+ PrimaryKey,
24
40
  CustomAndRequiredFields,
25
- RestTableInterfaces,
26
- RequestTableOverrides,
27
- ResponseDataType,
28
- RestShortTableNames
41
+ RequestTableOverrides
29
42
  > {
30
43
 
31
- public async execute() : Promise<apiReturn<ResponseDataType>> {
44
+ public putState(
45
+ response: AxiosResponse<DetermineResponseDataType<RequestMethod, RestTableInterface>>,
46
+ request: iAPI<Modify<RestTableInterface, RequestTableOverrides>> & CustomAndRequiredFields,
47
+ callback: () => void
48
+ ) {
49
+ this.config.reactBootstrap?.updateRestfulObjectArrays<RestTableInterface>({
50
+ callback,
51
+ dataOrCallback: [
52
+ removeInvalidKeys<RestTableInterface>({
53
+ ...request,
54
+ ...response?.data?.rest,
55
+ }, this.config.C6.TABLES)
56
+ ],
57
+ stateKey: this.config.restModel.TABLE_NAME,
58
+ uniqueObjectId: this.config.restModel.PRIMARY_SHORT
59
+ })
60
+ }
61
+
62
+ public postState(
63
+ response: AxiosResponse<DetermineResponseDataType<RequestMethod, RestTableInterface>>,
64
+ request: iAPI<Modify<RestTableInterface, RequestTableOverrides>> & CustomAndRequiredFields,
65
+ callback: () => void
66
+ ) {
67
+
68
+ if (1 !== this.config.restModel.PRIMARY_SHORT.length) {
69
+
70
+ console.error("C6 received unexpected result's given the primary key length");
71
+
72
+ } else {
73
+
74
+ const pk = this.config.restModel.PRIMARY_SHORT[0];
75
+
76
+ // TODO - should overrides be handled differently? Why override: (react/php), driver missmatches, aux data..
77
+ // @ts-ignore - this is technically a correct error, but we allow it anyway...
78
+ request[pk] = response.data?.created as RestTableInterface[PrimaryKey]
79
+
80
+ }
81
+
82
+ this.config.reactBootstrap?.updateRestfulObjectArrays<RestTableInterface>({
83
+ callback,
84
+ dataOrCallback: undefined !== request.dataInsertMultipleRows
85
+ ? request.dataInsertMultipleRows.map((request, index) => {
86
+ return removeInvalidKeys<RestTableInterface>({
87
+ ...request,
88
+ ...(index === 0 ? response?.data?.rest : {}),
89
+ }, this.config.C6.TABLES)
90
+ })
91
+ : [
92
+ removeInvalidKeys<RestTableInterface>({
93
+ ...request,
94
+ ...response?.data?.rest,
95
+ }, this.config.C6.TABLES)
96
+ ],
97
+ stateKey: this.config.restModel.TABLE_NAME,
98
+ uniqueObjectId: this.config.restModel.PRIMARY_SHORT as (keyof RestTableInterface)[]
99
+ })
100
+ }
101
+
102
+ public deleteState(
103
+ _response: AxiosResponse<DetermineResponseDataType<RequestMethod, RestTableInterface>>,
104
+ request: iAPI<Modify<RestTableInterface, RequestTableOverrides>> & CustomAndRequiredFields,
105
+ callback: () => void
106
+ ) {
107
+ this.config.reactBootstrap?.deleteRestfulObjectArrays<RestTableInterface>({
108
+ callback,
109
+ dataOrCallback: [
110
+ request as unknown as RestTableInterface,
111
+ ],
112
+ stateKey: this.config.restModel.TABLE_NAME,
113
+ uniqueObjectId: this.config.restModel.PRIMARY_SHORT as (keyof RestTableInterface)[]
114
+ })
115
+ }
116
+
117
+ public async execute(): Promise<apiReturn<DetermineResponseDataType<RequestMethod, RestTableInterface>>> {
118
+
119
+ type ResponseDataType = DetermineResponseDataType<RequestMethod, RestTableInterface>;
32
120
 
33
121
  const {
34
122
  C6,
35
123
  axios,
36
124
  restURL,
37
125
  withCredentials,
38
- tableName,
126
+ restModel,
127
+ reactBootstrap,
39
128
  requestMethod,
40
- queryCallback,
41
- responseCallback,
42
129
  skipPrimaryCheck,
43
130
  clearCache,
44
131
  } = this.config
45
132
 
133
+
134
+ await this.runLifecycleHooks<"beforeProcessing">(
135
+ "beforeProcessing", {
136
+ config: this.config,
137
+ request: this.request,
138
+ });
139
+
140
+
141
+ const tableName = restModel.TABLE_NAME;
142
+
46
143
  const fullTableList = Array.isArray(tableName) ? tableName : [tableName];
47
144
 
48
145
  const operatingTableFullName = fullTableList[0];
@@ -75,17 +172,7 @@ export class HttpExecutor<
75
172
 
76
173
  // an undefined query would indicate queryCallback returned undefined,
77
174
  // thus the request shouldn't fire as is in custom cache
78
- let query: RequestQueryBody<Modify<RestTableInterfaces, RequestTableOverrides>> | undefined | null;
79
-
80
- if ('function' === typeof queryCallback) {
81
-
82
- query = queryCallback(this.request); // obj or obj[]
83
-
84
- } else {
85
-
86
- query = queryCallback;
87
-
88
- }
175
+ let query: RequestQueryBody<Modify<RestTableInterface, RequestTableOverrides>> | undefined | null;
89
176
 
90
177
  if (undefined === query || null === query) {
91
178
 
@@ -122,7 +209,7 @@ export class HttpExecutor<
122
209
  }
123
210
 
124
211
  // this could return itself with a new page number, or undefined if the end is reached
125
- const apiRequest = async (): Promise<apiReturn<ResponseDataType>> => {
212
+ const apiRequest = async (): Promise<apiReturn<DetermineResponseDataType<RequestMethod, RestTableInterface>>> => {
126
213
 
127
214
  const {
128
215
  debug,
@@ -130,7 +217,7 @@ export class HttpExecutor<
130
217
  dataInsertMultipleRows,
131
218
  success,
132
219
  fetchDependencies = eFetchDependencies.NONE,
133
- error = "An unexpected API error occurred!"
220
+ error = "An unexpected API error occurred!"
134
221
  } = this.request
135
222
 
136
223
  if (C6.GET === requestMethod
@@ -250,7 +337,7 @@ export class HttpExecutor<
250
337
 
251
338
  let addBackPK: (() => void) | undefined;
252
339
 
253
- let apiResponse: string | boolean | number | undefined;
340
+ let apiResponse: RestTableInterface[PrimaryKey] | string | boolean | number | undefined;
254
341
 
255
342
  let returnGetNextPageFunction = false;
256
343
 
@@ -353,6 +440,12 @@ export class HttpExecutor<
353
440
 
354
441
  console.groupEnd()
355
442
 
443
+ this.runLifecycleHooks<"beforeExecution">(
444
+ "beforeExecution", {
445
+ config: this.config,
446
+ request: this.request
447
+ })
448
+
356
449
  const axiosActiveRequest: AxiosPromise<ResponseDataType> = axios![requestMethod.toLowerCase()]<ResponseDataType>(
357
450
  restRequestUri,
358
451
  ...((() => {
@@ -381,7 +474,7 @@ export class HttpExecutor<
381
474
  }
382
475
 
383
476
  return [
384
- convertForRequestBody<RestTableInterfaces>(query as RestTableInterfaces, fullTableList, C6, (message) => toast.error(message, toastOptions)),
477
+ convertForRequestBody<RestTableInterface>(query as RestTableInterface, fullTableList, C6, (message) => toast.error(message, toastOptions)),
385
478
  {
386
479
  withCredentials: withCredentials,
387
480
  }
@@ -390,7 +483,7 @@ export class HttpExecutor<
390
483
  } else if (requestMethod === PUT) {
391
484
 
392
485
  return [
393
- convertForRequestBody<RestTableInterfaces>(query as RestTableInterfaces, fullTableList, C6, (message) => toast.error(message, toastOptions)),
486
+ convertForRequestBody<RestTableInterface>(query as RestTableInterface, fullTableList, C6, (message) => toast.error(message, toastOptions)),
394
487
  {
395
488
  withCredentials: withCredentials,
396
489
  }
@@ -399,7 +492,7 @@ export class HttpExecutor<
399
492
 
400
493
  return [{
401
494
  withCredentials: withCredentials,
402
- data: convertForRequestBody<RestTableInterfaces>(query as RestTableInterfaces, fullTableList, C6, (message) => toast.error(message, toastOptions))
495
+ data: convertForRequestBody<RestTableInterface>(query as RestTableInterface, fullTableList, C6, (message) => toast.error(message, toastOptions))
403
496
  }]
404
497
 
405
498
  } else {
@@ -427,8 +520,9 @@ export class HttpExecutor<
427
520
 
428
521
  // returning the promise with this then is important for tests. todo - we could make that optional.
429
522
  // https://rapidapi.com/guides/axios-async-await
430
- return axiosActiveRequest.then(async (response): Promise<AxiosResponse<ResponseDataType, any>> => {
523
+ return axiosActiveRequest.then(async (response: AxiosResponse<ResponseDataType, any>): Promise<AxiosResponse<ResponseDataType, any>> => {
431
524
 
525
+ // noinspection SuspiciousTypeOfGuard
432
526
  if (typeof response.data === 'string') {
433
527
 
434
528
  if (isTest) {
@@ -454,6 +548,14 @@ export class HttpExecutor<
454
548
 
455
549
  }
456
550
 
551
+ this.runLifecycleHooks<"afterExecution">(
552
+ "afterExecution", {
553
+ config: this.config,
554
+ request: this.request,
555
+ response
556
+ })
557
+
558
+ // todo - this feels dumb now, but i digress
457
559
  apiResponse = TestRestfulResponse(response, success, error)
458
560
 
459
561
  if (false === apiResponse) {
@@ -468,11 +570,36 @@ export class HttpExecutor<
468
570
 
469
571
  }
470
572
 
471
- // stateful operations are done in the response callback - its leverages rest generated functions
472
- if (responseCallback) {
473
-
474
- responseCallback(response, this.request, apiResponse)
475
573
 
574
+ const callback = () => this.runLifecycleHooks<"afterCommit">(
575
+ "afterCommit", {
576
+ config: this.config,
577
+ request: this.request,
578
+ response
579
+ });
580
+
581
+ if (undefined !== reactBootstrap && response) {
582
+ switch (requestMethod) {
583
+ case GET:
584
+ reactBootstrap.updateRestfulObjectArrays<RestTableInterface>({
585
+ dataOrCallback: Array.isArray(response.data.rest) ? response.data.rest : [response.data.rest],
586
+ stateKey: this.config.restModel.TABLE_NAME,
587
+ uniqueObjectId: this.config.restModel.PRIMARY_SHORT as (keyof RestTableInterface)[],
588
+ callback
589
+ })
590
+ break;
591
+ case POST:
592
+ this.postState(response, this.request, callback);
593
+ break;
594
+ case PUT:
595
+ this.putState(response, this.request, callback);
596
+ break;
597
+ case DELETE:
598
+ this.deleteState(response, this.request, callback);
599
+ break;
600
+ }
601
+ } else {
602
+ callback();
476
603
  }
477
604
 
478
605
  if (C6.GET === requestMethod) {
@@ -1,26 +1,40 @@
1
- import {apiReturn} from "@carbonorm/carbonnode";
1
+ import {iRestMethods} from "@carbonorm/carbonnode";
2
2
  import { PoolConnection, RowDataPacket } from 'mysql2/promise';
3
3
  import {buildSelectQuery} from "../builders/sqlBuilder";
4
4
  import {Executor} from "./Executor";
5
5
 
6
6
 
7
- export class SqlExecutor<CustomAndRequiredFields extends object, RestTableInterfaces extends object, RequestTableOverrides extends object, ResponseDataType, RestShortTableNames extends string>
8
- extends Executor<CustomAndRequiredFields, RestTableInterfaces, RequestTableOverrides, ResponseDataType, RestShortTableNames> {
9
-
10
- public execute(): Promise<apiReturn<ResponseDataType>> {
7
+ export class SqlExecutor<
8
+ RequestMethod extends iRestMethods,
9
+ RestShortTableName extends string = any,
10
+ RestTableInterface extends { [key: string]: any } = any,
11
+ PrimaryKey extends Extract<keyof RestTableInterface, string> = Extract<keyof RestTableInterface, string>,
12
+ CustomAndRequiredFields extends { [key: string]: any } = any,
13
+ RequestTableOverrides extends { [key in keyof RestTableInterface]: any } = { [key in keyof RestTableInterface]: any }
14
+ >
15
+ extends Executor<
16
+ RequestMethod,
17
+ RestShortTableName,
18
+ RestTableInterface,
19
+ PrimaryKey,
20
+ CustomAndRequiredFields,
21
+ RequestTableOverrides
22
+ > {
23
+
24
+ public execute() {
11
25
  switch (this.config.requestMethod) {
12
26
  case 'GET':
13
27
  return (this.select(
14
- this.config.tableName as RestShortTableNames,
28
+ this.config.restModel.TABLE_NAME,
15
29
  undefined,
16
30
  this.request
17
31
  ) as Promise<any>).then(rows => ({rest: rows})) as any;
18
32
  case 'POST':
19
- return this.insert(this.config.tableName as RestShortTableNames, this.request) as any;
33
+ return this.insert(this.config.restModel.TABLE_NAME, this.request) as any;
20
34
  case 'PUT':
21
- return this.update(this.config.tableName as RestShortTableNames, undefined, this.request) as any;
35
+ return this.update(this.config.restModel.TABLE_NAME, undefined, this.request) as any;
22
36
  case 'DELETE':
23
- return this.delete(this.config.tableName as RestShortTableNames, undefined, this.request) as any;
37
+ return this.delete(this.config.restModel.TABLE_NAME, undefined, this.request) as any;
24
38
  }
25
39
  }
26
40
 
@@ -0,0 +1,88 @@
1
+ import { describe, expect, test } from '@jest/globals';
2
+ import { checkAllRequestsComplete } from '@carbonorm/carbonnode';
3
+ import { act, waitFor } from '@testing-library/react';
4
+ import { C6 } from "./C6";
5
+
6
+ const fillString = () => Math.random().toString(36).substring(2, 12);
7
+ const fillNumber = () => Math.floor(Math.random() * 1000000);
8
+
9
+ const RESERVED_COLUMNS = ['created_at', 'updated_at', 'deleted_at'];
10
+
11
+ function buildTestData(tableModel: any): Record<string, any> {
12
+ const data: Record<string, any> = {};
13
+ const validation = tableModel.TYPE_VALIDATION;
14
+
15
+ for (const col of Object.keys(validation)) {
16
+ const { MYSQL_TYPE, SKIP_COLUMN_IN_POST, MAX_LENGTH } = validation[col];
17
+
18
+ if (SKIP_COLUMN_IN_POST || RESERVED_COLUMNS.includes(col)) continue;
19
+
20
+ if (MYSQL_TYPE.startsWith('varchar') || MYSQL_TYPE === 'text') {
21
+ let str = fillString();
22
+ if (MAX_LENGTH) str = str.substring(0, MAX_LENGTH);
23
+ data[col] = str;
24
+ } else if (MYSQL_TYPE.includes('int') || MYSQL_TYPE === 'decimal') {
25
+ data[col] = fillNumber();
26
+ } else if (MYSQL_TYPE === 'json') {
27
+ data[col] = {};
28
+ } else if (MYSQL_TYPE === 'tinyint(1)') {
29
+ data[col] = 1;
30
+ } else {
31
+ data[col] = null;
32
+ }
33
+ }
34
+
35
+ return data;
36
+ }
37
+
38
+ describe('CarbonORM table API integration tests', () => {
39
+ for (const [shortName, tableModel] of Object.entries(C6.TABLES)) {
40
+ const primaryKeys: string[] = tableModel.PRIMARY_SHORT;
41
+
42
+ // Get restOrm binding
43
+ const restBinding = (C6 as any)[shortName[0].toUpperCase() + shortName.slice(1)];
44
+ if (!restBinding) continue;
45
+
46
+ test(`[${shortName}] GET → POST → GET → PUT → DELETE`, async () => {
47
+
48
+ const testData = buildTestData(tableModel);
49
+
50
+ await act(async () => {
51
+
52
+ // GET all
53
+ const all = await restBinding.Get({});
54
+ expect(all?.data?.rest).toBeDefined();
55
+
56
+ // POST one
57
+ const post = await restBinding.Post(testData);
58
+ expect(post?.data?.created).toBeDefined();
59
+
60
+ const postID = post?.data?.created;
61
+ const pkName = primaryKeys[0];
62
+ testData[pkName] = postID;
63
+
64
+ // GET single
65
+ const select = await restBinding.Get({
66
+ [C6.WHERE]: {
67
+ [tableModel[pkName.toUpperCase()]]: postID
68
+ }
69
+ });
70
+
71
+ expect(select?.data?.rest?.[0]?.[pkName]).toEqual(postID);
72
+
73
+ // PUT update
74
+ const updated = await restBinding.Put(testData);
75
+ expect(updated?.data?.updated).toBeDefined();
76
+
77
+ // DELETE
78
+ const deleted = await restBinding.Delete({ [pkName]: postID });
79
+ expect(deleted?.data?.deleted).toBeDefined();
80
+
81
+ // Wait for all requests to settle
82
+ await waitFor(() => {
83
+ expect(checkAllRequestsComplete()).toEqual(true);
84
+ }, { timeout: 10000, interval: 1000 });
85
+ });
86
+ }, 100000);
87
+ }
88
+ });