@carbonorm/carbonnode 6.0.20 → 6.1.1

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 (90) hide show
  1. package/README.md +521 -259
  2. package/dist/constants/C6Constants.d.ts +342 -338
  3. package/dist/executors/SqlExecutor.d.ts +1 -0
  4. package/dist/index.cjs.js +746 -290
  5. package/dist/index.cjs.js.map +1 -1
  6. package/dist/index.d.ts +1 -0
  7. package/dist/index.esm.js +737 -291
  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/orm/utils/sqlUtils.d.ts +1 -0
  15. package/dist/types/mysqlTypes.d.ts +6 -1
  16. package/dist/types/ormInterfaces.d.ts +7 -5
  17. package/dist/utils/sqlAllowList.d.ts +5 -3
  18. package/package.json +2 -2
  19. package/scripts/assets/handlebars/C6.test.ts.handlebars +4 -4
  20. package/src/__tests__/expressServer.e2e.test.ts +26 -17
  21. package/src/__tests__/fixtures/c6.fixture.ts +33 -0
  22. package/src/__tests__/httpExecutorSingular.e2e.test.ts +53 -14
  23. package/src/__tests__/normalizeSingularRequest.test.ts +26 -8
  24. package/src/__tests__/sakila-db/C6.js +1 -1
  25. package/src/__tests__/sakila-db/C6.mysqldump.json +1 -1
  26. package/src/__tests__/sakila-db/C6.mysqldump.sql +1 -1
  27. package/src/__tests__/sakila-db/C6.sqlAllowList.json +1 -1
  28. package/src/__tests__/sakila-db/C6.test.ts +4 -4
  29. package/src/__tests__/sakila-db/C6.ts +1 -1
  30. package/src/__tests__/sakila-db/sqlResponses/C6.actor.post.json +6 -6
  31. package/src/__tests__/sakila-db/sqlResponses/C6.actor.post.latest.json +3 -3
  32. package/src/__tests__/sakila-db/sqlResponses/C6.actor.put.json +1 -1
  33. package/src/__tests__/sakila-db/sqlResponses/C6.actor.put.lookup.json +3 -3
  34. package/src/__tests__/sakila-db/sqlResponses/C6.address.post.json +19 -12
  35. package/src/__tests__/sakila-db/sqlResponses/C6.address.post.latest.json +5 -5
  36. package/src/__tests__/sakila-db/sqlResponses/C6.address.put.json +1 -1
  37. package/src/__tests__/sakila-db/sqlResponses/C6.address.put.lookup.json +5 -5
  38. package/src/__tests__/sakila-db/sqlResponses/C6.category.post.json +4 -4
  39. package/src/__tests__/sakila-db/sqlResponses/C6.category.post.latest.json +2 -2
  40. package/src/__tests__/sakila-db/sqlResponses/C6.category.put.json +1 -1
  41. package/src/__tests__/sakila-db/sqlResponses/C6.category.put.lookup.json +2 -2
  42. package/src/__tests__/sakila-db/sqlResponses/C6.city.post.json +4 -4
  43. package/src/__tests__/sakila-db/sqlResponses/C6.city.post.latest.json +2 -2
  44. package/src/__tests__/sakila-db/sqlResponses/C6.city.put.json +1 -1
  45. package/src/__tests__/sakila-db/sqlResponses/C6.city.put.lookup.json +2 -2
  46. package/src/__tests__/sakila-db/sqlResponses/C6.country.post.json +4 -4
  47. package/src/__tests__/sakila-db/sqlResponses/C6.country.post.latest.json +2 -2
  48. package/src/__tests__/sakila-db/sqlResponses/C6.country.put.json +1 -1
  49. package/src/__tests__/sakila-db/sqlResponses/C6.country.put.lookup.json +2 -2
  50. package/src/__tests__/sakila-db/sqlResponses/C6.customer.post.json +10 -10
  51. package/src/__tests__/sakila-db/sqlResponses/C6.customer.post.latest.json +5 -5
  52. package/src/__tests__/sakila-db/sqlResponses/C6.customer.put.json +1 -1
  53. package/src/__tests__/sakila-db/sqlResponses/C6.customer.put.lookup.json +5 -5
  54. package/src/__tests__/sakila-db/sqlResponses/C6.film.post.json +4 -4
  55. package/src/__tests__/sakila-db/sqlResponses/C6.film.post.latest.json +2 -2
  56. package/src/__tests__/sakila-db/sqlResponses/C6.film.put.json +1 -1
  57. package/src/__tests__/sakila-db/sqlResponses/C6.film.put.lookup.json +2 -2
  58. package/src/__tests__/sakila-db/sqlResponses/C6.inventory.post.json +2 -2
  59. package/src/__tests__/sakila-db/sqlResponses/C6.inventory.post.latest.json +1 -1
  60. package/src/__tests__/sakila-db/sqlResponses/C6.inventory.put.json +1 -1
  61. package/src/__tests__/sakila-db/sqlResponses/C6.inventory.put.lookup.json +1 -1
  62. package/src/__tests__/sakila-db/sqlResponses/C6.language.post.json +4 -4
  63. package/src/__tests__/sakila-db/sqlResponses/C6.language.post.latest.json +2 -2
  64. package/src/__tests__/sakila-db/sqlResponses/C6.language.put.json +1 -1
  65. package/src/__tests__/sakila-db/sqlResponses/C6.language.put.lookup.json +2 -2
  66. package/src/__tests__/sakila-db/sqlResponses/C6.payment.post.json +4 -4
  67. package/src/__tests__/sakila-db/sqlResponses/C6.payment.post.latest.json +2 -2
  68. package/src/__tests__/sakila-db/sqlResponses/C6.payment.put.lookup.json +2 -2
  69. package/src/__tests__/sakila-db/sqlResponses/C6.rental.post.json +6 -6
  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__/sqlAllowList.test.ts +56 -1
  74. package/src/__tests__/sqlBuilders.complex.test.ts +62 -74
  75. package/src/__tests__/sqlBuilders.expressions.test.ts +58 -30
  76. package/src/__tests__/sqlBuilders.test.ts +106 -5
  77. package/src/constants/C6Constants.ts +3 -1
  78. package/src/executors/HttpExecutor.ts +2 -1
  79. package/src/executors/SqlExecutor.ts +29 -4
  80. package/src/index.ts +1 -0
  81. package/src/orm/builders/AggregateBuilder.ts +67 -106
  82. package/src/orm/builders/ConditionBuilder.ts +72 -103
  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/orm/utils/sqlUtils.ts +172 -4
  87. package/src/types/mysqlTypes.ts +130 -9
  88. package/src/types/ormInterfaces.ts +7 -7
  89. package/src/utils/normalizeSingularRequest.ts +11 -4
  90. package/src/utils/sqlAllowList.ts +44 -11
package/README.md CHANGED
@@ -9,362 +9,624 @@
9
9
 
10
10
  # CarbonNode
11
11
 
12
- CarbonNode is a part of the CarbonORM series. It is a NodeJS MySQL ORM that can run independently in the backend or paired with
13
- CarbonReact for 1=1 syntax. Note the CarbonNode + CarbonReact experience is unmatched in interoperability.
12
+ CarbonNode is a typed MySQL ORM runtime plus code generator for REST bindings.
13
+
14
+ It is built for:
15
+
16
+ - full-stack teams that want one query shape across frontend and backend
17
+ - explicit column references and safer query composition
18
+ - JSON-serializable SQL expression payloads with predictable parsing
19
+ - generated TypeScript bindings from live MySQL schema
20
+
21
+ It can run in two modes:
22
+
23
+ - SQL executor (`mysqlPool` set): executes SQL directly in Node
24
+ - HTTP executor (no pool, axios set): sends requests to a CarbonNode REST endpoint
25
+
26
+ ## Table of Contents
27
+
28
+ 1. [Install](#install)
29
+ 2. [Quickstart](#quickstart)
30
+ 3. [Execution Model](#execution-model)
31
+ 4. [C6 + CarbonReact State Management](#c6--carbonreact-state-management)
32
+ 5. [Canonical SQL Expression Grammar (6.1.0+)](#canonical-sql-expression-grammar-610)
33
+ 6. [Clause-by-Clause Usage](#clause-by-clause-usage)
34
+ 7. [Helper Builders](#helper-builders)
35
+ 8. [Singular vs Complex Requests](#singular-vs-complex-requests)
36
+ 9. [HTTP Query-String Pitfalls](#http-query-string-pitfalls)
37
+ 10. [Generator Output](#generator-output)
38
+ 11. [SQL Allowlist](#sql-allowlist)
39
+ 12. [Lifecycle Hooks and Websocket Broadcast](#lifecycle-hooks-and-websocket-broadcast)
40
+ 13. [Testing](#testing)
41
+ 14. [Migration Notes (6.0 -> 6.1)](#migration-notes-60---61)
42
+ 15. [AI Interpretation Contract](#ai-interpretation-contract)
43
+ 16. [Git Hooks](#git-hooks)
44
+ 17. [Support](#support)
45
+
46
+ ## Install
14
47
 
15
- # Purpose
48
+ ```bash
49
+ npm install @carbonorm/carbonnode
50
+ ```
16
51
 
17
- CarbonNode is designed to generate RESTful API bindings for a MySQL database. The generated code provides a simple and
18
- consistent interface for performing CRUD operations on the database tables. The goal is to reduce the amount of boilerplate
19
- code needed to interact with the database and to provide a more efficient and reliable way to work with MySQL data in a NodeJS
20
- environment. The major goals:
21
- - Allow a 1-1 interoperability when querying data from the frontend to the backend.
22
- - Language based Objects/Arrays for representing and modifying queries to eliminate string manipulation operations.
23
- - Explicit column references to allow for easier refactoring and code completion in IDEs.
24
- - Selecting a dead column will result in a compile time error instead of a runtime error.
25
- - TypeScript types generated for each table in the database.
26
- - Lifecycle hooks for each CRUD operation to allow for custom logic to be executed before and after the operation.
27
- - Validation of data types and formats before executing CRUD operations to ensure data integrity.
52
+ Peer dependencies:
28
53
 
29
- It's easier to scale your middleware than your database.
30
- CarbonNode aims to capture issues before they reach your database.
54
+ - `mysql2` for SQL executor mode
55
+ - `axios` for HTTP executor mode
56
+ - `express` only if hosting the REST route in your app
31
57
 
58
+ ## Quickstart
32
59
 
33
- ## Alpha Release
60
+ ### 1) Generate bindings
34
61
 
35
- This is an alpha release. The code is not yet ready for production. We are looking for feedback on the API and any bugs.
36
- Some features are not yet implemented. We are working on the documentation and will be adding more examples. Please
37
- check out [any issue](https://github.com/CarbonORM/CarbonWordPress/issues) we have open and feel free to contribute.
62
+ Generate `C6.ts` + `C6.test.ts` + dump artifacts into an output directory:
38
63
 
39
- ## Installation
64
+ ```bash
65
+ npx generateRestBindings \
66
+ --user root \
67
+ --pass password \
68
+ --host 127.0.0.1 \
69
+ --port 3306 \
70
+ --dbname sakila \
71
+ --prefix "" \
72
+ --output ./shared/rest
73
+ ```
40
74
 
41
- CarbonNode is available on [NPM](https://www.npmjs.com/). You'll need to have [NodeJS](https://nodejs.org/en/) installed
42
- which comes prepackaged with npm (node package manager).
75
+ Import from generated bindings:
43
76
 
44
- ```bash
45
- npm install @carbonorm/carbonnode
77
+ ```ts
78
+ import { C6, GLOBAL_REST_PARAMETERS, Actor } from "./shared/rest/C6";
46
79
  ```
47
80
 
48
- ## Generate Models
81
+ ### 2) Configure runtime
49
82
 
50
- The generator produces a single `C6.ts` file containing all tables, types, and REST bindings. Keep this file in version
51
- control and share it between server and client. All arguments are optional; the example below shows the defaults.
83
+ #### SQL executor mode (Node + direct DB)
52
84
 
53
- ```bash
54
- npx generateRestBindings --user root --pass password --host 127.0.0.1 --port 3306 --dbname carbonPHP --prefix carbon_ --output ./shared/rest/C6.ts
85
+ ```ts
86
+ import mysql from "mysql2/promise";
87
+ import { GLOBAL_REST_PARAMETERS } from "./shared/rest/C6";
88
+
89
+ GLOBAL_REST_PARAMETERS.mysqlPool = mysql.createPool({
90
+ host: "127.0.0.1",
91
+ user: "root",
92
+ password: "password",
93
+ database: "sakila",
94
+ });
55
95
  ```
56
96
 
57
- The generated file exports `C6`, `GLOBAL_REST_PARAMETERS`, `TABLES`, `ORM`, and per-table bindings (e.g. `Users`):
97
+ #### HTTP executor mode (frontend or remote client)
58
98
 
59
- ```typescript
60
- import { C6, GLOBAL_REST_PARAMETERS, Users } from "./shared/rest/C6";
61
- ```
99
+ ```ts
100
+ import { axiosInstance } from "@carbonorm/carbonnode";
101
+ import { GLOBAL_REST_PARAMETERS } from "./shared/rest/C6";
62
102
 
63
- You can view the generator source in
64
- [CarbonNode](https://github.com/CarbonORM/CarbonNode/blob/main/scripts/generateRestBindings.ts). We use
65
- [Handlebars templates](https://mustache.github.io/) to generate the code.
103
+ GLOBAL_REST_PARAMETERS.axios = axiosInstance;
104
+ GLOBAL_REST_PARAMETERS.restURL = "/api/rest/";
105
+ ```
66
106
 
67
- ### Runtime Setup
107
+ If you are using CarbonReact, wire `reactBootstrap` as described in [C6 + CarbonReact State Management](#c6--carbonreact-state-management).
68
108
 
69
- CarbonNode executes SQL directly when `GLOBAL_REST_PARAMETERS.mysqlPool` is provided. If no pool is set, it will use
70
- the HTTP executor (useful for frontends or non-Node runtimes).
109
+ ### 3) Host the REST endpoint (optional, Express)
71
110
 
72
- ```typescript
111
+ ```ts
112
+ import express from "express";
73
113
  import mysql from "mysql2/promise";
74
- import { GLOBAL_REST_PARAMETERS } from "./shared/rest/C6";
114
+ import { restExpressRequest } from "@carbonorm/carbonnode";
115
+ import { C6 } from "./shared/rest/C6";
116
+
117
+ const app = express();
118
+ app.set("query parser", "extended");
119
+ app.use(express.json());
120
+
121
+ const mysqlPool = mysql.createPool({
122
+ host: "127.0.0.1",
123
+ user: "root",
124
+ password: "password",
125
+ database: "sakila",
126
+ });
75
127
 
76
- GLOBAL_REST_PARAMETERS.mysqlPool = mysql.createPool({
77
- host: "127.0.0.1",
78
- user: "root",
79
- password: "password",
80
- database: "carbonPHP",
128
+ restExpressRequest({
129
+ router: app,
130
+ routePath: "/api/rest/:table{/:primary}",
131
+ C6,
132
+ mysqlPool,
81
133
  });
134
+ ```
135
+
136
+ ### 4) Query data
82
137
 
83
- // Optional HTTP path:
84
- // GLOBAL_REST_PARAMETERS.axios = axiosInstance;
85
- // GLOBAL_REST_PARAMETERS.restURL = "/rest/";
138
+ ```ts
139
+ import { C6, Actor } from "./shared/rest/C6";
86
140
 
87
- // Optional websocket broadcast on writes:
88
- // GLOBAL_REST_PARAMETERS.websocketBroadcast = (payload) => wsServer.broadcast(JSON.stringify(payload));
141
+ const result = await Actor.Get({
142
+ [C6.SELECT]: [
143
+ Actor.ACTOR_ID,
144
+ Actor.FIRST_NAME,
145
+ Actor.LAST_NAME,
146
+ ],
147
+ [C6.WHERE]: {
148
+ [Actor.LAST_NAME]: [C6.LIKE, [C6.LIT, "%PITT%"]],
149
+ },
150
+ [C6.PAGINATION]: {
151
+ [C6.LIMIT]: 10,
152
+ [C6.PAGE]: 1,
153
+ [C6.ORDER]: [[Actor.ACTOR_ID, C6.DESC]],
154
+ },
155
+ });
89
156
  ```
90
157
 
91
- ### Request Flow
158
+ ## Execution Model
92
159
 
93
160
  ```mermaid
94
161
  flowchart LR
95
- Client["App code\nActor.Get(...)" ] --> RestRequest["restOrm + restRequest"]
96
- RestRequest -->|"Node + mysqlPool"| SqlExec["SqlExecutor"] --> MySQL[("MySQL")]
97
- RestRequest -->|"No pool"| HttpExec["HttpExecutor"] --> RestApi["/rest/:table"]
98
- RestApi --> Express["ExpressHandler"] --> SqlExec
162
+ Client["App Code\nActor.Get(...)"] --> Request["restRequest facade"]
163
+ Request -->|"mysqlPool configured"| SQL["SqlExecutor"] --> DB[("MySQL")]
164
+ Request -->|"no mysqlPool"| HTTP["HttpExecutor"] --> REST["/api/rest/:table"]
165
+ REST --> Express["ExpressHandler"] --> SQL
99
166
  ```
100
167
 
101
- ### SQL Allowlist
168
+ `restRequest` chooses SQL executor when `mysqlPool` is present; otherwise it uses HTTP executor.
102
169
 
103
- To restrict which SQL statements can run in production, set `GLOBAL_REST_PARAMETERS.sqlAllowListPath` to a JSON file
104
- containing allowed SQL strings. When the path is set, `SqlExecutor` normalizes whitespace and validates each query against
105
- the allowlist. If the file is missing, an error is thrown; if the SQL is not listed, execution is blocked.
170
+ ## C6 + CarbonReact State Management
106
171
 
107
- ```typescript
108
- GLOBAL_REST_PARAMETERS.sqlAllowListPath = "/path/to/sqlAllowList.json";
109
- ```
172
+ If your app uses [CarbonReact](https://github.com/CarbonORM/CarbonReact), CarbonNode can keep table state in sync automatically after requests.
110
173
 
111
- Allowlist format:
174
+ Set:
112
175
 
113
- ```json
114
- [
115
- "SELECT * FROM `actor` LIMIT 1"
116
- ]
176
+ ```ts
177
+ import { GLOBAL_REST_PARAMETERS } from "./shared/rest/C6";
178
+
179
+ GLOBAL_REST_PARAMETERS.reactBootstrap = yourCarbonReactInstance;
117
180
  ```
118
181
 
119
- Generated tests in `src/__tests__/sakila-db/C6.test.ts` write response fixtures into `src/__tests__/sakila-db/sqlResponses/`
120
- and compile `src/__tests__/sakila-db/C6.sqlAllowList.json` after the suite finishes. Pass that file path to enable
121
- validation.
182
+ State sync behavior:
183
+
184
+ - HTTP executor path:
185
+ - `GET`: updates table state with response rows
186
+ - `POST`: inserts created row(s) into state
187
+ - `PUT`: updates matching row(s) by primary key
188
+ - `DELETE`: removes matching row(s) by primary key
189
+ - SQL executor path:
190
+ - `GET`: updates table state when `reactBootstrap` is set
191
+ - write sync is typically handled by your websocket/event layer
122
192
 
123
- When using the REST handler directly, forward the path as well:
193
+ How C6 identifies rows:
124
194
 
125
- ```typescript
126
- app.all("/rest/:table", ExpressHandler({ C6, mysqlPool, sqlAllowListPath }));
195
+ - `stateKey` is table name (`restModel.TABLE_NAME`)
196
+ - `uniqueObjectId` is table primary keys (`restModel.PRIMARY_SHORT`)
197
+
198
+ Per-request escape hatch:
199
+
200
+ ```ts
201
+ await SomeTable.Get({
202
+ /* query */
203
+ skipReactBootstrap: true,
204
+ });
127
205
  ```
128
206
 
129
- ### Generated Tests
207
+ This lets you opt out of automatic state writes when a call is read-only for the UI or you are running a background sync pass.
208
+
209
+ ## Canonical SQL Expression Grammar (6.1.0+)
210
+
211
+ `6.1.0` unifies expression parsing across `SELECT`, `WHERE`, `HAVING`, `ORDER`, and expression-capable `UPDATE`/`INSERT` values.
130
212
 
131
- The generator also writes `C6.test.ts` alongside `C6.ts`. Tests use Vitest and the generated bindings. Keep or delete the
132
- file depending on your workflow.
213
+ | Purpose | Canonical form |
214
+ | --- | --- |
215
+ | Known function | `[C6.FUNCTION_NAME, ...args]` |
216
+ | Custom function | `[C6.CALL, "FUNCTION_NAME", ...args]` |
217
+ | Alias | `[C6.AS, expression, "alias"]` |
218
+ | DISTINCT | `[C6.DISTINCT, expression]` |
219
+ | Literal binding | `[C6.LIT, value]` |
220
+ | ORDER term | `[expression, "ASC" \| "DESC"]` |
133
221
 
134
- The generator also writes `C6.MySqlDump.json`, `C6.mysqldump.sql`, and `C6.mysql.cnf` into the same output directory for
135
- debugging and inspection.
222
+ Normative rules:
136
223
 
137
- ### Templates
224
+ - Bare strings are references only (`table.column` or valid aliases in context).
225
+ - Non-reference strings must be wrapped with `[C6.LIT, value]`.
226
+ - `AS` and `DISTINCT` are wrappers, not positional tokens.
227
+ - `PAGINATION.ORDER` must be an array of order terms.
228
+ - Use `C6.CALL` for unknown/custom function names.
138
229
 
139
- Two templates are used to generate the output:
230
+ Removed legacy syntax (throws):
140
231
 
141
- 1) [C6.ts.handlebars](https://github.com/CarbonORM/CarbonNode/blob/main/scripts/assets/handlebars/C6.ts.handlebars)
142
- 2) [C6.test.ts.handlebars](https://github.com/CarbonORM/CarbonNode/blob/main/scripts/assets/handlebars/C6.test.ts.handlebars)
232
+ - `[fn, ..., C6.AS, alias]`
233
+ - `[column, C6.AS, alias]`
234
+ - object-rooted function expressions like `{ [C6.COUNT]: [...] }`
235
+ - implicit string literals in function arguments
236
+ - `ORDER` object-map syntax (now array terms only)
143
237
 
144
- #### Generation Example
238
+ ## Clause-by-Clause Usage
145
239
 
146
- 0) **npx generateRestBindings** is executed.
147
- 1) **The MySQL dump tool** outputs a structure for every table.
240
+ ### SELECT with wrappers
148
241
 
149
- ```mysql
150
- CREATE TABLE actor (
151
- actor_id SMALLINT UNSIGNED NOT NULL AUTO_INCREMENT,
152
- first_name VARCHAR(45) NOT NULL,
153
- last_name VARCHAR(45) NOT NULL,
154
- last_update TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
155
- PRIMARY KEY (actor_id),
156
- KEY idx_actor_last_name (last_name)
157
- ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
242
+ ```ts
243
+ import { C6, Actor } from "./shared/rest/C6";
244
+
245
+ const response = await Actor.Get({
246
+ [C6.SELECT]: [
247
+ [C6.AS, [C6.DISTINCT, Actor.FIRST_NAME], "distinct_name"],
248
+ [C6.AS, [C6.COUNT, Actor.ACTOR_ID], "cnt"],
249
+ [C6.CALL, "COALESCE", [C6.LIT, "Unknown"], Actor.LAST_NAME],
250
+ ],
251
+ });
158
252
  ```
159
253
 
160
- 2) **The generator** parses the table structure and creates an internal representation.
161
- ```typescript
162
- export interface iActor {
163
- 'actor_id'?: number;
164
- 'first_name'?: string;
165
- 'last_name'?: string;
166
- 'last_update'?: Date | number | string;
167
- }
254
+ ### WHERE with literals, BETWEEN, IN, AND/OR
168
255
 
169
- export type ActorPrimaryKeys =
170
- 'actor_id'
171
- ;
172
-
173
- const actor:
174
- C6RestfulModel<
175
- 'actor',
176
- iActor,
177
- ActorPrimaryKeys
178
- > = {
179
- TABLE_NAME: 'actor',
180
- ACTOR_ID: 'actor.actor_id',
181
- FIRST_NAME: 'actor.first_name',
182
- LAST_NAME: 'actor.last_name',
183
- LAST_UPDATE: 'actor.last_update',
184
- PRIMARY: [
185
- 'actor.actor_id',
186
- ],
187
- PRIMARY_SHORT: [
188
- 'actor_id',
256
+ ```ts
257
+ import { C6, Actor } from "./shared/rest/C6";
258
+
259
+ const response = await Actor.Get({
260
+ [C6.WHERE]: {
261
+ [C6.AND]: [
262
+ { [Actor.LAST_NAME]: [C6.LIKE, [C6.LIT, "S%"]] },
263
+ { [Actor.ACTOR_ID]: [C6.BETWEEN, [5, 50]] },
264
+ { [Actor.FIRST_NAME]: [C6.IN, [[C6.LIT, "NICK"], [C6.LIT, "ED"]]] },
189
265
  ],
190
- COLUMNS: {
191
- 'actor.actor_id': 'actor_id',
192
- 'actor.first_name': 'first_name',
193
- 'actor.last_name': 'last_name',
194
- 'actor.last_update': 'last_update',
195
- },
196
- TYPE_VALIDATION: {
197
- 'actor.actor_id': {
198
- MYSQL_TYPE: 'smallint',
199
- MAX_LENGTH: '',
200
- AUTO_INCREMENT: true,
201
- SKIP_COLUMN_IN_POST: false
202
- },
203
- 'actor.first_name': {
204
- MYSQL_TYPE: 'varchar',
205
- MAX_LENGTH: '45',
206
- AUTO_INCREMENT: false,
207
- SKIP_COLUMN_IN_POST: false
208
- },
209
- 'actor.last_name': {
210
- MYSQL_TYPE: 'varchar',
211
- MAX_LENGTH: '45',
212
- AUTO_INCREMENT: false,
213
- SKIP_COLUMN_IN_POST: false
214
- },
215
- 'actor.last_update': {
216
- MYSQL_TYPE: 'timestamp',
217
- MAX_LENGTH: '',
218
- AUTO_INCREMENT: false,
219
- SKIP_COLUMN_IN_POST: false
220
- },
221
- },
222
- REGEX_VALIDATION: {
223
- },
224
- LIFECYCLE_HOOKS: {
225
- GET: {beforeProcessing:{}, beforeExecution:{}, afterExecution:{}, afterCommit:{}},
226
- PUT: {beforeProcessing:{}, beforeExecution:{}, afterExecution:{}, afterCommit:{}},
227
- POST: {beforeProcessing:{}, beforeExecution:{}, afterExecution:{}, afterCommit:{}},
228
- DELETE: {beforeProcessing:{}, beforeExecution:{}, afterExecution:{}, afterCommit:{}},
229
- },
230
- TABLE_REFERENCES: {
266
+ },
267
+ });
268
+ ```
231
269
 
232
- },
233
- TABLE_REFERENCED_BY: {
234
- 'actor_id': [{
235
- TABLE: 'film_actor',
236
- COLUMN: 'actor_id',
237
- CONSTRAINT: 'fk_film_actor_actor',
238
- },],
239
- }
240
- }
270
+ ### JOIN
241
271
 
242
- export const Actor = {
243
- ...actor,
244
- ...restOrm<
245
- OrmGenerics<any, 'actor', iActor, ActorPrimaryKeys>
246
- >(() => ({
247
- ...GLOBAL_REST_PARAMETERS,
248
- restModel: actor
249
- }))
250
- }
272
+ ```ts
273
+ import { C6, Actor, Film_Actor } from "./shared/rest/C6";
274
+
275
+ const response = await Actor.Get({
276
+ [C6.SELECT]: [Actor.ACTOR_ID, Actor.FIRST_NAME],
277
+ [C6.JOIN]: {
278
+ [C6.INNER]: {
279
+ [Film_Actor.TABLE_NAME]: {
280
+ [Film_Actor.ACTOR_ID]: [C6.EQUAL, Actor.ACTOR_ID],
281
+ },
282
+ },
283
+ },
284
+ });
251
285
  ```
252
286
 
253
- 3) **Profit**
254
- You import from the frontend or backend using the same syntax:
287
+ ### ORDER and pagination
255
288
 
256
- ```typescript
257
- import { Actor, C6 } from "./shared/rest/C6";
289
+ ```ts
290
+ import { C6, Actor } from "./shared/rest/C6";
258
291
 
259
- // GET
260
- const actors = await Actor.Get({
261
- [C6.SELECT]: [
262
- Actor.ACTOR_ID,
263
- Actor.FIRST_NAME,
264
- Actor.LAST_NAME,
292
+ const response = await Actor.Get({
293
+ [C6.PAGINATION]: {
294
+ [C6.ORDER]: [
295
+ [Actor.LAST_NAME, C6.ASC],
296
+ [Actor.FIRST_NAME, C6.DESC],
265
297
  ],
266
- [C6.WHERE]: {
267
- [Actor.LAST_NAME]: { like: "%PITT%" },
268
- },
269
- [C6.PAGINATION]: { [C6.LIMIT]: 10 },
298
+ [C6.LIMIT]: 25,
299
+ [C6.PAGE]: 1,
300
+ },
270
301
  });
302
+ ```
271
303
 
272
- // POST
273
- await Actor.Post({
274
- [Actor.FIRST_NAME]: "Brad",
275
- [Actor.LAST_NAME]: "Pitt",
276
- });
304
+ `PAGE` is 1-based:
305
+
306
+ - `PAGE = 1` -> first page
307
+ - `PAGE = 2` -> second page
308
+ - `PAGE = 0` is coerced to `1`
309
+
310
+ ### UPDATE expression values
311
+
312
+ ```ts
313
+ import { C6, Actor } from "./shared/rest/C6";
277
314
 
278
- // PUT (singular)
279
315
  await Actor.Put({
280
- [Actor.ACTOR_ID]: 42,
281
- [Actor.LAST_NAME]: "Updated",
316
+ [Actor.ACTOR_ID]: 42,
317
+ [C6.UPDATE]: {
318
+ [Actor.FIRST_NAME]: [C6.CONCAT, [C6.LIT, "Mr. "], Actor.LAST_NAME],
319
+ },
282
320
  });
321
+ ```
322
+
323
+ ## Helper Builders
324
+
325
+ CarbonNode exports typed builders that return canonical tuples:
283
326
 
284
- // DELETE (singular)
285
- await Actor.Delete({
286
- [Actor.ACTOR_ID]: 42,
327
+ ```ts
328
+ import { fn, call, alias, distinct, lit, order } from "@carbonorm/carbonnode";
329
+ import { C6, Actor } from "./shared/rest/C6";
330
+
331
+ const response = await Actor.Get({
332
+ [C6.SELECT]: [
333
+ alias(distinct(Actor.FIRST_NAME), "distinct_name"),
334
+ alias(fn(C6.COUNT, Actor.ACTOR_ID), "cnt"),
335
+ call("COALESCE", lit("N/A"), Actor.LAST_NAME),
336
+ ],
337
+ [C6.PAGINATION]: {
338
+ [C6.ORDER]: [order(fn(C6.COUNT, Actor.ACTOR_ID), C6.DESC)],
339
+ [C6.LIMIT]: 5,
340
+ },
287
341
  });
288
342
  ```
289
343
 
290
- Example response payloads (HTTP executor):
344
+ ## Singular vs Complex Requests
291
345
 
292
- GET
346
+ Singular requests (primary key at root) are normalized into complex query format.
293
347
 
294
- ```json
295
- {
296
- "success": true,
297
- "rest": [
298
- { "actor_id": 1, "first_name": "PENELOPE", "last_name": "GUINESS" }
299
- ],
300
- "next": "Function"
301
- }
348
+ ```ts
349
+ await Actor.Get({ [Actor.ACTOR_ID]: 42 });
350
+ await Actor.Put({ [Actor.ACTOR_ID]: 42, [Actor.LAST_NAME]: "Updated" });
351
+ await Actor.Delete({ [Actor.ACTOR_ID]: 42 });
302
352
  ```
303
353
 
304
- POST
354
+ Behavior:
305
355
 
306
- ```json
307
- {
308
- "success": true,
309
- "created": 201,
310
- "rest": { "actor_id": 201, "first_name": "Brad", "last_name": "Pitt" }
311
- }
356
+ - `GET` with missing PKs remains a collection query.
357
+ - `PUT`/`DELETE` singular forms require full PK coverage.
358
+
359
+ ## HTTP Query-String Pitfalls
360
+
361
+ If you are manually building URLs, nested arrays must preserve tuple shape exactly.
362
+
363
+ Common failure:
364
+
365
+ - `WHERE[job_runs.job_type][0]=LIT&WHERE[job_runs.job_type][1]=avm_print`
366
+ - This is interpreted as operator tuple `[LIT, "avm_print"]`, and fails with:
367
+ - `Invalid or unsupported SQL operator detected: 'LIT'`
368
+
369
+ Correct structure:
370
+
371
+ - `WHERE[job_runs.job_type][0]==`
372
+ - `WHERE[job_runs.job_type][1][0]=LIT`
373
+ - `WHERE[job_runs.job_type][1][1]=avm_print`
374
+
375
+ Also ensure `ORDER` is under `PAGINATION`:
376
+
377
+ - correct: `PAGINATION[ORDER][0][0]=job_runs.created_at`
378
+ - incorrect: `ORDER[0][0]=job_runs.created_at`
379
+
380
+ Recommendation: use generated bindings + axios executor, not manual URL construction.
381
+
382
+ ## Generator Output
383
+
384
+ `generateRestBindings` writes:
385
+
386
+ - `C6.ts` (typed table model + REST bindings)
387
+ - `C6.test.ts` (generated test suite)
388
+ - `C6.mysqldump.sql`
389
+ - `C6.mysqldump.json`
390
+ - `C6.mysql.cnf`
391
+
392
+ Template sources:
393
+
394
+ - `scripts/assets/handlebars/C6.ts.handlebars`
395
+ - `scripts/assets/handlebars/C6.test.ts.handlebars`
396
+
397
+ ## SQL Allowlist
398
+
399
+ To enforce a SQL allowlist in production:
400
+
401
+ ```ts
402
+ import { GLOBAL_REST_PARAMETERS } from "./shared/rest/C6";
403
+
404
+ GLOBAL_REST_PARAMETERS.sqlAllowListPath = "/path/to/C6.sqlAllowList.json";
312
405
  ```
313
406
 
314
- PUT
407
+ Optional: add a project-specific normalizer for allowlist matching:
315
408
 
316
- ```json
317
- {
318
- "success": true,
319
- "updated": true,
320
- "rest": { "actor_id": 42, "last_name": "Updated" }
321
- }
409
+ ```ts
410
+ import { GLOBAL_REST_PARAMETERS } from "./shared/rest/C6";
411
+
412
+ GLOBAL_REST_PARAMETERS.sqlQueryNormalizer = (normalizedSql) =>
413
+ normalizedSql.toLowerCase();
322
414
  ```
323
415
 
324
- DELETE
416
+ `sqlQueryNormalizer` runs **after** CarbonNode's built-in normalization. Use it to enforce house-style matching (for example lowercase-only allowlist entries).
417
+
418
+ Allowlist file format:
325
419
 
326
420
  ```json
327
- {
328
- "success": true,
329
- "deleted": true,
330
- "rest": { "actor_id": 42 }
331
- }
421
+ [
422
+ "SELECT * FROM `actor` LIMIT 1"
423
+ ]
332
424
  ```
333
425
 
334
- SQL executor responses omit `success` and include `sql` for GETs plus `affected` for writes. Express responses from `ExpressHandler` add `success: true`.
426
+ Normalization behavior (important):
335
427
 
336
- SQL executor example (GET):
428
+ CarbonNode normalizes both:
337
429
 
338
- ```json
339
- {
340
- "rest": [
341
- { "actor_id": 1, "first_name": "PENELOPE", "last_name": "GUINESS" }
342
- ],
343
- "sql": {
344
- "sql": "SELECT * FROM `actor` LIMIT 10",
345
- "values": []
430
+ - each SQL entry in your allowlist file
431
+ - each runtime SQL statement before matching
432
+
433
+ So you should treat allowlist entries as **normalized query shapes**, not exact byte-for-byte logs.
434
+
435
+ Normalization includes:
436
+
437
+ - stripping ANSI color codes
438
+ - collapsing whitespace
439
+ - removing trailing `;`
440
+ - normalizing common geo function names (`ST_DISTANCE_SPHERE`, `ST_GEOMFROMTEXT`, `MBRCONTAINS`)
441
+ - normalizing `LIMIT`/`OFFSET` numeric literals to placeholders
442
+ - collapsing bind groups (for example `IN (?, ?, ?)` -> `IN (? ×*)`)
443
+ - collapsing repeated `VALUES` bind rows to a wildcard row shape
444
+
445
+ Example (conceptual):
446
+
447
+ ```sql
448
+ -- runtime SQL
449
+ SELECT * FROM `actor` WHERE actor.actor_id IN (?, ?, ?) LIMIT 100
450
+
451
+ -- normalized shape used for matching
452
+ SELECT * FROM `actor` WHERE actor.actor_id IN (? ×*) LIMIT ?
453
+ ```
454
+
455
+ When enabled:
456
+
457
+ - missing allowlist file -> error
458
+ - SQL not in allowlist -> blocked
459
+
460
+ Practical workflow:
461
+
462
+ 1. Run representative queries in tests/integration.
463
+ 2. Collect normalized SQL statements.
464
+ 3. Save them into your allowlist JSON.
465
+ 4. Set `GLOBAL_REST_PARAMETERS.sqlAllowListPath`.
466
+
467
+ ## Lifecycle Hooks and Websocket Broadcast
468
+
469
+ Generated models include lifecycle hook groups per method:
470
+
471
+ - `beforeProcessing`
472
+ - `beforeExecution`
473
+ - `afterExecution`
474
+ - `afterCommit`
475
+
476
+ Unlike other CarbonORM language bindings, C6.ts is not semi-persistent.
477
+ Modifications to the C6 object should be done at runtime, not by editing the generated file.
478
+
479
+ Websocket payloads for writes are supported via:
480
+
481
+ ```ts
482
+ GLOBAL_REST_PARAMETERS.websocketBroadcast = async (payload) => {
483
+ // broadcast payload to your websocket infrastructure
484
+ };
485
+ ```
486
+
487
+ ### Real-time communication explained (no websocket background required)
488
+
489
+ HTTP is request/response. A browser asks, server answers, done.
490
+
491
+ Real-time systems add **server push**: when data changes, the server proactively notifies connected clients so they can refresh or patch state immediately.
492
+
493
+ In CarbonNode:
494
+
495
+ - `POST`, `PUT`, `DELETE` can trigger `websocketBroadcast`
496
+ - `GET` does not broadcast
497
+ - CarbonNode does **not** run a websocket server for you
498
+ - you provide the `websocketBroadcast` function and route payloads into your existing websocket layer (`ws`, Socket.IO, SSE bridge, etc.)
499
+
500
+ Payload shape (high level):
501
+
502
+ - table + method metadata
503
+ - request body
504
+ - request primary key (when detectable)
505
+ - response row(s) and response primary key when available
506
+
507
+ Why this matters:
508
+
509
+ - keeps multiple tabs/users aligned without polling
510
+ - reduces stale UI after writes
511
+ - enables event-driven cache invalidation (`table + primary key`)
512
+
513
+ Minimal `ws` example:
514
+
515
+ ```ts
516
+ import { WebSocketServer } from "ws";
517
+ import { GLOBAL_REST_PARAMETERS } from "./shared/rest/C6";
518
+
519
+ const wss = new WebSocketServer({ server: httpServer });
520
+
521
+ GLOBAL_REST_PARAMETERS.websocketBroadcast = async (payload) => {
522
+ const message = JSON.stringify({ type: "db.write", payload });
523
+ for (const client of wss.clients) {
524
+ if (client.readyState === 1) {
525
+ client.send(message);
526
+ }
346
527
  }
528
+ };
529
+ ```
530
+
531
+ Client handling pattern:
532
+
533
+ 1. listen for `db.write` events
534
+ 2. inspect `payload.REST.TABLE_NAME` and primary keys
535
+ 3. invalidate/refetch relevant queries or patch local state
536
+
537
+ ## Testing
538
+
539
+ Run full validation:
540
+
541
+ ```bash
542
+ npm test
543
+ ```
544
+
545
+ This includes:
546
+
547
+ - build
548
+ - binding generation
549
+ - test suite
550
+
551
+ ## Migration Notes (6.0 -> 6.1)
552
+
553
+ Before:
554
+
555
+ ```ts
556
+ [C6.COUNT, Actor.ACTOR_ID, C6.AS, "cnt"]
557
+ ```
558
+
559
+ After:
560
+
561
+ ```ts
562
+ [C6.AS, [C6.COUNT, Actor.ACTOR_ID], "cnt"]
563
+ ```
564
+
565
+ Before:
566
+
567
+ ```ts
568
+ [C6.PAGINATION]: {
569
+ [C6.ORDER]: { [Actor.LAST_NAME]: C6.ASC }
347
570
  }
348
571
  ```
349
572
 
350
- Our CarbonReact extends this solution for automatic state and pagination management.
573
+ After:
351
574
 
575
+ ```ts
576
+ [C6.PAGINATION]: {
577
+ [C6.ORDER]: [[Actor.LAST_NAME, C6.ASC]]
578
+ }
579
+ ```
352
580
 
353
- # Git Hooks
581
+ Before:
354
582
 
355
- This project uses Git hooks to automate certain tasks:
583
+ ```ts
584
+ [C6.ST_GEOMFROMTEXT, ["POINT(-104.89 39.39)", 4326]]
585
+ ```
356
586
 
357
- - **post-commit**: Builds the project before pushing to ensure only working code is pushed
358
- - **post-push**: Automatically publishes to npm when the version number changes
587
+ After:
359
588
 
360
- To set up the Git hooks, run:
589
+ ```ts
590
+ [C6.ST_GEOMFROMTEXT, [C6.LIT, "POINT(-104.89 39.39)"], 4326]
591
+ ```
361
592
 
362
- ```bash
363
- npm run hooks:setup
593
+ ## AI Interpretation Contract
594
+
595
+ Use this section as a strict contract for automated query generation.
596
+
597
+ ```yaml
598
+ carbonnode:
599
+ version: "6.1.0+"
600
+ grammar:
601
+ known_function: "[C6C.<KNOWN_FN>, ...args]"
602
+ custom_function: "[C6C.CALL, 'FUNCTION_NAME', ...args]"
603
+ alias: "[C6C.AS, expression, 'alias']"
604
+ distinct: "[C6C.DISTINCT, expression]"
605
+ literal: "[C6C.LIT, value]"
606
+ order_term: "[expression, 'ASC' | 'DESC']"
607
+ required_rules:
608
+ - "Bare strings are references only."
609
+ - "Wrap non-reference strings with C6C.LIT."
610
+ - "PAGINATION.ORDER must be an array of order terms."
611
+ - "Use C6C.CALL for unknown function names."
612
+ forbidden_legacy:
613
+ - "[fn, ..., C6C.AS, alias]"
614
+ - "[column, C6C.AS, alias]"
615
+ - "{ [C6C.COUNT]: [...] }"
616
+ - "Implicit string literals in function args"
617
+ - "ORDER object-map syntax"
364
618
  ```
365
619
 
366
- This will configure Git to use the hooks in the `.githooks` directory. The hooks are automatically set up when you run `npm install` as well.
620
+ ## Git Hooks
621
+
622
+ This project uses Git hooks via `postinstall`:
623
+
624
+ - `post-commit`: builds project
625
+ - `post-push`: publishes to npm when version changes
626
+ - `npm install` runs `postinstall` to ensure hooks are configured
627
+
628
+ ## Support
367
629
 
368
- # Support and Issues
630
+ Report issues at:
369
631
 
370
- Any issues found should be reported on [GitHub](https://github.com/CarbonORM/CarbonNode/issues).
632
+ - [CarbonNode Issues](https://github.com/CarbonORM/CarbonNode/issues)