@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.
- package/README.md +521 -259
- package/dist/constants/C6Constants.d.ts +342 -338
- package/dist/executors/SqlExecutor.d.ts +1 -0
- package/dist/index.cjs.js +746 -290
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.esm.js +737 -291
- package/dist/index.esm.js.map +1 -1
- package/dist/orm/builders/AggregateBuilder.d.ts +5 -1
- package/dist/orm/builders/ConditionBuilder.d.ts +2 -3
- package/dist/orm/builders/ExpressionSerializer.d.ts +22 -0
- package/dist/orm/builders/PaginationBuilder.d.ts +4 -6
- package/dist/orm/queryHelpers.d.ts +12 -1
- package/dist/orm/utils/sqlUtils.d.ts +1 -0
- package/dist/types/mysqlTypes.d.ts +6 -1
- package/dist/types/ormInterfaces.d.ts +7 -5
- package/dist/utils/sqlAllowList.d.ts +5 -3
- package/package.json +2 -2
- package/scripts/assets/handlebars/C6.test.ts.handlebars +4 -4
- package/src/__tests__/expressServer.e2e.test.ts +26 -17
- package/src/__tests__/fixtures/c6.fixture.ts +33 -0
- package/src/__tests__/httpExecutorSingular.e2e.test.ts +53 -14
- package/src/__tests__/normalizeSingularRequest.test.ts +26 -8
- package/src/__tests__/sakila-db/C6.js +1 -1
- package/src/__tests__/sakila-db/C6.mysqldump.json +1 -1
- package/src/__tests__/sakila-db/C6.mysqldump.sql +1 -1
- package/src/__tests__/sakila-db/C6.sqlAllowList.json +1 -1
- package/src/__tests__/sakila-db/C6.test.ts +4 -4
- package/src/__tests__/sakila-db/C6.ts +1 -1
- package/src/__tests__/sakila-db/sqlResponses/C6.actor.post.json +6 -6
- package/src/__tests__/sakila-db/sqlResponses/C6.actor.post.latest.json +3 -3
- package/src/__tests__/sakila-db/sqlResponses/C6.actor.put.json +1 -1
- package/src/__tests__/sakila-db/sqlResponses/C6.actor.put.lookup.json +3 -3
- package/src/__tests__/sakila-db/sqlResponses/C6.address.post.json +19 -12
- package/src/__tests__/sakila-db/sqlResponses/C6.address.post.latest.json +5 -5
- package/src/__tests__/sakila-db/sqlResponses/C6.address.put.json +1 -1
- package/src/__tests__/sakila-db/sqlResponses/C6.address.put.lookup.json +5 -5
- package/src/__tests__/sakila-db/sqlResponses/C6.category.post.json +4 -4
- package/src/__tests__/sakila-db/sqlResponses/C6.category.post.latest.json +2 -2
- package/src/__tests__/sakila-db/sqlResponses/C6.category.put.json +1 -1
- package/src/__tests__/sakila-db/sqlResponses/C6.category.put.lookup.json +2 -2
- package/src/__tests__/sakila-db/sqlResponses/C6.city.post.json +4 -4
- package/src/__tests__/sakila-db/sqlResponses/C6.city.post.latest.json +2 -2
- package/src/__tests__/sakila-db/sqlResponses/C6.city.put.json +1 -1
- package/src/__tests__/sakila-db/sqlResponses/C6.city.put.lookup.json +2 -2
- package/src/__tests__/sakila-db/sqlResponses/C6.country.post.json +4 -4
- package/src/__tests__/sakila-db/sqlResponses/C6.country.post.latest.json +2 -2
- package/src/__tests__/sakila-db/sqlResponses/C6.country.put.json +1 -1
- package/src/__tests__/sakila-db/sqlResponses/C6.country.put.lookup.json +2 -2
- package/src/__tests__/sakila-db/sqlResponses/C6.customer.post.json +10 -10
- package/src/__tests__/sakila-db/sqlResponses/C6.customer.post.latest.json +5 -5
- package/src/__tests__/sakila-db/sqlResponses/C6.customer.put.json +1 -1
- package/src/__tests__/sakila-db/sqlResponses/C6.customer.put.lookup.json +5 -5
- package/src/__tests__/sakila-db/sqlResponses/C6.film.post.json +4 -4
- package/src/__tests__/sakila-db/sqlResponses/C6.film.post.latest.json +2 -2
- package/src/__tests__/sakila-db/sqlResponses/C6.film.put.json +1 -1
- package/src/__tests__/sakila-db/sqlResponses/C6.film.put.lookup.json +2 -2
- package/src/__tests__/sakila-db/sqlResponses/C6.inventory.post.json +2 -2
- package/src/__tests__/sakila-db/sqlResponses/C6.inventory.post.latest.json +1 -1
- package/src/__tests__/sakila-db/sqlResponses/C6.inventory.put.json +1 -1
- package/src/__tests__/sakila-db/sqlResponses/C6.inventory.put.lookup.json +1 -1
- package/src/__tests__/sakila-db/sqlResponses/C6.language.post.json +4 -4
- package/src/__tests__/sakila-db/sqlResponses/C6.language.post.latest.json +2 -2
- package/src/__tests__/sakila-db/sqlResponses/C6.language.put.json +1 -1
- package/src/__tests__/sakila-db/sqlResponses/C6.language.put.lookup.json +2 -2
- package/src/__tests__/sakila-db/sqlResponses/C6.payment.post.json +4 -4
- package/src/__tests__/sakila-db/sqlResponses/C6.payment.post.latest.json +2 -2
- package/src/__tests__/sakila-db/sqlResponses/C6.payment.put.lookup.json +2 -2
- package/src/__tests__/sakila-db/sqlResponses/C6.rental.post.json +6 -6
- package/src/__tests__/sakila-db/sqlResponses/C6.rental.post.latest.json +3 -3
- package/src/__tests__/sakila-db/sqlResponses/C6.rental.put.json +1 -1
- package/src/__tests__/sakila-db/sqlResponses/C6.rental.put.lookup.json +3 -3
- package/src/__tests__/sqlAllowList.test.ts +56 -1
- package/src/__tests__/sqlBuilders.complex.test.ts +62 -74
- package/src/__tests__/sqlBuilders.expressions.test.ts +58 -30
- package/src/__tests__/sqlBuilders.test.ts +106 -5
- package/src/constants/C6Constants.ts +3 -1
- package/src/executors/HttpExecutor.ts +2 -1
- package/src/executors/SqlExecutor.ts +29 -4
- package/src/index.ts +1 -0
- package/src/orm/builders/AggregateBuilder.ts +67 -106
- package/src/orm/builders/ConditionBuilder.ts +72 -103
- package/src/orm/builders/ExpressionSerializer.ts +275 -0
- package/src/orm/builders/PaginationBuilder.ts +24 -34
- package/src/orm/queryHelpers.ts +29 -0
- package/src/orm/utils/sqlUtils.ts +172 -4
- package/src/types/mysqlTypes.ts +130 -9
- package/src/types/ormInterfaces.ts +7 -7
- package/src/utils/normalizeSingularRequest.ts +11 -4
- 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
|
|
13
|
-
|
|
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
|
-
|
|
48
|
+
```bash
|
|
49
|
+
npm install @carbonorm/carbonnode
|
|
50
|
+
```
|
|
16
51
|
|
|
17
|
-
|
|
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
|
-
|
|
30
|
-
|
|
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
|
-
|
|
60
|
+
### 1) Generate bindings
|
|
34
61
|
|
|
35
|
-
|
|
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
|
-
|
|
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
|
-
|
|
42
|
-
which comes prepackaged with npm (node package manager).
|
|
75
|
+
Import from generated bindings:
|
|
43
76
|
|
|
44
|
-
```
|
|
45
|
-
|
|
77
|
+
```ts
|
|
78
|
+
import { C6, GLOBAL_REST_PARAMETERS, Actor } from "./shared/rest/C6";
|
|
46
79
|
```
|
|
47
80
|
|
|
48
|
-
|
|
81
|
+
### 2) Configure runtime
|
|
49
82
|
|
|
50
|
-
|
|
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
|
-
```
|
|
54
|
-
|
|
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
|
-
|
|
97
|
+
#### HTTP executor mode (frontend or remote client)
|
|
58
98
|
|
|
59
|
-
```
|
|
60
|
-
import {
|
|
61
|
-
|
|
99
|
+
```ts
|
|
100
|
+
import { axiosInstance } from "@carbonorm/carbonnode";
|
|
101
|
+
import { GLOBAL_REST_PARAMETERS } from "./shared/rest/C6";
|
|
62
102
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
103
|
+
GLOBAL_REST_PARAMETERS.axios = axiosInstance;
|
|
104
|
+
GLOBAL_REST_PARAMETERS.restURL = "/api/rest/";
|
|
105
|
+
```
|
|
66
106
|
|
|
67
|
-
|
|
107
|
+
If you are using CarbonReact, wire `reactBootstrap` as described in [C6 + CarbonReact State Management](#c6--carbonreact-state-management).
|
|
68
108
|
|
|
69
|
-
|
|
70
|
-
the HTTP executor (useful for frontends or non-Node runtimes).
|
|
109
|
+
### 3) Host the REST endpoint (optional, Express)
|
|
71
110
|
|
|
72
|
-
```
|
|
111
|
+
```ts
|
|
112
|
+
import express from "express";
|
|
73
113
|
import mysql from "mysql2/promise";
|
|
74
|
-
import {
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
// GLOBAL_REST_PARAMETERS.restURL = "/rest/";
|
|
138
|
+
```ts
|
|
139
|
+
import { C6, Actor } from "./shared/rest/C6";
|
|
86
140
|
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
158
|
+
## Execution Model
|
|
92
159
|
|
|
93
160
|
```mermaid
|
|
94
161
|
flowchart LR
|
|
95
|
-
Client["App
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
|
|
168
|
+
`restRequest` chooses SQL executor when `mysqlPool` is present; otherwise it uses HTTP executor.
|
|
102
169
|
|
|
103
|
-
|
|
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
|
-
|
|
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
|
-
|
|
174
|
+
Set:
|
|
112
175
|
|
|
113
|
-
```
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
176
|
+
```ts
|
|
177
|
+
import { GLOBAL_REST_PARAMETERS } from "./shared/rest/C6";
|
|
178
|
+
|
|
179
|
+
GLOBAL_REST_PARAMETERS.reactBootstrap = yourCarbonReactInstance;
|
|
117
180
|
```
|
|
118
181
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
|
|
193
|
+
How C6 identifies rows:
|
|
124
194
|
|
|
125
|
-
|
|
126
|
-
|
|
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
|
-
|
|
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
|
-
|
|
132
|
-
|
|
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
|
-
|
|
135
|
-
debugging and inspection.
|
|
222
|
+
Normative rules:
|
|
136
223
|
|
|
137
|
-
|
|
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
|
-
|
|
230
|
+
Removed legacy syntax (throws):
|
|
140
231
|
|
|
141
|
-
|
|
142
|
-
|
|
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
|
-
|
|
238
|
+
## Clause-by-Clause Usage
|
|
145
239
|
|
|
146
|
-
|
|
147
|
-
1) **The MySQL dump tool** outputs a structure for every table.
|
|
240
|
+
### SELECT with wrappers
|
|
148
241
|
|
|
149
|
-
```
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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
|
-
|
|
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
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
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
|
-
|
|
254
|
-
You import from the frontend or backend using the same syntax:
|
|
287
|
+
### ORDER and pagination
|
|
255
288
|
|
|
256
|
-
```
|
|
257
|
-
import {
|
|
289
|
+
```ts
|
|
290
|
+
import { C6, Actor } from "./shared/rest/C6";
|
|
258
291
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
[C6.
|
|
262
|
-
|
|
263
|
-
|
|
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.
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
[C6.PAGINATION]: { [C6.LIMIT]: 10 },
|
|
298
|
+
[C6.LIMIT]: 25,
|
|
299
|
+
[C6.PAGE]: 1,
|
|
300
|
+
},
|
|
270
301
|
});
|
|
302
|
+
```
|
|
271
303
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
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
|
-
|
|
281
|
-
|
|
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
|
-
|
|
285
|
-
|
|
286
|
-
|
|
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
|
-
|
|
344
|
+
## Singular vs Complex Requests
|
|
291
345
|
|
|
292
|
-
|
|
346
|
+
Singular requests (primary key at root) are normalized into complex query format.
|
|
293
347
|
|
|
294
|
-
```
|
|
295
|
-
{
|
|
296
|
-
|
|
297
|
-
|
|
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
|
-
|
|
354
|
+
Behavior:
|
|
305
355
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
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
|
-
|
|
407
|
+
Optional: add a project-specific normalizer for allowlist matching:
|
|
315
408
|
|
|
316
|
-
```
|
|
317
|
-
{
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
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
|
-
|
|
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
|
-
"
|
|
329
|
-
|
|
330
|
-
"rest": { "actor_id": 42 }
|
|
331
|
-
}
|
|
421
|
+
[
|
|
422
|
+
"SELECT * FROM `actor` LIMIT 1"
|
|
423
|
+
]
|
|
332
424
|
```
|
|
333
425
|
|
|
334
|
-
|
|
426
|
+
Normalization behavior (important):
|
|
335
427
|
|
|
336
|
-
|
|
428
|
+
CarbonNode normalizes both:
|
|
337
429
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
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
|
-
|
|
573
|
+
After:
|
|
351
574
|
|
|
575
|
+
```ts
|
|
576
|
+
[C6.PAGINATION]: {
|
|
577
|
+
[C6.ORDER]: [[Actor.LAST_NAME, C6.ASC]]
|
|
578
|
+
}
|
|
579
|
+
```
|
|
352
580
|
|
|
353
|
-
|
|
581
|
+
Before:
|
|
354
582
|
|
|
355
|
-
|
|
583
|
+
```ts
|
|
584
|
+
[C6.ST_GEOMFROMTEXT, ["POINT(-104.89 39.39)", 4326]]
|
|
585
|
+
```
|
|
356
586
|
|
|
357
|
-
|
|
358
|
-
- **post-push**: Automatically publishes to npm when the version number changes
|
|
587
|
+
After:
|
|
359
588
|
|
|
360
|
-
|
|
589
|
+
```ts
|
|
590
|
+
[C6.ST_GEOMFROMTEXT, [C6.LIT, "POINT(-104.89 39.39)"], 4326]
|
|
591
|
+
```
|
|
361
592
|
|
|
362
|
-
|
|
363
|
-
|
|
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
|
-
|
|
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
|
-
|
|
630
|
+
Report issues at:
|
|
369
631
|
|
|
370
|
-
|
|
632
|
+
- [CarbonNode Issues](https://github.com/CarbonORM/CarbonNode/issues)
|