contentbase 0.0.2 → 0.0.5
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/CLI.md +593 -0
- package/MCP-SERVER-SPEC.md +453 -0
- package/PRIMER.md +540 -0
- package/README.md +289 -13
- package/docs/README.md +110 -0
- package/docs/TABLE-OF-CONTENTS.md +7 -0
- package/docs/models.ts +38 -0
- package/models.ts +38 -0
- package/package.json +12 -3
- package/src/api/endpoints/actions.ts +34 -0
- package/src/api/endpoints/doc.ts +187 -0
- package/src/api/endpoints/docs-index.ts +26 -0
- package/src/api/endpoints/document.ts +171 -0
- package/src/api/endpoints/documents.ts +71 -0
- package/src/api/endpoints/inspect.ts +16 -0
- package/src/api/endpoints/models.ts +10 -0
- package/src/api/endpoints/query.ts +88 -0
- package/src/api/endpoints/root.ts +7 -0
- package/src/api/endpoints/search-reindex.ts +65 -0
- package/src/api/endpoints/search-status.ts +55 -0
- package/src/api/endpoints/search.ts +104 -0
- package/src/api/endpoints/semantic-search.ts +120 -0
- package/src/api/endpoints/text-search.ts +63 -0
- package/src/api/endpoints/validate.ts +34 -0
- package/src/api/helpers.ts +97 -0
- package/src/base-model.ts +12 -0
- package/src/cli/commands/action.ts +82 -44
- package/src/cli/commands/console.ts +124 -0
- package/src/cli/commands/create.ts +179 -53
- package/src/cli/commands/embed.ts +323 -0
- package/src/cli/commands/export.ts +58 -24
- package/src/cli/commands/extract.ts +174 -0
- package/src/cli/commands/help.ts +81 -0
- package/src/cli/commands/index.ts +17 -0
- package/src/cli/commands/init.ts +72 -48
- package/src/cli/commands/inspect.ts +56 -46
- package/src/cli/commands/mcp.ts +1219 -0
- package/src/cli/commands/search.ts +285 -0
- package/src/cli/commands/serve.ts +348 -0
- package/src/cli/commands/summary.ts +60 -0
- package/src/cli/commands/teach.ts +86 -0
- package/src/cli/commands/text-search.ts +134 -0
- package/src/cli/commands/validate.ts +126 -64
- package/src/cli/index.ts +88 -19
- package/src/cli/load-collection.ts +144 -17
- package/src/cli/registry.ts +28 -0
- package/src/collection.ts +361 -10
- package/src/define-model.ts +101 -4
- package/src/document.ts +47 -1
- package/src/extract-sections.ts +1 -1
- package/src/index.ts +14 -2
- package/src/model-instance.ts +35 -5
- package/src/node-shortcuts.ts +1 -1
- package/src/query/collection-query.ts +96 -9
- package/src/query/index.ts +7 -0
- package/src/query/query-dsl.ts +259 -0
- package/src/relationships/has-many.ts +39 -0
- package/src/relationships/index.ts +7 -2
- package/src/section.ts +2 -0
- package/src/types.ts +23 -1
- package/src/utils/index.ts +1 -0
- package/src/utils/match-pattern.ts +65 -0
- package/src/validator.ts +18 -1
- package/bun.lock +0 -473
- package/examples/sdlc-queries.ts +0 -161
- package/scripts/examples/01-collection-setup.ts +0 -46
- package/scripts/examples/02-querying.ts +0 -67
- package/scripts/examples/03-sections.ts +0 -36
- package/scripts/examples/04-relationships.ts +0 -54
- package/scripts/examples/05-document-api.ts +0 -54
- package/scripts/examples/06-extract-sections.ts +0 -55
- package/scripts/examples/07-validation.ts +0 -46
- package/scripts/examples/08-serialization.ts +0 -51
- package/scripts/examples/lib/format.ts +0 -87
- package/scripts/examples/lib/setup.ts +0 -21
- package/scripts/examples/run-all.ts +0 -43
- package/showcases/national-parks/models.ts +0 -74
- package/showcases/national-parks/parks/acadia.mdx +0 -40
- package/showcases/national-parks/parks/yosemite.mdx +0 -44
- package/showcases/national-parks/parks/zion.mdx +0 -44
- package/showcases/national-parks/queries.ts +0 -103
- package/showcases/national-parks/trails/angels-landing.mdx +0 -19
- package/showcases/national-parks/trails/cathedral-lakes.mdx +0 -19
- package/showcases/national-parks/trails/half-dome.mdx +0 -19
- package/showcases/national-parks/trails/jordan-pond-path.mdx +0 -19
- package/showcases/national-parks/trails/mist-trail.mdx +0 -19
- package/showcases/national-parks/trails/observation-point.mdx +0 -19
- package/showcases/national-parks/trails/precipice-trail.mdx +0 -19
- package/showcases/national-parks/trails/the-narrows.mdx +0 -19
- package/showcases/recipes/cuisines/chinese.mdx +0 -28
- package/showcases/recipes/cuisines/italian.mdx +0 -32
- package/showcases/recipes/cuisines/mexican.mdx +0 -28
- package/showcases/recipes/models.ts +0 -77
- package/showcases/recipes/queries.ts +0 -89
- package/showcases/recipes/recipes/chinese/egg-fried-rice.mdx +0 -43
- package/showcases/recipes/recipes/chinese/mapo-tofu.mdx +0 -47
- package/showcases/recipes/recipes/italian/bruschetta.mdx +0 -38
- package/showcases/recipes/recipes/italian/cacio-e-pepe.mdx +0 -39
- package/showcases/recipes/recipes/italian/tiramisu.mdx +0 -43
- package/showcases/recipes/recipes/mexican/chicken-tinga.mdx +0 -44
- package/showcases/recipes/recipes/mexican/guacamole.mdx +0 -39
- package/showcases/vinyl-collection/albums/bitches-brew.mdx +0 -36
- package/showcases/vinyl-collection/albums/i-put-a-spell-on-you.mdx +0 -35
- package/showcases/vinyl-collection/albums/in-rainbows.mdx +0 -35
- package/showcases/vinyl-collection/albums/kind-of-blue.mdx +0 -32
- package/showcases/vinyl-collection/albums/ok-computer.mdx +0 -37
- package/showcases/vinyl-collection/albums/wild-is-the-wind.mdx +0 -35
- package/showcases/vinyl-collection/artists/miles-davis.mdx +0 -27
- package/showcases/vinyl-collection/artists/nina-simone.mdx +0 -26
- package/showcases/vinyl-collection/artists/radiohead.mdx +0 -27
- package/showcases/vinyl-collection/models.ts +0 -73
- package/showcases/vinyl-collection/queries.ts +0 -87
- package/test/ast-query.test.ts +0 -128
- package/test/collection.test.ts +0 -99
- package/test/define-model.test.ts +0 -78
- package/test/document.test.ts +0 -225
- package/test/extract-sections.test.ts +0 -356
- package/test/fixtures/sdlc/epics/authentication.mdx +0 -42
- package/test/fixtures/sdlc/epics/searching-and-browsing.mdx +0 -21
- package/test/fixtures/sdlc/models.ts +0 -89
- package/test/fixtures/sdlc/stories/authentication/a-user-should-be-able-to-register.mdx +0 -20
- package/test/helpers.ts +0 -21
- package/test/model-instance.test.ts +0 -197
- package/test/query.test.ts +0 -167
- package/test/relationships.test.ts +0 -84
- package/test/section.test.ts +0 -99
- package/test/table-of-contents.test.ts +0 -91
- package/test/validator.test.ts +0 -62
- package/tsconfig.json +0 -18
- package/vitest.config.ts +0 -11
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import matter from 'gray-matter'
|
|
2
|
+
import { renderMarkdownToHtml, rewriteDocLinks } from '../helpers.js'
|
|
3
|
+
|
|
4
|
+
export const path = '/docs/:docPath(.*)'
|
|
5
|
+
export const description = 'Content-negotiated document serving (JSON, HTML, or Markdown)'
|
|
6
|
+
export const tags = ['docs']
|
|
7
|
+
|
|
8
|
+
export async function get(params: any, ctx: any) {
|
|
9
|
+
const collection = ctx.container._contentbaseCollection
|
|
10
|
+
let docPath: string = params.docPath || ''
|
|
11
|
+
|
|
12
|
+
// Determine format from extension or Accept header
|
|
13
|
+
let format = 'json'
|
|
14
|
+
if (docPath.endsWith('.json')) {
|
|
15
|
+
format = 'json'
|
|
16
|
+
docPath = docPath.slice(0, -5)
|
|
17
|
+
} else if (docPath.endsWith('.html')) {
|
|
18
|
+
format = 'html'
|
|
19
|
+
docPath = docPath.slice(0, -5)
|
|
20
|
+
} else if (docPath.endsWith('.md')) {
|
|
21
|
+
format = 'md'
|
|
22
|
+
docPath = docPath.slice(0, -3)
|
|
23
|
+
} else {
|
|
24
|
+
const accept = ctx.request.headers?.accept || ''
|
|
25
|
+
if (accept.includes('text/html')) format = 'html'
|
|
26
|
+
else if (accept.includes('text/markdown')) format = 'md'
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (!collection.available.includes(docPath)) {
|
|
30
|
+
ctx.response.status(404)
|
|
31
|
+
return { error: `Document not found: ${docPath}` }
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const doc = collection.document(docPath)
|
|
35
|
+
const modelDef = collection.findModelDefinition(docPath)
|
|
36
|
+
|
|
37
|
+
switch (format) {
|
|
38
|
+
case 'md': {
|
|
39
|
+
const raw = matter.stringify(doc.content, doc.meta)
|
|
40
|
+
ctx.response.type('text/markdown')
|
|
41
|
+
ctx.response.send(raw)
|
|
42
|
+
return
|
|
43
|
+
}
|
|
44
|
+
case 'html': {
|
|
45
|
+
const html = rewriteDocLinks(await renderMarkdownToHtml(doc.content))
|
|
46
|
+
const page = `<!DOCTYPE html>
|
|
47
|
+
<html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
|
|
48
|
+
<title>${doc.title}</title>
|
|
49
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
50
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
51
|
+
<link href="https://fonts.googleapis.com/css2?family=Fira+Code:wght@400;500;600&family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
|
|
52
|
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/atom-one-dark.min.css">
|
|
53
|
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"><\/script>
|
|
54
|
+
<style>
|
|
55
|
+
*, *::before, *::after { box-sizing: border-box; }
|
|
56
|
+
body {
|
|
57
|
+
font-family: 'Inter', system-ui, -apple-system, sans-serif;
|
|
58
|
+
max-width: 52rem;
|
|
59
|
+
margin: 0 auto;
|
|
60
|
+
padding: 2rem 1.5rem;
|
|
61
|
+
line-height: 1.7;
|
|
62
|
+
color: #1a1a2e;
|
|
63
|
+
background: #fafafa;
|
|
64
|
+
}
|
|
65
|
+
h1 { font-size: 2rem; font-weight: 600; margin: 2rem 0 1rem; color: #0f0f23; }
|
|
66
|
+
h2 { font-size: 1.5rem; font-weight: 600; margin: 2.5rem 0 0.75rem; color: #16163a; border-bottom: 1px solid #e2e2e8; padding-bottom: 0.4rem; }
|
|
67
|
+
h3 { font-size: 1.2rem; font-weight: 600; margin: 2rem 0 0.5rem; color: #1a1a2e; }
|
|
68
|
+
h4, h5, h6 { font-weight: 600; margin: 1.5rem 0 0.5rem; }
|
|
69
|
+
p { margin: 0 0 1rem; }
|
|
70
|
+
a { color: #2563eb; text-decoration: none; }
|
|
71
|
+
a:hover { text-decoration: underline; }
|
|
72
|
+
blockquote {
|
|
73
|
+
border-left: 3px solid #6366f1;
|
|
74
|
+
margin: 1rem 0;
|
|
75
|
+
padding: 0.5rem 1rem;
|
|
76
|
+
background: #f0f0ff;
|
|
77
|
+
color: #3730a3;
|
|
78
|
+
border-radius: 0 6px 6px 0;
|
|
79
|
+
}
|
|
80
|
+
blockquote p { margin: 0; }
|
|
81
|
+
code {
|
|
82
|
+
font-family: 'Fira Code', 'Cascadia Code', 'JetBrains Mono', monospace;
|
|
83
|
+
font-feature-settings: "liga" 1, "calt" 1;
|
|
84
|
+
-webkit-font-feature-settings: "liga" 1, "calt" 1;
|
|
85
|
+
font-size: 0.875em;
|
|
86
|
+
background: #ededf0;
|
|
87
|
+
padding: 0.15em 0.4em;
|
|
88
|
+
border-radius: 4px;
|
|
89
|
+
color: #d6336c;
|
|
90
|
+
}
|
|
91
|
+
pre {
|
|
92
|
+
margin: 1rem 0;
|
|
93
|
+
border-radius: 8px;
|
|
94
|
+
overflow: hidden;
|
|
95
|
+
background: #282c34;
|
|
96
|
+
box-shadow: 0 2px 8px rgba(0,0,0,0.12);
|
|
97
|
+
}
|
|
98
|
+
pre code {
|
|
99
|
+
display: block;
|
|
100
|
+
padding: 1.25rem 1.5rem;
|
|
101
|
+
overflow-x: auto;
|
|
102
|
+
font-size: 0.9rem;
|
|
103
|
+
line-height: 1.6;
|
|
104
|
+
background: none;
|
|
105
|
+
color: #abb2bf;
|
|
106
|
+
border-radius: 0;
|
|
107
|
+
}
|
|
108
|
+
pre code .hljs-comment { font-style: italic; }
|
|
109
|
+
ul, ol { padding-left: 1.5rem; margin: 0.5rem 0 1rem; }
|
|
110
|
+
li { margin: 0.3rem 0; }
|
|
111
|
+
li > ul, li > ol { margin: 0.2rem 0; }
|
|
112
|
+
table { border-collapse: collapse; width: 100%; margin: 1rem 0; }
|
|
113
|
+
th { background: #f0f0f5; font-weight: 600; text-align: left; }
|
|
114
|
+
td, th { border: 1px solid #ddd; padding: 0.6em 0.8em; font-size: 0.95rem; }
|
|
115
|
+
tr:nth-child(even) { background: #f8f8fb; }
|
|
116
|
+
hr { border: none; border-top: 1px solid #e2e2e8; margin: 2rem 0; }
|
|
117
|
+
img { max-width: 100%; height: auto; border-radius: 6px; }
|
|
118
|
+
.meta-header { color: #6b7280; font-size: 0.85rem; margin-bottom: 2rem; padding-bottom: 1rem; border-bottom: 1px solid #e2e2e8; }
|
|
119
|
+
.meta-header span { margin-right: 1.5rem; }
|
|
120
|
+
</style>
|
|
121
|
+
</head>
|
|
122
|
+
<body>
|
|
123
|
+
${doc.meta && Object.keys(doc.meta).length > 0 ? `<div class="meta-header">${Object.entries(doc.meta).filter(([k]) => k !== 'title').map(([k, v]) => `<span><strong>${k}:</strong> ${v}</span>`).join('')}</div>` : ''}
|
|
124
|
+
${html}
|
|
125
|
+
<script>hljs.highlightAll();<\/script>
|
|
126
|
+
</body></html>`
|
|
127
|
+
ctx.response.type('text/html')
|
|
128
|
+
ctx.response.send(page)
|
|
129
|
+
return
|
|
130
|
+
}
|
|
131
|
+
default: {
|
|
132
|
+
const result: Record<string, unknown> = {
|
|
133
|
+
id: doc.id,
|
|
134
|
+
title: doc.title,
|
|
135
|
+
meta: doc.meta,
|
|
136
|
+
content: doc.content,
|
|
137
|
+
outline: doc.toOutline(),
|
|
138
|
+
model: modelDef?.name || null,
|
|
139
|
+
createdAt: doc.createdAt,
|
|
140
|
+
updatedAt: doc.updatedAt,
|
|
141
|
+
size: doc.size,
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (modelDef) {
|
|
145
|
+
const instance = collection.getModel(docPath, modelDef)
|
|
146
|
+
const sectionKeys = modelDef.sections ? Object.keys(modelDef.sections) : []
|
|
147
|
+
const computedKeys = modelDef.computed ? Object.keys(modelDef.computed) : []
|
|
148
|
+
const relationshipKeys = modelDef.relationships ? Object.keys(modelDef.relationships) : []
|
|
149
|
+
|
|
150
|
+
if (sectionKeys.length) {
|
|
151
|
+
result.sections = {}
|
|
152
|
+
for (const key of sectionKeys) {
|
|
153
|
+
try {
|
|
154
|
+
(result.sections as any)[key] = instance.sections[key]
|
|
155
|
+
} catch {}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (computedKeys.length) {
|
|
160
|
+
result.computed = {}
|
|
161
|
+
for (const key of computedKeys) {
|
|
162
|
+
try {
|
|
163
|
+
(result.computed as any)[key] = instance.computed[key]
|
|
164
|
+
} catch {}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (relationshipKeys.length) {
|
|
169
|
+
result.relationships = {}
|
|
170
|
+
for (const key of relationshipKeys) {
|
|
171
|
+
try {
|
|
172
|
+
const rel = (instance.relationships as any)[key]
|
|
173
|
+
if ('fetchAll' in rel) {
|
|
174
|
+
(result.relationships as any)[key] = rel.fetchAll().map((i: any) => ({ id: i.id, title: i.title }))
|
|
175
|
+
} else if ('fetch' in rel) {
|
|
176
|
+
const parent = rel.fetch()
|
|
177
|
+
(result.relationships as any)[key] = parent ? { id: parent.id, title: parent.title } : null
|
|
178
|
+
}
|
|
179
|
+
} catch {}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return result
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { renderTocPage } from '../helpers.js'
|
|
2
|
+
|
|
3
|
+
export const path = '/docs'
|
|
4
|
+
export const description = 'Table of contents for all documents'
|
|
5
|
+
export const tags = ['docs']
|
|
6
|
+
|
|
7
|
+
export async function get(_params: any, ctx: any) {
|
|
8
|
+
const collection = ctx.container._contentbaseCollection
|
|
9
|
+
const accept = ctx.request.headers?.accept || ''
|
|
10
|
+
|
|
11
|
+
if (accept.includes('text/html') || !accept.includes('application/json')) {
|
|
12
|
+
const page = await renderTocPage(collection)
|
|
13
|
+
ctx.response.type('text/html')
|
|
14
|
+
ctx.response.send(page)
|
|
15
|
+
return
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return {
|
|
19
|
+
title: 'Table of Contents',
|
|
20
|
+
documents: collection.available.map((id: string) => {
|
|
21
|
+
const doc = collection.document(id)
|
|
22
|
+
const modelDef = collection.findModelDefinition(id)
|
|
23
|
+
return { id, title: doc.title, model: modelDef?.name || null }
|
|
24
|
+
})
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
import matter from 'gray-matter'
|
|
3
|
+
|
|
4
|
+
export const path = '/api/documents/:pathId(.*)'
|
|
5
|
+
export const description = 'CRUD operations on a single document'
|
|
6
|
+
export const tags = ['documents']
|
|
7
|
+
|
|
8
|
+
export async function get(params: any, ctx: any) {
|
|
9
|
+
const collection = ctx.container._contentbaseCollection
|
|
10
|
+
const pathId = params.pathId
|
|
11
|
+
|
|
12
|
+
if (!collection.available.includes(pathId)) {
|
|
13
|
+
ctx.response.status(404)
|
|
14
|
+
return { error: `Document not found: ${pathId}` }
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const doc = collection.document(pathId)
|
|
18
|
+
const modelDef = collection.findModelDefinition(pathId)
|
|
19
|
+
|
|
20
|
+
const result: Record<string, unknown> = {
|
|
21
|
+
id: doc.id,
|
|
22
|
+
title: doc.title,
|
|
23
|
+
meta: doc.meta,
|
|
24
|
+
content: doc.content,
|
|
25
|
+
outline: doc.toOutline(),
|
|
26
|
+
model: modelDef?.name || null,
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (modelDef) {
|
|
30
|
+
const instance = collection.getModel(pathId, modelDef)
|
|
31
|
+
const sectionKeys = modelDef.sections ? Object.keys(modelDef.sections) : []
|
|
32
|
+
const computedKeys = modelDef.computed ? Object.keys(modelDef.computed) : []
|
|
33
|
+
const relationshipKeys = modelDef.relationships ? Object.keys(modelDef.relationships) : []
|
|
34
|
+
|
|
35
|
+
if (sectionKeys.length) {
|
|
36
|
+
result.sections = {}
|
|
37
|
+
for (const key of sectionKeys) {
|
|
38
|
+
try {
|
|
39
|
+
(result.sections as any)[key] = instance.sections[key]
|
|
40
|
+
} catch {}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (computedKeys.length) {
|
|
45
|
+
result.computed = {}
|
|
46
|
+
for (const key of computedKeys) {
|
|
47
|
+
try {
|
|
48
|
+
(result.computed as any)[key] = instance.computed[key]
|
|
49
|
+
} catch {}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (relationshipKeys.length) {
|
|
54
|
+
result.relationships = {}
|
|
55
|
+
for (const key of relationshipKeys) {
|
|
56
|
+
try {
|
|
57
|
+
const rel = (instance.relationships as any)[key]
|
|
58
|
+
if ('fetchAll' in rel) {
|
|
59
|
+
(result.relationships as any)[key] = rel.fetchAll().map((i: any) => ({ id: i.id, title: i.title }))
|
|
60
|
+
} else if ('fetch' in rel) {
|
|
61
|
+
const parent = rel.fetch()
|
|
62
|
+
(result.relationships as any)[key] = parent ? { id: parent.id, title: parent.title } : null
|
|
63
|
+
}
|
|
64
|
+
} catch {}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return result
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export const putSchema = z.object({
|
|
73
|
+
meta: z.record(z.string(), z.any()).optional(),
|
|
74
|
+
content: z.string().optional(),
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
export async function put(params: any, ctx: any) {
|
|
78
|
+
if (ctx.container._contentbaseReadOnly) {
|
|
79
|
+
ctx.response.status(403)
|
|
80
|
+
return { error: 'Server is running in read-only mode' }
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const collection = ctx.container._contentbaseCollection
|
|
84
|
+
const pathId = ctx.params.pathId
|
|
85
|
+
|
|
86
|
+
if (!collection.available.includes(pathId)) {
|
|
87
|
+
ctx.response.status(404)
|
|
88
|
+
return { error: `Document not found: ${pathId}` }
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const doc = collection.document(pathId)
|
|
92
|
+
const currentMeta = { ...doc.meta }
|
|
93
|
+
const newMeta = params.meta ? { ...currentMeta, ...params.meta } : currentMeta
|
|
94
|
+
const newContent = params.content ?? doc.content
|
|
95
|
+
|
|
96
|
+
const fullContent = matter.stringify(newContent, newMeta)
|
|
97
|
+
await collection.saveItem(pathId, { content: fullContent })
|
|
98
|
+
|
|
99
|
+
return { updated: pathId, meta: newMeta }
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export const patchSchema = z.object({
|
|
103
|
+
heading: z.string(),
|
|
104
|
+
action: z.enum(['replace', 'append', 'remove']),
|
|
105
|
+
content: z.string().optional(),
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
export async function patch(params: any, ctx: any) {
|
|
109
|
+
if (ctx.container._contentbaseReadOnly) {
|
|
110
|
+
ctx.response.status(403)
|
|
111
|
+
return { error: 'Server is running in read-only mode' }
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const collection = ctx.container._contentbaseCollection
|
|
115
|
+
const pathId = ctx.params.pathId
|
|
116
|
+
|
|
117
|
+
if (!collection.available.includes(pathId)) {
|
|
118
|
+
ctx.response.status(404)
|
|
119
|
+
return { error: `Document not found: ${pathId}` }
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
let doc = collection.document(pathId)
|
|
123
|
+
|
|
124
|
+
switch (params.action) {
|
|
125
|
+
case 'replace': {
|
|
126
|
+
if (!params.content) {
|
|
127
|
+
ctx.response.status(400)
|
|
128
|
+
return { error: 'Content is required for replace action' }
|
|
129
|
+
}
|
|
130
|
+
doc = doc.replaceSectionContent(params.heading, params.content)
|
|
131
|
+
break
|
|
132
|
+
}
|
|
133
|
+
case 'append': {
|
|
134
|
+
if (!params.content) {
|
|
135
|
+
ctx.response.status(400)
|
|
136
|
+
return { error: 'Content is required for append action' }
|
|
137
|
+
}
|
|
138
|
+
doc = doc.appendToSection(params.heading, params.content)
|
|
139
|
+
break
|
|
140
|
+
}
|
|
141
|
+
case 'remove': {
|
|
142
|
+
doc = doc.removeSection(params.heading)
|
|
143
|
+
break
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const fullContent = matter.stringify(doc.content, doc.meta)
|
|
148
|
+
await collection.saveItem(pathId, { content: fullContent })
|
|
149
|
+
|
|
150
|
+
return { updated: pathId, action: params.action, heading: params.heading }
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async function del(_params: any, ctx: any) {
|
|
154
|
+
if (ctx.container._contentbaseReadOnly) {
|
|
155
|
+
ctx.response.status(403)
|
|
156
|
+
return { error: 'Server is running in read-only mode' }
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const collection = ctx.container._contentbaseCollection
|
|
160
|
+
const pathId = ctx.params.pathId
|
|
161
|
+
|
|
162
|
+
if (!collection.available.includes(pathId)) {
|
|
163
|
+
ctx.response.status(404)
|
|
164
|
+
return { error: `Document not found: ${pathId}` }
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
await collection.deleteItem(pathId)
|
|
168
|
+
return { deleted: pathId }
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export { del as delete }
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
import matter from 'gray-matter'
|
|
3
|
+
import { resolveModelDef } from '../helpers.js'
|
|
4
|
+
|
|
5
|
+
export const path = '/api/documents'
|
|
6
|
+
export const description = 'List or create documents'
|
|
7
|
+
export const tags = ['documents']
|
|
8
|
+
|
|
9
|
+
export const getSchema = z.object({
|
|
10
|
+
model: z.string().optional(),
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
export async function get(params: any, ctx: any) {
|
|
14
|
+
const collection = ctx.container._contentbaseCollection
|
|
15
|
+
|
|
16
|
+
let ids = collection.available as string[]
|
|
17
|
+
|
|
18
|
+
if (params.model) {
|
|
19
|
+
const def = resolveModelDef(collection, params.model)
|
|
20
|
+
if (!def) {
|
|
21
|
+
ctx.response.status(400)
|
|
22
|
+
return { error: `Unknown model: ${params.model}` }
|
|
23
|
+
}
|
|
24
|
+
const prefix = (def as any).prefix + '/'
|
|
25
|
+
ids = ids.filter((id: string) => id.startsWith(prefix))
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return ids.map((id: string) => {
|
|
29
|
+
const doc = collection.document(id)
|
|
30
|
+
return { id, title: doc.title, meta: doc.meta, size: doc.size, createdAt: doc.createdAt, updatedAt: doc.updatedAt }
|
|
31
|
+
})
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export const postSchema = z.object({
|
|
35
|
+
pathId: z.string(),
|
|
36
|
+
title: z.string(),
|
|
37
|
+
meta: z.record(z.string(), z.any()).optional(),
|
|
38
|
+
model: z.string().optional(),
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
export async function post(params: any, ctx: any) {
|
|
42
|
+
if (ctx.container._contentbaseReadOnly) {
|
|
43
|
+
ctx.response.status(403)
|
|
44
|
+
return { error: 'Server is running in read-only mode' }
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const collection = ctx.container._contentbaseCollection
|
|
48
|
+
|
|
49
|
+
if (collection.available.includes(params.pathId)) {
|
|
50
|
+
ctx.response.status(409)
|
|
51
|
+
return { error: `Document already exists: ${params.pathId}` }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const def = params.model
|
|
55
|
+
? resolveModelDef(collection, params.model)
|
|
56
|
+
: collection.findModelDefinition(params.pathId)
|
|
57
|
+
|
|
58
|
+
const metaData = { ...((def as any)?.defaults || {}), ...(params.meta || {}) }
|
|
59
|
+
|
|
60
|
+
const sectionHeadings = def
|
|
61
|
+
? Object.values((def as any).sections || {}).map((s: any) => `## ${s.heading}\n\n`)
|
|
62
|
+
: []
|
|
63
|
+
|
|
64
|
+
const body = [`# ${params.title}`, '', ...sectionHeadings].join('\n')
|
|
65
|
+
const content = matter.stringify(body, metaData)
|
|
66
|
+
|
|
67
|
+
await collection.saveItem(params.pathId, { content })
|
|
68
|
+
|
|
69
|
+
ctx.response.status(201)
|
|
70
|
+
return { created: params.pathId, model: def ? (def as any).name : null, meta: metaData }
|
|
71
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { buildSchemaJSON } from '../helpers.js'
|
|
2
|
+
|
|
3
|
+
export const path = '/api/inspect'
|
|
4
|
+
export const description = 'Collection overview — models, document count, actions'
|
|
5
|
+
export const tags = ['collection']
|
|
6
|
+
|
|
7
|
+
export async function get(_params: any, ctx: any) {
|
|
8
|
+
const collection = ctx.container._contentbaseCollection
|
|
9
|
+
const schema = buildSchemaJSON(collection)
|
|
10
|
+
return {
|
|
11
|
+
rootPath: collection.rootPath,
|
|
12
|
+
documentCount: collection.available.length,
|
|
13
|
+
models: Object.values(schema),
|
|
14
|
+
actions: collection.availableActions,
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { buildSchemaJSON } from '../helpers.js'
|
|
2
|
+
|
|
3
|
+
export const path = '/api/models'
|
|
4
|
+
export const description = 'All model definitions with schemas, sections, and relationships'
|
|
5
|
+
export const tags = ['collection']
|
|
6
|
+
|
|
7
|
+
export async function get(_params: any, ctx: any) {
|
|
8
|
+
const collection = ctx.container._contentbaseCollection
|
|
9
|
+
return buildSchemaJSON(collection)
|
|
10
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
import { resolveModelDef } from '../helpers.js'
|
|
3
|
+
import { queryDSLSchema, executeQueryDSL } from '../../query/query-dsl.js'
|
|
4
|
+
|
|
5
|
+
export const path = '/api/query'
|
|
6
|
+
export const description = 'Query model instances with filtering'
|
|
7
|
+
export const tags = ['query']
|
|
8
|
+
|
|
9
|
+
export const getSchema = z.object({
|
|
10
|
+
model: z.string(),
|
|
11
|
+
where: z.string().optional(),
|
|
12
|
+
select: z.string().optional(),
|
|
13
|
+
related: z.string().optional(),
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
export const postSchema = queryDSLSchema
|
|
17
|
+
|
|
18
|
+
export async function get(params: any, ctx: any) {
|
|
19
|
+
const collection = ctx.container._contentbaseCollection
|
|
20
|
+
|
|
21
|
+
const def = resolveModelDef(collection, params.model)
|
|
22
|
+
if (!def) {
|
|
23
|
+
ctx.response.status(400)
|
|
24
|
+
return { error: `Unknown model: ${params.model}` }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
let q = collection.query(def)
|
|
28
|
+
|
|
29
|
+
if (params.where) {
|
|
30
|
+
let conditions: any[]
|
|
31
|
+
try {
|
|
32
|
+
conditions = JSON.parse(params.where)
|
|
33
|
+
} catch {
|
|
34
|
+
ctx.response.status(400)
|
|
35
|
+
return { error: 'Invalid JSON in where parameter' }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
for (const condition of conditions) {
|
|
39
|
+
const { path: fieldPath, operator = 'eq', value } = condition
|
|
40
|
+
switch (operator) {
|
|
41
|
+
case 'eq': q = q.where(fieldPath, value); break
|
|
42
|
+
case 'in': q = q.whereIn(fieldPath, value); break
|
|
43
|
+
case 'notIn': q = q.whereNotIn(fieldPath, value); break
|
|
44
|
+
case 'gt': q = q.whereGt(fieldPath, value); break
|
|
45
|
+
case 'lt': q = q.whereLt(fieldPath, value); break
|
|
46
|
+
case 'gte': q = q.whereGte(fieldPath, value); break
|
|
47
|
+
case 'lte': q = q.whereLte(fieldPath, value); break
|
|
48
|
+
case 'contains': q = q.whereContains(fieldPath, value); break
|
|
49
|
+
case 'startsWith': q = q.whereStartsWith(fieldPath, value); break
|
|
50
|
+
case 'endsWith': q = q.whereEndsWith(fieldPath, value); break
|
|
51
|
+
case 'regex': q = q.whereRegex(fieldPath, value); break
|
|
52
|
+
case 'exists': q = q.whereExists(fieldPath); break
|
|
53
|
+
case 'notExists': q = q.whereNotExists(fieldPath); break
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const results = await q.fetchAll()
|
|
59
|
+
const selectFields = params.select ? params.select.split(',').map((s: string) => s.trim()) : null
|
|
60
|
+
const relatedFields = params.related ? params.related.split(',').map((s: string) => s.trim()) : undefined
|
|
61
|
+
|
|
62
|
+
return results.map((instance: any) => {
|
|
63
|
+
const json = instance.toJSON({ related: relatedFields })
|
|
64
|
+
if (selectFields && selectFields.length > 0) {
|
|
65
|
+
const filtered: Record<string, any> = {}
|
|
66
|
+
for (const key of selectFields) {
|
|
67
|
+
if (key in json) filtered[key] = json[key]
|
|
68
|
+
else if (key.startsWith('meta.') && json.meta) {
|
|
69
|
+
filtered[key] = json.meta[key.slice(5)]
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return filtered
|
|
73
|
+
}
|
|
74
|
+
return json
|
|
75
|
+
})
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export async function post(params: any, ctx: any) {
|
|
79
|
+
const collection = ctx.container._contentbaseCollection
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
const dsl = queryDSLSchema.parse(params)
|
|
83
|
+
return await executeQueryDSL(collection, dsl)
|
|
84
|
+
} catch (error: any) {
|
|
85
|
+
ctx.response.status(400)
|
|
86
|
+
return { error: error.message }
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
import pathModule from 'node:path'
|
|
3
|
+
|
|
4
|
+
export const path = '/api/search/reindex'
|
|
5
|
+
export const description = 'Trigger search index rebuild'
|
|
6
|
+
export const tags = ['mutation']
|
|
7
|
+
|
|
8
|
+
export const postSchema = z.object({
|
|
9
|
+
pathIds: z.array(z.string()).optional(),
|
|
10
|
+
force: z.boolean().optional(),
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
export async function post(params: any, ctx: any) {
|
|
14
|
+
const collection = ctx.container._contentbaseCollection
|
|
15
|
+
const rootPath = collection.rootPath
|
|
16
|
+
|
|
17
|
+
const { SemanticSearch } = await import('@soederpop/luca/agi')
|
|
18
|
+
if (!ctx.container.features.available.includes('semanticSearch')) {
|
|
19
|
+
SemanticSearch.attach(ctx.container)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const dbPath = pathModule.join(rootPath, '.contentbase/search.sqlite')
|
|
23
|
+
const ss = ctx.container.feature('semanticSearch', { dbPath })
|
|
24
|
+
await ss.initDb()
|
|
25
|
+
|
|
26
|
+
if (params.pathIds) {
|
|
27
|
+
await ss.reindex(params.pathIds)
|
|
28
|
+
} else if (params.force) {
|
|
29
|
+
await ss.reindex()
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Collect and re-index documents
|
|
33
|
+
const docs: any[] = []
|
|
34
|
+
const targetIds = params.pathIds || collection.available
|
|
35
|
+
|
|
36
|
+
for (const pathId of targetIds) {
|
|
37
|
+
if (!collection.available.includes(pathId)) continue
|
|
38
|
+
const doc = collection.document(pathId)
|
|
39
|
+
const modelDef = collection.findModelDefinition(pathId)
|
|
40
|
+
|
|
41
|
+
docs.push({
|
|
42
|
+
pathId,
|
|
43
|
+
model: modelDef?.name ?? undefined,
|
|
44
|
+
title: doc.title,
|
|
45
|
+
slug: doc.slug,
|
|
46
|
+
meta: doc.meta,
|
|
47
|
+
content: doc.content,
|
|
48
|
+
})
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const toIndex = params.force ? docs : docs.filter((d: any) => ss.needsReindex(d))
|
|
52
|
+
|
|
53
|
+
if (toIndex.length > 0) {
|
|
54
|
+
await ss.indexDocuments(toIndex)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
ss.removeStale(collection.available)
|
|
58
|
+
|
|
59
|
+
const stats = ss.getStats()
|
|
60
|
+
return {
|
|
61
|
+
reindexed: toIndex.length,
|
|
62
|
+
total: docs.length,
|
|
63
|
+
...stats,
|
|
64
|
+
}
|
|
65
|
+
}
|