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:
¨NW¨
2026-04-05 02:39:06 +01:00
parent fbc1f6122f
commit 8fcd8f8a9b
10 changed files with 3318 additions and 1 deletions

View 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();
```