@d2sutils/shopperv2
v1.7.3
Published
This is a simple utility service that provides a set of utility functions for the shopper service.
Readme
Shopper Utils v2
🇺🇦 Українська версія | 🇬🇧 English Version
🇬🇧 English Version
Description
Shopper Utils v2 is a utility service that provides a set of useful functions for working with the Shopper service. The library is written in TypeScript and includes various services for working with users, locations, caching, and other functionality.
Key Features
- 👥 User Management - working with users, clients, shoppers, and groups
- 📍 Locations - retrieving location and location group information
- 💾 Caching - Redis integration for fast data access
- 📧 Notifications - sending messages and notifications
- 🔧 Formatting - utilities for data formatting
- 📦 Archiving - working with ZIP archives
- 📊 Logging - integrated logging system
- 📚 References - powerful ReferenceManager for multilingual dictionaries
- 🐛 Debug utilities - tools for debugging, performance measurement, and smart logging
- 🔀 Auto-Batch - automatic splitting of long GET requests into batches with response merging
Quick Navigation
- Description
- Key Features
- Installation
- Quick Start
- API Documentation
- ServerService
- Configuration
- Dependencies
- Development
Installation
npm install @d2sutils/shopperv2Quick Start
import { instanceAppV2, user, location, cache } from '@d2sutils/shopperv2';
// Initialize the application
instanceAppV2('your-proxy-url', 'your-secret-key', 'redis://localhost:6379', true);
// Working with users
const users = await user.users({ filter: {}, limit: 10 });
const isClient = await user.isClient(123);
const userName = await user.getNameById(123);
// Working with locations
const locations = await location.all({ filter: {}, limit: 25 });
const groups = await location.groups({ filter: {}, limit: 25 });
// Working with cache
await cache.set('key', 'value', 3600);
const value = await cache.get('key');
// Working with references
import { ReferenceManager } from '@d2sutils/shopperv2';
await ReferenceManager.setReference('countries', countriesData);
const countryName = await ReferenceManager.getNameById('countries', 1, 'en');API Documentation
User Service
// Get all users
await user.users({ filter: {}, limit: 25 });
// Get clients
await user.clients({ filter: {}, limit: 25 });
// Get shoppers
await user.shoppers({ filter: {}, limit: 25 });
// Check if user is a client
await user.isClient(userId: number | number[]);
// Check if user is a manager
await user.isManager(userId: number | number[]);
// Get user by ID
await user.getById(id: number);
// Get current user
await user.me(token: string);Location Service
// Get all locations
await location.all({ filter: {}, limit: 25, id?: number });
// Get location groups
await location.groups({ filter: {}, parent_id?: number, limit: 25 });Cache Service
// Set value in cache
await cache.set(key: string, value: string, ttl?: number);
// Get value from cache
await cache.get(key: string);
// Check if key exists
await cache.has(key: string);
// Delete key
await cache.del(key: string);
// Clear service cache
await cache.clearService(service: string);
// Clear all cache
await cache.flushAll();Notification Service
// Send notification
await notification.send({
action: 'email', // action type
user_id: '123', // user ID
contact: '[email protected]', // contact
communication_id: 'comm_123', // communication ID
communication_name: 'Welcome Email', // communication name
subject: 'Welcome!', // subject
message: 'Thank you for registration', // message
params: { name: 'John' }, // additional parameters
header: { 'X-Custom': 'value' } // additional headers
});Format Service
// Apply filter to model
const filteredModel = format.applyFilter({
model: MyModel,
filter: { name: 'John', age: 25 },
config: {
like: ['name'], // fields for LIKE search
like_number: ['age'], // numeric fields for LIKE
in: ['status'], // fields for IN search
custom: {
customField: (value) => `custom_${value}`
}
}
});
// Format request body
const formattedBody = format.body({
name: 'John',
active: 'true', // will be converted to boolean
data: '{"key": "value"}', // will be parsed as JSON
items: ['item1', 'item2']
});Response Utils
A utility for formatting standardized API responses with support for various HTTP statuses.
Basic methods:
// Create success response
const successResponse = responseUtils.success({
data: { id: 1, name: 'John' },
message: 'Operation completed successfully'
});
// Create error response
const errorResponse = responseUtils.error({
message: 'Validation error',
status: 400
});
// Create response with data and notifications
const dataResponse = responseUtils.sendData({
data: { users: [] },
notifications: { unread: 5 }
});
// Create paginated response
const paginatedResponse = responseUtils.paginate({
data: [{ id: 1 }, { id: 2 }],
total: 100,
limit: 10,
page: 1,
other: { customField: 'value' },
notifications: { unread: 3 }
});HTTP status codes (convenience wrappers):
// 201 Created - for created resources
const createdResponse = responseUtils.created({
data: { id: 123, name: 'New Resource' },
message: 'Resource created successfully'
});
// 204 No Content - for successful operations with no content
const noContentResponse = responseUtils.noContent();
// 400 Bad Request - for invalid requests
const badRequestResponse = responseUtils.badRequest({
message: 'Invalid request parameters',
errors: { field: 'Field is required' }
});
// 401 Unauthorized - for unauthorized requests
const unauthorizedResponse = responseUtils.unauthorized({
message: 'Authentication required'
});
// 403 Forbidden - for forbidden operations
const forbiddenResponse = responseUtils.forbidden({
message: 'You do not have permission'
});
// 404 Not Found - for missing resources
const notFoundResponse = responseUtils.notFound({
message: 'User not found',
resource: 'User'
});
// 422 Validation Error - for validation errors
const validationErrorResponse = responseUtils.validationError({
message: 'Validation failed',
errors: {
email: 'Email is invalid',
password: 'Password must be at least 8 characters'
}
});
// 500 Internal Server Error - for server errors
const serverErrorResponse = responseUtils.serverError({
message: 'An unexpected error occurred',
error: errorObject // Shown only in development
});Additional utilities:
// Add metadata to response
const responseWithMeta = responseUtils.withMeta({
data: { users: [] },
meta: {
version: '1.0.0',
apiVersion: 'v2',
requestId: 'req-123'
}
});
// Result: { users: [], meta: { timestamp: '2024-...', version: '1.0.0', ... } }Usage examples in controllers:
import { responseUtils } from "@d2sutils/shopperv2";
import { Request, Response } from "express";
// Creating a resource
export const createUser = async (req: Request, res: Response) => {
const user = await userService.create(req.body);
return res.status(201).json(responseUtils.created({ data: user }));
};
// Retrieving a resource
export const getUser = async (req: Request, res: Response) => {
const user = await userService.findById(req.params.id);
if (!user) {
return res.status(404).json(responseUtils.notFound({ resource: 'User' }));
}
return res.json(responseUtils.success({ data: user }));
};
// Validation
export const updateUser = async (req: Request, res: Response) => {
const { error, value } = schema.validate(req.body);
if (error) {
return res.status(422).json(responseUtils.validationError({
errors: error.details.reduce((acc, detail) => {
acc[detail.path[0]] = detail.message;
return acc;
}, {})
}));
}
// ... processing
};
// Pagination
export const getUsers = async (req: Request, res: Response) => {
const { page = 1, limit = 10 } = req.query;
const { data, total } = await userService.findAll({ page, limit });
return res.json(responseUtils.paginate({ data, total, page, limit }));
};ZIP Utils
// Validate postal code for different countries
const isValidZip = validateZip('UA', '12345'); // true for Ukraine
const isValidZipDE = validateZip('DE', '12345'); // true for Germany
const isValidZipGB = validateZip('GB', 'SW1A1AA'); // true for United Kingdom
// Supported countries: AL, AD, AT, BY, BE, BA, BG, HR, CZ, CY, DK, EE, FI, FR, DE, GR, HU, IS, IE, IT, LV, LI, LT, LU, MK, MT, MD, MC, NL, NO, PL, PT, RO, RU, SM, RS, SK, SI, ES, SE, CH, TN, TR, UA, GB, VAAction Logger
// Get singleton logger instance (uses shared Redis connection)
const logger = ActionLogger.getInstance();
// Log user actions
await logger.logUserAction({
request_type: 'POST',
url: '/api/users',
user_id: 123,
origin: 'https://example.com',
referrer: 'https://google.com',
payload: { name: 'John' },
session_id: 'session_123',
response: { success: true }
});
// Disconnect not needed - uses shared connection
// await logger.disconnect(); // no longer neededActionLogger Features:
- ✅ Uses Singleton pattern
- ✅ Uses shared Redis connection with Cache Service
- ✅ Automatically initializes when calling
getInstance() - ✅ No separate Redis configuration needed - uses settings from
instanceAppV2
API Utils
// Execute GET request
const data = await api.get('user', 'users', { limit: 10 }, {
useCache: true,
cache_options: { ttl: 3600, name: 'users_cache' }
});
// Execute POST request
const result = await api.post('user', 'create', { name: 'John' }, {}, {
headers: { 'Content-Type': 'application/json' }
});
// Execute PUT request
const updated = await api.put('user', 'update/123', { name: 'Jane' });
// Universal request
const response = await api.request('GET', 'location', 'all', {}, { limit: 25 });Auto-Batch for long GET requests
The autoBatch option automatically splits GET requests with long query parameters into several batches and merges the responses into a single result. It is disabled by default — enable it explicitly on the call.
Problem: some GET requests contain very long parameters (for example, filter[id]=769,770,771,...,10840 — hundreds of IDs), which leads to an HTTP 431 error (Request Header Fields Too Large).
Solution: when autoBatch is enabled, the utility automatically:
- Analyzes all query parameters and finds the longest one (any key, not just
id) - If the URL exceeds the limit — splits only the longest parameter into batches, leaving the other parameters unchanged
- Executes parallel requests (up to 3 simultaneously)
- Recursively merges the responses into a single result
- If the server returns 431 — automatically switches to batching (fallback)
Basic usage:
// Regular request — without batching (as before)
const data = await api.get('locations', 'shops', {
filter: { id: '769,770,...,10840', client_id: 1 },
limit: 1000,
});
// With automatic batching
const data = await api.get('locations', 'shops', {
filter: { id: '769,770,...,10840', client_id: 1 },
limit: 1000,
}, { autoBatch: true });
// With a custom batch size (default is 100)
const data = await api.get('locations', 'shops', {
filter: { id: '769,770,...,10840', client_id: 1 },
limit: 1000,
}, { autoBatch: true, batchSize: 50 });How response merging works:
// If the API returns an array — concatenation
// [1, 2, 3] + [4, 5, 6] → [1, 2, 3, 4, 5, 6]
// If the API returns an object with data and pagination:
// { data: [...], pagination: { total: 100, pageSize: 25, ... } }
// + { data: [...], pagination: { total: 50, pageSize: 25, ... } }
// → { data: [...all items...], pagination: { total: 150, totalPages: 6, pageSize: 25, ... } }
// For other objects — recursive deep-merge:
// arrays → concatenation
// objects → recursively by keys
// primitives → value from the last responseCombining with cache:
const data = await api.get('locations', 'shops', {
filter: { id: longIdList, client_id: 1 },
limit: 1000,
}, {
autoBatch: true,
useCache: true,
cache_options: { ttl: 3600, name: 'shops_cache' },
});Options:
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| autoBatch | boolean | false | Enable automatic batching of long requests |
| batchSize | number | 100 | Number of items in a single batch |
Features:
- Analyzes any query parameter (not only
filter[id]), including nested keys - Default URL limit — 2000 characters
- Parallel batch execution (up to 3 simultaneously)
- Automatic fallback on HTTP 431 even if the URL seemed short
pagination.total— summed,pagination.totalPages— recalculated- Works with any value format: array, comma-separated string
Debug Utils
import { debug } from '@d2sutils/shopperv2';
// Measure promise execution time
const result = await debug.measurePromise(
user.users({ limit: 10 }),
'Fetch users'
);
// Outputs (if debug=true): ⏱️ Fetch users executed in 123.45ms
// Process tracking
debug.start('data_processing');
// ... some code ...
debug.end('data_processing');
// Always outputs: ⏱️ Process "data_processing" took 123.45ms
// Smart logs
debug.log('This message is visible only if debug=true');
debug.info('Informational message (only when debug=true)');
debug.debug('Debug message (only when debug=true)');
// Warnings and errors are always visible
debug.warn('Warning - always visible');
debug.error('Error - always visible');Debug Utils Features:
- ✅
measurePromise()- automatically measures promise execution time - ✅
start()/end()- track the execution time of any process - ✅ Smart logs -
log(),info(),debug()are shown only whendebug=true - ✅
warn()anderror()are always visible for important messages - ✅ Automatic time formatting with millisecond precision
Helper Utilities
// Free port (kill processes occupying it)
await ensurePortFree(3000, 5); // port 3000, 5 retries
// Generate color from string
const color = stringToColor('user123'); // generates hex color
const softColor = stringToColor('user123', true); // soft color
// Sleep execution
await sleep(1000); // pause for 1 second
// Normalize number to string
const normalized = normalNumber(123.456); // "123.46"
const normalizedInt = normalNumber(123); // "123"
// Remove undefined values from object
const cleaned = removeUndefinedDeep({
name: 'John',
age: undefined,
data: { value: 'test', empty: undefined }
}); // { name: 'John', data: { value: 'test' } }
// Slice string with ellipsis
const short = strSlice('Long text for slicing', 10); // "Long text..."
// Filter by date
const items = [
{ name: 'Event 1', date_start: '2024-01-01', date_end: '2024-01-31' },
{ name: 'Event 2', date_start: '2024-02-01', date_end: '2024-02-28' }
];
const filtered = filterByDate(items, '2024-01-15'); // returns Event 1ReferenceManager - Multilingual Dictionaries
// Create multilingual countries dictionary
const countries = [
{
id: 1,
code: "UA",
name_en: "Ukraine",
name_ru: "Украина",
name_uk: "Україна"
}
];
await ReferenceManager.setReference('countries', countries);
// Create dictionary with different field patterns
const regions = [
{
id: 3767,
country: "UA",
regionEn: "Khersonska Oblast",
regionRu: "Херсонская область",
regionUk: "Херсонська область"
}
];
await ReferenceManager.setReference('regions', regions);
// Create regular users dictionary
const users = [
{
id: 1,
name: "John Smith",
email: "[email protected]",
role: "admin"
}
];
await ReferenceManager.setReference('users', users);
// Get data from multilingual dictionaries
const countryNameUk = await ReferenceManager.getNameById('countries', 1, 'uk'); // "Україна"
const countryNameEn = await ReferenceManager.getNameById('countries', 1, 'en'); // "Ukraine"
// Work with different field patterns (regionEn, regionUk)
const regionNameUk = await ReferenceManager.getLocalizedField('regions', 3767, 'uk', 'region');
const regionNameEn = await ReferenceManager.getLocalizedField('regions', 3767, 'en', 'region');
// Work with regular dictionaries
const userName = await ReferenceManager.getNameById('users', 1, 'name'); // "John Smith"
const userEmail = await ReferenceManager.getFieldById('users', 1, 'email'); // "[email protected]"
// Search in dictionaries
const searchResults = await ReferenceManager.search('countries', 'Ukraine', {
field: 'name',
language: 'en'
});
// Fast search by ID (optimized - uses Redis indexes)
const city = await ReferenceManager.search('cities', '123', { field: 'id' });
const cities = await ReferenceManager.search('cities', '123, 456, 789', { field: 'id' });
// Fast search by array of IDs through indexes (O(1) for each ID)
const citiesByIds = await ReferenceManager.findByIds('cities', [123, 456, 789]);
// Search by array of values for any field
const citiesByCode = await ReferenceManager.findByField('cities', 'code', ['NYC', 'LA']);
const usersByEmail = await ReferenceManager.findByField('users', 'email', ['[email protected]']);
// For 'id' field automatically uses fast indexes
const citiesByIds2 = await ReferenceManager.findByField('cities', 'id', [123, 456]);
// Batch data retrieval
const countryNames = await ReferenceManager.getBatchNames('countries', [1, 2, 3], 'uk');
const userItems = await ReferenceManager.getBatchItems('users', [1, 2]);
// Dictionary metadata
const isMultilingual = await ReferenceManager.isMultilingual('countries'); // true
const languages = await ReferenceManager.getAvailableLanguages('countries'); // ["en", "ru", "uk"]
const fields = await ReferenceManager.getAvailableFields('users'); // ["id", "name", "email", "role"]
// Dictionary management
const allReferences = await ReferenceManager.listReferences();
await ReferenceManager.updateItem('users', 1, { role: 'manager' });
await ReferenceManager.deleteItem('users', 1);
await ReferenceManager.deleteReference('old_reference');Supported language field patterns:
name_en,name_ru,name_uk(underscore)nameEn,nameRu,nameUk(camelCase)regionEn,regionUk,titleEn(camelCase with prefix)NameEn,RegionUk,TitleEn(PascalCase)
Main methods:
setReference()- create/update dictionarygetReference()- get entire dictionarygetById()- get item by IDgetNameById()- get name with automatic pattern recognitiongetLocalizedField()- get language fieldsearch()- search by text (optimized for ID search)findByIds()- fast search by array of IDs through Redis indexes (O(1))findByField()- search by array of values for any fieldgetBatchNames()- batch name retrievalisMultilingual()- check if multilingual
Performance optimizations:
- ⚡
findByIds()- uses direct Redis indexes for fast ID search - ⚡
search()withfield: 'id'- automatically uses indexes instead of loading entire dictionary - ⚡
findByField()withfieldName: 'id'- automatically uses fast indexes
Configuration
The library supports the following configuration parameters:
proxy- proxy server URLsecret- secret key for authenticationredis_uri- Redis connection URI (optional)debug- debug mode (optional)
Dependencies
- Node.js 16+
- Redis 7+ (for caching)
- TypeScript 5.4+
Redis Architecture
The library uses a single Redis connection for all services:
- ✅ Cache Service - singleton for data caching
- ✅ ActionLogger - singleton, uses shared connection
- ✅ API Utils - uses Cache Service for request caching
- ✅ getRedis() - singleton for working with Redis and BullMQ workers
- 🔧 Configured once through
instanceAppV2() - 🚀 Optimized for performance and resources
ServerService - Managing Express Server and Workers
ServerService is a powerful class for creating and managing Express servers with support for workers, databases, caching, and other features.
Basic usage
import { ServerService } from "@d2sutils/shopperv2";
import { IDefaultConfig } from "@d2sutils/shopperv2";
import routes from "./api/routes";
const config: IDefaultConfig = {
name: "my-service",
app_url: "http://localhost:3000",
node: process.env.NODE_ENV || "development",
port: process.env.PORT || 3000,
proxy: process.env.PROXY_URI || "http://localhost:3000",
pg_uri: process.env.PG_URI || "postgres://localhost:5432",
pg_logging: process.env.PG_LOGGING === "true" || false,
pg_connect: process.env.PG_CONNECT || "false",
secret: process.env.SECRET_KEY || "secret",
redis_uri: process.env.REDIS_URI || "redis://localhost:6379",
};
const server = new ServerService(config, routes, {
healthCheck: true,
errorHandling: true,
});
server.start().catch((error) => {
console.error("Error starting service", error);
process.exit(1);
});Options configuration
const server = new ServerService(config, routes, {
// Hooks for various initialization stages
hooks: {
beforeInit: async () => {
console.log("Before initialization");
},
afterInit: async () => {
console.log("After initialization");
},
beforeListen: async () => {
console.log("Before server starts listening");
},
afterListen: async () => {
console.log("After server started");
// Cron jobs or other modules can be imported here
await import("./cron/index.cron");
},
},
// Database initialization
database: {
initialize: async () => {
await AppDataSource.initialize()
.then(() => {
console.log("Data Source has been initialized!");
})
.catch((error) => {
console.error("Error during Data Source initialization", error);
throw error;
});
},
},
// Redis initialization
redis: {
initialize: () => {
// Redis initialization (for example, creating workers)
getRedis(); // Or other logic
},
},
// Caching with automatic refresh
cache: {
functions: [
async () => {
// Function for loading cache
await ensureCacheUsers();
},
async () => {
await ensureCacheProjects();
},
],
interval: 1 * 60 * 1000, // Refresh every minute
},
// Port check before startup
portCheck: {
enabled: config.node === "local",
check: async (port: number | string) => {
await ensurePortFree(Number(port));
},
},
// Express JSON middleware settings
expressJson: {
limit: "10mb",
},
// Express URL encoded middleware settings
expressUrlencoded: {
limit: "10mb",
extended: true,
},
// Custom middleware
middleware: [
languageMiddleware,
authenticationMiddleware,
],
// Health check endpoint
healthCheck: true,
// Global error handling
errorHandling: true,
// Logger configuration
logger: {
configure: (proxy: string, name: string) => {
configureLogger(proxy, name);
},
},
// Registering routes in the proxy
routesList: RoutesList,
// Custom service URL
serviceUrl: "http://my-service:3000",
// Disable automatic dotenv loading
dotenv: false,
});Working with workers
The library supports separate startup of BullMQ workers and the Express server with three modes:
Creating workers
import { ServerService, getRedis } from "@d2sutils/shopperv2";
import { Worker } from "bullmq";
// Creating workers using the singleton Redis client
const autosaveWorker = new Worker(
"answersAutosaveQueue",
async (job) => {
// your processing logic
// getRedis() can be used for regular operations
const redis = getRedis();
await redis.get('key');
await redis.set('key', 'value');
},
{ connection: getRedis() as any } // One singleton client for BullMQ
);
const updateWorker = new Worker(
"updateQueue",
async (job) => {
// task processing
},
{ connection: getRedis() as any } // The same singleton client
);
// Using in ServerService
const server = new ServerService(config, routes, {
database: { initialize: async () => { /* ... */ } },
redis: { initialize: () => { getRedis(); } }, // Initialize Redis singleton
workers: [updateWorker, autosaveWorker], // Pass the array of workers
});
// The startup mode is determined automatically from arguments or environment variables
server.start();Startup modes
npm run start --only-worker- starts only workers (without Express)npm run start --only-service- starts only Express (without workers)npm run start- starts both Express and workers (default, if workers are passed)WORKER_MODE=only-worker npm run start- alternative way via environment variable
Benefits of the approach
- ✅ One Redis client - used for both BullMQ workers and regular operations
- ✅ Resource savings - one connection instead of two
- ✅ No version conflicts - everyone uses the same client
- ✅ Flexibility - workers can be run separately from the server for scaling
getRedis() for regular operations
import { getRedis } from "@d2sutils/shopperv2";
// Get the singleton Redis client
const redis = getRedis();
// Use for regular operations
await redis.get('key');
await redis.set('key', 'value');
await redis.del('key');
// ... other ioredis operationsDefining routes
Routes can be passed in three ways:
1. Array of Route objects:
import { ServerService, Route } from "@d2sutils/shopperv2";
const routes: Route[] = [
{
method: 'get',
path: '/api/users',
handler: async (req, res) => {
res.json({ users: [] });
},
type: 'public',
action: 'R',
label: 'Get users',
},
{
method: 'post',
path: '/api/users',
handler: [authMiddleware, createUserHandler],
type: 'private',
action: 'C',
label: 'Create user',
},
];
const server = new ServerService(config, routes, options);2. Express Router:
import express, { Router } from "express";
const router = Router();
router.get('/api/users', getUsersHandler);
router.post('/api/users', createUserHandler);
const server = new ServerService(config, router, options);3. Express App:
import express from "express";
const app = express();
app.get('/api/users', getUsersHandler);
app.post('/api/users', createUserHandler);
const server = new ServerService(config, app, options);Worker startup modes
ServerService automatically determines the startup mode from command-line arguments or environment variables:
Modes:
only-worker- starts only workers (without the Express server)only-service- starts only the Express server (without workers)both- starts both the server and workers (default, if workers are passed)
Mode determination:
- Command-line arguments:
--only-worker,--only-service - Environment variable:
WORKER_MODE=only-worker - Explicit setting in options:
workerMode: 'only-worker' - Default:
both(if workers are passed) oronly-service(if there are no workers)
Startup examples:
# Workers only
npm run start --only-worker
# Server only
npm run start --only-service
# Both (default)
npm run start
# Via environment variable
WORKER_MODE=only-worker npm run startLifecycle management
const server = new ServerService(config, routes, options);
// Start the server
await server.start();
// Get the Express app (after startup)
const app = server.getApp();
// Stop the server (graceful shutdown)
await server.stop();Graceful shutdown:
// Handle signals for proper termination
process.on('SIGTERM', async () => {
console.log('SIGTERM received, shutting down gracefully');
await server.stop();
process.exit(0);
});
process.on('SIGINT', async () => {
console.log('SIGINT received, shutting down gracefully');
await server.stop();
process.exit(0);
});Full usage example
import { config as ConfigDotenv } from "dotenv";
ConfigDotenv();
import "reflect-metadata";
import { instanceApp } from "@d2sutils/shopper";
import { ServerService, getRedis } from "@d2sutils/shopperv2";
import routes from "./api/routes";
import { routes as RoutesList } from "./api/routes/routes";
import { config } from "./config";
import { configureLogger } from "@d2sutils/logging";
import { AppDataSource } from "./config/data.source";
import { languageMiddleware } from "./middleware";
import { Worker } from "bullmq";
import {
ensureCacheCountries,
ensureCacheProgramTypes,
ensureCacheProjects,
} from "./utils/cache";
instanceApp(config.proxy, config.secret, config.redis_uri);
// Creating workers
const autosaveWorker = new Worker(
"answersAutosaveQueue",
async (job) => {
// Task processing
const redis = getRedis();
await redis.set(`job:${job.id}`, JSON.stringify(job.data));
},
{ connection: getRedis() as any }
);
const updateWorker = new Worker(
"updateQueue",
async (job) => {
// Task processing
},
{ connection: getRedis() as any }
);
const server = new ServerService(config, routes, {
hooks: {
afterListen: async () => {
// Import cron jobs after server startup
await import("./cron/execution.status.crone");
},
},
database: {
initialize: async () => {
await AppDataSource.initialize()
.then(() => {
console.warn("Data Source has been initialized!");
})
.catch((error) => {
console.error("Error during Data Source initialization", error);
throw error;
});
},
},
redis: {
initialize: () => {
// Redis initialization (if needed)
getRedis();
},
},
cache: {
functions: [
ensureCacheCountries,
ensureCacheProjects,
ensureCacheProgramTypes,
],
interval: 1 * 60 * 1000, // Refresh every minute
},
middleware: [languageMiddleware],
logger: {
configure: (proxy: string, name: string) => {
configureLogger(proxy, name);
},
},
routesList: RoutesList,
healthCheck: true,
errorHandling: true,
workers: [updateWorker, autosaveWorker],
});
export let app!: ReturnType<typeof server.getApp>;
server
.start()
.then(() => {
app = server.getApp();
console.log("Service started successfully");
})
.catch((error) => {
console.error("Error during service start", error);
process.exit(1);
});
// Graceful shutdown
process.on('SIGTERM', async () => {
console.log('SIGTERM received, shutting down gracefully');
await server.stop();
process.exit(0);
});Configuration options
| Option | Type | Description |
|--------|------|-------------|
| hooks | IServerServiceHooks | Hooks for various initialization stages |
| database | { initialize: () => Promise<void> } | Database initialization |
| redis | { initialize: () => void } | Redis initialization |
| cache | { functions: CacheFunction[], interval?: number } | Caching with automatic refresh |
| portCheck | { enabled: boolean, check: (port) => Promise<void> } | Port check before startup |
| middleware | RequestHandler[] | Custom Express middleware |
| expressJson | { limit?: string } | JSON middleware settings |
| expressUrlencoded | { limit?: string, extended?: boolean } | URL encoded middleware settings |
| connectionPool | IConnectionPoolConfig | Connection pool settings |
| dotenv | boolean | Load dotenv (default true) |
| healthCheck | boolean | Add /health endpoint |
| errorHandling | boolean | Global error handling |
| serviceUrl | string | Custom service URL |
| logger | { configure: (proxy, name) => void } | Logger configuration |
| routesList | any | List of routes for registration in the proxy |
| workers | Worker[] | Array of BullMQ workers |
| workerMode | 'only-worker' \| 'only-service' \| 'both' | Startup mode (optional) |
Features
- ✅ Automatic initialization -
instanceAppV2()is called automatically - ✅ Flexible routes - support for arrays, Router, and Express App
- ✅ Startup modes - automatic detection or explicit setting
- ✅ Graceful shutdown - proper closing of workers and server
- ✅ Caching - automatic refresh at an interval
- ✅ Hooks - extension points at various stages
- ✅ Health check - automatic
/healthendpoint - ✅ Error handling - global error handling
🚀 Development
Building the project
npm run buildDevelopment mode
npm run devProduction mode
npm run prodTesting
npm testPublishing
npm run publish📝 License
This project is licensed under the ISC License - see the LICENSE file for details.
Made with ❤️ by Dev2Studio
🇺🇦 Українська версія | 🇬🇧 English Version
