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 ```