@carbonorm/carbonnode 3.11.0 → 4.0.0

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.
@@ -13,7 +13,7 @@ export function ExpressHandler({C6, mysqlPool}: { C6: iC6Object, mysqlPool: Pool
13
13
  try {
14
14
  const incomingMethod = req.method.toUpperCase() as iRestMethods;
15
15
  const table = req.params.table;
16
- const primary = req.params.primary;
16
+ let primary = req.params.primary;
17
17
  // Support Axios interceptor promoting large GETs to POST with ?METHOD=GET
18
18
  const methodOverrideRaw = (req.query?.METHOD ?? req.query?.method) as unknown;
19
19
  const methodOverride = typeof methodOverrideRaw === 'string' ? methodOverrideRaw.toUpperCase() : undefined;
@@ -38,17 +38,40 @@ export function ExpressHandler({C6, mysqlPool}: { C6: iC6Object, mysqlPool: Pool
38
38
  return;
39
39
  }
40
40
 
41
- const primaryKeys = C6.TABLES[table].PRIMARY;
41
+ const restModel = C6.TABLES[table];
42
+ const primaryKeys = restModel.PRIMARY;
43
+ const primaryShortKeys = restModel.PRIMARY_SHORT ?? [];
44
+ const columnMap = restModel.COLUMNS ?? {};
45
+ const resolveShortKey = (fullKey: string, index: number) =>
46
+ (columnMap as any)[fullKey] ?? primaryShortKeys[index] ?? fullKey.split('.').pop() ?? fullKey;
47
+ const hasPrimaryKeyValues = (data: any) => {
48
+ if (!data || typeof data !== 'object') return false;
49
+ const whereClause = (data as any)[C6C.WHERE];
50
+ const hasKeyValue = (obj: any, fullKey: string, shortKey: string) => {
51
+ if (!obj || typeof obj !== 'object') return false;
52
+ const fullValue = obj[fullKey];
53
+ if (fullValue !== undefined && fullValue !== null) return true;
54
+ const shortValue = shortKey ? obj[shortKey] : undefined;
55
+ return shortValue !== undefined && shortValue !== null;
56
+ };
57
+ return primaryKeys.every((fullKey, index) => {
58
+ const shortKey = resolveShortKey(fullKey, index);
59
+ return hasKeyValue(whereClause, fullKey, shortKey) || hasKeyValue(data, fullKey, shortKey);
60
+ });
61
+ };
42
62
 
43
63
  if (primary && primaryKeys.length !== 1) {
44
- if (primaryKeys.length > 1) {
64
+ if (primaryKeys.length > 1 && hasPrimaryKeyValues(payload)) {
65
+ primary = undefined;
66
+ } else if (primaryKeys.length > 1) {
45
67
  res.status(400).json({error: `Table ${table} has multiple primary keys. Cannot implicitly determine key.`});
46
68
  return;
69
+ } else {
70
+ res.status(400).json({
71
+ error: `Table ${table} has no primary keys. Please specify one.`
72
+ });
73
+ return;
47
74
  }
48
- res.status(400).json({
49
- error: `Table ${table} has no primary keys. Please specify one.`
50
- });
51
- return;
52
75
  }
53
76
 
54
77
  const primaryKeyName = primaryKeys[0];
@@ -378,6 +378,60 @@ export abstract class ConditionBuilder<
378
378
  throw new Error('Unsupported operand type in SQL expression.');
379
379
  }
380
380
 
381
+ private isPlainArrayLiteral(value: any): boolean {
382
+ if (!Array.isArray(value)) return false;
383
+ return value.every(item => {
384
+ if (item === null) return true;
385
+ const type = typeof item;
386
+ if (type === 'string' || type === 'number' || type === 'boolean') return true;
387
+ if (Array.isArray(item)) return this.isPlainArrayLiteral(item);
388
+ if (item && typeof item === 'object') return this.isPlainObjectLiteral(item);
389
+ return false;
390
+ });
391
+ }
392
+
393
+ private isPlainObjectLiteral(value: any): boolean {
394
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
395
+ if (value instanceof Date) return false;
396
+ if (typeof Buffer !== 'undefined' && Buffer.isBuffer && Buffer.isBuffer(value)) return false;
397
+
398
+ const normalized = value instanceof Map ? Object.fromEntries(value) : value;
399
+ if (C6C.SUBSELECT in (normalized as any)) return false;
400
+
401
+ const entries = Object.entries(normalized as Record<string, any>);
402
+ if (entries.length === 0) return true;
403
+
404
+ if (entries.some(([key]) => this.isOperator(key) || this.BOOLEAN_OPERATORS.has(key))) {
405
+ return false;
406
+ }
407
+
408
+ if (entries.some(([key]) => typeof key === 'string' && (this.isColumnRef(key) || key.includes('.')))) {
409
+ return false;
410
+ }
411
+
412
+ return true;
413
+ }
414
+
415
+ protected serializeUpdateValue(
416
+ value: any,
417
+ params: any[] | Record<string, any>,
418
+ contextColumn?: string
419
+ ): string {
420
+ const normalized = value instanceof Map ? Object.fromEntries(value) : value;
421
+
422
+ if (this.isPlainArrayLiteral(normalized) || this.isPlainObjectLiteral(normalized)) {
423
+ return this.addParam(params, contextColumn ?? '', JSON.stringify(normalized));
424
+ }
425
+
426
+ const { sql, isReference, isExpression, isSubSelect } = this.serializeOperand(normalized, params, contextColumn);
427
+
428
+ if (!isReference && !isExpression && !isSubSelect && typeof normalized === 'object' && normalized !== null) {
429
+ throw new Error('Unsupported operand type in SQL expression.');
430
+ }
431
+
432
+ return sql;
433
+ }
434
+
381
435
  private ensurePlainObject<T>(value: T): any {
382
436
  if (value instanceof Map) {
383
437
  return Object.fromEntries(value as unknown as Map<string, any>);
@@ -18,13 +18,15 @@ export class PostQueryBuilder<G extends OrmGenerics> extends ConditionBuilder<G>
18
18
  const verb = C6C.REPLACE in this.request ? C6C.REPLACE : C6C.INSERT;
19
19
  const body = verb in this.request ? this.request[verb] : this.request;
20
20
  const keys = Object.keys(body);
21
- const params: any[] = []
21
+ const params: any[] | Record<string, any> = this.useNamedParams ? {} : [];
22
22
  const placeholders: string[] = []
23
23
 
24
24
 
25
25
  for (const key of keys) {
26
26
  const value = body[key];
27
- const placeholder = this.addParam(params, key, value);
27
+ const trimmed = this.trimTablePrefix(table, key);
28
+ const qualified = `${table}.${trimmed}`;
29
+ const placeholder = this.serializeUpdateValue(value, params, qualified);
28
30
  placeholders.push(placeholder);
29
31
  }
30
32
 
@@ -39,7 +39,9 @@ export class UpdateQueryBuilder<G extends OrmGenerics> extends PaginationBuilder
39
39
  .map(([col, val]) => {
40
40
  const trimmed = this.trimTablePrefix(table, col);
41
41
  const qualified = `${table}.${trimmed}`;
42
- return `\`${trimmed}\` = ${this.addParam(params, qualified, val)}`;
42
+ this.assertValidIdentifier(qualified, 'UPDATE SET');
43
+ const rightSql = this.serializeUpdateValue(val, params, qualified);
44
+ return `\`${trimmed}\` = ${rightSql}`;
43
45
  });
44
46
 
45
47
  sql += ` SET ${setClauses.join(', ')}`;
@@ -97,7 +97,13 @@ export type RequestQueryBody<
97
97
  export interface iCacheAPI<ResponseDataType = any> {
98
98
  requestArgumentsSerialized: string;
99
99
  request: AxiosPromise<ResponseDataType>;
100
- response?: AxiosResponse;
100
+ response?: AxiosResponse & {
101
+ __carbonTiming?: {
102
+ start: number;
103
+ end: number;
104
+ duration: number;
105
+ }
106
+ },
101
107
  final?: boolean;
102
108
  }
103
109
 
@@ -105,40 +111,48 @@ export interface iChangeC6Data {
105
111
  rowCount: number;
106
112
  }
107
113
 
108
- export interface iDeleteC6RestResponse<RestData = any, RequestData = any> extends iChangeC6Data, iC6RestResponse<RestData> {
114
+ // New discriminated REST response type based on HTTP method
115
+ export type C6RestResponse<
116
+ Method extends iRestMethods,
117
+ RestData extends { [key: string]: any },
118
+ Overrides = {}
119
+ > = {
120
+ rest: Method extends 'GET' ? Modify<RestData, Overrides>[] : Modify<RestData, Overrides>;
121
+ session?: any;
122
+ sql?: any;
123
+ } & (Method extends 'GET'
124
+ ? { next?: () => Promise<DetermineResponseDataType<'GET', RestData, Overrides>> }
125
+ : {});
126
+
127
+ export interface iC6RestResponse<RestData> {
128
+ // Backwards compatibility: base interface for rest/sql/session (singular)
129
+ rest: RestData;
130
+ session?: any;
131
+ sql?: any;
132
+ }
133
+
134
+ export interface iDeleteC6RestResponse<RestData extends { [key: string]: any; }, RequestData = any> extends iChangeC6Data, C6RestResponse<'DELETE', RestData> {
109
135
  deleted: boolean | number | string | RequestData;
110
136
  }
111
137
 
112
- export interface iPostC6RestResponse<RestData = any> extends iC6RestResponse<RestData> {
138
+ export interface iPostC6RestResponse<RestData extends { [key: string]: any; }> extends C6RestResponse<'POST', RestData> {
113
139
  created: boolean | number | string;
114
140
  }
115
141
 
116
- export interface iPutC6RestResponse<RestData = any, RequestData = any> extends iChangeC6Data, iC6RestResponse<RestData> {
142
+ export interface iPutC6RestResponse<RestData extends { [key: string]: any; }, RequestData = any> extends iChangeC6Data, C6RestResponse<'PUT', RestData> {
117
143
  updated: boolean | number | string | RequestData;
118
144
  }
119
145
 
120
- export interface iC6RestResponse<RestData> {
121
- rest: RestData;
122
- session?: any;
123
- sql?: any;
124
- }
125
-
126
146
  export interface iGetC6RestResponse<
127
147
  ResponseDataType extends { [key: string]: any },
128
148
  ResponseDataOverrides = {}
129
- > extends iC6RestResponse<
130
- // TODO - We removed Modify<ResponseDataType, ResponseDataOverrides> |
131
- Modify<ResponseDataType, ResponseDataOverrides>[]
132
- > {
133
- next?: () => Promise<DetermineResponseDataType<"GET", ResponseDataType, ResponseDataOverrides>>;
134
- }
149
+ > extends C6RestResponse<'GET', ResponseDataType, ResponseDataOverrides> {}
135
150
 
136
151
  export type DetermineResponseDataType<
137
152
  Method extends iRestMethods,
138
153
  RestTableInterface extends { [key: string]: any },
139
154
  ResponseDataOverrides = {}
140
- > = null |
141
- (Method extends 'POST'
155
+ > = (Method extends 'POST'
142
156
  ? iPostC6RestResponse<RestTableInterface>
143
157
  : Method extends 'GET'
144
158
  ? iGetC6RestResponse<RestTableInterface, ResponseDataOverrides>
@@ -1,46 +1,87 @@
1
- import {AxiosPromise} from "axios";
1
+ import { AxiosPromise } from "axios";
2
2
  import { iCacheAPI } from "../types/ormInterfaces";
3
3
 
4
- // do not remove entries from this array. It is used to track the progress of API requests.
5
- // position in array is important. Do not sort. To not add to begging.
6
- export let apiRequestCache: iCacheAPI[] = [];
7
-
8
- export let userCustomClearCache: (() => void)[] = [];
9
-
10
- interface iClearCache {
11
- ignoreWarning: boolean
12
- }
13
-
14
- export function clearCache(props?: iClearCache) {
15
-
16
- if (false === props?.ignoreWarning) {
17
-
18
- console.warn('The rest api clearCache should only be used with extreme care! Avoid using this in favor of using `cacheResults : boolean`.')
19
-
4
+ // -----------------------------------------------------------------------------
5
+ // Cache Storage
6
+ // -----------------------------------------------------------------------------
7
+ export const apiRequestCache = new Map<string, iCacheAPI>();
8
+ export const userCustomClearCache: (() => void)[] = [];
9
+
10
+ // -----------------------------------------------------------------------------
11
+ // Cache Key Generator (safe, fixed-size ~40 chars)
12
+ // -----------------------------------------------------------------------------
13
+ // -----------------------------------------------------------------------------
14
+ // Browser-safe deterministic hash (FNV-1a)
15
+ // -----------------------------------------------------------------------------
16
+ function fnv1a(str: string): string {
17
+ let h = 0x811c9dc5;
18
+ for (let i = 0; i < str.length; i++) {
19
+ h ^= str.charCodeAt(i);
20
+ h = (h * 0x01000193) >>> 0;
20
21
  }
21
-
22
- userCustomClearCache.map((f) => 'function' === typeof f && f());
23
-
24
- userCustomClearCache = apiRequestCache = []
25
-
22
+ return h.toString(16);
26
23
  }
27
24
 
28
- export function checkCache<ResponseDataType = any, RestShortTableNames = string>(cacheResult: iCacheAPI<ResponseDataType>, requestMethod: string, tableName: RestShortTableNames | RestShortTableNames[], request: any): false | AxiosPromise<ResponseDataType> {
29
-
30
- if (undefined === cacheResult) {
31
-
32
- return false;
25
+ function makeCacheKey(
26
+ method: string,
27
+ tableName: string | string[],
28
+ requestData: unknown,
29
+ ): string {
30
+ const raw = JSON.stringify([method, tableName, requestData]);
31
+ return fnv1a(raw);
32
+ }
33
33
 
34
+ // -----------------------------------------------------------------------------
35
+ // Clear Cache (no shared-array bugs)
36
+ // -----------------------------------------------------------------------------
37
+ export function clearCache(props?: { ignoreWarning?: boolean }): void {
38
+ if (!props?.ignoreWarning) {
39
+ console.warn(
40
+ "The REST API clearCache should only be used with extreme care!",
41
+ );
34
42
  }
35
43
 
36
- console.groupCollapsed('%c API: The request on (' + tableName + ') is in cache. Returning the request Promise!', 'color: #0c0')
37
-
38
- console.log('%c ' + requestMethod + ' ' + tableName, 'color: #0c0')
39
-
40
- console.log('%c Request Data (note you may see the success and/or error prompt):', 'color: #0c0', request)
44
+ for (const fn of userCustomClearCache) {
45
+ try {
46
+ fn();
47
+ } catch {}
48
+ }
41
49
 
42
- console.groupEnd()
50
+ apiRequestCache.clear();
51
+ }
43
52
 
44
- return cacheResult.request;
53
+ // -----------------------------------------------------------------------------
54
+ // Check Cache (dedupe via hashed key)
55
+ // -----------------------------------------------------------------------------
56
+ export function checkCache<ResponseDataType = any>(
57
+ method: string,
58
+ tableName: string | string[],
59
+ requestData: any,
60
+ ): AxiosPromise<ResponseDataType> | false {
61
+ const key = makeCacheKey(method, tableName, requestData);
62
+ const cached = apiRequestCache.get(key);
63
+
64
+ if (!cached) return false;
65
+
66
+ console.groupCollapsed(
67
+ `%c API cache hit for ${method} ${tableName}`,
68
+ "color:#0c0",
69
+ );
70
+ console.log("Request Data:", requestData);
71
+ console.groupEnd();
72
+
73
+ return cached.request;
74
+ }
45
75
 
76
+ // -----------------------------------------------------------------------------
77
+ // Store Cache Entry (drop-in compatible)
78
+ // -----------------------------------------------------------------------------
79
+ export function setCache<ResponseDataType = any>(
80
+ method: string,
81
+ tableName: string | string[],
82
+ requestData: any,
83
+ cacheEntry: iCacheAPI<ResponseDataType>,
84
+ ): void {
85
+ const key = makeCacheKey(method, tableName, requestData);
86
+ apiRequestCache.set(key, cacheEntry);
46
87
  }
@@ -2,13 +2,15 @@ import {apiRequestCache} from "./cacheManager";
2
2
 
3
3
  export function checkAllRequestsComplete(): true | (string[]) {
4
4
 
5
- const stillRunning = apiRequestCache.filter((cache) => undefined === cache.response)
5
+ const cacheEntries = Array.from(apiRequestCache.values())
6
+
7
+ const stillRunning = cacheEntries.filter((cache) => undefined === cache.response)
6
8
 
7
9
  if (stillRunning.length !== 0) {
8
10
 
9
11
  if (document === null || document === undefined) {
10
12
 
11
- throw new Error('document is undefined while waiting for API requests to complete (' + JSON.stringify(apiRequestCache) + ')')
13
+ throw new Error('document is undefined while waiting for API requests to complete (' + JSON.stringify(cacheEntries) + ')')
12
14
 
13
15
  }
14
16
 
@@ -21,4 +23,4 @@ export function checkAllRequestsComplete(): true | (string[]) {
21
23
 
22
24
  return true
23
25
 
24
- }
26
+ }
@@ -1,12 +1,5 @@
1
1
  const isNode = () => {
2
-
3
- console.log('Checking if running in Node.js environment...');
4
-
5
- const isNodeEnv = typeof process !== 'undefined' && !!process.versions?.node;
6
-
7
- console.log(`Is Node.js environment: ${isNodeEnv}`);
8
-
9
- return isNodeEnv;
2
+ return typeof process !== 'undefined' && !!process.versions?.node;
10
3
  }
11
4
 
12
5
  export default isNode;