@dreamtree-org/korm-js 1.0.59 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ControllerWrapper.js +1 -1
- package/KormError.js +1 -1
- package/README.md +78 -38
- package/ai-skills/korm-js.md +38 -37
- package/clients/BaseSyncTable.js +1 -1
- package/clients/SyncRunner.js +1 -0
- package/clients/mysql/index.js +1 -1
- package/clients/pg/index.js +1 -1
- package/clients/sqlite/index.js +1 -1
- package/index.d.ts +6 -6
- package/package.json +1 -1
package/ControllerWrapper.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
const mysqlWrapper=require("./clients/mysql"),sqliteWrapper=require("./clients/sqlite"),pgWrapper=require("./clients/pg"),KormError=require("./KormError"),AuthorizationService=require("./AuthorizationService"),{buildModelRequestSchema:buildModelRequestSchema}=require("./requestSchema"),{buildModelDescription:buildModelDescription,SCHEMA_API_VERSION:SCHEMA_API_VERSION}=require("./schemaDescribe"),InstanceMapper={mysql2:mysqlWrapper,sqlite:sqliteWrapper,pg:pgWrapper},dbClientMapper={mysql2:"mysql2",mysql:"mysql2",pg:"pg",postgresql:"pg",sqlite:"sqlite",sqlite3:"sqlite"};class ControllerWrapper{static db=null;static dbClient=null;static dbClientClass=null;static schema=null;static resolverPath=null;static dbInstance=null;static debug=!1;static _authz=new AuthorizationService;requestInstance=null;constructor(){this.requestInstance={}}static initializeKORM({db:e,dbClient:t,schema:s,resolverPath:i=null,debug:r=!1}){this.db=e,this.dbClient=t,this.schema=s,this.resolverPath=i,this.debug=r;const a=dbClientMapper[t];if(!a)throw new Error(`Database client ${t} not found`);const n=InstanceMapper[a];if(!n)throw new Error(`Database client ${t} not found`);return this.dbClientClass=n,this.dbInstance=new n(this),this}static setSchema(e){this.schema=e;const t=this.dbClientClass;if(!t)throw new Error(`Database client ${this.dbClient} not found`);return this.dbInstance=new t(this),this}static _resolveModelName(e){const t=this.schema||{};if(t[e])return e;return Object.keys(t).find(s=>t[s]&&t[s].table===e)||e}static authorize(e,t,s){return this._authz.registerAuthorize(this._resolveModelName(e),t,s),this}static scope(e,t){return this._authz.registerScope(this._resolveModelName(e),t),this}static resetAuthorization(){return this._authz.reset(),this}static async processRequest(e,t=null,s=null){let i=e;if(this._authz.hasRules()&&t){const r=this._resolveModelName(t),a=e&&e.action||"list";this._authz.enforce(r,a,e,s),i=this._authz.applyScope(r,a,e||{},s)}return await this.dbInstance.processRequest(i,t,s)}static async processRequestWithOthers(e,t=null,s=null){return await this.dbInstance.processRequest(e,t,s)}static async syncDatabase(){return await this.dbInstance.syncDatabase()}static async generateSchema(){return await this.dbInstance.generateSchema()}static loadModelClass(e){return this.dbInstance.hookService.loadModelClass(e)}static getModelInstance(e){return this.dbInstance.hookService.getModelInstance(e)}static getRequestJsonSchema(e){const t=this.schema||{},s=t[e]||Object.values(t).find(t=>t&&t.table===e);if(!s)throw KormError.unknownModel({model:e,available:Object.keys(t)});return buildModelRequestSchema(s,{title:`KormRequest<${e}>`})}static _modelHasSoftDelete(e){try{const t=this.dbInstance?.hookService?.getModelInstance?.(e);return!(!t||!0!==t.hasSoftDelete)}catch{return!1}}static describeModel(e,t=null){const s=this.schema||{},i=Object.entries(s).find(([t,s])=>t===e||s&&s.table===e);if(!i)throw KormError.unknownModel({model:e,available:Object.keys(s)});const[r,a]=i,n=buildModelDescription(r,a,{softDelete:this._modelHasSoftDelete(r)});return null!=t&&this._authz.hasRules()&&(n.actions=this._authz.availableActions(r,n.actions,t)),n}static describeSchema(){const e=this.schema||{},t=Object.entries(e).map(([e,t])=>buildModelDescription(e,t,{softDelete:this._modelHasSoftDelete(e)}));return{schemaApiVersion:SCHEMA_API_VERSION,models:t}}}module.exports=ControllerWrapper;
|
|
1
|
+
const mysqlWrapper=require("./clients/mysql"),sqliteWrapper=require("./clients/sqlite"),pgWrapper=require("./clients/pg"),KormError=require("./KormError"),AuthorizationService=require("./AuthorizationService"),{buildModelRequestSchema:buildModelRequestSchema}=require("./requestSchema"),{buildModelDescription:buildModelDescription,SCHEMA_API_VERSION:SCHEMA_API_VERSION}=require("./schemaDescribe"),InstanceMapper={mysql2:mysqlWrapper,sqlite:sqliteWrapper,pg:pgWrapper},dbClientMapper={mysql2:"mysql2",mysql:"mysql2",pg:"pg",postgresql:"pg",sqlite:"sqlite",sqlite3:"sqlite"};class ControllerWrapper{static db=null;static dbClient=null;static dbClientClass=null;static schema=null;static resolverPath=null;static dbInstance=null;static debug=!1;static _authz=new AuthorizationService;requestInstance=null;constructor(){this.requestInstance={}}static initializeKORM({db:e,dbClient:t,schema:s,resolverPath:i=null,debug:r=!1}){this.db=e,this.dbClient=t,this.schema=s,this.resolverPath=i,this.debug=r;const a=dbClientMapper[t];if(!a)throw new Error(`Database client ${t} not found`);const n=InstanceMapper[a];if(!n)throw new Error(`Database client ${t} not found`);return this.dbClientClass=n,this.dbInstance=new n(this),this}static setSchema(e){this.schema=e;const t=this.dbClientClass;if(!t)throw new Error(`Database client ${this.dbClient} not found`);return this.dbInstance=new t(this),this}static _resolveModelName(e){const t=this.schema||{};if(t[e])return e;return Object.keys(t).find(s=>t[s]&&t[s].table===e)||e}static authorize(e,t,s){return this._authz.registerAuthorize(this._resolveModelName(e),t,s),this}static scope(e,t){return this._authz.registerScope(this._resolveModelName(e),t),this}static resetAuthorization(){return this._authz.reset(),this}static async processRequest(e,t=null,s=null){let i=e;if(this._authz.hasRules()&&t){const r=this._resolveModelName(t),a=e&&e.action||"list";this._authz.enforce(r,a,e,s),i=this._authz.applyScope(r,a,e||{},s)}return await this.dbInstance.processRequest(i,t,s)}static async processRequestWithOthers(e,t=null,s=null){return await this.dbInstance.processRequest(e,t,s)}static async syncDatabase(e={}){return await this.dbInstance.syncDatabase(e)}static async generateSchema(){return await this.dbInstance.generateSchema()}static loadModelClass(e){return this.dbInstance.hookService.loadModelClass(e)}static getModelInstance(e){return this.dbInstance.hookService.getModelInstance(e)}static getRequestJsonSchema(e){const t=this.schema||{},s=t[e]||Object.values(t).find(t=>t&&t.table===e);if(!s)throw KormError.unknownModel({model:e,available:Object.keys(t)});return buildModelRequestSchema(s,{title:`KormRequest<${e}>`})}static _modelHasSoftDelete(e){try{const t=this.dbInstance?.hookService?.getModelInstance?.(e);return!(!t||!0!==t.hasSoftDelete)}catch{return!1}}static describeModel(e,t=null){const s=this.schema||{},i=Object.entries(s).find(([t,s])=>t===e||s&&s.table===e);if(!i)throw KormError.unknownModel({model:e,available:Object.keys(s)});const[r,a]=i,n=buildModelDescription(r,a,{softDelete:this._modelHasSoftDelete(r)});return null!=t&&this._authz.hasRules()&&(n.actions=this._authz.availableActions(r,n.actions,t)),n}static describeSchema(){const e=this.schema||{},t=Object.entries(e).map(([e,t])=>buildModelDescription(e,t,{softDelete:this._modelHasSoftDelete(e)}));return{schemaApiVersion:SCHEMA_API_VERSION,models:t}}}module.exports=ControllerWrapper;
|
package/KormError.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
const CODES=Object.freeze({NO_MATCHING_ROW:"NO_MATCHING_ROW",UNKNOWN_ACTION:"UNKNOWN_ACTION",VALIDATION_FAILED:"VALIDATION_FAILED",UNKNOWN_MODEL:"UNKNOWN_MODEL",NO_CUSTOM_ACTION_HOOK:"NO_CUSTOM_ACTION_HOOK",FORBIDDEN:"FORBIDDEN",INTERNAL:"INTERNAL"}),ACTIONS=Object.freeze(["count","sum","list","show","create","update","replace","upsert","sync","delete"]);function levenshtein(e,
|
|
1
|
+
const CODES=Object.freeze({NO_MATCHING_ROW:"NO_MATCHING_ROW",UNKNOWN_ACTION:"UNKNOWN_ACTION",VALIDATION_FAILED:"VALIDATION_FAILED",UNKNOWN_MODEL:"UNKNOWN_MODEL",NO_CUSTOM_ACTION_HOOK:"NO_CUSTOM_ACTION_HOOK",FORBIDDEN:"FORBIDDEN",SYNC_FK_ORPHAN:"SYNC_FK_ORPHAN",INTERNAL:"INTERNAL"}),ACTIONS=Object.freeze(["count","sum","list","show","create","update","replace","upsert","sync","delete"]);function levenshtein(e,t){const o=e.length,r=t.length;if(0===o)return r;if(0===r)return o;let n=Array.from({length:r+1},(e,t)=>t),i=new Array(r+1);for(let s=1;s<=o;s++){i[0]=s;for(let o=1;o<=r;o++){const r=e[s-1]===t[o-1]?0:1;i[o]=Math.min(n[o]+1,i[o-1]+1,n[o-1]+r)}[n,i]=[i,n]}return n[r]}function closestAction(e,t=ACTIONS){if(!e)return null;const o=String(e).toLowerCase();let r=null,n=1/0;for(const e of t){const t=levenshtein(o,e);t<n&&(n=t,r=e)}return n<=Math.max(2,Math.ceil(o.length/2))?r:null}class KormError extends Error{constructor(e,{code:t=CODES.INTERNAL,hint:o=null,context:r={},suggestedFixes:n=null}={}){super(e),this.name="KormError",this.code=t,this.hint=o,this.context=r,this.suggestedFixes=n,Error.captureStackTrace&&Error.captureStackTrace(this,KormError)}toJSON(){return{name:this.name,code:this.code,message:this.message,hint:this.hint,context:this.context,suggestedFixes:this.suggestedFixes}}}KormError.noMatchingRow=({action:e,model:t})=>{const o="update"===e?"No row matched the where clause. Use `upsert` to insert-or-update, or `sync` to reconcile.":"No row matched the where clause for this action.";return new KormError(`No matching row for action "${e}" on model "${t}".`,{code:CODES.NO_MATCHING_ROW,hint:o,context:{action:e,model:t}})},KormError.unknownAction=({action:e,model:t,hasCustomHook:o=!1})=>{const r=closestAction(e),n=o?CODES.NO_CUSTOM_ACTION_HOOK:CODES.UNKNOWN_ACTION,i=o?`No custom action hook found for "${t}.${e}".`:`Unknown action "${e}".`,s=r?`Did you mean "${r}"? Valid actions: ${ACTIONS.join(", ")}.`:`Valid actions: ${ACTIONS.join(", ")}. Custom actions require an on<Action>Action hook on the model.`;return new KormError(i,{code:n,hint:s,context:{action:e,model:t,validActions:ACTIONS,closest:r}})},KormError.unknownModel=({model:e,available:t=[]})=>{const o=t.length?`Available models: ${t.join(", ")}.`:"No models are registered in the schema.";return new KormError(`Model "${e}" not found.`,{code:CODES.UNKNOWN_MODEL,hint:o,context:{model:e,available:t}})},KormError.forbidden=({model:e,action:t,hint:o=null,context:r={}}={})=>new KormError(`Action "${t}" on model "${e}" is not permitted in this context.`,{code:CODES.FORBIDDEN,hint:o||"A registered authorize() predicate denied this request for the current context.",context:{action:t,model:e,...r}}),KormError.validationFailed=({errors:e=[],source:t=null})=>{const o=e.map(e=>({field:e.field,message:e.message,value:e.value,rule:e.rule})),r=o.map(e=>e.field).filter(Boolean),n=new KormError(`Validation failed${t?` for ${t}`:""}${r.length?`: ${r.join(", ")}`:""}.`,{code:CODES.VALIDATION_FAILED,hint:"Fix the listed fields and resubmit. See context.fields for per-field detail.",context:{source:t,fields:o}});return n.errors=e,n},KormError.foreignKeyOrphans=({table:e,offenders:t=[]})=>{const o=t.map(t=>{const o=`${t.rowsAffected} existing row${1===t.rowsAffected?"":"s"}`,r=JSON.stringify(t.backfillValue);return`"${e}.${t.column}" → "${t.parentTable}.${t.parentColumn}": ${o} would be back-filled with ${r}, which does not exist in "${t.parentTable}"`}).join("; ");return new KormError(`Cannot add foreign key on "${e}": existing rows would be orphaned (${o}).`,{code:CODES.SYNC_FK_ORPHAN,hint:"Adding this foreign-key column to a populated table would leave child rows pointing at a parent that does not exist, so the database would reject the constraint. Resolve the data first: make the column nullable (existing rows back-fill NULL, which is FK-safe), give it a default that exists in the parent, populate the column for existing rows before syncing, insert the missing parent rows, or clear the child table. No data was changed — korm aborted before altering the table.",context:{table:e,offenders:t}})},KormError.CODES=CODES,KormError.ACTIONS=ACTIONS,KormError.closestAction=closestAction,module.exports=KormError;
|
package/README.md
CHANGED
|
@@ -20,7 +20,7 @@ Have an idea, a bug, or just want to say hi? Reach out anytime — feedback from
|
|
|
20
20
|
|
|
21
21
|
## 💚 Sponsor & Support
|
|
22
22
|
|
|
23
|
-
KORM-JS is **free and open source**, built in the open and powered entirely by passion and late nights. Every feature you rely on — every multi-engine query, relation-aware join, and line of zero-boilerplate CRUD — represents hours of careful craft so that
|
|
23
|
+
KORM-JS is **free and open source**, built in the open and powered entirely by passion and late nights. Every feature you rely on — every multi-engine query, relation-aware join, and line of zero-boilerplate CRUD — represents hours of careful craft so that _your_ backend can ship faster and stay safer.
|
|
24
24
|
|
|
25
25
|
If this library has saved you time, sparked an idea, or spared you a pile of hand-written SQL, please consider supporting its continued development. **Your sponsorship keeps the updates coming, the bugs squashed, and the docs sharp — for the whole community.** Even the smallest contribution is a huge encouragement. 🙏
|
|
26
26
|
|
|
@@ -463,10 +463,10 @@ Replaces an entire row, keyed on the model's primary key (include the PK in
|
|
|
463
463
|
`data`). Supported on **all three engines**, but the semantics differ by what
|
|
464
464
|
each engine can express:
|
|
465
465
|
|
|
466
|
-
| Engine
|
|
467
|
-
|
|
|
468
|
-
| MySQL
|
|
469
|
-
| SQLite
|
|
466
|
+
| Engine | Statement | Semantics |
|
|
467
|
+
| -------- | ------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- |
|
|
468
|
+
| MySQL | `REPLACE INTO` | True delete + insert. Columns omitted from `data` reset to their column **DEFAULT** (or `NULL`). |
|
|
469
|
+
| SQLite | `INSERT OR REPLACE` | True delete + insert (same as MySQL). |
|
|
470
470
|
| Postgres | `INSERT … ON CONFLICT (pk) DO UPDATE` | **Merge**, not a true replace: columns omitted from `data` keep their existing values (Postgres has no native `REPLACE`). |
|
|
471
471
|
|
|
472
472
|
> ⚠️ **Cross-engine caveat:** on Postgres, `replace` is emulated and behaves
|
|
@@ -1643,18 +1643,18 @@ type|modifier1|modifier2|...
|
|
|
1643
1643
|
|
|
1644
1644
|
**Column Modifiers:**
|
|
1645
1645
|
|
|
1646
|
-
| Modifier
|
|
1647
|
-
|
|
|
1648
|
-
| `size:n`
|
|
1649
|
-
| `unsigned`¹
|
|
1650
|
-
| `primaryKey`
|
|
1651
|
-
| `autoIncrement`
|
|
1652
|
-
| `notNull`
|
|
1653
|
-
| `unique`
|
|
1654
|
-
| `default:value`
|
|
1655
|
-
| `onUpdate:value`²
|
|
1656
|
-
| `comment:text`
|
|
1657
|
-
| `foreignKey:table:column
|
|
1646
|
+
| Modifier | Description | Example |
|
|
1647
|
+
| -------------------------- | ------------------ | ---------- | --------------------------- | -------------- |
|
|
1648
|
+
| `size:n` | Column size | `varchar | size:255` |
|
|
1649
|
+
| `unsigned`¹ | Unsigned integer | `int | unsigned` |
|
|
1650
|
+
| `primaryKey` | Primary key column | `bigint | primaryKey` |
|
|
1651
|
+
| `autoIncrement` | Auto increment | `bigint | primaryKey | autoIncrement` |
|
|
1652
|
+
| `notNull` | Not nullable | `varchar | size:255 | notNull` |
|
|
1653
|
+
| `unique` | Unique constraint | `varchar | unique` |
|
|
1654
|
+
| `default:value` | Default value | `tinyint | default:1` |
|
|
1655
|
+
| `onUpdate:value`² | On update value | `timestamp | onUpdate:CURRENT_TIMESTAMP` |
|
|
1656
|
+
| `comment:text` | Column comment | `varchar | comment:User email address` |
|
|
1657
|
+
| `foreignKey:table:column`³ | Foreign key | `int | foreignKey:users:id` |
|
|
1658
1658
|
|
|
1659
1659
|
¹ **Engine-specific.** `unsigned` is honored on MySQL and SQLite. PostgreSQL has no unsigned integer type and silently drops the modifier.
|
|
1660
1660
|
² **Engine-specific.** `onUpdate` is honored on MySQL (emitted via `ON UPDATE <expr>`). PostgreSQL and SQLite log a one-time warning and ignore it — the modifier cannot be expressed inline on those engines. See [`docs/agents/05-multi-db-parity.md`](docs/agents/05-multi-db-parity.md).
|
|
@@ -1662,6 +1662,42 @@ type|modifier1|modifier2|...
|
|
|
1662
1662
|
|
|
1663
1663
|
> **Dependency ordering.** `syncDatabase()` creates tables in foreign-key dependency order (parents before the children that reference them), so you can declare your models in **any order** — a child listed before its parent still syncs cleanly (MySQL/PostgreSQL otherwise reject the forward FK reference). FKs to tables outside the schema impose no ordering; a genuine circular FK dependency falls back to declaration order with a warning.
|
|
1664
1664
|
|
|
1665
|
+
> **Orphan-row guard.** Adding a `notNull` foreign-key column to a table that **already has rows** makes the database back-fill those rows (with `0` for a numeric column, or the column default). If that value has no matching parent row, MySQL/PostgreSQL reject the new constraint with a cryptic driver error (MySQL `errno 1452`, `ER_NO_REFERENCED_ROW_2`). `syncDatabase()` now detects this **before** altering the table and throws a clear `KormError` (`code: 'SYNC_FK_ORPHAN'`) naming the child column, the parent it points at, the affected row count, and the offending back-fill value — **no data is mutated**. Resolve it by making the column `nullable` (existing rows back-fill `NULL`, which is FK-safe), giving it a `default` that exists in the parent, populating the column for existing rows before syncing, or inserting the missing parent rows.
|
|
1666
|
+
|
|
1667
|
+
#### `syncDatabase(options)` — additive-only, resilient, dry-run
|
|
1668
|
+
|
|
1669
|
+
`syncDatabase()` accepts an optional `options` object that controls how it reconciles drift between your schema and the live database. The defaults are **safe**: it never drops data and never half-migrates silently.
|
|
1670
|
+
|
|
1671
|
+
| Option | Default | Effect |
|
|
1672
|
+
| ----------------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
1673
|
+
| `dropColumns` | `false` | When `false` (default), a column that exists in the database but is **absent from the schema** is **left in place** (reported as drift, never dropped). Set `true` to drop such columns. |
|
|
1674
|
+
| `prune` | — | Alias for `dropColumns`. |
|
|
1675
|
+
| `continueOnError` | `false` | When `false` (default), the first failing statement throws and aborts the run (fail-fast). When `true`, a failing statement is **skipped and collected**, and the rest of the schema still applies. |
|
|
1676
|
+
| `dryRun` | `false` | When `true`, walks the same diff **without executing anything** and returns the planned operations (destructive drops flagged). The database is not touched. |
|
|
1677
|
+
|
|
1678
|
+
**Why the additive-only default matters.** Previously `syncDatabase()` dropped any column not declared in the schema — silently destroying data for drifted/legacy columns, and aborting the entire multi-table sync if the drop was blocked (e.g. the column was referenced by a foreign key). The default is now **additive** (create tables, add columns); destructive drops are opt-in.
|
|
1679
|
+
|
|
1680
|
+
**Return value.** A default sync resolves to `undefined` (unchanged). When you pass `dryRun: true` **or** `continueOnError: true`, it resolves to a **report**:
|
|
1681
|
+
|
|
1682
|
+
```javascript
|
|
1683
|
+
// Preview what a sync would do — change nothing
|
|
1684
|
+
const plan = await korm.syncDatabase({ dryRun: true });
|
|
1685
|
+
// {
|
|
1686
|
+
// dryRun: true,
|
|
1687
|
+
// plan: [{ table: 'users', operation: 'add', column: 'bio', destructive: false }, …],
|
|
1688
|
+
// skippedDrops: [{ table: 'products', column: 'warehouse' }], // drift left in place
|
|
1689
|
+
// applied: [], errors: [],
|
|
1690
|
+
// }
|
|
1691
|
+
|
|
1692
|
+
// Drop drifted columns too
|
|
1693
|
+
await korm.syncDatabase({ dropColumns: true });
|
|
1694
|
+
|
|
1695
|
+
// Keep going past failures, then inspect what was skipped
|
|
1696
|
+
const report = await korm.syncDatabase({ continueOnError: true });
|
|
1697
|
+
// report.errors → [{ table, operation, column, message }]
|
|
1698
|
+
// report.applied → [{ table, operation, column }]
|
|
1699
|
+
```
|
|
1700
|
+
|
|
1665
1701
|
**Special Default Values:**
|
|
1666
1702
|
|
|
1667
1703
|
- `now` or `now()` → `CURRENT_TIMESTAMP`
|
|
@@ -1727,7 +1763,9 @@ const result = await korm.processRequestWithOthers(requestBody, modelName, conte
|
|
|
1727
1763
|
// Set schema manually
|
|
1728
1764
|
korm.setSchema(schemaObject);
|
|
1729
1765
|
|
|
1730
|
-
// Sync database with schema (creates
|
|
1766
|
+
// Sync database with schema (additive-only by default: creates tables, adds
|
|
1767
|
+
// columns; never drops drifted columns unless you opt in). Options:
|
|
1768
|
+
// { dropColumns | prune, continueOnError, dryRun } — see "syncDatabase(options)".
|
|
1731
1769
|
await korm.syncDatabase();
|
|
1732
1770
|
|
|
1733
1771
|
// Generate schema from existing database
|
|
@@ -1766,18 +1804,18 @@ const modelInstance = korm.getModelInstance(modelDef);
|
|
|
1766
1804
|
|
|
1767
1805
|
### ProcessRequest Actions Summary
|
|
1768
1806
|
|
|
1769
|
-
| Action | Description
|
|
1770
|
-
| --------- |
|
|
1771
|
-
| `list` | Get multiple records
|
|
1772
|
-
| `show` | Get single record
|
|
1773
|
-
| `create` | Create new record
|
|
1774
|
-
| `update` | Update record(s)
|
|
1775
|
-
| `delete` | Delete record(s)
|
|
1776
|
-
| `count` | Count records
|
|
1777
|
-
| `sum` | Sum column or expression
|
|
1778
|
-
| `replace` | Replace full row by PK (all engines; pg = merge, see §8) | `data` (optional: `conflict`)
|
|
1779
|
-
| `upsert` | Insert or update
|
|
1780
|
-
| `sync` | Upsert + delete
|
|
1807
|
+
| Action | Description | Required Fields |
|
|
1808
|
+
| --------- | -------------------------------------------------------- | ---------------------------------------------------------------- |
|
|
1809
|
+
| `list` | Get multiple records | None (optional: `where`, `select`, `orderBy`, `limit`, `offset`) |
|
|
1810
|
+
| `show` | Get single record | `where` |
|
|
1811
|
+
| `create` | Create new record | `data` |
|
|
1812
|
+
| `update` | Update record(s) | `where`, `data` |
|
|
1813
|
+
| `delete` | Delete record(s) | `where` |
|
|
1814
|
+
| `count` | Count records | None (optional: `where`) |
|
|
1815
|
+
| `sum` | Sum column or expression | `data.sumColumn` or `data.sumFormula` (optional: `where`) |
|
|
1816
|
+
| `replace` | Replace full row by PK (all engines; pg = merge, see §8) | `data` (optional: `conflict`) |
|
|
1817
|
+
| `upsert` | Insert or update | `data`, `conflict` |
|
|
1818
|
+
| `sync` | Upsert + delete | `data`, `conflict`, `where` |
|
|
1781
1819
|
|
|
1782
1820
|
### Validation Rules
|
|
1783
1821
|
|
|
@@ -2324,14 +2362,16 @@ app.post('/api/:model', (req, res) =>
|
|
|
2324
2362
|
extends the native `Error`). Branch on `error.code` rather than
|
|
2325
2363
|
string-matching `error.message`. Full reference: [`doc/ERRORS.md`](doc/ERRORS.md).
|
|
2326
2364
|
|
|
2327
|
-
| `code` | Meaning
|
|
2328
|
-
| ----------------------- |
|
|
2329
|
-
| `NO_MATCHING_ROW` | A mutating action matched no row
|
|
2330
|
-
| `UNKNOWN_ACTION` | Action isn't built-in and has no custom hook
|
|
2331
|
-
| `NO_CUSTOM_ACTION_HOOK` | Custom action requested, no hook on the model
|
|
2332
|
-
| `VALIDATION_FAILED` | Input failed validation (`error.context.fields`)
|
|
2333
|
-
| `UNKNOWN_MODEL` | Model name not in the schema (`error.context.available`)
|
|
2334
|
-
| `
|
|
2365
|
+
| `code` | Meaning |
|
|
2366
|
+
| ----------------------- | ---------------------------------------------------------------- |
|
|
2367
|
+
| `NO_MATCHING_ROW` | A mutating action matched no row |
|
|
2368
|
+
| `UNKNOWN_ACTION` | Action isn't built-in and has no custom hook |
|
|
2369
|
+
| `NO_CUSTOM_ACTION_HOOK` | Custom action requested, no hook on the model |
|
|
2370
|
+
| `VALIDATION_FAILED` | Input failed validation (`error.context.fields`) |
|
|
2371
|
+
| `UNKNOWN_MODEL` | Model name not in the schema (`error.context.available`) |
|
|
2372
|
+
| `FORBIDDEN` | An `authorize()` predicate denied the request |
|
|
2373
|
+
| `SYNC_FK_ORPHAN` | Sync would orphan rows with a new FK (`error.context.offenders`) |
|
|
2374
|
+
| `INTERNAL` | Internal invariant / misconfiguration |
|
|
2335
2375
|
|
|
2336
2376
|
```javascript
|
|
2337
2377
|
const { KormError } = require('@dreamtree-org/korm-js');
|
package/ai-skills/korm-js.md
CHANGED
|
@@ -38,39 +38,39 @@ In Express/Next/Fastify the consumer just forwards `req.body` and the model name
|
|
|
38
38
|
|
|
39
39
|
`processRequest(request, modelName)` accepts a JSON object with these top-level fields:
|
|
40
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
|
|
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
49
|
| `orderBy` | object \| array \| string | `{column, direction}` / `"column"` / array of either. Object MUST use the `column` key — `{created_at:"desc"}` is rejected (`VALIDATION_FAILED`) |
|
|
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)
|
|
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
59
|
|
|
60
60
|
### Actions
|
|
61
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`
|
|
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
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
|
|
72
|
+
| `upsert` | Insert-or-update keyed by `conflict` columns |
|
|
73
|
+
| `sync` | Upsert matching `data` + delete non-matching within `where` scope |
|
|
74
74
|
|
|
75
75
|
### `where` operator cheat-sheet
|
|
76
76
|
|
|
@@ -139,15 +139,16 @@ Bindings come back as a separate array (never interpolated into `sql`).
|
|
|
139
139
|
`processRequest` throws a `KormError` (extends `Error`, so `e.message`
|
|
140
140
|
still works) with a machine-readable `code` you can branch on:
|
|
141
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
|
-
| `
|
|
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
|
+
| `SYNC_FK_ORPHAN` | `syncDatabase()` would add a foreign key that orphans existing rows (`e.context.offenders`); no data is mutated |
|
|
151
|
+
| `INTERNAL` | Internal invariant / misconfiguration |
|
|
151
152
|
|
|
152
153
|
```js
|
|
153
154
|
const { KormError } = require('@dreamtree-org/korm-js');
|
package/clients/BaseSyncTable.js
CHANGED
|
@@ -1 +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
|
|
1
|
+
const logger=require("../Logger"),KormError=require("../KormError"),SyncRunner=require("./SyncRunner"),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)},NUMERIC_TYPES=new Set(["INT","INTEGER","MEDIUMINT","SMALLINT","BIGINT","TINYINT","FLOAT","DOUBLE","REAL","DECIMAL","NUMERIC"]),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,t={}){return new SyncRunner(this,t).runTable(e)}async syncDatabase(e={}){if(!this.controllerWrapper?.schema)throw new Error("controllerWrapper.schema not set.");return new SyncRunner(this,e).runDatabase(this.controllerWrapper.schema)}_orderTablesByDependency(e){const t=Object.keys(e),n=this._buildFkDependencyMap(e,t),r=this._topoSort(t,n);if(r.length<t.length){const e=new Set(r),n=t.filter(t=>!e.has(t));logger.warn("syncDatabase: circular foreign-key dependency among",n,"— creating in declaration order; FK constraints may need a second pass."),r.push(...n)}return r}_buildFkDependencyMap(e,t){const n=new Map;for(const r of t){n.set(r,r);const t=e[r]?.table;t&&n.set(t,r)}const r=new Map;for(const a of t){const t=new Set;for(const r of this._foreignTargetsOf(e[a])){const e=n.get(r);e&&e!==a&&t.add(e)}r.set(a,t)}return r}_topoSort(e,t){const n=[],r=new Set;let a=!0;for(;n.length<e.length&&a;){a=!1;for(const s of e)!r.has(s)&&this._depsSatisfied(t.get(s),r)&&(n.push(s),r.add(s),a=!0)}return n}_depsSatisfied(e,t){for(const n of e)if(!t.has(n))return!1;return!0}_foreignTargetsOf(e){const t=[];for(const[n,r]of Object.entries(e?.columns||{})){const e="string"==typeof r?this.utils.formatColumnSchema(n,r):r;if(e?.hasForeignKey&&Array.isArray(e.foreignMapTables))for(const n of e.foreignMapTables)n?.table&&t.push(n.table)}return t}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 _assertNoOrphanForeignKeys(e,t){const n=[];for(const r of t?.add||[]){const t=await this._detectOrphanForNewFkColumn(e,r);t&&n.push(t)}if(n.length)throw KormError.foreignKeyOrphans({table:e,offenders:n})}async _detectOrphanForNewFkColumn(e,t){const n=this._fkOrphanProbe(t);if(!n)return null;const r=await this._tableRowCount(e);return 0===r?null:await this.existsTable(n.parentTable)?await this._parentHasValue(n.parentTable,n.parentColumn,n.backfillValue)?null:{column:t.name,parentTable:n.parentTable,parentColumn:n.parentColumn,rowsAffected:r,backfillValue:n.backfillValue}:null}_fkOrphanProbe(e){if(!e?.hasForeignKey)return null;const t=e.foreignMapTables?.[0];if(!t?.table)return null;const n=null!=e.default&&""!==e.default;return e.nullable&&!n?null:{parentTable:t.table,parentColumn:t.column||"id",backfillValue:n?e.default:this._impliedBackfillValue(e)}}_impliedBackfillValue(e){const t=String(e.type||"").toUpperCase(),n=String(e.columnType||"").toUpperCase().match(/^[A-Z]+/);return NUMERIC_TYPES.has(t)||n&&NUMERIC_TYPES.has(n[0])?0:""}async _tableRowCount(e){const t=await this.db(e).count("* as n").first();return Number(t?.n)||0}async _parentHasValue(e,t,n){return!!await this.db(e).where(t,n).first()}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;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const logger=require("../Logger");function normalizeOptions(t={}){return{dropColumns:!!(null!=t.dropColumns?t.dropColumns:t.prune),continueOnError:!!t.continueOnError,dryRun:!!t.dryRun}}class SyncRunner{constructor(t,o={}){this.sync=t,this.opts=normalizeOptions(o),this.report={dryRun:this.opts.dryRun,applied:[],plan:[],skippedDrops:[],errors:[]}}result(){return this.opts.dryRun||this.opts.continueOnError?this.report:void 0}async runDatabase(t){for(const o of this.sync._orderTablesByDependency(t))await this._syncTable(t[o]);return this.opts.dryRun||logger.info("Database synced by SyncTable..."),this.result()}async runTable(t){return await this._syncTable(t),this.result()}async _syncTable(t){const o=t.table;(await this.sync.existsTable(o)?await this._alter(t,o):await this._create(t,o))&&!this.opts.dryRun&&await this._extrasAndSeed(t,o)}async _create(t,o){if(this.opts.dryRun)return this.report.plan.push({table:o,operation:"createTable",column:null,destructive:!1}),!0;const r=await this._attempt({table:o,operation:"createTable",column:null},()=>this.sync.createTable(t));return r&&this.report.applied.push({table:o,operation:"createTable",column:null}),r}async _alter(t,o){const r=await this.sync.getAlterations(t);if(this._recordDrops(o,r),this.opts.dryRun)return this._planAlter(o,r),!0;const n={add:await this._screenOrphans(o,r.add||[]),drop:this.opts.dropColumns&&r.drop||[],modify:r.modify||[]};return await this._applyWork(o,n),!0}_recordDrops(t,o){if(!this.opts.dropColumns)for(const r of o.drop||[])this.report.skippedDrops.push({table:t,column:r.name}),logger.warn(`syncDatabase: column "${t}.${r.name}" exists in the database but not in the schema; left in place (additive-only). Pass { dropColumns: true } to drop it.`)}_planAlter(t,o){for(const r of o.add||[])this.report.plan.push({table:t,operation:"add",column:r.name,destructive:!1});for(const r of o.modify||[])this.report.plan.push({table:t,operation:"modify",column:r.name,destructive:!1});if(this.opts.dropColumns)for(const r of o.drop||[])this.report.plan.push({table:t,operation:"drop",column:r.name,destructive:!0})}async _screenOrphans(t,o){if(!this.opts.continueOnError)return await this.sync._assertNoOrphanForeignKeys(t,{add:o}),o;const r=[];for(const n of o){const o=await this.sync._detectOrphanForNewFkColumn(t,n);o?this.report.errors.push({table:t,operation:"add",column:n.name,message:`adding FK column would orphan ${o.rowsAffected} row(s) with no matching ${o.parentTable}.${o.parentColumn}`}):r.push(n)}return r}async _applyWork(t,o){const r=this._ops(o);if(0!==r.length)if(this.opts.continueOnError)for(const o of r){await this._attempt({table:t,operation:o.operation,column:o.column},()=>this.sync.alterTable(t,this._single(o)))&&this.report.applied.push({table:t,operation:o.operation,column:o.column})}else{await this.sync.alterTable(t,o);for(const o of r)this.report.applied.push({table:t,operation:o.operation,column:o.column})}}_ops(t){const o=[];for(const r of t.add||[])o.push({operation:"add",column:r.name,frm:r});for(const r of t.drop||[])o.push({operation:"drop",column:r.name,name:r.name});for(const r of t.modify||[])o.push({operation:"modify",column:r.name,frm:r});return o}_single(t){return"add"===t.operation?{add:[t.frm],drop:[],modify:[]}:"drop"===t.operation?{add:[],drop:[{name:t.name}],modify:[]}:{add:[],drop:[],modify:[t.frm]}}async _extrasAndSeed(t,o){await this._attempt({table:o,operation:"extras",column:null},()=>this.sync._applyExtras(t)),await this._attempt({table:o,operation:"seed",column:null},()=>this.sync.syncSeedData(t,o))}async _attempt(t,o){try{return await o(),!0}catch(o){if(!this.opts.continueOnError)throw o;return this.report.errors.push({...t,message:o.message}),!1}}}module.exports=SyncRunner;
|
package/clients/mysql/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
const BaseWrapper=require("../BaseWrapper"),BaseUtility=require("./BaseUtility"),SyncTable=require("./SyncTable"),CurdTable=require("./CurdTable");class MySQLWrapper extends BaseWrapper{constructor(e){super(e),this.db=e.db,this.utils=new BaseUtility,this.sync=new SyncTable(this.db,this.utils,e),this.curd=new CurdTable(this.db,this.utils,e)}async syncDatabase(){return this.sync.syncDatabase()}async processRequest(...e){return this.curd.processRequest(...e)}async generateSchema(){return this.sync.generateSchema()}}module.exports=MySQLWrapper;
|
|
1
|
+
const BaseWrapper=require("../BaseWrapper"),BaseUtility=require("./BaseUtility"),SyncTable=require("./SyncTable"),CurdTable=require("./CurdTable");class MySQLWrapper extends BaseWrapper{constructor(e){super(e),this.db=e.db,this.utils=new BaseUtility,this.sync=new SyncTable(this.db,this.utils,e),this.curd=new CurdTable(this.db,this.utils,e)}async syncDatabase(e={}){return this.sync.syncDatabase(e)}async processRequest(...e){return this.curd.processRequest(...e)}async generateSchema(){return this.sync.generateSchema()}}module.exports=MySQLWrapper;
|
package/clients/pg/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
const BaseWrapper=require("../BaseWrapper"),BaseUtility=require("./BaseUtility"),SyncTable=require("./SyncTable"),CurdTable=require("./CurdTable");class PostgresWrapper extends BaseWrapper{constructor(e){super(e),this.db=e.db,this.utils=new BaseUtility,this.sync=new SyncTable(this.db,this.utils,e),this.curd=new CurdTable(this.db,this.utils,e)}async syncDatabase(){return this.sync.syncDatabase()}async processRequest(...e){return this.curd.processRequest(...e)}async generateSchema(){return this.sync.generateSchema()}}module.exports=PostgresWrapper;
|
|
1
|
+
const BaseWrapper=require("../BaseWrapper"),BaseUtility=require("./BaseUtility"),SyncTable=require("./SyncTable"),CurdTable=require("./CurdTable");class PostgresWrapper extends BaseWrapper{constructor(e){super(e),this.db=e.db,this.utils=new BaseUtility,this.sync=new SyncTable(this.db,this.utils,e),this.curd=new CurdTable(this.db,this.utils,e)}async syncDatabase(e={}){return this.sync.syncDatabase(e)}async processRequest(...e){return this.curd.processRequest(...e)}async generateSchema(){return this.sync.generateSchema()}}module.exports=PostgresWrapper;
|
package/clients/sqlite/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
const BaseWrapper=require("../BaseWrapper"),BaseUtility=require("./BaseUtility"),SyncTable=require("./SyncTable"),CurdTable=require("./CurdTable");class SQLiteWrapper extends BaseWrapper{constructor(e){super(e),this.db=e.db,this.utils=new BaseUtility,this.sync=new SyncTable(this.db,this.utils,e),this.curd=new CurdTable(this.db,this.utils,e)}async syncDatabase(){return this.sync.syncDatabase()}async processRequest(...e){return this.curd.processRequest(...e)}async generateSchema(){return this.sync.generateSchema()}}module.exports=SQLiteWrapper;
|
|
1
|
+
const BaseWrapper=require("../BaseWrapper"),BaseUtility=require("./BaseUtility"),SyncTable=require("./SyncTable"),CurdTable=require("./CurdTable");class SQLiteWrapper extends BaseWrapper{constructor(e){super(e),this.db=e.db,this.utils=new BaseUtility,this.sync=new SyncTable(this.db,this.utils,e),this.curd=new CurdTable(this.db,this.utils,e)}async syncDatabase(e={}){return this.sync.syncDatabase(e)}async processRequest(...e){return this.curd.processRequest(...e)}async generateSchema(){return this.sync.generateSchema()}}module.exports=SQLiteWrapper;
|
package/index.d.ts
CHANGED
|
@@ -345,7 +345,11 @@ export class KormError extends Error {
|
|
|
345
345
|
static ACTIONS: readonly string[];
|
|
346
346
|
static closestAction(input: string, candidates?: string[]): string | null;
|
|
347
347
|
static noMatchingRow(opts: { action: string; model: string }): KormError;
|
|
348
|
-
static unknownAction(opts: {
|
|
348
|
+
static unknownAction(opts: {
|
|
349
|
+
action: string;
|
|
350
|
+
model?: string;
|
|
351
|
+
hasCustomHook?: boolean;
|
|
352
|
+
}): KormError;
|
|
349
353
|
static unknownModel(opts: { model: string; available?: string[] }): KormError;
|
|
350
354
|
static validationFailed(opts: { errors?: any[]; source?: string | null }): KormError;
|
|
351
355
|
static forbidden(opts: {
|
|
@@ -416,9 +420,5 @@ export interface CreateMcpServerOptions {
|
|
|
416
420
|
|
|
417
421
|
export const mcp: {
|
|
418
422
|
createServer(opts: CreateMcpServerOptions): McpServer;
|
|
419
|
-
generateTools(opts: {
|
|
420
|
-
controller: any;
|
|
421
|
-
schema: any;
|
|
422
|
-
mcpConfig: McpConfig;
|
|
423
|
-
}): McpTool[];
|
|
423
|
+
generateTools(opts: { controller: any; schema: any; mcpConfig: McpConfig }): McpTool[];
|
|
424
424
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dreamtree-org/korm-js",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Knowledge Object-Relational Mapping - A powerful, modular ORM system for Node.js with dynamic database operations, complex queries, relationships, and nested requests",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Partha Preetham Krishna",
|