contentbase 0.0.1
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 +460 -0
- package/bun.lock +473 -0
- package/examples/sdlc-queries.ts +161 -0
- package/package.json +41 -0
- package/showcases/national-parks/models.ts +74 -0
- package/showcases/national-parks/parks/acadia.mdx +40 -0
- package/showcases/national-parks/parks/yosemite.mdx +44 -0
- package/showcases/national-parks/parks/zion.mdx +44 -0
- package/showcases/national-parks/queries.ts +103 -0
- package/showcases/national-parks/trails/angels-landing.mdx +19 -0
- package/showcases/national-parks/trails/cathedral-lakes.mdx +19 -0
- package/showcases/national-parks/trails/half-dome.mdx +19 -0
- package/showcases/national-parks/trails/jordan-pond-path.mdx +19 -0
- package/showcases/national-parks/trails/mist-trail.mdx +19 -0
- package/showcases/national-parks/trails/observation-point.mdx +19 -0
- package/showcases/national-parks/trails/precipice-trail.mdx +19 -0
- package/showcases/national-parks/trails/the-narrows.mdx +19 -0
- package/showcases/recipes/cuisines/chinese.mdx +28 -0
- package/showcases/recipes/cuisines/italian.mdx +32 -0
- package/showcases/recipes/cuisines/mexican.mdx +28 -0
- package/showcases/recipes/models.ts +77 -0
- package/showcases/recipes/queries.ts +89 -0
- package/showcases/recipes/recipes/chinese/egg-fried-rice.mdx +43 -0
- package/showcases/recipes/recipes/chinese/mapo-tofu.mdx +47 -0
- package/showcases/recipes/recipes/italian/bruschetta.mdx +38 -0
- package/showcases/recipes/recipes/italian/cacio-e-pepe.mdx +39 -0
- package/showcases/recipes/recipes/italian/tiramisu.mdx +43 -0
- package/showcases/recipes/recipes/mexican/chicken-tinga.mdx +44 -0
- package/showcases/recipes/recipes/mexican/guacamole.mdx +39 -0
- package/showcases/vinyl-collection/albums/bitches-brew.mdx +36 -0
- package/showcases/vinyl-collection/albums/i-put-a-spell-on-you.mdx +35 -0
- package/showcases/vinyl-collection/albums/in-rainbows.mdx +35 -0
- package/showcases/vinyl-collection/albums/kind-of-blue.mdx +32 -0
- package/showcases/vinyl-collection/albums/ok-computer.mdx +37 -0
- package/showcases/vinyl-collection/albums/wild-is-the-wind.mdx +35 -0
- package/showcases/vinyl-collection/artists/miles-davis.mdx +27 -0
- package/showcases/vinyl-collection/artists/nina-simone.mdx +26 -0
- package/showcases/vinyl-collection/artists/radiohead.mdx +27 -0
- package/showcases/vinyl-collection/models.ts +73 -0
- package/showcases/vinyl-collection/queries.ts +87 -0
- package/src/ast-query.ts +132 -0
- package/src/cli/commands/action.ts +44 -0
- package/src/cli/commands/create.ts +59 -0
- package/src/cli/commands/export.ts +24 -0
- package/src/cli/commands/init.ts +75 -0
- package/src/cli/commands/inspect.ts +46 -0
- package/src/cli/commands/validate.ts +75 -0
- package/src/cli/index.ts +20 -0
- package/src/cli/load-collection.ts +53 -0
- package/src/collection.ts +399 -0
- package/src/define-model.ts +80 -0
- package/src/document.ts +468 -0
- package/src/index.ts +47 -0
- package/src/model-instance.ts +227 -0
- package/src/node-shortcuts.ts +87 -0
- package/src/parse.ts +123 -0
- package/src/query/collection-query.ts +149 -0
- package/src/query/index.ts +5 -0
- package/src/query/operators.ts +37 -0
- package/src/query/query-builder.ts +109 -0
- package/src/relationships/belongs-to.ts +50 -0
- package/src/relationships/has-many.ts +136 -0
- package/src/relationships/index.ts +57 -0
- package/src/relationships/types.ts +7 -0
- package/src/section.ts +29 -0
- package/src/types.ts +221 -0
- package/src/utils/index.ts +11 -0
- package/src/utils/inflect.ts +82 -0
- package/src/utils/normalize-headings.ts +31 -0
- package/src/utils/parse-table.ts +30 -0
- package/src/utils/read-directory.ts +35 -0
- package/src/utils/stringify-ast.ts +9 -0
- package/src/validator.ts +52 -0
- package/test/ast-query.test.ts +128 -0
- package/test/collection.test.ts +99 -0
- package/test/define-model.test.ts +78 -0
- package/test/document.test.ts +225 -0
- package/test/fixtures/sdlc/epics/authentication.mdx +42 -0
- package/test/fixtures/sdlc/epics/searching-and-browsing.mdx +21 -0
- package/test/fixtures/sdlc/models.ts +89 -0
- package/test/fixtures/sdlc/stories/authentication/a-user-should-be-able-to-register.mdx +20 -0
- package/test/helpers.ts +21 -0
- package/test/model-instance.test.ts +197 -0
- package/test/query.test.ts +167 -0
- package/test/relationships.test.ts +84 -0
- package/test/section.test.ts +99 -0
- package/test/validator.test.ts +62 -0
- package/tsconfig.json +18 -0
- package/vitest.config.ts +11 -0
package/README.md
ADDED
|
@@ -0,0 +1,460 @@
|
|
|
1
|
+
# Contentbase
|
|
2
|
+
|
|
3
|
+
**An ORM for your Markdown.**
|
|
4
|
+
|
|
5
|
+
Contentbase treats a folder of Markdown and MDX files as a typed, queryable database. Define models with Zod schemas, extract structured data from headings and lists, traverse parent/child relationships across documents, validate everything, and query it all with a fluent API.
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
import { Collection, defineModel, section, hasMany, z } from "contentbase";
|
|
9
|
+
import { toString } from "mdast-util-to-string";
|
|
10
|
+
|
|
11
|
+
const Story = defineModel("Story", {
|
|
12
|
+
meta: z.object({
|
|
13
|
+
status: z.enum(["draft", "ready", "shipped"]).default("draft"),
|
|
14
|
+
points: z.number().optional(),
|
|
15
|
+
}),
|
|
16
|
+
sections: {
|
|
17
|
+
acceptanceCriteria: section("Acceptance Criteria", {
|
|
18
|
+
extract: (q) => q.selectAll("listItem").map((n) => toString(n)),
|
|
19
|
+
schema: z.array(z.string()).min(1),
|
|
20
|
+
}),
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const collection = new Collection({ rootPath: "./content" });
|
|
25
|
+
await collection.load();
|
|
26
|
+
|
|
27
|
+
const stories = await collection
|
|
28
|
+
.query(Story)
|
|
29
|
+
.where("meta.status", "ready")
|
|
30
|
+
.fetchAll();
|
|
31
|
+
|
|
32
|
+
stories[0].meta.status; // "ready" (typed!)
|
|
33
|
+
stories[0].sections.acceptanceCriteria; // string[] (typed!)
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
No database. No build step. Your content is the source of truth.
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## Why
|
|
41
|
+
|
|
42
|
+
You already organize knowledge in Markdown: specs, stories, docs, runbooks, design decisions. But the moment you need to query across files, validate frontmatter, or extract structured data from a heading, you're writing brittle scripts.
|
|
43
|
+
|
|
44
|
+
Contentbase gives you the primitives to treat that content like a real data layer:
|
|
45
|
+
|
|
46
|
+
- **Schema-validated frontmatter** via Zod. Typos in your `status` field get caught, not shipped.
|
|
47
|
+
- **Sections as typed data.** A heading called "Acceptance Criteria" containing a bullet list becomes `string[]` on the model instance, validated and cached.
|
|
48
|
+
- **Relationships derived from document structure.** An Epic's `## Stories` heading with `### Story Name` sub-headings automatically yields a `hasMany` relationship. No join tables. No IDs to manage.
|
|
49
|
+
- **Full TypeScript inference.** `defineModel()` infers all five generic parameters from your config object. You never write a type annotation.
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## Install
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
bun add contentbase
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Contentbase is ESM-only and requires Node 18+ or Bun.
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## Core Concepts
|
|
64
|
+
|
|
65
|
+
### Documents
|
|
66
|
+
|
|
67
|
+
Every `.md` or `.mdx` file in your content directory becomes a `Document`. Documents have an `id` (the file path without the extension), lazily-parsed AST, frontmatter metadata, and a rich set of section operations.
|
|
68
|
+
|
|
69
|
+
```
|
|
70
|
+
content/
|
|
71
|
+
epics/
|
|
72
|
+
authentication.mdx -> id: "epics/authentication"
|
|
73
|
+
stories/
|
|
74
|
+
authentication/
|
|
75
|
+
user-can-register.mdx -> id: "stories/authentication/user-can-register"
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### Models
|
|
79
|
+
|
|
80
|
+
A model is a config object that describes one type of document. It declares:
|
|
81
|
+
|
|
82
|
+
- **meta** -- a Zod schema for frontmatter
|
|
83
|
+
- **sections** -- named extractions from heading-based sections
|
|
84
|
+
- **relationships** -- `hasMany` / `belongsTo` links between models
|
|
85
|
+
- **computed** -- derived values calculated from instance data
|
|
86
|
+
|
|
87
|
+
```ts
|
|
88
|
+
const Epic = defineModel("Epic", {
|
|
89
|
+
prefix: "epics",
|
|
90
|
+
meta: z.object({
|
|
91
|
+
priority: z.enum(["low", "medium", "high"]).optional(),
|
|
92
|
+
status: z.enum(["created", "in-progress", "complete"]).default("created"),
|
|
93
|
+
}),
|
|
94
|
+
relationships: {
|
|
95
|
+
stories: hasMany(() => Story, { heading: "Stories" }),
|
|
96
|
+
},
|
|
97
|
+
computed: {
|
|
98
|
+
isComplete: (self) => self.meta.status === "complete",
|
|
99
|
+
},
|
|
100
|
+
defaults: {
|
|
101
|
+
status: "created",
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
The `prefix` determines which files match this model. Files whose path starts with `"epics"` are Epics. If omitted, the prefix is auto-pluralized from the name (`"Epic"` -> `"epics"`).
|
|
107
|
+
|
|
108
|
+
### Collections
|
|
109
|
+
|
|
110
|
+
A `Collection` loads a directory tree and gives you access to documents and typed model instances.
|
|
111
|
+
|
|
112
|
+
```ts
|
|
113
|
+
const collection = new Collection({ rootPath: "./content" });
|
|
114
|
+
await collection.load();
|
|
115
|
+
|
|
116
|
+
// Register models for prefix-based matching
|
|
117
|
+
collection.register(Epic);
|
|
118
|
+
collection.register(Story);
|
|
119
|
+
|
|
120
|
+
// Get a typed instance
|
|
121
|
+
const epic = collection.getModel("epics/authentication", Epic);
|
|
122
|
+
epic.meta.priority; // "high" | "medium" | "low" | undefined
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
## Sections
|
|
128
|
+
|
|
129
|
+
Sections let you extract typed, structured data from the content beneath a heading.
|
|
130
|
+
|
|
131
|
+
Given this Markdown:
|
|
132
|
+
|
|
133
|
+
```md
|
|
134
|
+
## Acceptance Criteria
|
|
135
|
+
|
|
136
|
+
- Users can sign up with email and password
|
|
137
|
+
- Validation errors are shown inline
|
|
138
|
+
- Confirmation email is sent
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Define a section to extract the list items:
|
|
142
|
+
|
|
143
|
+
```ts
|
|
144
|
+
import { section } from "contentbase";
|
|
145
|
+
import { toString } from "mdast-util-to-string";
|
|
146
|
+
|
|
147
|
+
const Story = defineModel("Story", {
|
|
148
|
+
sections: {
|
|
149
|
+
acceptanceCriteria: section("Acceptance Criteria", {
|
|
150
|
+
extract: (query) =>
|
|
151
|
+
query.selectAll("listItem").map((node) => toString(node)),
|
|
152
|
+
schema: z.array(z.string()),
|
|
153
|
+
}),
|
|
154
|
+
},
|
|
155
|
+
});
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
The `extract` function receives an `AstQuery` scoped to the content under that heading. The `schema` is optional and used during validation.
|
|
159
|
+
|
|
160
|
+
Section data is **lazily computed and cached** -- the extract function only runs the first time you access the property.
|
|
161
|
+
|
|
162
|
+
```ts
|
|
163
|
+
instance.sections.acceptanceCriteria;
|
|
164
|
+
// ["Users can sign up with email and password", "Validation errors are shown inline", ...]
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
---
|
|
168
|
+
|
|
169
|
+
## Relationships
|
|
170
|
+
|
|
171
|
+
### hasMany
|
|
172
|
+
|
|
173
|
+
A `hasMany` relationship extracts child models from sub-headings. Given an Epic document:
|
|
174
|
+
|
|
175
|
+
```md
|
|
176
|
+
# Authentication
|
|
177
|
+
|
|
178
|
+
## Stories
|
|
179
|
+
|
|
180
|
+
### User can register
|
|
181
|
+
As a user I want to register...
|
|
182
|
+
|
|
183
|
+
### User can login
|
|
184
|
+
As a user I want to login...
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
Defining the relationship:
|
|
188
|
+
|
|
189
|
+
```ts
|
|
190
|
+
const Epic = defineModel("Epic", {
|
|
191
|
+
relationships: {
|
|
192
|
+
stories: hasMany(() => Story, { heading: "Stories" }),
|
|
193
|
+
},
|
|
194
|
+
});
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
Contentbase finds the `## Stories` heading, extracts each `###` sub-heading as a child document, and creates typed model instances:
|
|
198
|
+
|
|
199
|
+
```ts
|
|
200
|
+
const epic = collection.getModel("epics/authentication", Epic);
|
|
201
|
+
|
|
202
|
+
const stories = epic.relationships.stories.fetchAll();
|
|
203
|
+
stories.length; // 2
|
|
204
|
+
stories[0].title; // "User can register"
|
|
205
|
+
|
|
206
|
+
const first = epic.relationships.stories.first();
|
|
207
|
+
const last = epic.relationships.stories.last();
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
### belongsTo
|
|
211
|
+
|
|
212
|
+
A `belongsTo` relationship resolves a parent via a foreign key in frontmatter.
|
|
213
|
+
|
|
214
|
+
```yaml
|
|
215
|
+
# stories/authentication/user-can-register.mdx
|
|
216
|
+
---
|
|
217
|
+
status: created
|
|
218
|
+
epic: authentication
|
|
219
|
+
---
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
```ts
|
|
223
|
+
const Story = defineModel("Story", {
|
|
224
|
+
meta: z.object({
|
|
225
|
+
status: z.enum(["created", "in-progress", "complete"]).default("created"),
|
|
226
|
+
epic: z.string().optional(),
|
|
227
|
+
}),
|
|
228
|
+
relationships: {
|
|
229
|
+
epic: belongsTo(() => Epic, {
|
|
230
|
+
foreignKey: (doc) => doc.meta.epic as string,
|
|
231
|
+
}),
|
|
232
|
+
},
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
const story = collection.getModel(
|
|
236
|
+
"stories/authentication/user-can-register",
|
|
237
|
+
Story
|
|
238
|
+
);
|
|
239
|
+
const epic = story.relationships.epic.fetch();
|
|
240
|
+
epic.title; // "Authentication"
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
Relationship targets use thunks (`() => Epic`) so you can define circular references without import ordering issues.
|
|
244
|
+
|
|
245
|
+
---
|
|
246
|
+
|
|
247
|
+
## Querying
|
|
248
|
+
|
|
249
|
+
The query API filters typed model instances with a fluent builder:
|
|
250
|
+
|
|
251
|
+
```ts
|
|
252
|
+
// Simple equality
|
|
253
|
+
const epics = await collection
|
|
254
|
+
.query(Epic)
|
|
255
|
+
.where("meta.priority", "high")
|
|
256
|
+
.fetchAll();
|
|
257
|
+
|
|
258
|
+
// Object shorthand
|
|
259
|
+
const drafts = await collection
|
|
260
|
+
.query(Story)
|
|
261
|
+
.where({ "meta.status": "created" })
|
|
262
|
+
.fetchAll();
|
|
263
|
+
|
|
264
|
+
// Comparison operators
|
|
265
|
+
const urgent = await collection
|
|
266
|
+
.query(Story)
|
|
267
|
+
.where("meta.points", "gte", 5)
|
|
268
|
+
.fetchAll();
|
|
269
|
+
|
|
270
|
+
// Chainable methods
|
|
271
|
+
const results = await collection
|
|
272
|
+
.query(Story)
|
|
273
|
+
.whereIn("meta.status", ["created", "in-progress"])
|
|
274
|
+
.whereExists("meta.epic")
|
|
275
|
+
.fetchAll();
|
|
276
|
+
|
|
277
|
+
// Convenience accessors
|
|
278
|
+
const first = await collection.query(Epic).first();
|
|
279
|
+
const count = await collection.query(Epic).count();
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
Available operators: `eq`, `neq`, `in`, `notIn`, `gt`, `lt`, `gte`, `lte`, `contains`, `startsWith`, `endsWith`, `regex`, `exists`.
|
|
283
|
+
|
|
284
|
+
Queries filter by model type **before** creating instances, so you only pay the parsing cost for matching documents.
|
|
285
|
+
|
|
286
|
+
---
|
|
287
|
+
|
|
288
|
+
## Validation
|
|
289
|
+
|
|
290
|
+
Every model instance can be validated against its Zod schemas:
|
|
291
|
+
|
|
292
|
+
```ts
|
|
293
|
+
const instance = collection.getModel("epics/authentication", Epic);
|
|
294
|
+
const result = await instance.validate();
|
|
295
|
+
|
|
296
|
+
result.valid; // true
|
|
297
|
+
result.errors; // ZodIssue[]
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
Validation checks:
|
|
301
|
+
1. **Meta** against the model's Zod schema (with defaults applied)
|
|
302
|
+
2. **Sections** against any section-level schemas
|
|
303
|
+
|
|
304
|
+
```ts
|
|
305
|
+
if (instance.hasErrors) {
|
|
306
|
+
for (const [path, issue] of instance.errors) {
|
|
307
|
+
console.log(`${path}: ${issue.message}`);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
The standalone `validateDocument` function is also available for lower-level use.
|
|
313
|
+
|
|
314
|
+
---
|
|
315
|
+
|
|
316
|
+
## Serialization
|
|
317
|
+
|
|
318
|
+
```ts
|
|
319
|
+
const json = instance.toJSON();
|
|
320
|
+
// { id, title, meta }
|
|
321
|
+
|
|
322
|
+
const full = instance.toJSON({
|
|
323
|
+
sections: ["acceptanceCriteria"],
|
|
324
|
+
computed: ["isComplete"],
|
|
325
|
+
related: ["stories"],
|
|
326
|
+
});
|
|
327
|
+
// { id, title, meta, acceptanceCriteria: [...], isComplete: false, stories: [...] }
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
Export an entire collection:
|
|
331
|
+
|
|
332
|
+
```ts
|
|
333
|
+
const data = await collection.export();
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
---
|
|
337
|
+
|
|
338
|
+
## Document API
|
|
339
|
+
|
|
340
|
+
Documents expose a powerful AST manipulation layer built on the unified/remark ecosystem.
|
|
341
|
+
|
|
342
|
+
```ts
|
|
343
|
+
const doc = collection.document("epics/authentication");
|
|
344
|
+
|
|
345
|
+
// Read
|
|
346
|
+
doc.title; // "Authentication"
|
|
347
|
+
doc.slug; // "authentication"
|
|
348
|
+
doc.meta; // { priority: "high", status: "created" }
|
|
349
|
+
doc.content; // raw markdown (without frontmatter)
|
|
350
|
+
doc.rawContent; // full file content with frontmatter
|
|
351
|
+
|
|
352
|
+
// AST querying
|
|
353
|
+
const headings = doc.astQuery.selectAll("heading");
|
|
354
|
+
const h2s = doc.astQuery.headingsAtDepth(2);
|
|
355
|
+
const storiesHeading = doc.astQuery.findHeadingByText("Stories");
|
|
356
|
+
|
|
357
|
+
// Node shortcuts
|
|
358
|
+
doc.nodes.headings; // all headings
|
|
359
|
+
doc.nodes.links; // all links
|
|
360
|
+
doc.nodes.tables; // all table nodes
|
|
361
|
+
doc.nodes.tablesAsData; // tables as { headers, rows } objects
|
|
362
|
+
doc.nodes.codeBlocks; // all code blocks
|
|
363
|
+
|
|
364
|
+
// Section operations (immutable by default)
|
|
365
|
+
const trimmed = doc.removeSection("Stories"); // new Document
|
|
366
|
+
const updated = doc.replaceSectionContent("Stories", newMarkdown);
|
|
367
|
+
const expanded = doc.appendToSection("Stories", "### New Story\n\nDetails...");
|
|
368
|
+
|
|
369
|
+
// Mutable when you need it
|
|
370
|
+
doc.removeSection("Stories", { mutate: true });
|
|
371
|
+
|
|
372
|
+
// Persistence
|
|
373
|
+
await doc.save();
|
|
374
|
+
await doc.reload();
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
---
|
|
378
|
+
|
|
379
|
+
## Computed Properties
|
|
380
|
+
|
|
381
|
+
Derived values that are lazily evaluated from instance data:
|
|
382
|
+
|
|
383
|
+
```ts
|
|
384
|
+
const Epic = defineModel("Epic", {
|
|
385
|
+
meta: z.object({
|
|
386
|
+
status: z.enum(["created", "in-progress", "complete"]).default("created"),
|
|
387
|
+
}),
|
|
388
|
+
computed: {
|
|
389
|
+
isComplete: (self) => self.meta.status === "complete",
|
|
390
|
+
storyCount: (self) => self.relationships.stories.fetchAll().length,
|
|
391
|
+
},
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
const epic = collection.getModel("epics/authentication", Epic);
|
|
395
|
+
epic.computed.isComplete; // false
|
|
396
|
+
epic.computed.storyCount; // 2
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
---
|
|
400
|
+
|
|
401
|
+
## Plugins and Actions
|
|
402
|
+
|
|
403
|
+
```ts
|
|
404
|
+
// Register named actions on the collection
|
|
405
|
+
collection.action("publish", async (coll, instance, opts) => {
|
|
406
|
+
// your publish logic
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
await instance.runAction("publish", { target: "production" });
|
|
410
|
+
|
|
411
|
+
// Plugin system
|
|
412
|
+
function timestampPlugin(collection, options) {
|
|
413
|
+
collection.action("touch", async (coll, instance) => {
|
|
414
|
+
// update timestamps
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
collection.use(timestampPlugin, { format: "iso" });
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
---
|
|
422
|
+
|
|
423
|
+
## CLI
|
|
424
|
+
|
|
425
|
+
Contentbase ships with a CLI for common operations:
|
|
426
|
+
|
|
427
|
+
```bash
|
|
428
|
+
contentbase inspect # show collection info
|
|
429
|
+
contentbase validate # validate all documents
|
|
430
|
+
contentbase export # export collection as JSON
|
|
431
|
+
contentbase create Story # scaffold a new document
|
|
432
|
+
contentbase action publish # run a named action
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
---
|
|
436
|
+
|
|
437
|
+
## API Reference
|
|
438
|
+
|
|
439
|
+
### Top-level exports
|
|
440
|
+
|
|
441
|
+
| Export | Description |
|
|
442
|
+
| --- | --- |
|
|
443
|
+
| `Collection` | Loads and manages a directory of documents |
|
|
444
|
+
| `Document` | A single Markdown/MDX file with AST operations |
|
|
445
|
+
| `defineModel()` | Create a typed model definition |
|
|
446
|
+
| `section()` | Declare a section extraction |
|
|
447
|
+
| `hasMany()` | Declare a one-to-many relationship |
|
|
448
|
+
| `belongsTo()` | Declare a many-to-one relationship |
|
|
449
|
+
| `CollectionQuery` | Fluent query builder for model instances |
|
|
450
|
+
| `AstQuery` | MDAST query wrapper (select, visit, find) |
|
|
451
|
+
| `NodeShortcuts` | Convenience getters for common AST nodes |
|
|
452
|
+
| `createModelInstance()` | Low-level factory for model instances |
|
|
453
|
+
| `validateDocument()` | Standalone validation function |
|
|
454
|
+
| `z` | Re-exported from Zod (no extra dependency needed) |
|
|
455
|
+
|
|
456
|
+
---
|
|
457
|
+
|
|
458
|
+
## License
|
|
459
|
+
|
|
460
|
+
MIT
|