required https://github.com/eggjs/egg/pull/5654 --------- Signed-off-by: MK (fengmk2) <fengmk2@gmail.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
18 KiB
cnpmcore - Private NPM Registry for Enterprise
cnpmcore is a TypeScript-based private NPM registry implementation built with Egg.js framework. It provides enterprise-grade package management with support for MySQL/PostgreSQL databases, Redis caching, and optional Elasticsearch.
ALWAYS reference these instructions first and fallback to search or bash commands only when you encounter unexpected information that does not match the information here.
Code Style and Conventions
Linting and Formatting
- Linter: Oxlint (fast Rust-based linter)
- Formatter: Prettier with specific configuration
- Pre-commit hooks: Husky + lint-staged automatically format and lint on commit
Code Style Rules:
// From .prettierrc
{
"singleQuote": true, // Use single quotes
"trailingComma": "es5", // ES5 trailing commas
"tabWidth": 2, // 2-space indentation
"printWidth": 120, // 120 character line width
"arrowParens": "avoid" // Avoid parens when possible
}
// From .oxlintrc.json
{
"max-params": 6, // Maximum 6 function parameters
"no-console": "warn", // Warn on console usage
"import/no-anonymous-default-export": "error"
}
Linting Commands:
npm run lint # Check for linting errors
npm run lint:fix # Auto-fix linting issues
npm run typecheck # TypeScript type checking without build
TypeScript Conventions
- Use strict TypeScript with comprehensive type definitions
- Avoid
anytypes - use proper typing orunknown - Export types and interfaces for reusability
- Use ES modules (
import/export) syntax throughout
Testing Conventions
- Test files use
.test.tssuffix - Use
@eggjs/mockfor mocking and testing - Tests organized to mirror source structure in
test/directory - Use
assertfromnode:assert/strictfor assertions - Mock external dependencies using
mock()from@eggjs/mock
Test Naming Pattern:
describe('test/path/to/SourceFile.test.ts', () => {
describe('[HTTP_METHOD /api/path] functionName()', () => {
it('should handle expected behavior', async () => {
// Test implementation
});
});
});
Domain-Driven Design (DDD) Architecture
cnpmcore follows Domain-Driven Design principles with clear separation of concerns:
Layer Architecture (Dependency Flow)
Controller (HTTP Interface Layer)
↓ depends on
Service (Business Logic Layer)
↓ depends on
Repository (Data Access Layer)
↓ depends on
Model (ORM/Database Layer)
Entity (Domain Models - no dependencies, pure business logic)
Common (Utilities and Adapters - available to all layers)
Layer Responsibilities
Controller Layer (app/port/controller/):
- HTTP request/response handling
- Request validation using
@eggjs/typebox-validate - User authentication and authorization
- NO business logic - delegate to Services
- Inheritance:
YourController extends AbstractController extends MiddlewareController
Service Layer (app/core/service/):
- Core business logic implementation
- Orchestration of multiple repositories and entities
- Transaction management
- Event publishing
- NO HTTP concerns, NO direct database access
Repository Layer (app/repository/):
- Data access and persistence
- CRUD operations on Models
- Query building and optimization
- NO business logic
Entity Layer (app/core/entity/):
- Domain models with business behavior
- Pure business logic (no infrastructure dependencies)
- Immutable data structures where possible
- Rich domain objects (not anemic models)
Model Layer (app/repository/model/):
- ORM definitions using Leoric
- Database schema mapping
- Table and column definitions
- NO business logic
Repository Method Naming Convention
ALWAYS follow these naming patterns:
findSomething- Query a single model/entitysaveSomething- Save (create or update) a modelremoveSomething- Delete a modellistSomethings- Query multiple models (use plural)
Request Validation Trilogy
ALWAYS validate requests in this exact order:
-
Request Parameter Validation - First line of defense
// Use @eggjs/typebox-validate for type-safe validation // See app/port/typebox.ts for examples -
User Authentication & Token Permissions
// Token roles: 'read' | 'publish' | 'setting' const authorizedUser = await this.userRoleManager.requiredAuthorizedUser(ctx, 'publish'); -
Resource Authorization - Prevent horizontal privilege escalation
// Example: Ensure user is package maintainer await this.userRoleManager.requiredPackageMaintainer(pkg, authorizedUser); // Or use convenience method const { pkg } = await this.ensurePublishAccess(ctx, fullname);
Modifying Database Models
When changing a Model, update all 3 locations:
- SQL migration files:
sql/mysql/*.sqlANDsql/postgresql/*.sql - ORM Model:
app/repository/model/*.ts - Domain Entity:
app/core/entity/*.ts
NEVER auto-generate SQL migrations - manual review is required for safety.
Prerequisites and Environment Setup
- Node.js: Version 20.18.0 or higher (required by engines field in package.json)
- Database: MySQL 5.7+ or PostgreSQL 17+
- Cache: Redis 6+
- Optional: Elasticsearch 8.x for enhanced search capabilities
Working Effectively
Bootstrap and Build
# Install dependencies (takes ~2 minutes)
npm install
# Copy environment configuration
cp .env.example .env
# Lint code (very fast, <1 second)
npm run lint
# Fix linting issues
npm run lint:fix
# Build TypeScript (takes ~6 seconds)
npm run tsc
# Production build (takes ~6 seconds)
npm run tsc:prod
Database Setup - MySQL (Recommended for Development)
# Start MySQL + Redis services via Docker (takes ~1 minute to pull images initially)
docker compose -f docker-compose.yml up -d
# Verify services are running
docker compose ps
# Initialize database (takes <2 seconds)
CNPMCORE_DATABASE_NAME=cnpmcore bash ./prepare-database-mysql.sh
# For tests, create test database
mysql -h 127.0.0.1 -P 3306 -u root -e "CREATE DATABASE cnpmcore_unittest;"
Database Setup - PostgreSQL (Alternative)
# Start PostgreSQL + Redis services via Docker
docker compose -f docker-compose-postgres.yml up -d
# Initialize database (takes <1 second)
CNPMCORE_DATABASE_NAME=cnpmcore bash ./prepare-database-postgresql.sh
Development Server
# MySQL development server (starts in ~20 seconds)
npm run dev
# Server runs on http://127.0.0.1:7001
# PostgreSQL development server
npm run dev:postgresql
# Server runs on http://127.0.0.1:7001
Testing
# Run full test suite with MySQL - NEVER CANCEL: Takes 4+ minutes. Set timeout to 10+ minutes.
npm run test
# Run full test suite with PostgreSQL - NEVER CANCEL: Takes 4+ minutes. Set timeout to 10+ minutes.
npm run test:postgresql
# Run single test file (for faster iteration, takes ~12 seconds)
npm run test:local test/common/CryptoUtil.test.ts
# Test coverage with MySQL - NEVER CANCEL: Takes 5+ minutes. Set timeout to 15+ minutes.
npm run cov
# Test coverage with PostgreSQL - NEVER CANCEL: Takes 5+ minutes. Set timeout to 15+ minutes.
npm run cov:postgresql
CRITICAL TESTING NOTES:
- NEVER CANCEL build or test commands - they may take 4-15 minutes to complete
- Individual test files run much faster (~12 seconds) for development iteration
- Full test suite processes 100+ test files and requires database initialization
- Test failures may occur in CI environment; use individual test files for validation
Testing Philosophy:
- Write tests for all new features - No feature is complete without tests
- Test at the right layer - Controller tests for HTTP, Service tests for business logic
- Mock external dependencies - Use
mock()from@eggjs/mock - Use realistic test data - Create through
TestUtilhelper methods - Clean up after tests - Database is reset between test files
- Test both success and failure cases - Error paths are equally important
Common Test Patterns:
import { app, mock } from '@eggjs/mock/bootstrap';
import { TestUtil } from '../../../test/TestUtil';
describe('test/path/to/YourController.test.ts', () => {
describe('[GET /api/endpoint] methodName()', () => {
it('should return expected result', async () => {
// Setup
const { authorization } = await TestUtil.createUser();
// Execute
const res = await app
.httpRequest()
.get('/api/endpoint')
.set('authorization', authorization)
.expect(200);
// Assert
assert.equal(res.body.someField, expectedValue);
});
it('should handle unauthorized access', async () => {
const res = await app
.httpRequest()
.get('/api/endpoint')
.expect(401);
assert.equal(res.body.error, '[UNAUTHORIZED] Login first');
});
});
});
Production Commands
# CI pipeline commands - NEVER CANCEL: Takes 5+ minutes. Set timeout to 15+ minutes.
npm run ci # MySQL CI (includes lint, test, coverage, build)
npm run ci:postgresql # PostgreSQL CI
# Production start/stop
npm run start # Start as daemon
npm run stop # Stop daemon
npm run start:foreground # Start in foreground for debugging
Validation Scenarios
ALWAYS manually validate changes by running through these scenarios:
Basic API Validation
# Start development server
npm run dev
# Test registry root endpoint
curl http://127.0.0.1:7001
# Should return JSON with app metadata and stats
# Test authentication endpoint
curl http://127.0.0.1:7001/-/whoami
# Should return authentication error (expected when not logged in)
# Test package listing (initially empty)
curl http://127.0.0.1:7001/-/all
Admin User Setup and Package Publishing
# Register admin user (cnpmcore_admin) - requires allowPublicRegistration=true in config
npm login --registry=http://127.0.0.1:7001
# Verify login
npm whoami --registry=http://127.0.0.1:7001
# Test package publishing
npm publish --registry=http://127.0.0.1:7001
Architecture and Navigation
Project Structure
app/
├── common/ # Global utilities and adapters
│ ├── adapter/ # External service adapters (NpmRegistry, Binary, etc.)
│ └── enum/ # Shared enumerations
├── core/ # Business logic layer
│ ├── entity/ # Core domain models
│ ├── event/ # Event handlers and async processing
│ ├── service/ # Core business services
│ └── util/ # Internal utilities
├── port/ # Interface layer
│ ├── controller/ # HTTP controllers
│ ├── middleware/ # Express middleware
│ ├── schedule/ # Background job schedulers
│ └── webauth/ # WebAuth integration
├── repository/ # Data access layer
│ ├── model/ # ORM models
│ └── util/ # Repository utilities
└── infra/ # Infrastructure adapters
Key Services and Controllers
- PackageController: Main package CRUD operations
- PackageManagerService: Core package management business logic
- BinarySyncerService: Binary package synchronization
- ChangesStreamService: NPM registry change stream processing
- UserController: User authentication and profile management
Infrastructure Adapters (app/infra/)
Enterprise customization layer for PaaS integration. cnpmcore provides default implementations, but enterprises should implement their own based on their infrastructure:
- NFSClientAdapter: File storage abstraction (local/S3/OSS)
- QueueAdapter: Message queue integration
- AuthAdapter: Authentication system integration
- BinaryAdapter: Binary package storage adapter
These adapters allow cnpmcore to integrate with different cloud providers and enterprise systems without modifying core business logic.
Configuration Files
config/config.default.ts: Main application configurationconfig/database.ts: Database connection settingsconfig/binaries.ts: Binary package mirror configurations.env: Environment-specific variablestsconfig.json: TypeScript compilation settingstsconfig.prod.json: Production build settings
Common Development Tasks
Adding New Features
ALWAYS follow this workflow:
-
Plan the change - Identify which layers need modification
-
Run linter -
npm run lint:fixto establish clean baseline -
Bottom-up implementation - Build from data layer up to controller:
a. Model Layer (if new data structure needed):
- Add SQL migrations:
sql/mysql/*.sqlANDsql/postgresql/*.sql - Create Model:
app/repository/model/YourModel.ts - Run database migration scripts
b. Entity Layer (domain models):
- Create Entity:
app/core/entity/YourEntity.ts - Implement business logic and behavior
- Keep entities pure (no infrastructure dependencies)
c. Repository Layer (data access):
- Create Repository:
app/repository/YourRepository.ts - Follow naming:
findX,saveX,removeX,listXs - Inject dependencies using
@Inject()
d. Service Layer (business logic):
- Create Service:
app/core/service/YourService.ts - Orchestrate repositories and entities
- Use
@SingletonProto()for service lifecycle
e. Controller Layer (HTTP endpoints):
- Create Controller:
app/port/controller/YourController.ts - Extend
AbstractController - Add HTTP method decorators:
@HTTPMethod(),@HTTPBody(), etc. - Implement 3-step validation (params → auth → authorization)
- Add SQL migrations:
-
Add tests - Create test file:
test/path/matching/source/YourFile.test.ts -
Lint and test -
npm run lint:fix && npm run test:local test/your/test.test.ts -
Type check -
npm run typecheck -
Commit - Use semantic commit messages (feat/fix/chore/docs/test)
Example Controller Implementation:
import { AbstractController } from './AbstractController';
import { HTTPController, HTTPMethod, HTTPQuery, Inject } from 'egg';
@HTTPController()
export class YourController extends AbstractController {
@Inject()
private readonly yourService: YourService;
@HTTPMethod({ path: '/api/path', method: 'GET' })
async yourMethod(@HTTPQuery() params: YourQueryType) {
// 1. Validate params (done by @HTTPQuery with typebox)
// 2. Authenticate user
const user = await this.userRoleManager.requiredAuthorizedUser(this.ctx, 'read');
// 3. Authorize resource access (if needed)
// 4. Delegate to service
return await this.yourService.doSomething(params);
}
}
Database Migrations
- SQL files are in
sql/mysql/andsql/postgresql/ - Migration scripts automatically run during database preparation
- NEVER modify existing migration files - only add new ones
Background Jobs
- Schedulers are in
app/port/schedule/ - Include sync workers, cleanup tasks, and stream processors
- Jobs run automatically when development server starts
Troubleshooting
Database Connection Issues
# Check if services are running
docker compose ps
# Reset MySQL environment
docker compose -f docker-compose.yml down
docker compose -f docker-compose.yml up -d
CNPMCORE_DATABASE_NAME=cnpmcore bash ./prepare-database-mysql.sh
# Reset PostgreSQL environment
docker compose -f docker-compose-postgres.yml down
docker compose -f docker-compose-postgres.yml up -d
CNPMCORE_DATABASE_NAME=cnpmcore bash ./prepare-database-postgresql.sh
Build Issues
# Clean and rebuild
npm run clean
npm run tsc
# Check TypeScript configuration
npx tsc --noEmit
Test Issues
# Create missing test database
mysql -h 127.0.0.1 -P 3306 -u root -e "CREATE DATABASE cnpmcore_unittest;"
# Run single test for debugging
npm run test:local test/common/CryptoUtil.test.ts
CI/CD Integration
The project uses GitHub Actions with workflows in .github/workflows/:
nodejs.yml: Main CI pipeline with MySQL, PostgreSQL, and Elasticsearch testing- Multiple Node.js versions tested: 20, 22, 24
- CRITICAL: CI jobs include long-running tests that can take 15+ minutes per database type
Pre-commit Validation
ALWAYS run before committing:
npm run lint:fix # Fix linting issues
npm run tsc # Verify TypeScript compilation
npm run test:local test/path/to/relevant.test.ts # Run relevant tests
Docker Support
Development Environments
docker-compose.yml: MySQL + Redis + phpMyAdmindocker-compose-postgres.yml: PostgreSQL + Redis + pgAdmindocker-compose-es.yml: Elasticsearch integration
Production Images
# Build Alpine image
npm run images:alpine
# Build Debian image
npm run images:debian
External Dependencies
- Database: MySQL 9.x or PostgreSQL 17+
- Cache: Redis 6+
- Search: Elasticsearch 8.x (optional)
- Storage: Local filesystem or S3-compatible storage
- Framework: Egg.js with extensive TypeScript integration
Performance Notes
Command execution times (for timeout planning):
- Startup Time: ~20 seconds for development server
- Build Time: ~6 seconds for TypeScript compilation
- Test Time: 4-15 minutes for full suite (database dependent)
- Individual Test: ~12 seconds for single test file
- Package Installation: ~2 minutes for npm install
- Database Init: <2 seconds for either MySQL or PostgreSQL
- Linting: <1 second (oxlint is very fast)
Always account for these timings when setting timeouts for automated processes.
Semantic Commit Messages
Use conventional commit format for all commits:
feat:- New featuresfix:- Bug fixesdocs:- Documentation changeschore:- Maintenance taskstest:- Test additions or modificationsrefactor:- Code refactoringperf:- Performance improvements
Examples:
feat: add support for GitHub binary mirroring
fix: resolve authentication token expiration issue
docs: update API documentation for sync endpoints
test: add tests for package publication workflow