Files
APAW/.kilo/skills/nodejs-db-patterns/SKILL.md
¨NW¨ 8fcd8f8a9b feat: add comprehensive NodeJS development skills and rules
Based on Planner and Memory Manager analysis:

New Skills (8):
- nodejs-express-patterns: App structure, routing, middleware
- nodejs-security-owasp: OWASP Top 10 security practices
- nodejs-testing-jest: Unit/integration tests, mocking
- nodejs-auth-jwt: JWT authentication, OAuth, sessions
- nodejs-error-handling: Error classes, middleware, async handlers
- nodejs-middleware-patterns: Auth, validation, rate limiting
- nodejs-db-patterns: SQLite, PostgreSQL, MongoDB patterns
- nodejs-npm-management: package.json, scripts, dependencies

New Rules:
- nodejs.md: Code style, security, best practices

Updated:
- backend-developer.md: Added skills reference table

Milestone: #48 NodeJS Development Coverage
Related: Planner & Memory Manager analysis results
2026-04-05 02:39:06 +01:00

8.3 KiB

NodeJS Database Patterns

Database patterns for SQLite, PostgreSQL, and MongoDB.

SQLite with Knex

Connection

// src/db/connection.js
const knex = require('knex');

const db = knex({
  client: 'better-sqlite3',
  connection: {
    filename: process.env.DATABASE_URL || './data.db'
  },
  useNullAsDefault: true,
  pool: {
    min: 2,
    max: 10
  }
});

module.exports = db;

Migrations

// migrations/20240101000000_create_users.js
exports.up = function(knex) {
  return knex.schema.createTable('users', table => {
    table.increments('id').primary();
    table.string('email').unique().notNullable();
    table.string('password').notNullable();
    table.string('name');
    table.enu('role', ['user', 'admin']).defaultTo('user');
    table.timestamp('created_at').defaultTo(knex.fn.now());
    table.timestamp('updated_at').defaultTo(knex.fn.now());
    
    table.index('email');
  });
};

exports.down = function(knex) {
  return knex.schema.dropTable('users');
};

Model

// src/models/User.js
const db = require('../db/connection');

class User {
  static async create(data) {
    const [id] = await db('users').insert(data);
    return this.findById(id);
  }
  
  static async findById(id) {
    return db('users').where({ id }).first();
  }
  
  static async findByEmail(email) {
    return db('users').where({ email }).first();
  }
  
  static async findAll(options = {}) {
    const { page = 1, limit = 20 } = options;
    const offset = (page - 1) * limit;
    
    return db('users')
      .select('id', 'email', 'name', 'role', 'created_at')
      .limit(limit)
      .offset(offset);
  }
  
  static async update(id, data) {
    await db('users').where({ id }).update({
      ...data,
      updated_at: db.fn.now()
    });
    return this.findById(id);
  }
  
  static async delete(id) {
    return db('users').where({ id }).del();
  }
  
  toSafeObject() {
    const { password, ...safe } = this;
    return safe;
  }
}

module.exports = User;

Transactions

const db = require('./db/connection');

async function transferFunds(fromId, toId, amount) {
  return db.transaction(async (trx) => {
    // Check balance
    const from = await trx('accounts').where({ id: fromId }).first();
    if (from.balance < amount) {
      throw new Error('Insufficient funds');
    }
    
    // Debit
    await trx('accounts')
      .where({ id: fromId })
      .decrement('balance', amount);
    
    // Credit
    await trx('accounts')
      .where({ id: toId })
      .increment('balance', amount);
    
    // Log transaction
    await trx('transactions').insert({
      from_id: fromId,
      to_id: toId,
      amount
    });
  });
}

PostgreSQL

Connection (pg)

const { Pool } = require('pg');

const pool = new Pool({
  host: process.env.PG_HOST,
  port: process.env.PG_PORT,
  database: process.env.PG_DATABASE,
  user: process.env.PG_USER,
  password: process.env.PG_PASSWORD,
  max: 20,
  idleTimeoutMillis: 30000
});

// Query helper
async function query(text, params) {
  const start = Date.now();
  const result = await pool.query(text, params);
  const duration = Date.now() - start;
  
  console.log('Query:', { text: text.substring(0, 50), duration, rows: result.rowCount });
  
  return result;
}

module.exports = { query, pool };

Prepared Statements

// Named parameters
const { query } = require('./db');

const sql = 'SELECT * FROM users WHERE email = $1 AND role = $2';
const result = await query(sql, ['user@example.com', 'admin']);

// With parameterized queries
const text = 'INSERT INTO users (email, name) VALUES ($1, $2) RETURNING *';
const values = ['user@example.com', 'John'];
const result = await query(text, values);

Connection Pooling

const { Pool } = require('pg');

const pool = new Pool({
  max: 20,
  idleTimeoutMillis: 30000,
  connectionTimeoutMillis: 2000
});

pool.on('error', (err) => {
  console.error('Unexpected error on idle client', err);
});

// In-request connection
app.get('/users/:id', async (req, res) => {
  const client = await pool.connect();
  try {
    const result = await client.query('SELECT * FROM users WHERE id = $1', [req.params.id]);
    res.json(result.rows[0]);
  } finally {
    client.release();
  }
});

MongoDB

Connection (Mongoose)

const mongoose = require('mongoose');

mongoose.connect(process.env.MONGODB_URI, {
  useNewUrlParser: true,
  useUnifiedTopology: true
});

mongoose.connection.on('connected', () => {
  console.log('MongoDB connected');
});

mongoose.connection.on('error', (err) => {
  console.error('MongoDB error:', err);
});

Schema

// src/models/User.js
const mongoose = require('mongoose');

const userSchema = new mongoose.Schema({
  email: {
    type: String,
    required: true,
    unique: true,
    lowercase: true,
    trim: true
  },
  password: {
    type: String,
    required: true
  },
  name: {
    type: String,
    trim: true,
    maxlength: 100
  },
  role: {
    type: String,
    enum: ['user', 'admin'],
    default: 'user'
  }
}, {
  timestamps: true
});

// Indexes
userSchema.index({ email: 1 });

// Methods
userSchema.methods.toSafeObject = function() {
  const { password, ...safe } = this.toObject();
  return safe;
};

// Statics
userSchema.statics.findByEmail = function(email) {
  return this.findOne({ email });
};

module.exports = mongoose.model('User', userSchema);

CRUD Operations

// Create
const user = await User.create({
  email: 'test@example.com',
  password: hashedPassword,
  name: 'Test User'
});

// Read
const user = await User.findById(id);
const user = await User.findOne({ email });
const users = await User.find({ role: 'user' }).limit(20);

// Update
const user = await User.findByIdAndUpdate(id, data, { new: true });
const result = await User.updateMany({ role: 'user' }, { $set: { active: true } });

// Delete
await User.findByIdAndDelete(id);
await User.deleteMany({ inactive: true });

Aggregation

const stats = await User.aggregate([
  { $match: { role: 'user' } },
  { $group: {
    _id: '$role',
    total: { $sum: 1 },
    avgPosts: { $avg: '$postCount' }
  }},
  { $sort: { total: -1 } },
  { $limit: 10 }
]);

Pagination

async function paginate(Model, filter, options = {}) {
  const { page = 1, limit = 20 } = options;
  const skip = (page - 1) * limit;
  
  const [docs, total] = await Promise.all([
    Model.find(filter).skip(skip).limit(limit),
    Model.countDocuments(filter)
  ]);
  
  return {
    docs,
    total,
    page,
    pages: Math.ceil(total / limit)
  };
}

Common Patterns

Repository Pattern

// src/repositories/BaseRepository.js
class BaseRepository {
  constructor(model) {
    this.model = model;
  }
  
  async findById(id) {
    return this.model.findById(id);
  }
  
  async findOne(filter) {
    return this.model.findOne(filter);
  }
  
  async findAll(filter, options = {}) {
    const { page, limit } = options;
    let query = this.model.find(filter);
    
    if (page && limit) {
      query = query.skip((page - 1) * limit).limit(limit);
    }
    
    return query;
  }
  
  async create(data) {
    const doc = new this.model(data);
    return doc.save();
  }
  
  async update(id, data) {
    return this.model.findByIdAndUpdate(id, data, { new: true });
  }
  
  async delete(id) {
    return this.model.findByIdAndDelete(id);
  }
}

module.exports = BaseRepository;

Query Builder

// src/utils/queryBuilder.js
class QueryBuilder {
  constructor(model) {
    this.query = model.find();
  }
  
  filter(filters) {
    if (filters) {
      this.query = this.query.where(filters);
    }
    return this;
  }
  
  search(field, term) {
    if (term) {
      this.query = this.query.where({ [field]: new RegExp(term, 'i') });
    }
    return this;
  }
  
  sort(field, direction = 'asc') {
    if (field) {
      this.query = this.query.sort({ [field]: direction });
    }
    return this;
  }
  
  paginate(page = 1, limit = 20) {
    const skip = (page - 1) * limit;
    this.query = this.query.skip(skip).limit(limit);
    return this;
  }
  
  build() {
    return this.query;
  }
}

// Usage
const users = await new QueryBuilder(User)
  .filter({ role: 'user' })
  .search('name', 'john')
  .sort('createdAt', 'desc')
  .paginate(1, 20)
  .build();