contentbase 0.0.2 → 0.0.4
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/.mcp.json +9 -0
- package/CLI.md +593 -0
- package/MCP-DESCRIPTIONS-REVIEW.md +122 -0
- package/MCP-SERVER-SPEC.md +453 -0
- package/PRIMER.md +540 -0
- package/README.md +289 -13
- package/bun.lock +43 -4
- package/dist/cnotes +0 -0
- 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/public/web-demo/index.html +813 -0
- package/showcases/node_modules/.cache/.repl_history +3 -0
- package/showcases/vinyl-collection/artists/nina-simone.mdx +1 -1
- package/src/__tests__/semantic-search.integration.test.ts +284 -0
- 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/test/collection.test.ts +118 -2
- package/test/fixtures/sdlc/MODELS.md +106 -0
- package/test/fixtures/sdlc/SKILL.md +404 -0
- package/test/fixtures/sdlc/index.ts +9 -0
- package/test/fixtures/sdlc/models.ts +8 -6
- package/test/fixtures/sdlc/stories/authentication/a-user-should-be-able-to-login.mdx +20 -0
- package/test/fixtures/sdlc/templates/epic.md +23 -0
- package/test/fixtures/sdlc/templates/story.md +19 -0
- package/test/pattern.test.ts +191 -0
- package/test/query-dsl.test.ts +431 -0
- package/test/query.test.ts +29 -0
- package/test/relationships.test.ts +130 -0
- package/test/section.test.ts +61 -0
- package/test/table-of-contents.test.ts +49 -5
package/PRIMER.md
ADDED
|
@@ -0,0 +1,540 @@
|
|
|
1
|
+
# Contentbase API Primer
|
|
2
|
+
|
|
3
|
+
This document teaches you how to use contentbase by example, using theoretical models (`BlogPost`, `Author`, `Tag`). Pair this with your project's generated `MODELS.md` (run `cnotes summary`) to understand the specific models, attributes, sections, relationships, and computed properties available.
|
|
4
|
+
|
|
5
|
+
All examples below assume you already have a loaded collection:
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
import { Collection, defineModel, section, hasMany, belongsTo, z } from "contentbase";
|
|
9
|
+
|
|
10
|
+
const collection = new Collection({ rootPath: "./content" });
|
|
11
|
+
collection.register(BlogPost);
|
|
12
|
+
collection.register(Author);
|
|
13
|
+
await collection.load();
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## 1. Querying
|
|
19
|
+
|
|
20
|
+
Queries start with `collection.query(ModelDefinition)` and return typed model instances.
|
|
21
|
+
|
|
22
|
+
### Fetch all instances
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
const posts = await collection.query(BlogPost).fetchAll();
|
|
26
|
+
// posts: BlogPost instance[]
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### First and last
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
const newest = await collection.query(BlogPost).first();
|
|
33
|
+
const oldest = await collection.query(BlogPost).last();
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### Count
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
const total = await collection.query(BlogPost).count();
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### Filtering with `.where()`
|
|
43
|
+
|
|
44
|
+
The `where` method filters on any dot-path into the model instance (`meta.field`, `title`, `id`, `computed.field`, etc.).
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
// Simple equality
|
|
48
|
+
const drafts = await collection.query(BlogPost)
|
|
49
|
+
.where("meta.status", "draft")
|
|
50
|
+
.fetchAll();
|
|
51
|
+
|
|
52
|
+
// Object form (multiple conditions)
|
|
53
|
+
const results = await collection.query(BlogPost)
|
|
54
|
+
.where({ "meta.status": "published", "meta.category": "engineering" })
|
|
55
|
+
.fetchAll();
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Operator shortcuts
|
|
59
|
+
|
|
60
|
+
Each shortcut method applies a specific comparison operator:
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
// Greater than / less than
|
|
64
|
+
await collection.query(BlogPost).whereGt("meta.wordCount", 1000).fetchAll();
|
|
65
|
+
await collection.query(BlogPost).whereLt("meta.wordCount", 500).fetchAll();
|
|
66
|
+
|
|
67
|
+
// Greater/less than or equal
|
|
68
|
+
await collection.query(BlogPost).whereGte("meta.rating", 4).fetchAll();
|
|
69
|
+
await collection.query(BlogPost).whereLte("meta.rating", 3).fetchAll();
|
|
70
|
+
|
|
71
|
+
// Membership
|
|
72
|
+
await collection.query(BlogPost).whereIn("meta.status", ["draft", "review"]).fetchAll();
|
|
73
|
+
await collection.query(BlogPost).whereNotIn("meta.category", ["archived"]).fetchAll();
|
|
74
|
+
|
|
75
|
+
// String matching
|
|
76
|
+
await collection.query(BlogPost).whereContains("title", "TypeScript").fetchAll();
|
|
77
|
+
await collection.query(BlogPost).whereStartsWith("meta.slug", "intro-").fetchAll();
|
|
78
|
+
await collection.query(BlogPost).whereEndsWith("meta.slug", "-part-2").fetchAll();
|
|
79
|
+
|
|
80
|
+
// Regex
|
|
81
|
+
await collection.query(BlogPost).whereRegex("title", /react|vue/i).fetchAll();
|
|
82
|
+
|
|
83
|
+
// Existence
|
|
84
|
+
await collection.query(BlogPost).whereExists("meta.featuredImage").fetchAll();
|
|
85
|
+
await collection.query(BlogPost).whereNotExists("meta.deletedAt").fetchAll();
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### Chaining (AND logic)
|
|
89
|
+
|
|
90
|
+
All conditions are combined with AND logic:
|
|
91
|
+
|
|
92
|
+
```ts
|
|
93
|
+
const featured = await collection.query(BlogPost)
|
|
94
|
+
.where("meta.status", "published")
|
|
95
|
+
.whereGt("meta.wordCount", 500)
|
|
96
|
+
.whereExists("meta.featuredImage")
|
|
97
|
+
.fetchAll();
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## 2. Working with Model Instances
|
|
103
|
+
|
|
104
|
+
Model instances are created by the query system or directly via `collection.getModel()`.
|
|
105
|
+
|
|
106
|
+
### Core properties
|
|
107
|
+
|
|
108
|
+
```ts
|
|
109
|
+
const post = await collection.query(BlogPost).first();
|
|
110
|
+
|
|
111
|
+
post.id; // "posts/my-first-post" — the path-based ID
|
|
112
|
+
post.title; // "My First Post" — extracted from the first heading
|
|
113
|
+
post.slug; // "my-first-post" — kebab-cased title
|
|
114
|
+
post.meta; // { status: "published", category: "engineering", ... } — typed frontmatter
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### Computed properties
|
|
118
|
+
|
|
119
|
+
Computed properties are lazy getters defined in the model:
|
|
120
|
+
|
|
121
|
+
```ts
|
|
122
|
+
post.computed.readingTime; // 5 (minutes)
|
|
123
|
+
post.computed.isPublished; // true
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### Validation
|
|
127
|
+
|
|
128
|
+
```ts
|
|
129
|
+
const result = await post.validate();
|
|
130
|
+
// { valid: true, errors: [] }
|
|
131
|
+
// or { valid: false, errors: [ZodIssue, ...] }
|
|
132
|
+
|
|
133
|
+
post.hasErrors; // boolean — true after validate() finds issues
|
|
134
|
+
post.errors; // Map<string, ZodIssue>
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### Serialization
|
|
138
|
+
|
|
139
|
+
```ts
|
|
140
|
+
// Basic: includes id, title, and meta
|
|
141
|
+
post.toJSON();
|
|
142
|
+
|
|
143
|
+
// With specific sections, computed, and relationships
|
|
144
|
+
post.toJSON({
|
|
145
|
+
sections: ["summary", "tags"],
|
|
146
|
+
computed: ["readingTime"],
|
|
147
|
+
related: ["author"],
|
|
148
|
+
});
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
---
|
|
152
|
+
|
|
153
|
+
## 3. Sections
|
|
154
|
+
|
|
155
|
+
Sections extract structured data from specific headings in the markdown document.
|
|
156
|
+
|
|
157
|
+
### Defining sections
|
|
158
|
+
|
|
159
|
+
```ts
|
|
160
|
+
import { section } from "contentbase";
|
|
161
|
+
import { toString } from "contentbase"; // re-exported from mdast-util-to-string
|
|
162
|
+
|
|
163
|
+
const BlogPost = defineModel("BlogPost", {
|
|
164
|
+
prefix: "posts",
|
|
165
|
+
meta: z.object({
|
|
166
|
+
status: z.enum(["draft", "published"]).default("draft"),
|
|
167
|
+
}),
|
|
168
|
+
sections: {
|
|
169
|
+
// Extract list items as strings
|
|
170
|
+
tags: section("Tags", {
|
|
171
|
+
extract: (query) =>
|
|
172
|
+
query.selectAll("listItem").map((node) => toString(node)),
|
|
173
|
+
schema: z.array(z.string()),
|
|
174
|
+
}),
|
|
175
|
+
|
|
176
|
+
// Extract the full text of a section
|
|
177
|
+
summary: section("Summary", {
|
|
178
|
+
extract: (query) =>
|
|
179
|
+
query.selectAll("paragraph").map((node) => toString(node)).join("\n"),
|
|
180
|
+
}),
|
|
181
|
+
|
|
182
|
+
// With alternative heading names
|
|
183
|
+
tldr: section("TL;DR", {
|
|
184
|
+
extract: (query) =>
|
|
185
|
+
query.selectAll("paragraph").map((node) => toString(node)).join("\n"),
|
|
186
|
+
alternatives: ["TLDR", "Summary"],
|
|
187
|
+
}),
|
|
188
|
+
},
|
|
189
|
+
});
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
### Accessing sections
|
|
193
|
+
|
|
194
|
+
Sections are lazy — they are only extracted when first accessed:
|
|
195
|
+
|
|
196
|
+
```ts
|
|
197
|
+
const post = await collection.query(BlogPost).first();
|
|
198
|
+
|
|
199
|
+
post.sections.tags; // ["typescript", "react", "testing"]
|
|
200
|
+
post.sections.summary; // "This post covers..."
|
|
201
|
+
post.sections.tldr; // Falls back to "TLDR" or "Summary" heading if "TL;DR" not found
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
### The `extract` function
|
|
205
|
+
|
|
206
|
+
The `extract` function receives an `AstQuery` scoped to the nodes under the section heading (excluding the heading itself). Use it to query the markdown AST:
|
|
207
|
+
|
|
208
|
+
```ts
|
|
209
|
+
// Get all list items
|
|
210
|
+
query.selectAll("listItem").map((node) => toString(node));
|
|
211
|
+
|
|
212
|
+
// Get paragraphs
|
|
213
|
+
query.selectAll("paragraph");
|
|
214
|
+
|
|
215
|
+
// Get code blocks
|
|
216
|
+
query.selectAll("code");
|
|
217
|
+
|
|
218
|
+
// Get links
|
|
219
|
+
query.selectAll("link");
|
|
220
|
+
|
|
221
|
+
// Get tables as structured data
|
|
222
|
+
// (use the document's nodes.tablesAsData for convenience)
|
|
223
|
+
|
|
224
|
+
// Find a specific element
|
|
225
|
+
query.select("paragraph"); // first paragraph only
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
---
|
|
229
|
+
|
|
230
|
+
## 4. Relationships
|
|
231
|
+
|
|
232
|
+
Contentbase supports `hasMany` and `belongsTo` relationships between models.
|
|
233
|
+
|
|
234
|
+
### Defining relationships
|
|
235
|
+
|
|
236
|
+
```ts
|
|
237
|
+
import { hasMany, belongsTo } from "contentbase";
|
|
238
|
+
|
|
239
|
+
const Author = defineModel("Author", {
|
|
240
|
+
prefix: "authors",
|
|
241
|
+
meta: z.object({ name: z.string() }),
|
|
242
|
+
relationships: {
|
|
243
|
+
// Children are extracted from sub-headings under "Posts"
|
|
244
|
+
posts: hasMany(() => BlogPost, {
|
|
245
|
+
heading: "Posts",
|
|
246
|
+
}),
|
|
247
|
+
},
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
const BlogPost = defineModel("BlogPost", {
|
|
251
|
+
prefix: "posts",
|
|
252
|
+
meta: z.object({
|
|
253
|
+
status: z.enum(["draft", "published"]).default("draft"),
|
|
254
|
+
author: z.string().optional(),
|
|
255
|
+
}),
|
|
256
|
+
relationships: {
|
|
257
|
+
// Looks up the parent by extracting a foreign key from the child's meta
|
|
258
|
+
author: belongsTo(() => Author, {
|
|
259
|
+
foreignKey: (doc) => doc.meta.author as string,
|
|
260
|
+
}),
|
|
261
|
+
},
|
|
262
|
+
});
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
**Note:** Relationship targets use thunks (`() => Model`) to allow circular references without import order issues.
|
|
266
|
+
|
|
267
|
+
### hasMany
|
|
268
|
+
|
|
269
|
+
The `hasMany` relationship extracts child models from sub-headings within a parent heading in the document's markdown. For example, an Author document with a "## Posts" heading containing "### My First Post" and "### My Second Post" would yield two BlogPost instances.
|
|
270
|
+
|
|
271
|
+
```ts
|
|
272
|
+
const author = await collection.query(Author).first();
|
|
273
|
+
|
|
274
|
+
// Get all related posts
|
|
275
|
+
const posts = author.relationships.posts.fetchAll();
|
|
276
|
+
|
|
277
|
+
// Get first / last
|
|
278
|
+
const firstPost = author.relationships.posts.first();
|
|
279
|
+
const lastPost = author.relationships.posts.last();
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
### belongsTo
|
|
283
|
+
|
|
284
|
+
The `belongsTo` relationship resolves a parent model by computing its path ID from the child's metadata.
|
|
285
|
+
|
|
286
|
+
```ts
|
|
287
|
+
const post = await collection.query(BlogPost).first();
|
|
288
|
+
|
|
289
|
+
// Get the parent author
|
|
290
|
+
const author = post.relationships.author.fetch();
|
|
291
|
+
author.meta.name; // "Jane Doe"
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
### Custom ID generation
|
|
295
|
+
|
|
296
|
+
By default, hasMany generates child IDs as `{targetPrefix}/{parentSlug}/{childSlug}`. Override with the `id` option:
|
|
297
|
+
|
|
298
|
+
```ts
|
|
299
|
+
posts: hasMany(() => BlogPost, {
|
|
300
|
+
heading: "Posts",
|
|
301
|
+
id: (slug) => `posts/${slug}`,
|
|
302
|
+
});
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
---
|
|
306
|
+
|
|
307
|
+
## 5. Document API
|
|
308
|
+
|
|
309
|
+
Every model instance has a `document` property (non-enumerable) providing access to the underlying markdown document.
|
|
310
|
+
|
|
311
|
+
### Accessing the document
|
|
312
|
+
|
|
313
|
+
```ts
|
|
314
|
+
const post = await collection.query(BlogPost).first();
|
|
315
|
+
const doc = post.document;
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
### Document properties
|
|
319
|
+
|
|
320
|
+
```ts
|
|
321
|
+
doc.id; // "posts/my-first-post"
|
|
322
|
+
doc.title; // "My First Post" — from first heading
|
|
323
|
+
doc.slug; // "my-first-post"
|
|
324
|
+
doc.meta; // frontmatter as object
|
|
325
|
+
doc.content; // raw markdown string (without frontmatter)
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
### Node shortcuts
|
|
329
|
+
|
|
330
|
+
The `doc.nodes` helper provides convenient access to common AST elements:
|
|
331
|
+
|
|
332
|
+
```ts
|
|
333
|
+
doc.nodes.headings; // all Heading nodes
|
|
334
|
+
doc.nodes.firstHeading; // first heading
|
|
335
|
+
doc.nodes.lastHeading; // last heading
|
|
336
|
+
doc.nodes.headingsByDepth; // { 1: [...], 2: [...], 3: [...] }
|
|
337
|
+
doc.nodes.paragraphs; // all paragraphs
|
|
338
|
+
doc.nodes.links; // all links
|
|
339
|
+
doc.nodes.lists; // all lists
|
|
340
|
+
doc.nodes.codeBlocks; // all code blocks
|
|
341
|
+
doc.nodes.tables; // all table nodes
|
|
342
|
+
doc.nodes.tablesAsData; // tables parsed as Record<string, string>[][]
|
|
343
|
+
doc.nodes.leadingElementsAfterTitle; // elements between first and second heading
|
|
344
|
+
doc.nodes.images; // all images
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
### AST querying
|
|
348
|
+
|
|
349
|
+
```ts
|
|
350
|
+
// Get a scoped AstQuery for the full document
|
|
351
|
+
const q = doc.astQuery;
|
|
352
|
+
|
|
353
|
+
q.select("heading"); // first heading
|
|
354
|
+
q.selectAll("heading"); // all headings
|
|
355
|
+
q.headingsAtDepth(2); // all ## headings
|
|
356
|
+
q.findHeadingByText("Introduction"); // find by text (case-insensitive)
|
|
357
|
+
q.findAllHeadingsByText("Note", false); // substring match
|
|
358
|
+
q.findBetween(nodeA, nodeB); // nodes between two nodes
|
|
359
|
+
q.findAllAfter(node); // all nodes after a node
|
|
360
|
+
q.findAllBefore(node); // all nodes before a node
|
|
361
|
+
q.atLine(10); // node at a specific line
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
### Section extraction and querying
|
|
365
|
+
|
|
366
|
+
```ts
|
|
367
|
+
// Extract a section (heading + all content until next same-depth heading)
|
|
368
|
+
const sectionNodes = doc.extractSection("Introduction");
|
|
369
|
+
|
|
370
|
+
// Get an AstQuery scoped to a section's content (excludes the heading itself)
|
|
371
|
+
const sectionQuery = doc.querySection("Introduction");
|
|
372
|
+
sectionQuery.selectAll("listItem"); // list items within the "Introduction" section
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
### Section mutations
|
|
376
|
+
|
|
377
|
+
All mutation methods return a **new Document** by default (immutable). Pass `{ mutate: true }` to modify in place.
|
|
378
|
+
|
|
379
|
+
```ts
|
|
380
|
+
// Remove a section
|
|
381
|
+
const updated = doc.removeSection("Draft Notes");
|
|
382
|
+
|
|
383
|
+
// Replace section content with markdown string
|
|
384
|
+
const updated = doc.replaceSectionContent("Summary", "New summary paragraph here.");
|
|
385
|
+
|
|
386
|
+
// Replace with AST nodes
|
|
387
|
+
const updated = doc.replaceSectionContent("Summary", [paragraphNode]);
|
|
388
|
+
|
|
389
|
+
// Insert before / after a node
|
|
390
|
+
const heading = doc.astQuery.findHeadingByText("Conclusion");
|
|
391
|
+
const updated = doc.insertBefore(heading, "## New Section\n\nContent here.");
|
|
392
|
+
const updated = doc.insertAfter(heading, "Follow-up content.");
|
|
393
|
+
|
|
394
|
+
// Append to end of a section
|
|
395
|
+
const updated = doc.appendToSection("Notes", "- Another note");
|
|
396
|
+
|
|
397
|
+
// Mutate in place instead of creating a new document
|
|
398
|
+
doc.removeSection("Draft Notes", { mutate: true });
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
### Serialization and output
|
|
402
|
+
|
|
403
|
+
```ts
|
|
404
|
+
doc.stringify(); // current AST as markdown string
|
|
405
|
+
doc.toText(); // plain text of all nodes
|
|
406
|
+
doc.toOutline(); // indented heading outline
|
|
407
|
+
doc.rawContent; // frontmatter + content as a full markdown string
|
|
408
|
+
doc.toJSON(); // { id, meta, content, ast }
|
|
409
|
+
|
|
410
|
+
// Filter toText output
|
|
411
|
+
doc.toText((node) => node.type === "paragraph");
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
### Persistence
|
|
415
|
+
|
|
416
|
+
```ts
|
|
417
|
+
// Save document to disk (normalizes headings by default)
|
|
418
|
+
await doc.save();
|
|
419
|
+
await doc.save({ normalize: false });
|
|
420
|
+
|
|
421
|
+
// Reload from disk
|
|
422
|
+
await doc.reload();
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
---
|
|
426
|
+
|
|
427
|
+
## 6. Defining Models — Quick Reference
|
|
428
|
+
|
|
429
|
+
```ts
|
|
430
|
+
import { defineModel, section, hasMany, belongsTo, z } from "contentbase";
|
|
431
|
+
|
|
432
|
+
const BlogPost = defineModel("BlogPost", {
|
|
433
|
+
// File path prefix — documents matching "posts/**" belong to this model
|
|
434
|
+
prefix: "posts",
|
|
435
|
+
|
|
436
|
+
// Frontmatter schema (Zod)
|
|
437
|
+
meta: z.object({
|
|
438
|
+
status: z.enum(["draft", "published"]).default("draft"),
|
|
439
|
+
category: z.string().optional(),
|
|
440
|
+
author: z.string().optional(),
|
|
441
|
+
wordCount: z.number().optional(),
|
|
442
|
+
}),
|
|
443
|
+
|
|
444
|
+
// Default values merged before validation
|
|
445
|
+
defaults: {
|
|
446
|
+
status: "draft",
|
|
447
|
+
},
|
|
448
|
+
|
|
449
|
+
// Structured data extracted from markdown headings
|
|
450
|
+
sections: {
|
|
451
|
+
tags: section("Tags", {
|
|
452
|
+
extract: (query) =>
|
|
453
|
+
query.selectAll("listItem").map((n) => toString(n)),
|
|
454
|
+
schema: z.array(z.string()),
|
|
455
|
+
}),
|
|
456
|
+
},
|
|
457
|
+
|
|
458
|
+
// Relationships to other models
|
|
459
|
+
relationships: {
|
|
460
|
+
author: belongsTo(() => Author, {
|
|
461
|
+
foreignKey: (doc) => doc.meta.author as string,
|
|
462
|
+
}),
|
|
463
|
+
},
|
|
464
|
+
|
|
465
|
+
// Derived properties computed from the instance
|
|
466
|
+
computed: {
|
|
467
|
+
isPublished: (self) => self.meta.status === "published",
|
|
468
|
+
readingTime: (self) => Math.ceil((self.meta.wordCount ?? 0) / 200),
|
|
469
|
+
},
|
|
470
|
+
});
|
|
471
|
+
```
|
|
472
|
+
|
|
473
|
+
### Custom matching
|
|
474
|
+
|
|
475
|
+
By default, documents are matched to models by their `prefix` (path starts with `"posts/"`). Override with a `match` function for custom logic:
|
|
476
|
+
|
|
477
|
+
```ts
|
|
478
|
+
const SpecialPost = defineModel("SpecialPost", {
|
|
479
|
+
prefix: "posts",
|
|
480
|
+
match: (doc) => doc.meta.type === "special",
|
|
481
|
+
meta: z.object({ type: z.literal("special") }),
|
|
482
|
+
});
|
|
483
|
+
```
|
|
484
|
+
|
|
485
|
+
---
|
|
486
|
+
|
|
487
|
+
## 7. Collection API
|
|
488
|
+
|
|
489
|
+
```ts
|
|
490
|
+
const collection = new Collection({
|
|
491
|
+
rootPath: "./content",
|
|
492
|
+
name: "my-project", // optional display name
|
|
493
|
+
extensions: ["mdx", "md"], // file extensions to scan (default)
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
// Register models before loading
|
|
497
|
+
collection.register(BlogPost);
|
|
498
|
+
collection.register(Author);
|
|
499
|
+
|
|
500
|
+
// Load all files
|
|
501
|
+
await collection.load();
|
|
502
|
+
|
|
503
|
+
// Reload files from disk
|
|
504
|
+
await collection.load({ refresh: true });
|
|
505
|
+
|
|
506
|
+
// Access loaded data
|
|
507
|
+
collection.available; // all path IDs: ["posts/hello", "authors/jane", ...]
|
|
508
|
+
collection.items; // Map<string, CollectionItem>
|
|
509
|
+
collection.documents; // Map<string, Document> (lazily populated)
|
|
510
|
+
collection.modelDefinitions; // all registered model definitions
|
|
511
|
+
collection.loaded; // boolean
|
|
512
|
+
|
|
513
|
+
// Get a specific document
|
|
514
|
+
const doc = collection.document("posts/hello");
|
|
515
|
+
|
|
516
|
+
// Get a typed model instance directly
|
|
517
|
+
const post = collection.getModel("posts/hello", BlogPost);
|
|
518
|
+
|
|
519
|
+
// Determine which model matches a document
|
|
520
|
+
const def = collection.findModelDefinition("posts/hello"); // BlogPost definition
|
|
521
|
+
|
|
522
|
+
// Generate documentation
|
|
523
|
+
await collection.generateModelSummary(); // writes MODELS.md to rootPath
|
|
524
|
+
|
|
525
|
+
// Table of contents
|
|
526
|
+
const toc = collection.tableOfContents({ title: "Content Index" });
|
|
527
|
+
|
|
528
|
+
// Serialization
|
|
529
|
+
collection.toJSON(); // { models, itemIds }
|
|
530
|
+
await collection.export(); // includes full model data
|
|
531
|
+
|
|
532
|
+
// Actions
|
|
533
|
+
collection.action("rebuild-index", async (coll) => { /* ... */ });
|
|
534
|
+
await collection.runAction("rebuild-index");
|
|
535
|
+
collection.availableActions; // ["rebuild-index"]
|
|
536
|
+
|
|
537
|
+
// Persistence
|
|
538
|
+
await collection.saveItem("posts/new-post", { content: "---\ntitle: New\n---\n\n# New Post" });
|
|
539
|
+
await collection.deleteItem("posts/old-post");
|
|
540
|
+
```
|