@dreamtree-org/korm-js 1.0.56 → 1.0.58

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -186,6 +186,17 @@ app.get('/health', (req, res) => {
186
186
  });
187
187
  ```
188
188
 
189
+ ## Using KORM-JS with Next.js
190
+
191
+ KORM-JS is HTTP-framework-agnostic — mount `processRequest` inside the route
192
+ handlers / Server Actions Next.js already gives you (App Router). The complete
193
+ integration guide (setup, route handlers, hooks, deployment, and an
194
+ **SEO + GEO** section: `generateMetadata`, JSON-LD, `sitemap`, `robots`,
195
+ `llms.txt`) lives in [`docs/NEXTJS.md`](docs/NEXTJS.md).
196
+
197
+ A runnable App-Router example backed by KORM over SQLite is in
198
+ [`examples/nextjs-seo-geo/`](examples/nextjs-seo-geo/).
199
+
189
200
  ## Complete CRUD Operations Guide
190
201
 
191
202
  ### 1. Create Operation
@@ -1642,6 +1653,8 @@ type|modifier1|modifier2|...
1642
1653
  ² **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).
1643
1654
  ³ **Auto type-matching.** When the referenced table is part of the same schema, `syncDatabase()` automatically widens the foreign-key column's type to match the referenced primary key, so the FK constraint is always type-compatible. In particular, an `autoIncrement` primary key is emitted as `BIGINT UNSIGNED` (MySQL) / `BIGSERIAL` (PostgreSQL) / `INTEGER` (SQLite) via Knex's `.increments()`, so a child column written as `int|foreignKey:…` is created as `BIGINT` — you do **not** need to hand-match the width. Foreign keys to tables outside the schema (pre-existing/third-party) are created exactly as declared.
1644
1655
 
1656
+ > **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.
1657
+
1645
1658
  **Special Default Values:**
1646
1659
 
1647
1660
  - `now` or `now()` → `CURRENT_TIMESTAMP`
@@ -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){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
+ 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 this._orderTablesByDependency(e))await this.syncTable(e[t]),await this.syncSeedData(e[t],t);logger.info("Database synced by SyncTable...")}_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 s of t){const t=new Set;for(const r of this._foreignTargetsOf(e[s])){const e=n.get(r);e&&e!==s&&t.add(e)}r.set(s,t)}return r}_topoSort(e,t){const n=[],r=new Set;let s=!0;for(;n.length<e.length&&s;){s=!1;for(const a of e)!r.has(a)&&this._depsSatisfied(t.get(a),r)&&(n.push(a),r.add(a),s=!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 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,s]of Object.entries(e.columns)){const e=this._resolveColumnFrm(r,s),a=n[r];a?this.hasColumnChanged(a,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],s=String(r.type||"").toLowerCase();return t[n]=COLUMN_STRING_SUFFIXES.reduce((e,t)=>e+t(r),s),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(),s=this._typeDispatcher()[r];return s?s(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
+ import{NextResponse}from"next/server";import{KormError}from"@dreamtree-org/korm-js";import{getKorm}from"@/lib/korm";import{getAuthContext}from"@/lib/auth";const STATUS_BY_CODE={VALIDATION_FAILED:400,UNKNOWN_ACTION:400,NO_CUSTOM_ACTION_HOOK:400,UNKNOWN_MODEL:404,NO_MATCHING_ROW:404,FORBIDDEN:403,INTERNAL:500};export async function POST(t,{params:e}){const{model:o}=await e,r=await getKorm(),s=await getAuthContext(t);let n;try{n=await t.json()}catch{return NextResponse.json({error:"BAD_JSON",message:"Invalid JSON body"},{status:400})}try{const t=await r.processRequest(n,o,s);return NextResponse.json(t)}catch(t){if(t instanceof KormError){const e=STATUS_BY_CODE[t.code]??500;return NextResponse.json({error:t.code,message:t.message,context:t.context},{status:e})}throw t}}
@@ -0,0 +1,18 @@
1
+ // app/layout.js — root layout + site-wide default metadata.
2
+ import { SITE_URL, SITE_NAME, SITE_TAGLINE } from '@/lib/site';
3
+
4
+ export const metadata = {
5
+ metadataBase: new URL(SITE_URL),
6
+ title: { default: SITE_NAME, template: `%s · ${SITE_NAME}` },
7
+ description: SITE_TAGLINE,
8
+ openGraph: { siteName: SITE_NAME, type: 'website' },
9
+ twitter: { card: 'summary_large_image' },
10
+ };
11
+
12
+ export default function RootLayout({ children }) {
13
+ return (
14
+ <html lang="en">
15
+ <body>{children}</body>
16
+ </html>
17
+ );
18
+ }
@@ -0,0 +1 @@
1
+ import{getKorm}from"@/lib/korm";import{SITE_NAME,SITE_TAGLINE,postUrl}from"@/lib/site";export const revalidate=300;export async function GET(){const t=await getKorm(),{data:e}=await t.processRequest({action:"list",where:{status:"published"},select:["title","slug","excerpt"],orderBy:"-created_at",limit:100},"Post",{}),r=[`# ${SITE_NAME}`,`> ${SITE_TAGLINE}`,"","## Posts",...e.map(t=>`- [${t.title}](${postUrl(t.slug)}): ${t.excerpt||""}`),""].join("\n");return new Response(r,{headers:{"content-type":"text/plain; charset=utf-8"}})}
@@ -0,0 +1,29 @@
1
+ // app/page.js — home: list published posts read directly from KORM (no fetch hop).
2
+ import Link from 'next/link';
3
+ import { getKorm } from '@/lib/korm';
4
+ import { SITE_NAME, SITE_TAGLINE } from '@/lib/site';
5
+
6
+ export const revalidate = 60; // DB reads aren't auto-cached; revalidate the segment.
7
+
8
+ export default async function HomePage() {
9
+ const korm = await getKorm();
10
+ const { data: posts } = await korm.processRequest(
11
+ { action: 'list', where: { status: 'published' }, orderBy: '-created_at', limit: 50 },
12
+ 'Post',
13
+ {},
14
+ );
15
+
16
+ return (
17
+ <main>
18
+ <h1>{SITE_NAME}</h1>
19
+ <p>{SITE_TAGLINE}</p>
20
+ <ul>
21
+ {posts.map((p) => (
22
+ <li key={p.id}>
23
+ <Link href={`/posts/${p.slug}`}>{p.title}</Link>
24
+ </li>
25
+ ))}
26
+ </ul>
27
+ </main>
28
+ );
29
+ }
@@ -0,0 +1,27 @@
1
+ // app/posts/[slug]/JsonLd.js — schema.org Article JSON-LD built from a KORM row.
2
+ //
3
+ // This is the single most important GEO (Generative Engine Optimization) signal:
4
+ // ChatGPT, Perplexity, Claude, and Google AI Overviews extract structured facts
5
+ // from JSON-LD far more reliably than from prose.
6
+ import { postUrl } from '@/lib/site';
7
+
8
+ export function ArticleJsonLd({ post }) {
9
+ const ld = {
10
+ '@context': 'https://schema.org',
11
+ '@type': 'Article',
12
+ headline: post.title,
13
+ description: post.excerpt,
14
+ datePublished: post.created_at,
15
+ dateModified: post.updated_at || post.created_at,
16
+ mainEntityOfPage: postUrl(post.slug),
17
+ image: post.cover_image || undefined,
18
+ author: post.Author ? { '@type': 'Person', name: post.Author.name } : undefined,
19
+ };
20
+
21
+ return (
22
+ <script
23
+ type="application/ld+json"
24
+ dangerouslySetInnerHTML={{ __html: JSON.stringify(ld) }}
25
+ />
26
+ );
27
+ }
@@ -0,0 +1,59 @@
1
+ // app/posts/[slug]/page.js — per-post page with full SEO + GEO metadata.
2
+ import { notFound } from 'next/navigation';
3
+ import { getKorm } from '@/lib/korm';
4
+ import { SITE_NAME, postUrl } from '@/lib/site';
5
+ import { ArticleJsonLd } from './JsonLd';
6
+
7
+ export const revalidate = 60;
8
+
9
+ async function getPost(slug) {
10
+ const korm = await getKorm();
11
+ const { data } = await korm.processRequest(
12
+ { action: 'show', where: { slug, status: 'published' }, with: ['Author'] },
13
+ 'Post',
14
+ {},
15
+ );
16
+ return data;
17
+ }
18
+
19
+ // SEO surface: <title>, description, canonical, Open Graph, Twitter — all from the row.
20
+ export async function generateMetadata({ params }) {
21
+ const { slug } = await params;
22
+ const post = await getPost(slug);
23
+ if (!post) return {};
24
+
25
+ const url = postUrl(post.slug);
26
+ return {
27
+ title: post.title,
28
+ description: post.excerpt,
29
+ alternates: { canonical: url },
30
+ openGraph: {
31
+ type: 'article',
32
+ url,
33
+ title: post.title,
34
+ description: post.excerpt,
35
+ siteName: SITE_NAME,
36
+ publishedTime: post.created_at,
37
+ modifiedTime: post.updated_at,
38
+ authors: post.Author ? [post.Author.name] : [],
39
+ images: post.cover_image ? [{ url: post.cover_image }] : [],
40
+ },
41
+ twitter: { card: 'summary_large_image', title: post.title, description: post.excerpt },
42
+ };
43
+ }
44
+
45
+ export default async function PostPage({ params }) {
46
+ const { slug } = await params;
47
+ const post = await getPost(slug);
48
+ if (!post) notFound();
49
+
50
+ return (
51
+ <article>
52
+ {/* GEO surface: schema.org JSON-LD — what generative engines parse for facts. */}
53
+ <ArticleJsonLd post={post} />
54
+ <h1>{post.title}</h1>
55
+ {post.Author ? <p>By {post.Author.name}</p> : null}
56
+ <div dangerouslySetInnerHTML={{ __html: post.html || `<p>${post.body || ''}</p>` }} />
57
+ </article>
58
+ );
59
+ }
@@ -0,0 +1 @@
1
+ import{revalidatePath}from"next/cache";import{getKorm}from"@/lib/korm";import{getAuthContext}from"@/lib/auth";export async function createPost(t){const e=await getKorm(),a=await getAuthContext();await e.processRequest({action:"create",data:{title:t.get("title"),slug:t.get("slug"),excerpt:t.get("excerpt"),body:t.get("body"),status:"published"}},"Post",a),revalidatePath("/"),revalidatePath("/sitemap.xml")}
@@ -0,0 +1,26 @@
1
+ // app/posts/new/page.js — form wired to the createPost Server Action.
2
+ import { redirect } from 'next/navigation';
3
+ import { createPost } from '../actions';
4
+
5
+ export const metadata = { title: 'New post', robots: { index: false } };
6
+
7
+ async function submit(formData) {
8
+ 'use server';
9
+ await createPost(formData);
10
+ redirect(`/posts/${formData.get('slug')}`);
11
+ }
12
+
13
+ export default function NewPostPage() {
14
+ return (
15
+ <main>
16
+ <h1>New post</h1>
17
+ <form action={submit}>
18
+ <p><input name="title" placeholder="Title" required /></p>
19
+ <p><input name="slug" placeholder="slug" required /></p>
20
+ <p><input name="excerpt" placeholder="Excerpt" /></p>
21
+ <p><textarea name="body" placeholder="Body" /></p>
22
+ <button type="submit">Publish</button>
23
+ </form>
24
+ </main>
25
+ );
26
+ }
@@ -0,0 +1 @@
1
+ import{SITE_URL}from"@/lib/site";export default function robots(){return{rules:[{userAgent:"*",allow:"/",disallow:["/api/"]}],sitemap:`${SITE_URL}/sitemap.xml`,host:SITE_URL}}
@@ -0,0 +1 @@
1
+ import{getKorm}from"@/lib/korm";import{SITE_URL,postUrl}from"@/lib/site";export default async function sitemap(){const t=await getKorm(),{data:e}=await t.processRequest({action:"list",where:{status:"published"},select:["slug","updated_at","created_at"]},"Post",{});return[{url:SITE_URL,changeFrequency:"daily",priority:1},...e.map(t=>({url:postUrl(t.slug),lastModified:t.updated_at||t.created_at,changeFrequency:"weekly",priority:.8}))]}
@@ -0,0 +1 @@
1
+ export async function getAuthContext(){return{user:{id:"demo",role:"admin"},tenantId:"demo-tenant"}}
@@ -0,0 +1 @@
1
+ import"server-only";import path from"node:path";import knex from"knex";import{initializeKORM,helperUtility}from"@dreamtree-org/korm-js";const DB_FILE=process.env.DB_FILE||path.join(process.cwd(),"data","app.db"),db=knex({client:"better-sqlite3",connection:{filename:DB_FILE},useNullAsDefault:!0});export const korm=initializeKORM({db:db,dbClient:"sqlite3",debug:"production"!==process.env.NODE_ENV});let ready;export function getKorm(){return ready||(ready=(async()=>{const e=helperUtility.file.readJSON("schema/schema.json");return e?korm.setSchema(e):korm.setSchema(await korm.generateSchema()),korm})()),ready}
@@ -0,0 +1 @@
1
+ export const SITE_URL=process.env.SITE_URL||"https://example.com";export const SITE_NAME="KORM Blog";export const SITE_TAGLINE="A KORM-JS powered blog demonstrating SEO + GEO.";export const postUrl=o=>`${SITE_URL}/posts/${o}`;
@@ -0,0 +1 @@
1
+ const nextConfig={serverExternalPackages:["knex","better-sqlite3","@dreamtree-org/korm-js"]};module.exports=nextConfig;
@@ -0,0 +1 @@
1
+ const path=require("node:path"),fs=require("node:fs"),knex=require("knex"),{initializeKORM:initializeKORM,helperUtility:helperUtility}=require("@dreamtree-org/korm-js");async function main(){const e=path.join(__dirname,"..","data");fs.mkdirSync(e,{recursive:!0});const t=knex({client:"better-sqlite3",connection:{filename:path.join(e,"app.db")},useNullAsDefault:!0}),a=initializeKORM({db:t,dbClient:"sqlite3",debug:!0}),s=helperUtility.file.readJSON(path.join(__dirname,"..","schema","schema.json"));a.setSchema(s),await a.syncDatabase();const{data:n}=await a.processRequest({action:"create",data:{name:"Ada Lovelace"}},"Author",{}),i=n.id??n.insertId??1,o=[{author_id:i,title:"One JSON contract, three engines",slug:"one-json-contract",excerpt:"How KORM-JS turns a single JSON request into safe SQL across MySQL, Postgres, and SQLite.",body:"KORM-JS exposes a single { action, where, data, select, with } contract...",html:"<p>KORM-JS exposes a single <code>{ action, where, data, select, with }</code> contract.</p>",status:"published"},{author_id:i,title:"SEO + GEO for KORM-backed Next.js apps",slug:"seo-geo-nextjs",excerpt:"Projecting KORM rows into generateMetadata, JSON-LD, sitemap, robots, and llms.txt.",body:"Search engines and generative engines share one source of truth: your data...",html:"<p>Search engines and generative engines share one source of truth: your data.</p>",status:"published"}];for(const e of o)await a.processRequest({action:"create",data:e},"Post",{});console.log("✅ Seeded",o.length,"posts by",n.name),await t.destroy()}main().catch(e=>{console.error("❌ Seed failed:",e),process.exit(1)});
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dreamtree-org/korm-js",
3
- "version": "1.0.56",
3
+ "version": "1.0.58",
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",