@dreamtree-org/korm-js 1.0.54 → 1.0.56
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/AuthorizationService.js +1 -0
- package/BaseHelperUtility.js +1 -1
- package/ControllerWrapper.js +1 -1
- package/KormError.js +1 -0
- package/README.md +331 -42
- package/RequestValidator.js +1 -1
- package/ai-skills/korm-js.md +268 -0
- package/bin/korm-mcp.js +2 -0
- package/build.js +1 -1
- package/cli.js +1 -1
- package/clients/BaseSyncTable.js +1 -0
- package/clients/mysql/BaseUtility.js +1 -1
- package/clients/mysql/CurdTable.js +1 -1
- package/clients/mysql/DataTypeMap.js +1 -1
- package/clients/mysql/HookService.js +1 -1
- package/clients/mysql/QueryBuilder.js +1 -1
- package/clients/mysql/QueryService.js +1 -1
- package/clients/mysql/SyncTable.js +1 -1
- package/clients/pg/BaseUtility.js +1 -1
- package/clients/pg/CurdTable.js +1 -1
- package/clients/pg/DataTypeMap.js +1 -1
- package/clients/pg/HookService.js +1 -1
- package/clients/pg/QueryBuilder.js +1 -1
- package/clients/pg/QueryService.js +1 -1
- package/clients/pg/SyncTable.js +1 -1
- package/clients/sqlite/BaseUtility.js +1 -1
- package/clients/sqlite/CurdTable.js +1 -1
- package/clients/sqlite/HookService.js +1 -1
- package/clients/sqlite/QueryBuilder.js +1 -1
- package/clients/sqlite/QueryService.js +1 -1
- package/clients/sqlite/SyncTable.js +1 -1
- package/columnSchema.js +1 -0
- package/index.d.ts +424 -0
- package/index.js +1 -1
- package/jest.config.engine.js +1 -0
- package/jest.config.js +1 -1
- package/package.json +7 -2
- package/requestSchema.js +1 -0
- package/schemaDescribe.js +1 -0
- package/src/mcp/errors.js +1 -0
- package/src/mcp/schemaIntrospect.js +1 -0
- package/src/mcp/server.js +1 -0
- package/src/mcp/toolGenerator.js +1 -0
- package/TableSchemaSync.js +0 -1
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
# KORM-JS — AI assistant reference
|
|
2
|
+
|
|
3
|
+
> Skill installed by `npx @dreamtree-org/korm-js init --ai <provider>`.
|
|
4
|
+
> Source of truth: [`@dreamtree-org/korm-js`](https://www.npmjs.com/package/@dreamtree-org/korm-js).
|
|
5
|
+
> Re-run the installer to refresh this block when the library updates.
|
|
6
|
+
|
|
7
|
+
## What KORM-JS is
|
|
8
|
+
|
|
9
|
+
`@dreamtree-org/korm-js` is a **JSON-contract ORM** built on top of Knex. The consumer sends a single JSON request describing the operation; KORM translates it into safe, parameterized SQL across **MySQL, PostgreSQL, and SQLite**. Models and their relations are declared once; CRUD is never hand-written.
|
|
10
|
+
|
|
11
|
+
When helping the user, **always express data access as a KORM request object**, not as raw Knex calls or string SQL.
|
|
12
|
+
|
|
13
|
+
## Wiring (do not invent alternatives)
|
|
14
|
+
|
|
15
|
+
```js
|
|
16
|
+
const { initializeKORM } = require('@dreamtree-org/korm-js');
|
|
17
|
+
const knex = require('knex');
|
|
18
|
+
|
|
19
|
+
const db = knex({
|
|
20
|
+
client: 'mysql2', // 'mysql2' | 'pg' | 'sqlite3'
|
|
21
|
+
connection: {
|
|
22
|
+
/* ... */
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const korm = initializeKORM({
|
|
27
|
+
db,
|
|
28
|
+
dbClient: 'mysql', // 'mysql' | 'pg' | 'sqlite'
|
|
29
|
+
debug: false,
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const result = await korm.processRequest(requestObject, 'ModelName');
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
In Express/Next/Fastify the consumer just forwards `req.body` and the model name. KORM does **not** own routing — never suggest an HTTP framework as part of KORM itself.
|
|
36
|
+
|
|
37
|
+
## Request contract
|
|
38
|
+
|
|
39
|
+
`processRequest(request, modelName)` accepts a JSON object with these top-level fields:
|
|
40
|
+
|
|
41
|
+
| Field | Type | Purpose |
|
|
42
|
+
| ----------------------------------------------- | -------------------------- | -------------------------------------------------------------------------------------------------------- |
|
|
43
|
+
| `action` | string (required) | The operation: `list`, `show`, `create`, `update`, `delete`, `count`, `sum`, `replace`, `upsert`, `sync` |
|
|
44
|
+
| `where` | object \| array | Filter conditions for `list`/`show`/`update`/`delete`/`count`/`sum` |
|
|
45
|
+
| `data` | object \| array | Payload for `create`/`update`/`upsert`/`replace`/`sync` |
|
|
46
|
+
| `select` | array \| string | Columns to return (default: all) |
|
|
47
|
+
| `with` | array of strings | Relations to eager-load (dot-nested allowed: `"Post.Comment"`) |
|
|
48
|
+
| `withWhere` | object | Filters scoped to related rows only — does NOT filter parents |
|
|
49
|
+
| `orderBy` | object \| array \| string | `{column, direction}` / `"column"` / array of either |
|
|
50
|
+
| `limit` | number | Max rows |
|
|
51
|
+
| `offset` / `page` | number | Pagination |
|
|
52
|
+
| `groupBy` | array \| string | GROUP BY columns |
|
|
53
|
+
| `having` | object | Post-group filter |
|
|
54
|
+
| `distinct` | boolean \| array \| string | DISTINCT / DISTINCT ON |
|
|
55
|
+
| `join` / `innerJoin` / `leftJoin` / `rightJoin` | object \| array | Explicit joins (rarely needed — prefer `with`) |
|
|
56
|
+
| `conflict` | array | Conflict columns for `upsert` / `sync` |
|
|
57
|
+
| `other_requests` | object | Nested requests on related models; results returned under `other_responses` |
|
|
58
|
+
| `dryRun` | boolean | If `true`, return the SQL that would run without executing it (see "Inspecting queries" below) |
|
|
59
|
+
|
|
60
|
+
### Actions
|
|
61
|
+
|
|
62
|
+
| Action | Behavior |
|
|
63
|
+
| --------- | -------------------------------------------------------------------- |
|
|
64
|
+
| `list` | Multi-row read with where/order/limit/offset |
|
|
65
|
+
| `show` | Single-row read |
|
|
66
|
+
| `create` | Insert from `data` (object = 1 row, array = bulk) |
|
|
67
|
+
| `update` | Update rows matching `where` with `data` |
|
|
68
|
+
| `delete` | Delete (soft if the model declares soft-delete; otherwise hard) |
|
|
69
|
+
| `count` | COUNT(\*) of matching rows |
|
|
70
|
+
| `sum` | Sum a column or formula; needs `data.sumColumn` or `data.sumFormula` |
|
|
71
|
+
| `replace` | Full-row replace by PK, all engines (MySQL/SQLite = delete+insert; pg = ON CONFLICT merge — omitted cols retained). Optional `conflict` |
|
|
72
|
+
| `upsert` | Insert-or-update keyed by `conflict` columns |
|
|
73
|
+
| `sync` | Upsert matching `data` + delete non-matching within `where` scope |
|
|
74
|
+
|
|
75
|
+
### `where` operator cheat-sheet
|
|
76
|
+
|
|
77
|
+
Operators are **encoded as string prefixes on the value** (not separate keys):
|
|
78
|
+
|
|
79
|
+
| Operator | Value form | Example | SQL |
|
|
80
|
+
| ------------------- | --------------------------- | --------------------------- | --------------------- |
|
|
81
|
+
| Equals (default) | bare value | `{status: "active"}` | `= ?` |
|
|
82
|
+
| `>=` | `">=N"` | `{age: ">=18"}` | `>= ?` |
|
|
83
|
+
| `<=` | `"<=N"` | `{age: "<=65"}` | `<= ?` |
|
|
84
|
+
| `>` | `">N"` | `{price: ">100"}` | `> ?` |
|
|
85
|
+
| `<` | `"<N"` | `{price: "<500"}` | `< ?` |
|
|
86
|
+
| `!=` | `"!V"` | `{status: "!deleted"}` | `!= ?` |
|
|
87
|
+
| LIKE | `"%V%"` (or `"V%"`, `"%V"`) | `{name: "%john%"}` | `LIKE ?` |
|
|
88
|
+
| IN | `"[]a,b,c"` | `{role: "[]admin,user"}` | `IN (?, ?, ?)` |
|
|
89
|
+
| NOT IN | `"![]a,b"` | `{role: "![]banned"}` | `NOT IN (...)` |
|
|
90
|
+
| BETWEEN | `"><min,max"` | `{age: "><18,65"}` | `BETWEEN ? AND ?` |
|
|
91
|
+
| NOT BETWEEN | `"<>min,max"` | `{score: "<>0,50"}` | `NOT BETWEEN ? AND ?` |
|
|
92
|
+
| IS NULL | `null` | `{deleted_at: null}` | `IS NULL` |
|
|
93
|
+
| OR group | key prefix `"Or:"` | `{"Or:first_name": "John"}` | `OR (...)` |
|
|
94
|
+
| NOT EXISTS relation | `"!RelName": true` | `{"User.!UserRole": true}` | `NOT EXISTS (...)` |
|
|
95
|
+
|
|
96
|
+
Rules:
|
|
97
|
+
|
|
98
|
+
- Non-`Or:`-prefixed keys are ANDed together.
|
|
99
|
+
- Array form `where: [ {a: 1}, {b: 2} ]` is equivalent to object form for ANDs but lets you repeat the same column.
|
|
100
|
+
- `sumFormula` uses `{columnName}` placeholders and accepts only `+ - * / ( )` and decimal literals — **never interpolate user input**.
|
|
101
|
+
- All values flow through Knex bindings. **Do not hand-build SQL strings.**
|
|
102
|
+
|
|
103
|
+
### Relations (`with`)
|
|
104
|
+
|
|
105
|
+
Relation metadata lives on the model definition (`hasRelations`). The consumer just names them:
|
|
106
|
+
|
|
107
|
+
```js
|
|
108
|
+
{
|
|
109
|
+
action: "list",
|
|
110
|
+
where: { id: 1 },
|
|
111
|
+
with: ["UserDetail", "Post", "Post.Comment"],
|
|
112
|
+
withWhere: { "Post.status": "published" }
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
`withWhere` filters child rows but does **not** drop parent rows that have no matching children. To drop parents, filter on the relation in the top-level `where` (e.g. `{"Post.status": "published"}`).
|
|
117
|
+
|
|
118
|
+
Supported relation `type` values when defining a model: `"one"` (belongs-to / one-to-one) and `"many"` (one-to-many or many-to-many via `through`).
|
|
119
|
+
|
|
120
|
+
### Inspecting queries (`dryRun`)
|
|
121
|
+
|
|
122
|
+
Add `dryRun: true` to any request to get back the SQL it **would** run,
|
|
123
|
+
without executing it. Validation still runs; the database is untouched.
|
|
124
|
+
|
|
125
|
+
```js
|
|
126
|
+
await korm.processRequest(
|
|
127
|
+
{ action: 'delete', where: { status: 'archived' }, dryRun: true },
|
|
128
|
+
'Post'
|
|
129
|
+
);
|
|
130
|
+
// → { success: true, dryRun: true, action: 'delete', model: 'Post',
|
|
131
|
+
// sql: 'delete from `posts` where `status` = ?', bindings: ['archived'],
|
|
132
|
+
// statements: [{ sql, bindings }] } // `sync` returns 2 statements
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Bindings come back as a separate array (never interpolated into `sql`).
|
|
136
|
+
|
|
137
|
+
### Errors (`KormError`)
|
|
138
|
+
|
|
139
|
+
`processRequest` throws a `KormError` (extends `Error`, so `e.message`
|
|
140
|
+
still works) with a machine-readable `code` you can branch on:
|
|
141
|
+
|
|
142
|
+
| `code` | When |
|
|
143
|
+
| ----------------------- | ---------------------------------------------------- |
|
|
144
|
+
| `NO_MATCHING_ROW` | A mutating action matched no row |
|
|
145
|
+
| `UNKNOWN_ACTION` | Action isn't built-in and has no custom hook |
|
|
146
|
+
| `NO_CUSTOM_ACTION_HOOK` | Custom action requested, no hook on the model |
|
|
147
|
+
| `VALIDATION_FAILED` | Input failed validation (`e.context.fields`) |
|
|
148
|
+
| `UNKNOWN_MODEL` | Model name not in the schema (`e.context.available`) |
|
|
149
|
+
| `FORBIDDEN` | A registered `authorize()` predicate denied the request (`e.context.model`/`action`) |
|
|
150
|
+
| `INTERNAL` | Internal invariant / misconfiguration |
|
|
151
|
+
|
|
152
|
+
```js
|
|
153
|
+
const { KormError } = require('@dreamtree-org/korm-js');
|
|
154
|
+
try {
|
|
155
|
+
await korm.processRequest({ action: 'updaet' }, 'User');
|
|
156
|
+
} catch (e) {
|
|
157
|
+
if (e instanceof KormError && e.code === 'UNKNOWN_ACTION') {
|
|
158
|
+
// e.context.closest → "update" (typo suggestion); e.toJSON() for HTTP
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### Discovery + tool schema (`describeSchema` / `getRequestJsonSchema`)
|
|
164
|
+
|
|
165
|
+
Two read-only helpers for agent integration:
|
|
166
|
+
|
|
167
|
+
- `korm.describeSchema()` / `korm.describeModel('User')` — pure-data
|
|
168
|
+
description of tables, typed columns, relations, soft-delete flag, and
|
|
169
|
+
available actions. Use it to discover what's queryable before building
|
|
170
|
+
a request. Throws `KormError` (`code: 'UNKNOWN_MODEL'`) for a bad name.
|
|
171
|
+
Pass a context — `korm.describeModel('User', ctx)` — and `actions` is
|
|
172
|
+
filtered to those the current context may call (see authorization).
|
|
173
|
+
- `korm.getRequestJsonSchema('User')` — draft-2020-12 JSON Schema for
|
|
174
|
+
every valid request body for that model (an `action`-discriminated
|
|
175
|
+
`oneOf`). Attach it to an OpenAI/Anthropic tool definition or use it
|
|
176
|
+
for client-side prevalidation:
|
|
177
|
+
|
|
178
|
+
```js
|
|
179
|
+
const ctx = korm.describeModel('User'); // discovery
|
|
180
|
+
const schema = korm.getRequestJsonSchema('User'); // request contract
|
|
181
|
+
// OpenAI: { type: 'function', function: { name, description, parameters: schema } }
|
|
182
|
+
// Anthropic:{ name, description, input_schema: schema }
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
## Canonical examples
|
|
186
|
+
|
|
187
|
+
### Read with filter + pagination
|
|
188
|
+
|
|
189
|
+
```js
|
|
190
|
+
await korm.processRequest(
|
|
191
|
+
{
|
|
192
|
+
action: 'list',
|
|
193
|
+
where: { is_active: true, age: '>=18' },
|
|
194
|
+
select: ['id', 'username', 'email'],
|
|
195
|
+
orderBy: { column: 'created_at', direction: 'desc' },
|
|
196
|
+
limit: 20,
|
|
197
|
+
offset: 0,
|
|
198
|
+
},
|
|
199
|
+
'User'
|
|
200
|
+
);
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
### Create
|
|
204
|
+
|
|
205
|
+
```js
|
|
206
|
+
await korm.processRequest(
|
|
207
|
+
{
|
|
208
|
+
action: 'create',
|
|
209
|
+
data: { username: 'john_doe', email: 'john@example.com', age: 30 },
|
|
210
|
+
},
|
|
211
|
+
'User'
|
|
212
|
+
);
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
### Update by relation
|
|
216
|
+
|
|
217
|
+
```js
|
|
218
|
+
await korm.processRequest(
|
|
219
|
+
{
|
|
220
|
+
action: 'update',
|
|
221
|
+
where: { 'User.id': 1 },
|
|
222
|
+
data: { status: 'active' },
|
|
223
|
+
with: ['User'],
|
|
224
|
+
},
|
|
225
|
+
'Profile'
|
|
226
|
+
);
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
### Nested eager-load
|
|
230
|
+
|
|
231
|
+
```js
|
|
232
|
+
await korm.processRequest(
|
|
233
|
+
{
|
|
234
|
+
action: 'list',
|
|
235
|
+
where: { 'User.is_active': true },
|
|
236
|
+
select: ['id', 'title', 'User.username'],
|
|
237
|
+
with: ['User', 'User.UserDetail', 'Comment'],
|
|
238
|
+
withWhere: { 'Comment.is_approved': true },
|
|
239
|
+
limit: 5,
|
|
240
|
+
},
|
|
241
|
+
'Post'
|
|
242
|
+
);
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
### Upsert
|
|
246
|
+
|
|
247
|
+
```js
|
|
248
|
+
await korm.processRequest(
|
|
249
|
+
{
|
|
250
|
+
action: 'upsert',
|
|
251
|
+
data: { email: 'a@b.com', name: 'Alice' },
|
|
252
|
+
conflict: ['email'],
|
|
253
|
+
},
|
|
254
|
+
'User'
|
|
255
|
+
);
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
## Rules for AI assistants helping consumers
|
|
259
|
+
|
|
260
|
+
1. **Use the JSON contract.** When the user asks for a query, return a KORM request object plus the `processRequest` call — not raw Knex chains.
|
|
261
|
+
2. **Never concatenate user input into SQL.** All filtering goes through `where` operators above.
|
|
262
|
+
3. **Multi-DB.** Assume the same request runs on MySQL, Postgres, and SQLite. If a feature's _semantics_ differ by engine, call it out — e.g. `replace` is a true delete+insert on MySQL/SQLite but a merge on Postgres (omitted columns are retained); prefer `upsert` for portable insert-or-update.
|
|
263
|
+
4. **Don't invent operators.** If the user needs something not in the operator table, use `where` with relation traversal, `having`, or `groupBy` — or tell the user the contract doesn't support it.
|
|
264
|
+
5. **Don't invent fields.** The top-level keys above are the entire contract surface. No `filter`, no `query`, no `params`.
|
|
265
|
+
6. **Soft delete is per-model.** `delete` becomes a soft-delete only if the model declares it; don't assume.
|
|
266
|
+
7. **Preview before mutating.** For a risky write, add `dryRun: true` first to inspect the SQL, then re-issue without it.
|
|
267
|
+
8. **Handle errors by `code`.** Catch `KormError` and branch on `e.code` (table above) rather than string-matching `e.message`.
|
|
268
|
+
9. **Refresh this doc** by re-running `npx @dreamtree-org/korm-js init --ai <provider>` when the library is upgraded.
|
package/bin/korm-mcp.js
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";const path=require("path"),{initializeKORM:initializeKORM}=require("../index"),{createServer:createServer}=require("../src/mcp/server"),{McpConfigError:McpConfigError}=require("../src/mcp/errors");function parseArgv(e){const r={config:null,help:!1};for(let o=2;o<e.length;o++){const t=e[o];"--help"===t||"-h"===t?r.help=!0:"--config"===t||"-c"===t?r.config=e[++o]:t.startsWith("--config=")?r.config=t.slice(9):(process.stderr.write(`korm-mcp: unknown argument "${t}"\n`),r.help=!0)}return r}function printHelp(){process.stderr.write("korm-mcp — Model Context Protocol server for @dreamtree-org/korm-js\n\nUsage:\n korm-mcp --config <path-to-config.js>\n\nOptions:\n -c, --config <path> Path to a Node CJS module exporting { db, dbClient, schema, mcp }.\n -h, --help Show this help.\n\nSee docs/agents/11-mcp-server.md for the config shape.\n")}function requireConfigModule(e){try{const r=require(e);return r&&r.default?r.default:r}catch(r){throw new McpConfigError(`Failed to load config at ${e}: ${r.message}`)}}function assertConfigFields(e,r){if(!e||"object"!=typeof e)throw new McpConfigError(`Config at ${r} must export an object (got ${typeof e}).`);const o=["db","dbClient","schema"];for(const r of o)if(!e[r])throw new McpConfigError(`Config: \`${r}\` is required.`);if(!e.mcp||"object"!=typeof e.mcp)throw new McpConfigError("Config: `mcp` object is required (see spec §6).")}function loadConfig(e){if(!e)throw new McpConfigError("--config is required. See `korm-mcp --help`.");const r=path.resolve(process.cwd(),e),o=requireConfigModule(r);return assertConfigFields(o,r),o}const stderrLogger={info:(...e)=>process.stderr.write(`[korm-mcp] ${e.join(" ")}\n`),error:(...e)=>process.stderr.write(`[korm-mcp:error] ${e.join(" ")}\n`)};function installShutdownHandlers(e){const r=async r=>{stderrLogger.info(`received ${r}, shutting down`);try{await e.stop()}catch(e){stderrLogger.error(`stop error: ${e.message}`)}process.exit(0)};process.on("SIGINT",()=>r("SIGINT")),process.on("SIGTERM",()=>r("SIGTERM"))}async function main(e=process.argv){const r=parseArgv(e);let o;r.help&&(printHelp(),process.exit(0));try{o=loadConfig(r.config)}catch(e){process.stderr.write(`korm-mcp: ${e.message}\n`),process.exit(2)}const t=initializeKORM({db:o.db,dbClient:o.dbClient,schema:o.schema,resolverPath:o.resolverPath||null,debug:o.debug||!1}),n=require("../package.json"),s=createServer({controller:t,schema:o.schema,mcpConfig:o.mcp,packageInfo:{name:n.name,version:n.version}});installShutdownHandlers(s);try{await s.start({logger:stderrLogger}),stderrLogger.info(`started; ${s.tools.length} tools exposed (mode=${o.mcp.mode||"ro"})`)}catch(e){stderrLogger.error(`failed to start: ${e.message}`),process.exit(1)}}require.main===module&&main().catch(e=>{process.stderr.write(`korm-mcp: fatal: ${e.message}\n`),process.exit(1)}),module.exports={parseArgv:parseArgv,loadConfig:loadConfig,main:main};
|
package/build.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
const fs=require("fs"),path=require("path"),{execSync:execSync}=require("child_process");function progressBar(e,s,i=30){const o=e/s,n=Math.round(i*o),t=i-n,
|
|
2
|
+
const fs=require("fs"),path=require("path"),{execSync:execSync}=require("child_process"),FILE_ASSETS=["README.md","LICENSE","index.d.ts"],DIR_ASSETS=["templates","ai-skills"];function progressBar(e,s,i=30){const o=e/s,n=Math.round(i*o),t=i-n,r="█".repeat(n)+"-".repeat(t);process.stdout.write(`\r[${r}] ${(100*o).toFixed(1)}% (${e}/${s})`),e===s&&process.stdout.write("\n")}function getAllJsFiles(e,s=["node_modules","dist","test"]){let i=[];return fs.readdirSync(e).forEach(o=>{const n=path.join(e,o),t=fs.statSync(n);t&&t.isDirectory()?s.includes(o)||(i=i.concat(getAllJsFiles(n,s))):o.endsWith(".js")&&i.push(n)}),i}function ensureDir(e){fs.existsSync(e)||(fs.mkdirSync(e,{recursive:!0}),console.log(`✅ Created directory: ${e}`))}function minifyFile(e,s){try{return execSync(`npx terser "${e}" -o "${s}" --compress --mangle --comments false`,{stdio:"pipe"}),!0}catch(i){return fs.copyFileSync(e,s),!1}}function copyFile(e,s){fs.copyFileSync(e,s)}function copyDirectory(e,s){if(!fs.existsSync(e))return;ensureDir(s);fs.readdirSync(e,{withFileTypes:!0}).forEach(i=>{const o=path.join(e,i.name),n=path.join(s,i.name);i.isDirectory()?copyDirectory(o,n):copyFile(o,n)})}async function build(){console.log("🚀 KORM Build: Minifying all JS files to dist/ with progress bar\n"),fs.existsSync("dist")&&(fs.rmSync("dist",{recursive:!0,force:!0}),console.log("✅ Cleaned dist/")),ensureDir("dist");const e=getAllJsFiles(".",["node_modules","dist","test"]),s=e.length;let i=0,o=0,n=0;e.forEach((e,t)=>{const r=path.relative(".",e),c=path.join("dist",r);ensureDir(path.dirname(c));const l=fs.statSync(e).size,d=(minifyFile(e,c),fs.statSync(c).size);o+=l,n+=d,i++,progressBar(i,s)}),FILE_ASSETS.forEach(e=>{fs.existsSync(e)&&(copyFile(e,path.join("dist",e)),console.log(`✅ Copied: ${e}`))}),DIR_ASSETS.forEach(e=>{fs.existsSync(e)&&(copyDirectory(e,path.join("dist",e)),console.log(`✅ Copied ${e}/ to dist/`))}),fs.existsSync("node_modules")&&(execSync("cp -r node_modules dist/",{stdio:"pipe"}),console.log("✅ Copied node_modules/ to dist/"));const t=path.join(".","version-manager.js");let r=!1,c=null;if(!("1"===process.env.BUILD_SKIP_VERSION_BUMP)&&fs.existsSync(t))try{const e=new(require("./version-manager"));"function"==typeof e.smartAutoIncrement&&(c=await e.smartAutoIncrement(),r=!0,console.log(`✅ Version updated using version-manager.js: ${c}`))}catch(e){console.warn("⚠️ Could not update version using version-manager.js:",e.message)}if(fs.existsSync("package.json")){const e=JSON.parse(fs.readFileSync("package.json","utf8"));e.scripts&&(delete e.scripts.build,delete e.scripts.clean,delete e.scripts.minify,delete e.scripts["minify:js"],delete e.scripts.prepublishOnly),delete e.devDependencies,e.main="index.js",delete e.files,r&&c&&("string"==typeof c?(e.version=c,console.log(`✅ Set version in dist/package.json: ${c}`)):(console.warn(`⚠️ newVersion is not a string: ${typeof c} - ${JSON.stringify(c)}`),e.version=e.version||"1.0.0")),fs.writeFileSync(path.join("dist","package.json"),JSON.stringify(e,null,2)),console.log("✅ Created dist/package.json")}const l=((o-n)/o*100).toFixed(1);console.log("\n📊 Build Statistics:"),console.log(` JS files processed: ${s}`),console.log(` Original size: ${(o/1024).toFixed(1)} KB`),console.log(` Minified size: ${(n/1024).toFixed(1)} KB`),console.log(` Size reduction: ${l}%`),r&&c&&console.log(` New version: ${c}`),console.log("\n🎉 Build completed! Output in dist/")}require.main===module&&build().catch(console.error),module.exports={build:build,FILE_ASSETS:FILE_ASSETS,DIR_ASSETS:DIR_ASSETS};
|
package/cli.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
"use strict";const fs=require("fs"),path=require("path"),BEGIN="\x3c!-- BEGIN korm-js skill (auto-generated — re-run `npx @dreamtree-org/korm-js init --ai <provider>` to refresh) --\x3e",END="\x3c!-- END korm-js skill --\x3e",PROVIDERS={claude:{path:"CLAUDE.md",mode:"block"},openai:{path:"AGENTS.md",mode:"block"},gemini:{path:"GEMINI.md",mode:"block"},copilot:{path:".github/copilot-instructions.md",mode:"block"},kiro:{path:".kiro/steering/korm-js.md",mode:"file"},windsurf:{path:".windsurf/rules/korm-js.md",mode:"file"},cursor:{path:".cursor/rules/korm-js.mdc",mode:"file",frontmatter:"---\ndescription: KORM-JS request contract reference for @dreamtree-org/korm-js\nalwaysApply: false\n---\n\n"}},PROVIDER_ALIASES={"claude-code":"claude",codex:"openai","github-copilot":"copilot"};function parseArgs(e){const r={_:[],flags:{}};for(let o=0;o<e.length;o++){const n=e[o];if(n.startsWith("--")){const
|
|
2
|
+
"use strict";const fs=require("fs"),path=require("path"),BEGIN="\x3c!-- BEGIN korm-js skill (auto-generated — re-run `npx @dreamtree-org/korm-js init --ai <provider>` to refresh) --\x3e",END="\x3c!-- END korm-js skill --\x3e",PROVIDERS={claude:{path:"CLAUDE.md",mode:"block"},openai:{path:"AGENTS.md",mode:"block"},gemini:{path:"GEMINI.md",mode:"block"},copilot:{path:".github/copilot-instructions.md",mode:"block"},kiro:{path:".kiro/steering/korm-js.md",mode:"file"},windsurf:{path:".windsurf/rules/korm-js.md",mode:"file"},cursor:{path:".cursor/rules/korm-js.mdc",mode:"file",frontmatter:"---\ndescription: KORM-JS request contract reference for @dreamtree-org/korm-js\nalwaysApply: false\n---\n\n"}},PROVIDER_ALIASES={"claude-code":"claude",codex:"openai","github-copilot":"copilot"};function parseArgs(e){const r={_:[],flags:{}};for(let o=0;o<e.length;o++){const n=e[o];if(n.startsWith("--")){const t=n.slice(2),s=e[o+1];s&&!s.startsWith("--")?(r.flags[t]=s,o++):r.flags[t]=!0}else r._.push(n)}return r}function usage(){return["Usage:"," npx @dreamtree-org/korm-js init --ai <provider> [--cwd <dir>] [--force] [--dry-run]"," npx @dreamtree-org/korm-js mcp --config <path-to-config.js>","","Commands:"," init Install the KORM-JS AI-assistant skill into your project."," mcp Start the Model Context Protocol server (wraps `korm-mcp`).","","Providers:"," claude -> CLAUDE.md (block insert)"," openai -> AGENTS.md (block insert)"," gemini -> GEMINI.md (block insert)"," copilot -> .github/copilot-instructions.md (block insert)"," kiro -> .kiro/steering/korm-js.md"," windsurf -> .windsurf/rules/korm-js.md"," cursor -> .cursor/rules/korm-js.mdc","","Use --ai all to install every provider in one go."].join("\n")}function readSkillBody(){const e=path.join(__dirname,"ai-skills","korm-js.md");if(!fs.existsSync(e))throw new Error("Skill source missing at "+e+" — reinstall @dreamtree-org/korm-js.");return fs.readFileSync(e,"utf8")}function ensureDir(e){const r=path.dirname(e);r&&"."!==r&&!fs.existsSync(r)&&fs.mkdirSync(r,{recursive:!0})}function writeBlock(e,r,o){const n=BEGIN+"\n\n"+r.trim()+"\n\n"+END+"\n";let t,s;if(fs.existsSync(e)){const r=fs.readFileSync(e,"utf8"),o=r.indexOf(BEGIN),i=r.indexOf(END);if(-1!==o&&-1!==i&&i>o){const e=r.slice(0,o).replace(/\s+$/,""),c=r.slice(i+26).replace(/^\s+/,"");t=(e?e+"\n\n":"")+n+(c?"\n"+c:""),s="updated"}else{t=r.replace(/\s+$/,"")+"\n\n"+n,s="appended"}}else t=n,s="created";return o.dryRun?{action:s+" (dry-run)",path:e}:(ensureDir(e),fs.writeFileSync(e,t,"utf8"),{action:s,path:e})}function writeFile(e,r,o,n){const t=(o||"")+r;let s;if(fs.existsSync(e)){if(!n.force)return{action:"skipped (exists; pass --force to overwrite)",path:e};s="overwritten"}else s="created";return n.dryRun?{action:s+" (dry-run)",path:e}:(ensureDir(e),fs.writeFileSync(e,t,"utf8"),{action:s,path:e})}function installFor(e,r,o,n){const t=PROVIDERS[e],s=path.join(o,t.path);return"block"===t.mode?writeBlock(s,r,n):writeFile(s,r,t.frontmatter,n)}function resolveProvider(e){if(!e)return null;const r=String(e).toLowerCase();return"all"===r?"all":PROVIDERS[r]?r:PROVIDER_ALIASES[r]?PROVIDER_ALIASES[r]:null}function runInit(e){const r=e.flags.ai;r&&!0!==r||(console.error("Missing --ai <provider>.\n"),console.error(usage()),process.exit(1));const o=resolveProvider(r);o||(console.error("Unknown provider: "+r),console.error("Known: "+Object.keys(PROVIDERS).join(", ")+", all"),process.exit(1));const n=e.flags.cwd?path.resolve(String(e.flags.cwd)):process.cwd(),t={force:Boolean(e.flags.force),dryRun:Boolean(e.flags["dry-run"])},s=readSkillBody(),i=("all"===o?Object.keys(PROVIDERS):[o]).map(function(e){try{const r=installFor(e,s,n,t);return{provider:e,ok:!0,action:r.action,path:r.path}}catch(r){return{provider:e,ok:!1,error:r.message}}}),c=i.filter(function(e){return!e.ok});for(const e of i)e.ok?console.log(" ["+e.provider.padEnd(8)+"] "+e.action+" -> "+path.relative(n,e.path)):console.error(" ["+e.provider.padEnd(8)+"] FAILED: "+e.error);console.log(""),console.log(t.dryRun?"Dry run complete. No files were written.":"KORM-JS skill installed."),c.length&&process.exit(1)}function runMcp(e){const{main:r}=require("./bin/korm-mcp");return Promise.resolve().then(()=>r(["node","korm-mcp",...e])).catch(e=>{console.error("korm-mcp: fatal: "+(e&&e.message?e.message:e)),process.exit(1)})}function main(){const e=process.argv.slice(2),r=parseArgs(e),o=r._[0];if(o&&"--help"!==o&&"-h"!==o&&"help"!==o)if("init"!==o){if("mcp"===o)return runMcp(e.slice(e.indexOf("mcp")+1));console.error("Unknown command: "+o+"\n"),console.error(usage()),process.exit(1)}else runInit(r);else console.log(usage())}require.main===module&&main(),module.exports={PROVIDERS:PROVIDERS,PROVIDER_ALIASES:PROVIDER_ALIASES,parseArgs:parseArgs,resolveProvider:resolveProvider,BEGIN:BEGIN,END:END,usage:usage,readSkillBody:readSkillBody,ensureDir:ensureDir,writeBlock:writeBlock,writeFile:writeFile,installFor:installFor,runInit:runInit,runMcp:runMcp,main:main};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const logger=require("../Logger"),ENGINE_WARNINGS=new Set;function warnOnce(e,t){ENGINE_WARNINGS.has(e)||(ENGINE_WARNINGS.add(e),logger.warn(t))}const BASE_TYPE_DISPATCHER={VARCHAR:(e,t,n)=>e.string(t,n.size||255),CHAR:(e,t,n)=>e.string(t,n.size||255),TEXT:(e,t)=>e.text(t),MEDIUMTEXT:(e,t)=>e.text(t),LONGTEXT:(e,t)=>e.text(t),INT:(e,t)=>e.integer(t),INTEGER:(e,t)=>e.integer(t),MEDIUMINT:(e,t)=>e.integer(t),SMALLINT:(e,t)=>e.integer(t),BIGINT:(e,t)=>e.bigInteger(t),TINYINT:(e,t,n)=>e.tinyint?e.tinyint(t):e.specificType(t,n.size?`TINYINT(${n.size})`:"TINYINT"),BOOLEAN:(e,t)=>e.boolean(t),BOOL:(e,t)=>e.boolean(t),DATE:(e,t)=>e.date(t),DATETIME:(e,t)=>e.dateTime(t),TIMESTAMP:(e,t)=>e.timestamp(t),TIME:(e,t)=>e.time(t),JSON:(e,t)=>e.json(t),FLOAT:(e,t)=>e.float(t),DOUBLE:(e,t)=>e.double?e.double(t):e.float(t),REAL:(e,t)=>e.double?e.double(t):e.float(t),DECIMAL:(e,t)=>e.decimal(t),NUMERIC:(e,t)=>e.decimal(t),BINARY:(e,t)=>e.binary(t),VARBINARY:(e,t)=>e.binary(t),BLOB:(e,t)=>e.binary(t),UUID:(e,t)=>e.uuid?e.uuid(t):e.string(t,36)},COLUMN_STRING_SUFFIXES=[e=>e.size?`|size:${e.size}`:"",e=>e.isUnsigned?"|unsigned":"",e=>e.primary?"|primaryKey":"",e=>e.autoIncrement?"|autoIncrement":"",e=>e.nullable?"":"|notNull",e=>e.unique?"|unique":"",e=>null!=e.default&&""!==e.default?`|default:${e.default}`:"",e=>e.onUpdate?`|onUpdate:${e.onUpdate}`:"",e=>e.comment?`|comment:${e.comment}`:"",e=>e.hasForeignKey&&e.foreignMapTables?.[0]?`|foreignKey:${e.foreignMapTables[0].table}:${e.foreignMapTables[0].column}`:""];class BaseSyncTable{constructor(e,t,n=null){this.db=e,this.utils=t,this.controllerWrapper=n}_getClientName(){throw new Error("_getClientName must be overridden by engine subclass")}async existsTable(e){return this.db.schema.hasTable(e)}async syncTable(e){if(await this.existsTable(e.table)){const t=await this.getAlterations(e);await this.alterTable(e.table,t)}else await this.createTable(e);await this._applyExtras(e)}async syncDatabase(){if(!this.controllerWrapper?.schema)throw new Error("controllerWrapper.schema not set.");const e=this.controllerWrapper.schema;for(const t of Object.keys(e))await this.syncTable(e[t]),await this.syncSeedData(e[t],t);logger.info("Database synced by SyncTable...")}async syncSeedData(e,t){if(!e.seed||!Array.isArray(e.seed)||0===e.seed.length)return;const n=await this.db(e.table).count("* as n").first();Number(n?.n)>0?logger.info("Seed data already synced for",t):(await this.db(e.table).insert(e.seed),logger.info("Seed data synced for",t))}async generateSchema(){const e=await this._listTables(),t={},n=this._getHelperUtility();for(const r of e){const e=n?n.modelName(r):r;t[e]={table:r,alias:e,modelName:e,columns:this.getColumnString(await this.getCurrentColumns(r)),seed:[],hasRelations:await this._getRelations(r),indexes:[]}}return t}async createTable(e){await this.db.schema.createTable(e.table,t=>{for(const[n,r]of Object.entries(e.columns))this._applyColumnToBuilder(t,this._resolveColumnFrm(n,r))})}async alterTable(e,t){if(!t||"object"!=typeof t)throw new Error("alterations must be an object");(t.add?.length||0)+(t.drop?.length||0)+(t.modify?.length||0)>0?await this.db.schema.alterTable(e,e=>{for(const n of t.add||[])this._applyColumnToBuilder(e,n);for(const n of t.drop||[])e.dropColumn(n.name);for(const n of t.modify||[]){const t=this._applyColumnToBuilder(e,n);t&&"function"==typeof t.alter&&t.alter()}}):logger.info("No alterations to apply for",e)}async dropTable(e){await this.db.schema.dropTableIfExists(e)}async getCurrentColumns(e){const t=await this.db(e).columnInfo(),n={};for(const[e,r]of Object.entries(t))n[e]=this._formatColumnInfo(e,r);return n}_formatColumnInfo(e,t){const n=String(t.type||"").toLowerCase(),r=n.match(/^([a-z_]+)(?:\((\d+)(?:,\s*\d+)?\))?/);return{name:e,type:(r?r[1]:n).toUpperCase(),size:(r&&r[2]?Number(r[2]):t.maxLength||null)||null,nullable:!1!==t.nullable,default:this._parseDefault(t.defaultValue),primary:!1,unique:!1,autoIncrement:!1,isUnsigned:!1,hasForeignKey:!1,foreignMapTables:[],onUpdate:null,comment:""}}_parseDefault(e){if(null==e)return null;const t=String(e).trim();return""===t?null:t.replace(/^'+|'+$/g,"")}hasColumnChanged(e,t){return!1}async getAlterations(e){const t={add:[],drop:[],modify:[]},n=await this.getCurrentColumns(e.table);for(const[r,a]of Object.entries(e.columns)){const e=this._resolveColumnFrm(r,a),s=n[r];s?this.hasColumnChanged(s,e)&&t.modify.push(e):t.add.push(e)}for(const r of Object.keys(n))e.columns[r]||t.drop.push({name:r});return t}getColumnString(e){return Object.keys(e).reduce((t,n)=>{const r=e[n],a=String(r.type||"").toLowerCase();return t[n]=COLUMN_STRING_SUFFIXES.reduce((e,t)=>e+t(r),a),t},{})}_resolveColumnFrm(e,t){const n="string"==typeof t?this.utils.formatColumnSchema(e,t):t;return this._alignForeignKeyType(n)}_alignForeignKeyType(e){if(!e?.hasForeignKey)return e;const t=e.foreignMapTables?.[0];if(!t?.table)return e;const n=this._resolveParentColumnFrm(t);return n?{...e,...this._matchedForeignKeyType(n)}:e}_resolveParentColumnFrm(e){const t=this._findSchemaColumns(e.table);if(!t)return null;const n=t[e.column||"id"];return null==n?null:"string"==typeof n?this.utils.formatColumnSchema(e.column||"id",n):n}_findSchemaColumns(e){const t=this.controllerWrapper?.schema;if(!t)return null;for(const n of Object.keys(t)){const r=t[n];if(r&&(r.table===e||n===e))return r.columns||null}return null}_matchedForeignKeyType(e){return e.autoIncrement&&e.primary?{type:"BIGINT",size:null,columnType:"BIGINT",isUnsigned:!0}:{type:e.type,size:e.size,columnType:e.columnType,isUnsigned:e.isUnsigned}}_applyColumnToBuilder(e,t){if(t.autoIncrement&&t.primary)return this._buildIncrementsColumn(e,t);const n=this._typeBuilder(e,t.name,t);return this._applyColumnModifiers(n,t),n}_buildIncrementsColumn(e,t){const n=e.increments(t.name);return t.comment&&n.comment(t.comment),n}_applyColumnModifiers(e,t){if(this._applyConstraintModifiers(e,t),this._applyNullabilityAndDefault(e,t),t.comment&&e.comment(t.comment),t.hasForeignKey&&t.foreignMapTables?.[0]){const n=t.foreignMapTables[0];e.references(n.column||"id").inTable(n.table)}}_applyConstraintModifiers(e,t){t.primary&&e.primary(),t.unique&&e.unique(),t.isUnsigned&&this._supportsUnsigned()&&e.unsigned()}_applyNullabilityAndDefault(e,t){t.nullable?e.nullable():e.notNullable(),null!=t.default&&""!==t.default&&e.defaultTo(this._renderDefault(t.default))}_typeBuilder(e,t,n){const r=String(n.type||"").toUpperCase(),a=this._typeDispatcher()[r];return a?a(e,t,n):e.specificType(t,n.columnType||(n.size?`${r}(${n.size})`:r))}_typeDispatcher(){return BASE_TYPE_DISPATCHER}_renderDefault(e){const t=String(e).trim();return"CURRENT_TIMESTAMP"===t.toUpperCase()||"NOW()"===t.toUpperCase()?this.db.fn.now():/^-?\d+(\.\d+)?$/.test(t)?Number(t):"true"===t||"false"===t?"true"===t:t}_supportsUnsigned(){return!0}_getHelperUtility(){try{return new(require(`./${this._getClientName()}/HelperUtility`))}catch{return null}}async _applyExtras(e){}async _getRelations(e){return{}}async _listTables(){throw new Error("_listTables must be overridden by engine subclass")}_warnOnUnsupportedModifier(e,t,n){warnOnce(`${this._getClientName()}.${e}`,`[${this._getClientName()}] '${e}' modifier is not supported on this engine (seen on ${t}.${n}). See docs/agents/05-multi-db-parity.md.`)}}module.exports=BaseSyncTable;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
const{getDefaultTypeSize:getDefaultTypeSize,getDbType:getDbType}=require("./DataTypeMap");class BaseUtility{getModel(e,t){const n=e?.schema;let r=null;const l=Object.keys(n);if(l.forEach(e=>{e===t&&(r=n[e])}),r||l.forEach(e=>{const l=n[e];l.table===t&&(r=l)}),r)return{...r,name:t,columns:Object.keys(r.columns).map(e=>{const t=r.columns[e];return this.parseColumnString(e,t)})};throw
|
|
1
|
+
const{getDefaultTypeSize:getDefaultTypeSize,getDbType:getDbType}=require("./DataTypeMap"),KormError=require("../../KormError");class BaseUtility{getModel(e,t){const n=e?.schema;let r=null;const l=Object.keys(n);if(l.forEach(e=>{e===t&&(r=n[e])}),r||l.forEach(e=>{const l=n[e];l.table===t&&(r=l)}),r)return{...r,name:t,columns:Object.keys(r.columns).map(e=>{const t=r.columns[e];return this.parseColumnString(e,t)})};throw KormError.unknownModel({model:t,available:Object.keys(n||{})})}map2DbDefault(e){switch(e){case"now":case"now()":return"CURRENT_TIMESTAMP";default:return e}}getCollenedValue(e,{splitChar:t=":",give:n="right",trimWrap:r="{}"}={}){if(null==e)return null;const l=String(e),i=l.indexOf(t),a=e=>{if(!r)return e;if("string"==typeof r&&2===r.length){const[t,n]=r;return e.startsWith(t)&&e.endsWith(n)?e.slice(1,-1):e}return"string"==typeof r&&r.length&&e.startsWith(r)&&e.endsWith(r)?e.slice(r.length,-r.length):e};if(i>=0){const e=l.slice(0,i).trim(),r=l.slice(i+t.length).trim();return"both"===n?{left:a(e),right:a(r)}:a("left"===n?e:r)}return a(l)}parseForeignKey(e){if(!e)return{foreignMapTables:[]};const t=this.getCollenedValue(e),[n,r="id"]=String(t||"").split(":"),l=(this.getCollenedValue(n)||"").split(",").filter(Boolean),i=(this.getCollenedValue(r)||"").split(",").filter(Boolean);return{foreignMapTables:l.map((e,t)=>({table:e,column:i[t]||i[0]||"id"}))}}parseColumnString(e,t){const[n,...r]=String(t).split("|"),l=e=>r.find(t=>t.startsWith(e+":")),i=e=>r.join("|").includes(e),a=e=>e&&e.includes(":")?e.slice(e.indexOf(":")+1):null,s=l("default"),o=l("onUpdate"),u=l("comment"),m=l("foreignKey"),c=l("size"),g=this.parseForeignKey(m),p=!!m,E=getDbType(n,c?a(c):null),f=c?a(c):getDefaultTypeSize(E);return{name:e,type:E,size:f,isUnsigned:i("unsigned")||i("primaryKey")||p,columnType:`${n}${f?`(${f})`:""}`,nullable:!i("notNull"),primary:i("primaryKey"),autoIncrement:i("autoIncrement"),unique:i("unique"),default:this.map2DbDefault(a(s)),onUpdate:this.map2DbDefault(a(o)),comment:a(u)||"",hasForeignKey:p,...g}}formatColumnSchema(e,t){return this.parseColumnString(e,t)}formatColumnDef(e,t){return{name:e,type:t.DATA_TYPE,size:t.CHARACTER_MAXIMUM_LENGTH||getDefaultTypeSize(t.DATA_TYPE),isUnsigned:String(t.COLUMN_TYPE||"").toLowerCase().includes("unsigned"),columnType:t.COLUMN_TYPE,nullable:"YES"===t.IS_NULLABLE,primary:"PRI"===t.COLUMN_KEY,unique:"UNI"===t.COLUMN_KEY,autoIncrement:String(t.EXTRA||"").toLowerCase().includes("auto_increment"),hasForeignKey:"MUL"===t.COLUMN_KEY,comment:t.COMMENT,default:t.COLUMN_DEFAULT,onUpdate:((e="")=>{const t=String(e).match(/on update\s+([a-zA-Z_]+)/i);return t?t[1]:null})(t.EXTRA),foreignMapTables:"MUL"===t.COLUMN_KEY&&t.REFERENCED_TABLE_NAME?[{table:t.REFERENCED_TABLE_NAME,column:t.REFERENCED_COLUMN_NAME}]:[]}}escapeComment(e){return null==e?"":String(e).replace(/'/g,"\\'")}getDefaultTypeSize(e){return getDefaultTypeSize(e)}}module.exports=BaseUtility;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
const QueryService=require("./QueryService"),HookService=require("./HookService");class CurdTable{constructor(e,r,t=null){if(this.db=e,this.utils=r,this.controllerWrapper=t,this.hookService=new HookService(e,r,t),this.queryService=new QueryService(e,r,t),!this.queryService)throw new
|
|
1
|
+
const QueryService=require("./QueryService"),HookService=require("./HookService"),KormError=require("../../KormError");class CurdTable{constructor(e,r,t=null){if(this.db=e,this.utils=r,this.controllerWrapper=t,this.hookService=new HookService(e,r,t),this.queryService=new QueryService(e,r,t),!this.queryService)throw new KormError("CurdTable requires queryService (execute*Query / getQuery).",{code:KormError.CODES.INTERNAL})}async processRequest(e,r=null,t={}){const o=this.controllerWrapper,s=this.utils.getModel(this.controllerWrapper,r),c=e?.action||"list";let i=null;const a={model:s,action:c,request:e,ctx:t,controller:o};if(this.hookService?.executeValidatorHook&&await this.hookService.executeValidatorHook({...a}),e?.dryRun)return this.buildDryRunResult(s,e,c);switch(this.hookService?.executeBeforeHook&&(e.beforeActionData=await this.hookService.executeBeforeHook({...a})),c){case"count":i=await this.queryService.executeCountQuery(s,e);break;case"sum":i=await this.queryService.executeSumQuery(s,e);break;case"list":i=await this.hookService.executeHasSoftDeleteHook(s)?await this.queryService.getSoftDeleteQuery(s,e):await this.queryService.getQuery(s,e);break;case"show":i=await this.queryService.executeShowQuery(s,e);break;case"create":i=await this.queryService.executeCreateQuery(s,e);break;case"update":{const r=await this.queryService.executeUpdateQuery(s,e);if(!r)throw KormError.noMatchingRow({action:c,model:s.name});i={message:"Record updated successfully",data:r,success:!0};break}case"replace":if(i=await this.queryService.executeReplaceQuery(s,e),!i)throw KormError.noMatchingRow({action:c,model:s.name});i={message:"Record replaced successfully",data:i,success:!0};break;case"upsert":if(i=await this.queryService.executeUpsertQuery(s,e),!i)throw KormError.noMatchingRow({action:c,model:s.name});i={message:"Record upserted successfully",data:i,success:!0};break;case"sync":if(i=await this.queryService.executeSyncQuery(s,e),!i)throw KormError.noMatchingRow({action:c,model:s.name});i={message:"Record synced successfully",data:i,success:!0};break;case"delete":{let r=null;if(r=await this.hookService.executeHasSoftDeleteHook(s)?await this.queryService.executeSoftDeleteQuery(s,e):await this.queryService.executeDeleteQuery(s,e),!r)throw KormError.noMatchingRow({action:c,model:s.name});i={message:"Record deleted successfully",data:r,success:!0};break}default:if(!this.hookService?.executeCustomAction)throw KormError.unknownAction({action:c,model:s?.name});i=await this.hookService.executeCustomAction({...a})}if(this.hookService?.executeAfterHook&&(i=await this.hookService.executeAfterHook({...a,data:i})),e?.other_requests&&"object"==typeof e.other_requests){const r={},o=Object.entries(e.other_requests);for(const[e,s]of o)Array.isArray(s)?r[e]=await Promise.all(s.map(r=>this.processRequest(r,e,t))):r[e]=await this.processRequest(s,e,t);i.other_responses=r}return i}async buildDryRunResult(e,r,t){if(!["list","show","count","sum","create","update","replace","upsert","sync","delete"].includes(t))throw KormError.unknownAction({action:t,model:e?.name});let o=t,s=r;const c=await this.hookService.executeHasSoftDeleteHook(e);return c&&"delete"===t?o="softDelete":c&&"list"===t&&(s={...r,where:{...r.where||{},deleted_at:null}}),this.queryService.buildDryRun(e,s,o)}}module.exports=CurdTable;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
const DataTypeMap=[{type:"number",maxSize:1,dbType:"tinyint"},{type:"number",maxSize:2,dbType:"smallint"},{type:"number",maxSize:3,dbType:"mediumint"},{type:"number",maxSize:4,dbType:"int"},{type:"number",maxSize:8,dbType:"bigint"},{type:"number",maxSize:null,dbType:"decimal"},{type:"number",maxSize:null,dbType:"numeric"},{type:"number",maxSize:null,dbType:"float"},{type:"number",maxSize:null,dbType:"double"},{type:"number",maxSize:null,dbType:"real"},{type:"number",maxSize:null,dbType:"bit"},{type:"string",maxSize:255,dbType:"varchar"},{type:"string",maxSize:1,dbType:"char"},{type:"string",maxSize:65535,dbType:"text"},{type:"string",maxSize:255,dbType:"tinytext"},{type:"string",maxSize:65535,dbType:"text"},{type:"string",maxSize:16777215,dbType:"mediumtext"},{type:"string",maxSize:4294967295,dbType:"longtext"},{type:"string",maxSize:255,dbType:"enum"},{type:"string",maxSize:255,dbType:"set"},{type:"string",maxSize:null,dbType:"json"},{type:"string",maxSize:36,dbType:"uuid"},{type:"buffer",maxSize:255,dbType:"binary"},{type:"buffer",maxSize:255,dbType:"varbinary"},{type:"buffer",maxSize:255,dbType:"tinyblob"},{type:"buffer",maxSize:65535,dbType:"blob"},{type:"buffer",maxSize:16777215,dbType:"mediumblob"},{type:"buffer",maxSize:4294967295,dbType:"longblob"},{type:"boolean",maxSize:null,dbType:"tinyint(1)"},{type:"boolean",maxSize:null,dbType:"boolean"},{type:"date",maxSize:null,dbType:"date"},{type:"date",maxSize:null,dbType:"datetime"},{type:"date",maxSize:null,dbType:"timestamp"},{type:"date",maxSize:null,dbType:"time"},{type:"date",maxSize:null,dbType:"year"},{type:"string",maxSize:null,dbType:"geometry"},{type:"string",maxSize:null,dbType:"point"},{type:"string",maxSize:null,dbType:"linestring"},{type:"string",maxSize:null,dbType:"polygon"},{type:"string",maxSize:null,dbType:"multipoint"},{type:"string",maxSize:null,dbType:"multilinestring"},{type:"string",maxSize:null,dbType:"multipolygon"},{type:"string",maxSize:null,dbType:"geometrycollection"},{type:"string",maxSize:null,dbType:"USER-DEFINED"}];function getDbType(e,t,p){if(p){const
|
|
1
|
+
const DataTypeMap=[{type:"number",maxSize:1,dbType:"tinyint"},{type:"number",maxSize:2,dbType:"smallint"},{type:"number",maxSize:3,dbType:"mediumint"},{type:"number",maxSize:4,dbType:"int"},{type:"number",maxSize:8,dbType:"bigint"},{type:"number",maxSize:null,dbType:"decimal"},{type:"number",maxSize:null,dbType:"numeric"},{type:"number",maxSize:null,dbType:"float"},{type:"number",maxSize:null,dbType:"double"},{type:"number",maxSize:null,dbType:"real"},{type:"number",maxSize:null,dbType:"bit"},{type:"string",maxSize:255,dbType:"varchar"},{type:"string",maxSize:1,dbType:"char"},{type:"string",maxSize:65535,dbType:"text"},{type:"string",maxSize:255,dbType:"tinytext"},{type:"string",maxSize:65535,dbType:"text"},{type:"string",maxSize:16777215,dbType:"mediumtext"},{type:"string",maxSize:4294967295,dbType:"longtext"},{type:"string",maxSize:255,dbType:"enum"},{type:"string",maxSize:255,dbType:"set"},{type:"string",maxSize:null,dbType:"json"},{type:"string",maxSize:36,dbType:"uuid"},{type:"buffer",maxSize:255,dbType:"binary"},{type:"buffer",maxSize:255,dbType:"varbinary"},{type:"buffer",maxSize:255,dbType:"tinyblob"},{type:"buffer",maxSize:65535,dbType:"blob"},{type:"buffer",maxSize:16777215,dbType:"mediumblob"},{type:"buffer",maxSize:4294967295,dbType:"longblob"},{type:"boolean",maxSize:null,dbType:"tinyint(1)"},{type:"boolean",maxSize:null,dbType:"boolean"},{type:"date",maxSize:null,dbType:"date"},{type:"date",maxSize:null,dbType:"datetime"},{type:"date",maxSize:null,dbType:"timestamp"},{type:"date",maxSize:null,dbType:"time"},{type:"date",maxSize:null,dbType:"year"},{type:"string",maxSize:null,dbType:"geometry"},{type:"string",maxSize:null,dbType:"point"},{type:"string",maxSize:null,dbType:"linestring"},{type:"string",maxSize:null,dbType:"polygon"},{type:"string",maxSize:null,dbType:"multipoint"},{type:"string",maxSize:null,dbType:"multilinestring"},{type:"string",maxSize:null,dbType:"multipolygon"},{type:"string",maxSize:null,dbType:"geometrycollection"},{type:"string",maxSize:null,dbType:"USER-DEFINED"}];function _normaliseMaxSize(e){if(null==e||""===e)return null;const t=Number(e);return Number.isNaN(t)?null:t}function getDbType(e,t,p){const y=_normaliseMaxSize(t);if(p){const t=DataTypeMap.find(t=>t.type===e&&t.maxSize===y&&t.dbType.toLowerCase()===p.toLowerCase());if(t)return t.dbType}let i=DataTypeMap.find(t=>t.type===e&&t.maxSize===y);if(!i){const t=DataTypeMap.filter(t=>t.type===e&&null===t.maxSize);i=t.length>1&&p?t.find(e=>e.dbType.toLowerCase()===p.toLowerCase()):t[0]}return i?i.dbType:e}function getSchemaType(e){return DataTypeMap.find(t=>t.dbType===e)?.type}function getDefaultTypeSize(e){return DataTypeMap.find(t=>t.dbType===e)?.maxSize}module.exports={DataTypeMap:DataTypeMap,getDbType:getDbType,getSchemaType:getSchemaType,getDefaultTypeSize:getDefaultTypeSize};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
const path=require("path"),logger=require("../../Logger");class HookService{constructor(e,t,o=null){this.db=e,this.utils=t,this.controllerWrapper=o,this.controllerWrapper.hookService=this,this.appRoot=o&&o.resolverPath?o.resolverPath:process.cwd()}loadModelClass(e){try{const t=path.join(this.appRoot,"models",`${e}.model.js`);delete require.cache[require.resolve(t)];return require(t)}catch(e){return void logger.debug("loadModelClass error",{err:e})}}getModelInstance(e){const t="string"==typeof e?e:e.modelName,o=this.loadModelClass(t);if(o)return"function"==typeof o?new o:o}resolveModelHook(e,t,o){const r=e.modelName,s=this.loadModelClass(r);if(!s)return;const l=this.getModelInstance(e);let i;if("validate"===t)i="validate";else if("on"===t)i=`on${o.charAt(0).toUpperCase()+o.slice(1)}`;else if("before"===t)i=`before${o.charAt(0).toUpperCase()+o.slice(1)}`;else if("after"===t)i=`after${o.charAt(0).toUpperCase()+o.slice(1)}`;else{if("custom"!==t)return;i=`on${o.charAt(0).toUpperCase()+o.slice(1)}Action`}return"function"==typeof l[i]?l[i].bind(l):"function"==typeof s[i]?s[i].bind(s):void 0}async executeValidatorHook({model:e,action:t,request:o,ctx:r,controller:s}){const l=this.resolveModelHook(e,"validate",t);if(l)return await l({model:e,action:t,request:o,context:r,db:this.db,utils:this.utils,controller:s})}async executeBeforeHook({model:e,action:t,request:o,ctx:r,controller:s}){const l=this.resolveModelHook(e,"before",t);if(l)return await l({model:e,action:t,request:o,context:r,db:this.db,utils:this.utils,controller:s})}async executeAfterHook({model:e,action:t,data:o,request:r,ctx:s,controller:l}){const i=this.resolveModelHook(e,"after",t);return i?await i({model:e,action:t,data:o,request:r,context:s,db:this.db,utils:this.utils,controller:l}):o}async executeCustomAction({model:e,action:t,request:o,ctx:r,controller:s}){const l=this.resolveModelHook(e,"custom",t);if(l)return await l({model:e,action:t,request:o,context:r,db:this.db,utils:this.utils,controller:s});throw
|
|
1
|
+
const path=require("path"),KormError=require("../../KormError"),logger=require("../../Logger");class HookService{constructor(e,t,o=null){this.db=e,this.utils=t,this.controllerWrapper=o,this.controllerWrapper.hookService=this,this.appRoot=o&&o.resolverPath?o.resolverPath:process.cwd()}loadModelClass(e){try{const t=path.join(this.appRoot,"models",`${e}.model.js`);delete require.cache[require.resolve(t)];return require(t)}catch(e){return void logger.debug("loadModelClass error",{err:e})}}getModelInstance(e){const t="string"==typeof e?e:e.modelName,o=this.loadModelClass(t);if(o)return"function"==typeof o?new o:o}resolveModelHook(e,t,o){const r=e.modelName,s=this.loadModelClass(r);if(!s)return;const l=this.getModelInstance(e);let i;if("validate"===t)i="validate";else if("on"===t)i=`on${o.charAt(0).toUpperCase()+o.slice(1)}`;else if("before"===t)i=`before${o.charAt(0).toUpperCase()+o.slice(1)}`;else if("after"===t)i=`after${o.charAt(0).toUpperCase()+o.slice(1)}`;else{if("custom"!==t)return;i=`on${o.charAt(0).toUpperCase()+o.slice(1)}Action`}return"function"==typeof l[i]?l[i].bind(l):"function"==typeof s[i]?s[i].bind(s):void 0}async executeValidatorHook({model:e,action:t,request:o,ctx:r,controller:s}){const l=this.resolveModelHook(e,"validate",t);if(l)return await l({model:e,action:t,request:o,context:r,db:this.db,utils:this.utils,controller:s})}async executeBeforeHook({model:e,action:t,request:o,ctx:r,controller:s}){const l=this.resolveModelHook(e,"before",t);if(l)return await l({model:e,action:t,request:o,context:r,db:this.db,utils:this.utils,controller:s})}async executeAfterHook({model:e,action:t,data:o,request:r,ctx:s,controller:l}){const i=this.resolveModelHook(e,"after",t);return i?await i({model:e,action:t,data:o,request:r,context:s,db:this.db,utils:this.utils,controller:l}):o}async executeCustomAction({model:e,action:t,request:o,ctx:r,controller:s}){const l=this.resolveModelHook(e,"custom",t);if(l)return await l({model:e,action:t,request:o,context:r,db:this.db,utils:this.utils,controller:s});throw KormError.unknownAction({action:t,model:e.modelName||e.name,hasCustomHook:!0})}async executeHasSoftDeleteHook(e){const t=this.getModelInstance(e);if(!t)return!1;return t.hasOwnProperty("hasSoftDelete")&&!0===t.hasSoftDelete}}module.exports=HookService;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
const HelperUtility=require("./HelperUtility"),logger=require("../../Logger");class QueryBuilder{constructor(e,t,r=null){this.db=e,this.utils=t,this.controllerWrapper=r,this.helperUtility=new HelperUtility}getHookService(){return this.controllerWrapper.hookService}getQueryBuilder(e,t=null){let r=this.db(e.table);return t&&(r=t),r._getMyModel=()=>e,r}parseValue(e){return this.helperUtility.parseValue(e)}parseWhereValue(e){return this.helperUtility.parseWhereValue(e)}parseWhereColumn(e){return this.helperUtility.parseWhereColumn(e)}_applyOrWhereCondition(e,t,r,o){if(null!==o)
|
|
1
|
+
const HelperUtility=require("./HelperUtility"),logger=require("../../Logger");class QueryBuilder{constructor(e,t,r=null){this.db=e,this.utils=t,this.controllerWrapper=r,this.helperUtility=new HelperUtility}getHookService(){return this.controllerWrapper.hookService}getQueryBuilder(e,t=null){let r=this.db(e.table);return t&&(r=t),r._getMyModel=()=>e,r}parseValue(e){return this.helperUtility.parseValue(e)}parseWhereValue(e){return this.helperUtility.parseWhereValue(e)}parseWhereColumn(e){return this.helperUtility.parseWhereColumn(e)}_applyOrWhereCondition(e,t,r,o){if(null!==o)switch(r){case"between":e.orWhereBetween(t,o);break;case"notBetween":e.orWhereNotBetween(t,o);break;case"in":e.orWhereIn(t,o);break;case"notIn":e.orWhereNotIn(t,o);break;case"like":e.orWhereRaw("?? LIKE ?",[t,o]);break;default:Array.isArray(o)?e.orWhereIn(t,o):e.orWhere(t,r,o)}else e.orWhereNull(t)}_applyAndWhereCondition(e,t,r,o){if(null!==o)switch(r){case"between":e.whereBetween(t,o);break;case"notBetween":e.whereNotBetween(t,o);break;case"in":e.whereIn(t,o);break;case"notIn":e.whereNotIn(t,o);break;case"like":e.whereRaw("?? LIKE ?",[t,o]);break;default:Array.isArray(o)?e.whereIn(t,o):e.where(t,r,o)}else e.whereNull(t)}buildWithTree(e,t=null){return this.helperUtility.dotWalkTree(e,{resolver:({current:e,part:r,source:o})=>t?t({current:e,part:r,source:o}):{}})}_getWithWhereForRelation(e,t){if(!e||"object"!=typeof e)return{direct:{},nested:{}};const r=`${t}.`,o={},i={};for(const[t,n]of Object.entries(e))if(t.startsWith(r)){const e=t.slice(r.length);e.includes(".")?i[e]=n:o[e]=n}return{direct:o,nested:i}}_applyWithWhereConditions(e,t){for(const[r,o]of Object.entries(t)){const{joinType:t="AND",column:i}=this.parseWhereColumn(r),{operator:n,value:s}=this.parseWhereValue(o);"AND"===t?this._applyAndWhereCondition(e,i,n,s):this._applyOrWhereCondition(e,i,n,s)}}async fetchRelatedRows(e,t,r={}){if(e.through){const o=await this.db(e.through).whereIn(e.throughLocalKey,t),i=this.db(e.table).whereIn(e.foreignKey,o.map(t=>t[e.throughForeignKey]));return Object.keys(r).length>0&&this._applyWithWhereConditions(i,r),i}{const o=this.db(e.table).whereIn(e.foreignKey,t);return Object.keys(r).length>0&&this._applyWithWhereConditions(o,r),o}}async fetchAndAttachRelated(e){const{parentRows:t,relName:r,model:o,withTree:i,relation:n,withWhere:s={}}=e;if(!n){const e=this.getHookService().getModelInstance(o),n=`get${r.charAt(0).toUpperCase()+r.slice(1)}Relation`;if("function"==typeof e[n]){const{direct:l,nested:a}=this._getWithWhereForRelation(s,r),h={rows:t,relName:r,model:o,withTree:i,controller:this.controllerWrapper,relation:o.hasRelations[r],qb:this,db:this.db,withWhere:l,nestedWithWhere:a};await e[n](h)}else logger.warn(`Method ${n} not found in model ${o.name}`);return t}const l=t.map(e=>e[n.localKey]);let a=[];const h="one"===n?.type,{direct:c,nested:p}=this._getWithWhereForRelation(s,r);let u={...c};try{const e=this.utils.getModel(this.controllerWrapper,r);if(this.controllerWrapper?.hookService){await this.controllerWrapper.hookService.executeHasSoftDeleteHook(e)&&(u={...u,deleted_at:null})}}catch(e){}a=await this.fetchRelatedRows(n,l,u);const y=new Map;for(const e of a){const t=e[n.foreignKey];y.has(t)||y.set(t,[]),y.get(t).push(e)}for(const e of t){const t=e[n.localKey];h&&1==y.get(t)?.length?e[r]=y.get(t)[0]:e[r]=y.get(t)||[]}const d=Object.keys(i);for(const e of d){const o=i[e],n=t.filter(e=>h?e[r]:e[r].length>0).map(e=>e[r]).reduce((e,t)=>e.concat(t),[]),s=this.utils.getModel(this.controllerWrapper,r);await this.fetchAndAttachRelated({parentRows:n,relName:e,model:s,withTree:o,relation:s.hasRelations[e],withWhere:p})}return t}filterWhere(e,t=""){return t?Object.keys(e).filter(e=>e.includes(t)).reduce((r,o)=>(r[o.replace(t,"")]=e[o],r),{}):Object.keys(e).filter(e=>!e.includes(".")).reduce((t,r)=>(t[r]=e[r],t),{})}buildSelectQuery(e,t){const{where:r={},with:o,select:i,orderBy:n={column:"id",direction:"asc"},limit:s=10,offset:l=0,page:a,groupBy:h,having:c,distinct:p,join:u,leftJoin:y,rightJoin:d,innerJoin:f}=t,g=this.getQueryBuilder(e);i&&(Array.isArray(i)||"string"==typeof i)?g.select(i):g.select("*"),p&&(Array.isArray(p)||"string"==typeof p?g.distinct(p):g.distinct()),u&&this._applyJoins(g,u,"join"),y&&this._applyJoins(g,y,"leftJoin"),d&&this._applyJoins(g,d,"rightJoin"),f&&this._applyJoins(g,f,"innerJoin"),this._applyWhereClause(g,r,o),h&&g.groupBy(h),c&&this._applyHavingClause(g,c),n&&this._applyOrderBy(g,n);let W=l;return a&&s>0&&(W=(Math.max(1,parseInt(a))-1)*s),s>0&&(g.limit(s),W>0&&g.offset(W)),g}async getQuery(e,t){try{const{where:r={},with:o,withWhere:i,limit:n=10,offset:s=0,page:l,join:a,leftJoin:h,rightJoin:c,innerJoin:p}=t,u=this.buildSelectQuery(e,t);let y=!1;const d=n;let f=s,g=1,W=0;l&&n>0&&(g=Math.max(1,parseInt(l)),f=(g-1)*n);const w=[],b=u.toSQL();w.push(b.sql);const m=await u;if(o&&o.length>0){const t=this.buildWithTree(o);for(const r of Object.keys(t))await this.fetchAndAttachRelated({parentRows:m,relName:r,model:e,withTree:t[r],relation:e.hasRelations[r],withWhere:i||{}})}let _=null;if(n>0)try{const t=this.getQueryBuilder(e);r&&Object.keys(r).length>0&&this._applyWhereClause(t,r),a&&this._applyJoins(t,a,"join"),h&&this._applyJoins(t,h,"leftJoin"),c&&this._applyJoins(t,c,"rightJoin"),p&&this._applyJoins(t,p,"innerJoin");const o=t.count("* as cnt");w.push(o.toSQL().sql);_=(await o.first()).cnt}catch(e){logger.warn("Failed to get total count:",e.message),_=m.length}n>0&&null!==_&&(W=Math.ceil(_/n),y=g<W);const A=y?g+1:null,j=g>1?g-1:null;return{data:m,totalCount:_,...this.controllerWrapper.debug?{sqlDebug:w}:{},...n>0?{pagination:{page:g,limit:d,offset:f,totalPages:W,hasNext:y,hasPrev:g>1,nextPage:A,prevPage:j}}:{}}}catch(t){throw logger.error("QueryService.getQuery error:",t),new Error(`Failed to execute query: ${t.message} on model ${e.name}`)}}getSumQuery(e,t){const{where:r={},join:o,leftJoin:i,rightJoin:n,innerJoin:s}=t,l=t.data||t,a=l.sumColumn,h=l.sumFormula;if(!a&&!h)throw new Error("Sum action requires either data.sumColumn or data.sumFormula");const c=e=>"`"+String(e).replace(/[`\\]/g,"")+"`";let p;if(a){const e=String(a).trim().replace(/[^a-zA-Z0-9_]/g,"");if(!e)throw new Error("data.sumColumn must be a valid column name");p="SUM("+c(e)+")"}else{const e=String(h).trim();if(!/\{[a-zA-Z0-9_]+\}/.test(e))throw new Error("data.sumFormula must contain at least one {columnName}");let t=0;for(const r of e)if("("===r)t++;else if(")"===r&&(t--,t<0))break;if(0!==t)throw new Error("data.sumFormula has unbalanced or misordered parentheses ( and )");const r=e.replace(/\{[a-zA-Z0-9_]+\}/g,"@");if(!/^[\s0-9.+*\/\-()@]+$/.test(r))throw new Error("data.sumFormula may only use BODMAS: numbers, + - * /, parentheses, and {columnName}");p="SUM("+e.replace(/\{([a-zA-Z0-9_]+)\}/g,(e,t)=>c(t))+")"}const u=this.getQueryBuilder(e);return o&&this._applyJoins(u,o,"join"),i&&this._applyJoins(u,i,"leftJoin"),n&&this._applyJoins(u,n,"rightJoin"),s&&this._applyJoins(u,s,"innerJoin"),this._applyWhereClause(u,r,[]),u.select(this.db.raw(p+" as sum")),u}_applyWhereWithArray(e,t,r){for(const o of t)this._applyWhereClause(e,o,r)}_applyWhereClause(e,t,r=[]){if(Array.isArray(t))this._applyWhereWithArray(e,t,r);else{if(t&&Object.keys(t).length>0){let r=this.helperUtility.getDotWalkQuery(t);logger.debug({filteredWhere:r}),r=this.helperUtility.objectFilter(r,(e,t)=>!e.startsWith("!")&&("__exists__"!==e&&("object"!=typeof t||null===t)));for(const[t,o]of Object.entries(r)){const{joinType:r="AND",column:i}=this.parseWhereColumn(t),{operator:n,value:s}=this.parseWhereValue(o);"AND"===r?this._applyAndWhereCondition(e,i,n,s):this._applyOrWhereCondition(e,i,n,s)}}this._applyNestedWhere(e,t,r)}}_getNestedWhereConditions(e,t){if(!e||"object"!=typeof e)return{};const r=`${t}.`,o={};for(const[i,n]of Object.entries(e))if(i.startsWith(r)){o[i.slice(r.length)]=n}else i===t&&!0===n&&(o.__exists__=!0);return o}_getTopLevelRelationsFromWhere(e){if(!e||"object"!=typeof e)return[];const t=new Set;for(const r of Object.keys(e))if(r.includes(".")){const e=r.split(".")[0];t.add(e)}else r.startsWith("!")&&t.add(r);return[...t]}_applyNestedWhere(e,t,r){const o=this,i=e._getMyModel(),n=this._getTopLevelRelationsFromWhere(t);if(0!==n.length)for(const r of n){const n=r.startsWith("!"),s=n?r.slice(1):r,l=this._getNestedWhereConditions(t,r);if(0===Object.keys(l).length)continue;const a=i.hasRelations?.[s];if(!a){logger.warn(`Relation ${s} not found in model ${i.modelName}`);continue}let h;try{h=this.utils.getModel(this.controllerWrapper,a.table||s)}catch(e){try{h=this.utils.getModel(this.controllerWrapper,s)}catch(e){logger.warn(`Model for relation ${s} (table: ${a.table}) not found`);continue}}e[n?"whereNotExists":"whereExists"](function(){const e=o.getQueryBuilder(h,this.select("*").from(a.table));e.whereRaw("??.?? = ??.??",[a.table,a.foreignKey,i.table,a.localKey]);if(!(!0===l.__exists__&&1===Object.keys(l).length)){const t={...l};delete t.__exists__,o._applyWhereClause(e,t,[])}})}}_applyJoins(e,t,r){const o=Array.isArray(t)?t:[t];for(const t of o)"string"==typeof t?e[r](t):"object"==typeof t&&(t.table&&t.on?e[r](t.table,t.on):t.table&&t.first&&t.operator&&t.second&&e[r](t.table,t.first,t.operator,t.second))}_applyWithWhere(e,t){try{if(Array.isArray(t))for(const r of t)"string"==typeof r?e.withWhere(r):"object"==typeof r&&e.withWhere(r.column,r.operator,r.value);else if("object"==typeof t)for(const[r,o]of Object.entries(t))e.withWhere(r,o)}catch(e){logger.warn("Failed to apply withWhere:",e.message)}}_applyHavingClause(e,t){for(const[r,o]of Object.entries(t))"object"==typeof o&&o.operator?e.having(r,o.operator,o.value):e.having(r,o)}_applyOrderBy(e,t){if(Array.isArray(t))for(const r of t)"string"==typeof r?e.orderBy(r):"object"==typeof r&&e.orderBy(r.column,r.direction||"asc");else"string"==typeof t?e.orderBy(t):"object"==typeof t&&e.orderBy(t.column,t.direction||"asc")}}module.exports=QueryBuilder;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
const QueryBuilder=require("./QueryBuilder");class QueryService{constructor(e,t,r=null){this.db=e,this.utils=t,this.controllerWrapper=r,this.queryBuilder=new QueryBuilder(e,t,r)}async getQuery(e,t){return await this.queryBuilder.getQuery(e,t)}async getSoftDeleteQuery(e,t){return await this.queryBuilder.getQuery(e,{...t,where:{...t.where||{},deleted_at:null}})}async executeShowQuery(e,t){const r=await this.getQuery(e,{...t,limit:1,offset:0});return r.data.length>0?r.data[0]:null}async executeCountQuery(e,t){const r=await this.
|
|
1
|
+
const QueryBuilder=require("./QueryBuilder");class QueryService{constructor(e,t,r=null){this.db=e,this.utils=t,this.controllerWrapper=r,this.queryBuilder=new QueryBuilder(e,t,r)}async getQuery(e,t){return await this.queryBuilder.getQuery(e,t)}async getSoftDeleteQuery(e,t){return await this.queryBuilder.getQuery(e,{...t,where:{...t.where||{},deleted_at:null}})}async executeShowQuery(e,t){const r=await this.getQuery(e,{...t,limit:1,offset:0});return r.data.length>0?r.data[0]:null}_buildCountQuery(e,t={}){const r=this.queryBuilder.getQueryBuilder(e),{join:u,leftJoin:i,rightJoin:a,innerJoin:n,where:s={}}=t;return u&&this.queryBuilder._applyJoins(r,u,"join"),i&&this.queryBuilder._applyJoins(r,i,"leftJoin"),a&&this.queryBuilder._applyJoins(r,a,"rightJoin"),n&&this.queryBuilder._applyJoins(r,n,"innerJoin"),this.queryBuilder._applyWhereClause(r,s,[]),r.count()}async executeCountQuery(e,t){const r=await this._buildCountQuery(e,t||{});return Object.values(r[0])[0]}async executeSumQuery(e,t){const r=await this.queryBuilder.getSumQuery(e,t).first(),u=r&&null!=r.sum?r.sum:0;return Number(u)}async executeCreateQuery(e,t){const r=await this.db(e.table).insert(t.data),u=Array.isArray(r)?r[0]:r;if(null==u)return[];const i=e.columns&&e.columns.find(e=>e.primary),a=i&&i.name?i.name:"id",n=await this.db(e.table).where(a,u).select("*");return Array.isArray(n)?n:[n]}async executeUpdateQuery(e,t){return await this.db.transaction(async r=>(await r(e.table).where(t.where).update(t.data),await r(e.table).where(t.where).first()))}async executeDeleteQuery(e,t){return await this.db(e.table).where(t.where).delete()}async executeSoftDeleteQuery(e,t){await this.db(e.table).where(t.where).update({deleted_at:new Date});const r=await this.db(e.table).where(t.where).select("*");return Array.isArray(r)?r:[r]}async executeUpsertQuery(e,t){return await this.db(e.table).insert(t.data).onConflict(t.conflict).merge(t.data)}_buildReplaceQuery(e,t){const{sql:r,bindings:u}=this.db(e.table).insert(t.data).toSQL();return this.db.raw(r.replace(/^\s*insert/i,"REPLACE"),u)}async executeReplaceQuery(e,t){return await this._buildReplaceQuery(e,t),t.data}buildDryRun(e,t,r){const u=this._dryRunBuilders(e,t,r).map(e=>{const t=e.toSQL();return{sql:t.sql,bindings:t.bindings}});return{success:!0,dryRun:!0,action:r,model:e.name,sql:u[0]?u[0].sql:null,bindings:u[0]?u[0].bindings:[],statements:u}}_dryRunBuilders(e,t,r){const u=e.table,i=t.where||{};switch(r){case"list":return[this.queryBuilder.buildSelectQuery(e,t)];case"show":return[this.queryBuilder.buildSelectQuery(e,{...t,limit:1,offset:0})];case"count":return[this._buildCountQuery(e,t)];case"sum":return[this.queryBuilder.getSumQuery(e,t)];case"create":return[this.db(u).insert(t.data).returning("*")];case"update":return[this.db(u).where(i).update(t.data).returning("*")];case"softDelete":return[this.db(u).where(i).update({deleted_at:new Date}).returning("*")];case"delete":return[this.db(u).where(i).delete()];case"replace":return[this._buildReplaceQuery(e,t)];case"upsert":return[this.db(u).insert(t.data).onConflict(t.conflict).merge(t.data)];case"sync":return[this.db(u).insert(t.data).onConflict(t.conflict).merge(t.data),this.db(u).where(i).delete()];default:return[]}}async executeSyncQuery(e,t){return this.db.transaction(async r=>({insertOrUpdateQuery:await r(e.table).insert(t.data).onConflict(t.conflict).merge(t.data),deleteQuery:await r(e.table).where(t.where).delete()}))}}module.exports=QueryService;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
const
|
|
1
|
+
const BaseSyncTable=require("../BaseSyncTable"),logger=require("../../Logger");class MySQLSyncTable extends BaseSyncTable{_getClientName(){return"mysql"}async _listTables(){const e=this.db.client.database(),[t]=await this.db.raw("SHOW TABLES");return t.map(t=>t[`Tables_in_${e}`]).filter(Boolean)}async _applyExtras(e){for(const[t,a]of Object.entries(e.columns)){const l="string"==typeof a?this.utils.formatColumnSchema(t,a):a;if(!l.onUpdate)continue;const s=l.columnType||(l.size?`${l.type}(${l.size})`:l.type),n=l.nullable?"NULL":"NOT NULL",r=null!=l.default&&""!==l.default?` DEFAULT ${this._renderRawDefault(l.default)}`:"",o=`ALTER TABLE \`${e.table}\` MODIFY COLUMN \`${t}\` ${s} ${n}${r} ON UPDATE ${l.onUpdate}`;await this.db.raw(o)}}_renderRawDefault(e){const t=String(e).trim();return/^current_timestamp$/i.test(t)||/^now\(\)$/i.test(t)?"CURRENT_TIMESTAMP":/^-?\d+(\.\d+)?$/.test(t)?t:/^(true|false)$/i.test(t)?t.toUpperCase():`'${t.replace(/'/g,"''")}'`}async _getRelations(e){const t=this.db.client.database(),a=await this.db("information_schema.KEY_COLUMN_USAGE").where({TABLE_SCHEMA:t,TABLE_NAME:e}).whereNotNull("REFERENCED_TABLE_NAME").select("COLUMN_NAME AS local_column","REFERENCED_TABLE_NAME AS ref_table","REFERENCED_COLUMN_NAME AS ref_column"),l={};for(const e of a)l[e.local_column]={one:{table:e.ref_table,column:e.ref_column}};return l}async getTablesOfDatabase(){return this._listTables()}async executeSql(e,t=[]){return logger.debug("executeSql (legacy)",{sql:e,params:t}),this.db.raw(e,t)}}module.exports=MySQLSyncTable;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
const{getDefaultTypeSize:getDefaultTypeSize,getDbType:getDbType}=require("./DataTypeMap");class BaseUtility{isInternalDefault(e){if("string"!=typeof e)return!1;const t=/^CURRENT_TIMESTAMP$/i.test(e),n=/^[a-zA-Z_][a-zA-Z0-9_]*\s*\(.*\)$/.test(e),r=/::[a-zA-Z0-9_]+/.test(e);return t||n||r}getModel(e,t){const n=e?.schema;let r=null;const i=Object.keys(n);if(i.forEach(e=>{e===t&&(r=n[e])}),r||i.forEach(e=>{const i=n[e];i.table===t&&(r=i)}),r)return{...r,name:t,columns:Object.keys(r.columns).map(e=>{const t=r.columns[e];return this.parseColumnString(e,t)})};throw
|
|
1
|
+
const{getDefaultTypeSize:getDefaultTypeSize,getDbType:getDbType}=require("./DataTypeMap"),KormError=require("../../KormError");class BaseUtility{isInternalDefault(e){if("string"!=typeof e)return!1;const t=/^CURRENT_TIMESTAMP$/i.test(e),n=/^[a-zA-Z_][a-zA-Z0-9_]*\s*\(.*\)$/.test(e),r=/::[a-zA-Z0-9_]+/.test(e);return t||n||r}getModel(e,t){const n=e?.schema;let r=null;const i=Object.keys(n);if(i.forEach(e=>{e===t&&(r=n[e])}),r||i.forEach(e=>{const i=n[e];i.table===t&&(r=i)}),r)return{...r,name:t,columns:Object.keys(r.columns).map(e=>{const t=r.columns[e];return this.parseColumnString(e,t)})};throw KormError.unknownModel({model:t,available:Object.keys(n||{})})}map2DbDefault(e){return"now"===e?"CURRENT_TIMESTAMP":e}getCollenedValue(e,{splitChar:t=":",give:n="right",trimWrap:r="{}"}={}){if(null==e)return null;const i=String(e),l=i.indexOf(t),a=e=>{if(!r)return e;if("string"==typeof r&&2===r.length){const[t,n]=r;return e.startsWith(t)&&e.endsWith(n)?e.slice(1,-1):e}return"string"==typeof r&&r.length&&e.startsWith(r)&&e.endsWith(r)?e.slice(r.length,-r.length):e};if(l>=0){const e=i.slice(0,l).trim(),r=i.slice(l+t.length).trim();return"both"===n?{left:a(e),right:a(r)}:a("left"===n?e:r)}return a(i)}parseForeignKey(e){if(!e)return{foreignMapTables:[]};const t=this.getCollenedValue(e),[n,r="id"]=String(t||"").split(":"),i=(this.getCollenedValue(n)||"").split(",").filter(Boolean),l=(this.getCollenedValue(r)||"").split(",").filter(Boolean);return{foreignMapTables:i.map((e,t)=>({table:e,column:l[t]||l[0]||"id"}))}}parseColumnString(e,t){const[n,...r]=String(t).split("|"),i=e=>r.find(t=>t.startsWith(e+":")),l=e=>r.join("|").includes(e),a=e=>e&&e.includes(":")?e.slice(e.indexOf(":")+1):null,s=i("default"),o=i("onUpdate"),u=i("comment"),m=i("foreignKey"),g=i("size"),p=this.parseForeignKey(m),E=!!m,T=getDbType(n,g?a(g):null),c=g?a(g):getDefaultTypeSize(T);return{name:e,type:T,size:+c,isUnsigned:l("unsigned")||l("primaryKey")||E,columnType:`${n}${c?`(${c})`:""}`,nullable:!l("notNull"),primary:l("primaryKey"),autoIncrement:l("autoIncrement"),unique:l("unique"),default:this.map2DbDefault(a(s)),onUpdate:this.map2DbDefault(a(o)),comment:a(u)||"",hasForeignKey:E,...p}}formatColumnSchema(e,t){return this.parseColumnString(e,t)}formatColumnDef(e,t){return{name:e,type:t.DATA_TYPE,size:+(t.CHARACTER_MAXIMUM_LENGTH||getDefaultTypeSize(t.DATA_TYPE)),isUnsigned:String(t.COLUMN_TYPE||"").toLowerCase().includes("unsigned"),columnType:t.COLUMN_TYPE,nullable:"YES"===t.IS_NULLABLE,primary:"PRIMARY KEY"===t.CONSTRAINT_TYPE,unique:"UNIQUE"===t.CONSTRAINT_TYPE,autoIncrement:String(t.EXTRA||"").toLowerCase().includes("auto_increment"),hasForeignKey:"FOREIGN KEY"===t.CONSTRAINT_TYPE,comment:t.COMMENT||"",default:t.COLUMN_DEFAULT,onUpdate:((e="")=>{const t=String(e).match(/on update\s+([a-zA-Z_]+)/i);return t?t[1]:null})(t.EXTRA),foreignMapTables:"FOREIGN KEY"===t.CONSTRAINT_TYPE&&t.REFERENCED_TABLE_NAME?[{table:t.REFERENCED_TABLE_NAME,column:t.REFERENCED_COLUMN_NAME}]:[]}}escapeComment(e){return null==e?"":String(e).replace(/'/g,"\\'")}getDefaultTypeSize(e){return getDefaultTypeSize(e)}}module.exports=BaseUtility;
|
package/clients/pg/CurdTable.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
const QueryService=require("./QueryService"),HookService=require("./HookService");class CurdTable{constructor(e,r,t=null){if(this.db=e,this.utils=r,this.controllerWrapper=t,this.hookService=new HookService(e,r,t),this.queryService=new QueryService(e,r,t),!this.queryService)throw new
|
|
1
|
+
const QueryService=require("./QueryService"),HookService=require("./HookService"),KormError=require("../../KormError");class CurdTable{constructor(e,r,t=null){if(this.db=e,this.utils=r,this.controllerWrapper=t,this.hookService=new HookService(e,r,t),this.queryService=new QueryService(e,r,t),!this.queryService)throw new KormError("CurdTable requires queryService (execute*Query / getQuery).",{code:KormError.CODES.INTERNAL})}async processRequest(e,r=null,t=null){const o=this.controllerWrapper,s=this.utils.getModel(this.controllerWrapper,r),c=e?.action||"list";let i=null;const a={model:s,action:c,request:e,ctx:t,controller:o};if(this.hookService?.executeValidatorHook&&await this.hookService.executeValidatorHook({...a}),e?.dryRun)return this.buildDryRunResult(s,e,c);switch(this.hookService?.executeBeforeHook&&(e.beforeActionData=await this.hookService.executeBeforeHook({...a})),c){case"count":i=await this.queryService.executeCountQuery(s,e);break;case"sum":i=await this.queryService.executeSumQuery(s,e);break;case"list":i=await this.hookService.executeHasSoftDeleteHook(s)?await this.queryService.getSoftDeleteQuery(s,e):await this.queryService.getQuery(s,e);break;case"show":i=await this.queryService.executeShowQuery(s,e);break;case"create":i=await this.queryService.executeCreateQuery(s,e);break;case"update":{const r=await this.queryService.executeUpdateQuery(s,e);if(!r)throw KormError.noMatchingRow({action:c,model:s.name});i={message:"Record updated successfully",data:r,success:!0};break}case"replace":if(i=await this.queryService.executeReplaceQuery(s,e),!i)throw KormError.noMatchingRow({action:c,model:s.name});i={message:"Record replaced successfully",data:i,success:!0};break;case"upsert":if(i=await this.queryService.executeUpsertQuery(s,e),!i)throw KormError.noMatchingRow({action:c,model:s.name});i={message:"Record upserted successfully",data:i,success:!0};break;case"sync":if(i=await this.queryService.executeSyncQuery(s,e),!i)throw KormError.noMatchingRow({action:c,model:s.name});i={message:"Record synced successfully",data:i,success:!0};break;case"delete":{let r=null;if(r=await this.hookService.executeHasSoftDeleteHook(s)?await this.queryService.executeSoftDeleteQuery(s,e):await this.queryService.executeDeleteQuery(s,e),!r)throw KormError.noMatchingRow({action:c,model:s.name});i={message:"Record deleted successfully",data:r,success:!0};break}default:if(!this.hookService?.executeCustomAction)throw KormError.unknownAction({action:c,model:s?.name});i=await this.hookService.executeCustomAction({...a})}if(this.hookService?.executeAfterHook&&(i=await this.hookService.executeAfterHook({...a,data:i})),e?.other_requests&&"object"==typeof e.other_requests){const r={},o=Object.entries(e.other_requests);for(const[e,s]of o)Array.isArray(s)?r[e]=await Promise.all(s.map(r=>this.processRequest(r,e,t))):r[e]=await this.processRequest(s,e,t);i.other_responses=r}return i}async buildDryRunResult(e,r,t){if(!["list","show","count","sum","create","update","replace","upsert","sync","delete"].includes(t))throw KormError.unknownAction({action:t,model:e?.name});let o=t,s=r;const c=await this.hookService.executeHasSoftDeleteHook(e);return c&&"delete"===t?o="softDelete":c&&"list"===t&&(s={...r,where:{...r.where||{},deleted_at:null}}),this.queryService.buildDryRun(e,s,o)}}module.exports=CurdTable;
|