file: ./src/content/getting-started/ai-tools-integrations.mdx
meta: {
"title": "AI Tools Integrations",
"description": "Learn how to integrate double tie with AI tools and language models through the standardized llms.txt file. This guide explains how to access and import the llms.txt file to enhance your development workflow with AI-powered tools like Cursor."
}
## Overview
The `llms.txt` file is a standardized resource designed to provide concise, LLM-friendly information about a website. By accessing this file, you can enhance your experience with tools that utilize language models, such as Cursor. This document will guide you on how to import the `llms.txt` file into your tools.
## Accessing the `llms.txt` or `llms-full.txt` File
To access the `llms.txt` or `llms-full.txt` file, simply navigate to the following URL in your web browser:
[https://doubletie.com/llms.txt](https://doubletie.com/llms.txt)
[https://doubletie.com/llms-full.txt](https://doubletie.com/llms-full.txt)
This file contains essential information structured in a way that is both human-readable and easily processed by language models. It includes:
* A brief overview of the project or website.
* Links to detailed documentation and resources.
* Additional context that can assist in understanding the content.
## Importing into Cursor
Once you have accessed the `llms.txt` file, you can import it into Cursor or similar tools by following these steps:
1. **Open Cursor**: Launch the Cursor application on your device.
2. **Navigate to Import Options**: Look for the import feature within the tool. This is typically found in the settings or tools menu.
3. **Enter the URL**: When prompted, enter the URL of the `llms.txt` file:
```
https://doubletie.com/llms.txt
```
4. **Confirm Import**: Follow any additional prompts to confirm the import. Cursor will fetch the content from the provided URL and integrate it into your workspace.
5. **Utilize the Information**: Once imported, you can leverage the structured information from the `llms.txt` file to enhance your queries, access relevant documentation, and improve your overall experience with the tool.
## Benefits of Using `llms.txt`
* **Concise Information**: Quickly access essential details without sifting through complex HTML pages.
* **Enhanced Context**: Get relevant links and descriptions that help you understand the content better.
* **Improved Workflow**: Streamline your development process by having all necessary information at your fingertips.
file: ./src/content/getting-started/index.mdx
meta: {
"title": "Introduction to Doubletie",
"description": "A TypeScript toolkit for building self-hostable backend SDKs. Double Tie provides a complete set of tools to help developers create type-safe, self-hostable backend services that can be distributed as npm packages."
}
## What is Doubletie?
Doubletie is an open-source toolkit that transforms how developers build and distribute backend services. Built for modern development teams, it provides a unified solution for:
* Type-safe database operations
* Self-hostable backend services
* Distributable npm packages
* Complete end-to-end type safety
Gone are the days of:
* Complex monolithic backend frameworks
* Lack of type safety between client and server
* Vendor lock-in for backend services
* Limited control over data privacy
* Complex deployment and hosting setups
## Core Principles
### 1. Type Safety First
Building with TypeScript isn't just about developer experience - it's about creating more reliable software:
* End-to-end type inference from database to client
* Zero runtime type errors
* Comprehensive TypeScript support
* Type-safe query building
### 2. Developer Experience
Backend development should feel natural in your workflow:
* Functional programming patterns
* Modern TypeScript patterns
* Intuitive model layer
* Comprehensive documentation
### 3. Self-Hosting as Standard
Every service should be self-hostable:
* Package your backend as npm modules
* Multiple deployment targets
* Zero config deployment
* Complete data control
### 4. Modular Architecture
Build and distribute backend functionality piece by piece:
* Composable packages
* Flexible adapter system
* Extensible through mixins
* Plugin architecture
## Get Started
Ready to modernize your backend infrastructure? Choose your path:
## Coming Soon
We're actively working on additional packages to complete the toolkit:
* ๐๏ธ `@doubletie/sdk-builder` - Create type-safe SDK packages
* ๐ก `@doubletie/client` - Type-safe client library generator
* ๐ ๏ธ `@doubletie/cli` - Migration and deployment tools
* ๐ `@doubletie/model-mapper` - Schema to type-safe models
* โก `@doubletie/functions` - Self-hostable function runtime
## Why Double Tie?
Traditional backend frameworks focus on building monolithic applications. Double Tie takes a different approach by helping developers create distributable, self-hostable backend SDKs. This enables:
* ๐ข Companies to maintain control of their data while using third-party services
* ๐ฉโ๐ป Developers to distribute backend functionality as npm packages
* ๐ Better data privacy through self-hosting
* ๐ฆ Easier integration of backend services into existing applications
file: ./src/content/legals/cookie-policy.mdx
meta: {
"title": "Cookie Policy"
}
## Introduction
This Cookie Policy explains how and why Consent Management Inc (โwe,โ โus,โ โourโ) uses cookies and similar tracking technologies on our Website, [consent.io](https://consent.io) (โWebsiteโ). By using our Website, you consent to our use of cookies in accordance with this policy.
## What Are Cookies?
Cookies are small data files placed on your device when you visit a Website. They help the Website remember your actions and preferences (such as login details or language settings) over a period of time, so you do not have to re-enter them during each visit.
## Types of Cookies We Use
### 1. Essential Cookies
* These cookies are necessary for the Website to function and cannot be disabled. They include cookies that enable you to log in securely.
### 2. Analytics Cookies
* We use analytics cookies, including those deployed by PostHog, to collect data on how you interact with our Website. This non-personally identifiable data is used to improve the Websiteโs functionality and user experience.
### 3. Functionality and Preference Cookies
* These cookies allow the Website to remember choices you make (such as your preferred language) and provide a more personalized experience.
## Managing Cookies
Most browsers allow you to control cookies through their settings. You can:
* Block or delete cookies via your browser settings.
* Opt-out of specific cookies or tracking technologies when such options are provided on the Website.
Please note that disabling cookies may affect the functionality of this Website and your overall user experience.
## Third-Party Cookies
We may allow third parties (for example, analytics providers like PostHog) to place cookies on our Website. These third parties have their own privacy policies and we do not control the cookies they may use.
## Changes to This Cookie Policy
We may update the Cookie Policy periodically to reflect changes in our practices or for operational, legal, or regulatory reasons. The current version will always be posted on our Website with an updated effective date.
## Contact Us
For questions about this Cookie Policy, please contact us at:
> **Consent Management Inc**\
> 2261 Market Street STE 86311
> San Francisco, CA 94114\
> Email: [support@consent.io](mailto:support@consent.io)
file: ./src/content/legals/privacy-policy.mdx
meta: {
"title": "Privacy Policy"
}
## Introduction
Consent Management Inc (โwe,โ โus,โ โourโ) operates the website [consent.io](https://consent.io) (โWebsiteโ). This Privacy Policy describes how we collect, use, disclose, and protect your personal information. By accessing or using the Website, you agree to the practices described in this policy. If you do not agree with these practices, please do not use our Website.
## Applicable Laws and Scope
This policy is designed for a Delaware-based company and informs users worldwide. When you are located in jurisdictions with additional privacy requirements (for example, under the GDPR or CCPA), certain rights and protections may apply. Please refer to the relevant sections below and consult applicable laws for your jurisdiction.
## Information We Collect
### 1. Personal Data
* We may collect personal data that you voluntarily provide when interacting with our Website (for example, your name, email address, and telephone number).
### 2. Automatically Collected Data
* We automatically collect non-personally identifiable information through technologies like cookies and web beacons. This may include IP addresses, browser types, and device identifiers.
### 3. Analytics Data
* We use PostHog to track analytics on our Website. PostHog may collect information such as usage behavior, page views, and interaction data. This information is used solely for enhancing website performance and user experience. For more details, please review PostHogโs own privacy practices on their website.
## How We Use Your Information
We use your personal information to:
* Provide and improve our Website and services.
* Analyze website traffic and usage trends.
* Communicate with you regarding inquiries and updates (subject to your consent where applicable).
* Comply with legal and regulatory requirements.
## Legal Basis for Processing (For Users in Jurisdictions Requiring It)
Where required by law (for example, under the GDPR), our processing is based on:
* Your consent for analytics and marketing purposes.
* Our legitimate interests in operating and improving our Website.
* Any contractual obligations you have with us.
## Information Sharing and Disclosure
We do not sell or rent your personal information. We may share your personal data with:
* **Third-party service providers:** for example, analytics providers like PostHog, who process data on our behalf. These service providers are contractually obligated to maintain the confidentiality and security of your data.
* **Legal authorities:** if required by law or to protect our rights and interests.
* **Other parties:** only with your explicit consent.
## International Data Transfers
Since we serve a worldwide audience, your personal data may be transferred toโand maintained onโservers located outside your state, country, or other jurisdiction (including the United States). We take appropriate steps to ensure that your data is treated securely in accordance with this Privacy Policy and applicable laws.
## Data Retention and Security
* **Data Retention:** We retain personal data only as long as necessary to fulfill its intended purposes or as required by applicable law.
* **Security Measures:** We implement reasonable administrative, technical, and physical safeguards to protect your personal information.
## Your Rights
Depending on your jurisdiction, you may have the right to:
* Access, correct, or delete your personal data.
* Object to or restrict certain processing.
* Withdraw consent.
To exercise these rights, please contact us using the details provided below.
## Children's Privacy
Our Website is not directed to children under 13 years of age. We do not knowingly collect personal information from children. If you believe that we have inadvertently collected such information, please contact us immediately.
## Policy Updates
We may update this Privacy Policy occasionally. If we make material changes, we will update the effective date at the top of the policy and notify you by any required means.
## Contact Us
For questions or concerns regarding this Privacy Policy or our practices, please contact us at:
> **Consent Management Inc**\
> 2261 Market Street STE 86311
> San Francisco, CA 94114\
> Email: [support@consent.io](mailto:support@consent.io)
file: ./src/content/getting-started/open-source/contributing.mdx
meta: {
"title": "Contributing to doubletie.com"
}
We love your input! We want to make contributing to doubletie.com as easy and transparent as possible, whether it's:
* Reporting a bug
* Discussing the current state of the code
* Submitting a fix
* Proposing new features
* Becoming a maintainer
## ๐ License
By contributing to Consent Management (c15t), you agree that your contributions will be licensed under the MIT License. This is a copyleft license that ensures the software and all derivatives remain free and open source.
[Read the full license here](/docs/getting-started/open-source/license)
## ๐ House Rules
### Before You Start
* Check existing [issues](https://github.com/consentdotio/doubletie/issues) and [PRs](https://github.com/consentdotio/doubletie/pulls) first
* **Always create an issue before starting development**
* Follow our PR template carefully
### Issue Approval Process
We use the `needs-approval` label to manage contributions:
#### For Contributors
* ๐ซ **Needs Approval First:**
* New features
* Large-scale refactoring
* Architecture changes
* *Wait for a doubletie.com team member to remove the `needs-approval` label*
* โ
**Can Start Immediately:**
* Bug fixes
* Documentation updates
* Performance improvements
* Security fixes
* Tests
## Development Process
We use GitHub to host code, to track issues and feature requests, as well as accept pull requests.
### ๐ค Automated Workflows
We leverage several automated workflows to ensure code quality:
1. **Code Quality**
* Formatting: Biome automatically formats code
* Types: TypeScript checks run on every PR
* Tests: Vitest runs the test suite
* Build: Turbo ensures everything builds correctly
2. **Dependencies**
* Renovate keeps dependencies up to date
* PNPM manages our packages
* Changesets handles our versioning
3. **Pull Requests**
* PR titles are checked for semantic versioning
* Automated code review for common issues
* Required checks must pass before merging
## Getting Started
1. Fork the repo and create your branch from `main`:
```sh
git clone https://github.com/your-username/c15t.git
cd c15t
git switch -c my-feature
```
2. Install dependencies:
```sh
corepack enable # Sets up PNPM
pnpm install # Installs dependencies
```
3. Make your changes and ensure the following pass:
```sh
pnpm fmt # Format code
pnpm test # Run tests
pnpm build # Build packages
```
## Pull Request Process
1. **Create an Issue First**
* For features/refactoring: Wait for approval (needs-approval label)
* For bugs/docs: Can start work immediately
2. **Make Your Changes**
* Follow our coding standards (enforced by Biome)
* Add tests for new functionality
* Update documentation as needed
3. **Create Pull Request**
* Use our PR template
* Link the related issue
* Add screenshots for UI changes
* Describe your changes clearly
4. **Automated Checks**
The following will run automatically:
* Code formatting (Biome)
* Type checking (TypeScript)
* Tests (Vitest)
* Build verification (Turbo)
* Dependency checks (Renovate)
* PR title format
* Issue linking
5. **Review Process**
* Maintainers will review your code
* Address any requested changes
* Once approved, it will be merged
## Release Process
Releases are automated through our CI/CD pipeline:
1. Merge to `main` triggers version check
2. Changesets determines version bump
3. New version is published to npm
4. GitHub release is created
5. Documentation is updated
## Development Guidelines
### Code Style
We use Biome for formatting and linting. Configuration is in `biome.jsonc`.
### Commits
Follow [Conventional Commits](https://www.conventionalcommits.org/):
* `feat:` New features
* `fix:` Bug fixes
* `docs:` Documentation
* `chore:` Maintenance
* `refactor:` Code changes
* `test:` Test changes
### Testing
* Write tests for new features
* Update tests for changes
* Run `pnpm test` locally
### Documentation
* Update docs with new features
* Include code examples
* Update README if needed
## Questions?
Don't hesitate to:
* Open an issue
* Start a discussion
* Ask in comments
## Important License Note
doubletie.com is licensed under the MIT License. By contributing to this project, you agree to license your contributions under the same license. This means:
* โ
You can use the code commercially
* โ
You can modify the code
* โ
You can distribute the code
* โ
You can use the code privately
* โ
You can sublicense the code
But you must:
* ๐ Include the original license
* ๐ Include copyright notice
[Learn more about MIT License](https://choosealicense.com/licenses/mit/)
file: ./src/content/getting-started/open-source/license.mdx
meta: {
"title": "License"
}
Double Tie is licensed under the MIT License. By contributing to this project, you agree to license your contributions under the same license. This means:
* โ
You can use the code commercially
* โ
You can modify the code
* โ
You can distribute the code
* โ
You can use the code privately
* โ
You can sublicense the code
But you must:
* ๐ Include the original license
* ๐ Include copyright notice
***
## Full License
The MIT License (MIT)
Copyright ยฉ 2025 Consent Management Inc
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
file: ./src/content/getting-started/open-source/why-open-source.mdx
meta: {
"title": "Building Self-Hosted SDKs in the Open",
"description": "We believe great developer tools should be built in the open, with transparency and community collaboration at their core. This philosophy guides how we're building modern self-hosted SDK infrastructure."
}
## Open Source Foundation
Doubletie is built with a strong open source foundation:
* **Core Platform**: MIT licensed, ensuring your SDK tools remain free and open
* **Type Safety**: End-to-end type inference from database to client
* **Query Builder**: Built on Kysely for type-safe database interactions
* **Deploy Options**: Support for Docker, AWS, Vercel and more
## Why We Chose Open Source
Self-hosted SDK infrastructure should be:
1. **Transparent**
* See exactly how queries are built
* Audit the code handling database operations
* Understand the complete data flow
2. **Community-Driven**
* Benefit from collective expertise
* Shape the future of SDK tools
* Share best practices globally
3. **Trustworthy**
* No black boxes in SDK management
* Full visibility into data handling
* Community-verified security
4. **Flexible**
* Self-host for complete control
* Customize to your exact needs
* Integrate with your existing stack
## Our Commitment
By choosing open source, we commit to:
* **Transparency**: All core code is public and auditable
* **Community**: Decisions made with community input
* **Quality**: Enterprise-grade while remaining open
* **Longevity**: Sustainable open source development
## Get Started
Join us in building the future of self-hosted SDKs:
file: ./src/content/packages/query-builder/advanced-usage.mdx
meta: {
"title": "Advanced Usage",
"description": "Type utilities and advanced patterns for the Query Builder"
}
## Type Utilities
The Database types use several advanced TypeScript utilities to provide enhanced type-safety:
* `DrainOuterGeneric` - Helps with generic type handling
* `DeepPartial` - Creates a deeply optional version of a type
* Type constraints like `TTableName extends keyof TDatabaseSchema & string`
These utilities ensure you get proper type checking and autocompletion when using the query builder.
## Custom Expression Builders
```typescript
// Create a custom WHERE clause for date ranges
function dateRange(
eb: ExpressionBuilder,
column: keyof DB[TB] & string,
start: Date,
end: Date
) {
return eb.and([
eb(column, '>=', start),
eb(column, '<=', end)
]);
}
// Usage
const posts = await db.selectFrom('posts')
.where(eb => dateRange(eb, 'created_at', startDate, endDate))
.select(['id', 'title'])
.execute();
```
## Custom Model Extensions
```typescript
// Define type-safe model extensions
interface UserModelExtensions {
findByEmail(email: string): Promise;
findActiveUsers(): Promise;
deactivateUser(id: number): Promise;
}
const userSchema: ModelSchema = {
fields: {
id: { type: 'number' },
name: { type: 'string' },
email: { type: 'string' },
status: { type: 'string' }
},
extensions: {
async findByEmail(email: string) {
return this.findOne('email', email);
},
async findActiveUsers() {
return this.find('status', 'active');
},
async deactivateUser(id: number) {
await this.updatePartial(id, { status: 'inactive' });
}
}
};
// The model will have type-safe extensions
const userModel = db.getModel('users');
const user = await userModel.findByEmail('test@example.com');
await userModel.deactivateUser(user.id);
```
## Extending Type Definitions
When using the query builder with your database schema, you'll define your own schema types:
```typescript
// Define your database schema
interface Database {
users: {
id: number;
name: string;
email: string;
created_at: Date;
};
posts: {
id: number;
title: string;
content: string;
user_id: number;
created_at: Date;
};
}
// Create a type-safe database instance
const db = createDatabase({
dialect: new PostgresDialect(/* connection config */),
});
// The types system will now provide full autocompletion and type checking
const users = await db.selectFrom('users') // Only 'users' or 'posts' allowed
.where('id', '>', 10) // Type-safe column references
.select(['id', 'name']) // Only existing columns allowed
.execute();
```
## Dynamic Queries
```typescript
// Build queries dynamically based on runtime conditions
function createUserQuery(filters: Record) {
let query = db.selectFrom('users');
if (filters.name) {
query = query.where('name', 'like', `%${filters.name}%`);
}
if (filters.status) {
query = query.where('status', '=', filters.status);
}
if (filters.sortBy && filters.sortDirection) {
query = query.orderBy(
filters.sortBy as keyof Database['users'] & string,
filters.sortDirection as 'asc' | 'desc'
);
}
return query.selectAll();
}
// Usage
const activeUsers = await createUserQuery({
status: 'active',
sortBy: 'created_at',
sortDirection: 'desc'
}).execute();
```
## Working with Raw SQL
```typescript
import { sql } from 'kysely';
// Use raw SQL when needed
const result = await db.selectFrom('users')
.select([
'id',
'name',
sql`CONCAT(first_name, ' ', last_name)`.as('full_name'),
sql`EXTRACT(YEAR FROM created_at)`.as('join_year')
])
.execute();
```
file: ./src/content/packages/query-builder/core-types.mdx
meta: {
"title": "Core Types",
"description": "Essential type definitions for the Query Builder"
}
## DatabaseConfig
Configuration options for creating a database instance.
```typescript
type DatabaseConfig = {
/** The database dialect to use */
dialect: Dialect;
/** Whether the database is isolated */
isolated?: boolean;
/** Function to log database events */
log?: (event: LogEvent) => void;
/** Whether to enable debug mode */
debug?: boolean;
};
```
**Example:**
```typescript
import { createDatabase } from '@doubletie/query-builder';
import { PostgresDialect } from 'kysely';
const db = createDatabase({
dialect: new PostgresDialect({
host: 'localhost',
database: 'mydb',
user: 'postgres',
password: 'password'
}),
debug: true,
log: (event) => console.log(event)
});
```
## MigratorOptions
Options for configuring database migrations.
```typescript
type MigratorOptions = {
/** Migration provider that supplies the migrations */
provider: MigrationProvider;
/** Whether to allow migrations to be run in a different order than they were created */
allowUnorderedMigrations?: boolean;
};
```
**Example:**
```typescript
import { FileMigrationProvider } from 'kysely';
// Create a migrator using the database instance
const migrator = db.createMigrator({
provider: new FileMigrationProvider({
migrationFolder: './migrations'
}),
allowUnorderedMigrations: false
});
// Run migrations
await migrator.migrateToLatest();
```
file: ./src/content/packages/query-builder/database-api.mdx
meta: {
"title": "Database API",
"description": "Complete reference for the Database interface"
}
The core `Database` interface provides all database operations and utilities. It includes:
* Dialect-specific methods (`isSqlite`, `isMysql`, `isPostgres`)
* Model creation and registration methods
* Transaction support
* Query builders for SELECT, INSERT, UPDATE, DELETE operations
* Utility methods for common database tasks
## Key Methods
```typescript
interface Database {
// Dialect helpers
isSqlite: () => boolean;
isMysql: () => boolean;
isPostgres: () => boolean;
// Model management
model: (...) => Model;
registerModels: (registry: RegistryType) => Database;
getModel: (...) => Model;
// Transaction handling
transaction: (...) => Promise;
isTransaction: () => boolean;
// Query builders
selectFrom: (...) => SelectQueryBuilder;
insertInto: (...) => InsertQueryBuilder;
updateTable: (...) => UpdateQueryBuilder;
deleteFrom: (...) => DeleteQueryBuilder;
// Advanced queries
with: (...) => any;
selectNoFrom: () => any;
// Utility methods
tuple: (...) => Function;
cast: (...) => any;
streamFrom: (...) => SelectQueryBuilder;
findNotNull: (...) => SelectQueryBuilder;
// Connection management
destroy: () => Promise;
}
```
## Query Builder Examples
### Basic Select Query
```typescript
const users = await db.selectFrom('users')
.where('status', '=', 'active')
.selectAll()
.execute();
```
### Select with Joins
```typescript
const usersWithPosts = await db.selectFrom('users')
.leftJoin('posts', 'users.id', 'posts.user_id')
.select([
'users.id as userId',
'users.name',
'posts.id as postId',
'posts.title'
])
.where('users.status', '=', 'active')
.execute();
```
### Tuple Comparison
```typescript
const result = await db.selectFrom('orders')
.where((eb) => {
const tupleBuilder = db.tuple(['product_id', 'status']);
return eb(
tupleBuilder(eb),
'=',
db.tuple([10, 'shipped'])(eb)
);
})
.selectAll()
.execute();
```
### Casting Values
```typescript
const numericValue = db.cast('123', 'INTEGER');
const query = db.selectFrom('products')
.where('price', '<', numericValue)
.selectAll();
```
### Finding Non-Null Values
```typescript
const activeUsers = await db.findNotNull('users', 'last_login')
.where('status', '=', 'active')
.execute();
```
### Streaming Results (SQLite Only)
```typescript
const stream = await db.streamFrom('logs', ['id', 'message', 'created_at'])
.where('level', '=', 'error')
.stream();
```
### Insert Queries
```typescript
// Simple insert
await db.insertInto('users')
.values({
name: 'John',
email: 'john@example.com'
})
.execute();
// Insert with returning
const user = await db.insertInto('users')
.values({
name: 'Jane',
email: 'jane@example.com'
})
.returningAll()
.executeTakeFirst();
// Bulk insert
await db.insertInto('logs')
.values([
{ message: 'Log 1', level: 'info' },
{ message: 'Log 2', level: 'error' }
])
.execute();
// Insert with default values
await db.insertDefault('timestamps')
.execute();
```
### Update Queries
```typescript
// Update with conditions
await db.updateTable('users')
.set({ status: 'inactive' })
.where('last_login', '<', oneMonthAgo)
.execute();
// Update a single column
await db.updateColumn('products', 'price', 9.99)
.where('id', '=', 123)
.execute();
```
### Delete Queries
```typescript
// Simple delete
await db.deleteFrom('sessions')
.where('expires_at', '<', new Date())
.execute();
```
### WITH Clauses (Common Table Expressions)
```typescript
const result = await db
.with('active_users', (qb) =>
qb.selectFrom('users')
.where('status', '=', 'active')
.select(['id', 'name'])
)
.selectFrom('active_users')
.leftJoin('orders', 'active_users.id', 'orders.user_id')
.select(['active_users.name', 'count(orders.id) as order_count'])
.groupBy('active_users.id')
.execute();
```
file: ./src/content/packages/query-builder/index.mdx
meta: {
"title": "Introduction to Query Builder",
"description": "A TypeScript toolkit for building self-hostable backend SDKs. Double Tie provides a complete set of tools to help developers create type-safe, self-hostable backend services that can be distributed as npm packages."
}
## Core Types
### DatabaseConfig
Configuration options for creating a database instance.
```typescript
type DatabaseConfig = {
/** The database dialect to use */
dialect: Dialect;
/** Whether the database is isolated */
isolated?: boolean;
/** Function to log database events */
log?: (event: LogEvent) => void;
/** Whether to enable debug mode */
debug?: boolean;
};
```
**Example:**
```typescript
import { createDatabase } from '@doubletie/query-builder';
import { PostgresDialect } from 'kysely';
const db = createDatabase({
dialect: new PostgresDialect({
host: 'localhost',
database: 'mydb',
user: 'postgres',
password: 'password'
}),
debug: true,
log: (event) => console.log(event)
});
```
### MigratorOptions
Options for configuring database migrations.
```typescript
type MigratorOptions = {
/** Migration provider that supplies the migrations */
provider: MigrationProvider;
/** Whether to allow migrations to be run in a different order than they were created */
allowUnorderedMigrations?: boolean;
};
```
**Example:**
```typescript
import { FileMigrationProvider } from 'kysely';
// Create a migrator using the database instance
const migrator = db.createMigrator({
provider: new FileMigrationProvider({
migrationFolder: './migrations'
}),
allowUnorderedMigrations: false
});
// Run migrations
await migrator.migrateToLatest();
```
## Transaction Types
### TransactionCallback
Function type for executing operations within a transaction.
```typescript
type TransactionCallback = (
trx: TransactionResponse
) => Promise;
```
### TransactionResponse
Object passed to transaction callbacks with the transaction instance and afterCommit hook.
```typescript
type TransactionResponse = {
/** Transaction instance */
transaction: Kysely;
/** Register a callback to execute after the transaction commits */
afterCommit: (callback: AfterCommitCallback) => void;
};
```
### AfterCommitCallback
Function to be executed after a transaction is successfully committed.
```typescript
type AfterCommitCallback = () => Promise;
```
**Example:**
```typescript
// Execute operations in a transaction with afterCommit hooks
await db.transaction(async ({ transaction, afterCommit }) => {
// Perform database operations
const user = await transaction
.insertInto('users')
.values({ name: 'John', email: 'john@example.com' })
.returningAll()
.executeTakeFirst();
// Register a callback to run after successful commit
afterCommit(async () => {
await sendWelcomeEmail(user.email);
});
return user;
});
```
## Model Registration
### ModelRegistry
Registry for defining and storing model definitions.
```typescript
type ModelRegistry = {
[TTableName in keyof TDatabaseSchema & string]?: {
/** The model schema definition */
schema: DeepPartial>;
/** The primary key specification */
primaryKey: PrimaryKeySpecification;
};
};
```
**Example:**
```typescript
// Define model schemas
const userSchema: ModelSchema = {
fields: {
id: { type: 'number' },
name: { type: 'string' },
email: { type: 'string' },
created_at: { type: 'date' }
},
extensions: {
async findByEmail(email: string) {
return this.findOne('email', email);
}
}
};
// Register models with the database
const models = {
users: {
schema: userSchema,
primaryKey: { field: 'id' }
}
};
db.registerModels(models);
// Get a registered model
const userModel = db.getModel('users');
```
## Database Interface
The core `Database` interface provides all database operations and utilities. It includes:
* Dialect-specific methods (`isSqlite`, `isMysql`, `isPostgres`)
* Model creation and registration methods
* Transaction support
* Query builders for SELECT, INSERT, UPDATE, DELETE operations
* Utility methods for common database tasks
### Key Methods
```typescript
interface Database {
// Dialect helpers
isSqlite: () => boolean;
isMysql: () => boolean;
isPostgres: () => boolean;
// Model management
model: (...) => Model;
registerModels: (registry: RegistryType) => Database;
getModel: (...) => Model;
// Transaction handling
transaction: (...) => Promise;
isTransaction: () => boolean;
// Query builders
selectFrom: (...) => SelectQueryBuilder;
insertInto: (...) => InsertQueryBuilder;
updateTable: (...) => UpdateQueryBuilder;
deleteFrom: (...) => DeleteQueryBuilder;
// Advanced queries
with: (...) => any;
selectNoFrom: () => any;
// Utility methods
tuple: (...) => Function;
cast: (...) => any;
streamFrom: (...) => SelectQueryBuilder;
findNotNull: (...) => SelectQueryBuilder;
// Connection management
destroy: () => Promise;
}
```
**Examples:**
```typescript
// Create a simple query
const users = await db.selectFrom('users')
.where('status', '=', 'active')
.selectAll()
.execute();
// Use tuple comparison
const result = await db.selectFrom('orders')
.where((eb) => {
const tupleBuilder = db.tuple(['product_id', 'status']);
return eb(
tupleBuilder(eb),
'=',
db.tuple([10, 'shipped'])(eb)
);
})
.selectAll()
.execute();
// Cast values
const numericValue = db.cast('123', 'INTEGER');
const query = db.selectFrom('products')
.where('price', '<', numericValue)
.selectAll();
// Find rows with non-null values
const activeUsers = await db.findNotNull('users', 'last_login')
.where('status', '=', 'active')
.execute();
// Stream results (SQLite only)
const stream = await db.streamFrom('logs', ['id', 'message', 'created_at'])
.where('level', '=', 'error')
.stream();
```
## Type Utilities
The Database types use several advanced TypeScript utilities to provide enhanced type-safety:
* `DrainOuterGeneric` - Helps with generic type handling
* `DeepPartial` - Creates a deeply optional version of a type
* Type constraints like `TTableName extends keyof TDatabaseSchema & string`
These utilities ensure you get proper type checking and autocompletion when using the query builder.
## Extending Type Definitions
When using the query builder with your database schema, you'll define your own schema types:
```typescript
// Define your database schema
interface Database {
users: {
id: number;
name: string;
email: string;
created_at: Date;
};
posts: {
id: number;
title: string;
content: string;
user_id: number;
created_at: Date;
};
}
// Create a type-safe database instance
const db = createDatabase({
dialect: new PostgresDialect(/* connection config */),
});
// The types system will now provide full autocompletion and type checking
const users = await db.selectFrom('users') // Only 'users' or 'posts' allowed
.where('id', '>', 10) // Type-safe column references
.select(['id', 'name']) // Only existing columns allowed
.execute();
```
For more information on using the query builder, refer to the main README documentation and example code.
file: ./src/content/packages/query-builder/models.mdx
meta: {
"title": "Model Registration and Usage",
"description": "Using the type-safe model layer for database operations"
}
## ModelRegistry
Registry for defining and storing model definitions.
```typescript
type ModelRegistry = {
[TTableName in keyof TDatabaseSchema & string]?: {
/** The model schema definition */
schema: DeepPartial>;
/** The primary key specification */
primaryKey: PrimaryKeySpecification;
};
};
```
## ModelSchema
```typescript
type ModelSchema = {
fields: { [K in keyof TEntityType]: FieldDefinition };
relations?: Record>;
indexes?: Array<{
name?: string;
columns: Array;
unique?: boolean;
}>;
extensions?: ModelExtensions;
};
```
## Defining and Registering Models
```typescript
// Define model schemas
const userSchema: ModelSchema = {
fields: {
id: { type: 'number' },
name: { type: 'string' },
email: { type: 'string' },
created_at: { type: 'date' }
},
extensions: {
async findByEmail(email: string) {
return this.findOne('email', email);
}
}
};
// Register models with the database
const models = {
users: {
schema: userSchema,
primaryKey: { field: 'id' }
}
};
db.registerModels(models);
// Get a registered model
const userModel = db.getModel('users');
```
## Using Model Methods
```typescript
// Basic CRUD operations
const user = await userModel.findById(1);
const users = await userModel.find('status', 'active');
const newUser = await userModel.insertRequired({ name: 'Alice', email: 'alice@example.com' });
const updated = await userModel.updatePartial(1, { name: 'Updated Name' });
await userModel.deleteFrom().where('id', '=', 1).execute();
// Using custom extensions
const userByEmail = await userModel.findByEmail('john@example.com');
// Advanced queries
const results = await userModel.findByTuple(
['name', 'status'],
['John', 'active']
);
```
## Transaction Support
```typescript
await db.transaction(async ({ transaction }) => {
// The model automatically uses the transaction context
const user = await userModel.findById(1);
// Update within the transaction
await userModel.updatePartial(user.id, { status: 'updated' });
// Both operations are part of the same transaction
});
```
file: ./src/content/packages/query-builder/transactions.mdx
meta: {
"title": "Transaction Types and Usage",
"description": "Working with database transactions in Query Builder"
}
## TransactionCallback
Function type for executing operations within a transaction.
```typescript
type TransactionCallback = (
trx: TransactionResponse
) => Promise;
```
## TransactionResponse
Object passed to transaction callbacks with the transaction instance and afterCommit hook.
```typescript
type TransactionResponse = {
/** Transaction instance */
transaction: Kysely;
/** Register a callback to execute after the transaction commits */
afterCommit: (callback: AfterCommitCallback) => void;
};
```
## AfterCommitCallback
Function to be executed after a transaction is successfully committed.
```typescript
type AfterCommitCallback = () => Promise;
```
## Basic Transaction Example
```typescript
// Execute operations in a transaction with afterCommit hooks
await db.transaction(async ({ transaction, afterCommit }) => {
// Perform database operations
const user = await transaction
.insertInto('users')
.values({ name: 'John', email: 'john@example.com' })
.returningAll()
.executeTakeFirst();
// Register a callback to run after successful commit
afterCommit(async () => {
await sendWelcomeEmail(user.email);
});
return user;
});
```
## Nested Transactions
```typescript
// Parent transaction
await db.transaction(async ({ transaction: tx1, afterCommit }) => {
// Nested transaction - reuses the same transaction
await db.transaction(async ({ transaction: tx2 }) => {
// tx1 and tx2 refer to the same transaction
console.log(tx1 === tx2); // true
// Perform operations in the nested scope
await tx2.insertInto('logs')
.values({ message: 'Nested transaction' })
.execute();
});
// Callbacks run only after the outermost transaction commits
afterCommit(async () => {
console.log('Transaction committed');
});
});
```
## Error Handling
```typescript
try {
await db.transaction(async ({ transaction }) => {
// If any operation fails, the entire transaction is rolled back
await transaction.insertInto('users')
.values({ name: 'Jane', email: 'jane@example.com' })
.execute();
// This will throw an error if a constraint is violated
await transaction.insertInto('users')
.values({ name: 'Jane', email: 'jane@example.com' }) // Duplicate email
.execute();
// This code won't execute if the above throws
await transaction.insertInto('logs')
.values({ message: 'User created' })
.execute();
});
} catch (error) {
console.error('Transaction failed:', error);
// All changes have been rolled back
}
```
file: ./src/content/packages/query-builder/mixins/assign.mdx
meta: {
"title": "Assign Properties",
"description": "Enhances models with data assignment capabilities for building modifiable model instances"
}
The `withAssignProperties` mixin (also exported as `withAssign`) adds data assignment capabilities to your models. This allows you to create model instances with attached data and update them in a immutable way.
## Usage
```typescript
import { withAssignProperties } from '@doubletie/query-builder/mixins';
import { createDatabase, createModel } from '@doubletie/query-builder';
// Create a database and model
const db = createDatabase(/* config */);
const UserModel = createModel(db, 'users', 'id');
// Apply the assign mixin
const UserWithAssign = withAssignProperties(UserModel);
// Create a user
const user = await UserWithAssign.findById(1);
// Create a model instance with the user data
const userInstance = {
...UserWithAssign,
...user
};
// Use assign to create a new instance with updated data
const updatedUser = userInstance.assign({
name: 'Updated Name',
status: 'inactive'
});
// The original instance is unchanged
console.log(userInstance.name); // Original name
console.log(updatedUser.name); // "Updated Name"
// Update in database
await UserModel.updateTable()
.where('id', '=', updatedUser.id)
.set({
name: updatedUser.name,
status: updatedUser.status
})
.execute();
```
## API Reference
### withAssignProperties()
```typescript
function withAssignProperties<
TDatabase,
TTableName extends keyof TDatabase & string,
TIdColumnName extends keyof TDatabase[TTableName] & string
>(model: ModelFunctions): ModelWithAssign
```
Enhances a model with the `assign` method.
### ModelWithAssign
```typescript
interface ModelWithAssign extends ModelFunctions {
/**
* Assigns data values to the model instance
*
* @param data - Data to assign (can be partial)
* @returns Model instance with assigned data and all model properties
*/
assign(data?: Partial>): any;
}
```
### assign()
```typescript
assign(data?: Partial>): any
```
Creates a new model instance with the assigned data. This method:
* Creates a copy of the current instance
* Merges the new data into the copy
* Returns the new instance without modifying the original
## Combining with Other Mixins
The assign mixin works well with other mixins to create rich model instances:
```typescript
import {
withAssignProperties,
withGlobalId
} from '@doubletie/query-builder/mixins';
// Apply multiple mixins
const UserWithFeatures = withAssignProperties(
withGlobalId(UserModel, 'id', 'User')
);
// Create an instance
const userInstance = {
...UserWithFeatures,
...user
};
// Use methods from both mixins
const globalId = userInstance.getGlobalId(userInstance.id);
const updatedUser = userInstance.assign({ name: 'New Name' });
```
## Best Practices
* Always create a new instance with `assign` rather than modifying the original
* When updating the database, extract only the fields you want to update
* Combine with other mixins like `withGlobalId` or `withSlug` for richer models
* Consider using TypeScript to ensure type safety for model instances
file: ./src/content/packages/query-builder/mixins/created-at.mdx
meta: {
"title": "Created At",
"description": "Automatically adds creation timestamps to database records"
}
The `withCreatedAt` mixin automatically adds timestamps when records are created in the database. This eliminates the need to manually set created\_at fields on every insert operation.
## Usage
```typescript
import { withCreatedAt } from '@doubletie/query-builder/mixins';
import { createDatabase, createModel } from '@doubletie/query-builder';
// Create a database and model
const db = createDatabase(/* config */);
const UserModel = createModel(db, 'users', 'id');
// Apply the created_at mixin
const UserWithTimestamp = withCreatedAt(UserModel, 'createdAt');
// Insert a record without specifying createdAt
const user = await UserWithTimestamp.insertInto()
.values({
name: 'John Doe',
email: 'john@example.com',
// createdAt is automatically added
})
.returningAll()
.executeTakeFirstOrThrow();
console.log(user.createdAt); // Current timestamp
```
## API Reference
### withCreatedAt()
```typescript
function withCreatedAt<
TDatabase,
TTableName extends keyof TDatabase & string,
TIdColumnName extends keyof TDatabase[TTableName] & string
>(
model: ModelFunctions,
field: keyof TDatabase[TTableName] & string
): ModelFunctions & {
processDataBeforeInsert(data: T): T & { [K in typeof field]: Date };
}
```
Enhances a model with automatic createdAt timestamp functionality.
#### Parameters
* `model` - The base model to enhance
* `field` - The name of the field to use for creation timestamps (e.g., 'createdAt')
#### Returns
Enhanced model that automatically sets the creation timestamp field during insert operations.
## Features
* **Automatic Timestamps**: Sets the specified field to the current date/time on insert
* **Batch Support**: Works with both single and batch insert operations
* **Respect Existing Values**: Preserves existing timestamp values if already provided
* **Composition**: Works well with other mixins in combination
## Example: Batch Inserts
```typescript
// Batch insert with automatic timestamps
const users = await UserWithTimestamp.insertInto()
.values([
{ name: 'Alice', email: 'alice@example.com' },
{ name: 'Bob', email: 'bob@example.com' }
])
.returningAll()
.execute();
// Both records have createdAt timestamps
users.forEach(user => {
console.log(`${user.name}: ${user.createdAt}`);
});
```
## Example: With Existing Timestamp
```typescript
// Using a specific date instead of the current time
const specificDate = new Date('2023-01-01T00:00:00Z');
const user = await UserWithTimestamp.insertInto()
.values({
name: 'Jane',
email: 'jane@example.com',
createdAt: specificDate
})
.returningAll()
.executeTakeFirstOrThrow();
console.log(user.createdAt); // 2023-01-01T00:00:00Z (preserved)
```
## Combining with Other Mixins
```typescript
import { withCreatedAt, withUpdatedAt } from '@doubletie/query-builder/mixins';
// Apply both timestamp mixins
const UserWithTimestamps = withUpdatedAt(
withCreatedAt(UserModel, 'createdAt'),
'updatedAt'
);
// Now both timestamps will be managed automatically
const user = await UserWithTimestamps.insertInto()
.values({
name: 'Tim',
email: 'tim@example.com'
})
.returningAll()
.executeTakeFirstOrThrow();
console.log(user.createdAt); // Set on creation
console.log(user.updatedAt); // Also set on creation
```
file: ./src/content/packages/query-builder/mixins/cursorable.mdx
meta: {
"title": "Cursorable",
"description": "Adds cursor-based pagination for efficient list traversal"
}
The `withCursorable` mixin adds cursor-based pagination to your models, following the Relay Connection specification pattern. This pagination approach is ideal for efficiently traversing large datasets and provides stable pagination even when items are added or removed.
## Usage
```typescript
import { withCursorable } from '@doubletie/query-builder/mixins';
import { createDatabase, createModel } from '@doubletie/query-builder';
// Create a database and model
const db = createDatabase(/* config */);
const UserModel = createModel(db, 'users', 'id');
// Apply the cursorable mixin with sorting configuration
const UserWithPagination = withCursorable(UserModel, {
sortKeys: {
newest: [
['createdAt', { direction: 'desc', reversible: true }],
['id', { direction: 'asc' }]
],
alphabetical: [
['name', { direction: 'asc' }],
['id', { direction: 'asc' }]
]
},
limit: 10,
max: 50
});
// Get first page of users sorted by newest first
const firstPage = await UserWithPagination.getCursorableConnection({
first: 10,
sortKey: 'newest'
});
// Get next page using the endCursor
const nextPage = await UserWithPagination.getCursorableConnection({
first: 10,
after: firstPage.pageInfo.endCursor,
sortKey: 'newest'
});
// Get previous page (backward pagination)
const prevPage = await UserWithPagination.getCursorableConnection({
last: 10,
before: firstPage.pageInfo.startCursor,
sortKey: 'newest'
});
```
## API Reference
### withCursorable()
```typescript
function withCursorable<
TDatabase,
TTableName extends keyof TDatabase & string,
TIdColumnName extends keyof TDatabase[TTableName] & string
>(
model: ModelFunctions,
config: {
sortKeys: Record[]>;
max?: number;
limit?: number;
sortKey?: string;
}
): ModelFunctions & CursorableMethods
```
Enhances a model with cursor-based pagination methods.
#### Configuration Options
* `sortKeys`: Record mapping sort key names to column sort configurations
* `max`: Maximum number of records that can be requested in a single page (default: 100)
* `limit`: Default number of records to return per page (default: 10)
* `sortKey`: Default sort key to use when none is specified
#### Column Sort Configuration
```typescript
type ColumnSort = [keyof TTable & string, ColumnSortOptions];
type ColumnSortOptions = {
/** Sort direction, either 'asc' (ascending) or 'desc' (descending) */
direction?: 'asc' | 'desc';
/** Whether the sort direction can be reversed for backward pagination */
reversible?: boolean;
/** Whether this column contains timestamp values that may need special handling */
timestamp?: boolean;
/** Optional SQL modifier to apply to the column in ORDER BY clauses */
modifier?: string;
};
```
## Added Methods
### getCursorableQuery()
```typescript
getCursorableQuery(
options: CursorableOptions
): ReturnType
```
Creates a query builder configured for cursor-based pagination.
###
file: ./src/content/packages/query-builder/mixins/global-id.mdx
meta: {
"title": "Global ID",
"description": "Adds global ID functionality for GraphQL-friendly identifiers"
}
The `withGlobalId` mixin provides global ID functionality for models, enabling easy integration with GraphQL APIs and systems that require globally unique identifiers. This mixin follows the pattern of prefixing local IDs with a type name (e.g., `User_123`).
## Usage
```typescript
import { withGlobalId } from '@doubletie/query-builder/mixins';
import { createDatabase, createModel } from '@doubletie/query-builder';
// Create a database and model
const db = createDatabase(/* config */);
const UserModel = createModel(db, 'users', 'id');
// Apply the global ID mixin with a type prefix
const UserWithGlobalId = withGlobalId(UserModel, 'id', 'User');
// Get a record
const user = await UserWithGlobalId.findById(123);
// Generate a global ID
const globalId = UserWithGlobalId.getGlobalId(user.id);
console.log(globalId); // "User_123"
// Find a record using a global ID
const foundUser = await UserWithGlobalId.findByGlobalId('User_123');
```
## API Reference
### withGlobalId()
```typescript
function withGlobalId<
TDatabase,
TTableName extends keyof TDatabase & string,
TIdColumnName extends keyof TDatabase[TTableName] & string,
TIdType extends SelectType
>(
model: ModelFunctions,
idColumnName: TIdColumnName,
type?: string
): ModelFunctions & GlobalIdMethods
```
Enhances a model with global ID functionality.
#### Parameters
* `model` - The base model to enhance
* `idColumnName` - Name of the ID column
* `type` - Type prefix for global IDs (defaults to table name in uppercase)
#### Returns
Enhanced model with global ID methods.
## Added Methods
### getGlobalId()
```typescript
getGlobalId(id: TIdType | string | number): string
```
Generates a global ID from a local ID by adding the type prefix.
### getLocalId()
```typescript
getLocalId(globalId: string): TIdType
```
Extracts the local ID from a global ID by removing the type prefix.
### parseGlobalId()
```typescript
parseGlobalId(globalId: string | null | undefined): TIdType | string | null
```
Parses a global ID, returning just the ID portion or null if invalid.
### findByGlobalId()
```typescript
findByGlobalId(
globalId: string,
options?: { throwIfNotFound?: boolean; error?: typeof NoResultError }
): Promise | undefined>
```
Finds a record by its global ID.
### getByGlobalId()
```typescript
getByGlobalId(
globalId: string,
error?: typeof NoResultError
): Promise>
```
Gets a record by its global ID, throwing an error if not found.
### findByGlobalIds()
```typescript
findByGlobalIds(
globalIds: string[]
): Promise>>
```
Finds multiple records by their global IDs.
## Utility Functions
The mixin also exports these standalone utility functions:
### decodeFromGlobalId()
```typescript
function decodeFromGlobalId(
globalId: string,
parse: (id: string) => TIdType
): { type: string; id: TIdType }
```
Decodes a global ID into its type and ID components.
### decodeTypeFromGlobalId()
```typescript
function decodeTypeFromGlobalId(globalId: string): string | null
```
Extracts just the type portion from a global ID.
## Examples
### GraphQL Integration
```typescript
import { GraphQLID } from 'graphql';
const UserType = new GraphQLObjectType({
name: 'User',
fields: {
id: {
type: GraphQLID,
resolve: (user) => UserWithGlobalId.getGlobalId(user.id)
},
// other fields...
}
});
// In a resolver
const resolvers = {
Query: {
user: async (_, { id }) => {
return UserWithGlobalId.findByGlobalId(id);
}
}
};
```
### Batch Loading
```typescript
// Load multiple users by global IDs
const users = await UserWithGlobalId.findByGlobalIds([
'User_1',
'User_2',
'User_3'
]);
// Map results by ID for quick lookup
const userMap = users.reduce((map, user) => {
const globalId = UserWithGlobalId.getGlobalId(user.id);
map[globalId] = user;
return map;
}, {});
```
### Error Handling
```typescript
// Get by global ID with custom error
try {
const user = await UserWithGlobalId.getByGlobalId('User_999', CustomError);
} catch (error) {
// Handle custom error
}
```
file: ./src/content/packages/query-builder/mixins/id-generator.mdx
meta: {
"title": "ID Generator",
"description": "Generates unique, prefixed IDs for database records"
}
The `withIdGenerator` mixin adds capabilities for generating unique, prefixed IDs for your database records. This is especially useful for systems that need human-readable IDs or where exposing sequential numeric IDs is not desirable.
## Usage
```typescript
import { withIdGenerator } from '@doubletie/query-builder/mixins';
import { createDatabase, createModel } from '@doubletie/query-builder';
// Create a database and model
const db = createDatabase(/* config */);
const UserModel = createModel(db, 'users', 'id');
// Apply the ID generator mixin with a prefix
const UserWithIds = withIdGenerator(UserModel, {
prefix: 'user',
autoGenerate: true
});
// Generate a unique ID
const uniqueId = UserWithIds.generateId();
console.log(uniqueId); // "user_az7GpL9..."
// Insert with auto-generated ID
const user = await UserWithIds.insertInto()
.values({
name: 'Jane Doe',
email: 'jane@example.com'
// id will be automatically generated
})
.returningAll()
.executeTakeFirstOrThrow();
console.log(user.id); // "user_bK3mP8q..."
// Insert with explicitly generated ID
const explicitUser = await UserWithIds.insertWithGeneratedId({
name: 'John Smith',
email: 'john@example.com'
});
```
## API Reference
### withIdGenerator()
```typescript
function withIdGenerator<
TDatabase extends Record = any,
TTableName extends keyof TDatabase & string = string,
TIdField extends keyof TDatabase[TTableName] & string = string
>(
model: ModelFunctions,
options: IdGeneratorOptions = { prefix: 'id' }
): ModelFunctions & IdGeneratorMethods
```
Enhances a model with ID generation capabilities.
#### Parameters
* `model` - The base model to enhance
* `options` - Configuration options for ID generation
### IdGeneratorOptions
```typescript
interface IdGeneratorOptions {
/**
* The prefix to use for generated IDs
*/
prefix: string;
/**
* The column name to use for the ID (defaults to model's primary key)
*/
idColumn?: string;
/**
* Whether to automatically generate IDs during insert operations
*/
autoGenerate?: boolean;
}
```
## Added Methods
### generateId()
```typescript
generateId(): string
```
Generates a unique ID with the configured prefix.
### insertWithGeneratedId()
```typescript
insertWithGeneratedId(
data: Omit, TIdField>
): Promise>
```
Inserts a record with an automatically generated ID.
### isGeneratedId()
```typescript
isGeneratedId(id: string): boolean
```
Checks if an ID matches the pattern of IDs generated by this mixin.
## Features
* **Customizable Prefixes**: Generate IDs with meaningful prefixes (e.g., `user_`, `post_`)
* **Automatic Generation**: Optionally auto-generate IDs for all insert operations
* **Custom ID Column**: Specify which column should contain the generated IDs
* **Unique and Random**: Generated IDs are unique and use a cryptographically strong random base
## Examples
### Configuration Options
```typescript
// Basic configuration
const ProductModel = withIdGenerator(baseModel, {
prefix: 'prod'
});
// Advanced configuration
const OrderModel = withIdGenerator(baseModel, {
prefix: 'order',
idColumn: 'orderId',
autoGenerate: true
});
```
### Working with Generated IDs
```typescript
// Generate multiple IDs
const ids = Array.from({ length: 5 }, () => UserWithIds.generateId());
// Check if an ID was generated by this mixin
const isGenerated = UserWithIds.isGeneratedId('user_abc123');
console.log(isGenerated); // true or false
```
### Batch Insert with Auto-Generated IDs
```typescript
const products = await ProductModel.insertInto()
.values([
{ name: 'Product 1', price: 29.99 },
{ name: 'Product 2', price: 49.99 },
{ name: 'Product 3', price: 19.99 }
])
.returningAll()
.execute();
// Each product gets a unique ID
products.forEach(product => {
console.log(`${product.name}: ${product.id}`);
});
```
### Combining with Other Mixins
```typescript
import { withIdGenerator, withUpdatedAt } from '@doubletie/query-builder/mixins';
// Apply multiple mixins
const ProductWithFeatures = withUpdatedAt(
withIdGenerator(ProductModel, { prefix: 'prod' }),
'updatedAt'
);
// Use methods from both mixins
const product = await ProductWithFeatures.insertWithGeneratedId({
name: 'New Product',
price: 99.99
});
console.log(product.id); // Generated ID
console.log(product.updatedAt); // Current timestamp
```
file: ./src/content/packages/query-builder/mixins/index.mdx
meta: {
"title": "Model Mixins",
"description": "Powerful model enhancement system that extends functionality with reusable, composable patterns"
}
Model mixins extend your database models with powerful behaviors and utilities. Built on a functional composition pattern, mixins allow you to easily combine multiple enhancements together to create more specialized models.
## Available Mixins
Double Tie Query Builder provides the following built-in mixins:
| Mixin | Description |
| ---------------------------------------------------------------- | ------------------------------------------------------------- |
| [`withAssignProperties`](/packages/query-builder/mixins/assign) | Creates model instances with data assignment capabilities |
| [`withCreatedAt`](/packages/query-builder/mixins/created-at) | Automatically adds timestamps when records are created |
| [`withUpdatedAt`](/packages/query-builder/mixins/updated-at) | Automatically updates timestamps when records are modified |
| [`withGlobalId`](/packages/query-builder/mixins/global-id) | Adds global ID functionality for GraphQL-friendly identifiers |
| [`withIdGenerator`](/packages/query-builder/mixins/id-generator) | Generates unique IDs with custom prefixes |
| [`withSlug`](/packages/query-builder/mixins/slug) | Creates and manages URL-friendly slugs |
| [`withCursorable`](/packages/query-builder/mixins/cursorable) | Adds cursor-based pagination for efficient list traversal |
## Composing Mixins
Mixins can be combined using functional composition to create models with multiple enhanced behaviors:
```typescript
import { createDatabase, createModel } from '@doubletie/query-builder';
import {
withUpdatedAt,
withSlug,
withIdGenerator
} from '@doubletie/query-builder/mixins';
// Create a base model
const db = createDatabase(/* config */);
const baseModel = createModel(db, 'posts', 'id');
// Apply multiple mixins with composition
const PostModel = withUpdatedAt(
withSlug(
withIdGenerator(baseModel, { prefix: 'post' }),
'slug',
'title'
),
'updatedAt'
);
// Now PostModel has all three behaviors
const post = await PostModel.insertWithGeneratedId({
title: 'Hello World',
content: 'This is my first post'
}); // Has ID, slug, and timestamps
```
## Creating Custom Mixins
You can create your own custom mixins by following the functional enhancement pattern:
```typescript
function withCustomBehavior<
TDatabase,
TTableName extends keyof TDatabase & string,
TIdColumnName extends keyof TDatabase[TTableName] & string
>(model: ModelFunctions) {
return {
...model,
// Add new methods
customMethod() {
// Implementation
},
// Override existing methods
findById: async (id, ...args) => {
console.log(`Finding record with ID: ${id}`);
return model.findById(id, ...args);
}
};
}
```
file: ./src/content/packages/query-builder/mixins/updated-at.mdx
meta: {
"title": "Update At",
"description": "Automatically manages updated_at timestamps when records are modified"
}
The `withUpdatedAt` mixin automatically updates timestamps when records are modified in the database. This ensures you always know when a record was last changed without manually setting the timestamp on every update.
## Usage
```typescript
import { withUpdatedAt } from '@doubletie/query-builder/mixins';
import { createDatabase, createModel } from '@doubletie/query-builder';
// Create a database and model
const db = createDatabase(/* config */);
const UserModel = createModel(db, 'users', 'id');
// Apply the updated_at mixin
const UserWithTimestamp = withUpdatedAt(UserModel, 'updatedAt');
// Update a record
const updatedUser = await UserWithTimestamp.updateTable()
.where('id', '=', 1)
.set({
name: 'Updated Name'
// updatedAt is automatically added
})
.returningAll()
.executeTakeFirstOrThrow();
console.log(updatedUser.updatedAt); // Current timestamp
```
## API Reference
### withUpdatedAt()
```typescript
function withUpdatedAt<
TDatabase,
TTableName extends keyof TDatabase & string,
TIdColumnName extends keyof TDatabase[TTableName] & string
>(
model: ModelFunctions,
field: keyof TDatabase[TTableName] & string
): ModelWithUpdateById
```
Enhances a model with automatic updatedAt timestamp functionality and adds a convenient `updateById` method.
#### Parameters
* `model` - The base model to enhance
* `field` - The name of the field to use for update timestamps (e.g., 'updatedAt')
#### Returns
Enhanced model that automatically sets the update timestamp field during update operations and provides an updateById convenience method.
## Features
* **Automatic Timestamps**: Sets the timestamp field to current date/time on every update
* **Intercepts Set Method**: Adds the timestamp to all update operations
* **Direct Update Method**: Provides a convenient `updateById` method
* **SQLite Compatibility**: Formats dates appropriately for SQLite compatibility
## Additional Methods
### updateById()
```typescript
updateById(
id: Readonly>,
column: TColumnName,
value: TDatabase[TTableName][TColumnName]
): Promise>
```
A convenience method that updates a single column value by ID and automatically updates the timestamp.
#### Parameters
* `id` - The ID of the record to update
* `column` - The column to update
* `value` - The new value for the column
#### Returns
The updated record with the new value and updated timestamp.
## Example: Using updateById
```typescript
// Simple update using updateById
const user = await UserWithTimestamp.updateById(
1, // ID of the user to update
'status', // Column to update
'inactive' // New value
);
console.log(user.status); // 'inactive'
console.log(user.updatedAt); // Current timestamp
```
## Example: Using With Transactions
```typescript
await db.transaction(async ({ transaction }) => {
// Updates within a transaction still automatically set updatedAt
const user = await UserWithTimestamp.updateTable()
.where('id', '=', 1)
.set({
name: 'Transaction Update',
score: 42
})
.returningAll()
.executeTakeFirstOrThrow();
console.log(user.updatedAt); // Current timestamp
});
```
## Combining with Other Mixins
```typescript
import { withUpdatedAt, withGlobalId } from '@doubletie/query-builder/mixins';
// Apply multiple mixins
const UserWithFeatures = withUpdatedAt(
withGlobalId(UserModel, 'id', 'User'),
'updatedAt'
);
// Use global ID to find and update a record
const globalId = 'User_123';
const localId = UserWithFeatures.getLocalId(globalId);
const user = await UserWithFeatures.updateById(
localId,
'name',
'Updated via Global ID'
);
```
file: ./src/content/packages/query-builder/mixins/with-slug.mdx
meta: {
"title": "Slug",
"description": "Creates and manages URL-friendly slugs for database records"
}
The `withSlug` mixin adds slug generation and management capabilities to your models. Slugs are URL-friendly string identifiers, commonly used in content management systems and blogs to create readable, SEO-friendly URLs.
## Usage
```typescript
import { withSlug } from '@doubletie/query-builder/mixins';
import { createDatabase, createModel } from '@doubletie/query-builder';
// Create a database and model
const db = createDatabase(/* config */);
const ArticleModel = createModel(db, 'articles', 'id');
// Simple usage with default options
const ArticleWithSlug = withSlug(ArticleModel, 'slug', 'title');
// Create an article with automatic slug generation
const article = await ArticleWithSlug.insertWithSlug({
title: 'My First Article',
content: 'This is the content of my first article.'
});
console.log(article.slug); // "my-first-article"
// Find an article by its slug
const foundArticle = await ArticleWithSlug.findBySlug('my-first-article');
```
## API Reference
### withSlug()
```typescript
function withSlug<
TDatabase,
TTableName extends keyof TDatabase & string,
TIdColumnName extends keyof TDatabase[TTableName] & string
>(
model: ModelFunctions,
slugField?: keyof TDatabase[TTableName] & string,
sourceField?: keyof TDatabase[TTableName] & string
): SlugModelType |
((options: Options) =>
SlugModelType)
```
Enhances a model with slug generation and management capabilities.
#### Basic Parameters (Simple Form)
* `model` - The base model to enhance
* `slugField` - Field to store the slug (e.g., 'slug')
* `sourceField` - Source field to generate slug from (e.g., 'title')
#### Advanced Options (Configuration Form)
```typescript
type Options = {
/** Field to store the slug */
field: keyof TDatabase[TTableName] & string;
/** Source fields to generate slug from */
sources: Array;
/** Operation to apply to the slug */
operation?: Operation;
/** Additional slug options */
slugOptions?: SlugOptions;
};
type SlugOptions = {
/** Separator to use between words (default: '-') */
separator?: string;
/** Maximum length to truncate slugs */
truncate?: number;
/** Dictionary of words to replace */
dictionary?: Record;
};
```
## Added Methods
### findBySlug()
```typescript
findBySlug(
value: string,
column?: keyof TDatabase[TTableName] & string
): Promise | undefined>
```
Finds a record by its slug.
### insertWithSlug()
```typescript
insertWithSlug(
values: TableValues
): Promise>
```
Inserts a record with an automatically generated slug.
### insertIfNotExistsWithSlug()
```typescript
insertIfNotExistsWithSlug(
values: TableValues,
uniqueColumn: keyof TDatabase[TTableName] & string
): Promise | undefined>
```
Inserts a record with a slug if it doesn't already exist based on a unique column.
### upsertWithSlug()
```typescript
upsertWithSlug(
criteria: {
column: keyof TDatabase[TTableName] & string;
value: unknown;
},
insertValues: TableValues,
updateValues?: TableValues
): Promise | undefined>
```
Updates an existing record or inserts a new one, generating a slug if needed.
### insertMany()
```typescript
insertMany(
values: Array>
): Promise>>
```
Inserts multiple records with automatically generated slugs.
### generateUniqueSlug()
```typescript
generateUniqueSlug(
values: TableValues
): Promise
```
Generates a unique slug for the given values without inserting a record.
## Features
* **Automatic Slug Generation**: Create URL-friendly slugs from title/name fields
* **Customizable Formatting**: Control separator, length, and word replacement
* **Uniqueness Handling**: Automatically adds numerical suffixes to ensure uniqueness
* **Multiple Source Fields**: Generate slugs from multiple fields (e.g., combining name and category)
* **Batch Operations**: Support for inserting multiple records with unique slugs
## Examples
### Basic Slug Generation
```typescript
// Create a post with a slug
const post = await PostWithSlug.insertWithSlug({
title: 'Hello World!',
content: 'This is my first post.'
});
console.log(post.slug); // "hello-world"
```
### Advanced Configuration
```typescript
// Advanced configuration with custom options
const PostWithCustomSlug = withSlug(PostModel)({
field: 'slug',
sources: ['title', 'category'],
slugOptions: {
separator: '_',
truncate: 50,
dictionary: {
'javascript': 'js',
'typescript': 'ts'
}
}
});
// Create a post with custom slug generation
const post = await PostWithCustomSlug.insertWithSlug({
title: 'Getting Started with JavaScript',
category: 'Programming Tutorials'
});
console.log(post.slug); // "getting_started_with_js_programming_tutorials"
```
### Handling Duplicate Slugs
```typescript
// First post with a title
const post1 = await PostWithSlug.insertWithSlug({
title: 'My Post Title'
});
// Second post with the same title
const post2 = await PostWithSlug.insertWithSlug({
title: 'My Post Title'
});
console.log(post1.slug); // "my-post-title"
console.log(post2.slug); // "my-post-title-2"
```
### Using with Other Mixins
```typescript
import { withSlug, withCreatedAt, withUpdatedAt } from '@doubletie/query-builder/mixins';
// Apply multiple mixins
const PostWithFeatures = withUpdatedAt(
withCreatedAt(
withSlug(PostModel, 'slug', 'title'),
'createdAt'
),
'updatedAt'
);
// Create a post with all features
const post = await PostWithFeatures.insertWithSlug({
title: 'Featured Post',
content: 'Content with all the features.'
});
console.log(post.slug); // "featured-post"
console.log(post.createdAt); // Current timestamp
console.log(post.updatedAt); // Current timestamp
```