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
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
---
|
|
2
|
+
priority: high
|
|
3
|
+
status: created
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Authentication
|
|
7
|
+
|
|
8
|
+
The Authentication stories cover users logging in and out of the application, as well as the roles and permissions granted to these users and how they are enforced in the application.
|
|
9
|
+
|
|
10
|
+
## Stories
|
|
11
|
+
|
|
12
|
+
### A User should be able to register.
|
|
13
|
+
|
|
14
|
+
As a User I would like to register so that I can use the application.
|
|
15
|
+
|
|
16
|
+
#### Acceptance Criteria
|
|
17
|
+
|
|
18
|
+
- A user can visit the signup form, supply their name, email, and password
|
|
19
|
+
- The signup form should validate the user's information and supply errors
|
|
20
|
+
- The user should receive a confirmation email
|
|
21
|
+
- The user should show up in our database as confirmed after clicking the confirmation link
|
|
22
|
+
|
|
23
|
+
#### Mockups
|
|
24
|
+
|
|
25
|
+
- [Invision: Registration Form](https://invisionapp.com)
|
|
26
|
+
- [Invision: Registration Form Error State](https://invisionapp.com)
|
|
27
|
+
|
|
28
|
+
### A User should be able to login.
|
|
29
|
+
|
|
30
|
+
As a User I would like to login so that I can use the application.
|
|
31
|
+
|
|
32
|
+
#### Acceptance Criteria
|
|
33
|
+
|
|
34
|
+
- A user can visit the signup form, supply their name, email, and password
|
|
35
|
+
- The signup form should validate the user's information and supply errors
|
|
36
|
+
- The user should receive a confirmation email
|
|
37
|
+
- The user should show up in our database as confirmed after clicking the confirmation link
|
|
38
|
+
|
|
39
|
+
#### Mockups
|
|
40
|
+
|
|
41
|
+
- [Invision: Login Form](https://invisionapp.com)
|
|
42
|
+
- [Invision: Login Form Error State ](https://invisionapp.com)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
---
|
|
2
|
+
status: created
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Searching And Browsing
|
|
6
|
+
|
|
7
|
+
This epic covers the stories related to searching for a specific item.
|
|
8
|
+
|
|
9
|
+
## Stories
|
|
10
|
+
|
|
11
|
+
### Searching for a product by category
|
|
12
|
+
|
|
13
|
+
As a user I want to be able to search for a product by category so that I can find the right product for me.
|
|
14
|
+
|
|
15
|
+
### Searching for a product by manufacturer
|
|
16
|
+
|
|
17
|
+
As a user I want to be able to search for a product by manufacturer so that I can find the right product for me.
|
|
18
|
+
|
|
19
|
+
### Searching for a product by name
|
|
20
|
+
|
|
21
|
+
As a user I want to be able to search for a product by name so that I can find the right product for me.
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import {
|
|
2
|
+
defineModel,
|
|
3
|
+
section,
|
|
4
|
+
hasMany,
|
|
5
|
+
belongsTo,
|
|
6
|
+
z,
|
|
7
|
+
type ModelDefinition,
|
|
8
|
+
type HasManyDefinition,
|
|
9
|
+
type BelongsToDefinition,
|
|
10
|
+
type SectionDefinition,
|
|
11
|
+
} from "../../../src/index";
|
|
12
|
+
import { toString } from "mdast-util-to-string";
|
|
13
|
+
|
|
14
|
+
const epicMeta = z.object({
|
|
15
|
+
priority: z.enum(["low", "medium", "high"]).optional(),
|
|
16
|
+
status: z
|
|
17
|
+
.enum(["created", "in-progress", "complete"])
|
|
18
|
+
.default("created"),
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const storyMeta = z.object({
|
|
22
|
+
status: z
|
|
23
|
+
.enum(["created", "in-progress", "complete"])
|
|
24
|
+
.default("created"),
|
|
25
|
+
epic: z.string().optional(),
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
/** Explicit type for Epic so circular Epic↔Story inference doesn’t collapse to never */
|
|
29
|
+
export type EpicDef = ModelDefinition<
|
|
30
|
+
"Epic",
|
|
31
|
+
typeof epicMeta,
|
|
32
|
+
Record<string, never>,
|
|
33
|
+
{ stories: HasManyDefinition<StoryDef> },
|
|
34
|
+
{ isComplete: (self: any) => boolean }
|
|
35
|
+
>;
|
|
36
|
+
|
|
37
|
+
/** Explicit type for Story so circular Epic↔Story inference doesn’t collapse to never */
|
|
38
|
+
export type StoryDef = ModelDefinition<
|
|
39
|
+
"Story",
|
|
40
|
+
typeof storyMeta,
|
|
41
|
+
Record<string, SectionDefinition<any>>,
|
|
42
|
+
{ epic: BelongsToDefinition<EpicDef> },
|
|
43
|
+
{ isComplete: (self: any) => boolean }
|
|
44
|
+
>;
|
|
45
|
+
|
|
46
|
+
export const Epic: EpicDef = defineModel("Epic", {
|
|
47
|
+
prefix: "epics",
|
|
48
|
+
meta: epicMeta,
|
|
49
|
+
relationships: {
|
|
50
|
+
stories: hasMany(() => Story, {
|
|
51
|
+
heading: "Stories",
|
|
52
|
+
}),
|
|
53
|
+
},
|
|
54
|
+
computed: {
|
|
55
|
+
isComplete: (self: any) => self.meta.status === "complete",
|
|
56
|
+
},
|
|
57
|
+
defaults: {
|
|
58
|
+
status: "created",
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
export const Story: StoryDef = defineModel("Story", {
|
|
63
|
+
prefix: "stories",
|
|
64
|
+
meta: storyMeta,
|
|
65
|
+
sections: {
|
|
66
|
+
acceptanceCriteria: section("Acceptance Criteria", {
|
|
67
|
+
extract: (query) =>
|
|
68
|
+
query.selectAll("listItem").map((n) => toString(n)),
|
|
69
|
+
schema: z.array(z.string()),
|
|
70
|
+
}),
|
|
71
|
+
mockups: section("Mockups", {
|
|
72
|
+
extract: (query) =>
|
|
73
|
+
Object.fromEntries(
|
|
74
|
+
query
|
|
75
|
+
.selectAll("link")
|
|
76
|
+
.map((l: any) => [toString(l), l.url])
|
|
77
|
+
),
|
|
78
|
+
schema: z.record(z.string(), z.string()),
|
|
79
|
+
}),
|
|
80
|
+
},
|
|
81
|
+
relationships: {
|
|
82
|
+
epic: belongsTo(() => Epic, {
|
|
83
|
+
foreignKey: (doc) => doc.meta.epic as string,
|
|
84
|
+
}),
|
|
85
|
+
},
|
|
86
|
+
computed: {
|
|
87
|
+
isComplete: (self: any) => self.meta.status === "complete",
|
|
88
|
+
},
|
|
89
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
---
|
|
2
|
+
status: created
|
|
3
|
+
epic: authentication
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# A User should be able to register.
|
|
7
|
+
|
|
8
|
+
As a User I would like to register so that I can use the application.
|
|
9
|
+
|
|
10
|
+
## Acceptance Criteria
|
|
11
|
+
|
|
12
|
+
- A user can visit the signup form, supply their name, email, and password
|
|
13
|
+
- The signup form should validate the user's information and supply errors
|
|
14
|
+
- The user should receive a confirmation email
|
|
15
|
+
- The user should show up in our database as confirmed after clicking the confirmation link
|
|
16
|
+
|
|
17
|
+
## Mockups
|
|
18
|
+
|
|
19
|
+
- [Invision: Registration Form](https://invisionapp.com)
|
|
20
|
+
- [Invision: Registration Form Error State](https://invisionapp.com)
|
package/test/helpers.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import { Collection } from "../src/collection";
|
|
3
|
+
import { Epic, Story } from "./fixtures/sdlc/models";
|
|
4
|
+
|
|
5
|
+
const dir = import.meta.dirname ?? new URL(".", import.meta.url).pathname;
|
|
6
|
+
|
|
7
|
+
export const FIXTURES_PATH = path.resolve(
|
|
8
|
+
dir,
|
|
9
|
+
"fixtures/sdlc"
|
|
10
|
+
);
|
|
11
|
+
|
|
12
|
+
export async function createTestCollection(): Promise<Collection> {
|
|
13
|
+
const collection = new Collection({
|
|
14
|
+
rootPath: FIXTURES_PATH,
|
|
15
|
+
name: "test-sdlc",
|
|
16
|
+
});
|
|
17
|
+
collection.register(Epic);
|
|
18
|
+
collection.register(Story);
|
|
19
|
+
await collection.load();
|
|
20
|
+
return collection;
|
|
21
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from "vitest";
|
|
2
|
+
import { Collection } from "../src/collection";
|
|
3
|
+
import { createModelInstance } from "../src/model-instance";
|
|
4
|
+
import { createTestCollection } from "./helpers";
|
|
5
|
+
import { Epic, Story } from "./fixtures/sdlc/models";
|
|
6
|
+
|
|
7
|
+
describe("createModelInstance", () => {
|
|
8
|
+
let collection: Collection;
|
|
9
|
+
|
|
10
|
+
beforeEach(async () => {
|
|
11
|
+
collection = await createTestCollection();
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
describe("core properties", () => {
|
|
15
|
+
it("has correct id", () => {
|
|
16
|
+
const doc = collection.document("epics/authentication");
|
|
17
|
+
const instance = createModelInstance(doc, Epic, collection);
|
|
18
|
+
expect(instance.id).toBe("epics/authentication");
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("has correct title", () => {
|
|
22
|
+
const doc = collection.document("epics/authentication");
|
|
23
|
+
const instance = createModelInstance(doc, Epic, collection);
|
|
24
|
+
expect(instance.title).toBe("Authentication");
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("has correct slug", () => {
|
|
28
|
+
const doc = collection.document("epics/authentication");
|
|
29
|
+
const instance = createModelInstance(doc, Epic, collection);
|
|
30
|
+
expect(instance.slug).toBe("authentication");
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("references the original document", () => {
|
|
34
|
+
const doc = collection.document("epics/authentication");
|
|
35
|
+
const instance = createModelInstance(doc, Epic, collection);
|
|
36
|
+
expect(instance.document).toBe(doc);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("references the collection", () => {
|
|
40
|
+
const doc = collection.document("epics/authentication");
|
|
41
|
+
const instance = createModelInstance(doc, Epic, collection);
|
|
42
|
+
expect(instance.collection).toBe(collection);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe("meta", () => {
|
|
47
|
+
it("returns Zod-parsed frontmatter", () => {
|
|
48
|
+
const doc = collection.document("epics/authentication");
|
|
49
|
+
const instance = createModelInstance(doc, Epic, collection);
|
|
50
|
+
expect(instance.meta.priority).toBe("high");
|
|
51
|
+
expect(instance.meta.status).toBe("created");
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("applies defaults where fields are missing", () => {
|
|
55
|
+
const doc = collection.document("epics/searching-and-browsing");
|
|
56
|
+
const instance = createModelInstance(doc, Epic, collection);
|
|
57
|
+
expect(instance.meta.status).toBe("created");
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
describe("sections", () => {
|
|
62
|
+
it("lazily extracts section data", () => {
|
|
63
|
+
const doc = collection.document(
|
|
64
|
+
"stories/authentication/a-user-should-be-able-to-register"
|
|
65
|
+
);
|
|
66
|
+
const instance = createModelInstance(doc, Story, collection);
|
|
67
|
+
const criteria = instance.sections.acceptanceCriteria;
|
|
68
|
+
expect(Array.isArray(criteria)).toBe(true);
|
|
69
|
+
expect(criteria.length).toBe(4);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("extracts mockups as record", () => {
|
|
73
|
+
const doc = collection.document(
|
|
74
|
+
"stories/authentication/a-user-should-be-able-to-register"
|
|
75
|
+
);
|
|
76
|
+
const instance = createModelInstance(doc, Story, collection);
|
|
77
|
+
const mockups = instance.sections.mockups;
|
|
78
|
+
expect(typeof mockups).toBe("object");
|
|
79
|
+
expect(Object.keys(mockups).length).toBeGreaterThan(0);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
describe("computed", () => {
|
|
84
|
+
it("evaluates computed properties", () => {
|
|
85
|
+
const doc = collection.document("epics/authentication");
|
|
86
|
+
const instance = createModelInstance(doc, Epic, collection);
|
|
87
|
+
expect(instance.computed.isComplete).toBe(false);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
describe("relationships", () => {
|
|
92
|
+
it("hasMany fetchAll returns related instances", () => {
|
|
93
|
+
const doc = collection.document("epics/authentication");
|
|
94
|
+
const instance = createModelInstance(doc, Epic, collection);
|
|
95
|
+
const stories = instance.relationships.stories.fetchAll();
|
|
96
|
+
expect(stories.length).toBe(2);
|
|
97
|
+
expect(stories[0].title).toBeDefined();
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("hasMany first returns first child", () => {
|
|
101
|
+
const doc = collection.document("epics/authentication");
|
|
102
|
+
const instance = createModelInstance(doc, Epic, collection);
|
|
103
|
+
const first = instance.relationships.stories.first();
|
|
104
|
+
expect(first).toBeDefined();
|
|
105
|
+
expect(first!.title).toContain("register");
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("hasMany last returns last child", () => {
|
|
109
|
+
const doc = collection.document("epics/authentication");
|
|
110
|
+
const instance = createModelInstance(doc, Epic, collection);
|
|
111
|
+
const last = instance.relationships.stories.last();
|
|
112
|
+
expect(last).toBeDefined();
|
|
113
|
+
expect(last!.title).toContain("login");
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("belongsTo fetch returns parent", () => {
|
|
117
|
+
const doc = collection.document(
|
|
118
|
+
"stories/authentication/a-user-should-be-able-to-register"
|
|
119
|
+
);
|
|
120
|
+
const instance = createModelInstance(doc, Story, collection);
|
|
121
|
+
const epic = instance.relationships.epic.fetch();
|
|
122
|
+
expect(epic.title).toBe("Authentication");
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
describe("validation", () => {
|
|
127
|
+
it("returns valid for good data", async () => {
|
|
128
|
+
const doc = collection.document("epics/authentication");
|
|
129
|
+
const instance = createModelInstance(doc, Epic, collection);
|
|
130
|
+
const result = await instance.validate();
|
|
131
|
+
expect(result.valid).toBe(true);
|
|
132
|
+
expect(result.errors.length).toBe(0);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("returns errors for bad meta", async () => {
|
|
136
|
+
const doc = collection.createDocument({
|
|
137
|
+
id: "test/bad",
|
|
138
|
+
content: "# Bad Doc\n",
|
|
139
|
+
meta: { status: "INVALID_STATUS" },
|
|
140
|
+
});
|
|
141
|
+
const instance = createModelInstance(doc, Epic, collection);
|
|
142
|
+
const result = await instance.validate();
|
|
143
|
+
expect(result.valid).toBe(false);
|
|
144
|
+
expect(result.errors.length).toBeGreaterThan(0);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("populates errors map", async () => {
|
|
148
|
+
const doc = collection.createDocument({
|
|
149
|
+
id: "test/bad",
|
|
150
|
+
content: "# Bad Doc\n",
|
|
151
|
+
meta: { status: "INVALID_STATUS" },
|
|
152
|
+
});
|
|
153
|
+
const instance = createModelInstance(doc, Epic, collection);
|
|
154
|
+
await instance.validate();
|
|
155
|
+
expect(instance.hasErrors).toBe(true);
|
|
156
|
+
expect(instance.errors.size).toBeGreaterThan(0);
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
describe("toJSON", () => {
|
|
161
|
+
it("returns id, title, meta by default", () => {
|
|
162
|
+
const doc = collection.document("epics/authentication");
|
|
163
|
+
const instance = createModelInstance(doc, Epic, collection);
|
|
164
|
+
const json = instance.toJSON();
|
|
165
|
+
expect(json.id).toBe("epics/authentication");
|
|
166
|
+
expect(json.title).toBe("Authentication");
|
|
167
|
+
expect(json.meta).toBeDefined();
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it("includes requested sections", () => {
|
|
171
|
+
const doc = collection.document(
|
|
172
|
+
"stories/authentication/a-user-should-be-able-to-register"
|
|
173
|
+
);
|
|
174
|
+
const instance = createModelInstance(doc, Story, collection);
|
|
175
|
+
const json = instance.toJSON({
|
|
176
|
+
sections: ["acceptanceCriteria"],
|
|
177
|
+
});
|
|
178
|
+
expect(json.acceptanceCriteria).toBeDefined();
|
|
179
|
+
expect(Array.isArray(json.acceptanceCriteria)).toBe(true);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it("includes requested computed values", () => {
|
|
183
|
+
const doc = collection.document("epics/authentication");
|
|
184
|
+
const instance = createModelInstance(doc, Epic, collection);
|
|
185
|
+
const json = instance.toJSON({ computed: ["isComplete"] });
|
|
186
|
+
expect(json.isComplete).toBe(false);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it("includes requested relationships", () => {
|
|
190
|
+
const doc = collection.document("epics/authentication");
|
|
191
|
+
const instance = createModelInstance(doc, Epic, collection);
|
|
192
|
+
const json = instance.toJSON({ related: ["stories"] });
|
|
193
|
+
expect(json.stories).toBeDefined();
|
|
194
|
+
expect(Array.isArray(json.stories)).toBe(true);
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
});
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from "vitest";
|
|
2
|
+
import { QueryBuilder } from "../src/query/query-builder";
|
|
3
|
+
import { operators } from "../src/query/operators";
|
|
4
|
+
import { Collection } from "../src/collection";
|
|
5
|
+
import { createTestCollection } from "./helpers";
|
|
6
|
+
import { Epic } from "./fixtures/sdlc/models";
|
|
7
|
+
|
|
8
|
+
describe("QueryBuilder", () => {
|
|
9
|
+
it("builds eq conditions with two args", () => {
|
|
10
|
+
const qb = new QueryBuilder();
|
|
11
|
+
qb.where("meta.status", "active");
|
|
12
|
+
expect(qb.conditions).toEqual([
|
|
13
|
+
{ path: "meta.status", operator: "eq", value: "active" },
|
|
14
|
+
]);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("builds conditions with explicit operator", () => {
|
|
18
|
+
const qb = new QueryBuilder();
|
|
19
|
+
qb.where("meta.count", "gt", 5);
|
|
20
|
+
expect(qb.conditions[0].operator).toBe("gt");
|
|
21
|
+
expect(qb.conditions[0].value).toBe(5);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("builds conditions from object shorthand", () => {
|
|
25
|
+
const qb = new QueryBuilder();
|
|
26
|
+
qb.where({ "meta.status": "active", "meta.type": "post" });
|
|
27
|
+
expect(qb.conditions.length).toBe(2);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("whereIn adds in condition", () => {
|
|
31
|
+
const qb = new QueryBuilder();
|
|
32
|
+
qb.whereIn("meta.tags", ["a", "b"]);
|
|
33
|
+
expect(qb.conditions[0].operator).toBe("in");
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("whereNotIn adds notIn condition", () => {
|
|
37
|
+
const qb = new QueryBuilder();
|
|
38
|
+
qb.whereNotIn("meta.status", ["archived"]);
|
|
39
|
+
expect(qb.conditions[0].operator).toBe("notIn");
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("supports all chainable methods", () => {
|
|
43
|
+
const qb = new QueryBuilder();
|
|
44
|
+
const result = qb
|
|
45
|
+
.where("a", "1")
|
|
46
|
+
.whereGt("b", 2)
|
|
47
|
+
.whereLt("c", 3)
|
|
48
|
+
.whereGte("d", 4)
|
|
49
|
+
.whereLte("e", 5)
|
|
50
|
+
.whereContains("f", "hello")
|
|
51
|
+
.whereStartsWith("g", "pre")
|
|
52
|
+
.whereEndsWith("h", "suf")
|
|
53
|
+
.whereRegex("i", /test/)
|
|
54
|
+
.whereExists("j")
|
|
55
|
+
.whereNotExists("k");
|
|
56
|
+
|
|
57
|
+
expect(result).toBe(qb); // chaining returns this
|
|
58
|
+
expect(qb.conditions.length).toBe(11);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("where with three args returns this (bug fix)", () => {
|
|
62
|
+
const qb = new QueryBuilder();
|
|
63
|
+
const result = qb.where("path", "neq", "value");
|
|
64
|
+
expect(result).toBe(qb);
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
describe("operators", () => {
|
|
69
|
+
it("eq handles primitive equality", () => {
|
|
70
|
+
expect(operators.eq("a", "a")).toBe(true);
|
|
71
|
+
expect(operators.eq("a", "b")).toBe(false);
|
|
72
|
+
expect(operators.eq(1, 1)).toBe(true);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("eq handles deep equality", () => {
|
|
76
|
+
expect(operators.eq({ a: 1 }, { a: 1 })).toBe(true);
|
|
77
|
+
expect(operators.eq({ a: 1 }, { a: 2 })).toBe(false);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("neq is negation of eq", () => {
|
|
81
|
+
expect(operators.neq("a", "b")).toBe(true);
|
|
82
|
+
expect(operators.neq("a", "a")).toBe(false);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("in checks membership", () => {
|
|
86
|
+
expect(operators.in("a", ["a", "b"])).toBe(true);
|
|
87
|
+
expect(operators.in("c", ["a", "b"])).toBe(false);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("notIn checks non-membership", () => {
|
|
91
|
+
expect(operators.notIn("c", ["a", "b"])).toBe(true);
|
|
92
|
+
expect(operators.notIn("a", ["a", "b"])).toBe(false);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("gt/lt/gte/lte compare values", () => {
|
|
96
|
+
expect(operators.gt(5, 3)).toBe(true);
|
|
97
|
+
expect(operators.lt(3, 5)).toBe(true);
|
|
98
|
+
expect(operators.gte(5, 5)).toBe(true);
|
|
99
|
+
expect(operators.lte(5, 5)).toBe(true);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("contains checks string inclusion", () => {
|
|
103
|
+
expect(operators.contains("hello world", "world")).toBe(true);
|
|
104
|
+
expect(operators.contains("hello", "world")).toBe(false);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("startsWith/endsWith check string prefixes/suffixes", () => {
|
|
108
|
+
expect(operators.startsWith("hello", "hel")).toBe(true);
|
|
109
|
+
expect(operators.endsWith("hello", "llo")).toBe(true);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("regex tests patterns", () => {
|
|
113
|
+
expect(operators.regex("hello123", /\d+/)).toBe(true);
|
|
114
|
+
expect(operators.regex("hello123", "\\d+")).toBe(true);
|
|
115
|
+
expect(operators.regex("hello", /\d+/)).toBe(false);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("exists checks for defined values", () => {
|
|
119
|
+
expect(operators.exists("value", true)).toBe(true);
|
|
120
|
+
expect(operators.exists(null, true)).toBe(false);
|
|
121
|
+
expect(operators.exists(undefined, true)).toBe(false);
|
|
122
|
+
expect(operators.exists(null, false)).toBe(true);
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
describe("CollectionQuery", () => {
|
|
127
|
+
let collection: Collection;
|
|
128
|
+
|
|
129
|
+
beforeEach(async () => {
|
|
130
|
+
collection = await createTestCollection();
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("fetchAll returns matching model instances", async () => {
|
|
134
|
+
const epics = await collection.query(Epic).fetchAll();
|
|
135
|
+
expect(epics.length).toBe(2);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("where filters results", async () => {
|
|
139
|
+
const epics = await collection
|
|
140
|
+
.query(Epic)
|
|
141
|
+
.where("meta.priority", "high")
|
|
142
|
+
.fetchAll();
|
|
143
|
+
expect(epics.length).toBe(1);
|
|
144
|
+
expect(epics[0].meta.priority).toBe("high");
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("first returns first result", async () => {
|
|
148
|
+
const first = await collection.query(Epic).first();
|
|
149
|
+
expect(first).toBeDefined();
|
|
150
|
+
expect(first!.title).toBeDefined();
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("last returns last result", async () => {
|
|
154
|
+
const last = await collection.query(Epic).last();
|
|
155
|
+
expect(last).toBeDefined();
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("count returns correct count", async () => {
|
|
159
|
+
const count = await collection.query(Epic).count();
|
|
160
|
+
expect(count).toBe(2);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it("empty conditions returns all of model type", async () => {
|
|
164
|
+
const all = await collection.query(Epic).fetchAll();
|
|
165
|
+
expect(all.length).toBe(2);
|
|
166
|
+
});
|
|
167
|
+
});
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from "vitest";
|
|
2
|
+
import { Collection } from "../src/collection";
|
|
3
|
+
import { createModelInstance } from "../src/model-instance";
|
|
4
|
+
import { createTestCollection } from "./helpers";
|
|
5
|
+
import { Epic, Story } from "./fixtures/sdlc/models";
|
|
6
|
+
|
|
7
|
+
describe("HasManyRelationship", () => {
|
|
8
|
+
let collection: Collection;
|
|
9
|
+
|
|
10
|
+
beforeEach(async () => {
|
|
11
|
+
collection = await createTestCollection();
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("extracts child headings from parent section", () => {
|
|
15
|
+
const epic = collection.getModel("epics/authentication", Epic);
|
|
16
|
+
const stories = epic.relationships.stories.fetchAll();
|
|
17
|
+
expect(stories.length).toBe(2);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("child instances have correct titles", () => {
|
|
21
|
+
const epic = collection.getModel("epics/authentication", Epic);
|
|
22
|
+
const stories = epic.relationships.stories.fetchAll();
|
|
23
|
+
expect(stories[0].title).toContain("register");
|
|
24
|
+
expect(stories[1].title).toContain("login");
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("first returns first child", () => {
|
|
28
|
+
const epic = collection.getModel("epics/authentication", Epic);
|
|
29
|
+
const first = epic.relationships.stories.first();
|
|
30
|
+
expect(first).toBeDefined();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("last returns last child", () => {
|
|
34
|
+
const epic = collection.getModel("epics/authentication", Epic);
|
|
35
|
+
const last = epic.relationships.stories.last();
|
|
36
|
+
expect(last).toBeDefined();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("works with epic that has multiple stories", () => {
|
|
40
|
+
const epic = collection.getModel(
|
|
41
|
+
"epics/searching-and-browsing",
|
|
42
|
+
Epic
|
|
43
|
+
);
|
|
44
|
+
const stories = epic.relationships.stories.fetchAll();
|
|
45
|
+
expect(stories.length).toBe(3);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("computes IDs as targetPrefix/parentSlug/childSlug", () => {
|
|
49
|
+
const epic = collection.getModel("epics/authentication", Epic);
|
|
50
|
+
const stories = epic.relationships.stories.fetchAll();
|
|
51
|
+
expect(stories[0].id).toContain("stories/");
|
|
52
|
+
expect(stories[0].id).toContain("authentication");
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe("BelongsToRelationship", () => {
|
|
57
|
+
let collection: Collection;
|
|
58
|
+
|
|
59
|
+
beforeEach(async () => {
|
|
60
|
+
collection = await createTestCollection();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("resolves parent by foreign key", () => {
|
|
64
|
+
const story = collection.getModel(
|
|
65
|
+
"stories/authentication/a-user-should-be-able-to-register",
|
|
66
|
+
Story
|
|
67
|
+
);
|
|
68
|
+
const epic = story.relationships.epic.fetch();
|
|
69
|
+
expect(epic.title).toBe("Authentication");
|
|
70
|
+
expect(epic.id).toBe("epics/authentication");
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("throws if parent not found", () => {
|
|
74
|
+
const doc = collection.createDocument({
|
|
75
|
+
id: "test/orphan",
|
|
76
|
+
content: "# Orphan Story\n",
|
|
77
|
+
meta: { epic: "nonexistent" },
|
|
78
|
+
});
|
|
79
|
+
const instance = createModelInstance(doc, Story, collection);
|
|
80
|
+
expect(() => instance.relationships.epic.fetch()).toThrow(
|
|
81
|
+
'Could not find Epic'
|
|
82
|
+
);
|
|
83
|
+
});
|
|
84
|
+
});
|