@carbonorm/carbonnode 3.7.16 → 3.7.18
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/index.cjs.js +167 -174
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.esm.js +168 -175
- package/dist/index.esm.js.map +1 -1
- package/package.json +1 -1
- package/scripts/assets/handlebars/C6.test.ts.handlebars +27 -0
- package/src/__tests__/httpExecutorSingular.e2e.test.ts +81 -0
- package/src/__tests__/normalizeSingularRequest.test.ts +8 -8
- package/src/__tests__/sakila-db/C6.js +1 -1
- package/src/__tests__/sakila-db/C6.test.ts +27 -0
- package/src/__tests__/sakila-db/C6.ts +1 -1
- package/src/api/convertForRequestBody.ts +2 -2
- package/src/api/executors/HttpExecutor.ts +22 -42
- package/src/api/handlers/ExpressHandler.ts +10 -23
- package/src/api/utils/normalizeSingularRequest.ts +15 -4
|
@@ -59,5 +59,32 @@ describe('sakila-db generated C6 bindings', () => {
|
|
|
59
59
|
await waitForRequests();
|
|
60
60
|
});
|
|
61
61
|
}
|
|
62
|
+
|
|
63
|
+
it('[actor] PUT fully qualified keys', async () => {
|
|
64
|
+
const Actor = C6.ORM.Actor;
|
|
65
|
+
const testId = 1;
|
|
66
|
+
|
|
67
|
+
let result = await Actor.Get({ [Actor.ACTOR_ID]: testId } as any);
|
|
68
|
+
let data = result?.data ?? result;
|
|
69
|
+
const originalLastName = data?.rest?.[0]?.last_name;
|
|
70
|
+
|
|
71
|
+
await Actor.Put({
|
|
72
|
+
[Actor.ACTOR_ID]: testId,
|
|
73
|
+
[Actor.LAST_NAME]: 'Updated',
|
|
74
|
+
} as any);
|
|
75
|
+
|
|
76
|
+
result = await Actor.Get({
|
|
77
|
+
[Actor.ACTOR_ID]: testId,
|
|
78
|
+
cacheResults: false,
|
|
79
|
+
} as any);
|
|
80
|
+
data = result?.data ?? result;
|
|
81
|
+
expect(data?.rest?.[0]?.last_name).toBe('Updated');
|
|
82
|
+
|
|
83
|
+
await Actor.Put({
|
|
84
|
+
[Actor.ACTOR_ID]: testId,
|
|
85
|
+
[Actor.LAST_NAME]: originalLastName,
|
|
86
|
+
} as any);
|
|
87
|
+
await waitForRequests();
|
|
88
|
+
});
|
|
62
89
|
});
|
|
63
90
|
|
|
@@ -1940,7 +1940,7 @@ export type RestTableInterfaces = iActor
|
|
|
1940
1940
|
|
|
1941
1941
|
export const C6 : iC6Object<RestTableInterfaces> = {
|
|
1942
1942
|
...C6Constants,
|
|
1943
|
-
C6VERSION: '3.7.
|
|
1943
|
+
C6VERSION: '3.7.18',
|
|
1944
1944
|
IMPORT: async (tableName: string) : Promise<iDynamicApiImport> => {
|
|
1945
1945
|
|
|
1946
1946
|
tableName = tableName.toLowerCase();
|
|
@@ -71,8 +71,8 @@ export default function <
|
|
|
71
71
|
}
|
|
72
72
|
}
|
|
73
73
|
}
|
|
74
|
-
} else if (Object.
|
|
75
|
-
// Already a fully qualified column name
|
|
74
|
+
} else if (Object.keys(tableDefinition.COLUMNS).includes(value)) {
|
|
75
|
+
// Already using a fully qualified column name
|
|
76
76
|
const columnValue = restfulObject[value];
|
|
77
77
|
payload[value] = columnValue;
|
|
78
78
|
|
|
@@ -19,7 +19,6 @@ import {apiRequestCache, checkCache, userCustomClearCache} from "../utils/cacheM
|
|
|
19
19
|
import {sortAndSerializeQueryObject} from "../utils/sortAndSerializeQueryObject";
|
|
20
20
|
import {Executor} from "./Executor";
|
|
21
21
|
import {toastOptions, toastOptionsDevs} from "variables/toastOptions";
|
|
22
|
-
import {normalizeSingularRequest} from "../utils/normalizeSingularRequest";
|
|
23
22
|
|
|
24
23
|
export class HttpExecutor<
|
|
25
24
|
G extends OrmGenerics
|
|
@@ -334,9 +333,6 @@ export class HttpExecutor<
|
|
|
334
333
|
|
|
335
334
|
}
|
|
336
335
|
|
|
337
|
-
let addBackPK: (() => void) | undefined;
|
|
338
|
-
let removedPrimaryKV: { key: string; value: any } | undefined;
|
|
339
|
-
|
|
340
336
|
let apiResponse: G['RestTableInterface'][G['PrimaryKey']] | string | boolean | number | undefined;
|
|
341
337
|
|
|
342
338
|
let returnGetNextPageFunction = false;
|
|
@@ -350,7 +346,9 @@ export class HttpExecutor<
|
|
|
350
346
|
|
|
351
347
|
// todo - aggregate primary key check with condition check
|
|
352
348
|
// check if PK exists in query, clone so pop does not affect the real data
|
|
353
|
-
const
|
|
349
|
+
const primaryKeyList = structuredClone(TABLES[operatingTable]?.PRIMARY);
|
|
350
|
+
const primaryKeyFullyQualified = primaryKeyList?.pop();
|
|
351
|
+
const primaryKey = primaryKeyFullyQualified?.split('.')?.pop();
|
|
354
352
|
|
|
355
353
|
if (needsConditionOrPrimaryCheck) {
|
|
356
354
|
|
|
@@ -374,7 +372,7 @@ export class HttpExecutor<
|
|
|
374
372
|
|
|
375
373
|
if (undefined === query
|
|
376
374
|
|| null === query
|
|
377
|
-
||
|
|
375
|
+
|| (!(primaryKey! in query) && !(primaryKeyFullyQualified && primaryKeyFullyQualified in query))) {
|
|
378
376
|
|
|
379
377
|
if (true === debug && isLocal()) {
|
|
380
378
|
|
|
@@ -386,8 +384,8 @@ export class HttpExecutor<
|
|
|
386
384
|
|
|
387
385
|
}
|
|
388
386
|
|
|
389
|
-
|
|
390
|
-
|
|
387
|
+
const providedPrimary = query?.[primaryKey!] ?? (primaryKeyFullyQualified ? query?.[primaryKeyFullyQualified] : undefined);
|
|
388
|
+
if (undefined === providedPrimary || null === providedPrimary) {
|
|
391
389
|
|
|
392
390
|
toast.error('The primary key (' + primaryKey + ') provided is undefined or null explicitly!!')
|
|
393
391
|
|
|
@@ -405,27 +403,21 @@ export class HttpExecutor<
|
|
|
405
403
|
if (POST !== requestMethod
|
|
406
404
|
&& undefined !== query
|
|
407
405
|
&& null !== query
|
|
408
|
-
&& undefined !== primaryKey
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
query ??= {} as RequestQueryBody<
|
|
418
|
-
G['RequestMethod'],
|
|
419
|
-
G['RestTableInterface'],
|
|
420
|
-
G['CustomAndRequiredFields'],
|
|
421
|
-
G['RequestTableOverrides']
|
|
422
|
-
>;
|
|
423
|
-
query[primaryKey] = removedPkValue;
|
|
424
|
-
}
|
|
406
|
+
&& undefined !== primaryKey) {
|
|
407
|
+
|
|
408
|
+
const primaryVal = query[primaryKey!] ?? (primaryKeyFullyQualified ? query[primaryKeyFullyQualified] : undefined);
|
|
409
|
+
|
|
410
|
+
if (undefined !== primaryVal) {
|
|
411
|
+
|
|
412
|
+
restRequestUri += primaryVal + '/'
|
|
413
|
+
|
|
414
|
+
console.log('query', query, 'primaryKey', primaryKey)
|
|
425
415
|
|
|
426
|
-
|
|
416
|
+
} else {
|
|
417
|
+
|
|
418
|
+
console.log('query', query)
|
|
427
419
|
|
|
428
|
-
|
|
420
|
+
}
|
|
429
421
|
|
|
430
422
|
} else {
|
|
431
423
|
|
|
@@ -473,19 +465,11 @@ export class HttpExecutor<
|
|
|
473
465
|
withCredentials: withCredentials,
|
|
474
466
|
};
|
|
475
467
|
|
|
476
|
-
// Normalize singular request (GET/PUT/DELETE) into complex ORM shape
|
|
477
|
-
const normalizedQuery = normalizeSingularRequest(
|
|
478
|
-
requestMethod as any,
|
|
479
|
-
query as any,
|
|
480
|
-
restModel as any,
|
|
481
|
-
removedPrimaryKV
|
|
482
|
-
) as typeof query;
|
|
483
|
-
|
|
484
468
|
switch (requestMethod) {
|
|
485
469
|
case GET:
|
|
486
470
|
return [{
|
|
487
471
|
...baseConfig,
|
|
488
|
-
params:
|
|
472
|
+
params: query
|
|
489
473
|
}];
|
|
490
474
|
|
|
491
475
|
case POST:
|
|
@@ -498,12 +482,12 @@ export class HttpExecutor<
|
|
|
498
482
|
return [convert(query), baseConfig];
|
|
499
483
|
|
|
500
484
|
case PUT:
|
|
501
|
-
return [convert(
|
|
485
|
+
return [convert(query), baseConfig];
|
|
502
486
|
|
|
503
487
|
case DELETE:
|
|
504
488
|
return [{
|
|
505
489
|
...baseConfig,
|
|
506
|
-
data: convert(
|
|
490
|
+
data: convert(query)
|
|
507
491
|
}];
|
|
508
492
|
|
|
509
493
|
default:
|
|
@@ -523,10 +507,6 @@ export class HttpExecutor<
|
|
|
523
507
|
|
|
524
508
|
}
|
|
525
509
|
|
|
526
|
-
// todo - wip verify this works
|
|
527
|
-
// we had removed the value from the request to add to the URI.
|
|
528
|
-
addBackPK?.(); // adding back so post-processing methods work
|
|
529
|
-
|
|
530
510
|
// returning the promise with this then is important for tests. todo - we could make that optional.
|
|
531
511
|
// https://rapidapi.com/guides/axios-async-await
|
|
532
512
|
return axiosActiveRequest.then(async (response: AxiosResponse<ResponseDataType, any>): Promise<AxiosResponse<ResponseDataType, any>> => {
|
|
@@ -35,30 +35,17 @@ export function ExpressHandler({C6, mysqlPool}: { C6: iC6Object, mysqlPool: Pool
|
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
const primaryKeyName = primaryKeys[0];
|
|
38
|
-
const primaryKeyShort = primaryKeyName.split('.')[1];
|
|
39
38
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
case 'DELETE':
|
|
51
|
-
if (primary) {
|
|
52
|
-
payload[C6C.WHERE][primaryKeyName] = primary;
|
|
53
|
-
} else {
|
|
54
|
-
res.status(400).json({error: `Invalid request: ${method} requires a primary key (${primaryKeyName}).`});
|
|
55
|
-
}
|
|
56
|
-
break;
|
|
57
|
-
case 'POST':
|
|
58
|
-
break;
|
|
59
|
-
default:
|
|
60
|
-
res.status(405).json({error: `Method ${method} not allowed`});
|
|
61
|
-
return;
|
|
39
|
+
// If a primary key was provided in the URL, merge it into the payload.
|
|
40
|
+
// Support both complex requests using WHERE and singular requests
|
|
41
|
+
// where the primary key lives at the root of the payload.
|
|
42
|
+
if (primary) {
|
|
43
|
+
if (payload[C6C.WHERE]) {
|
|
44
|
+
payload[C6C.WHERE][primaryKeyName] =
|
|
45
|
+
payload[C6C.WHERE][primaryKeyName] ?? primary;
|
|
46
|
+
} else {
|
|
47
|
+
(payload as any)[primaryKeyName] =
|
|
48
|
+
(payload as any)[primaryKeyName] ?? primary;
|
|
62
49
|
}
|
|
63
50
|
}
|
|
64
51
|
|
|
@@ -60,7 +60,9 @@ export function normalizeSingularRequest<
|
|
|
60
60
|
if (value === undefined) {
|
|
61
61
|
// 2) fully-qualified key matching this short key (from PRIMARY list or by concatenation)
|
|
62
62
|
const fqCandidate = `${restModel.TABLE_NAME}.${pkShort}`;
|
|
63
|
-
const fqKey = pkFulls.find(
|
|
63
|
+
const fqKey = pkFulls.find(
|
|
64
|
+
fq => fq === fqCandidate || fq.endsWith(`.${pkShort}`)
|
|
65
|
+
) ?? fqCandidate;
|
|
64
66
|
value = requestObj[fqKey];
|
|
65
67
|
}
|
|
66
68
|
if (value === undefined && removedPrimary) {
|
|
@@ -93,9 +95,18 @@ export function normalizeSingularRequest<
|
|
|
93
95
|
...rest
|
|
94
96
|
} = request as any;
|
|
95
97
|
|
|
98
|
+
// Map short primary keys to fully-qualified column names
|
|
99
|
+
const shortToFull: Record<string, string> = {};
|
|
100
|
+
for (const [full, short] of Object.entries(restModel.COLUMNS || {})) {
|
|
101
|
+
shortToFull[short as string] = full;
|
|
102
|
+
}
|
|
103
|
+
const pkFullValues = Object.fromEntries(
|
|
104
|
+
Object.entries(pkValues).map(([k, v]) => [shortToFull[k] ?? k, v])
|
|
105
|
+
);
|
|
106
|
+
|
|
96
107
|
if (requestMethod === C6C.GET) {
|
|
97
108
|
const normalized: any = {
|
|
98
|
-
WHERE: { ...
|
|
109
|
+
WHERE: { ...pkFullValues },
|
|
99
110
|
};
|
|
100
111
|
// Preserve pagination if any was added previously
|
|
101
112
|
if ((request as any)[C6C.PAGINATION]) {
|
|
@@ -115,7 +126,7 @@ export function normalizeSingularRequest<
|
|
|
115
126
|
if (requestMethod === C6C.DELETE) {
|
|
116
127
|
const normalized: any = {
|
|
117
128
|
[C6C.DELETE]: true,
|
|
118
|
-
WHERE: { ...
|
|
129
|
+
WHERE: { ...pkFullValues },
|
|
119
130
|
};
|
|
120
131
|
return {
|
|
121
132
|
...normalized,
|
|
@@ -143,7 +154,7 @@ export function normalizeSingularRequest<
|
|
|
143
154
|
|
|
144
155
|
const normalized: any = {
|
|
145
156
|
[C6C.UPDATE]: updateBody,
|
|
146
|
-
WHERE: { ...
|
|
157
|
+
WHERE: { ...pkFullValues },
|
|
147
158
|
};
|
|
148
159
|
|
|
149
160
|
return {
|