UNPKG

@starbemtech/star-db-query-builder

Version:

A query builder to be used with mysql or postgres

782 lines (707 loc) 17.9 kB
# joins Executes queries with JOIN operations to combine data from multiple tables. ## Signature ```typescript joins<T>({ tableName: string, dbClient: IDatabaseClient, select: string[], joins: JoinClause[], where?: Conditions<T>, groupBy?: string[], orderBy?: OrderBy, limit?: number, offset?: number, unaccent?: boolean }): Promise<T[]> ``` ## Parameters | Parameter | Type | Required | Description | | ----------- | ----------------- | -------- | -------------------------------------------------- | | `tableName` | `string` | ✅ | Name of the main database table | | `dbClient` | `IDatabaseClient` | ✅ | Database client instance | | `select` | `string[]` | ✅ | Array of field names to select (must be specified) | | `joins` | `JoinClause[]` | ✅ | Array of JOIN clauses | | `where` | `Conditions<T>` | ❌ | Conditions to filter records | | `groupBy` | `string[]` | ❌ | Fields to group by | | `orderBy` | `OrderBy` | ❌ | Sort order specification | | `limit` | `number` | ❌ | Maximum number of records to return | | `offset` | `number` | ❌ | Number of records to skip | | `unaccent` | `boolean` | ❌ | Enable unaccent search for PostgreSQL | ## JoinClause Interface ```typescript interface JoinClause { type: 'INNER' | 'LEFT' | 'RIGHT' | 'FULL' table: string on: string } ``` ## Return Value - **Type**: `Promise<T[]>` - **Description**: Returns an array of records from the joined tables ## Examples ### Basic JOIN ```typescript import { joins } from '@starbemtech/star-db-query-builder' // Get users with their orders const usersWithOrders = await joins({ tableName: 'users', dbClient, select: [ 'users.id', 'users.name', 'users.email', 'orders.total', 'orders.created_at', ], joins: [ { type: 'LEFT', table: 'orders', on: 'users.id = orders.user_id', }, ], where: { 'users.status': { operator: '=', value: 'active' }, }, }) console.log(usersWithOrders) // [ // { // id: 'user-1', // name: 'John Doe', // email: 'john@example.com', // total: 150.00, // created_at: '2023-12-01T10:00:00.000Z' // }, // // ... more results // ] ``` ### Multiple JOINs ```typescript // Complex report with multiple tables const report = await joins({ tableName: 'users', dbClient, select: [ 'users.name', 'users.email', 'COUNT(orders.id) as order_count', 'SUM(orders.total) as total_spent', 'plans.name as plan_name', 'plans.price as plan_price', ], joins: [ { type: 'LEFT', table: 'orders', on: 'users.id = orders.user_id', }, { type: 'LEFT', table: 'user_plans', on: 'users.id = user_plans.user_id', }, { type: 'LEFT', table: 'plans', on: 'user_plans.plan_id = plans.id', }, ], where: { 'users.status': { operator: '=', value: 'active' }, }, groupBy: [ 'users.id', 'users.name', 'users.email', 'plans.name', 'plans.price', ], orderBy: [{ field: 'total_spent', direction: 'DESC' }], }) ``` ### Different JOIN Types ```typescript // INNER JOIN - only users with orders const usersWithOrders = await joins({ tableName: 'users', dbClient, select: ['users.name', 'orders.total'], joins: [ { type: 'INNER', table: 'orders', on: 'users.id = orders.user_id', }, ], }) // LEFT JOIN - all users, with or without orders const allUsersWithOrders = await joins({ tableName: 'users', dbClient, select: ['users.name', 'orders.total'], joins: [ { type: 'LEFT', table: 'orders', on: 'users.id = orders.user_id', }, ], }) // RIGHT JOIN - all orders, with or without users const ordersWithUsers = await joins({ tableName: 'users', dbClient, select: ['users.name', 'orders.total'], joins: [ { type: 'RIGHT', table: 'orders', on: 'users.id = orders.user_id', }, ], }) // FULL OUTER JOIN - all users and all orders const allData = await joins({ tableName: 'users', dbClient, select: ['users.name', 'orders.total'], joins: [ { type: 'FULL', table: 'orders', on: 'users.id = orders.user_id', }, ], }) ``` ### Complex WHERE Conditions with JOINs ```typescript // Users with orders from last month const recentUsers = await joins({ tableName: 'users', dbClient, select: ['users.name', 'users.email', 'orders.total', 'orders.created_at'], joins: [ { type: 'INNER', table: 'orders', on: 'users.id = orders.user_id', }, ], where: { AND: [ { 'users.status': { operator: '=', value: 'active' } }, { 'orders.created_at': { operator: '>=', value: new Date('2023-11-01') }, }, { 'orders.total': { operator: '>', value: 100 } }, ], }, orderBy: [{ field: 'orders.created_at', direction: 'DESC' }], }) ``` ### Aggregation with JOINs ```typescript // User statistics with order data const userStats = await joins({ tableName: 'users', dbClient, select: [ 'users.id', 'users.name', 'COUNT(orders.id) as order_count', 'SUM(orders.total) as total_spent', 'AVG(orders.total) as avg_order_value', 'MAX(orders.created_at) as last_order_date', ], joins: [ { type: 'LEFT', table: 'orders', on: 'users.id = orders.user_id', }, ], where: { 'users.created_at': { operator: '>=', value: new Date('2023-01-01') }, }, groupBy: ['users.id', 'users.name'], having: { 'COUNT(orders.id)': { operator: '>', value: 0 }, }, orderBy: [{ field: 'total_spent', direction: 'DESC' }], }) ``` ### Pagination with JOINs ```typescript // Paginated user orders const paginatedOrders = await joins({ tableName: 'users', dbClient, select: [ 'users.name', 'users.email', 'orders.id', 'orders.total', 'orders.status', 'orders.created_at', ], joins: [ { type: 'INNER', table: 'orders', on: 'users.id = orders.user_id', }, ], where: { 'orders.status': { operator: '=', value: 'completed' }, }, limit: 20, offset: 0, orderBy: [{ field: 'orders.created_at', direction: 'DESC' }], }) ``` ### TypeScript Usage ```typescript interface UserWithOrder { user_id: string user_name: string user_email: string order_id: string order_total: number order_status: string order_created_at: Date } // Typed usage const usersWithOrders: UserWithOrder[] = await joins<UserWithOrder>({ tableName: 'users', dbClient, select: [ 'users.id as user_id', 'users.name as user_name', 'users.email as user_email', 'orders.id as order_id', 'orders.total as order_total', 'orders.status as order_status', 'orders.created_at as order_created_at', ], joins: [ { type: 'LEFT', table: 'orders', on: 'users.id = orders.user_id', }, ], where: { 'users.status': { operator: '=', value: 'active' }, }, }) ``` ### Error Handling ```typescript try { const result = await joins({ tableName: 'users', dbClient, select: ['users.name', 'orders.total'], joins: [ { type: 'LEFT', table: 'orders', on: 'users.id = orders.user_id', }, ], }) console.log(`Found ${result.length} records`) } catch (error) { console.error('Join query error:', error.message) // Handle error appropriately } ``` ## Generated SQL Examples ### Simple LEFT JOIN ```sql SELECT users.id, users.name, users.email, orders.total, orders.created_at FROM users LEFT JOIN orders ON users.id = orders.user_id WHERE users.status = $1 ``` ### Multiple JOINs with GROUP BY ```sql SELECT users.name, users.email, COUNT(orders.id) as order_count, SUM(orders.total) as total_spent, plans.name as plan_name FROM users LEFT JOIN orders ON users.id = orders.user_id LEFT JOIN user_plans ON users.id = user_plans.user_id LEFT JOIN plans ON user_plans.plan_id = plans.id WHERE users.status = $1 GROUP BY users.id, users.name, users.email, plans.name ORDER BY total_spent DESC ``` ### Complex WHERE with JOINs ```sql SELECT users.name, users.email, orders.total, orders.created_at FROM users INNER JOIN orders ON users.id = orders.user_id WHERE (users.status = $1 AND orders.created_at >= $2 AND orders.total > $3) ORDER BY orders.created_at DESC ``` ## Best Practices ### 1. Always Specify SELECT Fields ```typescript // Good: Explicit field selection const result = await joins({ tableName: 'users', dbClient, select: ['users.id', 'users.name', 'orders.total'], joins: [ { type: 'LEFT', table: 'orders', on: 'users.id = orders.user_id', }, ], }) // Avoid: Not specifying select fields const result = await joins({ tableName: 'users', dbClient, select: [], // This will cause issues joins: [ { type: 'LEFT', table: 'orders', on: 'users.id = orders.user_id', }, ], }) ``` ### 2. Use Table Aliases for Clarity ```typescript // Good: Use table prefixes for clarity const result = await joins({ tableName: 'users', dbClient, select: [ 'users.id as user_id', 'users.name as user_name', 'orders.id as order_id', 'orders.total as order_total', ], joins: [ { type: 'LEFT', table: 'orders', on: 'users.id = orders.user_id', }, ], }) ``` ### 3. Choose Appropriate JOIN Types ```typescript // Use INNER JOIN when you need matching records from both tables const usersWithOrders = await joins({ tableName: 'users', dbClient, select: ['users.name', 'orders.total'], joins: [ { type: 'INNER', // Only users who have orders table: 'orders', on: 'users.id = orders.user_id', }, ], }) // Use LEFT JOIN when you want all records from the main table const allUsersWithOrders = await joins({ tableName: 'users', dbClient, select: ['users.name', 'orders.total'], joins: [ { type: 'LEFT', // All users, even those without orders table: 'orders', on: 'users.id = orders.user_id', }, ], }) ``` ### 4. Use Proper Indexing ```sql -- Ensure proper indexes exist for JOIN conditions CREATE INDEX idx_orders_user_id ON orders(user_id); CREATE INDEX idx_user_plans_user_id ON user_plans(user_id); CREATE INDEX idx_user_plans_plan_id ON user_plans(plan_id); ``` ### 5. Handle NULL Values in JOINs ```typescript // Handle NULL values from LEFT JOINs const result = await joins({ tableName: 'users', dbClient, select: [ 'users.name', 'COALESCE(orders.total, 0) as total_spent', 'CASE WHEN orders.id IS NULL THEN 0 ELSE 1 END as has_orders', ], joins: [ { type: 'LEFT', table: 'orders', on: 'users.id = orders.user_id', }, ], }) ``` ## Common Use Cases ### 1. User Dashboard Data ```typescript const getUserDashboardData = async (userId: string) => { return joins({ tableName: 'users', dbClient, select: [ 'users.name', 'users.email', 'COUNT(orders.id) as total_orders', 'SUM(orders.total) as total_spent', 'plans.name as current_plan', 'plans.price as plan_price', ], joins: [ { type: 'LEFT', table: 'orders', on: 'users.id = orders.user_id', }, { type: 'LEFT', table: 'user_plans', on: 'users.id = user_plans.user_id AND user_plans.is_active = true', }, { type: 'LEFT', table: 'plans', on: 'user_plans.plan_id = plans.id', }, ], where: { 'users.id': { operator: '=', value: userId }, }, groupBy: [ 'users.id', 'users.name', 'users.email', 'plans.name', 'plans.price', ], }) } ``` ### 2. Sales Report ```typescript const getSalesReport = async (startDate: Date, endDate: Date) => { return joins({ tableName: 'orders', dbClient, select: [ 'users.name as customer_name', 'users.email as customer_email', 'orders.id as order_id', 'orders.total as order_total', 'orders.status as order_status', 'products.name as product_name', 'order_items.quantity', 'order_items.price as item_price', ], joins: [ { type: 'INNER', table: 'users', on: 'orders.user_id = users.id', }, { type: 'INNER', table: 'order_items', on: 'orders.id = order_items.order_id', }, { type: 'INNER', table: 'products', on: 'order_items.product_id = products.id', }, ], where: { AND: [ { 'orders.created_at': { operator: '>=', value: startDate } }, { 'orders.created_at': { operator: '<=', value: endDate } }, { 'orders.status': { operator: '=', value: 'completed' } }, ], }, orderBy: [{ field: 'orders.created_at', direction: 'DESC' }], }) } ``` ### 3. Product Analytics ```typescript const getProductAnalytics = async () => { return joins({ tableName: 'products', dbClient, select: [ 'products.name as product_name', 'categories.name as category_name', 'COUNT(order_items.id) as times_ordered', 'SUM(order_items.quantity) as total_quantity_sold', 'SUM(order_items.price * order_items.quantity) as total_revenue', 'AVG(order_items.price) as avg_price', ], joins: [ { type: 'LEFT', table: 'categories', on: 'products.category_id = categories.id', }, { type: 'LEFT', table: 'order_items', on: 'products.id = order_items.product_id', }, { type: 'LEFT', table: 'orders', on: 'order_items.order_id = orders.id AND orders.status = "completed"', }, ], groupBy: ['products.id', 'products.name', 'categories.name'], having: { 'COUNT(order_items.id)': { operator: '>', value: 0 }, }, orderBy: [{ field: 'total_revenue', direction: 'DESC' }], }) } ``` ### 4. User Activity Feed ```typescript const getUserActivityFeed = async (userId: string, limit: number = 50) => { return joins({ tableName: 'users', dbClient, select: [ 'activity_logs.action', 'activity_logs.description', 'activity_logs.created_at', 'orders.id as order_id', 'orders.total as order_total', 'products.name as product_name', ], joins: [ { type: 'LEFT', table: 'activity_logs', on: 'users.id = activity_logs.user_id', }, { type: 'LEFT', table: 'orders', on: 'activity_logs.entity_id = orders.id AND activity_logs.entity_type = "order"', }, { type: 'LEFT', table: 'order_items', on: 'orders.id = order_items.order_id', }, { type: 'LEFT', table: 'products', on: 'order_items.product_id = products.id', }, ], where: { 'users.id': { operator: '=', value: userId }, }, limit, orderBy: [{ field: 'activity_logs.created_at', direction: 'DESC' }], }) } ``` ## Performance Considerations ### 1. Index Strategy for JOINs ```sql -- Create indexes on JOIN columns CREATE INDEX idx_orders_user_id ON orders(user_id); CREATE INDEX idx_order_items_order_id ON order_items(order_id); CREATE INDEX idx_order_items_product_id ON order_items(product_id); CREATE INDEX idx_user_plans_user_id ON user_plans(user_id); CREATE INDEX idx_user_plans_plan_id ON user_plans(plan_id); -- Composite indexes for common query patterns CREATE INDEX idx_orders_user_status_created ON orders(user_id, status, created_at); CREATE INDEX idx_order_items_order_product ON order_items(order_id, product_id); ``` ### 2. Query Optimization ```typescript // Good: Use indexed fields in WHERE clauses const result = await joins({ tableName: 'users', dbClient, select: ['users.name', 'orders.total'], joins: [ { type: 'INNER', table: 'orders', on: 'users.id = orders.user_id', }, ], where: { 'users.status': { operator: '=', value: 'active' }, // Indexed field 'orders.created_at': { operator: '>=', value: new Date('2023-01-01') }, // Indexed field }, }) // Avoid: Using non-indexed fields in WHERE clauses const result = await joins({ tableName: 'users', dbClient, select: ['users.name', 'orders.total'], joins: [ { type: 'INNER', table: 'orders', on: 'users.id = orders.user_id', }, ], where: { 'users.bio': { operator: 'LIKE', value: '%developer%' }, // Non-indexed field }, }) ``` ### 3. Limit Result Sets ```typescript // Always use LIMIT for large result sets const result = await joins({ tableName: 'users', dbClient, select: ['users.name', 'orders.total'], joins: [ { type: 'LEFT', table: 'orders', on: 'users.id = orders.user_id', }, ], limit: 1000, // Prevent memory issues orderBy: [{ field: 'users.created_at', direction: 'DESC' }], }) ``` ## Error Messages Common error messages you might encounter: - `Table name is required` - The `tableName` parameter is missing - `DB client is required` - The `dbClient` parameter is missing - `column "field_name" does not exist` - Invalid field name in SELECT or WHERE clause - `relation "table_name" does not exist` - Invalid table name in JOIN clause - `syntax error at or near "JOIN"` - Invalid JOIN syntax - Database-specific errors from the underlying database driver