@carbonorm/carbonnode 3.0.2 → 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,31 +6,117 @@ 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<
28
+ RequestMethod extends iRestMethods,
17
29
  RestShortTableName extends string = any,
18
30
  RestTableInterface extends { [key: string]: any } = any,
19
31
  PrimaryKey extends Extract<keyof RestTableInterface, string> = Extract<keyof RestTableInterface, string>,
20
32
  CustomAndRequiredFields extends { [key: string]: any } = any,
21
- RequestTableOverrides extends { [key: string]: any; } = { [key in keyof RestTableInterface]: any },
22
- ResponseDataType = any
33
+ RequestTableOverrides extends { [key in keyof RestTableInterface]: any } = { [key in keyof RestTableInterface]: any }
23
34
  >
24
35
  extends Executor<
36
+ RequestMethod,
25
37
  RestShortTableName,
26
38
  RestTableInterface,
27
39
  PrimaryKey,
28
40
  CustomAndRequiredFields,
29
- RequestTableOverrides,
30
- ResponseDataType
41
+ RequestTableOverrides
31
42
  > {
32
43
 
33
- 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>;
34
120
 
35
121
  const {
36
122
  C6,
@@ -38,13 +124,20 @@ export class HttpExecutor<
38
124
  restURL,
39
125
  withCredentials,
40
126
  restModel,
127
+ reactBootstrap,
41
128
  requestMethod,
42
- queryCallback,
43
- responseCallback,
44
129
  skipPrimaryCheck,
45
130
  clearCache,
46
131
  } = this.config
47
132
 
133
+
134
+ await this.runLifecycleHooks<"beforeProcessing">(
135
+ "beforeProcessing", {
136
+ config: this.config,
137
+ request: this.request,
138
+ });
139
+
140
+
48
141
  const tableName = restModel.TABLE_NAME;
49
142
 
50
143
  const fullTableList = Array.isArray(tableName) ? tableName : [tableName];
@@ -81,16 +174,6 @@ export class HttpExecutor<
81
174
  // thus the request shouldn't fire as is in custom cache
82
175
  let query: RequestQueryBody<Modify<RestTableInterface, RequestTableOverrides>> | undefined | null;
83
176
 
84
- if ('function' === typeof queryCallback) {
85
-
86
- query = queryCallback(this.request); // obj or obj[]
87
-
88
- } else {
89
-
90
- query = queryCallback;
91
-
92
- }
93
-
94
177
  if (undefined === query || null === query) {
95
178
 
96
179
  if (this.request.debug && isLocal) {
@@ -126,7 +209,7 @@ export class HttpExecutor<
126
209
  }
127
210
 
128
211
  // this could return itself with a new page number, or undefined if the end is reached
129
- const apiRequest = async (): Promise<apiReturn<ResponseDataType>> => {
212
+ const apiRequest = async (): Promise<apiReturn<DetermineResponseDataType<RequestMethod, RestTableInterface>>> => {
130
213
 
131
214
  const {
132
215
  debug,
@@ -134,7 +217,7 @@ export class HttpExecutor<
134
217
  dataInsertMultipleRows,
135
218
  success,
136
219
  fetchDependencies = eFetchDependencies.NONE,
137
- error = "An unexpected API error occurred!"
220
+ error = "An unexpected API error occurred!"
138
221
  } = this.request
139
222
 
140
223
  if (C6.GET === requestMethod
@@ -357,6 +440,12 @@ export class HttpExecutor<
357
440
 
358
441
  console.groupEnd()
359
442
 
443
+ this.runLifecycleHooks<"beforeExecution">(
444
+ "beforeExecution", {
445
+ config: this.config,
446
+ request: this.request
447
+ })
448
+
360
449
  const axiosActiveRequest: AxiosPromise<ResponseDataType> = axios![requestMethod.toLowerCase()]<ResponseDataType>(
361
450
  restRequestUri,
362
451
  ...((() => {
@@ -431,8 +520,9 @@ export class HttpExecutor<
431
520
 
432
521
  // returning the promise with this then is important for tests. todo - we could make that optional.
433
522
  // https://rapidapi.com/guides/axios-async-await
434
- return axiosActiveRequest.then(async (response): Promise<AxiosResponse<ResponseDataType, any>> => {
523
+ return axiosActiveRequest.then(async (response: AxiosResponse<ResponseDataType, any>): Promise<AxiosResponse<ResponseDataType, any>> => {
435
524
 
525
+ // noinspection SuspiciousTypeOfGuard
436
526
  if (typeof response.data === 'string') {
437
527
 
438
528
  if (isTest) {
@@ -458,6 +548,14 @@ export class HttpExecutor<
458
548
 
459
549
  }
460
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
461
559
  apiResponse = TestRestfulResponse(response, success, error)
462
560
 
463
561
  if (false === apiResponse) {
@@ -472,11 +570,36 @@ export class HttpExecutor<
472
570
 
473
571
  }
474
572
 
475
- // stateful operations are done in the response callback - its leverages rest generated functions
476
- if (responseCallback) {
477
-
478
- responseCallback(response, this.request, apiResponse)
479
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();
480
603
  }
481
604
 
482
605
  if (C6.GET === requestMethod) {
@@ -1,27 +1,27 @@
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
7
  export class SqlExecutor<
8
+ RequestMethod extends iRestMethods,
8
9
  RestShortTableName extends string = any,
9
10
  RestTableInterface extends { [key: string]: any } = any,
10
11
  PrimaryKey extends Extract<keyof RestTableInterface, string> = Extract<keyof RestTableInterface, string>,
11
12
  CustomAndRequiredFields extends { [key: string]: any } = any,
12
- RequestTableOverrides extends { [key: string]: any; } = { [key in keyof RestTableInterface]: any },
13
- ResponseDataType = any
13
+ RequestTableOverrides extends { [key in keyof RestTableInterface]: any } = { [key in keyof RestTableInterface]: any }
14
14
  >
15
15
  extends Executor<
16
+ RequestMethod,
16
17
  RestShortTableName,
17
18
  RestTableInterface,
18
19
  PrimaryKey,
19
20
  CustomAndRequiredFields,
20
- RequestTableOverrides,
21
- ResponseDataType
21
+ RequestTableOverrides
22
22
  > {
23
23
 
24
- public execute(): Promise<apiReturn<ResponseDataType>> {
24
+ public execute() {
25
25
  switch (this.config.requestMethod) {
26
26
  case 'GET':
27
27
  return (this.select(
@@ -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
+ });