@carbonorm/carbonnode 6.0.19 → 6.1.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.
Files changed (89) hide show
  1. package/README.md +46 -1
  2. package/dist/constants/C6Constants.d.ts +342 -338
  3. package/dist/executors/SqlExecutor.d.ts +8 -0
  4. package/dist/index.cjs.js +751 -272
  5. package/dist/index.cjs.js.map +1 -1
  6. package/dist/index.d.ts +1 -0
  7. package/dist/index.esm.js +744 -273
  8. package/dist/index.esm.js.map +1 -1
  9. package/dist/orm/builders/AggregateBuilder.d.ts +5 -1
  10. package/dist/orm/builders/ConditionBuilder.d.ts +2 -3
  11. package/dist/orm/builders/ExpressionSerializer.d.ts +22 -0
  12. package/dist/orm/builders/PaginationBuilder.d.ts +4 -6
  13. package/dist/orm/queryHelpers.d.ts +12 -1
  14. package/dist/types/mysqlTypes.d.ts +6 -1
  15. package/dist/types/ormInterfaces.d.ts +7 -5
  16. package/dist/utils/cacheManager.d.ts +3 -2
  17. package/package.json +2 -2
  18. package/scripts/assets/handlebars/C6.test.ts.handlebars +4 -4
  19. package/src/__tests__/cacheManager.test.ts +28 -0
  20. package/src/__tests__/expressServer.e2e.test.ts +26 -17
  21. package/src/__tests__/httpExecutorSingular.e2e.test.ts +53 -14
  22. package/src/__tests__/normalizeSingularRequest.test.ts +26 -8
  23. package/src/__tests__/sakila-db/C6.js +1 -1
  24. package/src/__tests__/sakila-db/C6.mysqldump.json +1 -1
  25. package/src/__tests__/sakila-db/C6.mysqldump.sql +1 -1
  26. package/src/__tests__/sakila-db/C6.sqlAllowList.json +1 -1
  27. package/src/__tests__/sakila-db/C6.test.ts +4 -4
  28. package/src/__tests__/sakila-db/C6.ts +1 -1
  29. package/src/__tests__/sakila-db/sqlResponses/C6.actor.post.json +11 -4
  30. package/src/__tests__/sakila-db/sqlResponses/C6.actor.post.latest.json +3 -3
  31. package/src/__tests__/sakila-db/sqlResponses/C6.actor.put.json +1 -1
  32. package/src/__tests__/sakila-db/sqlResponses/C6.actor.put.lookup.json +3 -3
  33. package/src/__tests__/sakila-db/sqlResponses/C6.address.post.json +26 -7
  34. package/src/__tests__/sakila-db/sqlResponses/C6.address.post.latest.json +5 -5
  35. package/src/__tests__/sakila-db/sqlResponses/C6.address.put.json +1 -1
  36. package/src/__tests__/sakila-db/sqlResponses/C6.address.put.lookup.json +5 -5
  37. package/src/__tests__/sakila-db/sqlResponses/C6.category.post.json +9 -3
  38. package/src/__tests__/sakila-db/sqlResponses/C6.category.post.latest.json +2 -2
  39. package/src/__tests__/sakila-db/sqlResponses/C6.category.put.json +1 -1
  40. package/src/__tests__/sakila-db/sqlResponses/C6.category.put.lookup.json +2 -2
  41. package/src/__tests__/sakila-db/sqlResponses/C6.city.post.json +10 -3
  42. package/src/__tests__/sakila-db/sqlResponses/C6.city.post.latest.json +2 -2
  43. package/src/__tests__/sakila-db/sqlResponses/C6.city.put.json +1 -1
  44. package/src/__tests__/sakila-db/sqlResponses/C6.city.put.lookup.json +2 -2
  45. package/src/__tests__/sakila-db/sqlResponses/C6.country.post.json +9 -3
  46. package/src/__tests__/sakila-db/sqlResponses/C6.country.post.latest.json +2 -2
  47. package/src/__tests__/sakila-db/sqlResponses/C6.country.put.json +1 -1
  48. package/src/__tests__/sakila-db/sqlResponses/C6.country.put.lookup.json +2 -2
  49. package/src/__tests__/sakila-db/sqlResponses/C6.customer.post.json +18 -6
  50. package/src/__tests__/sakila-db/sqlResponses/C6.customer.post.latest.json +5 -5
  51. package/src/__tests__/sakila-db/sqlResponses/C6.customer.put.json +1 -1
  52. package/src/__tests__/sakila-db/sqlResponses/C6.customer.put.lookup.json +5 -5
  53. package/src/__tests__/sakila-db/sqlResponses/C6.film.post.json +18 -3
  54. package/src/__tests__/sakila-db/sqlResponses/C6.film.post.latest.json +2 -2
  55. package/src/__tests__/sakila-db/sqlResponses/C6.film.put.json +1 -1
  56. package/src/__tests__/sakila-db/sqlResponses/C6.film.put.lookup.json +2 -2
  57. package/src/__tests__/sakila-db/sqlResponses/C6.inventory.post.json +9 -2
  58. package/src/__tests__/sakila-db/sqlResponses/C6.inventory.post.latest.json +1 -1
  59. package/src/__tests__/sakila-db/sqlResponses/C6.inventory.put.json +1 -1
  60. package/src/__tests__/sakila-db/sqlResponses/C6.inventory.put.lookup.json +1 -1
  61. package/src/__tests__/sakila-db/sqlResponses/C6.language.post.json +9 -3
  62. package/src/__tests__/sakila-db/sqlResponses/C6.language.post.latest.json +2 -2
  63. package/src/__tests__/sakila-db/sqlResponses/C6.language.put.json +1 -1
  64. package/src/__tests__/sakila-db/sqlResponses/C6.language.put.lookup.json +2 -2
  65. package/src/__tests__/sakila-db/sqlResponses/C6.payment.post.json +13 -3
  66. package/src/__tests__/sakila-db/sqlResponses/C6.payment.post.latest.json +2 -2
  67. package/src/__tests__/sakila-db/sqlResponses/C6.payment.put.lookup.json +2 -2
  68. package/src/__tests__/sakila-db/sqlResponses/C6.rental.join.json +10 -10
  69. package/src/__tests__/sakila-db/sqlResponses/C6.rental.post.json +14 -4
  70. package/src/__tests__/sakila-db/sqlResponses/C6.rental.post.latest.json +3 -3
  71. package/src/__tests__/sakila-db/sqlResponses/C6.rental.put.json +1 -1
  72. package/src/__tests__/sakila-db/sqlResponses/C6.rental.put.lookup.json +3 -3
  73. package/src/__tests__/sqlBuilders.complex.test.ts +62 -74
  74. package/src/__tests__/sqlBuilders.expressions.test.ts +58 -30
  75. package/src/__tests__/sqlBuilders.test.ts +68 -4
  76. package/src/__tests__/sqlExecutorPostUuid.test.ts +185 -0
  77. package/src/constants/C6Constants.ts +3 -1
  78. package/src/executors/HttpExecutor.ts +35 -6
  79. package/src/executors/SqlExecutor.ts +232 -4
  80. package/src/index.ts +1 -0
  81. package/src/orm/builders/AggregateBuilder.ts +67 -106
  82. package/src/orm/builders/ConditionBuilder.ts +69 -93
  83. package/src/orm/builders/ExpressionSerializer.ts +275 -0
  84. package/src/orm/builders/PaginationBuilder.ts +24 -34
  85. package/src/orm/queryHelpers.ts +29 -0
  86. package/src/types/mysqlTypes.ts +130 -9
  87. package/src/types/ormInterfaces.ts +7 -7
  88. package/src/utils/cacheManager.ts +6 -4
  89. package/src/utils/normalizeSingularRequest.ts +11 -4
@@ -1,7 +1,11 @@
1
1
  import { Executor } from "../../executors/Executor";
2
2
  import { OrmGenerics } from "../../types/ormGenerics";
3
+ import { iSerializedExpression, tSqlParams } from "./ExpressionSerializer";
3
4
  export declare abstract class AggregateBuilder<G extends OrmGenerics> extends Executor<G> {
4
5
  protected selectAliases: Set<string>;
5
6
  protected assertValidIdentifier(_identifier: string, _context: string): void;
6
- buildAggregateField(field: string | any[], params?: any[] | Record<string, any>): string;
7
+ protected isReferenceExpression(value: string): boolean;
8
+ protected isKnownFunction(functionName: string): boolean;
9
+ protected serializeExpression(expression: any, params?: tSqlParams, context?: string, contextColumn?: string): iSerializedExpression;
10
+ buildAggregateField(field: string | any[], params?: tSqlParams): string;
7
11
  }
@@ -9,6 +9,7 @@ export declare abstract class ConditionBuilder<G extends OrmGenerics> extends Ag
9
9
  protected registerAlias(alias: string, table: string): void;
10
10
  protected assertValidIdentifier(identifier: string, context: string): void;
11
11
  protected isColumnRef(ref: string): boolean;
12
+ protected isReferenceExpression(value: string): boolean;
12
13
  abstract build(table: string): SqlBuilderResult;
13
14
  execute(): Promise<DetermineResponseDataType<G['RequestMethod'], G['RestTableInterface']>>;
14
15
  private readonly BOOLEAN_OPERATORS;
@@ -19,12 +20,10 @@ export declare abstract class ConditionBuilder<G extends OrmGenerics> extends Ag
19
20
  private normalizeOperatorKey;
20
21
  private formatOperator;
21
22
  private isOperator;
22
- private looksLikeSafeFunctionExpression;
23
23
  private ensureWrapped;
24
24
  private joinBooleanParts;
25
- private normalizeFunctionField;
26
- private buildFunctionCall;
27
25
  private serializeOperand;
26
+ private isExpressionTuple;
28
27
  private isPlainArrayLiteral;
29
28
  private isPlainObjectLiteral;
30
29
  private resolveColumnDefinition;
@@ -0,0 +1,22 @@
1
+ export type tSqlParams = any[] | Record<string, any>;
2
+ export interface iSerializedExpression {
3
+ sql: string;
4
+ isReference: boolean;
5
+ isExpression: boolean;
6
+ isSubSelect: boolean;
7
+ }
8
+ export interface iExpressionSerializerHooks {
9
+ assertValidIdentifier(identifier: string, context: string): void;
10
+ isReference(value: string): boolean;
11
+ addParam?: (params: tSqlParams, column: string, value: any) => string;
12
+ buildScalarSubSelect?: (subRequest: any, params: tSqlParams) => string;
13
+ onAlias?: (alias: string) => void;
14
+ isKnownFunction?: (functionName: string) => boolean;
15
+ }
16
+ export interface iExpressionSerializerOptions {
17
+ hooks: iExpressionSerializerHooks;
18
+ params?: tSqlParams;
19
+ context: string;
20
+ contextColumn?: string;
21
+ }
22
+ export declare const serializeSqlExpression: (value: any, opts: iExpressionSerializerOptions) => iSerializedExpression;
@@ -6,12 +6,10 @@ export declare abstract class PaginationBuilder<G extends OrmGenerics> extends J
6
6
  *
7
7
  * Accepted structures:
8
8
  * ```ts
9
- * ORDER: {
10
- * // simple column with direction
11
- * [property_units.UNIT_ID]: "DESC",
12
- * // function call (array of arguments)
13
- * [C6Constants.ST_DISTANCE_SPHERE]: [property_units.LOCATION, F(property_units.LOCATION, "pu_target")]
14
- * }
9
+ * ORDER: [
10
+ * [property_units.UNIT_ID, "DESC"],
11
+ * [[C6Constants.ST_DISTANCE_SPHERE, property_units.LOCATION, F(property_units.LOCATION, "pu_target")], "ASC"],
12
+ * ]
15
13
  * ```
16
14
  */
17
15
  buildPaginationClause(pagination: any, params?: any[] | Record<string, any>): string;
@@ -1,4 +1,9 @@
1
- type DerivedTableSpec = Record<string, any> & {};
1
+ import { C6C } from "../constants/C6Constants";
2
+ import { OrderDirection, OrderTerm, SQLExpression, SQLKnownFunction } from "../types/mysqlTypes";
3
+ type DerivedTableSpec = Record<string, any> & {
4
+ [C6C.SUBSELECT]?: Record<string, any>;
5
+ [C6C.AS]?: string;
6
+ };
2
7
  export declare const isDerivedTableKey: (key: string) => boolean;
3
8
  export declare const resolveDerivedTable: (key: string) => DerivedTableSpec | undefined;
4
9
  export declare const derivedTable: <T extends DerivedTableSpec>(spec: T) => T;
@@ -8,4 +13,10 @@ export declare const fieldEq: (leftCol: string, rightCol: string, leftAlias: str
8
13
  export declare const distSphere: (fromCol: string, toCol: string, fromAlias: string, toAlias: string) => any[];
9
14
  export declare const bbox: (minLng: number, minLat: number, maxLng: number, maxLat: number) => any[];
10
15
  export declare const stContains: (envelope: string, shape: string) => any[];
16
+ export declare const fn: <Fn extends SQLKnownFunction>(functionName: Fn, ...args: SQLExpression[]) => [Fn, ...SQLExpression[]];
17
+ export declare const call: (functionName: string, ...args: SQLExpression[]) => [typeof C6C.CALL, string, ...SQLExpression[]];
18
+ export declare const alias: (expression: SQLExpression, aliasName: string) => [typeof C6C.AS, SQLExpression, string];
19
+ export declare const distinct: (expression: SQLExpression) => [typeof C6C.DISTINCT, SQLExpression];
20
+ export declare const lit: (value: any) => [typeof C6C.LIT, any];
21
+ export declare const order: (expression: SQLExpression, direction?: OrderDirection) => OrderTerm;
11
22
  export {};
@@ -1,4 +1,9 @@
1
- export type SQLFunction = 'COUNT' | 'GROUP_CONCAT' | 'MAX' | 'MIN' | 'SUM' | 'DISTINCT';
1
+ export declare const SQL_KNOWN_FUNCTIONS: readonly ["ADDDATE", "ADDTIME", "CONCAT", "CONVERT_TZ", "COUNT", "COUNT_ALL", "CURRENT_DATE", "CURRENT_TIMESTAMP", "DAY", "DAY_HOUR", "DAY_MICROSECOND", "DAY_MINUTE", "DAY_SECOND", "DAYNAME", "DAYOFMONTH", "DAYOFWEEK", "DAYOFYEAR", "DATE", "DATE_ADD", "DATEDIFF", "DATE_SUB", "DATE_FORMAT", "EXTRACT", "FROM_DAYS", "FROM_UNIXTIME", "GET_FORMAT", "GROUP_CONCAT", "HEX", "HOUR", "HOUR_MICROSECOND", "HOUR_MINUTE", "HOUR_SECOND", "INTERVAL", "LOCALTIME", "LOCALTIMESTAMP", "MAKEDATE", "MAKETIME", "MAX", "MBRContains", "MICROSECOND", "MIN", "MINUTE", "MINUTE_MICROSECOND", "MINUTE_SECOND", "MONTH", "MONTHNAME", "NOW", "POINT", "POLYGON", "SECOND", "SECOND_MICROSECOND", "ST_Area", "ST_AsBinary", "ST_AsText", "ST_Buffer", "ST_Contains", "ST_Crosses", "ST_Difference", "ST_Dimension", "ST_Disjoint", "ST_Distance", "ST_Distance_Sphere", "ST_EndPoint", "ST_Envelope", "ST_Equals", "ST_GeomFromGeoJSON", "ST_GeomFromText", "ST_GeomFromWKB", "ST_Intersects", "ST_Length", "ST_MakeEnvelope", "ST_Overlaps", "ST_Point", "ST_SetSRID", "ST_SRID", "ST_StartPoint", "ST_SymDifference", "ST_Touches", "ST_Union", "ST_Within", "ST_X", "ST_Y", "STR_TO_DATE", "SUBDATE", "SUBTIME", "SUM", "SYSDATE", "TIME", "TIME_FORMAT", "TIME_TO_SEC", "TIMEDIFF", "TIMESTAMP", "TIMESTAMPADD", "TIMESTAMPDIFF", "TO_DAYS", "TO_SECONDS", "TRANSACTION_TIMESTAMP", "UNHEX", "UNIX_TIMESTAMP", "UTC_DATE", "UTC_TIME", "UTC_TIMESTAMP", "WEEKDAY", "WEEKOFYEAR", "YEARWEEK"];
2
+ export type SQLKnownFunction = typeof SQL_KNOWN_FUNCTIONS[number];
3
+ export type SQLFunction = SQLKnownFunction;
2
4
  export type SQLComparisonOperator = '=' | '!=' | '<' | '<=' | '>' | '>=' | 'IN' | 'NOT IN' | 'LIKE' | 'IS NULL' | 'IS NOT NULL' | 'BETWEEN' | 'LESS_THAN' | 'GREATER_THAN';
3
5
  export type JoinType = 'INNER' | 'LEFT_OUTER' | 'RIGHT_OUTER';
4
6
  export type OrderDirection = 'ASC' | 'DESC';
7
+ export type SQLExpression = string | number | boolean | null | SQLExpressionTuple;
8
+ export type SQLExpressionTuple = ['AS', SQLExpression, string] | ['DISTINCT', SQLExpression] | ['CALL', string, ...SQLExpression[]] | ['LIT', any] | ['PARAM', any] | ['SUBSELECT', Record<string, any>] | [SQLKnownFunction, ...SQLExpression[]];
9
+ export type OrderTerm = [SQLExpression, OrderDirection?];
@@ -2,7 +2,7 @@ import type { AxiosInstance, AxiosResponse } from "axios";
2
2
  import type { Pool } from "mysql2/promise";
3
3
  import { eFetchDependencies } from "./dynamicFetching";
4
4
  import { Modify } from "./modifyTypes";
5
- import { JoinType, OrderDirection, SQLComparisonOperator, SQLFunction } from "./mysqlTypes";
5
+ import { JoinType, OrderTerm, SQLComparisonOperator, SQLExpression } from "./mysqlTypes";
6
6
  import type { CarbonReact, iStateAdapter } from "@carbonorm/carbonreact";
7
7
  import type { OrmGenerics } from "./ormGenerics";
8
8
  import { restOrm } from "../api/restOrm";
@@ -41,7 +41,7 @@ export type SubSelect<T extends {
41
41
  };
42
42
  export type SelectField<T extends {
43
43
  [key: string]: any;
44
- } = any> = keyof T | [keyof T, 'AS', string] | [SQLFunction, keyof T] | [SQLFunction, keyof T, string] | SubSelect<T>;
44
+ } = any> = keyof T | SQLExpression | SubSelect<T>;
45
45
  export type WhereClause<T = any> = Partial<T> | LogicalGroup<T> | ComparisonClause<T>;
46
46
  export type LogicalGroup<T = any> = {
47
47
  [logicalGroup: string]: Array<WhereClause<T>>;
@@ -54,10 +54,10 @@ export type JoinClause<T = any> = {
54
54
  export type Join<T = any> = {
55
55
  [K in JoinType]?: JoinClause<T>;
56
56
  };
57
- export type Pagination<T = any> = {
57
+ export type Pagination = {
58
58
  PAGE?: number;
59
59
  LIMIT?: number | null;
60
- ORDER?: Partial<Record<keyof T, OrderDirection>>;
60
+ ORDER?: OrderTerm[];
61
61
  };
62
62
  export type RequestGetPutDeleteBody<T extends {
63
63
  [key: string]: any;
@@ -67,7 +67,7 @@ export type RequestGetPutDeleteBody<T extends {
67
67
  DELETE?: boolean;
68
68
  WHERE?: WhereClause<T>;
69
69
  JOIN?: Join<T>;
70
- PAGINATION?: Pagination<T>;
70
+ PAGINATION?: Pagination;
71
71
  };
72
72
  export type RequestPostBody<T extends {
73
73
  [key: string]: any;
@@ -80,6 +80,7 @@ export type iAPI<T extends {
80
80
  }> = T & {
81
81
  dataInsertMultipleRows?: T[];
82
82
  cacheResults?: boolean;
83
+ skipReactBootstrap?: boolean;
83
84
  fetchDependencies?: number | eFetchDependencies | Awaited<iGetC6RestResponse<any>>[];
84
85
  debug?: boolean;
85
86
  success?: string | ((r: AxiosResponse) => string | void);
@@ -105,6 +106,7 @@ export interface iCacheResponse<ResponseDataType = any> {
105
106
  export interface iCacheAPI<ResponseDataType = any> {
106
107
  requestArgumentsSerialized: string;
107
108
  request: Promise<iCacheResponse<ResponseDataType>>;
109
+ allowListStatus?: "allowed" | "denied" | "not verified";
108
110
  response?: iCacheResponse<ResponseDataType> & {
109
111
  __carbonTiming?: {
110
112
  start: number;
@@ -1,10 +1,11 @@
1
1
  import type { iCacheAPI, iCacheResponse } from "../types/ormInterfaces";
2
2
  import { LogContext } from "./logLevel";
3
+ import { SqlAllowListStatus } from "./logSql";
3
4
  export declare const apiRequestCache: Map<string, iCacheAPI<any>>;
4
5
  export declare const userCustomClearCache: (() => void)[];
5
6
  export declare function clearCache(props?: {
6
7
  ignoreWarning?: boolean;
7
8
  }): void;
8
- export declare function checkCache<ResponseDataType = any>(method: string, tableName: string | string[], requestData: any, logContext: LogContext): Promise<iCacheResponse<ResponseDataType>> | false;
9
+ export declare function checkCache<ResponseDataType = any>(method: string, tableName: string | string[], requestData: any, logContext?: LogContext, allowListStatus?: SqlAllowListStatus): Promise<iCacheResponse<ResponseDataType>> | false;
9
10
  export declare function setCache<ResponseDataType = any>(method: string, tableName: string | string[], requestData: any, cacheEntry: iCacheAPI<ResponseDataType>): void;
10
- export declare function evictCacheEntry(method: string, tableName: string | string[], requestData: any, logContext?: LogContext): boolean;
11
+ export declare function evictCacheEntry(method: string, tableName: string | string[], requestData: any, logContext?: LogContext, allowListStatus?: SqlAllowListStatus): boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@carbonorm/carbonnode",
3
- "version": "6.0.19",
3
+ "version": "6.1.0",
4
4
  "browser": "dist/index.umd.js",
5
5
  "repository": {
6
6
  "type": "git",
@@ -20,7 +20,7 @@
20
20
  "hooksPath": ".githooks"
21
21
  },
22
22
  "dependencies": {
23
- "@carbonorm/carbonreact": "^6.0.3",
23
+ "@carbonorm/carbonreact": "^6.0.4",
24
24
  "buffer": "^6.0.3",
25
25
  "geojson": "^0.5.0",
26
26
  "handlebars": "^4.7.8",
@@ -101,7 +101,7 @@ function buildScalarValue(meta: any, columnName: string, seedRow: any) {
101
101
  'geometrycollection',
102
102
  ];
103
103
  if (geometryTypes.some((type) => mysqlType.includes(type))) {
104
- return "ST_GeomFromText('POINT(0 0)')";
104
+ return [C6.ST_GEOMFROMTEXT, [C6.LIT, 'POINT(0 0)']];
105
105
  }
106
106
 
107
107
  if (mysqlType === 'json') return {};
@@ -236,7 +236,7 @@ function buildUpdatedValue(meta: any, columnName: string, currentValue: any) {
236
236
 
237
237
  if (mysqlType === 'year') return new Date().getFullYear();
238
238
  if (geometryTypes.some((type) => mysqlType.includes(type))) {
239
- return "ST_GeomFromText('POINT(1 1)')";
239
+ return [C6.ST_GEOMFROMTEXT, [C6.LIT, 'POINT(1 1)']];
240
240
  }
241
241
  if (mysqlType === 'json') return { updated: true };
242
242
 
@@ -525,7 +525,7 @@ describe('sakila-db generated C6 bindings', () => {
525
525
  restBinding.Get({
526
526
  [C6.PAGINATION]: {
527
527
  [C6.LIMIT]: 1,
528
- [C6.ORDER]: { [primaryFull]: 'DESC' },
528
+ [C6.ORDER]: [[primaryFull, C6.DESC]],
529
529
  },
530
530
  cacheResults: false,
531
531
  } as any)
@@ -550,7 +550,7 @@ describe('sakila-db generated C6 bindings', () => {
550
550
  restBinding.Get({
551
551
  [C6.PAGINATION]: {
552
552
  [C6.LIMIT]: 1,
553
- [C6.ORDER]: { [primaryFull]: 'DESC' },
553
+ [C6.ORDER]: [[primaryFull, C6.DESC]],
554
554
  },
555
555
  cacheResults: false,
556
556
  } as any)
@@ -79,14 +79,42 @@ describe("cacheManager with map storage", () => {
79
79
  },
80
80
  },
81
81
  } as any,
82
+ allowListStatus: "allowed",
82
83
  final: true,
83
84
  });
84
85
 
85
86
  expect(evictCacheEntry("GET", "table", requestData, { verbose: true })).toBe(true);
86
87
  expect(logSpy).toHaveBeenCalledTimes(1);
87
88
  expect(String(logSpy.mock.calls[0]?.[0] ?? "")).toContain("[CACHE EVICTED]");
89
+ expect(String(logSpy.mock.calls[0]?.[0] ?? "")).toContain("[VERIFIED]");
88
90
  expect(String(logSpy.mock.calls[0]?.[0] ?? "")).toContain("[SELECT]");
89
91
 
90
92
  logSpy.mockRestore();
91
93
  });
94
+
95
+ it("logs verified for cache hits when allowlist is provided", () => {
96
+ const logSpy = vi.spyOn(console, "log").mockImplementation(() => undefined);
97
+ const mockRequest = Promise.resolve({ data: { rest: [] } }) as AxiosPromise;
98
+
99
+ setCache("GET", "table", requestData, {
100
+ requestArgumentsSerialized: "serialized",
101
+ request: mockRequest,
102
+ response: {
103
+ data: {
104
+ sql: {
105
+ sql: "SELECT * FROM table WHERE id = 1",
106
+ },
107
+ },
108
+ } as any,
109
+ final: true,
110
+ });
111
+
112
+ const cached = checkCache("GET", "table", requestData, { verbose: true }, "allowed");
113
+ expect(cached).toBe(mockRequest);
114
+ expect(logSpy).toHaveBeenCalledTimes(1);
115
+ expect(String(logSpy.mock.calls[0]?.[0] ?? "")).toContain("[CACHE HIT]");
116
+ expect(String(logSpy.mock.calls[0]?.[0] ?? "")).toContain("[VERIFIED]");
117
+
118
+ logSpy.mockRestore();
119
+ });
92
120
  });
@@ -3,7 +3,7 @@ import axios from "axios";
3
3
  import { AddressInfo } from "net";
4
4
  import {describe, it, expect, beforeAll, afterAll} from "vitest";
5
5
  import { restOrm } from "@carbonorm/carbonnode";
6
- import {Actor, C6, Film_Actor} from "./sakila-db/C6.js";
6
+ import {Actor, C6, Film_Actor, TABLES} from "./sakila-db/C6";
7
7
  import {C6C} from "../constants/C6Constants";
8
8
  import createTestServer from "./fixtures/createTestServer";
9
9
 
@@ -13,14 +13,14 @@ let restURL: string;
13
13
  let axiosClient: ReturnType<typeof axios.create>;
14
14
  const actorHttp = restOrm<any>(() => ({
15
15
  C6,
16
- restModel: C6.TABLES.actor,
16
+ restModel: TABLES.actor,
17
17
  restURL,
18
18
  axios: axiosClient,
19
19
  verbose: false,
20
20
  }));
21
21
  const filmActorHttp = restOrm<any>(() => ({
22
22
  C6,
23
- restModel: C6.TABLES.film_actor,
23
+ restModel: TABLES.film_actor,
24
24
  restURL,
25
25
  axios: axiosClient,
26
26
  verbose: false,
@@ -100,30 +100,33 @@ describe("ExpressHandler e2e", () => {
100
100
  } as any);
101
101
 
102
102
  let data = await actorRequest("GET", {
103
- [C6C.WHERE]: { [Actor.FIRST_NAME]: first_name, [Actor.LAST_NAME]: last_name },
103
+ [C6C.WHERE]: {
104
+ [Actor.FIRST_NAME]: [C6C.EQUAL, [C6C.LIT, first_name]],
105
+ [Actor.LAST_NAME]: [C6C.EQUAL, [C6C.LIT, last_name]],
106
+ },
104
107
  [C6C.PAGINATION]: { [C6C.LIMIT]: 1 },
105
108
  } as any);
106
109
 
107
110
  expect(data?.rest).toHaveLength(1);
108
- const testId = data?.rest[0].actor_id;
111
+ const testId = Number(data?.rest[0].actor_id);
109
112
 
110
113
  await actorRequest("PUT", {
111
- [C6C.WHERE]: { [Actor.ACTOR_ID]: testId },
114
+ [C6C.WHERE]: { [Actor.ACTOR_ID]: [C6C.EQUAL, [C6C.LIT, Number(testId)]] },
112
115
  [C6C.UPDATE]: { first_name: "Updated" },
113
116
  } as any);
114
117
 
115
118
  data = await actorRequest("GET", {
116
- [C6C.WHERE]: { [Actor.ACTOR_ID]: testId },
119
+ [C6C.WHERE]: { [Actor.ACTOR_ID]: [C6C.EQUAL, [C6C.LIT, Number(testId)]] },
117
120
  } as any);
118
121
  expect(data?.rest).toHaveLength(1);
119
122
  expect(data?.rest[0].first_name).toBe("Updated");
120
123
 
121
124
  await actorRequest("DELETE", {
122
- [C6C.WHERE]: { [Actor.ACTOR_ID]: testId },
125
+ [C6C.WHERE]: { [Actor.ACTOR_ID]: [C6C.EQUAL, [C6C.LIT, Number(testId)]] },
123
126
  [C6C.DELETE]: true,
124
127
  } as any);
125
128
  data = await actorRequest("GET", {
126
- [C6C.WHERE]: { [Actor.ACTOR_ID]: testId },
129
+ [C6C.WHERE]: { [Actor.ACTOR_ID]: [C6C.EQUAL, [C6C.LIT, Number(testId)]] },
127
130
  cacheResults: false,
128
131
  } as any);
129
132
  expect(Array.isArray(data?.rest)).toBe(true);
@@ -142,20 +145,23 @@ describe("ExpressHandler e2e", () => {
142
145
  const payload = { greeting: "hello", flags: [1, true] };
143
146
 
144
147
  let data = await actorRequest("GET", {
145
- [C6C.WHERE]: { [Actor.FIRST_NAME]: first_name, [Actor.LAST_NAME]: last_name },
148
+ [C6C.WHERE]: {
149
+ [Actor.FIRST_NAME]: [C6C.EQUAL, [C6C.LIT, first_name]],
150
+ [Actor.LAST_NAME]: [C6C.EQUAL, [C6C.LIT, last_name]],
151
+ },
146
152
  [C6C.PAGINATION]: { [C6C.LIMIT]: 1 },
147
153
  } as any);
148
154
 
149
- const actorId = data?.rest?.[0]?.actor_id;
155
+ const actorId = Number(data?.rest?.[0]?.actor_id);
150
156
  expect(actorId).toBeTruthy();
151
157
 
152
158
  await actorRequest("PUT", {
153
- [C6C.WHERE]: { [Actor.ACTOR_ID]: actorId },
159
+ [C6C.WHERE]: { [Actor.ACTOR_ID]: [C6C.EQUAL, [C6C.LIT, Number(actorId)]] },
154
160
  [C6C.UPDATE]: { first_name: payload },
155
161
  } as any);
156
162
 
157
163
  data = await actorRequest("GET", {
158
- [C6C.WHERE]: { [Actor.ACTOR_ID]: actorId },
164
+ [C6C.WHERE]: { [Actor.ACTOR_ID]: [C6C.EQUAL, [C6C.LIT, Number(actorId)]] },
159
165
  } as any);
160
166
 
161
167
  expect(data?.rest?.[0]?.first_name).toBe(JSON.stringify(payload));
@@ -171,11 +177,14 @@ describe("ExpressHandler e2e", () => {
171
177
  } as any);
172
178
 
173
179
  const data = await actorRequest("GET", {
174
- [C6C.WHERE]: { [Actor.FIRST_NAME]: first_name, [Actor.LAST_NAME]: last_name },
180
+ [C6C.WHERE]: {
181
+ [Actor.FIRST_NAME]: [C6C.EQUAL, [C6C.LIT, first_name]],
182
+ [Actor.LAST_NAME]: [C6C.EQUAL, [C6C.LIT, last_name]],
183
+ },
175
184
  [C6C.PAGINATION]: { [C6C.LIMIT]: 1 },
176
185
  } as any);
177
186
 
178
- const actorId = data?.rest?.[0]?.actor_id;
187
+ const actorId = Number(data?.rest?.[0]?.actor_id);
179
188
  expect(actorId).toBeTruthy();
180
189
 
181
190
  const operatorLike = { [C6C.GREATER_THAN]: "oops" } as any;
@@ -183,12 +192,12 @@ describe("ExpressHandler e2e", () => {
183
192
  try {
184
193
  const actorSql = restOrm<any>(() => ({
185
194
  C6,
186
- restModel: C6.TABLES.actor,
195
+ restModel: TABLES.actor,
187
196
  mysqlPool: pool,
188
197
  verbose: false,
189
198
  }));
190
199
  await actorSql.Put({
191
- [C6C.WHERE]: { [Actor.ACTOR_ID]: actorId },
200
+ [C6C.WHERE]: { [Actor.ACTOR_ID]: [C6C.EQUAL, [C6C.LIT, Number(actorId)]] },
192
201
  [C6C.UPDATE]: { first_name: operatorLike },
193
202
  } as any);
194
203
  throw new Error('Expected PUT to reject for operator-like payload.');
@@ -2,8 +2,8 @@ import mysql from "mysql2/promise";
2
2
  import axios from "axios";
3
3
  import { AddressInfo } from "net";
4
4
  import { describe, it, expect, beforeAll, afterAll, vi } from "vitest";
5
- import { restOrm } from "@carbonorm/carbonnode";
6
- import { Actor, C6 } from "./sakila-db/C6.js";
5
+ import {iGetC6RestResponse, restOrm } from "@carbonorm/carbonnode";
6
+ import {Actor, C6, iActor, TABLES} from "./sakila-db/C6";
7
7
  import { C6C } from "../constants/C6Constants";
8
8
  import createTestServer from "./fixtures/createTestServer";
9
9
 
@@ -13,7 +13,7 @@ let restURL: string;
13
13
  let axiosClient: ReturnType<typeof axios.create>;
14
14
  const actorHttp = restOrm<any>(() => ({
15
15
  C6,
16
- restModel: C6.TABLES.actor,
16
+ restModel: TABLES.actor,
17
17
  restURL,
18
18
  axios: axiosClient,
19
19
  verbose: false,
@@ -76,23 +76,26 @@ describe("HttpExecutor singular e2e", () => {
76
76
  step = "GET-complex";
77
77
  // Fetch inserted id using complex query
78
78
  let data = await actorRequest("GET", {
79
- [C6C.WHERE]: { [Actor.FIRST_NAME]: first_name, [Actor.LAST_NAME]: last_name },
79
+ [C6C.WHERE]: {
80
+ [Actor.FIRST_NAME]: [C6C.EQUAL, [C6C.LIT, first_name]],
81
+ [Actor.LAST_NAME]: [C6C.EQUAL, [C6C.LIT, last_name]],
82
+ },
80
83
  [C6C.PAGINATION]: { [C6C.LIMIT]: 1 },
81
84
  });
82
85
 
83
86
  expect(data.rest).toHaveLength(1);
84
- const testId = data.rest[0].actor_id;
87
+ const testId = Number(data.rest[0].actor_id);
85
88
 
86
89
  step = "GET-singular";
87
90
  // GET singular
88
- data = await actorRequest("GET", { actor_id: testId } as any);
91
+ data = await actorRequest("GET", { actor_id: Number(testId) } as any);
89
92
  expect(data.rest).toHaveLength(1);
90
93
  expect(data.rest[0].actor_id).toBe(testId);
91
94
 
92
95
  step = "PUT-singular";
93
96
  // PUT singular
94
- await actorRequest("PUT", { actor_id: testId, first_name: "Updated" } as any);
95
- data = await actorRequest("GET", { actor_id: testId, cacheResults: false } as any);
97
+ await actorRequest("PUT", { actor_id: Number(testId), first_name: "Updated" } as any);
98
+ data = await actorRequest("GET", { actor_id: Number(testId), cacheResults: false } as any);
96
99
  expect(data.rest).toHaveLength(1);
97
100
  expect(data.rest[0].first_name).toBe("Updated");
98
101
 
@@ -105,7 +108,7 @@ describe("HttpExecutor singular e2e", () => {
105
108
  } as any;
106
109
  const actorHttpWithReact = restOrm<any>(() => ({
107
110
  C6,
108
- restModel: C6.TABLES.actor,
111
+ restModel: TABLES.actor,
109
112
  restURL,
110
113
  axios: axiosClient,
111
114
  verbose: false,
@@ -123,8 +126,8 @@ describe("HttpExecutor singular e2e", () => {
123
126
 
124
127
  step = "DELETE-singular";
125
128
  // DELETE singular
126
- await actorRequest("DELETE", { actor_id: testId } as any);
127
- data = await actorRequest("GET", { actor_id: testId, cacheResults: false } as any);
129
+ await actorRequest("DELETE", { actor_id: Number(testId) } as any);
130
+ data = await actorRequest("GET", { actor_id: Number(testId), cacheResults: false } as any);
128
131
  expect(Array.isArray(data.rest)).toBe(true);
129
132
  expect(data.rest.length).toBe(0);
130
133
  } catch (error: any) {
@@ -133,9 +136,9 @@ describe("HttpExecutor singular e2e", () => {
133
136
  });
134
137
 
135
138
  it("exposes next when pagination continues", async () => {
136
- const data = await actorRequest("GET", {
139
+ const data: iGetC6RestResponse<iActor, {}> = await actorRequest("GET", {
137
140
  [C6C.PAGINATION]: { [C6C.LIMIT]: 2 },
138
- } as any);
141
+ });
139
142
 
140
143
  expect(Array.isArray(data.rest)).toBe(true);
141
144
  expect(data.rest).toHaveLength(2);
@@ -148,7 +151,7 @@ describe("HttpExecutor singular e2e", () => {
148
151
  });
149
152
 
150
153
  it("exposes limit 1 does not expose next", async () => {
151
- const data = await actorRequest("GET", {
154
+ const data: iGetC6RestResponse<iActor, {}> = await actorRequest("GET", {
152
155
  [C6C.PAGINATION]: { [C6C.LIMIT]: 1 },
153
156
  } as any);
154
157
 
@@ -157,4 +160,40 @@ describe("HttpExecutor singular e2e", () => {
157
160
  expect(typeof data.next).toBe("undefined");
158
161
 
159
162
  });
163
+
164
+ it("skips reactBootstrap state sync when skipReactBootstrap is true", async () => {
165
+ const updateStub = vi.fn();
166
+ const reactBootstrap = {
167
+ updateRestfulObjectArrays: updateStub,
168
+ deleteRestfulObjectArrays: vi.fn(),
169
+ } as any;
170
+
171
+ const actorHttpWithReact = restOrm<any>(() => ({
172
+ C6,
173
+ restModel: TABLES.actor,
174
+ restURL,
175
+ axios: axiosClient,
176
+ verbose: false,
177
+ reactBootstrap,
178
+ }));
179
+
180
+ const skipped = await actorHttpWithReact.Get({
181
+ [C6C.PAGINATION]: { [C6C.LIMIT]: 1 },
182
+ skipReactBootstrap: true,
183
+ cacheResults: false,
184
+ } as any);
185
+
186
+ expect(Array.isArray(skipped.rest)).toBe(true);
187
+ expect(skipped.rest).toHaveLength(1);
188
+ expect(updateStub).not.toHaveBeenCalled();
189
+
190
+ const normal = await actorHttpWithReact.Get({
191
+ [C6C.PAGINATION]: { [C6C.LIMIT]: 1 },
192
+ cacheResults: false,
193
+ } as any);
194
+
195
+ expect(Array.isArray(normal.rest)).toBe(true);
196
+ expect(normal.rest).toHaveLength(1);
197
+ expect(updateStub).toHaveBeenCalledTimes(1);
198
+ });
160
199
  });
@@ -33,13 +33,15 @@ function makeModel(table: string, pkShorts: string[], extraCols: string[] = []):
33
33
  } as any;
34
34
  }
35
35
 
36
+ const litEq = (value: any) => [C6C.EQUAL, [C6C.LIT, value]];
37
+
36
38
  describe('normalizeSingularRequest', () => {
37
39
  it('converts GET singular T into WHERE by PK', () => {
38
40
  const model = makeModel('actor', ['actor_id'], ['first_name']);
39
41
  const req = { actor_id: 5 } as any;
40
42
  const out = normalizeSingularRequest('GET', req, model);
41
43
  expect(out).toHaveProperty(C6C.WHERE);
42
- expect((out as any)[C6C.WHERE]).toEqual({ 'actor.actor_id': 5 });
44
+ expect((out as any)[C6C.WHERE]).toEqual({ 'actor.actor_id': litEq(5) });
43
45
  });
44
46
 
45
47
  it('converts DELETE singular T into DELETE:true and WHERE by PK', () => {
@@ -47,14 +49,14 @@ describe('normalizeSingularRequest', () => {
47
49
  const req = { actor_id: 7 } as any;
48
50
  const out = normalizeSingularRequest('DELETE', req, model);
49
51
  expect((out as any)[C6C.DELETE]).toBe(true);
50
- expect((out as any)[C6C.WHERE]).toEqual({ 'actor.actor_id': 7 });
52
+ expect((out as any)[C6C.WHERE]).toEqual({ 'actor.actor_id': litEq(7) });
51
53
  });
52
54
 
53
55
  it('converts PUT singular T into UPDATE (non-PK fields) and WHERE by PK', () => {
54
56
  const model = makeModel('actor', ['actor_id'], ['first_name']);
55
57
  const req = { actor_id: 9, first_name: 'NEW' } as any;
56
58
  const out = normalizeSingularRequest('PUT', req, model);
57
- expect((out as any)[C6C.WHERE]).toEqual({ 'actor.actor_id': 9 });
59
+ expect((out as any)[C6C.WHERE]).toEqual({ 'actor.actor_id': litEq(9) });
58
60
  expect((out as any)[C6C.UPDATE]).toEqual({ first_name: 'NEW' });
59
61
  });
60
62
 
@@ -76,7 +78,7 @@ describe('normalizeSingularRequest', () => {
76
78
  const model = makeModel('link', ['from_id', 'to_id']);
77
79
  const ok = { from_id: 1, to_id: 2 } as any;
78
80
  const out = normalizeSingularRequest('GET', ok, model);
79
- expect((out as any)[C6C.WHERE]).toEqual({ 'link.from_id': 1, 'link.to_id': 2 });
81
+ expect((out as any)[C6C.WHERE]).toEqual({ 'link.from_id': litEq(1), 'link.to_id': litEq(2) });
80
82
 
81
83
  const missing = { from_id: 1 } as any;
82
84
  expect(() => normalizeSingularRequest('DELETE', missing, model)).toThrow(/Missing: \[to_id\]/);
@@ -102,11 +104,27 @@ describe('normalizeSingularRequest', () => {
102
104
  const out = normalizeSingularRequest('GET', req, model);
103
105
  expect(out).toBe(req);
104
106
  });
107
+
108
+ it('preserves skipReactBootstrap metadata across normalization', () => {
109
+ const model = makeModel('actor', ['actor_id'], ['first_name']);
110
+ const req = {
111
+ actor_id: 1,
112
+ first_name: 'S',
113
+ cacheResults: false,
114
+ skipReactBootstrap: true,
115
+ } as any;
116
+
117
+ const out = normalizeSingularRequest('PUT', req, model) as any;
118
+ expect(out.skipReactBootstrap).toBe(true);
119
+ expect(out.cacheResults).toBe(false);
120
+ expect(out[C6C.UPDATE]).toEqual({ first_name: 'S' });
121
+ });
122
+
105
123
  it('accepts fully-qualified PK and maps WHERE/UPDATE to short keys', () => {
106
124
  const model = makeModel('actor', ['actor_id'], ['first_name']);
107
125
  const req = { 'actor.actor_id': 12, 'actor.first_name': 'FN' } as any;
108
126
  const out = normalizeSingularRequest('PUT', req, model) as any;
109
- expect(out[C6C.WHERE]).toEqual({ 'actor.actor_id': 12 });
127
+ expect(out[C6C.WHERE]).toEqual({ 'actor.actor_id': litEq(12) });
110
128
  expect(out[C6C.UPDATE]).toEqual({ first_name: 'FN' });
111
129
  });
112
130
 
@@ -114,7 +132,7 @@ describe('normalizeSingularRequest', () => {
114
132
  const model = makeModel('actor', ['actor_id'], ['first_name']);
115
133
  const req = { 'actor.actor_id': 44, first_name: 'Mix' } as any;
116
134
  const out = normalizeSingularRequest('PUT', req, model) as any;
117
- expect(out[C6C.WHERE]).toEqual({ 'actor.actor_id': 44 });
135
+ expect(out[C6C.WHERE]).toEqual({ 'actor.actor_id': litEq(44) });
118
136
  expect(out[C6C.UPDATE]).toEqual({ first_name: 'Mix' });
119
137
  });
120
138
 
@@ -123,13 +141,13 @@ describe('normalizeSingularRequest', () => {
123
141
  const req = { 'actor.actor_id': 77 } as any;
124
142
  const out = normalizeSingularRequest('DELETE', req, model) as any;
125
143
  expect(out[C6C.DELETE]).toBe(true);
126
- expect(out[C6C.WHERE]).toEqual({ 'actor.actor_id': 77 });
144
+ expect(out[C6C.WHERE]).toEqual({ 'actor.actor_id': litEq(77) });
127
145
  });
128
146
 
129
147
  it('supports composite PKs with fully-qualified keys', () => {
130
148
  const model = makeModel('link', ['from_id', 'to_id']);
131
149
  const req = { 'link.from_id': 1, 'link.to_id': 2, 'link.label': 'L' } as any;
132
150
  const out = normalizeSingularRequest('PUT', req, model) as any;
133
- expect(out[C6C.WHERE]).toEqual({ 'link.from_id': 1, 'link.to_id': 2 });
151
+ expect(out[C6C.WHERE]).toEqual({ 'link.from_id': litEq(1), 'link.to_id': litEq(2) });
134
152
  });
135
153
  });
@@ -1342,7 +1342,7 @@ export const TABLES = {
1342
1342
  };
1343
1343
  export const C6 = {
1344
1344
  ...C6Constants,
1345
- C6VERSION: '6.0.19',
1345
+ C6VERSION: '6.1.0',
1346
1346
  IMPORT: async (tableName) => {
1347
1347
  tableName = tableName.toLowerCase();
1348
1348
  // if tableName is not a key in the TABLES object then throw an error