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
This commit is contained in:
433
.kilo/skills/nodejs-db-patterns/SKILL.md
Normal file
433
.kilo/skills/nodejs-db-patterns/SKILL.md
Normal file
@@ -0,0 +1,433 @@
|
||||
# NodeJS Database Patterns
|
||||
|
||||
Database patterns for SQLite, PostgreSQL, and MongoDB.
|
||||
|
||||
## SQLite with Knex
|
||||
|
||||
### Connection
|
||||
|
||||
```javascript
|
||||
// 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
|
||||
|
||||
```javascript
|
||||
// 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
|
||||
|
||||
```javascript
|
||||
// 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
|
||||
|
||||
```javascript
|
||||
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)
|
||||
|
||||
```javascript
|
||||
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
|
||||
|
||||
```javascript
|
||||
// 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
|
||||
|
||||
```javascript
|
||||
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)
|
||||
|
||||
```javascript
|
||||
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
|
||||
|
||||
```javascript
|
||||
// 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
|
||||
|
||||
```javascript
|
||||
// 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
|
||||
|
||||
```javascript
|
||||
const stats = await User.aggregate([
|
||||
{ $match: { role: 'user' } },
|
||||
{ $group: {
|
||||
_id: '$role',
|
||||
total: { $sum: 1 },
|
||||
avgPosts: { $avg: '$postCount' }
|
||||
}},
|
||||
{ $sort: { total: -1 } },
|
||||
{ $limit: 10 }
|
||||
]);
|
||||
```
|
||||
|
||||
### Pagination
|
||||
|
||||
```javascript
|
||||
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
|
||||
|
||||
```javascript
|
||||
// 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
|
||||
|
||||
```javascript
|
||||
// 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();
|
||||
```
|
||||
Reference in New Issue
Block a user