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.
Files changed (89) hide show
  1. package/README.md +460 -0
  2. package/bun.lock +473 -0
  3. package/examples/sdlc-queries.ts +161 -0
  4. package/package.json +41 -0
  5. package/showcases/national-parks/models.ts +74 -0
  6. package/showcases/national-parks/parks/acadia.mdx +40 -0
  7. package/showcases/national-parks/parks/yosemite.mdx +44 -0
  8. package/showcases/national-parks/parks/zion.mdx +44 -0
  9. package/showcases/national-parks/queries.ts +103 -0
  10. package/showcases/national-parks/trails/angels-landing.mdx +19 -0
  11. package/showcases/national-parks/trails/cathedral-lakes.mdx +19 -0
  12. package/showcases/national-parks/trails/half-dome.mdx +19 -0
  13. package/showcases/national-parks/trails/jordan-pond-path.mdx +19 -0
  14. package/showcases/national-parks/trails/mist-trail.mdx +19 -0
  15. package/showcases/national-parks/trails/observation-point.mdx +19 -0
  16. package/showcases/national-parks/trails/precipice-trail.mdx +19 -0
  17. package/showcases/national-parks/trails/the-narrows.mdx +19 -0
  18. package/showcases/recipes/cuisines/chinese.mdx +28 -0
  19. package/showcases/recipes/cuisines/italian.mdx +32 -0
  20. package/showcases/recipes/cuisines/mexican.mdx +28 -0
  21. package/showcases/recipes/models.ts +77 -0
  22. package/showcases/recipes/queries.ts +89 -0
  23. package/showcases/recipes/recipes/chinese/egg-fried-rice.mdx +43 -0
  24. package/showcases/recipes/recipes/chinese/mapo-tofu.mdx +47 -0
  25. package/showcases/recipes/recipes/italian/bruschetta.mdx +38 -0
  26. package/showcases/recipes/recipes/italian/cacio-e-pepe.mdx +39 -0
  27. package/showcases/recipes/recipes/italian/tiramisu.mdx +43 -0
  28. package/showcases/recipes/recipes/mexican/chicken-tinga.mdx +44 -0
  29. package/showcases/recipes/recipes/mexican/guacamole.mdx +39 -0
  30. package/showcases/vinyl-collection/albums/bitches-brew.mdx +36 -0
  31. package/showcases/vinyl-collection/albums/i-put-a-spell-on-you.mdx +35 -0
  32. package/showcases/vinyl-collection/albums/in-rainbows.mdx +35 -0
  33. package/showcases/vinyl-collection/albums/kind-of-blue.mdx +32 -0
  34. package/showcases/vinyl-collection/albums/ok-computer.mdx +37 -0
  35. package/showcases/vinyl-collection/albums/wild-is-the-wind.mdx +35 -0
  36. package/showcases/vinyl-collection/artists/miles-davis.mdx +27 -0
  37. package/showcases/vinyl-collection/artists/nina-simone.mdx +26 -0
  38. package/showcases/vinyl-collection/artists/radiohead.mdx +27 -0
  39. package/showcases/vinyl-collection/models.ts +73 -0
  40. package/showcases/vinyl-collection/queries.ts +87 -0
  41. package/src/ast-query.ts +132 -0
  42. package/src/cli/commands/action.ts +44 -0
  43. package/src/cli/commands/create.ts +59 -0
  44. package/src/cli/commands/export.ts +24 -0
  45. package/src/cli/commands/init.ts +75 -0
  46. package/src/cli/commands/inspect.ts +46 -0
  47. package/src/cli/commands/validate.ts +75 -0
  48. package/src/cli/index.ts +20 -0
  49. package/src/cli/load-collection.ts +53 -0
  50. package/src/collection.ts +399 -0
  51. package/src/define-model.ts +80 -0
  52. package/src/document.ts +468 -0
  53. package/src/index.ts +47 -0
  54. package/src/model-instance.ts +227 -0
  55. package/src/node-shortcuts.ts +87 -0
  56. package/src/parse.ts +123 -0
  57. package/src/query/collection-query.ts +149 -0
  58. package/src/query/index.ts +5 -0
  59. package/src/query/operators.ts +37 -0
  60. package/src/query/query-builder.ts +109 -0
  61. package/src/relationships/belongs-to.ts +50 -0
  62. package/src/relationships/has-many.ts +136 -0
  63. package/src/relationships/index.ts +57 -0
  64. package/src/relationships/types.ts +7 -0
  65. package/src/section.ts +29 -0
  66. package/src/types.ts +221 -0
  67. package/src/utils/index.ts +11 -0
  68. package/src/utils/inflect.ts +82 -0
  69. package/src/utils/normalize-headings.ts +31 -0
  70. package/src/utils/parse-table.ts +30 -0
  71. package/src/utils/read-directory.ts +35 -0
  72. package/src/utils/stringify-ast.ts +9 -0
  73. package/src/validator.ts +52 -0
  74. package/test/ast-query.test.ts +128 -0
  75. package/test/collection.test.ts +99 -0
  76. package/test/define-model.test.ts +78 -0
  77. package/test/document.test.ts +225 -0
  78. package/test/fixtures/sdlc/epics/authentication.mdx +42 -0
  79. package/test/fixtures/sdlc/epics/searching-and-browsing.mdx +21 -0
  80. package/test/fixtures/sdlc/models.ts +89 -0
  81. package/test/fixtures/sdlc/stories/authentication/a-user-should-be-able-to-register.mdx +20 -0
  82. package/test/helpers.ts +21 -0
  83. package/test/model-instance.test.ts +197 -0
  84. package/test/query.test.ts +167 -0
  85. package/test/relationships.test.ts +84 -0
  86. package/test/section.test.ts +99 -0
  87. package/test/validator.test.ts +62 -0
  88. package/tsconfig.json +18 -0
  89. package/vitest.config.ts +11 -0
@@ -0,0 +1,109 @@
1
+ import type { Operator } from "./operators";
2
+
3
+ export interface Condition {
4
+ path: string;
5
+ operator: Operator;
6
+ value: unknown;
7
+ }
8
+
9
+ export class QueryBuilder {
10
+ #conditions: Condition[] = [];
11
+
12
+ get conditions(): Condition[] {
13
+ return [...this.#conditions];
14
+ }
15
+
16
+ /**
17
+ * Add a where condition.
18
+ *
19
+ * Three call signatures:
20
+ * where({ "meta.status": "active" }) -- object shorthand, implicit eq
21
+ * where("meta.status", "active") -- two args, implicit eq
22
+ * where("meta.priority", "gt", 5) -- three args, explicit operator
23
+ *
24
+ * Always returns `this` for chaining (fixes bug in original).
25
+ */
26
+ where(
27
+ pathOrObject: string | Record<string, unknown>,
28
+ operatorOrValue?: Operator | unknown,
29
+ value?: unknown
30
+ ): this {
31
+ if (typeof pathOrObject === "object" && pathOrObject !== null) {
32
+ for (const [k, v] of Object.entries(pathOrObject)) {
33
+ this.#conditions.push({ path: k, operator: "eq", value: v });
34
+ }
35
+ return this;
36
+ }
37
+
38
+ if (value === undefined) {
39
+ // Two-arg form: where("path", value) -- implicit eq
40
+ this.#conditions.push({
41
+ path: pathOrObject,
42
+ operator: "eq",
43
+ value: operatorOrValue,
44
+ });
45
+ } else {
46
+ // Three-arg form: where("path", operator, value)
47
+ this.#conditions.push({
48
+ path: pathOrObject,
49
+ operator: operatorOrValue as Operator,
50
+ value,
51
+ });
52
+ }
53
+ return this;
54
+ }
55
+
56
+ whereIn(path: string, values: unknown[]): this {
57
+ this.#conditions.push({
58
+ path,
59
+ operator: "in",
60
+ value: values.filter(Boolean),
61
+ });
62
+ return this;
63
+ }
64
+
65
+ whereNotIn(path: string, values: unknown[]): this {
66
+ this.#conditions.push({ path, operator: "notIn", value: values });
67
+ return this;
68
+ }
69
+
70
+ whereGt(path: string, value: unknown): this {
71
+ return this.where(path, "gt", value);
72
+ }
73
+
74
+ whereLt(path: string, value: unknown): this {
75
+ return this.where(path, "lt", value);
76
+ }
77
+
78
+ whereGte(path: string, value: unknown): this {
79
+ return this.where(path, "gte", value);
80
+ }
81
+
82
+ whereLte(path: string, value: unknown): this {
83
+ return this.where(path, "lte", value);
84
+ }
85
+
86
+ whereContains(path: string, value: string): this {
87
+ return this.where(path, "contains", value);
88
+ }
89
+
90
+ whereStartsWith(path: string, value: string): this {
91
+ return this.where(path, "startsWith", value);
92
+ }
93
+
94
+ whereEndsWith(path: string, value: string): this {
95
+ return this.where(path, "endsWith", value);
96
+ }
97
+
98
+ whereRegex(path: string, pattern: RegExp | string): this {
99
+ return this.where(path, "regex", pattern);
100
+ }
101
+
102
+ whereExists(path: string): this {
103
+ return this.where(path, "exists", true);
104
+ }
105
+
106
+ whereNotExists(path: string): this {
107
+ return this.where(path, "exists", false);
108
+ }
109
+ }
@@ -0,0 +1,50 @@
1
+ import type { Document } from "../document";
2
+ import type { Collection } from "../collection";
3
+ import type {
4
+ BelongsToDefinition,
5
+ ModelDefinition,
6
+ InferModelInstance,
7
+ BelongsToAccessor,
8
+ ModelInstanceFactory,
9
+ } from "../types";
10
+
11
+ export class BelongsToRelationship<
12
+ TTarget extends ModelDefinition<any, any, any, any, any>,
13
+ > implements BelongsToAccessor<TTarget>
14
+ {
15
+ #document: Document;
16
+ #collection: Collection;
17
+ #definition: BelongsToDefinition<TTarget>;
18
+ #factory: ModelInstanceFactory;
19
+
20
+ constructor(
21
+ document: Document,
22
+ collection: Collection,
23
+ definition: BelongsToDefinition<TTarget>,
24
+ factory: ModelInstanceFactory
25
+ ) {
26
+ this.#document = document;
27
+ this.#collection = collection;
28
+ this.#definition = definition;
29
+ this.#factory = factory;
30
+ }
31
+
32
+ fetch(): InferModelInstance<TTarget> {
33
+ const targetDef = this.#definition.target();
34
+ const foreignKeyValue = this.#definition.foreignKey({
35
+ id: this.#document.id,
36
+ meta: this.#document.meta,
37
+ });
38
+
39
+ const relatedId = `${targetDef.prefix}/${foreignKeyValue}`;
40
+
41
+ if (!this.#collection.items.has(relatedId)) {
42
+ throw new Error(
43
+ `Could not find ${targetDef.name} with id "${relatedId}"`
44
+ );
45
+ }
46
+
47
+ const doc = this.#collection.document(relatedId);
48
+ return this.#factory(doc, targetDef, this.#collection);
49
+ }
50
+ }
@@ -0,0 +1,136 @@
1
+ import { toString } from "mdast-util-to-string";
2
+ import { kebabCase } from "../utils/inflect";
3
+ import type { Document } from "../document";
4
+ import type { Collection } from "../collection";
5
+ import type {
6
+ HasManyDefinition,
7
+ ModelDefinition,
8
+ InferModelInstance,
9
+ HasManyAccessor,
10
+ ModelInstanceFactory,
11
+ } from "../types";
12
+ import type { Root, Heading, Content, RootContent } from "mdast";
13
+
14
+ interface ChildNode {
15
+ title: string;
16
+ id: string;
17
+ ast: Root;
18
+ section: Content[];
19
+ }
20
+
21
+ export class HasManyRelationship<
22
+ TTarget extends ModelDefinition<any, any, any, any, any>,
23
+ > implements HasManyAccessor<TTarget>
24
+ {
25
+ #document: Document;
26
+ #collection: Collection;
27
+ #definition: HasManyDefinition<TTarget>;
28
+ #factory: ModelInstanceFactory;
29
+
30
+ constructor(
31
+ document: Document,
32
+ collection: Collection,
33
+ definition: HasManyDefinition<TTarget>,
34
+ factory: ModelInstanceFactory
35
+ ) {
36
+ this.#document = document;
37
+ this.#collection = collection;
38
+ this.#definition = definition;
39
+ this.#factory = factory;
40
+ }
41
+
42
+ /**
43
+ * Extract child nodes from the document AST.
44
+ *
45
+ * Algorithm (matching the original):
46
+ * 1. Find the parent heading by text (e.g., "Stories")
47
+ * 2. Extract the section under that heading
48
+ * 3. Filter for child headings at depth = parentHeading.depth + 1
49
+ * 4. For each child heading, extract its sub-section
50
+ * 5. Compute an ID from the parent title and child slug
51
+ */
52
+ private extractChildNodes(): ChildNode[] {
53
+ const { astQuery } = this.#document;
54
+ const parentHeading = astQuery.findHeadingByText(this.#definition.heading);
55
+ if (!parentHeading) return [];
56
+
57
+ const sectionNodes = this.#document
58
+ .extractSection(parentHeading as Content)
59
+ .slice(1);
60
+
61
+ const childDepth = (parentHeading as Heading).depth + 1;
62
+
63
+ // Get all child headings at the expected depth
64
+ const childHeadings = sectionNodes.filter(
65
+ (n: any) => n.type === "heading" && n.depth === childDepth
66
+ );
67
+
68
+ return childHeadings.map((heading: any) => {
69
+ // For each child heading, find its sub-section
70
+ // (from this heading to the next heading at the same depth, or end of parent section)
71
+ const headingIdx = sectionNodes.indexOf(heading);
72
+ const nextIdx = sectionNodes.findIndex(
73
+ (n: any, i: number) =>
74
+ i > headingIdx && n.type === "heading" && n.depth === childDepth
75
+ );
76
+ const section =
77
+ nextIdx === -1
78
+ ? sectionNodes.slice(headingIdx)
79
+ : sectionNodes.slice(headingIdx, nextIdx);
80
+
81
+ const title = toString(heading);
82
+ const slug = kebabCase(title.toLowerCase());
83
+ const targetDef = this.#definition.target();
84
+
85
+ const id = this.#definition.id
86
+ ? this.#definition.id(slug)
87
+ : `${targetDef.prefix}/${kebabCase(this.#document.title.toLowerCase())}/${slug}`;
88
+
89
+ return {
90
+ title,
91
+ id,
92
+ section,
93
+ ast: {
94
+ type: "root" as const,
95
+ children: section as RootContent[],
96
+ },
97
+ };
98
+ });
99
+ }
100
+
101
+ fetchAll(): InferModelInstance<TTarget>[] {
102
+ const targetDef = this.#definition.target();
103
+ const childNodes = this.extractChildNodes();
104
+
105
+ return childNodes.map(({ id, ast }) => {
106
+ // If the document already exists in the collection, use it
107
+ if (this.#collection.items.has(id)) {
108
+ const doc = this.#collection.document(id);
109
+ return this.#factory(doc, targetDef, this.#collection);
110
+ }
111
+
112
+ // Otherwise create an in-memory document from the extracted AST
113
+ const doc = this.#collection.createDocument({
114
+ id,
115
+ meta: this.#definition.meta ? this.#definition.meta({}) : {},
116
+ ast: ast as Root,
117
+ });
118
+ return this.#factory(doc, targetDef, this.#collection);
119
+ });
120
+ }
121
+
122
+ first(): InferModelInstance<TTarget> | undefined {
123
+ return this.fetchAll()[0];
124
+ }
125
+
126
+ last(): InferModelInstance<TTarget> | undefined {
127
+ const all = this.fetchAll();
128
+ return all[all.length - 1];
129
+ }
130
+
131
+ async create(): Promise<InferModelInstance<TTarget>[]> {
132
+ const models = this.fetchAll();
133
+ await Promise.all(models.map((m: any) => m.save()));
134
+ return models;
135
+ }
136
+ }
@@ -0,0 +1,57 @@
1
+ import type {
2
+ HasManyDefinition,
3
+ BelongsToDefinition,
4
+ ModelDefinition,
5
+ DocumentRef,
6
+ } from "../types";
7
+
8
+ /**
9
+ * Declare a hasMany relationship.
10
+ * Child models are extracted from sub-headings under a parent heading.
11
+ *
12
+ * The target parameter is a thunk (() => ModelDef) to allow circular references.
13
+ */
14
+ export function hasMany<
15
+ TTarget extends ModelDefinition<any, any, any, any, any>,
16
+ >(
17
+ target: () => TTarget,
18
+ options: {
19
+ heading: string;
20
+ meta?: (self: any) => Record<string, unknown>;
21
+ id?: (slug: string) => string;
22
+ }
23
+ ): HasManyDefinition<TTarget> {
24
+ return {
25
+ type: "hasMany",
26
+ target,
27
+ heading: options.heading,
28
+ meta: options.meta,
29
+ id: options.id,
30
+ };
31
+ }
32
+
33
+ /**
34
+ * Declare a belongsTo relationship.
35
+ * The foreign key function receives a DocumentRef (with id and meta)
36
+ * and returns the id fragment of the parent.
37
+ */
38
+ export function belongsTo<
39
+ TTarget extends ModelDefinition<any, any, any, any, any>,
40
+ >(
41
+ target: () => TTarget,
42
+ options: {
43
+ foreignKey: (doc: DocumentRef) => string;
44
+ }
45
+ ): BelongsToDefinition<TTarget> {
46
+ return {
47
+ type: "belongsTo",
48
+ target,
49
+ foreignKey: options.foreignKey,
50
+ };
51
+ }
52
+
53
+ export type {
54
+ HasManyDefinition,
55
+ BelongsToDefinition,
56
+ RelationshipDefinition,
57
+ } from "../types";
@@ -0,0 +1,7 @@
1
+ export type {
2
+ HasManyDefinition,
3
+ BelongsToDefinition,
4
+ RelationshipDefinition,
5
+ HasManyAccessor,
6
+ BelongsToAccessor,
7
+ } from "../types";
package/src/section.ts ADDED
@@ -0,0 +1,29 @@
1
+ import type { z } from "zod";
2
+ import type { AstQuery } from "./ast-query";
3
+ import type { SectionDefinition } from "./types";
4
+
5
+ /**
6
+ * Helper function to create a SectionDefinition with proper type inference.
7
+ * The generic T is inferred from the extract function's return type.
8
+ *
9
+ * Without this helper, TypeScript would widen the return type of extract to `unknown`.
10
+ *
11
+ * Usage:
12
+ * section("Acceptance Criteria", {
13
+ * extract: (query) => query.selectAll("listItem").map(toString),
14
+ * schema: z.array(z.string()).min(1),
15
+ * })
16
+ */
17
+ export function section<T>(
18
+ heading: string,
19
+ options: {
20
+ extract: (query: AstQuery) => T;
21
+ schema?: z.ZodType<T>;
22
+ }
23
+ ): SectionDefinition<T> {
24
+ return {
25
+ heading,
26
+ extract: options.extract,
27
+ schema: options.schema,
28
+ };
29
+ }
package/src/types.ts ADDED
@@ -0,0 +1,221 @@
1
+ import type { z } from "zod";
2
+ import type { Root } from "mdast";
3
+ import type { AstQuery } from "./ast-query";
4
+
5
+ // ─── Fundamental types ───
6
+
7
+ /** The raw item stored in Collection.items before a Document is created */
8
+ export interface CollectionItem {
9
+ raw: string;
10
+ content: string;
11
+ meta: Record<string, unknown>;
12
+ path: string;
13
+ createdAt: Date;
14
+ updatedAt: Date;
15
+ }
16
+
17
+ /** Options when constructing a Collection */
18
+ export interface CollectionOptions {
19
+ rootPath: string;
20
+ extensions?: string[];
21
+ name?: string;
22
+ }
23
+
24
+ // ─── Section system ───
25
+
26
+ /**
27
+ * A section definition declares how to extract structured data from
28
+ * a heading-based section of a document.
29
+ */
30
+ export interface SectionDefinition<T = unknown> {
31
+ /** The heading text to find in the document */
32
+ heading: string;
33
+ /** Extract structured data from the section's AST query */
34
+ extract: (query: AstQuery) => T;
35
+ /** Optional Zod schema to validate the extracted value */
36
+ schema?: z.ZodType<T>;
37
+ }
38
+
39
+ // ─── Relationship system ───
40
+
41
+ export interface HasManyDefinition<
42
+ TTarget extends ModelDefinition<any, any, any, any, any> = any,
43
+ > {
44
+ type: "hasMany";
45
+ target: () => TTarget;
46
+ heading: string;
47
+ meta?: (self: any) => Record<string, unknown>;
48
+ id?: (slug: string) => string;
49
+ }
50
+
51
+ export interface BelongsToDefinition<
52
+ TTarget extends ModelDefinition<any, any, any, any, any> = any,
53
+ > {
54
+ type: "belongsTo";
55
+ target: () => TTarget;
56
+ foreignKey: (doc: DocumentRef) => string;
57
+ }
58
+
59
+ export type RelationshipDefinition<
60
+ TTarget extends ModelDefinition<any, any, any, any, any> = any,
61
+ > = HasManyDefinition<TTarget> | BelongsToDefinition<TTarget>;
62
+
63
+ /** A minimal document reference for relationship foreign key functions */
64
+ export interface DocumentRef {
65
+ id: string;
66
+ meta: Record<string, unknown>;
67
+ }
68
+
69
+ // ─── Model Definition (the config object) ───
70
+
71
+ /**
72
+ * ModelDefinition is the static config object produced by defineModel().
73
+ * It carries all type parameters needed to fully type a model instance.
74
+ *
75
+ * TName - string literal type for the model name
76
+ * TMeta - the Zod schema type for frontmatter metadata
77
+ * TSections - record of section definitions
78
+ * TRelationships - record of relationship definitions
79
+ * TComputed - record of computed property functions
80
+ */
81
+ export interface ModelDefinition<
82
+ TName extends string = string,
83
+ TMeta extends z.ZodTypeAny = z.ZodTypeAny,
84
+ TSections extends Record<string, SectionDefinition<any>> = Record<
85
+ string,
86
+ never
87
+ >,
88
+ TRelationships extends Record<string, RelationshipDefinition<any>> = Record<
89
+ string,
90
+ never
91
+ >,
92
+ TComputed extends Record<string, (self: any) => any> = Record<
93
+ string,
94
+ never
95
+ >,
96
+ > {
97
+ readonly name: TName;
98
+ prefix: string;
99
+ meta: TMeta;
100
+ sections: TSections;
101
+ relationships: TRelationships;
102
+ computed: TComputed;
103
+ match?: (doc: DocumentRef) => boolean;
104
+ defaults?: Partial<z.input<TMeta>>;
105
+
106
+ /** The inferred Zod schema for convenience (same as meta) */
107
+ schema: TMeta;
108
+ }
109
+
110
+ // ─── Model Instance (the runtime object) ───
111
+
112
+ /**
113
+ * InferModelInstance takes a ModelDefinition and produces the shape
114
+ * of the runtime model instance.
115
+ */
116
+ export type InferModelInstance<
117
+ TDef extends ModelDefinition<any, any, any, any, any>,
118
+ > =
119
+ TDef extends ModelDefinition<
120
+ infer _TName,
121
+ infer TMeta,
122
+ infer TSections,
123
+ infer TRelationships,
124
+ infer TComputed
125
+ >
126
+ ? {
127
+ // Core properties
128
+ readonly id: string;
129
+ readonly title: string;
130
+ readonly slug: string;
131
+ readonly document: import("./document.js").Document;
132
+ readonly collection: import("./collection.js").Collection;
133
+
134
+ // Typed meta from Zod schema
135
+ readonly meta: z.infer<TMeta>;
136
+
137
+ // Sections: each key maps to the return type of its extract function
138
+ readonly sections: {
139
+ readonly [K in keyof TSections]: TSections[K] extends SectionDefinition<
140
+ infer U
141
+ >
142
+ ? U
143
+ : never;
144
+ };
145
+
146
+ // Relationships: each key becomes an accessor object
147
+ readonly relationships: {
148
+ [K in keyof TRelationships]: TRelationships[K] extends HasManyDefinition<
149
+ infer TTarget
150
+ >
151
+ ? HasManyAccessor<TTarget>
152
+ : TRelationships[K] extends BelongsToDefinition<infer TTarget>
153
+ ? BelongsToAccessor<TTarget>
154
+ : never;
155
+ };
156
+
157
+ // Computed: each key maps to the return type of the computed function
158
+ readonly computed: {
159
+ readonly [K in keyof TComputed]: TComputed[K] extends (
160
+ self: any,
161
+ ) => infer R
162
+ ? R
163
+ : never;
164
+ };
165
+
166
+ // Validation
167
+ validate(): Promise<ValidationResult>;
168
+ readonly errors: Map<string, import("zod").ZodIssue>;
169
+ readonly hasErrors: boolean;
170
+
171
+ // Serialization
172
+ toJSON(options?: SerializeOptions): Record<string, unknown>;
173
+
174
+ // Actions
175
+ runAction(
176
+ name: string,
177
+ options?: Record<string, unknown>,
178
+ ): Promise<unknown>;
179
+
180
+ // Persistence
181
+ save(options?: SaveOptions): Promise<void>;
182
+ }
183
+ : never;
184
+
185
+ export interface HasManyAccessor<
186
+ TTarget extends ModelDefinition<any, any, any, any, any>,
187
+ > {
188
+ fetchAll(): InferModelInstance<TTarget>[];
189
+ first(): InferModelInstance<TTarget> | undefined;
190
+ last(): InferModelInstance<TTarget> | undefined;
191
+ create(): Promise<InferModelInstance<TTarget>[]>;
192
+ }
193
+
194
+ export interface BelongsToAccessor<
195
+ TTarget extends ModelDefinition<any, any, any, any, any>,
196
+ > {
197
+ fetch(): InferModelInstance<TTarget>;
198
+ }
199
+
200
+ export interface ValidationResult {
201
+ valid: boolean;
202
+ errors: import("zod").ZodIssue[];
203
+ }
204
+
205
+ export interface SerializeOptions {
206
+ related?: string[];
207
+ sections?: string[];
208
+ computed?: string[];
209
+ }
210
+
211
+ export interface SaveOptions {
212
+ normalize?: boolean;
213
+ extension?: string;
214
+ }
215
+
216
+ /** Factory function type used to break circular dependency between model-instance and relationships */
217
+ export type ModelInstanceFactory = (
218
+ doc: import("./document.js").Document,
219
+ definition: ModelDefinition<any, any, any, any, any>,
220
+ collection: import("./collection.js").Collection,
221
+ ) => any;
@@ -0,0 +1,11 @@
1
+ export {
2
+ pluralize,
3
+ singularize,
4
+ kebabCase,
5
+ camelCase,
6
+ upperFirst,
7
+ } from "./inflect";
8
+ export { stringifyAst } from "./stringify-ast";
9
+ export { parseTable } from "./parse-table";
10
+ export { normalizeHeadings } from "./normalize-headings";
11
+ export { readDirectory } from "./read-directory";
@@ -0,0 +1,82 @@
1
+ const IRREGULAR: Record<string, string> = {
2
+ person: "people",
3
+ child: "children",
4
+ man: "men",
5
+ woman: "women",
6
+ mouse: "mice",
7
+ goose: "geese",
8
+ ox: "oxen",
9
+ datum: "data",
10
+ index: "indices",
11
+ matrix: "matrices",
12
+ vertex: "vertices",
13
+ };
14
+
15
+ const UNCOUNTABLE = new Set([
16
+ "sheep",
17
+ "fish",
18
+ "deer",
19
+ "series",
20
+ "species",
21
+ "money",
22
+ "rice",
23
+ "information",
24
+ "equipment",
25
+ "media",
26
+ "data",
27
+ ]);
28
+
29
+ export function pluralize(word: string): string {
30
+ const lower = word.toLowerCase();
31
+
32
+ if (UNCOUNTABLE.has(lower)) return word;
33
+ if (IRREGULAR[lower]) {
34
+ return word[0] + IRREGULAR[lower].slice(1);
35
+ }
36
+
37
+ if (/s$/i.test(word)) return word;
38
+ if (/([^aeiou])y$/i.test(word)) return word.replace(/y$/i, "ies");
39
+ if (/(x|ch|ss|sh)$/i.test(word)) return word + "es";
40
+
41
+ return word + "s";
42
+ }
43
+
44
+ export function singularize(word: string): string {
45
+ const lower = word.toLowerCase();
46
+
47
+ if (UNCOUNTABLE.has(lower)) return word;
48
+
49
+ const irregularEntry = Object.entries(IRREGULAR).find(
50
+ ([, v]) => v === lower
51
+ );
52
+ if (irregularEntry) {
53
+ return word[0] + irregularEntry[0].slice(1);
54
+ }
55
+
56
+ if (word.endsWith("ies")) return word.slice(0, -3) + "y";
57
+ if (/ses$/i.test(word) || /xes$/i.test(word) || /ches$/i.test(word) || /shes$/i.test(word)) {
58
+ return word.slice(0, -2);
59
+ }
60
+ if (word.endsWith("s") && !word.endsWith("ss")) {
61
+ return word.slice(0, -1);
62
+ }
63
+ return word;
64
+ }
65
+
66
+ export function kebabCase(str: string): string {
67
+ return str
68
+ .replace(/([a-z])([A-Z])/g, "$1-$2")
69
+ .replace(/[\s_]+/g, "-")
70
+ .replace(/[^\w-]/g, "")
71
+ .toLowerCase();
72
+ }
73
+
74
+ export function camelCase(str: string): string {
75
+ return str
76
+ .replace(/[-_\s]+(.)?/g, (_, c) => (c ? c.toUpperCase() : ""))
77
+ .replace(/^[A-Z]/, (c) => c.toLowerCase());
78
+ }
79
+
80
+ export function upperFirst(str: string): string {
81
+ return str.charAt(0).toUpperCase() + str.slice(1);
82
+ }