@dreamtree-org/korm-js 1.0.56 → 1.0.57
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 +11 -0
- package/examples/nextjs-seo-geo/app/api/[model]/route.js +1 -0
- package/examples/nextjs-seo-geo/app/layout.js +18 -0
- package/examples/nextjs-seo-geo/app/llms.txt/route.js +1 -0
- package/examples/nextjs-seo-geo/app/page.js +29 -0
- package/examples/nextjs-seo-geo/app/posts/[slug]/JsonLd.js +27 -0
- package/examples/nextjs-seo-geo/app/posts/[slug]/page.js +59 -0
- package/examples/nextjs-seo-geo/app/posts/actions.js +1 -0
- package/examples/nextjs-seo-geo/app/posts/new/page.js +26 -0
- package/examples/nextjs-seo-geo/app/robots.js +1 -0
- package/examples/nextjs-seo-geo/app/sitemap.js +1 -0
- package/examples/nextjs-seo-geo/lib/auth.js +1 -0
- package/examples/nextjs-seo-geo/lib/korm.js +1 -0
- package/examples/nextjs-seo-geo/lib/site.js +1 -0
- package/examples/nextjs-seo-geo/next.config.js +1 -0
- package/examples/nextjs-seo-geo/scripts/seed.js +1 -0
- package/package.json +1 -1
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
|
|
@@ -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.
|
|
3
|
+
"version": "1.0.57",
|
|
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",
|