Master PostgreSQL integration with Node.js. Learn connection pooling, complex queries, transactions, and full-text search capabilities.
Master PostgreSQL integration with Node.js. Learn connection pooling, complex queries, transactions, and full-text search capabilities.
Set up PostgreSQL with Node.js using node-postgres (pg)
Implement connection pooling for optimal performance
Master complex SQL queries, joins, and subqueries
Understand ACID transactions and concurrent operations
Create stored procedures and database functions
Implement full-text search and advanced indexing
PostgreSQL is a powerful, open-source relational database with advanced features like JSONB support, full-text search, and extensibility.
Learn about Installation & Dependencies.
1# Install PostgreSQL (macOS)
2brew install postgresql@15
3brew services start postgresql@15
4
5# Create database and user
6createdb myapp_development
7createuser myapp_user --createdb --login
8
9# Install Node.js dependencies
10npm install pg dotenv bcryptjs
11npm install --save-dev @types/pg nodemon
12
13# Optional: Install query builder
14npm install knex # or prisma, sequelize1const { Pool, Client } = require('pg');
2require('dotenv').config();
3
4class PostgreSQLConnection {
5 constructor() {
6 this.pool = null;
7 this.isConnected = false;
8 this.config = {
9 host: process.env.DB_HOST || 'localhost',
10 port: process.env.DB_PORT || 5432,
11 database: process.env.DB_NAME || 'myapp_development',
12 user: process.env.DB_USER || 'myapp_user',
13 password: process.env.DB_PASSWORD || '',
14
15 // Connection pool settings
16 max: 20, // Maximum number of clients in the pool
17 min: 2, // Minimum number of clients in the pool
18 idleTimeoutMillis: 30000, // Close idle clients after 30 seconds
19 connectionTimeoutMillis: 2000, // Return error after 2 seconds if connection could not be established
20 maxUses: 7500, // Close (and replace) a connection after it has been used this many times
21
22 // SSL configuration for production
23 ssl: process.env.NODE_ENV === 'production' ? {
24 rejectUnauthorized: false // Set to true in production with proper certificates
25 } : false,
26
27 // Statement timeout
28 statement_timeout: 30000, // 30 seconds
29 query_timeout: 30000,
30
31 // Application name for monitoring
32 application_name: 'nodejs_app'
33 };
34 }
35
36 async initialize() {
37 try {
38 // Create connection pool
39 this.pool = new Pool(this.config);
40
41 // Test connection
42 const client = await this.pool.connect();
43 const result = await client.query('SELECT NOW() as current_time, version() as version');
44 client.release();
45
46 console.log('✅ PostgreSQL connected successfully');
47 console.log(\`📊 Database: \${this.config.database}\`);
48 console.log(\`🕐 Server time: \${result.rows[0].current_time}\`);
49 console.log(\`📝 Version: \${result.rows[0].version.split(' ')[0]} \${result.rows[0].version.split(' ')[1]}\`);
50
51 this.isConnected = true;
52
53 // Handle pool events
54 this.pool.on('connect', (client) => {
55 console.log(\`🔗 New client connected (PID: \${client.processID})\`);
56 });
57
58 this.pool.on('error', (err, client) => {
59 console.error('❌ Unexpected error on idle client', err);
60 this.isConnected = false;
61 });
62
63 this.pool.on('remove', (client) => {
64 console.log(\`🔌 Client removed (PID: \${client.processID})\`);
65 });
66
67 // Graceful shutdown
68 process.on('SIGINT', async () => {
69 await this.close();
70 process.exit(0);
71 });
72
73 process.on('SIGTERM', async () => {
74 await this.close();
75 process.exit(0);
76 });
77
78 } catch (error) {
79 console.error('❌ PostgreSQL connection failed:', error.message);
80 throw error;
81 }
82 }
83
84 // Get a client from the pool
85 async getClient() {
86 if (!this.pool) {
87 throw new Error('Database not initialized. Call initialize() first.');
88 }
89 return await this.pool.connect();
90 }
91
92 // Execute a simple query
93 async query(text, params = []) {
94 const start = Date.now();
95 try {
96 const result = await this.pool.query(text, params);
97 const duration = Date.now() - start;
98
99 // Log slow queries (> 100ms)
100 if (duration > 100) {
101 console.warn(\`🐌 Slow query (\${duration}ms): \${text.substring(0, 100)}...\`);
102 }
103
104 return result;
105 } catch (error) {
106 console.error('❌ Query error:', error.message);
107 console.error('📝 Query:', text);
108 console.error('📋 Params:', params);
109 throw error;
110 }
111 }
112
113 // Execute query with transaction
114 async transaction(callback) {
115 const client = await this.getClient();
116
117 try {
118 await client.query('BEGIN');
119 console.log('🔄 Transaction started');
120
121 const result = await callback(client);
122
123 await client.query('COMMIT');
124 console.log('✅ Transaction committed');
125
126 return result;
127 } catch (error) {
128 await client.query('ROLLBACK');
129 console.log('🔄 Transaction rolled back');
130 throw error;
131 } finally {
132 client.release();
133 }
134 }
135
136 // Get pool status
137 getPoolStatus() {
138 if (!this.pool) return { status: 'not_initialized' };
139
140 return {
141 totalCount: this.pool.totalCount,
142 idleCount: this.pool.idleCount,
143 waitingCount: this.pool.waitingCount,
144 maxConnections: this.config.max,
145 isConnected: this.isConnected
146 };
147 }
148
149 // Close all connections
150 async close() {
151 if (this.pool) {
152 await this.pool.end();
153 console.log('🔌 PostgreSQL connection pool closed');
154 this.isConnected = false;
155 }
156 }
157
158 // Health check
159 async healthCheck() {
160 try {
161 const result = await this.query('SELECT 1 as health_check');
162 return {
163 status: 'healthy',
164 timestamp: new Date(),
165 pool: this.getPoolStatus()
166 };
167 } catch (error) {
168 return {
169 status: 'unhealthy',
170 error: error.message,
171 timestamp: new Date()
172 };
173 }
174 }
175}
176
177module.exports = new PostgreSQLConnection();Design a comprehensive database schema with proper relationships, constraints, and indexes.
1-- Enable extensions
2CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
3CREATE EXTENSION IF NOT EXISTS "pgcrypto";
4CREATE EXTENSION IF NOT EXISTS "pg_trgm"; -- For similarity searches
5
6-- Create custom types
7CREATE TYPE user_role AS ENUM ('admin', 'user', 'moderator', 'premium');
8CREATE TYPE order_status AS ENUM ('pending', 'processing', 'shipped', 'delivered', 'cancelled');
9CREATE TYPE payment_status AS ENUM ('pending', 'completed', 'failed', 'refunded');
10
11-- Users table with comprehensive fields
12CREATE TABLE users (
13 id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
14 email VARCHAR(255) UNIQUE NOT NULL,
15 password_hash VARCHAR(255) NOT NULL,
16 first_name VARCHAR(100) NOT NULL,
17 last_name VARCHAR(100) NOT NULL,
18 phone VARCHAR(20),
19 date_of_birth DATE,
20 role user_role DEFAULT 'user',
21 is_active BOOLEAN DEFAULT true,
22 is_email_verified BOOLEAN DEFAULT false,
23 avatar_url TEXT,
24
25 -- Address information (normalized)
26 address_line1 VARCHAR(255),
27 address_line2 VARCHAR(255),
28 city VARCHAR(100),
29 state VARCHAR(100),
30 postal_code VARCHAR(20),
31 country VARCHAR(100) DEFAULT 'United States',
32
33 -- Preferences as JSONB
34 preferences JSONB DEFAULT '{}',
35
36 -- Security fields
37 last_login TIMESTAMPTZ,
38 login_attempts INTEGER DEFAULT 0,
39 locked_until TIMESTAMPTZ,
40 password_reset_token VARCHAR(255),
41 password_reset_expires TIMESTAMPTZ,
42 email_verification_token VARCHAR(255),
43
44 -- Audit fields
45 created_at TIMESTAMPTZ DEFAULT NOW(),
46 updated_at TIMESTAMPTZ DEFAULT NOW(),
47 created_by UUID REFERENCES users(id),
48 updated_by UUID REFERENCES users(id)
49);
50
51-- Categories table with hierarchy support
52CREATE TABLE categories (
53 id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
54 name VARCHAR(255) NOT NULL,
55 slug VARCHAR(255) UNIQUE NOT NULL,
56 description TEXT,
57 parent_id UUID REFERENCES categories(id),
58 image_url TEXT,
59 is_active BOOLEAN DEFAULT true,
60 sort_order INTEGER DEFAULT 0,
61
62 -- SEO fields
63 meta_title VARCHAR(255),
64 meta_description TEXT,
65
66 -- Hierarchy path for efficient queries
67 path TEXT, -- e.g., '/electronics/computers/laptops/'
68
69 created_at TIMESTAMPTZ DEFAULT NOW(),
70 updated_at TIMESTAMPTZ DEFAULT NOW()
71);
72
73-- Products table with advanced features
74CREATE TABLE products (
75 id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
76 name VARCHAR(255) NOT NULL,
77 slug VARCHAR(255) UNIQUE NOT NULL,
78 description TEXT,
79 short_description VARCHAR(500),
80 sku VARCHAR(100) UNIQUE NOT NULL,
81 category_id UUID NOT NULL REFERENCES categories(id),
82
83 -- Pricing
84 price DECIMAL(10,2) NOT NULL CHECK (price >= 0),
85 compare_at_price DECIMAL(10,2) CHECK (compare_at_price >= price),
86 cost DECIMAL(10,2) CHECK (cost >= 0),
87
88 -- Inventory
89 track_inventory BOOLEAN DEFAULT true,
90 inventory_quantity INTEGER DEFAULT 0 CHECK (inventory_quantity >= 0),
91 allow_backorder BOOLEAN DEFAULT false,
92
93 -- Physical properties
94 weight DECIMAL(8,2), -- in kg
95 dimensions JSONB, -- {length, width, height}
96
97 -- Product attributes as JSONB
98 attributes JSONB DEFAULT '{}',
99
100 -- SEO and marketing
101 meta_title VARCHAR(255),
102 meta_description TEXT,
103 featured_image_url TEXT,
104 gallery_images JSONB DEFAULT '[]',
105
106 -- Status
107 is_active BOOLEAN DEFAULT true,
108 is_featured BOOLEAN DEFAULT false,
109 published_at TIMESTAMPTZ,
110
111 -- Audit
112 created_at TIMESTAMPTZ DEFAULT NOW(),
113 updated_at TIMESTAMPTZ DEFAULT NOW(),
114 created_by UUID REFERENCES users(id),
115 updated_by UUID REFERENCES users(id)
116);
117
118-- Product variants for different options (size, color, etc.)
119CREATE TABLE product_variants (
120 id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
121 product_id UUID NOT NULL REFERENCES products(id) ON DELETE CASCADE,
122 title VARCHAR(255) NOT NULL,
123 sku VARCHAR(100) UNIQUE NOT NULL,
124
125 -- Variant-specific pricing
126 price DECIMAL(10,2) NOT NULL CHECK (price >= 0),
127 compare_at_price DECIMAL(10,2) CHECK (compare_at_price >= price),
128 cost DECIMAL(10,2) CHECK (cost >= 0),
129
130 -- Variant-specific inventory
131 inventory_quantity INTEGER DEFAULT 0 CHECK (inventory_quantity >= 0),
132
133 -- Option values (e.g., {color: 'red', size: 'large'})
134 option_values JSONB NOT NULL DEFAULT '{}',
135
136 -- Physical properties
137 weight DECIMAL(8,2),
138 dimensions JSONB,
139
140 -- Status
141 is_active BOOLEAN DEFAULT true,
142
143 created_at TIMESTAMPTZ DEFAULT NOW(),
144 updated_at TIMESTAMPTZ DEFAULT NOW()
145);
146
147-- Orders table
148CREATE TABLE orders (
149 id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
150 order_number VARCHAR(50) UNIQUE NOT NULL,
151 user_id UUID NOT NULL REFERENCES users(id),
152
153 -- Order totals
154 subtotal DECIMAL(10,2) NOT NULL CHECK (subtotal >= 0),
155 tax_amount DECIMAL(10,2) NOT NULL DEFAULT 0 CHECK (tax_amount >= 0),
156 shipping_amount DECIMAL(10,2) NOT NULL DEFAULT 0 CHECK (shipping_amount >= 0),
157 discount_amount DECIMAL(10,2) NOT NULL DEFAULT 0 CHECK (discount_amount >= 0),
158 total_amount DECIMAL(10,2) NOT NULL CHECK (total_amount >= 0),
159
160 -- Status
161 status order_status DEFAULT 'pending',
162 payment_status payment_status DEFAULT 'pending',
163
164 -- Shipping address
165 shipping_address JSONB NOT NULL,
166 billing_address JSONB,
167
168 -- Tracking
169 tracking_number VARCHAR(255),
170 shipped_at TIMESTAMPTZ,
171 delivered_at TIMESTAMPTZ,
172
173 -- Notes
174 customer_notes TEXT,
175 admin_notes TEXT,
176
177 created_at TIMESTAMPTZ DEFAULT NOW(),
178 updated_at TIMESTAMPTZ DEFAULT NOW()
179);
180
181-- Order items
182CREATE TABLE order_items (
183 id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
184 order_id UUID NOT NULL REFERENCES orders(id) ON DELETE CASCADE,
185 product_id UUID NOT NULL REFERENCES products(id),
186 product_variant_id UUID REFERENCES product_variants(id),
187
188 quantity INTEGER NOT NULL CHECK (quantity > 0),
189 price DECIMAL(10,2) NOT NULL CHECK (price >= 0),
190 total DECIMAL(10,2) NOT NULL CHECK (total >= 0),
191
192 -- Snapshot of product details at time of order
193 product_snapshot JSONB NOT NULL,
194
195 created_at TIMESTAMPTZ DEFAULT NOW()
196);
197
198-- Reviews and ratings
199CREATE TABLE reviews (
200 id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
201 product_id UUID NOT NULL REFERENCES products(id) ON DELETE CASCADE,
202 user_id UUID NOT NULL REFERENCES users(id),
203 order_id UUID REFERENCES orders(id),
204
205 rating INTEGER NOT NULL CHECK (rating >= 1 AND rating <= 5),
206 title VARCHAR(255),
207 content TEXT,
208
209 -- Verification
210 is_verified BOOLEAN DEFAULT false,
211
212 -- Moderation
213 is_approved BOOLEAN DEFAULT true,
214 moderated_by UUID REFERENCES users(id),
215 moderated_at TIMESTAMPTZ,
216
217 -- Helpfulness
218 helpful_count INTEGER DEFAULT 0,
219 total_votes INTEGER DEFAULT 0,
220
221 created_at TIMESTAMPTZ DEFAULT NOW(),
222 updated_at TIMESTAMPTZ DEFAULT NOW(),
223
224 -- Unique constraint to prevent duplicate reviews
225 CONSTRAINT unique_user_product_review UNIQUE (user_id, product_id)
226);
227
228-- Create indexes for performance
229CREATE INDEX idx_users_email ON users(email);
230CREATE INDEX idx_users_role_active ON users(role, is_active);
231CREATE INDEX idx_users_created_at ON users(created_at);
232CREATE INDEX idx_users_last_login ON users(last_login);
233
234CREATE INDEX idx_categories_parent_id ON categories(parent_id);
235CREATE INDEX idx_categories_slug ON categories(slug);
236CREATE INDEX idx_categories_active ON categories(is_active);
237
238CREATE INDEX idx_products_category_id ON products(category_id);
239CREATE INDEX idx_products_slug ON products(slug);
240CREATE INDEX idx_products_sku ON products(sku);
241CREATE INDEX idx_products_active_featured ON products(is_active, is_featured);
242CREATE INDEX idx_products_price ON products(price);
243CREATE INDEX idx_products_published_at ON products(published_at);
244
245-- GIN indexes for JSONB fields
246CREATE INDEX idx_products_attributes ON products USING GIN(attributes);
247CREATE INDEX idx_users_preferences ON users USING GIN(preferences);
248
249-- Full-text search indexes
250CREATE INDEX idx_products_search ON products USING GIN(to_tsvector('english', name || ' ' || COALESCE(description, '')));
251CREATE INDEX idx_users_search ON users USING GIN(to_tsvector('english', first_name || ' ' || last_name || ' ' || email));
252
253CREATE INDEX idx_variants_product_id ON product_variants(product_id);
254CREATE INDEX idx_variants_sku ON product_variants(sku);
255
256CREATE INDEX idx_orders_user_id ON orders(user_id);
257CREATE INDEX idx_orders_status ON orders(status);
258CREATE INDEX idx_orders_created_at ON orders(created_at);
259CREATE INDEX idx_orders_order_number ON orders(order_number);
260
261CREATE INDEX idx_order_items_order_id ON order_items(order_id);
262CREATE INDEX idx_order_items_product_id ON order_items(product_id);
263
264CREATE INDEX idx_reviews_product_id ON reviews(product_id);
265CREATE INDEX idx_reviews_user_id ON reviews(user_id);
266CREATE INDEX idx_reviews_rating ON reviews(rating);
267CREATE INDEX idx_reviews_approved ON reviews(is_approved);
268
269-- Trigger to update updated_at timestamps
270CREATE OR REPLACE FUNCTION update_updated_at_column()
271RETURNS TRIGGER AS $$
272BEGIN
273 NEW.updated_at = NOW();
274 RETURN NEW;
275END;
276$$ language 'plpgsql';
277
278-- Apply trigger to tables
279CREATE TRIGGER update_users_updated_at BEFORE UPDATE ON users FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
280CREATE TRIGGER update_categories_updated_at BEFORE UPDATE ON categories FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
281CREATE TRIGGER update_products_updated_at BEFORE UPDATE ON products FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
282CREATE TRIGGER update_variants_updated_at BEFORE UPDATE ON product_variants FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
283CREATE TRIGGER update_orders_updated_at BEFORE UPDATE ON orders FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
284CREATE TRIGGER update_reviews_updated_at BEFORE UPDATE ON reviews FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();Master complex SQL queries, joins, subqueries, and window functions for sophisticated data retrieval.
1const db = require('../config/database');
2
3class QueryService {
4 // Complex product search with filters and facets
5 async searchProducts(searchParams) {
6 const {
7 query = '',
8 categoryId = null,
9 minPrice = 0,
10 maxPrice = null,
11 inStock = null,
12 rating = null,
13 page = 1,
14 limit = 20,
15 sortBy = 'relevance',
16 sortOrder = 'desc'
17 } = searchParams;
18
19 try {
20 // Build dynamic WHERE conditions
21 const conditions = ['p.is_active = true'];
22 const params = [];
23 let paramIndex = 1;
24
25 // Full-text search
26 if (query) {
27 conditions.push(\`to_tsvector('english', p.name || ' ' || COALESCE(p.description, '')) @@ plainto_tsquery('english', $\${paramIndex})\`);
28 params.push(query);
29 paramIndex++;
30 }
31
32 // Category filter (including subcategories)
33 if (categoryId) {
34 conditions.push(\`(p.category_id = $\${paramIndex} OR p.category_id IN (
35 WITH RECURSIVE subcategories AS (
36 SELECT id FROM categories WHERE id = $\${paramIndex}
37 UNION ALL
38 SELECT c.id FROM categories c
39 INNER JOIN subcategories s ON c.parent_id = s.id
40 )
41 SELECT id FROM subcategories
42 ))\`);
43 params.push(categoryId);
44 paramIndex++;
45 }
46
47 // Price range filter
48 if (minPrice > 0) {
49 conditions.push(\`p.price >= $\${paramIndex}\`);
50 params.push(minPrice);
51 paramIndex++;
52 }
53
54 if (maxPrice) {
55 conditions.push(\`p.price <= $\${paramIndex}\`);
56 params.push(maxPrice);
57 paramIndex++;
58 }
59
60 // Stock filter
61 if (inStock !== null) {
62 if (inStock) {
63 conditions.push(\`(p.track_inventory = false OR p.inventory_quantity > 0)\`);
64 } else {
65 conditions.push(\`(p.track_inventory = true AND p.inventory_quantity = 0)\`);
66 }
67 }
68
69 // Rating filter
70 if (rating) {
71 conditions.push(\`avg_rating.rating >= $\${paramIndex}\`);
72 params.push(rating);
73 paramIndex++;
74 }
75
76 // Build ORDER BY clause
77 let orderBy = '';
78 switch (sortBy) {
79 case 'price':
80 orderBy = \`p.price \${sortOrder}\`;
81 break;
82 case 'name':
83 orderBy = \`p.name \${sortOrder}\`;
84 break;
85 case 'created':
86 orderBy = \`p.created_at \${sortOrder}\`;
87 break;
88 case 'rating':
89 orderBy = \`COALESCE(avg_rating.rating, 0) \${sortOrder}\`;
90 break;
91 case 'relevance':
92 default:
93 if (query) {
94 orderBy = \`ts_rank(to_tsvector('english', p.name || ' ' || COALESCE(p.description, '')), plainto_tsquery('english', $1)) DESC\`;
95 } else {
96 orderBy = 'p.created_at DESC';
97 }
98 break;
99 }
100
101 const offset = (page - 1) * limit;
102
103 const searchQuery = \`
104 WITH product_data AS (
105 SELECT
106 p.*,
107 c.name as category_name,
108 c.slug as category_slug,
109
110 -- Calculate average rating
111 COALESCE(avg_rating.rating, 0) as avg_rating,
112 COALESCE(avg_rating.review_count, 0) as review_count,
113
114 -- Calculate discount percentage
115 CASE
116 WHEN p.compare_at_price > p.price THEN
117 ROUND((1 - p.price / p.compare_at_price) * 100)
118 ELSE 0
119 END as discount_percentage,
120
121 -- Check stock status
122 CASE
123 WHEN p.track_inventory = false THEN 'in_stock'
124 WHEN p.inventory_quantity > 10 THEN 'in_stock'
125 WHEN p.inventory_quantity > 0 THEN 'low_stock'
126 ELSE 'out_of_stock'
127 END as stock_status,
128
129 -- Search ranking (only when searching)
130 \${query ? "ts_rank(to_tsvector('english', p.name || ' ' || COALESCE(p.description, '')), plainto_tsquery('english', $1)) as search_rank" : '0 as search_rank'}
131
132 FROM products p
133 INNER JOIN categories c ON p.category_id = c.id
134 LEFT JOIN (
135 SELECT
136 product_id,
137 ROUND(AVG(rating)::numeric, 1) as rating,
138 COUNT(*) as review_count
139 FROM reviews
140 WHERE is_approved = true
141 GROUP BY product_id
142 ) avg_rating ON p.id = avg_rating.product_id
143
144 WHERE \${conditions.join(' AND ')}
145 ),
146
147 total_count AS (
148 SELECT COUNT(*) as total FROM product_data
149 )
150
151 SELECT
152 pd.*,
153 tc.total
154 FROM product_data pd
155 CROSS JOIN total_count tc
156 ORDER BY \${orderBy}
157 LIMIT $\${paramIndex} OFFSET $\${paramIndex + 1}
158 \`;
159
160 params.push(limit, offset);
161
162 const result = await db.query(searchQuery, params);
163
164 // Get facet data for filtering
165 const facetsQuery = \`
166 SELECT
167 'categories' as facet_type,
168 c.id as value,
169 c.name as label,
170 COUNT(p.id) as count
171 FROM products p
172 INNER JOIN categories c ON p.category_id = c.id
173 WHERE p.is_active = true
174 GROUP BY c.id, c.name
175 HAVING COUNT(p.id) > 0
176
177 UNION ALL
178
179 SELECT
180 'price_ranges' as facet_type,
181 CASE
182 WHEN p.price < 25 THEN '0-25'
183 WHEN p.price < 50 THEN '25-50'
184 WHEN p.price < 100 THEN '50-100'
185 WHEN p.price < 250 THEN '100-250'
186 ELSE '250+'
187 END as value,
188 CASE
189 WHEN p.price < 25 THEN 'Under $25'
190 WHEN p.price < 50 THEN '$25 - $50'
191 WHEN p.price < 100 THEN '$50 - $100'
192 WHEN p.price < 250 THEN '$100 - $250'
193 ELSE '$250+'
194 END as label,
195 COUNT(*) as count
196 FROM products p
197 WHERE p.is_active = true
198 GROUP BY value, label
199
200 ORDER BY facet_type, count DESC
201 \`;
202
203 const facetsResult = await db.query(facetsQuery);
204
205 // Format facets
206 const facets = {};
207 facetsResult.rows.forEach(row => {
208 if (!facets[row.facet_type]) {
209 facets[row.facet_type] = [];
210 }
211 facets[row.facet_type].push({
212 value: row.value,
213 label: row.label,
214 count: parseInt(row.count)
215 });
216 });
217
218 const products = result.rows;
219 const total = products.length > 0 ? parseInt(products[0].total) : 0;
220 const totalPages = Math.ceil(total / limit);
221
222 return {
223 success: true,
224 products: products.map(p => ({
225 ...p,
226 total: undefined // Remove total from individual products
227 })),
228 pagination: {
229 currentPage: page,
230 totalPages,
231 totalItems: total,
232 itemsPerPage: limit,
233 hasNextPage: page < totalPages,
234 hasPrevPage: page > 1
235 },
236 facets,
237 searchParams
238 };
239
240 } catch (error) {
241 throw new Error(\`Product search failed: \${error.message}\`);
242 }
243 }
244
245 // Advanced user analytics with window functions
246 async getUserAnalytics(timeframe = '30 days') {
247 try {
248 const query = \`
249 WITH user_metrics AS (
250 SELECT
251 u.id,
252 u.first_name,
253 u.last_name,
254 u.email,
255 u.role,
256 u.created_at,
257 u.last_login,
258
259 -- Order metrics
260 COUNT(DISTINCT o.id) as total_orders,
261 COALESCE(SUM(o.total_amount), 0) as total_spent,
262 COALESCE(AVG(o.total_amount), 0) as avg_order_value,
263 MAX(o.created_at) as last_order_date,
264
265 -- Review metrics
266 COUNT(DISTINCT r.id) as total_reviews,
267 COALESCE(AVG(r.rating), 0) as avg_rating_given,
268
269 -- Calculate customer lifetime value
270 COALESCE(SUM(o.total_amount), 0) as lifetime_value,
271
272 -- Calculate days since last activity
273 EXTRACT(DAY FROM NOW() - GREATEST(u.last_login, MAX(o.created_at))) as days_since_activity,
274
275 -- Window functions for ranking
276 ROW_NUMBER() OVER (ORDER BY COALESCE(SUM(o.total_amount), 0) DESC) as value_rank,
277 ROW_NUMBER() OVER (ORDER BY COUNT(DISTINCT o.id) DESC) as order_rank,
278
279 -- Percentile calculations
280 PERCENT_RANK() OVER (ORDER BY COALESCE(SUM(o.total_amount), 0)) as value_percentile
281
282 FROM users u
283 LEFT JOIN orders o ON u.id = o.user_id
284 AND o.created_at >= NOW() - INTERVAL \${timeframe === '30 days' ? "'30 days'" : timeframe === '90 days' ? "'90 days'" : "'1 year'"}
285 AND o.status IN ('delivered', 'shipped')
286 LEFT JOIN reviews r ON u.id = r.user_id
287 AND r.created_at >= NOW() - INTERVAL \${timeframe === '30 days' ? "'30 days'" : timeframe === '90 days' ? "'90 days'" : "'1 year'"}
288
289 WHERE u.is_active = true
290 GROUP BY u.id, u.first_name, u.last_name, u.email, u.role, u.created_at, u.last_login
291 ),
292
293 customer_segments AS (
294 SELECT
295 *,
296 -- Customer segmentation based on RFM analysis
297 CASE
298 WHEN lifetime_value >= 1000 AND days_since_activity <= 30 THEN 'VIP'
299 WHEN lifetime_value >= 500 AND days_since_activity <= 60 THEN 'High Value'
300 WHEN lifetime_value >= 100 AND days_since_activity <= 90 THEN 'Regular'
301 WHEN total_orders > 0 AND days_since_activity <= 180 THEN 'At Risk'
302 WHEN total_orders > 0 THEN 'Lost'
303 ELSE 'New'
304 END as customer_segment,
305
306 -- Customer lifecycle stage
307 CASE
308 WHEN total_orders = 0 THEN 'Prospect'
309 WHEN total_orders = 1 THEN 'New Customer'
310 WHEN total_orders <= 5 THEN 'Developing Customer'
311 WHEN total_orders <= 10 THEN 'Established Customer'
312 ELSE 'Loyal Customer'
313 END as lifecycle_stage
314
315 FROM user_metrics
316 )
317
318 SELECT
319 *,
320 -- Calculate quartiles for value segmentation
321 NTILE(4) OVER (ORDER BY lifetime_value) as value_quartile
322 FROM customer_segments
323 ORDER BY lifetime_value DESC, total_orders DESC
324 \`;
325
326 const result = await db.query(query);
327
328 // Calculate summary statistics
329 const summaryQuery = \`
330 WITH metrics AS (
331 SELECT
332 COUNT(*) as total_users,
333 COUNT(CASE WHEN total_orders > 0 THEN 1 END) as customers,
334 AVG(total_spent) as avg_customer_value,
335 SUM(total_spent) as total_revenue,
336 COUNT(CASE WHEN customer_segment = 'VIP' THEN 1 END) as vip_customers,
337 COUNT(CASE WHEN customer_segment = 'At Risk' THEN 1 END) as at_risk_customers,
338 COUNT(CASE WHEN customer_segment = 'Lost' THEN 1 END) as lost_customers
339 FROM (\${query.replace('ORDER BY lifetime_value DESC, total_orders DESC', '')}) segmented_users
340 )
341 SELECT * FROM metrics
342 \`;
343
344 const summaryResult = await db.query(summaryQuery);
345
346 return {
347 success: true,
348 users: result.rows,
349 summary: summaryResult.rows[0],
350 timeframe,
351 generatedAt: new Date()
352 };
353
354 } catch (error) {
355 throw new Error(\`User analytics failed: \${error.message}\`);
356 }
357 }
358
359 // Product performance analysis with sales data
360 async getProductPerformanceAnalysis(categoryId = null) {
361 try {
362 const categoryFilter = categoryId ? 'AND p.category_id = $1' : '';
363 const params = categoryId ? [categoryId] : [];
364
365 const query = \`
366 WITH product_performance AS (
367 SELECT
368 p.id,
369 p.name,
370 p.sku,
371 p.price,
372 p.inventory_quantity,
373 p.created_at,
374 c.name as category_name,
375
376 -- Sales metrics
377 COALESCE(sales.total_sold, 0) as units_sold,
378 COALESCE(sales.total_revenue, 0) as total_revenue,
379 COALESCE(sales.avg_order_quantity, 0) as avg_order_quantity,
380
381 -- Review metrics
382 COALESCE(reviews.avg_rating, 0) as avg_rating,
383 COALESCE(reviews.review_count, 0) as review_count,
384
385 -- Calculate inventory turnover
386 CASE
387 WHEN p.inventory_quantity > 0 THEN
388 COALESCE(sales.total_sold, 0)::DECIMAL / p.inventory_quantity
389 ELSE 0
390 END as inventory_turnover,
391
392 -- Calculate revenue per unit
393 CASE
394 WHEN COALESCE(sales.total_sold, 0) > 0 THEN
395 COALESCE(sales.total_revenue, 0) / COALESCE(sales.total_sold, 0)
396 ELSE 0
397 END as revenue_per_unit,
398
399 -- Days since launch
400 EXTRACT(DAY FROM NOW() - p.created_at) as days_active,
401
402 -- Stock status
403 CASE
404 WHEN p.track_inventory = false THEN 'unlimited'
405 WHEN p.inventory_quantity > 50 THEN 'high_stock'
406 WHEN p.inventory_quantity > 10 THEN 'medium_stock'
407 WHEN p.inventory_quantity > 0 THEN 'low_stock'
408 ELSE 'out_of_stock'
409 END as stock_status
410
411 FROM products p
412 INNER JOIN categories c ON p.category_id = c.id
413
414 LEFT JOIN (
415 SELECT
416 oi.product_id,
417 SUM(oi.quantity) as total_sold,
418 SUM(oi.total) as total_revenue,
419 AVG(oi.quantity) as avg_order_quantity,
420 COUNT(DISTINCT oi.order_id) as order_count
421 FROM order_items oi
422 INNER JOIN orders o ON oi.order_id = o.id
423 WHERE o.status IN ('delivered', 'shipped')
424 AND o.created_at >= NOW() - INTERVAL '90 days'
425 GROUP BY oi.product_id
426 ) sales ON p.id = sales.product_id
427
428 LEFT JOIN (
429 SELECT
430 product_id,
431 AVG(rating) as avg_rating,
432 COUNT(*) as review_count
433 FROM reviews
434 WHERE is_approved = true
435 GROUP BY product_id
436 ) reviews ON p.id = reviews.product_id
437
438 WHERE p.is_active = true \${categoryFilter}
439 ),
440
441 ranked_products AS (
442 SELECT
443 *,
444 -- Performance rankings
445 ROW_NUMBER() OVER (ORDER BY total_revenue DESC) as revenue_rank,
446 ROW_NUMBER() OVER (ORDER BY units_sold DESC) as volume_rank,
447 ROW_NUMBER() OVER (ORDER BY avg_rating DESC, review_count DESC) as rating_rank,
448 ROW_NUMBER() OVER (ORDER BY inventory_turnover DESC) as turnover_rank,
449
450 -- Performance scores (0-100)
451 ROUND(
452 (PERCENT_RANK() OVER (ORDER BY total_revenue) * 30 +
453 PERCENT_RANK() OVER (ORDER BY units_sold) * 25 +
454 PERCENT_RANK() OVER (ORDER BY avg_rating) * 25 +
455 PERCENT_RANK() OVER (ORDER BY inventory_turnover) * 20) * 100
456 ) as performance_score,
457
458 -- Category rankings
459 ROW_NUMBER() OVER (PARTITION BY category_name ORDER BY total_revenue DESC) as category_revenue_rank
460
461 FROM product_performance
462 ),
463
464 performance_categories AS (
465 SELECT
466 *,
467 -- Performance categories
468 CASE
469 WHEN performance_score >= 80 THEN 'Top Performer'
470 WHEN performance_score >= 60 THEN 'Good Performer'
471 WHEN performance_score >= 40 THEN 'Average Performer'
472 WHEN performance_score >= 20 THEN 'Poor Performer'
473 ELSE 'Underperformer'
474 END as performance_category,
475
476 -- Action recommendations
477 CASE
478 WHEN stock_status = 'out_of_stock' AND units_sold > 10 THEN 'Restock Immediately'
479 WHEN stock_status = 'low_stock' AND inventory_turnover > 2 THEN 'Increase Inventory'
480 WHEN stock_status = 'high_stock' AND inventory_turnover < 0.5 THEN 'Reduce Inventory'
481 WHEN avg_rating < 3 AND review_count > 5 THEN 'Product Quality Review'
482 WHEN units_sold = 0 AND days_active > 30 THEN 'Marketing Review'
483 WHEN performance_score >= 80 THEN 'Feature Product'
484 ELSE 'Monitor'
485 END as recommended_action
486
487 FROM ranked_products
488 )
489
490 SELECT * FROM performance_categories
491 ORDER BY performance_score DESC, total_revenue DESC
492 \`;
493
494 const result = await db.query(query, params);
495
496 // Get category summary if no specific category filter
497 let categorySummary = null;
498 if (!categoryId) {
499 const categoryQuery = \`
500 SELECT
501 category_name,
502 COUNT(*) as product_count,
503 SUM(total_revenue) as category_revenue,
504 SUM(units_sold) as category_units_sold,
505 AVG(avg_rating) as avg_category_rating,
506 AVG(performance_score) as avg_performance_score
507 FROM (\${query.replace('ORDER BY performance_score DESC, total_revenue DESC', '')}) products
508 GROUP BY category_name
509 ORDER BY category_revenue DESC
510 \`;
511
512 const categoryResult = await db.query(categoryQuery, params);
513 categorySummary = categoryResult.rows;
514 }
515
516 return {
517 success: true,
518 products: result.rows,
519 categorySummary,
520 categoryFilter: categoryId,
521 generatedAt: new Date()
522 };
523
524 } catch (error) {
525 throw new Error(\`Product performance analysis failed: \${error.message}\`);
526 }
527 }
528}
529
530module.exports = new QueryService();Implement robust transaction handling for data consistency and integrity in complex operations.
1const db = require('../config/database');
2
3class OrderService {
4 // Complex order creation with inventory management
5 async createOrder(orderData) {
6 const { userId, items, shippingAddress, billingAddress = null } = orderData;
7
8 return await db.transaction(async (client) => {
9 try {
10 // Step 1: Validate user exists and is active
11 const userQuery = 'SELECT id, email, first_name, last_name FROM users WHERE id = $1 AND is_active = true';
12 const userResult = await client.query(userQuery, [userId]);
13
14 if (userResult.rows.length === 0) {
15 throw new Error('User not found or inactive');
16 }
17
18 const user = userResult.rows[0];
19
20 // Step 2: Validate products and check inventory
21 const productIds = items.map(item => item.productId);
22 const productQuery = \`
23 SELECT
24 p.id, p.name, p.price, p.track_inventory, p.inventory_quantity, p.is_active,
25 pv.id as variant_id, pv.price as variant_price, pv.inventory_quantity as variant_inventory
26 FROM products p
27 LEFT JOIN product_variants pv ON p.id = pv.product_id AND pv.id = ANY($2)
28 WHERE p.id = ANY($1) AND p.is_active = true
29 \`;
30
31 const variantIds = items
32 .filter(item => item.variantId)
33 .map(item => item.variantId);
34
35 const productResult = await client.query(productQuery, [
36 productIds,
37 variantIds.length > 0 ? variantIds : null
38 ]);
39
40 if (productResult.rows.length === 0) {
41 throw new Error('No valid products found');
42 }
43
44 const availableProducts = new Map();
45 productResult.rows.forEach(row => {
46 const key = row.variant_id ? \`\${row.id}-\${row.variant_id}\` : row.id;
47 availableProducts.set(key, {
48 id: row.id,
49 name: row.name,
50 price: row.variant_id ? row.variant_price : row.price,
51 variantId: row.variant_id,
52 trackInventory: row.track_inventory,
53 inventoryQuantity: row.variant_id ? row.variant_inventory : row.inventory_quantity
54 });
55 });
56
57 // Step 3: Validate inventory and calculate totals
58 let subtotal = 0;
59 const validatedItems = [];
60 const inventoryUpdates = [];
61
62 for (const item of items) {
63 const key = item.variantId ? \`\${item.productId}-\${item.variantId}\` : item.productId;
64 const product = availableProducts.get(key);
65
66 if (!product) {
67 throw new Error(\`Product not found: \${item.productId}\`);
68 }
69
70 if (product.trackInventory && product.inventoryQuantity < item.quantity) {
71 throw new Error(\`Insufficient inventory for \${product.name}. Available: \${product.inventoryQuantity}, Requested: \${item.quantity}\`);
72 }
73
74 const itemTotal = product.price * item.quantity;
75 subtotal += itemTotal;
76
77 validatedItems.push({
78 productId: product.id,
79 variantId: product.variantId,
80 quantity: item.quantity,
81 price: product.price,
82 total: itemTotal,
83 productSnapshot: {
84 name: product.name,
85 price: product.price,
86 variantId: product.variantId
87 }
88 });
89
90 // Prepare inventory updates
91 if (product.trackInventory) {
92 if (product.variantId) {
93 inventoryUpdates.push({
94 type: 'variant',
95 id: product.variantId,
96 quantity: item.quantity
97 });
98 } else {
99 inventoryUpdates.push({
100 type: 'product',
101 id: product.id,
102 quantity: item.quantity
103 });
104 }
105 }
106 }
107
108 // Step 4: Calculate taxes and shipping (simplified)
109 const taxRate = 0.08; // 8% tax
110 const taxAmount = subtotal * taxRate;
111 const shippingAmount = subtotal > 100 ? 0 : 10; // Free shipping over $100
112 const totalAmount = subtotal + taxAmount + shippingAmount;
113
114 // Step 5: Generate order number
115 const orderNumberResult = await client.query(\`
116 SELECT 'ORD-' || LPAD(COALESCE(MAX(CAST(SUBSTRING(order_number FROM 5) AS INTEGER)), 0) + 1, 8, '0') as order_number
117 FROM orders
118 WHERE order_number LIKE 'ORD-%'
119 \`);
120
121 const orderNumber = orderNumberResult.rows[0].order_number;
122
123 // Step 6: Create order record
124 const orderQuery = \`
125 INSERT INTO orders (
126 order_number, user_id, subtotal, tax_amount, shipping_amount,
127 total_amount, shipping_address, billing_address, status, payment_status
128 ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, 'pending', 'pending')
129 RETURNING id, order_number, created_at
130 \`;
131
132 const orderResult = await client.query(orderQuery, [
133 orderNumber,
134 userId,
135 subtotal,
136 taxAmount,
137 shippingAmount,
138 totalAmount,
139 JSON.stringify(shippingAddress),
140 billingAddress ? JSON.stringify(billingAddress) : JSON.stringify(shippingAddress)
141 ]);
142
143 const order = orderResult.rows[0];
144
145 // Step 7: Create order items
146 for (const item of validatedItems) {
147 const itemQuery = \`
148 INSERT INTO order_items (
149 order_id, product_id, product_variant_id, quantity, price, total, product_snapshot
150 ) VALUES ($1, $2, $3, $4, $5, $6, $7)
151 \`;
152
153 await client.query(itemQuery, [
154 order.id,
155 item.productId,
156 item.variantId,
157 item.quantity,
158 item.price,
159 item.total,
160 JSON.stringify(item.productSnapshot)
161 ]);
162 }
163
164 // Step 8: Update inventory
165 for (const update of inventoryUpdates) {
166 if (update.type === 'product') {
167 await client.query(
168 'UPDATE products SET inventory_quantity = inventory_quantity - $1 WHERE id = $2',
169 [update.quantity, update.id]
170 );
171 } else if (update.type === 'variant') {
172 await client.query(
173 'UPDATE product_variants SET inventory_quantity = inventory_quantity - $1 WHERE id = $2',
174 [update.quantity, update.id]
175 );
176 }
177 }
178
179 // Step 9: Create order history entry
180 await client.query(\`
181 INSERT INTO order_history (order_id, status, notes, created_by)
182 VALUES ($1, 'pending', 'Order created', $2)
183 \`, [order.id, userId]);
184
185 // Step 10: Return complete order data
186 const completeOrderQuery = \`
187 SELECT
188 o.*,
189 json_agg(
190 json_build_object(
191 'id', oi.id,
192 'product_id', oi.product_id,
193 'product_variant_id', oi.product_variant_id,
194 'quantity', oi.quantity,
195 'price', oi.price,
196 'total', oi.total,
197 'product_snapshot', oi.product_snapshot
198 ) ORDER BY oi.id
199 ) as items
200 FROM orders o
201 LEFT JOIN order_items oi ON o.id = oi.order_id
202 WHERE o.id = $1
203 GROUP BY o.id
204 \`;
205
206 const completeOrderResult = await client.query(completeOrderQuery, [order.id]);
207
208 return {
209 success: true,
210 order: completeOrderResult.rows[0],
211 message: 'Order created successfully'
212 };
213
214 } catch (error) {
215 // Transaction will be automatically rolled back
216 throw error;
217 }
218 });
219 }
220
221 // Order status update with history tracking
222 async updateOrderStatus(orderId, newStatus, notes = '', updatedBy) {
223 return await db.transaction(async (client) => {
224 try {
225 // Get current order
226 const currentOrderResult = await client.query(
227 'SELECT id, status, user_id FROM orders WHERE id = $1',
228 [orderId]
229 );
230
231 if (currentOrderResult.rows.length === 0) {
232 throw new Error('Order not found');
233 }
234
235 const currentOrder = currentOrderResult.rows[0];
236
237 // Validate status transition
238 const validTransitions = {
239 'pending': ['processing', 'cancelled'],
240 'processing': ['shipped', 'cancelled'],
241 'shipped': ['delivered'],
242 'delivered': [], // Final state
243 'cancelled': [] // Final state
244 };
245
246 if (!validTransitions[currentOrder.status].includes(newStatus)) {
247 throw new Error(\`Invalid status transition from \${currentOrder.status} to \${newStatus}\`);
248 }
249
250 // Update order status
251 const updateOrderQuery = \`
252 UPDATE orders
253 SET status = $1, updated_at = NOW()
254 \${newStatus === 'shipped' ? ', shipped_at = NOW()' : ''}
255 \${newStatus === 'delivered' ? ', delivered_at = NOW()' : ''}
256 WHERE id = $2
257 RETURNING *
258 \`;
259
260 const updatedOrderResult = await client.query(updateOrderQuery, [newStatus, orderId]);
261
262 // Add to order history
263 await client.query(\`
264 INSERT INTO order_history (order_id, status, notes, created_by)
265 VALUES ($1, $2, $3, $4)
266 \`, [orderId, newStatus, notes, updatedBy]);
267
268 // Handle inventory restoration for cancelled orders
269 if (newStatus === 'cancelled') {
270 const orderItemsResult = await client.query(\`
271 SELECT oi.*, p.track_inventory
272 FROM order_items oi
273 INNER JOIN products p ON oi.product_id = p.id
274 WHERE oi.order_id = $1
275 \`, [orderId]);
276
277 for (const item of orderItemsResult.rows) {
278 if (item.track_inventory) {
279 if (item.product_variant_id) {
280 await client.query(\`
281 UPDATE product_variants
282 SET inventory_quantity = inventory_quantity + $1
283 WHERE id = $2
284 \`, [item.quantity, item.product_variant_id]);
285 } else {
286 await client.query(\`
287 UPDATE products
288 SET inventory_quantity = inventory_quantity + $1
289 WHERE id = $2
290 \`, [item.quantity, item.product_id]);
291 }
292 }
293 }
294 }
295
296 return {
297 success: true,
298 order: updatedOrderResult.rows[0],
299 message: \`Order status updated to \${newStatus}\`
300 };
301
302 } catch (error) {
303 throw error;
304 }
305 });
306 }
307
308 // Bulk order processing
309 async processBulkOrders(orderIds, newStatus, notes = '', processedBy) {
310 const results = {
311 successful: [],
312 failed: []
313 };
314
315 // Process each order in its own transaction
316 for (const orderId of orderIds) {
317 try {
318 const result = await this.updateOrderStatus(orderId, newStatus, notes, processedBy);
319 results.successful.push({
320 orderId,
321 result: result.order
322 });
323 } catch (error) {
324 results.failed.push({
325 orderId,
326 error: error.message
327 });
328 }
329 }
330
331 return {
332 success: true,
333 results,
334 summary: {
335 total: orderIds.length,
336 successful: results.successful.length,
337 failed: results.failed.length
338 }
339 };
340 }
341}
342
343module.exports = new OrderService();Create reusable database logic with stored procedures and functions for complex business operations.
1-- Function to calculate product recommendations based on purchase history
2CREATE OR REPLACE FUNCTION get_product_recommendations(
3 user_id_param UUID,
4 limit_param INTEGER DEFAULT 10
5)
6RETURNS TABLE (
7 product_id UUID,
8 product_name VARCHAR,
9 category_name VARCHAR,
10 price DECIMAL,
11 avg_rating DECIMAL,
12 recommendation_score DECIMAL,
13 recommendation_reason TEXT
14) AS $$
15BEGIN
16 RETURN QUERY
17 WITH user_purchases AS (
18 -- Get user's purchase history
19 SELECT DISTINCT p.category_id, p.id as purchased_product_id
20 FROM orders o
21 INNER JOIN order_items oi ON o.id = oi.order_id
22 INNER JOIN products p ON oi.product_id = p.id
23 WHERE o.user_id = user_id_param
24 AND o.status IN ('delivered', 'shipped')
25 ),
26
27 similar_users AS (
28 -- Find users with similar purchase patterns
29 SELECT
30 o2.user_id,
31 COUNT(*) as common_categories,
32 COUNT(*) * 1.0 / (
33 SELECT COUNT(DISTINCT category_id) FROM user_purchases
34 ) as similarity_score
35 FROM user_purchases up
36 INNER JOIN products p ON up.category_id = p.category_id
37 INNER JOIN order_items oi ON p.id = oi.product_id
38 INNER JOIN orders o2 ON oi.order_id = o2.id
39 WHERE o2.user_id != user_id_param
40 AND o2.status IN ('delivered', 'shipped')
41 GROUP BY o2.user_id
42 HAVING COUNT(*) >= 2
43 ORDER BY similarity_score DESC
44 LIMIT 50
45 ),
46
47 recommended_products AS (
48 SELECT
49 p.id,
50 p.name,
51 c.name as category_name,
52 p.price,
53 COALESCE(reviews.avg_rating, 0) as avg_rating,
54 COUNT(DISTINCT su.user_id) as purchased_by_similar_users,
55
56 -- Calculate recommendation score
57 (COUNT(DISTINCT su.user_id) * 0.4 + -- Collaborative filtering
58 COALESCE(reviews.avg_rating, 0) * 0.3 + -- Rating score
59 (CASE WHEN p.category_id IN (SELECT category_id FROM user_purchases) THEN 5 ELSE 0 END) * 0.3 -- Category affinity
60 ) as recommendation_score,
61
62 -- Generate recommendation reason
63 CASE
64 WHEN p.category_id IN (SELECT category_id FROM user_purchases) THEN 'Based on your previous purchases'
65 WHEN COUNT(DISTINCT su.user_id) > 5 THEN 'Popular with customers like you'
66 WHEN COALESCE(reviews.avg_rating, 0) >= 4.5 THEN 'Highly rated product'
67 ELSE 'Recommended for you'
68 END as recommendation_reason
69
70 FROM products p
71 INNER JOIN categories c ON p.category_id = c.id
72 INNER JOIN order_items oi ON p.id = oi.product_id
73 INNER JOIN orders o ON oi.order_id = o.id
74 INNER JOIN similar_users su ON o.user_id = su.user_id
75 LEFT JOIN (
76 SELECT
77 product_id,
78 AVG(rating) as avg_rating,
79 COUNT(*) as review_count
80 FROM reviews
81 WHERE is_approved = true
82 GROUP BY product_id
83 ) reviews ON p.id = reviews.product_id
84
85 WHERE p.is_active = true
86 AND p.id NOT IN (SELECT purchased_product_id FROM user_purchases)
87 AND o.status IN ('delivered', 'shipped')
88
89 GROUP BY p.id, p.name, c.name, p.price, reviews.avg_rating, p.category_id
90 HAVING COUNT(DISTINCT su.user_id) >= 2
91 )
92
93 SELECT
94 rp.id,
95 rp.name,
96 rp.category_name,
97 rp.price,
98 rp.avg_rating,
99 rp.recommendation_score,
100 rp.recommendation_reason
101 FROM recommended_products rp
102 ORDER BY rp.recommendation_score DESC
103 LIMIT limit_param;
104END;
105$$ LANGUAGE plpgsql;
106
107-- Function to calculate inventory metrics
108CREATE OR REPLACE FUNCTION calculate_inventory_metrics(
109 product_id_param UUID DEFAULT NULL,
110 days_back INTEGER DEFAULT 90
111)
112RETURNS TABLE (
113 product_id UUID,
114 product_name VARCHAR,
115 current_inventory INTEGER,
116 units_sold INTEGER,
117 inventory_turnover DECIMAL,
118 days_of_supply DECIMAL,
119 reorder_point INTEGER,
120 recommended_order_quantity INTEGER,
121 status TEXT
122) AS $$
123DECLARE
124 analysis_period INTERVAL := (days_back || ' days')::INTERVAL;
125BEGIN
126 RETURN QUERY
127 WITH sales_data AS (
128 SELECT
129 p.id,
130 p.name,
131 p.inventory_quantity,
132 p.track_inventory,
133 COALESCE(SUM(oi.quantity), 0) as total_sold,
134 COALESCE(AVG(oi.quantity), 0) as avg_daily_sales
135 FROM products p
136 LEFT JOIN order_items oi ON p.id = oi.product_id
137 LEFT JOIN orders o ON oi.order_id = o.id
138 WHERE (product_id_param IS NULL OR p.id = product_id_param)
139 AND p.is_active = true
140 AND (o.created_at >= NOW() - analysis_period OR o.created_at IS NULL)
141 AND (o.status IN ('delivered', 'shipped') OR o.status IS NULL)
142 GROUP BY p.id, p.name, p.inventory_quantity, p.track_inventory
143 ),
144
145 inventory_calculations AS (
146 SELECT
147 sd.*,
148 -- Calculate inventory turnover
149 CASE
150 WHEN sd.inventory_quantity > 0 THEN
151 sd.total_sold::DECIMAL / sd.inventory_quantity
152 ELSE 0
153 END as turnover_ratio,
154
155 -- Calculate days of supply
156 CASE
157 WHEN sd.avg_daily_sales > 0 THEN
158 sd.inventory_quantity::DECIMAL / (sd.avg_daily_sales / days_back)
159 ELSE 999
160 END as supply_days,
161
162 -- Calculate reorder point (lead time = 7 days, safety stock = 3 days)
163 CASE
164 WHEN sd.avg_daily_sales > 0 THEN
165 CEIL((sd.avg_daily_sales / days_back) * 10) -- 10 days (7 lead + 3 safety)
166 ELSE 5
167 END as reorder_level,
168
169 -- Calculate economic order quantity (simplified)
170 CASE
171 WHEN sd.avg_daily_sales > 0 THEN
172 CEIL(SQRT(2 * sd.total_sold * 50 / 2)) -- Simplified EOQ formula
173 ELSE 10
174 END as order_quantity
175
176 FROM sales_data sd
177 )
178
179 SELECT
180 ic.id,
181 ic.name,
182 ic.inventory_quantity,
183 ic.total_sold,
184 ROUND(ic.turnover_ratio, 2),
185 ROUND(ic.supply_days, 1),
186 ic.reorder_level,
187 ic.order_quantity,
188
189 -- Determine status
190 CASE
191 WHEN NOT ic.track_inventory THEN 'No Tracking'
192 WHEN ic.inventory_quantity <= 0 THEN 'Out of Stock'
193 WHEN ic.inventory_quantity <= ic.reorder_level THEN 'Reorder Required'
194 WHEN ic.supply_days < 7 THEN 'Low Stock'
195 WHEN ic.supply_days > 60 THEN 'Overstock'
196 ELSE 'Normal'
197 END
198
199 FROM inventory_calculations ic
200 ORDER BY
201 CASE
202 WHEN ic.inventory_quantity <= 0 THEN 1
203 WHEN ic.inventory_quantity <= ic.reorder_level THEN 2
204 WHEN ic.supply_days < 7 THEN 3
205 ELSE 4
206 END,
207 ic.supply_days ASC;
208END;
209$$ LANGUAGE plpgsql;
210
211-- Stored procedure for monthly sales report
212CREATE OR REPLACE FUNCTION generate_monthly_sales_report(
213 report_year INTEGER,
214 report_month INTEGER
215)
216RETURNS TABLE (
217 metric_name TEXT,
218 current_value DECIMAL,
219 previous_value DECIMAL,
220 change_percent DECIMAL,
221 trend TEXT
222) AS $$
223DECLARE
224 current_start DATE := DATE(report_year || '-' || LPAD(report_month::TEXT, 2, '0') || '-01');
225 current_end DATE := current_start + INTERVAL '1 month' - INTERVAL '1 day';
226 previous_start DATE := current_start - INTERVAL '1 month';
227 previous_end DATE := previous_start + INTERVAL '1 month' - INTERVAL '1 day';
228BEGIN
229 RETURN QUERY
230 WITH current_metrics AS (
231 SELECT
232 COUNT(DISTINCT o.id) as order_count,
233 COUNT(DISTINCT o.user_id) as customer_count,
234 SUM(o.total_amount) as total_revenue,
235 AVG(o.total_amount) as avg_order_value,
236 SUM(oi.quantity) as units_sold
237 FROM orders o
238 INNER JOIN order_items oi ON o.id = oi.order_id
239 WHERE o.created_at BETWEEN current_start AND current_end
240 AND o.status IN ('delivered', 'shipped')
241 ),
242
243 previous_metrics AS (
244 SELECT
245 COUNT(DISTINCT o.id) as order_count,
246 COUNT(DISTINCT o.user_id) as customer_count,
247 SUM(o.total_amount) as total_revenue,
248 AVG(o.total_amount) as avg_order_value,
249 SUM(oi.quantity) as units_sold
250 FROM orders o
251 INNER JOIN order_items oi ON o.id = oi.order_id
252 WHERE o.created_at BETWEEN previous_start AND previous_end
253 AND o.status IN ('delivered', 'shipped')
254 )
255
256 SELECT
257 'Total Revenue'::TEXT,
258 COALESCE(cm.total_revenue, 0),
259 COALESCE(pm.total_revenue, 0),
260 CASE
261 WHEN COALESCE(pm.total_revenue, 0) > 0 THEN
262 ROUND(((COALESCE(cm.total_revenue, 0) - COALESCE(pm.total_revenue, 0)) / COALESCE(pm.total_revenue, 0)) * 100, 2)
263 ELSE 0
264 END,
265 CASE
266 WHEN COALESCE(cm.total_revenue, 0) > COALESCE(pm.total_revenue, 0) THEN 'Up'
267 WHEN COALESCE(cm.total_revenue, 0) < COALESCE(pm.total_revenue, 0) THEN 'Down'
268 ELSE 'Flat'
269 END
270 FROM current_metrics cm
271 CROSS JOIN previous_metrics pm
272
273 UNION ALL
274
275 SELECT
276 'Order Count'::TEXT,
277 COALESCE(cm.order_count, 0),
278 COALESCE(pm.order_count, 0),
279 CASE
280 WHEN COALESCE(pm.order_count, 0) > 0 THEN
281 ROUND(((COALESCE(cm.order_count, 0) - COALESCE(pm.order_count, 0)) / COALESCE(pm.order_count, 0)) * 100, 2)
282 ELSE 0
283 END,
284 CASE
285 WHEN COALESCE(cm.order_count, 0) > COALESCE(pm.order_count, 0) THEN 'Up'
286 WHEN COALESCE(cm.order_count, 0) < COALESCE(pm.order_count, 0) THEN 'Down'
287 ELSE 'Flat'
288 END
289 FROM current_metrics cm
290 CROSS JOIN previous_metrics pm
291
292 UNION ALL
293
294 SELECT
295 'Average Order Value'::TEXT,
296 COALESCE(cm.avg_order_value, 0),
297 COALESCE(pm.avg_order_value, 0),
298 CASE
299 WHEN COALESCE(pm.avg_order_value, 0) > 0 THEN
300 ROUND(((COALESCE(cm.avg_order_value, 0) - COALESCE(pm.avg_order_value, 0)) / COALESCE(pm.avg_order_value, 0)) * 100, 2)
301 ELSE 0
302 END,
303 CASE
304 WHEN COALESCE(cm.avg_order_value, 0) > COALESCE(pm.avg_order_value, 0) THEN 'Up'
305 WHEN COALESCE(cm.avg_order_value, 0) < COALESCE(pm.avg_order_value, 0) THEN 'Down'
306 ELSE 'Flat'
307 END
308 FROM current_metrics cm
309 CROSS JOIN previous_metrics pm
310
311 UNION ALL
312
313 SELECT
314 'Customer Count'::TEXT,
315 COALESCE(cm.customer_count, 0),
316 COALESCE(pm.customer_count, 0),
317 CASE
318 WHEN COALESCE(pm.customer_count, 0) > 0 THEN
319 ROUND(((COALESCE(cm.customer_count, 0) - COALESCE(pm.customer_count, 0)) / COALESCE(pm.customer_count, 0)) * 100, 2)
320 ELSE 0
321 END,
322 CASE
323 WHEN COALESCE(cm.customer_count, 0) > COALESCE(pm.customer_count, 0) THEN 'Up'
324 WHEN COALESCE(cm.customer_count, 0) < COALESCE(pm.customer_count, 0) THEN 'Down'
325 ELSE 'Flat'
326 END
327 FROM current_metrics cm
328 CROSS JOIN previous_metrics pm;
329END;
330$$ LANGUAGE plpgsql;Implement advanced search capabilities with PostgreSQL's full-text search features.
1const db = require('../config/database');
2
3class SearchService {
4 // Initialize search configurations
5 async initializeSearch() {
6 try {
7 // Create custom text search configuration for products
8 await db.query(\`
9 -- Create custom dictionary for product terms
10 CREATE TEXT SEARCH CONFIGURATION product_search (COPY = english);
11
12 -- Add custom word mappings for product-specific terms
13 CREATE TEXT SEARCH DICTIONARY product_dict (
14 TEMPLATE = simple,
15 STOPWORDS = english
16 );
17
18 -- Update search configuration to use custom dictionary
19 ALTER TEXT SEARCH CONFIGURATION product_search
20 ALTER MAPPING FOR asciiword, asciihword, hword_asciipart, word, hword, hword_part
21 WITH product_dict, english_stem;
22 \`);
23
24 console.log('✅ Search configuration initialized');
25 } catch (error) {
26 // Configuration may already exist
27 if (!error.message.includes('already exists')) {
28 console.error('❌ Search initialization error:', error.message);
29 }
30 }
31 }
32
33 // Advanced product search with ranking
34 async searchProducts(searchParams) {
35 const {
36 query = '',
37 categoryId = null,
38 priceRange = null,
39 rating = null,
40 inStock = null,
41 sortBy = 'relevance',
42 page = 1,
43 limit = 20
44 } = searchParams;
45
46 try {
47 const conditions = ['p.is_active = true'];
48 const params = [];
49 let paramIndex = 1;
50
51 // Build search query with ranking
52 let searchSelect = '';
53 let searchOrderBy = '';
54
55 if (query) {
56 // Create search vector and query
57 searchSelect = \`,
58 ts_rank_cd(
59 setweight(to_tsvector('product_search', p.name), 'A') ||
60 setweight(to_tsvector('product_search', COALESCE(p.description, '')), 'B') ||
61 setweight(to_tsvector('product_search', COALESCE(p.short_description, '')), 'C') ||
62 setweight(to_tsvector('product_search', c.name), 'D'),
63 plainto_tsquery('product_search', $\${paramIndex})
64 ) as search_rank,
65
66 ts_headline('product_search', p.description, plainto_tsquery('product_search', $\${paramIndex}),
67 'MaxWords=35, MinWords=15, ShortWord=3, HighlightAll=false, MaxFragments=2'
68 ) as search_snippet
69 \`;
70
71 conditions.push(\`
72 (setweight(to_tsvector('product_search', p.name), 'A') ||
73 setweight(to_tsvector('product_search', COALESCE(p.description, '')), 'B') ||
74 setweight(to_tsvector('product_search', COALESCE(p.short_description, '')), 'C') ||
75 setweight(to_tsvector('product_search', c.name), 'D')) @@ plainto_tsquery('product_search', $\${paramIndex})
76 \`);
77
78 params.push(query);
79 paramIndex++;
80 }
81
82 // Category filter with hierarchy
83 if (categoryId) {
84 conditions.push(\`
85 (p.category_id = $\${paramIndex} OR p.category_id IN (
86 WITH RECURSIVE category_tree AS (
87 SELECT id FROM categories WHERE id = $\${paramIndex}
88 UNION ALL
89 SELECT c.id FROM categories c
90 INNER JOIN category_tree ct ON c.parent_id = ct.id
91 )
92 SELECT id FROM category_tree
93 ))
94 \`);
95 params.push(categoryId);
96 paramIndex++;
97 }
98
99 // Price range filter
100 if (priceRange) {
101 if (priceRange.min !== undefined) {
102 conditions.push(\`p.price >= $\${paramIndex}\`);
103 params.push(priceRange.min);
104 paramIndex++;
105 }
106 if (priceRange.max !== undefined) {
107 conditions.push(\`p.price <= $\${paramIndex}\`);
108 params.push(priceRange.max);
109 paramIndex++;
110 }
111 }
112
113 // Rating filter
114 if (rating) {
115 conditions.push(\`COALESCE(reviews.avg_rating, 0) >= $\${paramIndex}\`);
116 params.push(rating);
117 paramIndex++;
118 }
119
120 // Stock filter
121 if (inStock !== null) {
122 if (inStock) {
123 conditions.push(\`(p.track_inventory = false OR p.inventory_quantity > 0)\`);
124 } else {
125 conditions.push(\`(p.track_inventory = true AND p.inventory_quantity = 0)\`);
126 }
127 }
128
129 // Build ORDER BY clause
130 switch (sortBy) {
131 case 'relevance':
132 searchOrderBy = query ? 'search_rank DESC, ' : '';
133 searchOrderBy += 'p.is_featured DESC, reviews.avg_rating DESC';
134 break;
135 case 'price_low':
136 searchOrderBy = 'p.price ASC';
137 break;
138 case 'price_high':
139 searchOrderBy = 'p.price DESC';
140 break;
141 case 'rating':
142 searchOrderBy = 'COALESCE(reviews.avg_rating, 0) DESC, reviews.review_count DESC';
143 break;
144 case 'newest':
145 searchOrderBy = 'p.created_at DESC';
146 break;
147 case 'popular':
148 searchOrderBy = 'COALESCE(sales.total_sold, 0) DESC';
149 break;
150 default:
151 searchOrderBy = 'p.is_featured DESC, p.created_at DESC';
152 }
153
154 const offset = (page - 1) * limit;
155
156 const searchQuery = \`
157 WITH search_results AS (
158 SELECT
159 p.id,
160 p.name,
161 p.slug,
162 p.description,
163 p.short_description,
164 p.price,
165 p.compare_at_price,
166 p.featured_image_url,
167 p.is_featured,
168 p.inventory_quantity,
169 p.track_inventory,
170 p.created_at,
171
172 c.name as category_name,
173 c.slug as category_slug,
174
175 COALESCE(reviews.avg_rating, 0) as avg_rating,
176 COALESCE(reviews.review_count, 0) as review_count,
177 COALESCE(sales.total_sold, 0) as total_sold,
178
179 -- Stock status
180 CASE
181 WHEN p.track_inventory = false THEN 'in_stock'
182 WHEN p.inventory_quantity > 10 THEN 'in_stock'
183 WHEN p.inventory_quantity > 0 THEN 'low_stock'
184 ELSE 'out_of_stock'
185 END as stock_status,
186
187 -- Discount percentage
188 CASE
189 WHEN p.compare_at_price > p.price THEN
190 ROUND((1 - p.price / p.compare_at_price) * 100)
191 ELSE 0
192 END as discount_percentage
193
194 \${searchSelect}
195
196 FROM products p
197 INNER JOIN categories c ON p.category_id = c.id
198
199 LEFT JOIN (
200 SELECT
201 product_id,
202 ROUND(AVG(rating)::numeric, 1) as avg_rating,
203 COUNT(*) as review_count
204 FROM reviews
205 WHERE is_approved = true
206 GROUP BY product_id
207 ) reviews ON p.id = reviews.product_id
208
209 LEFT JOIN (
210 SELECT
211 oi.product_id,
212 SUM(oi.quantity) as total_sold
213 FROM order_items oi
214 INNER JOIN orders o ON oi.order_id = o.id
215 WHERE o.status IN ('delivered', 'shipped')
216 AND o.created_at >= NOW() - INTERVAL '90 days'
217 GROUP BY oi.product_id
218 ) sales ON p.id = sales.product_id
219
220 WHERE \${conditions.join(' AND ')}
221 ),
222
223 facet_data AS (
224 SELECT
225 json_build_object(
226 'categories', (
227 SELECT json_agg(
228 json_build_object(
229 'id', cat_facet.category_id,
230 'name', cat_facet.category_name,
231 'count', cat_facet.product_count
232 )
233 )
234 FROM (
235 SELECT
236 c.id as category_id,
237 c.name as category_name,
238 COUNT(p.id) as product_count
239 FROM categories c
240 INNER JOIN products p ON c.id = p.category_id
241 WHERE p.is_active = true
242 GROUP BY c.id, c.name
243 HAVING COUNT(p.id) > 0
244 ORDER BY product_count DESC
245 LIMIT 20
246 ) cat_facet
247 ),
248 'price_ranges', (
249 SELECT json_agg(
250 json_build_object(
251 'range', price_facet.price_range,
252 'label', price_facet.price_label,
253 'count', price_facet.product_count
254 )
255 )
256 FROM (
257 SELECT
258 CASE
259 WHEN p.price < 25 THEN '0-25'
260 WHEN p.price < 50 THEN '25-50'
261 WHEN p.price < 100 THEN '50-100'
262 WHEN p.price < 250 THEN '100-250'
263 ELSE '250+'
264 END as price_range,
265 CASE
266 WHEN p.price < 25 THEN 'Under $25'
267 WHEN p.price < 50 THEN '$25 - $50'
268 WHEN p.price < 100 THEN '$50 - $100'
269 WHEN p.price < 250 THEN '$100 - $250'
270 ELSE '$250+'
271 END as price_label,
272 COUNT(*) as product_count
273 FROM products p
274 WHERE p.is_active = true
275 GROUP BY price_range, price_label
276 ORDER BY MIN(p.price)
277 ) price_facet
278 ),
279 'ratings', (
280 SELECT json_agg(
281 json_build_object(
282 'rating', rating_facet.rating_threshold,
283 'label', rating_facet.rating_label,
284 'count', rating_facet.product_count
285 )
286 )
287 FROM (
288 SELECT
289 unnest(ARRAY[4, 3, 2, 1]) as rating_threshold,
290 unnest(ARRAY['4+ Stars', '3+ Stars', '2+ Stars', '1+ Stars']) as rating_label,
291 unnest(ARRAY[
292 (SELECT COUNT(*) FROM search_results WHERE avg_rating >= 4),
293 (SELECT COUNT(*) FROM search_results WHERE avg_rating >= 3),
294 (SELECT COUNT(*) FROM search_results WHERE avg_rating >= 2),
295 (SELECT COUNT(*) FROM search_results WHERE avg_rating >= 1)
296 ]) as product_count
297 ) rating_facet
298 )
299 ) as facets
300 ),
301
302 total_count AS (
303 SELECT COUNT(*) as total FROM search_results
304 )
305
306 SELECT
307 sr.*,
308 tc.total as total_results,
309 fd.facets
310 FROM search_results sr
311 CROSS JOIN total_count tc
312 CROSS JOIN facet_data fd
313 ORDER BY \${searchOrderBy}
314 LIMIT $\${paramIndex} OFFSET $\${paramIndex + 1}
315 \`;
316
317 params.push(limit, offset);
318
319 const result = await db.query(searchQuery, params);
320
321 const products = result.rows.map(row => ({
322 ...row,
323 total_results: undefined,
324 facets: undefined
325 }));
326
327 const totalResults = result.rows.length > 0 ? parseInt(result.rows[0].total_results) : 0;
328 const facets = result.rows.length > 0 ? result.rows[0].facets : null;
329
330 return {
331 success: true,
332 products,
333 facets,
334 pagination: {
335 currentPage: page,
336 totalPages: Math.ceil(totalResults / limit),
337 totalResults,
338 itemsPerPage: limit,
339 hasNextPage: page < Math.ceil(totalResults / limit),
340 hasPrevPage: page > 1
341 },
342 searchParams,
343 executionTime: Date.now()
344 };
345
346 } catch (error) {
347 throw new Error(\`Search failed: \${error.message}\`);
348 }
349 }
350
351 // Search suggestions and autocomplete
352 async getSearchSuggestions(query, limit = 10) {
353 try {
354 if (!query || query.length < 2) {
355 return { success: true, suggestions: [] };
356 }
357
358 const suggestionQuery = \`
359 WITH product_suggestions AS (
360 SELECT
361 'product' as type,
362 p.name as suggestion,
363 p.slug,
364 'in ' || c.name as context,
365 ts_rank_cd(to_tsvector('product_search', p.name), plainto_tsquery('product_search', $1)) as rank,
366 p.featured_image_url as image_url
367 FROM products p
368 INNER JOIN categories c ON p.category_id = c.id
369 WHERE p.is_active = true
370 AND to_tsvector('product_search', p.name) @@ plainto_tsquery('product_search', $1)
371 ORDER BY rank DESC
372 LIMIT $2
373 ),
374
375 category_suggestions AS (
376 SELECT
377 'category' as type,
378 c.name as suggestion,
379 c.slug,
380 CASE
381 WHEN c.parent_id IS NOT NULL THEN 'in ' || pc.name
382 ELSE 'category'
383 END as context,
384 ts_rank_cd(to_tsvector('product_search', c.name), plainto_tsquery('product_search', $1)) as rank,
385 NULL as image_url
386 FROM categories c
387 LEFT JOIN categories pc ON c.parent_id = pc.id
388 WHERE c.is_active = true
389 AND to_tsvector('product_search', c.name) @@ plainto_tsquery('product_search', $1)
390 ORDER BY rank DESC
391 LIMIT $3
392 ),
393
394 fuzzy_suggestions AS (
395 SELECT
396 'product' as type,
397 p.name as suggestion,
398 p.slug,
399 'similar to "' || $1 || '"' as context,
400 similarity(p.name, $1) as rank,
401 p.featured_image_url as image_url
402 FROM products p
403 WHERE p.is_active = true
404 AND similarity(p.name, $1) > 0.3
405 ORDER BY rank DESC
406 LIMIT $4
407 )
408
409 SELECT * FROM product_suggestions
410 UNION ALL
411 SELECT * FROM category_suggestions
412 UNION ALL
413 SELECT * FROM fuzzy_suggestions
414
415 ORDER BY rank DESC
416 LIMIT $2
417 \`;
418
419 const result = await db.query(suggestionQuery, [
420 query,
421 limit,
422 Math.ceil(limit / 3),
423 Math.ceil(limit / 4)
424 ]);
425
426 return {
427 success: true,
428 suggestions: result.rows
429 };
430
431 } catch (error) {
432 throw new Error(\`Search suggestions failed: \${error.message}\`);
433 }
434 }
435
436 // Update search statistics
437 async logSearch(query, results, userId = null) {
438 try {
439 await db.query(\`
440 INSERT INTO search_logs (query, results_count, user_id, created_at)
441 VALUES ($1, $2, $3, NOW())
442 \`, [query, results, userId]);
443
444 } catch (error) {
445 // Log error but don't fail the search
446 console.error('Search logging failed:', error.message);
447 }
448 }
449}
450
451module.exports = new SearchService();Learn about Practice Exercises.
Exercise 1: Financial Transaction System
Build a banking transaction system with ACID compliance, transaction logging, and fraud detection using PostgreSQL's advanced features.
Exercise 2: Multi-tenant Application
Design a multi-tenant SaaS application with row-level security, tenant isolation, and performance optimization.
Exercise 3: Advanced Reporting System
Create a comprehensive reporting system with window functions, CTEs, and stored procedures for business intelligence.
Connection Pooling:
Optimize performance with proper connection management
Complex Queries:
Master joins, subqueries, and window functions
ACID Transactions:
Ensure data consistency with proper transaction handling
Stored Procedures:
Encapsulate business logic in reusable database functions
Full-text Search:
Implement advanced search with ranking and suggestions
Performance:
Use indexes, query optimization, and monitoring for production
You've mastered PostgreSQL integration with Node.js! You can now build enterprise-grade applications with complex queries, transactions, and advanced search capabilities.
Your progress is saved in this browser, on this device only.