{post.data.title}
{post.data.description}
Type-safe content collections for Igniter.js
npm install @igniter-js/collections



Type-safe ORM for content collections
Prisma-like API for Markdown, JSON, and YAML files with schema validation, lifecycle hooks, and declarative views.
---
Managing content-driven applications shouldn't require a database. Whether you're building a documentation site, a blog, or a configuration management system, you need:
- ✅ Type-safe content — Catch errors at build time, not runtime
- ✅ Familiar API — Prisma-like queries you already know
- ✅ Runtime flexibility — Deploy anywhere (Bun, Node.js, Redis, S3)
- ✅ Developer experience — Autocomplete everywhere, zero boilerplate
- ✅ Production-ready — Lifecycle hooks, validation, and observability built-in
---
``bashUsing npm
npm install @igniter-js/collections zod
$3
`typescript
import { IgniterCollections, IgniterCollectionModel } from '@igniter-js/collections';
import { NodeFsAdapter } from '@igniter-js/collections/adapters';
import { z } from 'zod';// 1️⃣ Define your content schema
const Posts = IgniterCollectionModel.create('posts')
.withBasePath('content/posts')
.withSchema(z.object({
title: z.string(),
description: z.string(),
published: z.boolean().default(false),
tags: z.array(z.string()).optional(),
author: z.string(),
}))
.build();
// 2️⃣ Create the manager
const docs = IgniterCollections.create()
.withAdapter(new NodeFsAdapter())
.withBasePath(process.cwd())
.addCollection(Posts)
.build();
// 3️⃣ Use it like Prisma!
const post = await docs.posts.create({
data: {
title: 'Getting Started with Igniter.js',
description: 'Learn how to build type-safe content-driven apps',
published: true,
tags: ['tutorial', 'typescript'],
author: 'Felipe Barcelos',
}
});
console.log('Created post:', post.id);
// Created post: 9f3e4d2a-8b7c-4e1f-a3d2-9c8b7e6f5a4d
`✅ Success! You just created a type-safe Markdown file with validated frontmatter.
---
🎯 Core Concepts
$3
`
┌─────────────────────────────────────────────────────────┐
│ Your Application │
├─────────────────────────────────────────────────────────┤
│ docs.posts.findMany({ where: { published: true } }) │
└────────────┬────────────────────────────────────────────┘
│ Type-safe API
▼
┌─────────────────────────────────────────────────────────┐
│ IgniterCollectionManager (Proxy) │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Posts │ │ Docs │ │ Authors │ │
│ │ Manager │ │ Manager │ │ Manager │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
└────────────┬────────────────────────────────────────────┘
│ Hooks + Validation
▼
┌─────────────────────────────────────────────────────────┐
│ Adapter Layer │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌─────────┐│
│ │ BunFs │ │ Redis │ │ S3 │ │ Mock ││
│ │ Adapter │ │ Adapter │ │ Adapter │ │ Adapter ││
│ └──────────┘ └──────────┘ └──────────┘ └─────────┘│
└────────────┬────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ Storage Backend │
│ Files • Redis • S3 • In-Memory │
└─────────────────────────────────────────────────────────┘
`$3
- Builder → Immutable configuration (
.withSchema(), .withAdapter())
- Manager → Operational CRUD instance (.findMany(), .create())
- Adapter → Pluggable storage backend (filesystem, Redis, S3)
- Schema → Runtime validation (Zod, JSON Schema, StandardSchemaV1)
- Hooks → Lifecycle interception (.onCreated(), .onDeleted())
- Views → Declarative data shaping (stats, transforms, actions)---
📖 Usage Examples
$3
`typescript
import { IgniterCollections, IgniterCollectionModel } from '@igniter-js/collections';
import { NodeFsAdapter } from '@igniter-js/collections/adapters';
import { z } from 'zod';const Posts = IgniterCollectionModel.create('posts')
.withBasePath('content/posts')
.withFilePattern('{id}.mdx') // Default pattern
.withSchema(z.object({
title: z.string(),
published: z.boolean(),
}))
.build();
const docs = IgniterCollections.create()
.withAdapter(new NodeFsAdapter())
.addCollection(Posts)
.build();
// Create a post
const post = await docs.posts.create({
data: { title: 'Hello World', published: true },
});
// Find all published posts
const publishedPosts = await docs.posts.findMany({
where: { published: true },
});
// Update a post
const updated = await docs.posts.update({
where: { id: post.id },
data: { published: false },
});
// Delete a post
await docs.posts.delete({
where: { id: post.id },
});
`$3
`typescript
// Complex filtering
const results = await docs.posts.findMany({
where: {
published: true,
category: { in: ['tutorial', 'guide'] },
title: { contains: 'TypeScript' },
views: { gte: 100, lt: 1000 },
tags: { has: 'featured' },
},
orderBy: { createdAt: 'desc' },
take: 10,
skip: 0,
});// Count matching documents
const count = await docs.posts.count({
where: { published: true },
});
// Find unique document
const post = await docs.posts.findUnique({
where: { id: 'my-post-id' },
});
`$3
`typescript
const Posts = IgniterCollectionModel.create('posts')
.withBasePath('content/posts')
.withSchema(postSchema)
// Add created timestamp automatically
.onCreated(async ({ value }) => {
return {
...value,
data: {
...value.data,
createdAt: new Date().toISOString(),
},
};
})
// Prevent unpublishing approved content
.onUpdated(({ newValue, previousValue }) => {
if (
previousValue.data.status === 'approved' &&
newValue.data.status === 'draft'
) {
return false; // ❌ Cancel operation
}
return newValue; // ✅ Allow update
})
// Audit deletions
.onDeleted(async ({ value, manager }) => {
await manager.collection('audit').create({
data: {
action: 'deleted',
collection: 'posts',
documentId: value.id,
timestamp: new Date(),
},
});
return true; // ✅ Proceed with deletion
})
.build();
`$3
`typescript
import { z } from 'zod';const blogPostSchema = z.object({
// Required fields
title: z.string().min(1).max(200),
description: z.string().min(10).max(500),
author: z.string(),
// Optional with defaults
published: z.boolean().default(false),
featured: z.boolean().default(false),
// Complex types
tags: z.array(z.string()).min(1).max(10),
category: z.enum(['tutorial', 'guide', 'news', 'update']),
// Dates
publishedAt: z.string().datetime().optional(),
updatedAt: z.string().datetime().optional(),
// Nested objects
seo: z.object({
title: z.string().optional(),
description: z.string().optional(),
image: z.string().url().optional(),
}).optional(),
});
const Posts = IgniterCollectionModel.create('posts')
.withSchema(blogPostSchema)
.build();
// TypeScript knows the exact shape of your data!
const post = await docs.posts.create({
data: {
title: 'My Post',
description: 'A great post about TypeScript',
author: 'John Doe',
tags: ['typescript', 'tutorial'],
category: 'tutorial',
},
});
// ✅ Type-safe access
console.log(post.data.title); // string
console.log(post.data.published); // boolean (default: false)
console.log(post.data.tags); // string[]
`$3
Views allow you to shape data for specific UI needs without custom query logic.
`typescript
const Posts = IgniterCollectionModel.create('posts')
.withSchema(postSchema)
.withViews([
{
name: 'dashboard',
title: 'Blog Dashboard',
// Default query for this view
defaultQuery: {
orderBy: { createdAt: 'desc' },
take: 50,
},
// Stats (aggregations)
stats: {
totalPosts: { type: 'count' },
publishedCount: { type: 'count', where: { published: true } },
averageViews: { type: 'avg', field: 'views' },
},
// Transformations
transforms: [
{ type: 'group', field: 'category' },
],
// UI component tree (JSON-compatible)
tree: [
{
component: 'Metric',
props: { title: 'Total Posts' },
valuePath: '/stats/totalPosts',
},
{
component: 'Table',
props: { columns: ['title', 'category', 'views'] },
valuePath: '/items',
},
],
},
])
.build();// Render the view
const dashboard = await docs.posts.views.render('dashboard');
console.log(dashboard.stats.totalPosts); // 42
console.log(dashboard.stats.publishedCount); // 28
console.log(dashboard.items); // Grouped by category
`$3
The Schema Registry allows you to define collections entirely in JSON files—perfect for plugin systems, CMS platforms, or any scenario where collections need to be added/removed without code changes.
#### Complete Schema File Structure
Here's a complete schema definition showing all available capabilities:
`json
// .fractal/schemas/posts.schema.json
{
"name": "posts",
"basePath": "content/posts",
"filePattern": "{id}.mdx",
"schema": {
"type": "object",
"properties": {
"title": { "type": "string", "minLength": 1, "maxLength": 200 },
"description": { "type": "string", "minLength": 10 },
"author": { "type": "string" },
"published": { "type": "boolean", "default": false },
"featured": { "type": "boolean", "default": false },
"tags": {
"type": "array",
"items": { "type": "string" },
"minItems": 1
},
"category": {
"type": "string",
"enum": ["tutorial", "guide", "news", "update"]
},
"views": { "type": "number", "default": 0 },
"publishedAt": { "type": "string", "format": "date-time" },
"seo": {
"type": "object",
"properties": {
"title": { "type": "string" },
"description": { "type": "string" },
"image": { "type": "string", "format": "uri" }
}
}
},
"required": ["title", "description", "author"]
},
"hooks": {
"onCreated": ".fractal/schemas/hooks/posts.onCreate.ts",
"onUpdated": ".fractal/schemas/hooks/posts.onUpdate.ts",
"onDeleted": ".fractal/schemas/hooks/posts.onDelete.ts",
"onList": ".fractal/schemas/hooks/posts.onList.ts"
},
"views": [
{
"name": "dashboard",
"title": "Blog Dashboard",
"description": "Overview of blog posts with key metrics",
"defaultQuery": {
"orderBy": { "createdAt": "desc" },
"take": 50
},
"stats": {
"totalPosts": {
"type": "count"
},
"publishedCount": {
"type": "count",
"where": { "published": true }
},
"draftCount": {
"type": "count",
"where": { "published": false }
},
"featuredCount": {
"type": "count",
"where": { "featured": true }
},
"totalViews": {
"type": "sum",
"field": "views"
},
"averageViews": {
"type": "avg",
"field": "views"
}
},
"transforms": [
{
"type": "group",
"field": "category"
}
],
"tree": [
{
"component": "Grid",
"props": { "columns": 4, "gap": 4 },
"children": [
{
"component": "Metric",
"props": {
"title": "Total Posts",
"icon": "FileText"
},
"valuePath": "/stats/totalPosts"
},
{
"component": "Metric",
"props": {
"title": "Published",
"icon": "CheckCircle",
"color": "green"
},
"valuePath": "/stats/publishedCount"
},
{
"component": "Metric",
"props": {
"title": "Drafts",
"icon": "Edit",
"color": "orange"
},
"valuePath": "/stats/draftCount"
},
{
"component": "Metric",
"props": {
"title": "Total Views",
"icon": "Eye"
},
"valuePath": "/stats/totalViews"
}
]
},
{
"component": "Table",
"props": {
"columns": [
{ "key": "title", "label": "Title" },
{ "key": "category", "label": "Category" },
{ "key": "author", "label": "Author" },
{ "key": "views", "label": "Views" },
{ "key": "published", "label": "Published" }
]
},
"valuePath": "/items"
}
],
"actions": {
"publish": {
"description": "Publish a draft post",
"params": {
"type": "object",
"properties": {
"postId": { "type": "string" }
},
"required": ["postId"]
},
"handler": ".fractal/schemas/actions/posts.publish.ts",
"confirm": {
"title": "Publish Post",
"message": "Are you sure you want to publish this post?",
"variant": "info"
}
},
"unpublish": {
"description": "Unpublish a published post",
"params": {
"type": "object",
"properties": {
"postId": { "type": "string" }
},
"required": ["postId"]
},
"handler": ".fractal/schemas/actions/posts.unpublish.ts",
"confirm": {
"title": "Unpublish Post",
"message": "This will hide the post from public view.",
"variant": "warning"
}
},
"toggleFeatured": {
"description": "Toggle featured status",
"params": {
"type": "object",
"properties": {
"postId": { "type": "string" }
},
"required": ["postId"]
},
"handler": ".fractal/schemas/actions/posts.toggleFeatured.ts"
},
"bulkDelete": {
"description": "Delete multiple posts",
"params": {
"type": "object",
"properties": {
"postIds": {
"type": "array",
"items": { "type": "string" }
}
},
"required": ["postIds"]
},
"handler": ".fractal/schemas/actions/posts.bulkDelete.ts",
"confirm": {
"title": "Delete Posts",
"message": "This action cannot be undone. Are you sure?",
"variant": "danger"
}
}
}
},
{
"name": "analytics",
"title": "Content Analytics",
"description": "Performance metrics for published content",
"defaultQuery": {
"where": { "published": true },
"orderBy": { "views": "desc" },
"take": 20
},
"stats": {
"topPerformer": {
"type": "custom",
"fn": ".fractal/schemas/stats/posts.topPerformer.ts"
},
"totalEngagement": {
"type": "sum",
"field": "views",
"where": { "published": true }
}
},
"transforms": [
{
"type": "group",
"field": "author"
}
],
"tree": [
{
"component": "Chart",
"props": {
"type": "bar",
"title": "Top 10 Posts by Views"
},
"valuePath": "/items"
}
]
}
]
}
`#### Hook Files (External TypeScript)
Hooks can be defined in external files for better maintainability:
.fractal/schemas/hooks/posts.onCreate.ts`typescript
import type { IgniterCollectionOnCreatedHook } from '@igniter-js/collections';export const onCreated: IgniterCollectionOnCreatedHook = async ({ value, manager }) => {
// Add automatic timestamps
return {
...value,
data: {
...value.data,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
slug: value.data.title
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/(^-|-$)/g, ''),
},
};
};
`
.fractal/schemas/hooks/posts.onUpdate.ts`typescript
import type { IgniterCollectionOnUpdatedHook } from '@igniter-js/collections';export const onUpdated: IgniterCollectionOnUpdatedHook = async ({
newValue,
previousValue
}) => {
// Prevent unpublishing featured posts
if (previousValue.data.featured && !newValue.data.published) {
throw new Error('Cannot unpublish a featured post. Remove featured status first.');
}
// Update timestamp
return {
...newValue,
data: {
...newValue.data,
updatedAt: new Date().toISOString(),
},
};
};
`
.fractal/schemas/hooks/posts.onDelete.ts`typescript
import type { IgniterCollectionOnDeletedHook } from '@igniter-js/collections';export const onDeleted: IgniterCollectionOnDeletedHook = async ({
value,
manager
}) => {
// Create audit log
await manager.collection('audit').create({
data: {
action: 'deleted',
collection: 'posts',
documentId: value.id,
documentTitle: value.data.title,
deletedBy: 'system',
timestamp: new Date().toISOString(),
},
});
return true; // Proceed with deletion
};
`
.fractal/schemas/hooks/posts.onList.ts`typescript
import type { IgniterCollectionOnListHook } from '@igniter-js/collections';export const onList: IgniterCollectionOnListHook = async ({ values }) => {
// Filter out future scheduled posts
const now = new Date();
return values.filter((post) => {
if (!post.data.published) return false;
if (!post.data.publishedAt) return true;
return new Date(post.data.publishedAt) <= now;
});
};
`#### View Action Handlers
Actions can also be defined in external files:
.fractal/schemas/actions/posts.publish.ts`typescript
import type { IgniterCollectionViewActionHandler } from '@igniter-js/collections';export const handler: IgniterCollectionViewActionHandler = async ({
manager,
params
}) => {
try {
const post = await manager.collection('posts').findUnique({
where: { id: params.postId },
});
if (!post) {
return {
success: false,
error: 'Post not found',
};
}
if (post.data.published) {
return {
success: false,
error: 'Post is already published',
};
}
const updated = await manager.collection('posts').update({
where: { id: params.postId },
data: {
published: true,
publishedAt: new Date().toISOString(),
},
});
return {
success: true,
data: updated,
updates: {
'/stats/publishedCount': '+1',
'/stats/draftCount': '-1',
},
};
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
};
}
};
`
.fractal/schemas/actions/posts.toggleFeatured.ts`typescript
import type { IgniterCollectionViewActionHandler } from '@igniter-js/collections';export const handler: IgniterCollectionViewActionHandler = async ({
manager,
params
}) => {
const post = await manager.collection('posts').findUnique({
where: { id: params.postId },
});
if (!post) {
return { success: false, error: 'Post not found' };
}
const updated = await manager.collection('posts').update({
where: { id: params.postId },
data: {
featured: !post.data.featured,
},
});
return {
success: true,
data: updated,
updates: {
'/stats/featuredCount': updated.data.featured ? '+1' : '-1',
},
};
};
`
.fractal/schemas/actions/posts.bulkDelete.ts`typescript
import type { IgniterCollectionViewActionHandler } from '@igniter-js/collections';export const handler: IgniterCollectionViewActionHandler = async ({
manager,
params
}) => {
const deletedIds: string[] = [];
const errors: Array<{ id: string; error: string }> = [];
for (const postId of params.postIds) {
try {
await manager.collection('posts').delete({
where: { id: postId },
});
deletedIds.push(postId);
} catch (error) {
errors.push({
id: postId,
error: error instanceof Error ? error.message : 'Unknown error',
});
}
}
return {
success: errors.length === 0,
data: {
deleted: deletedIds.length,
failed: errors.length,
errors,
},
updates: {
'/stats/totalPosts':
-${deletedIds.length},
},
};
};
`
.fractal/schemas/stats/posts.topPerformer.ts`typescript
import type { IgniterCollectionDocument } from '@igniter-js/collections';export function topPerformer(items: IgniterCollectionDocument[]): any {
if (items.length === 0) return null;
return items.reduce((top, current) => {
return (current.data.views || 0) > (top.data.views || 0) ? current : top;
});
}
`#### Initializing with Schema Registry
Option 1: Auto-Discovery (Recommended)
`typescript
import { IgniterCollections } from '@igniter-js/collections';
import { NodeFsAdapter } from '@igniter-js/collections/adapters';const docs = IgniterCollections.create()
.withAdapter(new NodeFsAdapter())
.withBasePath(process.cwd())
.withSchemaRegistry('.fractal/schemas/*.schema.json', {
autoWatch: true, // Hot-reload on schema changes (dev mode)
})
.build();
// Collections are available automatically!
const posts = await docs.posts.findMany();
const authors = await docs.authors.findMany();
const comments = await docs.comments.findMany();
// All collections discovered from .fractal/schemas/
`Option 2: Multiple Directories (Plugin System)
`typescript
const docs = IgniterCollections.create()
.withAdapter(new NodeFsAdapter())
.withSchemaRegistry([
'.fractal/schemas/*/.schema.json', // Core schemas
'plugins//schemas/.schema.json', // Plugin schemas
'node_modules/@my-org//schemas/.json', // NPM package schemas
], {
autoWatch: process.env.NODE_ENV === 'development',
})
.build();// Auto-prefixing prevents conflicts:
// plugins/blog/schemas/posts.schema.json → blog:posts
// plugins/docs/schemas/posts.schema.json → docs:posts
// .fractal/schemas/posts.schema.json → posts
await docs['blog:posts'].findMany();
await docs['docs:posts'].findMany();
await docs.posts.findMany(); // Core posts
`#### Complete Usage Examples
Example 1: Using Registry Collections
`typescript
// The manager is already aware of all schema-defined collections
const docs = IgniterCollections.create()
.withAdapter(new NodeFsAdapter())
.withSchemaRegistry('.fractal/schemas')
.build();// CRUD operations work exactly the same
const post = await docs.posts.create({
data: {
title: 'My First Post',
description: 'This is a test post',
author: 'John Doe',
category: 'tutorial',
tags: ['typescript', 'igniter'],
},
content: '# Hello World\n\nThis is the content.',
});
// Hooks defined in .onCreate.ts run automatically
console.log(post.data.slug); // 'my-first-post' (auto-generated)
console.log(post.data.createdAt); // '2026-01-24T...' (auto-added)
// Find with filters
const tutorials = await docs.posts.findMany({
where: {
category: 'tutorial',
published: true,
},
orderBy: { views: 'desc' },
});
`Example 2: Using Views
`typescript
// Render the dashboard view
const dashboard = await docs.posts.views.render('dashboard');console.log(dashboard.stats);
// {
// totalPosts: 42,
// publishedCount: 28,
// draftCount: 14,
// featuredCount: 5,
// totalViews: 15420,
// averageViews: 367
// }
console.log(dashboard.items);
// {
// tutorial: [{ id: '...', data: {...} }, ...],
// guide: [{ id: '...', data: {...} }, ...],
// news: [{ id: '...', data: {...} }, ...],
// }
// Render analytics view
const analytics = await docs.posts.views.render('analytics');
console.log(analytics.stats.topPerformer);
// { id: 'post-123', data: { title: 'Most Popular Post', views: 5000 } }
`Example 3: Executing View Actions
`typescript
// Get the view manager
const postsView = docs.posts.views;// Execute the publish action
const result = await postsView.executeAction('publish', {
postId: 'draft-post-123',
});
if (result.success) {
console.log('Post published!', result.data);
// UI can update state using result.updates
} else {
console.error('Publish failed:', result.error);
}
// Toggle featured status
await postsView.executeAction('toggleFeatured', {
postId: 'post-123',
});
// Bulk delete
const bulkResult = await postsView.executeAction('bulkDelete', {
postIds: ['post-1', 'post-2', 'post-3'],
});
console.log(
Deleted ${bulkResult.data.deleted} posts);
if (bulkResult.data.failed > 0) {
console.error('Failed:', bulkResult.data.errors);
}
`Example 4: Hot-Reloading Schemas (Development)
`typescript
const docs = IgniterCollections.create()
.withAdapter(new NodeFsAdapter())
.withSchemaRegistry('.fractal/schemas', {
autoWatch: true,
})
.build();// Start watching for schema changes
docs.startSchemaWatching((event) => {
console.log('Schema changed:', event);
// { type: 'added' | 'updated' | 'removed', name: 'posts' }
});
// Now edit .fractal/schemas/posts.schema.json
// The manager automatically reloads the schema!
// Manually refresh if needed
await docs.refreshSchemas();
// Stop watching
docs.stopSchemaWatching();
`Example 5: Multi-Tenant Plugin System
`typescript
// CMS platform with plugin architecture
const cms = IgniterCollections.create()
.withAdapter(new BunRedisAdapter({
url: process.env.REDIS_URL,
}))
.withSchemaRegistry([
'core/schemas/*.schema.json',
'plugins//schemas/.schema.json',
])
.build();// Each plugin contributes its own collections
// plugins/ecommerce/schemas/products.schema.json → ecommerce:products
// plugins/blog/schemas/posts.schema.json → blog:posts
// plugins/analytics/schemas/events.schema.json → analytics:events
// Access plugin collections
const products = await cms['ecommerce:products'].findMany();
const blogPosts = await cms['blog:posts'].findMany();
const events = await cms['analytics:events'].findMany();
// Cross-collection operations
const product = await cms['ecommerce:products'].findUnique({
where: { id: 'prod-123' }
});
await cms['analytics:events'].create({
data: {
type: 'product_viewed',
productId: product.id,
productName: product.data.name,
timestamp: new Date().toISOString(),
},
});
`Example 6: Schema-Driven Admin UI
`typescript
// Build an admin UI dynamically from schemas
const cms = IgniterCollections.create()
.withAdapter(new NodeFsAdapter())
.withSchemaRegistry('.fractal/schemas')
.build();// Get all collection definitions
const collections = cms.definitions();
// For each collection, generate a UI panel
for (const [name, definition] of Object.entries(collections)) {
console.log(
Collection: ${name});
console.log(Path: ${definition.basePath});
console.log(Schema:, definition.schema);
// Render views
for (const view of definition.views) {
console.log( View: ${view.name});
console.log( Tree:, view.tree); // UI component tree
console.log( Actions:, Object.keys(view.actions || {}));
}
}// The UI framework (React, Vue, etc.) can use this metadata
// to generate CRUD forms, dashboards, and action buttons automatically
`$3
Organize related content hierarchically.
`typescript
const Plans = IgniterCollectionModel.create('plans')
.withBasePath('.fractal/plans')
.withSchema(planSchema)
.build();// Define a sub-collection for tasks within plans
const Tasks = Plans.collections.create('tasks')
.withBasePath('/tasks') // Relative to parent: .fractal/plans/{id}/tasks/
.withSchema(taskSchema)
.build();
// Access sub-collections
const plan = await docs.plans.findUnique({ where: { id: 'PLN-001' } });
const tasks = await plan.tasks.findMany({
where: { status: 'pending' },
});
`$3
#### Bun (High Performance)
`typescript
import { BunFsAdapter } from '@igniter-js/collections/adapters';const docs = IgniterCollections.create()
.withAdapter(new BunFsAdapter()) // Native Bun syscalls
.addCollection(Posts)
.build();
`#### Redis (Distributed)
`typescript
import { BunRedisAdapter } from '@igniter-js/collections/adapters';const docs = IgniterCollections.create()
.withAdapter(new BunRedisAdapter({
url: 'redis://localhost:6379',
keyPrefix: 'content:',
ttl: 3600, // Optional TTL in seconds
}))
.addCollection(Posts)
.build();
`#### S3 (Cloud Storage)
`typescript
import { BunS3Adapter } from '@igniter-js/collections/adapters';const docs = IgniterCollections.create()
.withAdapter(new BunS3Adapter({
bucket: 'my-content-bucket',
region: 'us-east-1',
endpoint: 'https://s3.amazonaws.com', // Or R2, MinIO, etc.
}))
.addCollection(Posts)
.build();
`#### Mock (Testing)
`typescript
import { MockAdapter } from '@igniter-js/collections/adapters';const mockAdapter = new MockAdapter();
const docs = IgniterCollections.create()
.withAdapter(mockAdapter)
.addCollection(Posts)
.build();
// In tests
await docs.posts.create({ data: { title: 'Test Post', published: true } });
expect(mockAdapter.calls.write).toHaveLength(1);
expect(mockAdapter.state.size).toBe(1);
`---
🌍 Real-World Examples
$3
`typescript
import { IgniterCollections, IgniterCollectionModel } from '@igniter-js/collections';
import { NodeFsAdapter } from '@igniter-js/collections/adapters';
import { z } from 'zod';const Docs = IgniterCollectionModel.create('docs')
.withBasePath('content/docs')
.withSchema(z.object({
title: z.string(),
description: z.string(),
category: z.enum(['guide', 'api', 'tutorial']),
order: z.number().int().min(0),
}))
.build();
const docs = IgniterCollections.create()
.withAdapter(new NodeFsAdapter())
.addCollection(Docs)
.build();
// Build navigation tree
const navigation = await docs.docs.findMany({
orderBy: { order: 'asc' },
});
// Group by category
const byCategory = navigation.reduce((acc, doc) => {
const category = doc.data.category;
if (!acc[category]) acc[category] = [];
acc[category].push(doc);
return acc;
}, {} as Record);
`$3
`typescript
const Posts = IgniterCollectionModel.create('posts')
.withSchema(z.object({
title: z.string(),
publishedAt: z.string().datetime(),
status: z.enum(['draft', 'scheduled', 'published']),
}))
// Filter out future posts
.onList(({ values }) => {
const now = new Date();
return values.filter((post) => {
if (post.data.status !== 'published') return false;
return new Date(post.data.publishedAt) <= now;
});
})
.build();
`$3
`typescript
import { BunRedisAdapter } from '@igniter-js/collections/adapters';// Tenant-specific manager factory
function createTenantDocs(tenantId: string) {
return IgniterCollections.create()
.withAdapter(new BunRedisAdapter({
url: process.env.REDIS_URL!,
keyPrefix:
tenant:${tenantId}:,
}))
.addCollection(Posts)
.build();
}const tenantA = createTenantDocs('tenant-a');
const tenantB = createTenantDocs('tenant-b');
// Isolated data per tenant
await tenantA.posts.create({ data: { title: 'A Post' } });
await tenantB.posts.create({ data: { title: 'B Post' } });
`$3
`typescript
const Configs = IgniterCollectionModel.create('configs')
.withBasePath('.config')
.withFilePattern('{id}.json')
.withSchema(z.object({
environment: z.enum(['dev', 'staging', 'production']),
apiUrl: z.string().url(),
features: z.record(z.boolean()),
}))
// Validate on update
.onUpdated(({ newValue }) => {
if (newValue.data.environment === 'production') {
if (!newValue.data.apiUrl.includes('api.prod.com')) {
throw new Error('Production config must use prod API');
}
}
return newValue;
})
.build();
`$3
`typescript
import { BunS3Adapter, NodeFsAdapter } from '@igniter-js/collections/adapters';// Source: Local filesystem
const localDocs = IgniterCollections.create()
.withAdapter(new NodeFsAdapter())
.addCollection(Posts)
.build();
// Destination: S3 bucket
const cloudDocs = IgniterCollections.create()
.withAdapter(new BunS3Adapter({ bucket: 'my-content' }))
.addCollection(Posts)
.build();
// Migrate all posts
const posts = await localDocs.posts.findMany();
for (const post of posts) {
await cloudDocs.posts.create({
id: post.id,
data: post.data,
content: post.content,
});
console.log(
Migrated: ${post.id});
}
`---
📚 API Reference
$3
The main entry point for creating a collection manager.
`typescript
class IgniterCollectionsBuilder {
static create(): IgniterCollectionsBuilder<{}>
withBasePath(path: string | string[]): this
withAdapter(adapter: IgniterCollectionAdapter): this
withSchemaRegistry(path: string | string[], options?: RegistryOptions): this
withTelemetry(telemetry: IgniterTelemetryManager): this
withLogger(logger: IgniterLogger): this
withGlobalHooks(hooks: IgniterCollectionModelHooks): this
addCollection(collection: Definition): IgniterCollectionsBuilder
build(): IIgniterCollectionsManager
}
`Methods:
| Method | Parameters | Returns | Description |
|--------|------------|---------|-------------|
|
create() | None | Builder | Static factory for new builder |
| withBasePath() | path: string \| string[] | this | Set root path(s) for collections |
| withAdapter() | adapter: IgniterCollectionAdapter | this | Required. Set storage adapter |
| withSchemaRegistry() | path: string \| string[], options? | this | Enable dynamic schema loading from JSON files |
| withTelemetry() | telemetry: IgniterTelemetryManager | this | Connect to telemetry system |
| withLogger() | logger: IgniterLogger | this | Set custom logger |
| withGlobalHooks() | hooks: IgniterCollectionModelHooks | this | Apply hooks to all collections |
| addCollection() | collection: Definition | Builder | Register a collection (type-safe) |
| build() | None | Manager | Build the operational manager |Example:
`typescript
const docs = IgniterCollections.create()
.withAdapter(new NodeFsAdapter())
.withBasePath(process.cwd())
.addCollection(Posts)
.addCollection(Docs)
.build();
`---
$3
Defines the schema, hooks, and configuration for a single collection.
`typescript
class IgniterCollectionModelBuilder {
static create(name: TName): Builder
withBasePath(path: string): this
withFilePattern(pattern: string, idGen?: () => string): this
withSchema(schema: S): Builder, TViews, TName>
withViews(views: ViewDefinition[]): this
onCreated(hook: OnCreatedHook): this
onUpdated(hook: OnUpdatedHook): this
onDeleted(hook: OnDeletedHook): this
onRead(hook: OnReadHook): this
onList(hook: OnListHook): this
build(): IgniterCollectionModelDefinition
}
`Methods:
| Method | Parameters | Returns | Description |
|--------|------------|---------|-------------|
|
create() | name: string | Builder | Start building a collection |
| withBasePath() | path: string | this | Set collection directory path |
| withFilePattern() | pattern: string, idGen?: Function | this | Set file naming pattern (default: {id}.mdx) |
| withSchema() | schema: StandardSchemaV1 | Builder | Set validation schema (Zod, JSON Schema) |
| withViews() | views: ViewDefinition[] | this | Register declarative views |
| onCreated() | hook: Function | this | Hook for document creation |
| onUpdated() | hook: Function | this | Hook for document updates |
| onDeleted() | hook: Function | this | Hook for document deletion |
| onRead() | hook: Function | this | Hook for single document reads |
| onList() | hook: Function | this | Hook for bulk document listings |
| build() | None | Definition | Build immutable definition |Example:
`typescript
const Posts = IgniterCollectionModel.create('posts')
.withBasePath('content/posts')
.withFilePattern('{id}.mdx')
.withSchema(postSchema)
.onCreated(({ value }) => {
console.log('Created:', value.id);
return value;
})
.build();
`---
$3
The operational instance for a specific collection.
`typescript
interface IIgniterCollectionModel {
// Read operations
findUnique(args: FindUniqueArgs): Promise | null>
findMany(args?: FindManyArgs): Promise[]>
count(args?: CountArgs): Promise
// Write operations
create(args: CreateArgs): Promise>
update(args: UpdateArgs): Promise>
delete(args: DeleteArgs): Promise>
// Views
views: IIgniterCollectionViewManager
// Metadata
definition: IgniterCollectionModelDefinition
}
`Methods:
| Method | Arguments | Returns | Description |
|--------|-----------|---------|-------------|
|
findMany() | { where?, orderBy?, take?, skip? } | Promise | Find multiple documents with filtering and pagination |
| findUnique() | { where: { id } } | Promise | Find single document by ID |
| create() | { id?, data, content? } | Promise | Create new document with validation |
| update() | { where, data } | Promise | Update existing document (partial) |
| delete() | { where: { id } } | Promise | Delete document and return last state |
| count() | { where? } | Promise | Count matching documents |Type: Document
`typescript
interface IgniterCollectionDocument {
id: string;
path: string;
data: TSchema; // Validated frontmatter
content: string; // Markdown/text content
raw: string; // Full file content
}
`---
$3
#### Where Clause
`typescript
interface IgniterCollectionWhereClause {
// Direct equality
fieldName?: value;
// Operators
fieldName?: {
equals?: value;
not?: value;
in?: value[];
notIn?: value[];
lt?: number | Date;
lte?: number | Date;
gt?: number | Date;
gte?: number | Date;
contains?: string;
startsWith?: string;
endsWith?: string;
};
// Array operators
arrayField?: {
has?: item;
hasEvery?: item[];
hasSome?: item[];
isEmpty?: boolean;
length?: number;
};
}
`Example:
`typescript
const results = await docs.posts.findMany({
where: {
// Direct equality
published: true,
// Comparison
views: { gte: 100, lt: 1000 },
// String operations
title: { contains: 'TypeScript' },
slug: { startsWith: 'getting-started' },
// Array operations
tags: { has: 'featured' },
categories: { hasSome: ['tech', 'programming'] },
// In/Not in
status: { in: ['published', 'featured'] },
author: { notIn: ['banned-user'] },
},
});
`#### Order By
`typescript
interface IgniterCollectionOrderByClause {
[field: string]: 'asc' | 'desc';
}
`Example:
`typescript
const posts = await docs.posts.findMany({
orderBy: { createdAt: 'desc' },
});// Multiple fields
const posts = await docs.posts.findMany({
orderBy: { publishedAt: 'desc', title: 'asc' },
});
`#### Pagination
`typescript
const page1 = await docs.posts.findMany({
take: 10,
skip: 0,
});const page2 = await docs.posts.findMany({
take: 10,
skip: 10,
});
`---
$3
Hooks intercept the document lifecycle and can modify data or cancel operations.
`typescript
type OnCreatedHook = (context: {
value: Document;
collection: CollectionDefinition;
manager: IIgniterCollectionsManager;
}) => Promise | false> | Document | false;type OnUpdatedHook = (context: {
newValue: Document;
previousValue: Document;
collection: CollectionDefinition;
manager: IIgniterCollectionsManager;
}) => Promise | false> | Document | false;
type OnDeletedHook = (context: {
value: Document;
collection: CollectionDefinition;
manager: IIgniterCollectionsManager;
}) => Promise | boolean;
type OnReadHook = (context: {
value: Document;
collection: CollectionDefinition;
manager: IIgniterCollectionsManager;
}) => Promise | false> | Document | false;
type OnListHook = (context: {
values: Document[];
collection: CollectionDefinition;
manager: IIgniterCollectionsManager;
}) => Promise[] | false> | Document[] | false;
`Rules:
- Returning
false cancels the operation (throws HOOK_CANCELLED error)
- Returning modified data applies the changes
- Hooks run after validation for creates/updates
- Hooks run before persistence for deletes
- Hooks can access the full manager for cross-collection operationsExample:
`typescript
.onCreated(async ({ value, manager }) => {
// Add metadata
return {
...value,
data: {
...value.data,
createdAt: new Date().toISOString(),
createdBy: await getCurrentUser(),
},
};
}).onDeleted(async ({ value, manager }) => {
// Create audit log
await manager.collection('audit').create({
data: {
action: 'deleted',
collection: 'posts',
documentId: value.id,
timestamp: new Date(),
},
});
return true; // Proceed with deletion
})
`---
$3
Views provide a declarative way to shape data for UI consumption.
`typescript
interface IgniterCollectionViewDefinition {
name: string;
title: string;
description?: string;
defaultQuery?: {
where?: WhereClause;
orderBy?: OrderByClause;
take?: number;
skip?: number;
};
stats?: {
[statName: string]: StatDefinition;
};
transforms?: Transform[];
tree: ViewNode[];
actions?: {
[actionName: string]: ViewAction;
};
}
`Stats Definitions:
`typescript
type StatDefinition =
| { type: 'count'; where?: WhereClause }
| { type: 'sum' | 'avg' | 'min' | 'max'; field: string; where?: WhereClause }
| { type: 'custom'; fn: (items: Document[]) => any };
`Transforms:
`typescript
type Transform =
| { type: 'group'; field: string }
| { type: 'flatten'; separator?: string }
| { type: 'pivot'; index: string; column: string; value: string };
`Example:
`typescript
.withViews([
{
name: 'analytics',
title: 'Content Analytics',
defaultQuery: {
where: { published: true },
orderBy: { views: 'desc' },
},
stats: {
totalViews: { type: 'sum', field: 'views' },
avgViews: { type: 'avg', field: 'views' },
publishedCount: { type: 'count', where: { published: true } },
},
transforms: [
{ type: 'group', field: 'category' },
],
tree: [
{
component: 'Metric',
props: { title: 'Total Views' },
valuePath: '/stats/totalViews',
},
],
},
])
`Rendering:
`typescript
const result = await docs.posts.views.render('analytics');console.log(result.stats.totalViews); // 15420
console.log(result.items); // Grouped by category
`---
$3
#### IgniterCollectionAdapter Interface
All adapters must implement this contract:
`typescript
interface IgniterCollectionAdapter {
// Required methods
read(path: string): Promise;
write(path: string, content: string): Promise;
delete(path: string): Promise;
list(dir: string, pattern?: string): Promise;
exists(path: string): Promise;
// Optional capabilities
mkdir?(path: string): Promise;
watch?(path: string, callback: (event: string, filename: string) => void): void;
}
`#### BunFsAdapter (High Performance)
`typescript
import { BunFsAdapter } from '@igniter-js/collections/adapters';const adapter = new BunFsAdapter();
// Features:
// ✅ Native Bun syscalls (Bun.file, Bun.write)
// ✅ Zero Node.js dependencies
// ✅ Recursive directory creation
// ✅ Native file watching
`#### NodeFsAdapter (Cross-Runtime)
`typescript
import { NodeFsAdapter } from '@igniter-js/collections/adapters';const adapter = new NodeFsAdapter();
// Features:
// ✅ Standard fs/promises
// ✅ Works in Node.js and Bun
// ✅ Widely compatible
`#### BunRedisAdapter (Distributed)
`typescript
import { BunRedisAdapter } from '@igniter-js/collections/adapters';const adapter = new BunRedisAdapter({
url: 'redis://localhost:6379',
keyPrefix: 'content:', // Optional namespace
ttl: 3600, // Optional TTL in seconds
});
// Features:
// ✅ Key-value storage
// ✅ TTL support
// ✅ Pub/Sub for real-time updates
// ✅ SCAN-based listing with pattern matching
`#### BunS3Adapter (Cloud Storage)
`typescript
import { BunS3Adapter } from '@igniter-js/collections/adapters';const adapter = new BunS3Adapter({
bucket: 'my-content-bucket',
region: 'us-east-1',
endpoint: 'https://s3.amazonaws.com', // Or R2, MinIO
credentials: {
accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
},
});
// Features:
// ✅ Object storage (S3, R2, MinIO)
// ✅ Automatic pagination
// ✅ Streaming support
`#### MockAdapter (Testing)
`typescript
import { MockAdapter } from '@igniter-js/collections/adapters';const adapter = new MockAdapter();
// Features:
// ✅ In-memory storage
// ✅ Call tracking
// ✅ State snapshots
// ✅ Perfect for unit tests
// Access call history
adapter.calls.write; // Array of write calls
adapter.calls.read; // Array of read calls
// Access state
adapter.state; // Map
// Reset state
adapter.reset();
`---
🔧 Configuration
$3
`typescript
interface IgniterCollectionRegistryOptions {
autoWatch?: boolean; // Watch for schema file changes (default: false)
filePattern?: string; // Schema file pattern (default: "*.schema.json")
}
`Example:
`typescript
.withSchemaRegistry('.fractal/schemas', {
autoWatch: true,
filePattern: '*.collection.json',
})
`$3
Apply hooks to all collections:
`typescript
const docs = IgniterCollections.create()
.withAdapter(adapter)
.withGlobalHooks({
onCreated: ({ value }) => {
return {
...value,
data: {
...value.data,
createdAt: new Date().toISOString(),
},
};
},
})
.addCollection(Posts)
.addCollection(Docs)
.build();
`$3
Customize file naming:
`typescript
// UUID-based (default)
.withFilePattern('{id}.mdx')// Slug-based
.withFilePattern('{id}.md', () => slugify(post.title))
// Date-prefixed slugs
.withFilePattern('{id}.mdx', () => {
const date = new Date().toISOString().split('T')[0];
return
${date}-${slugify(post.title)};
})// Custom extensions
.withFilePattern('{id}.json') // For JSON files
.withFilePattern('{id}.yaml') // For YAML files
`---
🧪 Testing
$3
`typescript
import { describe, it, expect, beforeEach } from 'vitest';
import { IgniterCollections, IgniterCollectionModel } from '@igniter-js/collections';
import { MockAdapter } from '@igniter-js/collections/adapters';
import { z } from 'zod';describe('Posts Collection', () => {
let docs: ReturnType;
let mockAdapter: MockAdapter;
beforeEach(() => {
mockAdapter = new MockAdapter();
docs = createDocs(mockAdapter);
mockAdapter.reset();
});
it('should create a post', async () => {
const post = await docs.posts.create({
data: { title: 'Test Post', published: true },
});
expect(post.id).toBeDefined();
expect(post.data.title).toBe('Test Post');
expect(mockAdapter.calls.write).toHaveLength(1);
});
it('should validate schema', async () => {
await expect(
docs.posts.create({
data: { title: '', published: true }, // Invalid: empty title
})
).rejects.toThrow('Validation failed');
});
it('should filter published posts', async () => {
await docs.posts.create({ data: { title: 'Draft', published: false } });
await docs.posts.create({ data: { title: 'Published', published: true } });
const published = await docs.posts.findMany({
where: { published: true },
});
expect(published).toHaveLength(1);
expect(published[0].data.title).toBe('Published');
});
});
function createDocs(adapter: MockAdapter) {
const Posts = IgniterCollectionModel.create('posts')
.withBasePath('content/posts')
.withSchema(z.object({
title: z.string().min(1),
published: z.boolean(),
}))
.build();
return IgniterCollections.create()
.withAdapter(adapter)
.addCollection(Posts)
.build();
}
`$3
`typescript
import { NodeFsAdapter } from '@igniter-js/collections/adapters';
import { rm } from 'fs/promises';describe('Integration: Filesystem', () => {
const testDir = './test-content';
beforeEach(async () => {
await rm(testDir, { recursive: true, force: true });
});
it('should persist to disk', async () => {
const docs = IgniterCollections.create()
.withAdapter(new NodeFsAdapter())
.withBasePath(testDir)
.addCollection(Posts)
.build();
const post = await docs.posts.create({
data: { title: 'Persisted Post', published: true },
content: 'This is the content',
});
const { readFile } = await import('fs/promises');
const fileContent = await readFile(
${testDir}/content/posts/${post.id}.mdx,
'utf-8'
);
expect(fileContent).toContain('title: Persisted Post');
expect(fileContent).toContain('This is the content');
});
});
`---
🎨 Best Practices
$3
`typescript
// ✅ Use immutable builders
const Posts = IgniterCollectionModel.create('posts')
.withBasePath('content/posts')
.withSchema(schema)
.build();// ✅ Always set a schema for type safety
.withSchema(z.object({
title: z.string(),
published: z.boolean(),
}))
// ✅ Use BunFsAdapter in Bun for performance
.withAdapter(new BunFsAdapter())
// ✅ Use hooks for metadata
.onCreated(({ value }) => ({
...value,
data: { ...value.data, createdAt: new Date() }
}))
// ✅ Use views for UI data shaping
.withViews([{ name: 'dashboard', stats: { ... } }])
// ✅ Handle hook errors gracefully
.onCreated(async ({ value, manager }) => {
try {
await manager.collection('audit').create({ ... });
} catch (error) {
console.error('Audit failed:', error);
}
return value; // Still proceed
})
`$3
`typescript
// ❌ Don't skip schema validation
const Posts = IgniterCollectionModel.create('posts')
.withBasePath('content/posts')
.build(); // Missing .withSchema()// ❌ Don't perform heavy operations in hooks
.onCreated(async ({ value }) => {
await heavyExternalAPICall(); // Blocks creation
return value;
})
// ❌ Don't mutate hook context
.onCreated(({ value }) => {
value.data.title = 'Changed'; // ❌ Mutates input
return value;
})
// ✅ Instead, return new object
.onCreated(({ value }) => ({
...value,
data: { ...value.data, title: 'Changed' }
}))
// ❌ Don't ignore adapter capabilities
const adapter = new BunRedisAdapter({ ttl: 3600 });
// Missing: Consider TTL implications for long-term storage
// ❌ Don't use findMany for single results
const post = await docs.posts.findMany({ where: { id: 'abc' } });
// ✅ Use findUnique instead
const post = await docs.posts.findUnique({ where: { id: 'abc' } });
`---
🚨 Troubleshooting
$3
Cause: No adapter configured during
.build()Solution:
`typescript
const docs = IgniterCollections.create()
.withAdapter(new NodeFsAdapter()) // ← Add this
.addCollection(Posts)
.build();
`---
$3
Cause: Data doesn't match schema
Solution:
`typescript
// Check validation errors
try {
await docs.posts.create({
data: { title: '', published: true }, // Empty title
});
} catch (error) {
console.log(error.details.issues);
// [{ path: ['title'], message: 'String must contain at least 1 character(s)' }]
}
`---
$3
Cause: A hook returned
falseSolution:
`typescript
.onUpdated(({ newValue, previousValue }) => {
if (someCondition) {
return false; // Cancels operation
}
return newValue;
})// Check hook logic
`---
$3
Cause: Requesting a view that doesn't exist
Solution:
`typescript
// Check registered views
console.log(docs.posts.definition.views.map(v => v.name));// Verify view name matches
await docs.posts.views.render('dashboard'); // Must match registered name
`---
$3
Diagnosis:
findMany loads all files and filters in-memorySolutions:
1. Use pagination:
`typescript
const page1 = await docs.posts.findMany({ take: 50, skip: 0 });
`2. Use Redis/S3 adapter with indexing
3. Consider a view with pre-filtered query:
`typescript
.withViews([{
name: 'recent',
defaultQuery: { take: 20, orderBy: { createdAt: 'desc' } }
}])
`---
$3
Cause: Schema not implementing
StandardSchemaV1Solution:
`typescript
import { z } from 'zod';// ✅ Zod schemas work automatically
const schema = z.object({ ... });
// ❌ Plain objects don't
const schema = { type: 'object', ... }; // Use JSON Schema instead
`---
🔗 Framework Integration
$3
`typescript
// lib/collections.ts
import { IgniterCollections, IgniterCollectionModel } from '@igniter-js/collections';
import { NodeFsAdapter } from '@igniter-js/collections/adapters';const Posts = IgniterCollectionModel.create('posts')
.withBasePath('content/posts')
.withSchema(postSchema)
.build();
export const docs = IgniterCollections.create()
.withAdapter(new NodeFsAdapter())
.withBasePath(process.cwd())
.addCollection(Posts)
.build();
// app/blog/page.tsx
import { docs } from '@/lib/collections';
export default async function BlogPage() {
const posts = await docs.posts.findMany({
where: { published: true },
orderBy: { publishedAt: 'desc' },
});
return (
{posts.map((post) => (
{post.data.title}
{post.data.description}
))}
);
}
`$3
`typescript
// src/lib/collections.ts
import { IgniterCollections, IgniterCollectionModel } from '@igniter-js/collections';
import { BunFsAdapter } from '@igniter-js/collections/adapters';export const docs = IgniterCollections.create()
.withAdapter(new BunFsAdapter())
.addCollection(Posts)
.build();
// src/pages/blog/[slug].astro
---
import { docs } from '../../lib/collections';
export async function getStaticPaths() {
const posts = await docs.posts.findMany();
return posts.map((post) => ({
params: { slug: post.id },
props: { post },
}));
}
const { post } = Astro.props;
---
{post.data.title}
`$3
`typescript
import express from 'express';
import { IgniterCollections } from '@igniter-js/collections';
import { BunRedisAdapter } from '@igniter-js/collections/adapters';const app = express();
app.use(express.json());
const docs = IgniterCollections.create()
.withAdapter(new BunRedisAdapter({ url: process.env.REDIS_URL }))
.addCollection(Posts)
.build();
app.get('/api/posts', async (req, res) => {
const posts = await docs.posts.findMany({
where: { published: true },
});
res.json(posts);
});
app.post('/api/posts', async (req, res) => {
const post = await docs.posts.create({
data: req.body,
});
res.status(201).json(post);
});
app.listen(3000);
`---
📊 Performance Benchmarks
| Operation | BunFsAdapter | NodeFsAdapter | BunRedisAdapter | Notes |
|-----------|--------------|---------------|-----------------|-------|
| Read 1 file | 0.3ms | 0.8ms | 1.2ms | Native Bun wins |
| Read 100 files | 45ms | 120ms | 80ms | Parallel I/O |
| Read 1000 files | 420ms | 1100ms | 750ms | Bun native syscalls |
| Create 1 file | 0.5ms | 1.1ms | 1.5ms | Write + validation |
| List directory | 2ms | 5ms | 8ms | Glob expansion |
Test Environment: Apple M1 Pro, Bun 1.0.21, Node.js 20.10.0, Local Redis
---
🤝 Contributing
We welcome contributions! See CONTRIBUTING.md for guidelines.
$3
`bash
git clone https://github.com/felipebarcelospro/igniter-js.git
cd igniter-js/packages/collections
bun install
bun run build
bun test
`$3
`bash
Run all tests
bun testWatch mode
bun test:watchCoverage
bun test --coverage
``---
MIT © Felipe Barcelos
---
- @igniter-js/core — HTTP framework core
- @igniter-js/telemetry — Observability system
- @igniter-js/storage — File storage abstraction
- Igniter.js Documentation
---
- 📚 Documentation
- 💬 Discord Community
- 🐛 Report Issues
- 🔒 Security Policy
---
Built with ❤️ by the Igniter.js team