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
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
defineModel,
|
|
3
|
-
section,
|
|
4
|
-
hasMany,
|
|
5
|
-
belongsTo,
|
|
6
|
-
z,
|
|
7
|
-
} from "../../src/index";
|
|
8
|
-
import { toString } from "mdast-util-to-string";
|
|
9
|
-
import { parseTable } from "../../src/utils/parse-table";
|
|
10
|
-
|
|
11
|
-
// ─── Artist (parent) ───
|
|
12
|
-
|
|
13
|
-
export const Artist = defineModel("Artist", {
|
|
14
|
-
prefix: "artists",
|
|
15
|
-
meta: z.object({
|
|
16
|
-
genre: z.string(),
|
|
17
|
-
origin: z.string(),
|
|
18
|
-
active: z.string(),
|
|
19
|
-
}),
|
|
20
|
-
sections: {
|
|
21
|
-
influences: section("Influences", {
|
|
22
|
-
extract: (q) => q.selectAll("listItem").map((n) => toString(n)),
|
|
23
|
-
schema: z.array(z.string()),
|
|
24
|
-
}),
|
|
25
|
-
},
|
|
26
|
-
relationships: {
|
|
27
|
-
albums: hasMany(() => Album, { heading: "Discography" }),
|
|
28
|
-
},
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
// ─── Album ───
|
|
32
|
-
|
|
33
|
-
export const Album = defineModel("Album", {
|
|
34
|
-
prefix: "albums",
|
|
35
|
-
meta: z.object({
|
|
36
|
-
artist: z.string(),
|
|
37
|
-
year: z.number(),
|
|
38
|
-
genre: z.string(),
|
|
39
|
-
format: z.enum(["LP", "EP", "Single", "2xLP"]).default("LP"),
|
|
40
|
-
rating: z.number().min(1).max(5).optional(),
|
|
41
|
-
condition: z
|
|
42
|
-
.enum(["mint", "near-mint", "very-good", "good", "fair", "poor"])
|
|
43
|
-
.default("very-good"),
|
|
44
|
-
}),
|
|
45
|
-
sections: {
|
|
46
|
-
tracklist: section("Tracklist", {
|
|
47
|
-
extract: (q) => {
|
|
48
|
-
const tables = q.selectAll("table");
|
|
49
|
-
if (tables.length > 0) {
|
|
50
|
-
return parseTable(tables[0]);
|
|
51
|
-
}
|
|
52
|
-
return q.selectAll("listItem").map((n) => toString(n));
|
|
53
|
-
},
|
|
54
|
-
schema: z.union([
|
|
55
|
-
z.array(z.record(z.string(), z.string())),
|
|
56
|
-
z.array(z.string()),
|
|
57
|
-
]),
|
|
58
|
-
}),
|
|
59
|
-
personnel: section("Personnel", {
|
|
60
|
-
extract: (q) => q.selectAll("listItem").map((n) => toString(n)),
|
|
61
|
-
schema: z.array(z.string()),
|
|
62
|
-
}),
|
|
63
|
-
},
|
|
64
|
-
relationships: {
|
|
65
|
-
artist: belongsTo(() => Artist, {
|
|
66
|
-
foreignKey: (doc) => doc.meta.artist as string,
|
|
67
|
-
}),
|
|
68
|
-
},
|
|
69
|
-
computed: {
|
|
70
|
-
isClassic: (self: any) => self.meta.year < 1980,
|
|
71
|
-
decade: (self: any) => `${Math.floor(self.meta.year / 10) * 10}s`,
|
|
72
|
-
},
|
|
73
|
-
});
|
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Example queries for the Vinyl Collection showcase.
|
|
3
|
-
*
|
|
4
|
-
* Run with: bun showcases/vinyl-collection/queries.ts
|
|
5
|
-
*/
|
|
6
|
-
import { Collection } from "../../src/index";
|
|
7
|
-
import { Artist, Album } from "./models";
|
|
8
|
-
|
|
9
|
-
const collection = new Collection({
|
|
10
|
-
rootPath: new URL(".", import.meta.url).pathname,
|
|
11
|
-
});
|
|
12
|
-
|
|
13
|
-
collection.register(Artist);
|
|
14
|
-
collection.register(Album);
|
|
15
|
-
await collection.load();
|
|
16
|
-
|
|
17
|
-
// ── Full collection ──
|
|
18
|
-
const allAlbums = await collection.query(Album).fetchAll();
|
|
19
|
-
console.log(`Total albums in collection: ${allAlbums.length}`);
|
|
20
|
-
|
|
21
|
-
// ── Filter by genre ──
|
|
22
|
-
const jazzAlbums = await collection
|
|
23
|
-
.query(Album)
|
|
24
|
-
.where("meta.genre", "contains", "Jazz")
|
|
25
|
-
.fetchAll();
|
|
26
|
-
console.log(
|
|
27
|
-
`\nJazz albums: ${jazzAlbums.map((a) => `${a.title} (${a.meta.year})`).join(", ")}`
|
|
28
|
-
);
|
|
29
|
-
|
|
30
|
-
// ── Filter by decade (computed) ──
|
|
31
|
-
console.log(`\nAlbums by decade:`);
|
|
32
|
-
for (const album of allAlbums) {
|
|
33
|
-
console.log(` ${album.title} — ${album.computed.decade}`);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// ── 5-star records ──
|
|
37
|
-
const topRated = await collection
|
|
38
|
-
.query(Album)
|
|
39
|
-
.where("meta.rating", 5)
|
|
40
|
-
.fetchAll();
|
|
41
|
-
console.log(
|
|
42
|
-
`\n5-star albums: ${topRated.map((a) => a.title).join(", ")}`
|
|
43
|
-
);
|
|
44
|
-
|
|
45
|
-
// ── Classics (pre-1980) ──
|
|
46
|
-
console.log(`\nClassics (pre-1980):`);
|
|
47
|
-
for (const album of allAlbums) {
|
|
48
|
-
if (album.computed.isClassic) {
|
|
49
|
-
console.log(` ${album.title} (${album.meta.year})`);
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// ── Condition report ──
|
|
54
|
-
const mint = await collection
|
|
55
|
-
.query(Album)
|
|
56
|
-
.whereIn("meta.condition", ["mint", "near-mint"])
|
|
57
|
-
.fetchAll();
|
|
58
|
-
console.log(
|
|
59
|
-
`\nMint / near-mint condition: ${mint.map((a) => a.title).join(", ")}`
|
|
60
|
-
);
|
|
61
|
-
|
|
62
|
-
// ── Tracklist as structured data ──
|
|
63
|
-
const kob = collection.getModel("albums/kind-of-blue", Album);
|
|
64
|
-
console.log(`\n--- ${kob.title} ---`);
|
|
65
|
-
console.log("Tracklist:", kob.sections.tracklist);
|
|
66
|
-
console.log("Personnel:", kob.sections.personnel);
|
|
67
|
-
|
|
68
|
-
// ── Artist → Albums relationship ──
|
|
69
|
-
const miles = collection.getModel("artists/miles-davis", Artist);
|
|
70
|
-
console.log(`\n--- ${miles.title} ---`);
|
|
71
|
-
console.log("Influences:", miles.sections.influences);
|
|
72
|
-
const milesAlbums = miles.relationships.albums.fetchAll();
|
|
73
|
-
console.log(
|
|
74
|
-
`Albums in discography: ${milesAlbums.map((a) => a.title).join(", ")}`
|
|
75
|
-
);
|
|
76
|
-
|
|
77
|
-
// ── Album → Artist relationship ──
|
|
78
|
-
const okc = collection.getModel("albums/ok-computer", Album);
|
|
79
|
-
const artist = okc.relationships.artist.fetch();
|
|
80
|
-
console.log(`\n${okc.title} by ${artist.title} (${artist.meta.origin})`);
|
|
81
|
-
|
|
82
|
-
// ── Serialize ──
|
|
83
|
-
const json = kob.toJSON({
|
|
84
|
-
sections: ["tracklist", "personnel"],
|
|
85
|
-
computed: ["isClassic", "decade"],
|
|
86
|
-
});
|
|
87
|
-
console.log(`\nKind of Blue as JSON:`, JSON.stringify(json, null, 2));
|
package/test/ast-query.test.ts
DELETED
|
@@ -1,128 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach } from "vitest";
|
|
2
|
-
import { AstQuery } from "../src/ast-query";
|
|
3
|
-
import { Collection } from "../src/collection";
|
|
4
|
-
import { createTestCollection } from "./helpers";
|
|
5
|
-
import type { Heading } from "mdast";
|
|
6
|
-
|
|
7
|
-
describe("AstQuery", () => {
|
|
8
|
-
let collection: Collection;
|
|
9
|
-
let query: AstQuery;
|
|
10
|
-
|
|
11
|
-
beforeEach(async () => {
|
|
12
|
-
collection = await createTestCollection();
|
|
13
|
-
const doc = collection.document("epics/authentication");
|
|
14
|
-
query = doc.astQuery;
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
it("select returns first matching node", () => {
|
|
18
|
-
const heading = query.select("heading");
|
|
19
|
-
expect(heading).toBeDefined();
|
|
20
|
-
expect(heading!.type).toBe("heading");
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
it("selectAll returns all matching nodes", () => {
|
|
24
|
-
const headings = query.selectAll("heading");
|
|
25
|
-
expect(headings.length).toBeGreaterThan(1);
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
it("selectAll with attribute filter", () => {
|
|
29
|
-
const h2s = query.selectAll('heading[depth="2"]');
|
|
30
|
-
expect(h2s.length).toBeGreaterThanOrEqual(1);
|
|
31
|
-
for (const h of h2s) {
|
|
32
|
-
expect((h as Heading).depth).toBe(2);
|
|
33
|
-
}
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
it("visit walks all nodes", () => {
|
|
37
|
-
let count = 0;
|
|
38
|
-
query.visit(() => {
|
|
39
|
-
count++;
|
|
40
|
-
});
|
|
41
|
-
expect(count).toBeGreaterThan(0);
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
it("findBefore finds preceding node", () => {
|
|
45
|
-
const headings = query.selectAll("heading");
|
|
46
|
-
if (headings.length >= 2) {
|
|
47
|
-
const before = query.findBefore(headings[1]);
|
|
48
|
-
expect(before).toBeDefined();
|
|
49
|
-
}
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
it("findAfter finds following node", () => {
|
|
53
|
-
const heading = query.select("heading")!;
|
|
54
|
-
const after = query.findAfter(heading);
|
|
55
|
-
expect(after).toBeDefined();
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
it("findAllAfter returns all following nodes", () => {
|
|
59
|
-
const heading = query.select("heading")!;
|
|
60
|
-
const after = query.findAllAfter(heading);
|
|
61
|
-
expect(after.length).toBeGreaterThan(0);
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
it("findBetween returns nodes between two markers", () => {
|
|
65
|
-
const headings = query.selectAll("heading");
|
|
66
|
-
if (headings.length >= 2) {
|
|
67
|
-
const between = query.findBetween(headings[0], headings[1]);
|
|
68
|
-
expect(between).toBeDefined();
|
|
69
|
-
}
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
it("headingsAtDepth returns correct headings (bug fix)", () => {
|
|
73
|
-
const h1s = query.headingsAtDepth(1);
|
|
74
|
-
expect(h1s.length).toBe(1);
|
|
75
|
-
expect(h1s[0].depth).toBe(1);
|
|
76
|
-
|
|
77
|
-
const h3s = query.headingsAtDepth(3);
|
|
78
|
-
expect(h3s.length).toBeGreaterThanOrEqual(2);
|
|
79
|
-
for (const h of h3s) {
|
|
80
|
-
expect(h.depth).toBe(3);
|
|
81
|
-
}
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
it("findHeadingByText finds exact match (case insensitive)", () => {
|
|
85
|
-
const heading = query.findHeadingByText("stories");
|
|
86
|
-
expect(heading).toBeDefined();
|
|
87
|
-
expect(heading!.depth).toBe(2);
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
it("findHeadingByText with substring matching", () => {
|
|
91
|
-
const heading = query.findHeadingByText("register", false);
|
|
92
|
-
expect(heading).toBeDefined();
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
it("findAllHeadingsByText returns multiple matches", () => {
|
|
96
|
-
const headings = query.findAllHeadingsByText("Acceptance Criteria");
|
|
97
|
-
expect(headings.length).toBeGreaterThanOrEqual(2);
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
it("findNextSiblingHeadingTo finds same-depth sibling", () => {
|
|
101
|
-
const h3s = query.headingsAtDepth(3);
|
|
102
|
-
if (h3s.length >= 2) {
|
|
103
|
-
const next = query.findNextSiblingHeadingTo(h3s[0]);
|
|
104
|
-
expect(next).toBeDefined();
|
|
105
|
-
expect(next!.depth).toBe(3);
|
|
106
|
-
}
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
it("findNextSiblingHeadingTo returns undefined at end", () => {
|
|
110
|
-
const h3s = query.headingsAtDepth(3);
|
|
111
|
-
const last = h3s[h3s.length - 1];
|
|
112
|
-
const next = query.findNextSiblingHeadingTo(last);
|
|
113
|
-
expect(next).toBeUndefined();
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
it("atLine returns node at specific line", () => {
|
|
117
|
-
const first = query.ast.children[0];
|
|
118
|
-
if (first.position) {
|
|
119
|
-
const found = query.atLine(first.position.start.line);
|
|
120
|
-
expect(found).toBeDefined();
|
|
121
|
-
}
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
it("atLine returns undefined for non-existent line", () => {
|
|
125
|
-
const found = query.atLine(99999);
|
|
126
|
-
expect(found).toBeUndefined();
|
|
127
|
-
});
|
|
128
|
-
});
|
package/test/collection.test.ts
DELETED
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach } from "vitest";
|
|
2
|
-
import { Collection } from "../src/collection";
|
|
3
|
-
import { createTestCollection, FIXTURES_PATH } from "./helpers";
|
|
4
|
-
import { Epic, Story } from "./fixtures/sdlc/models";
|
|
5
|
-
|
|
6
|
-
describe("Collection", () => {
|
|
7
|
-
let collection: Collection;
|
|
8
|
-
|
|
9
|
-
beforeEach(async () => {
|
|
10
|
-
collection = await createTestCollection();
|
|
11
|
-
});
|
|
12
|
-
|
|
13
|
-
it("loads all mdx files from rootPath", () => {
|
|
14
|
-
expect(collection.loaded).toBe(true);
|
|
15
|
-
expect(collection.available.length).toBeGreaterThanOrEqual(3);
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
it("returns correct path IDs", () => {
|
|
19
|
-
expect(collection.available).toContain("epics/authentication");
|
|
20
|
-
expect(collection.available).toContain("epics/searching-and-browsing");
|
|
21
|
-
expect(collection.available).toContain(
|
|
22
|
-
"stories/authentication/a-user-should-be-able-to-register"
|
|
23
|
-
);
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
it("lazily creates documents", () => {
|
|
27
|
-
const doc = collection.document("epics/authentication");
|
|
28
|
-
expect(doc).toBeDefined();
|
|
29
|
-
expect(doc.id).toBe("epics/authentication");
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
it("caches documents", () => {
|
|
33
|
-
const doc1 = collection.document("epics/authentication");
|
|
34
|
-
const doc2 = collection.document("epics/authentication");
|
|
35
|
-
expect(doc1).toBe(doc2);
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
it("throws if not loaded", () => {
|
|
39
|
-
const fresh = new Collection({ rootPath: FIXTURES_PATH });
|
|
40
|
-
expect(() => fresh.document("epics/authentication")).toThrow(
|
|
41
|
-
"Collection has not been loaded"
|
|
42
|
-
);
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
it("throws for unknown pathId", () => {
|
|
46
|
-
expect(() => collection.document("nonexistent")).toThrow(
|
|
47
|
-
'Could not find document "nonexistent"'
|
|
48
|
-
);
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
it("registers model definitions", () => {
|
|
52
|
-
expect(collection.getModelDefinition("Epic")).toBeDefined();
|
|
53
|
-
expect(collection.getModelDefinition("Story")).toBeDefined();
|
|
54
|
-
expect(collection.modelDefinitions.length).toBe(2);
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
it("finds model definition by pathId prefix", () => {
|
|
58
|
-
const def = collection.findModelDefinition("epics/authentication");
|
|
59
|
-
expect(def?.name).toBe("Epic");
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
it("gets typed model instances", () => {
|
|
63
|
-
const epic = collection.getModel("epics/authentication", Epic);
|
|
64
|
-
expect(epic.id).toBe("epics/authentication");
|
|
65
|
-
expect(epic.title).toBe("Authentication");
|
|
66
|
-
expect(epic.meta.status).toBe("created");
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
it("creates typed queries", async () => {
|
|
70
|
-
const epics = await collection.query(Epic).fetchAll();
|
|
71
|
-
expect(epics.length).toBe(2);
|
|
72
|
-
expect(epics[0].meta.status).toBeDefined();
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
it("serializes to JSON", () => {
|
|
76
|
-
const json = collection.toJSON();
|
|
77
|
-
expect(json.models).toBeDefined();
|
|
78
|
-
expect(json.itemIds).toBeDefined();
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
it("supports actions", async () => {
|
|
82
|
-
let called = false;
|
|
83
|
-
collection.action("test-action", () => {
|
|
84
|
-
called = true;
|
|
85
|
-
});
|
|
86
|
-
expect(collection.availableActions).toContain("test-action");
|
|
87
|
-
await collection.runAction("test-action");
|
|
88
|
-
expect(called).toBe(true);
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
it("supports plugins", () => {
|
|
92
|
-
let pluginCalled = false;
|
|
93
|
-
collection.use((coll) => {
|
|
94
|
-
pluginCalled = true;
|
|
95
|
-
expect(coll).toBe(collection);
|
|
96
|
-
});
|
|
97
|
-
expect(pluginCalled).toBe(true);
|
|
98
|
-
});
|
|
99
|
-
});
|
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
2
|
-
import { defineModel, z, section, hasMany, belongsTo } from "../src/index";
|
|
3
|
-
|
|
4
|
-
describe("defineModel", () => {
|
|
5
|
-
it("creates a ModelDefinition with the given name", () => {
|
|
6
|
-
const MyModel = defineModel("MyModel");
|
|
7
|
-
expect(MyModel.name).toBe("MyModel");
|
|
8
|
-
});
|
|
9
|
-
|
|
10
|
-
it("auto-pluralizes the prefix from the name", () => {
|
|
11
|
-
const Epic = defineModel("Epic");
|
|
12
|
-
expect(Epic.prefix).toBe("epics");
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
it("uses custom prefix when provided", () => {
|
|
16
|
-
const MyModel = defineModel("MyModel", { prefix: "custom" });
|
|
17
|
-
expect(MyModel.prefix).toBe("custom");
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
it("stores the meta Zod schema", () => {
|
|
21
|
-
const schema = z.object({ status: z.string() });
|
|
22
|
-
const MyModel = defineModel("MyModel", { meta: schema });
|
|
23
|
-
expect(MyModel.meta).toBe(schema);
|
|
24
|
-
expect(MyModel.schema).toBe(schema);
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
it("creates passthrough schema when meta is omitted", () => {
|
|
28
|
-
const MyModel = defineModel("MyModel");
|
|
29
|
-
const result = MyModel.meta.safeParse({ anything: "goes" });
|
|
30
|
-
expect(result.success).toBe(true);
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
it("stores sections", () => {
|
|
34
|
-
const MyModel = defineModel("MyModel", {
|
|
35
|
-
sections: {
|
|
36
|
-
items: section("Items", {
|
|
37
|
-
extract: (q) => q.selectAll("listItem"),
|
|
38
|
-
}),
|
|
39
|
-
},
|
|
40
|
-
});
|
|
41
|
-
expect(MyModel.sections.items.heading).toBe("Items");
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
it("stores relationships", () => {
|
|
45
|
-
const Target = defineModel("Target");
|
|
46
|
-
const MyModel = defineModel("MyModel", {
|
|
47
|
-
relationships: {
|
|
48
|
-
targets: hasMany(() => Target, { heading: "Targets" }),
|
|
49
|
-
},
|
|
50
|
-
});
|
|
51
|
-
expect(MyModel.relationships.targets.type).toBe("hasMany");
|
|
52
|
-
expect(MyModel.relationships.targets.heading).toBe("Targets");
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
it("stores computed properties", () => {
|
|
56
|
-
const MyModel = defineModel("MyModel", {
|
|
57
|
-
computed: {
|
|
58
|
-
foo: () => 42,
|
|
59
|
-
},
|
|
60
|
-
});
|
|
61
|
-
expect(MyModel.computed.foo).toBeDefined();
|
|
62
|
-
expect(MyModel.computed.foo({})).toBe(42);
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
it("stores match function", () => {
|
|
66
|
-
const fn = (doc: any) => doc.id.startsWith("special");
|
|
67
|
-
const MyModel = defineModel("MyModel", { match: fn });
|
|
68
|
-
expect(MyModel.match).toBe(fn);
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
it("stores defaults", () => {
|
|
72
|
-
const MyModel = defineModel("MyModel", {
|
|
73
|
-
meta: z.object({ status: z.string().default("draft") }),
|
|
74
|
-
defaults: { status: "draft" },
|
|
75
|
-
});
|
|
76
|
-
expect(MyModel.defaults).toEqual({ status: "draft" });
|
|
77
|
-
});
|
|
78
|
-
});
|
package/test/document.test.ts
DELETED
|
@@ -1,225 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach } from "vitest";
|
|
2
|
-
import { Collection } from "../src/collection";
|
|
3
|
-
import { createTestCollection } from "./helpers";
|
|
4
|
-
import { toString } from "mdast-util-to-string";
|
|
5
|
-
|
|
6
|
-
describe("Document", () => {
|
|
7
|
-
let collection: Collection;
|
|
8
|
-
|
|
9
|
-
beforeEach(async () => {
|
|
10
|
-
collection = await createTestCollection();
|
|
11
|
-
});
|
|
12
|
-
|
|
13
|
-
it("lazily parses AST", () => {
|
|
14
|
-
const doc = collection.document("epics/authentication");
|
|
15
|
-
// Content should exist but AST is not parsed yet
|
|
16
|
-
expect(doc.content).toBeDefined();
|
|
17
|
-
// Accessing ast triggers parsing
|
|
18
|
-
expect(doc.ast.type).toBe("root");
|
|
19
|
-
expect(doc.ast.children.length).toBeGreaterThan(0);
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
it("extracts title from first heading", () => {
|
|
23
|
-
const doc = collection.document("epics/authentication");
|
|
24
|
-
expect(doc.title).toBe("Authentication");
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
it("falls back to id if no heading", () => {
|
|
28
|
-
const doc = collection.createDocument({
|
|
29
|
-
id: "test/no-heading",
|
|
30
|
-
content: "Just some text without a heading.",
|
|
31
|
-
});
|
|
32
|
-
expect(doc.title).toBe("test/no-heading");
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
it("generates slug from title", () => {
|
|
36
|
-
const doc = collection.document("epics/authentication");
|
|
37
|
-
expect(doc.slug).toBe("authentication");
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
it("exposes parsed frontmatter as meta", () => {
|
|
41
|
-
const doc = collection.document("epics/authentication");
|
|
42
|
-
expect(doc.meta.priority).toBe("high");
|
|
43
|
-
expect(doc.meta.status).toBe("created");
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
it("serializes rawContent with frontmatter", () => {
|
|
47
|
-
const doc = collection.document("epics/authentication");
|
|
48
|
-
expect(doc.rawContent).toContain("---");
|
|
49
|
-
expect(doc.rawContent).toContain("priority: high");
|
|
50
|
-
expect(doc.rawContent).toContain("# Authentication");
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
it("omits frontmatter block if meta is empty", () => {
|
|
54
|
-
const doc = collection.createDocument({
|
|
55
|
-
id: "test/no-meta",
|
|
56
|
-
content: "# Hello\n",
|
|
57
|
-
});
|
|
58
|
-
expect(doc.rawContent).not.toContain("---");
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
it("provides astQuery", () => {
|
|
62
|
-
const doc = collection.document("epics/authentication");
|
|
63
|
-
expect(doc.astQuery.select("heading")).toBeDefined();
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
it("provides nodes shortcuts", () => {
|
|
67
|
-
const doc = collection.document("epics/authentication");
|
|
68
|
-
expect(doc.nodes.headings.length).toBeGreaterThan(0);
|
|
69
|
-
expect(doc.nodes.firstHeading).toBeDefined();
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
describe("extractSection", () => {
|
|
73
|
-
it("extracts section by heading text", () => {
|
|
74
|
-
const doc = collection.document("epics/authentication");
|
|
75
|
-
const section = doc.extractSection("Stories");
|
|
76
|
-
expect(section.length).toBeGreaterThan(1);
|
|
77
|
-
expect(toString(section[0])).toBe("Stories");
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
it("throws for non-existent heading", () => {
|
|
81
|
-
const doc = collection.document("epics/authentication");
|
|
82
|
-
expect(() => doc.extractSection("Nonexistent")).toThrow(
|
|
83
|
-
"Heading not found"
|
|
84
|
-
);
|
|
85
|
-
});
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
describe("querySection", () => {
|
|
89
|
-
it("returns scoped AstQuery without the heading", () => {
|
|
90
|
-
const doc = collection.document("epics/authentication");
|
|
91
|
-
const query = doc.querySection("Stories");
|
|
92
|
-
const headings = query.selectAll("heading");
|
|
93
|
-
// Should have the story sub-headings but not "Stories" itself
|
|
94
|
-
expect(headings.length).toBeGreaterThan(0);
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
it("returns empty query for non-existent heading", () => {
|
|
98
|
-
const doc = collection.document("epics/authentication");
|
|
99
|
-
const query = doc.querySection("Nonexistent");
|
|
100
|
-
expect(query.selectAll("heading").length).toBe(0);
|
|
101
|
-
});
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
describe("immutable section mutations", () => {
|
|
105
|
-
it("removeSection returns new Document", () => {
|
|
106
|
-
const doc = collection.document("epics/authentication");
|
|
107
|
-
const originalContent = doc.content;
|
|
108
|
-
const newDoc = doc.removeSection("Stories");
|
|
109
|
-
expect(newDoc).not.toBe(doc);
|
|
110
|
-
expect(doc.content).toBe(originalContent);
|
|
111
|
-
expect(newDoc.content).not.toContain("A User should be able to register");
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
it("replaceSectionContent returns new Document", () => {
|
|
115
|
-
const doc = collection.document("epics/authentication");
|
|
116
|
-
const newDoc = doc.replaceSectionContent(
|
|
117
|
-
"Stories",
|
|
118
|
-
"New stories content here."
|
|
119
|
-
);
|
|
120
|
-
expect(newDoc).not.toBe(doc);
|
|
121
|
-
expect(newDoc.content).toContain("New stories content here");
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
it("insertBefore returns new Document", () => {
|
|
125
|
-
const doc = collection.document("epics/authentication");
|
|
126
|
-
const heading = doc.astQuery.findHeadingByText("Stories");
|
|
127
|
-
expect(heading).toBeDefined();
|
|
128
|
-
const newDoc = doc.insertBefore(heading!, "Inserted before.");
|
|
129
|
-
expect(newDoc).not.toBe(doc);
|
|
130
|
-
expect(newDoc.content).toContain("Inserted before");
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
it("insertAfter returns new Document", () => {
|
|
134
|
-
const doc = collection.document("epics/authentication");
|
|
135
|
-
const heading = doc.astQuery.findHeadingByText("Stories");
|
|
136
|
-
expect(heading).toBeDefined();
|
|
137
|
-
const newDoc = doc.insertAfter(heading!, "Inserted after.");
|
|
138
|
-
expect(newDoc).not.toBe(doc);
|
|
139
|
-
expect(newDoc.content).toContain("Inserted after");
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
it("appendToSection returns new Document", () => {
|
|
143
|
-
const doc = collection.document("epics/authentication");
|
|
144
|
-
const newDoc = doc.appendToSection(
|
|
145
|
-
"Stories",
|
|
146
|
-
"### New Story\n\nAppended story."
|
|
147
|
-
);
|
|
148
|
-
expect(newDoc).not.toBe(doc);
|
|
149
|
-
expect(newDoc.content).toContain("New Story");
|
|
150
|
-
});
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
describe("mutable mutations", () => {
|
|
154
|
-
it("removeSection with mutate modifies in place", () => {
|
|
155
|
-
const doc = collection.createDocument({
|
|
156
|
-
id: "test/mutable",
|
|
157
|
-
content:
|
|
158
|
-
"# Test\n\n## Section A\n\nContent A\n\n## Section B\n\nContent B\n",
|
|
159
|
-
});
|
|
160
|
-
const result = doc.removeSection("Section A", { mutate: true });
|
|
161
|
-
expect(result).toBe(doc);
|
|
162
|
-
expect(doc.content).not.toContain("Content A");
|
|
163
|
-
expect(doc.content).toContain("Content B");
|
|
164
|
-
});
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
it("replaceContent returns new Document", () => {
|
|
168
|
-
const doc = collection.document("epics/authentication");
|
|
169
|
-
const newDoc = doc.replaceContent("# Completely new content");
|
|
170
|
-
expect(newDoc).not.toBe(doc);
|
|
171
|
-
expect(newDoc.content).toBe("# Completely new content");
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
it("appendContent returns new Document", () => {
|
|
175
|
-
const doc = collection.createDocument({
|
|
176
|
-
id: "test/append",
|
|
177
|
-
content: "# Start\n",
|
|
178
|
-
});
|
|
179
|
-
const newDoc = doc.appendContent("\n## Added\n");
|
|
180
|
-
expect(newDoc).not.toBe(doc);
|
|
181
|
-
expect(newDoc.content).toContain("Added");
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
it("toJSON serializes document", () => {
|
|
185
|
-
const doc = collection.document("epics/authentication");
|
|
186
|
-
const json = doc.toJSON();
|
|
187
|
-
expect(json.id).toBe("epics/authentication");
|
|
188
|
-
expect(json.meta).toBeDefined();
|
|
189
|
-
expect(json.content).toBeDefined();
|
|
190
|
-
expect(json.ast).toBeDefined();
|
|
191
|
-
});
|
|
192
|
-
|
|
193
|
-
it("toText extracts plain text", () => {
|
|
194
|
-
const doc = collection.document("epics/authentication");
|
|
195
|
-
const text = doc.toText();
|
|
196
|
-
expect(text).toContain("Authentication");
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
describe("toOutline", () => {
|
|
200
|
-
it("returns indented heading outline", () => {
|
|
201
|
-
const doc = collection.document("epics/authentication");
|
|
202
|
-
const outline = doc.toOutline();
|
|
203
|
-
expect(outline).toBe(
|
|
204
|
-
[
|
|
205
|
-
"- Authentication",
|
|
206
|
-
" - Stories",
|
|
207
|
-
" - A User should be able to register.",
|
|
208
|
-
" - Acceptance Criteria",
|
|
209
|
-
" - Mockups",
|
|
210
|
-
" - A User should be able to login.",
|
|
211
|
-
" - Acceptance Criteria",
|
|
212
|
-
" - Mockups",
|
|
213
|
-
].join("\n")
|
|
214
|
-
);
|
|
215
|
-
});
|
|
216
|
-
|
|
217
|
-
it("returns empty string for document with no headings", () => {
|
|
218
|
-
const doc = collection.createDocument({
|
|
219
|
-
id: "test/no-headings",
|
|
220
|
-
content: "Just some text without any headings.",
|
|
221
|
-
});
|
|
222
|
-
expect(doc.toOutline()).toBe("");
|
|
223
|
-
});
|
|
224
|
-
});
|
|
225
|
-
});
|