@exyconn/common 2.0.0 → 2.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +864 -261
- package/dist/{index-BLltj-zN.d.ts → client/hooks/index.d.mts} +1 -195
- package/dist/{index-CIUdLBjA.d.mts → client/hooks/index.d.ts} +1 -195
- package/dist/client/hooks/index.js +2276 -0
- package/dist/client/hooks/index.js.map +1 -0
- package/dist/client/hooks/index.mjs +2217 -0
- package/dist/client/hooks/index.mjs.map +1 -0
- package/dist/client/index.d.mts +4 -1
- package/dist/client/index.d.ts +4 -1
- package/dist/client/index.js +2693 -19
- package/dist/client/index.js.map +1 -1
- package/dist/client/index.mjs +2634 -21
- package/dist/client/index.mjs.map +1 -1
- package/dist/client/web/index.d.mts +1461 -0
- package/dist/client/web/index.d.ts +1461 -0
- package/dist/client/web/index.js +2681 -0
- package/dist/client/web/index.js.map +1 -0
- package/dist/client/web/index.mjs +2618 -0
- package/dist/client/web/index.mjs.map +1 -0
- package/dist/data/brand-identity.d.mts +149 -0
- package/dist/data/brand-identity.d.ts +149 -0
- package/dist/data/brand-identity.js +235 -0
- package/dist/data/brand-identity.js.map +1 -0
- package/dist/data/brand-identity.mjs +220 -0
- package/dist/data/brand-identity.mjs.map +1 -0
- package/dist/data/countries.d.mts +61 -0
- package/dist/data/countries.d.ts +61 -0
- package/dist/data/countries.js +987 -0
- package/dist/data/countries.js.map +1 -0
- package/dist/data/countries.mjs +971 -0
- package/dist/data/countries.mjs.map +1 -0
- package/dist/data/currencies.d.mts +19 -0
- package/dist/data/currencies.d.ts +19 -0
- package/dist/data/currencies.js +162 -0
- package/dist/data/currencies.js.map +1 -0
- package/dist/data/currencies.mjs +153 -0
- package/dist/data/currencies.mjs.map +1 -0
- package/dist/data/index.d.mts +7 -0
- package/dist/data/index.d.ts +7 -0
- package/dist/data/index.js +2087 -0
- package/dist/data/index.js.map +1 -0
- package/dist/data/index.mjs +1948 -0
- package/dist/data/index.mjs.map +1 -0
- package/dist/data/phone-codes.d.mts +15 -0
- package/dist/data/phone-codes.d.ts +15 -0
- package/dist/data/phone-codes.js +219 -0
- package/dist/data/phone-codes.js.map +1 -0
- package/dist/data/phone-codes.mjs +211 -0
- package/dist/data/phone-codes.mjs.map +1 -0
- package/dist/data/regex.d.mts +287 -0
- package/dist/data/regex.d.ts +287 -0
- package/dist/data/regex.js +306 -0
- package/dist/data/regex.js.map +1 -0
- package/dist/data/regex.mjs +208 -0
- package/dist/data/regex.mjs.map +1 -0
- package/dist/data/timezones.d.mts +16 -0
- package/dist/data/timezones.d.ts +16 -0
- package/dist/data/timezones.js +98 -0
- package/dist/data/timezones.js.map +1 -0
- package/dist/data/timezones.mjs +89 -0
- package/dist/data/timezones.mjs.map +1 -0
- package/dist/index-01hoqibP.d.ts +119 -0
- package/dist/index-D3yCCjBZ.d.mts +119 -0
- package/dist/index-D9a9oxQy.d.ts +305 -0
- package/dist/index-DKn4raO7.d.ts +222 -0
- package/dist/index-DuxL84IW.d.mts +305 -0
- package/dist/index-NS8dS0p9.d.mts +222 -0
- package/dist/index-Nqm5_lwT.d.ts +188 -0
- package/dist/index-jBi3V6e5.d.mts +188 -0
- package/dist/index.d.mts +21 -729
- package/dist/index.d.ts +21 -729
- package/dist/index.js +3470 -97
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +3457 -104
- package/dist/index.mjs.map +1 -1
- package/dist/server/configs/index.d.mts +602 -0
- package/dist/server/configs/index.d.ts +602 -0
- package/dist/server/configs/index.js +707 -0
- package/dist/server/configs/index.js.map +1 -0
- package/dist/server/configs/index.mjs +665 -0
- package/dist/server/configs/index.mjs.map +1 -0
- package/dist/server/index.d.mts +3 -0
- package/dist/server/index.d.ts +3 -0
- package/dist/server/index.js +699 -0
- package/dist/server/index.js.map +1 -1
- package/dist/server/index.mjs +662 -1
- package/dist/server/index.mjs.map +1 -1
- package/dist/shared/config/index.d.mts +40 -0
- package/dist/shared/config/index.d.ts +40 -0
- package/dist/shared/config/index.js +58 -0
- package/dist/shared/config/index.js.map +1 -0
- package/dist/shared/config/index.mjs +51 -0
- package/dist/shared/config/index.mjs.map +1 -0
- package/dist/shared/constants/index.d.mts +593 -0
- package/dist/shared/constants/index.d.ts +593 -0
- package/dist/shared/constants/index.js +391 -0
- package/dist/shared/constants/index.js.map +1 -0
- package/dist/shared/constants/index.mjs +360 -0
- package/dist/shared/constants/index.mjs.map +1 -0
- package/dist/shared/index.d.mts +5 -1
- package/dist/shared/index.d.ts +5 -1
- package/dist/shared/types/index.d.mts +140 -0
- package/dist/shared/types/index.d.ts +140 -0
- package/dist/shared/types/index.js +4 -0
- package/dist/shared/types/index.js.map +1 -0
- package/dist/shared/types/index.mjs +3 -0
- package/dist/shared/types/index.mjs.map +1 -0
- package/dist/shared/utils/index.d.mts +255 -0
- package/dist/shared/utils/index.d.ts +255 -0
- package/dist/shared/utils/index.js +623 -0
- package/dist/shared/utils/index.js.map +1 -0
- package/dist/shared/utils/index.mjs +324 -0
- package/dist/shared/utils/index.mjs.map +1 -0
- package/dist/shared/validation/index.d.mts +258 -0
- package/dist/shared/validation/index.d.ts +258 -0
- package/dist/shared/validation/index.js +185 -0
- package/dist/shared/validation/index.js.map +1 -0
- package/dist/shared/validation/index.mjs +172 -0
- package/dist/shared/validation/index.mjs.map +1 -0
- package/package.json +151 -56
- package/dist/index-DEzgM15j.d.ts +0 -67
- package/dist/index-DNFVgQx8.d.ts +0 -1375
- package/dist/index-DbV04Dx8.d.mts +0 -67
- package/dist/index-DfqEP6Oe.d.mts +0 -1375
package/README.md
CHANGED
|
@@ -1,6 +1,39 @@
|
|
|
1
1
|
# @exyconn/common
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/@exyconn/common)
|
|
4
|
+
[](https://github.com/exyconn/common/actions/workflows/ci.yml)
|
|
5
|
+
[](https://codecov.io/gh/exyconn/common)
|
|
6
|
+
[](https://opensource.org/licenses/MIT)
|
|
7
|
+
[](https://www.typescriptlang.org/)
|
|
8
|
+
[](https://bundlephobia.com/package/@exyconn/common)
|
|
9
|
+
[](https://common-docs.exyconn.com)
|
|
10
|
+
|
|
11
|
+
Production-ready common utilities, hooks, types, and data shared across all Exyconn projects (botify.life, exyconn.com, partywings.fun, sibera.work, spentiva.com).
|
|
12
|
+
|
|
13
|
+
## 📚 Documentation
|
|
14
|
+
|
|
15
|
+
**Full documentation available at: [common-docs.exyconn.com](https://common-docs.exyconn.com)**
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Table of Contents
|
|
20
|
+
|
|
21
|
+
- [Installation](#installation)
|
|
22
|
+
- [Features](#features)
|
|
23
|
+
- [Quick Start](#quick-start)
|
|
24
|
+
- [Module Reference](#module-reference)
|
|
25
|
+
- [Server](#server-module)
|
|
26
|
+
- [Client](#client-module)
|
|
27
|
+
- [Shared](#shared-module)
|
|
28
|
+
- [Data](#data-module)
|
|
29
|
+
- [React Hooks](#react-hooks)
|
|
30
|
+
- [API Reference](#api-reference)
|
|
31
|
+
- [Development](#development)
|
|
32
|
+
- [Peer Dependencies](#peer-dependencies)
|
|
33
|
+
- [Contributing](#contributing)
|
|
34
|
+
- [License](#license)
|
|
35
|
+
|
|
36
|
+
---
|
|
4
37
|
|
|
5
38
|
## Installation
|
|
6
39
|
|
|
@@ -12,333 +45,903 @@ yarn add @exyconn/common
|
|
|
12
45
|
pnpm add @exyconn/common
|
|
13
46
|
```
|
|
14
47
|
|
|
48
|
+
### Requirements
|
|
49
|
+
|
|
50
|
+
- Node.js >= 18.0.0
|
|
51
|
+
- React >= 18.0.0 (for hooks)
|
|
52
|
+
- TypeScript >= 5.0.0 (recommended)
|
|
53
|
+
|
|
15
54
|
## Features
|
|
16
55
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
56
|
+
| Category | Description |
|
|
57
|
+
|----------|-------------|
|
|
58
|
+
| 🖥️ **Server utilities** | Response helpers, middleware, logging, database connections, dynamic configs |
|
|
59
|
+
| 🌐 **Client utilities** | HTTP clients, React hooks (54+), response parsing |
|
|
60
|
+
| 📝 **Shared types** | TypeScript interfaces for API responses, users, etc. |
|
|
61
|
+
| 🗃️ **Data modules** | Countries, currencies, phone codes, timezones, brand logos |
|
|
62
|
+
| ✅ **Validation** | Common patterns, regex, and validation helpers (Zod integration) |
|
|
63
|
+
| 📅 **Date utilities** | Enhanced date-fns with timezone support |
|
|
64
|
+
| 🎨 **Brand Identity** | Complete brand configs with logos, colors, and SEO |
|
|
65
|
+
| 🔢 **Enums & Constants** | STATUS, SORT, ROLE, BREAKPOINTS, and more |
|
|
66
|
+
| ⚙️ **Dynamic Configs** | Production-ready CORS, Rate Limiting, Server configs |
|
|
67
|
+
| 🧪 **Fully Tested** | Comprehensive test coverage with Vitest |
|
|
25
68
|
|
|
26
|
-
|
|
69
|
+
---
|
|
27
70
|
|
|
28
|
-
|
|
71
|
+
## Quick Start
|
|
72
|
+
|
|
73
|
+
### Import by Module (Recommended)
|
|
74
|
+
|
|
75
|
+
```typescript
|
|
76
|
+
// Server utilities
|
|
77
|
+
import { successResponse, errorResponse } from '@exyconn/common/server/response';
|
|
78
|
+
import { createCorsOptions, createRateLimiter } from '@exyconn/common/server/configs';
|
|
79
|
+
import { createLogger } from '@exyconn/common/server/logger';
|
|
80
|
+
import { connectDB } from '@exyconn/common/server/db';
|
|
81
|
+
|
|
82
|
+
// Client utilities
|
|
83
|
+
import { useLocalStorage, useDebounce } from '@exyconn/common/client/hooks';
|
|
84
|
+
import { createHttpClient } from '@exyconn/common/client/http';
|
|
85
|
+
|
|
86
|
+
// Shared utilities
|
|
87
|
+
import { isValidEmail, VALIDATION_PATTERNS } from '@exyconn/common/shared/validation';
|
|
88
|
+
import { getEnv, isProd } from '@exyconn/common/shared/config';
|
|
89
|
+
|
|
90
|
+
// Data modules
|
|
91
|
+
import { countries, getCountryByCode } from '@exyconn/common/data/countries';
|
|
92
|
+
import { BRANDS, getBrandById } from '@exyconn/common/data/brand-identity';
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### Import Everything (Not Recommended for Bundle Size)
|
|
29
96
|
|
|
30
97
|
```typescript
|
|
31
98
|
import { server, client, shared, data } from '@exyconn/common';
|
|
32
99
|
```
|
|
33
100
|
|
|
34
|
-
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## Module Reference
|
|
104
|
+
|
|
105
|
+
### Server Module
|
|
106
|
+
|
|
107
|
+
**Import:** `@exyconn/common/server` or specific submodules
|
|
108
|
+
|
|
109
|
+
#### Server Configs (`@exyconn/common/server/configs`)
|
|
110
|
+
|
|
111
|
+
Dynamic configuration system for production applications.
|
|
112
|
+
|
|
113
|
+
##### ConfigBuilder
|
|
35
114
|
|
|
36
115
|
```typescript
|
|
37
|
-
|
|
38
|
-
|
|
116
|
+
import {
|
|
117
|
+
createConfig,
|
|
118
|
+
buildConfig,
|
|
119
|
+
ConfigBuilder,
|
|
120
|
+
// Types
|
|
121
|
+
type AppConfig,
|
|
122
|
+
type ServerConfig,
|
|
123
|
+
type DatabaseConfig,
|
|
124
|
+
type AuthConfig,
|
|
125
|
+
type LoggingConfig,
|
|
126
|
+
} from '@exyconn/common/server/configs';
|
|
127
|
+
|
|
128
|
+
// Method 1: Using ConfigBuilder (fluent API)
|
|
129
|
+
const config = createConfig()
|
|
130
|
+
.setServer({
|
|
131
|
+
name: 'my-api-server',
|
|
132
|
+
port: 4000,
|
|
133
|
+
environment: 'production',
|
|
134
|
+
})
|
|
135
|
+
.setDatabase({
|
|
136
|
+
uri: process.env.MONGODB_URI!,
|
|
137
|
+
maxPoolSize: 50,
|
|
138
|
+
})
|
|
139
|
+
.setAuth({
|
|
140
|
+
jwtSecret: process.env.JWT_SECRET!,
|
|
141
|
+
jwtExpiresIn: '7d',
|
|
142
|
+
})
|
|
143
|
+
.setLogging({
|
|
144
|
+
level: 'info',
|
|
145
|
+
file: true,
|
|
146
|
+
})
|
|
147
|
+
.addProductionOrigin('https://myapp.com')
|
|
148
|
+
.addProductionOrigin('https://api.myapp.com')
|
|
149
|
+
.addCorsPattern('.myapp.com')
|
|
150
|
+
.addRateLimitTier('upload', {
|
|
151
|
+
windowMs: 60000,
|
|
152
|
+
maxRequests: 5,
|
|
153
|
+
message: 'Upload limit exceeded',
|
|
154
|
+
})
|
|
155
|
+
.setCustom('featureFlags', { newFeature: true })
|
|
156
|
+
.loadFromEnv()
|
|
157
|
+
.build();
|
|
158
|
+
|
|
159
|
+
// Method 2: Quick build from partial config
|
|
160
|
+
const quickConfig = buildConfig({
|
|
161
|
+
server: { name: 'quick-server', port: 3000 },
|
|
162
|
+
database: { uri: 'mongodb://localhost/mydb' },
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
// Validate configuration
|
|
166
|
+
const validation = createConfig().setServer({ name: '' }).validate();
|
|
167
|
+
if (!validation.valid) {
|
|
168
|
+
console.error('Config errors:', validation.errors);
|
|
169
|
+
}
|
|
170
|
+
```
|
|
39
171
|
|
|
40
|
-
|
|
41
|
-
|
|
172
|
+
**ConfigBuilder Methods:**
|
|
173
|
+
|
|
174
|
+
| Method | Parameters | Description |
|
|
175
|
+
|--------|------------|-------------|
|
|
176
|
+
| `setServer(config)` | `Partial<ServerConfig>` | Set server configuration |
|
|
177
|
+
| `setDatabase(config)` | `Partial<DatabaseConfig>` | Set database configuration |
|
|
178
|
+
| `setAuth(config)` | `Partial<AuthConfig>` | Set auth configuration |
|
|
179
|
+
| `setLogging(config)` | `Partial<LoggingConfig>` | Set logging configuration |
|
|
180
|
+
| `setCorsOrigins(config)` | `Partial<CorsOriginsConfig>` | Set CORS origins |
|
|
181
|
+
| `addProductionOrigin(origin)` | `string` | Add a production CORS origin |
|
|
182
|
+
| `addDevelopmentOrigin(origin)` | `string` | Add a development CORS origin |
|
|
183
|
+
| `addCorsPattern(pattern)` | `string` | Add subdomain pattern |
|
|
184
|
+
| `setRateLimit(config)` | `Partial<RateLimitConfig>` | Set rate limit config |
|
|
185
|
+
| `addRateLimitTier(name, tier)` | `string, RateLimitTier` | Add custom rate limit tier |
|
|
186
|
+
| `setCustom(key, value)` | `string, unknown` | Set custom config value |
|
|
187
|
+
| `loadFromEnv()` | - | Load config from environment variables |
|
|
188
|
+
| `validate()` | - | Validate configuration, returns `{ valid, errors }` |
|
|
189
|
+
| `build()` | - | Build final configuration |
|
|
190
|
+
|
|
191
|
+
##### CORS Configuration
|
|
42
192
|
|
|
43
|
-
|
|
44
|
-
import {
|
|
193
|
+
```typescript
|
|
194
|
+
import {
|
|
195
|
+
createCorsOptions,
|
|
196
|
+
createBrandCorsOptions,
|
|
197
|
+
createMultiBrandCorsOptions,
|
|
198
|
+
// Presets
|
|
199
|
+
DEFAULT_CORS_CONFIG,
|
|
200
|
+
EXYCONN_CORS_CONFIG,
|
|
201
|
+
STRICT_CORS_CONFIG,
|
|
202
|
+
PERMISSIVE_CORS_CONFIG,
|
|
203
|
+
// Types
|
|
204
|
+
type CorsConfig,
|
|
205
|
+
} from '@exyconn/common/server/configs';
|
|
206
|
+
|
|
207
|
+
// Custom CORS configuration
|
|
208
|
+
const corsOptions = createCorsOptions({
|
|
209
|
+
productionOrigins: ['https://myapp.com', 'https://api.myapp.com'],
|
|
210
|
+
developmentOrigins: ['http://localhost:3000', 'http://localhost:5173'],
|
|
211
|
+
allowedSubdomains: ['.myapp.com'],
|
|
212
|
+
originPatterns: [/\.myapp\.(com|io)$/],
|
|
213
|
+
allowNoOrigin: true, // Allow mobile apps, curl
|
|
214
|
+
allowAllInDev: true, // Allow all in development
|
|
215
|
+
credentials: true,
|
|
216
|
+
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'],
|
|
217
|
+
allowedHeaders: ['Content-Type', 'Authorization', 'X-API-Key'],
|
|
218
|
+
exposedHeaders: ['X-Total-Count'],
|
|
219
|
+
maxAge: 86400, // 24 hours preflight cache
|
|
220
|
+
customValidator: (origin) => origin.includes('trusted'), // Custom validation
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
// Single brand CORS (auto-includes www and subdomains)
|
|
224
|
+
const brandCors = createBrandCorsOptions('myapp.com', {
|
|
225
|
+
credentials: true,
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
// Multi-brand CORS
|
|
229
|
+
const multiBrandCors = createMultiBrandCorsOptions(
|
|
230
|
+
['myapp.com', 'myapp.io', 'api.myapp.com'],
|
|
231
|
+
{ allowNoOrigin: false }
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
// Usage with Express
|
|
235
|
+
import cors from 'cors';
|
|
236
|
+
app.use(cors(corsOptions));
|
|
237
|
+
```
|
|
45
238
|
|
|
46
|
-
|
|
47
|
-
|
|
239
|
+
**CorsConfig Options:**
|
|
240
|
+
|
|
241
|
+
| Option | Type | Default | Required | Description |
|
|
242
|
+
|--------|------|---------|----------|-------------|
|
|
243
|
+
| `productionOrigins` | `string[]` | `[]` | No | Allowed production origins |
|
|
244
|
+
| `developmentOrigins` | `string[]` | Common localhost ports | No | Allowed development origins |
|
|
245
|
+
| `allowedSubdomains` | `string[]` | `[]` | No | Subdomain patterns (e.g., `.myapp.com`) |
|
|
246
|
+
| `originPatterns` | `RegExp[]` | `[]` | No | Regex patterns for origin matching |
|
|
247
|
+
| `allowNoOrigin` | `boolean` | `true` | No | Allow requests with no origin |
|
|
248
|
+
| `allowAllInDev` | `boolean` | `true` | No | Allow all origins in development |
|
|
249
|
+
| `customValidator` | `(origin: string) => boolean` | - | No | Custom origin validator |
|
|
250
|
+
| `credentials` | `boolean` | `true` | No | Include credentials |
|
|
251
|
+
| `methods` | `string[]` | All standard methods | No | Allowed HTTP methods |
|
|
252
|
+
| `allowedHeaders` | `string[]` | Common headers | No | Allowed request headers |
|
|
253
|
+
| `exposedHeaders` | `string[]` | Common response headers | No | Exposed response headers |
|
|
254
|
+
| `maxAge` | `number` | `86400` | No | Preflight cache duration (seconds) |
|
|
255
|
+
|
|
256
|
+
##### Rate Limiter Configuration
|
|
48
257
|
|
|
49
|
-
|
|
50
|
-
import {
|
|
258
|
+
```typescript
|
|
259
|
+
import {
|
|
260
|
+
// Factory functions
|
|
261
|
+
createRateLimiter,
|
|
262
|
+
createStandardRateLimiter,
|
|
263
|
+
createStrictRateLimiter,
|
|
264
|
+
createDdosRateLimiter,
|
|
265
|
+
createApiRateLimiter,
|
|
266
|
+
// Builder
|
|
267
|
+
rateLimiter,
|
|
268
|
+
RateLimiterBuilder,
|
|
269
|
+
// Key generators
|
|
270
|
+
defaultKeyGenerator,
|
|
271
|
+
createPrefixedKeyGenerator,
|
|
272
|
+
createUserKeyGenerator,
|
|
273
|
+
createApiKeyGenerator,
|
|
274
|
+
// Constants
|
|
275
|
+
DEFAULT_RATE_LIMIT_TIERS,
|
|
276
|
+
// Types
|
|
277
|
+
type RateLimitTierConfig,
|
|
278
|
+
} from '@exyconn/common/server/configs';
|
|
279
|
+
|
|
280
|
+
// Method 1: Factory functions with defaults
|
|
281
|
+
const standardLimiter = createStandardRateLimiter(); // 100 req/15min
|
|
282
|
+
const strictLimiter = createStrictRateLimiter(); // 20 req/15min
|
|
283
|
+
const ddosLimiter = createDdosRateLimiter(); // 60 req/1min
|
|
284
|
+
const apiLimiter = createApiRateLimiter(); // 30 req/1min (by API key)
|
|
285
|
+
|
|
286
|
+
// Method 2: Custom configuration
|
|
287
|
+
const customLimiter = createRateLimiter({
|
|
288
|
+
windowMs: 5 * 60 * 1000, // 5 minutes
|
|
289
|
+
maxRequests: 50,
|
|
290
|
+
message: 'Too many requests, please slow down.',
|
|
291
|
+
skipSuccessfulRequests: false,
|
|
292
|
+
skipFailedRequests: false,
|
|
293
|
+
}, {
|
|
294
|
+
standardHeaders: true,
|
|
295
|
+
legacyHeaders: false,
|
|
296
|
+
keyGenerator: defaultKeyGenerator,
|
|
297
|
+
skip: (req) => req.headers['x-skip-rate-limit'] === 'true',
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
// Method 3: Builder pattern (most flexible)
|
|
301
|
+
const uploadLimiter = rateLimiter('STRICT')
|
|
302
|
+
.windowMinutes(5)
|
|
303
|
+
.max(10)
|
|
304
|
+
.message('Upload limit exceeded. Please wait.')
|
|
305
|
+
.keyByIp()
|
|
306
|
+
.skipWhen((req) => req.user?.isPremium)
|
|
307
|
+
.build();
|
|
308
|
+
|
|
309
|
+
// User-based rate limiting
|
|
310
|
+
const userLimiter = rateLimiter()
|
|
311
|
+
.windowHours(1)
|
|
312
|
+
.max(1000)
|
|
313
|
+
.keyBy((req) => req.userId || defaultKeyGenerator(req))
|
|
314
|
+
.build();
|
|
315
|
+
|
|
316
|
+
// API key based rate limiting
|
|
317
|
+
const apiKeyLimiter = rateLimiter('API')
|
|
318
|
+
.keyByApiKey('x-api-key')
|
|
319
|
+
.build();
|
|
320
|
+
|
|
321
|
+
// Usage with Express
|
|
322
|
+
app.use('/api/', ddosLimiter);
|
|
323
|
+
app.use('/api/', standardLimiter);
|
|
324
|
+
app.post('/api/auth/login', strictLimiter);
|
|
325
|
+
app.post('/api/upload', uploadLimiter);
|
|
51
326
|
```
|
|
52
327
|
|
|
53
|
-
|
|
328
|
+
**RateLimiterBuilder Methods:**
|
|
329
|
+
|
|
330
|
+
| Method | Parameters | Description |
|
|
331
|
+
|--------|------------|-------------|
|
|
332
|
+
| `windowMs(ms)` | `number` | Set window in milliseconds |
|
|
333
|
+
| `windowMinutes(minutes)` | `number` | Set window in minutes |
|
|
334
|
+
| `windowHours(hours)` | `number` | Set window in hours |
|
|
335
|
+
| `max(requests)` | `number` | Set max requests in window |
|
|
336
|
+
| `message(msg)` | `string` | Set error message |
|
|
337
|
+
| `skipSuccessful(skip?)` | `boolean` | Skip successful requests (default: true) |
|
|
338
|
+
| `skipFailed(skip?)` | `boolean` | Skip failed requests (default: true) |
|
|
339
|
+
| `keyBy(generator)` | `(req) => string` | Custom key generator |
|
|
340
|
+
| `keyByIp()` | - | Key by IP address (default) |
|
|
341
|
+
| `keyByApiKey(headerName?)` | `string` | Key by API key header |
|
|
342
|
+
| `skipWhen(predicate)` | `(req) => boolean` | Skip when condition is true |
|
|
343
|
+
| `build()` | - | Build rate limiter middleware |
|
|
344
|
+
|
|
345
|
+
**Available Rate Limit Tiers:**
|
|
346
|
+
|
|
347
|
+
| Tier | Window | Max Requests | Use Case |
|
|
348
|
+
|------|--------|--------------|----------|
|
|
349
|
+
| `STANDARD` | 15 min | 100 | General API endpoints |
|
|
350
|
+
| `STRICT` | 15 min | 20 | Auth endpoints (login, signup) |
|
|
351
|
+
| `DDOS` | 1 min | 60 | Global DDoS protection |
|
|
352
|
+
| `VERY_STRICT` | 1 hour | 5 | Password reset, sensitive actions |
|
|
353
|
+
| `RELAXED` | 15 min | 500 | High-traffic endpoints |
|
|
354
|
+
| `API` | 1 min | 30 | Third-party API access |
|
|
355
|
+
|
|
356
|
+
#### Response Helpers (`@exyconn/common/server/response`)
|
|
54
357
|
|
|
55
358
|
```typescript
|
|
56
|
-
|
|
57
|
-
|
|
359
|
+
import {
|
|
360
|
+
// Success responses
|
|
361
|
+
successResponse, // 200 OK
|
|
362
|
+
successResponseArr, // 200 OK with pagination
|
|
363
|
+
createdResponse, // 201 Created
|
|
364
|
+
noContentResponse, // 204 No Content
|
|
365
|
+
|
|
366
|
+
// Error responses
|
|
367
|
+
badRequestResponse, // 400 Bad Request
|
|
368
|
+
unauthorizedResponse, // 401 Unauthorized
|
|
369
|
+
forbiddenResponse, // 403 Forbidden
|
|
370
|
+
notFoundResponse, // 404 Not Found
|
|
371
|
+
conflictResponse, // 409 Conflict
|
|
372
|
+
serverErrorResponse, // 500 Internal Server Error
|
|
373
|
+
|
|
374
|
+
// Utilities
|
|
375
|
+
extractColumns,
|
|
376
|
+
|
|
377
|
+
// Types
|
|
378
|
+
type ApiResponse,
|
|
379
|
+
type PaginationData,
|
|
380
|
+
type ColumnMetadata,
|
|
381
|
+
} from '@exyconn/common/server/response';
|
|
382
|
+
|
|
383
|
+
// Basic success response
|
|
384
|
+
app.get('/api/user/:id', async (req, res) => {
|
|
385
|
+
const user = await User.findById(req.params.id);
|
|
386
|
+
return successResponse(res, user, 'User retrieved successfully');
|
|
387
|
+
});
|
|
388
|
+
// Response: { status: 'success', statusCode: 200, message: '...', data: {...} }
|
|
389
|
+
|
|
390
|
+
// Paginated array response (auto-extracts columns for table rendering)
|
|
391
|
+
app.get('/api/users', async (req, res) => {
|
|
392
|
+
const { page = 1, limit = 10 } = req.query;
|
|
393
|
+
const users = await User.find().skip((page - 1) * limit).limit(limit);
|
|
394
|
+
const total = await User.countDocuments();
|
|
395
|
+
|
|
396
|
+
return successResponseArr(res, users, {
|
|
397
|
+
total,
|
|
398
|
+
page,
|
|
399
|
+
limit,
|
|
400
|
+
totalPages: Math.ceil(total / limit),
|
|
401
|
+
hasNextPage: page < Math.ceil(total / limit),
|
|
402
|
+
hasPrevPage: page > 1,
|
|
403
|
+
}, 'Users retrieved');
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
// Error responses
|
|
407
|
+
return badRequestResponse(res, 'Invalid email format', { field: 'email' });
|
|
408
|
+
return unauthorizedResponse(res, 'Token expired');
|
|
409
|
+
return forbiddenResponse(res, 'Insufficient permissions');
|
|
410
|
+
return notFoundResponse(res, 'User not found');
|
|
411
|
+
return serverErrorResponse(res, 'Database connection failed');
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
**Response Functions:**
|
|
415
|
+
|
|
416
|
+
| Function | Status | Parameters | Description |
|
|
417
|
+
|----------|--------|------------|-------------|
|
|
418
|
+
| `successResponse` | 200 | `(res, data, message?)` | Standard success |
|
|
419
|
+
| `successResponseArr` | 200 | `(res, data[], pagination?, message?)` | Paginated array |
|
|
420
|
+
| `createdResponse` | 201 | `(res, data, message?)` | Resource created |
|
|
421
|
+
| `noContentResponse` | 200* | `(res, data?, message?)` | No data found |
|
|
422
|
+
| `badRequestResponse` | 400 | `(res, message?, errors?)` | Invalid request |
|
|
423
|
+
| `unauthorizedResponse` | 401 | `(res, message?)` | Authentication required |
|
|
424
|
+
| `forbiddenResponse` | 403 | `(res, message?)` | Access denied |
|
|
425
|
+
| `notFoundResponse` | 404 | `(res, message?)` | Resource not found |
|
|
426
|
+
| `conflictResponse` | 409 | `(res, message?)` | Resource conflict |
|
|
427
|
+
| `serverErrorResponse` | 500 | `(res, message?, error?)` | Server error |
|
|
428
|
+
|
|
429
|
+
#### Logger (`@exyconn/common/server/logger`)
|
|
58
430
|
|
|
59
|
-
|
|
431
|
+
```typescript
|
|
60
432
|
import {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
433
|
+
createLogger,
|
|
434
|
+
logger, // Default instance
|
|
435
|
+
simpleLogger, // Console-only logger
|
|
436
|
+
createMorganStream,
|
|
437
|
+
stream, // Morgan stream for default logger
|
|
438
|
+
type LoggerConfig,
|
|
439
|
+
} from '@exyconn/common/server/logger';
|
|
440
|
+
|
|
441
|
+
// Default logger (writes to console + files)
|
|
442
|
+
logger.info('Server started', { port: 3000 });
|
|
443
|
+
logger.error('Database error', { error: err.message, stack: err.stack });
|
|
444
|
+
logger.warn('Rate limit approaching');
|
|
445
|
+
logger.debug('Request received', { method: 'GET', path: '/api/users' });
|
|
446
|
+
|
|
447
|
+
// Custom logger
|
|
448
|
+
const customLogger = createLogger({
|
|
449
|
+
level: 'debug', // 'error' | 'warn' | 'info' | 'http' | 'debug'
|
|
450
|
+
logsDir: 'logs', // Directory for log files
|
|
451
|
+
maxSize: '20m', // Max file size before rotation
|
|
452
|
+
maxFiles: '14d', // Keep logs for 14 days
|
|
453
|
+
errorMaxFiles: '30d', // Keep error logs longer
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
// Simple logger (no files, just console)
|
|
457
|
+
simpleLogger.info('Quick debug message');
|
|
458
|
+
|
|
459
|
+
// With Morgan HTTP logger
|
|
460
|
+
import morgan from 'morgan';
|
|
461
|
+
app.use(morgan('combined', { stream }));
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
#### Database (`@exyconn/common/server/db`)
|
|
465
|
+
|
|
466
|
+
```typescript
|
|
75
467
|
import {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
468
|
+
connectDB,
|
|
469
|
+
disconnectDB,
|
|
470
|
+
getConnectionStatus,
|
|
471
|
+
type DbConnectionOptions,
|
|
472
|
+
} from '@exyconn/common/server/db';
|
|
473
|
+
|
|
474
|
+
// Connect with default options
|
|
475
|
+
await connectDB(process.env.MONGODB_URI!, {}, logger);
|
|
476
|
+
|
|
477
|
+
// Connect with custom options
|
|
478
|
+
await connectDB(process.env.MONGODB_URI!, {
|
|
479
|
+
maxPoolSize: 50,
|
|
480
|
+
minPoolSize: 10,
|
|
481
|
+
socketTimeoutMS: 45000,
|
|
482
|
+
retryWrites: true,
|
|
483
|
+
}, logger);
|
|
484
|
+
|
|
485
|
+
// Check status
|
|
486
|
+
const status = getConnectionStatus(); // 'connected' | 'disconnected' | ...
|
|
487
|
+
|
|
488
|
+
// Disconnect gracefully
|
|
489
|
+
await disconnectDB(logger);
|
|
82
490
|
```
|
|
83
491
|
|
|
84
|
-
|
|
492
|
+
#### Middleware (`@exyconn/common/server/middleware`)
|
|
85
493
|
|
|
86
494
|
```typescript
|
|
87
|
-
import
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
495
|
+
import {
|
|
496
|
+
authenticateJWT,
|
|
497
|
+
optionalAuthenticateJWT,
|
|
498
|
+
authenticateApiKey,
|
|
499
|
+
extractOrganization,
|
|
500
|
+
type AuthRequest,
|
|
501
|
+
type JWTPayload,
|
|
502
|
+
} from '@exyconn/common/server/middleware';
|
|
503
|
+
|
|
504
|
+
// JWT Authentication (required)
|
|
505
|
+
app.use('/api/protected', authenticateJWT(process.env.JWT_SECRET!));
|
|
506
|
+
|
|
507
|
+
// JWT Authentication (optional)
|
|
508
|
+
app.use('/api/public', optionalAuthenticateJWT(process.env.JWT_SECRET!));
|
|
509
|
+
|
|
510
|
+
// API Key Authentication
|
|
511
|
+
app.use('/api/external', authenticateApiKey(async (key) => {
|
|
512
|
+
const apiKey = await ApiKey.findOne({ key, isActive: true });
|
|
513
|
+
return { valid: !!apiKey, organizationId: apiKey?.organizationId };
|
|
514
|
+
}));
|
|
515
|
+
|
|
516
|
+
// Access auth data in routes
|
|
517
|
+
app.get('/api/profile', authenticateJWT(secret), (req: AuthRequest, res) => {
|
|
518
|
+
const userId = req.userId;
|
|
519
|
+
const orgId = req.organizationId;
|
|
520
|
+
});
|
|
94
521
|
```
|
|
95
522
|
|
|
96
|
-
|
|
523
|
+
#### Server Utils (`@exyconn/common/server/utils`)
|
|
524
|
+
|
|
525
|
+
```typescript
|
|
526
|
+
import {
|
|
527
|
+
buildFilter,
|
|
528
|
+
buildPagination,
|
|
529
|
+
buildPaginationMeta,
|
|
530
|
+
} from '@exyconn/common/server/utils';
|
|
531
|
+
|
|
532
|
+
// Build MongoDB filter dynamically
|
|
533
|
+
const filter = buildFilter({
|
|
534
|
+
organizationId: req.organizationId,
|
|
535
|
+
search: req.query.q,
|
|
536
|
+
searchFields: ['name', 'email', 'phone'],
|
|
537
|
+
status: req.query.status,
|
|
538
|
+
startDate: req.query.from,
|
|
539
|
+
endDate: req.query.to,
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
// Calculate pagination
|
|
543
|
+
const pagination = buildPagination({
|
|
544
|
+
page: parseInt(req.query.page) || 1,
|
|
545
|
+
limit: parseInt(req.query.limit),
|
|
546
|
+
defaultLimit: 10,
|
|
547
|
+
maxLimit: 100,
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
// Build pagination meta
|
|
551
|
+
const meta = buildPaginationMeta(totalCount, pagination.page, pagination.limit);
|
|
552
|
+
```
|
|
553
|
+
|
|
554
|
+
---
|
|
555
|
+
|
|
556
|
+
### Client Module
|
|
557
|
+
|
|
558
|
+
**Import:** `@exyconn/common/client` or specific submodules
|
|
559
|
+
|
|
560
|
+
#### React Hooks (`@exyconn/common/client/hooks`)
|
|
561
|
+
|
|
562
|
+
50+ production-ready React hooks organized by category.
|
|
563
|
+
|
|
564
|
+
##### State & Logic Hooks
|
|
565
|
+
|
|
566
|
+
| Hook | Description | Example |
|
|
567
|
+
|------|-------------|---------|
|
|
568
|
+
| `useToggle` | Boolean toggle | `const [isOpen, toggle] = useToggle(false)` |
|
|
569
|
+
| `useCounter` | Numeric counter | `const { count, increment, decrement } = useCounter(0)` |
|
|
570
|
+
| `usePrevious` | Track previous value | `const prevValue = usePrevious(value)` |
|
|
571
|
+
| `useObjectState` | Partial state updates | `const [state, setState] = useObjectState({ a: 1 })` |
|
|
572
|
+
| `useHistoryState` | State with undo/redo | `const [value, setValue, { undo, redo }] = useHistoryState('')` |
|
|
573
|
+
| `useList` | Array operations | `const [list, { push, removeAt }] = useList([])` |
|
|
574
|
+
| `useMap` | Map operations | `const [map, { set, remove }] = useMap()` |
|
|
575
|
+
| `useSet` | Set operations | `const [set, { add, toggle }] = useSet()` |
|
|
576
|
+
|
|
577
|
+
##### Side Effects & Timing Hooks
|
|
578
|
+
|
|
579
|
+
| Hook | Description | Example |
|
|
580
|
+
|------|-------------|---------|
|
|
581
|
+
| `useDebounce` | Debounce value | `const debouncedSearch = useDebounce(search, 500)` |
|
|
582
|
+
| `useThrottle` | Throttle value | `const throttled = useThrottle(value, 100)` |
|
|
583
|
+
| `useTimeout` | Execute after delay | `const { reset, clear } = useTimeout(fn, 3000)` |
|
|
584
|
+
| `useInterval` | Repeated execution | `const { start, stop } = useInterval(fn, 1000)` |
|
|
585
|
+
| `useCountdown` | Countdown timer | `const { count, start, stop } = useCountdown({ seconds: 60 })` |
|
|
586
|
+
|
|
587
|
+
##### Browser & Document Hooks
|
|
588
|
+
|
|
589
|
+
| Hook | Description | Example |
|
|
590
|
+
|------|-------------|---------|
|
|
591
|
+
| `useWindowSize` | Track dimensions | `const { width, height } = useWindowSize()` |
|
|
592
|
+
| `useWindowScroll` | Track scroll | `const { y, scrollToTop } = useWindowScroll()` |
|
|
593
|
+
| `useDocumentTitle` | Set title | `useDocumentTitle('My Page')` |
|
|
594
|
+
| `useVisibilityChange` | Tab visibility | `const isVisible = useVisibilityChange()` |
|
|
595
|
+
| `useLockBodyScroll` | Prevent scroll | `useLockBodyScroll(isModalOpen)` |
|
|
596
|
+
| `useIsClient` | Client check | `const isClient = useIsClient()` |
|
|
597
|
+
|
|
598
|
+
##### Events & Interaction Hooks
|
|
599
|
+
|
|
600
|
+
| Hook | Description | Example |
|
|
601
|
+
|------|-------------|---------|
|
|
602
|
+
| `useEventListener` | Attach events | `useEventListener('keydown', handler)` |
|
|
603
|
+
| `useKeyPress` | Detect key | `const isEnter = useKeyPress('Enter')` |
|
|
604
|
+
| `useHover` | Track hover | `const [ref, isHovered] = useHover()` |
|
|
605
|
+
| `useClickAway` | Click outside | `useClickAway(ref, closeHandler)` |
|
|
606
|
+
| `useLongPress` | Long press | `const props = useLongPress(handler, { delay: 500 })` |
|
|
607
|
+
| `useCopyToClipboard` | Copy text | `const [copied, copy] = useCopyToClipboard()` |
|
|
608
|
+
|
|
609
|
+
##### Media & Device Hooks
|
|
610
|
+
|
|
611
|
+
| Hook | Description | Example |
|
|
612
|
+
|------|-------------|---------|
|
|
613
|
+
| `useMediaQuery` | Responsive | `const isMobile = useMediaQuery('(max-width: 768px)')` |
|
|
614
|
+
| `useBattery` | Battery status | `const { level, charging } = useBattery()` |
|
|
615
|
+
| `useNetworkState` | Network info | `const { online, effectiveType } = useNetworkState()` |
|
|
616
|
+
| `useIdle` | User inactivity | `const isIdle = useIdle(30000)` |
|
|
617
|
+
| `useGeolocation` | User location | `const { latitude, longitude } = useGeolocation()` |
|
|
618
|
+
| `useThemeDetector` | System theme | `const isDark = useThemeDetector()` |
|
|
619
|
+
|
|
620
|
+
##### Storage Hooks
|
|
621
|
+
|
|
622
|
+
| Hook | Description | Example |
|
|
623
|
+
|------|-------------|---------|
|
|
624
|
+
| `useLocalStorage` | Persist in localStorage | `const [value, setValue] = useLocalStorage('key', initial)` |
|
|
625
|
+
| `useSessionStorage` | Session storage | `const [value, setValue] = useSessionStorage('key', initial)` |
|
|
626
|
+
|
|
627
|
+
##### Data Fetching Hooks
|
|
628
|
+
|
|
629
|
+
| Hook | Description | Example |
|
|
630
|
+
|------|-------------|---------|
|
|
631
|
+
| `useFetch` | Data fetching | `const { data, loading, error } = useFetch('/api/data')` |
|
|
632
|
+
| `useScript` | Load scripts | `const { loaded, error } = useScript('https://...')` |
|
|
633
|
+
|
|
634
|
+
##### Performance Hooks
|
|
635
|
+
|
|
636
|
+
| Hook | Description | Example |
|
|
637
|
+
|------|-------------|---------|
|
|
638
|
+
| `useRenderCount` | Count renders | `const count = useRenderCount()` |
|
|
639
|
+
| `useMeasure` | Element dimensions | `const [ref, { width, height }] = useMeasure()` |
|
|
640
|
+
| `useIntersectionObserver` | Visibility detection | `const [ref, entry] = useIntersectionObserver()` |
|
|
641
|
+
|
|
642
|
+
#### HTTP Client (`@exyconn/common/client/http`)
|
|
643
|
+
|
|
644
|
+
```typescript
|
|
645
|
+
import { createHttpClient, withFormData, withTimeout } from '@exyconn/common/client/http';
|
|
646
|
+
|
|
647
|
+
const api = createHttpClient({
|
|
648
|
+
baseURL: 'https://api.example.com',
|
|
649
|
+
timeout: 30000,
|
|
650
|
+
getAuthToken: () => localStorage.getItem('token'),
|
|
651
|
+
onUnauthorized: () => window.location.href = '/login',
|
|
652
|
+
});
|
|
653
|
+
|
|
654
|
+
const users = await api.get('/users');
|
|
655
|
+
const newUser = await api.post('/users', { name: 'John' });
|
|
656
|
+
const file = await api.post('/upload', formData, withFormData());
|
|
657
|
+
```
|
|
658
|
+
|
|
659
|
+
---
|
|
660
|
+
|
|
661
|
+
### Shared Module
|
|
662
|
+
|
|
663
|
+
**Import:** `@exyconn/common/shared` or specific submodules
|
|
664
|
+
|
|
665
|
+
#### Validation (`@exyconn/common/shared/validation`)
|
|
97
666
|
|
|
98
667
|
```typescript
|
|
99
668
|
import {
|
|
100
669
|
VALIDATION_PATTERNS,
|
|
101
|
-
VALIDATION_MESSAGES,
|
|
102
670
|
isValidEmail,
|
|
103
671
|
isValidPassword,
|
|
104
672
|
isValidPhone,
|
|
105
|
-
isValidUrl
|
|
673
|
+
isValidUrl,
|
|
106
674
|
} from '@exyconn/common/shared/validation';
|
|
675
|
+
|
|
676
|
+
isValidEmail('test@example.com'); // true
|
|
677
|
+
isValidPassword('Password1!', 'strong'); // true
|
|
678
|
+
isValidPhone('+1 234 567 8901'); // true
|
|
679
|
+
|
|
680
|
+
// Use patterns directly
|
|
681
|
+
VALIDATION_PATTERNS.EMAIL.test('test@example.com');
|
|
682
|
+
VALIDATION_PATTERNS.SLUG.test('my-blog-post');
|
|
107
683
|
```
|
|
108
684
|
|
|
109
|
-
|
|
685
|
+
**Available Patterns:**
|
|
686
|
+
|
|
687
|
+
| Pattern | Description |
|
|
688
|
+
|---------|-------------|
|
|
689
|
+
| `EMAIL` | Email validation |
|
|
690
|
+
| `PASSWORD_STRONG` | Strong password (8+ chars, upper, lower, number, special) |
|
|
691
|
+
| `PASSWORD_MEDIUM` | Medium password (8+ chars, upper, lower, number) |
|
|
692
|
+
| `PHONE_INTERNATIONAL` | International phone |
|
|
693
|
+
| `URL` / `URL_STRICT` | URL validation |
|
|
694
|
+
| `SLUG` | Kebab-case slug |
|
|
695
|
+
| `USERNAME` | Username (3-30 chars, alphanumeric, _, -) |
|
|
696
|
+
| `IPV4` | IPv4 address |
|
|
697
|
+
| `HEX_COLOR` | Hex color code |
|
|
698
|
+
| `DATE_ISO` | ISO date format |
|
|
699
|
+
|
|
700
|
+
#### Environment Config (`@exyconn/common/shared/config`)
|
|
110
701
|
|
|
111
702
|
```typescript
|
|
112
|
-
import {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
subtractTime,
|
|
123
|
-
getDayBoundaries,
|
|
124
|
-
getAge,
|
|
125
|
-
isWeekend,
|
|
126
|
-
DATE_FORMATS
|
|
127
|
-
} from '@exyconn/common/shared';
|
|
703
|
+
import { getEnv, getEnvOptional, isProd, isDev, validateEnv } from '@exyconn/common/shared/config';
|
|
704
|
+
|
|
705
|
+
const port = getEnv('PORT', 3000); // Returns number
|
|
706
|
+
const secret = getEnv('JWT_SECRET'); // Required, throws if missing
|
|
707
|
+
const optional = getEnvOptional('OPTIONAL'); // Returns undefined if missing
|
|
708
|
+
|
|
709
|
+
if (isProd()) { /* Production only */ }
|
|
710
|
+
if (isDev()) { /* Development only */ }
|
|
711
|
+
|
|
712
|
+
validateEnv(['DATABASE_URL', 'JWT_SECRET']); // Throws with missing vars
|
|
128
713
|
```
|
|
129
714
|
|
|
130
|
-
|
|
715
|
+
---
|
|
716
|
+
|
|
717
|
+
### Data Module
|
|
718
|
+
|
|
719
|
+
**Import:** `@exyconn/common/data` or specific files
|
|
720
|
+
|
|
721
|
+
#### Countries (`@exyconn/common/data/countries`)
|
|
131
722
|
|
|
132
723
|
```typescript
|
|
133
|
-
// Countries with nested states/cities + flags
|
|
134
724
|
import {
|
|
135
|
-
countries,
|
|
136
|
-
getCountryByCode,
|
|
137
|
-
getStatesByCountry,
|
|
138
|
-
getCitiesByState,
|
|
725
|
+
default as countries,
|
|
726
|
+
getCountryByCode,
|
|
727
|
+
getStatesByCountry,
|
|
139
728
|
searchCountries,
|
|
140
729
|
getFlag,
|
|
141
|
-
getAllCountriesWithFlags,
|
|
142
|
-
codeToFlag
|
|
143
730
|
} from '@exyconn/common/data/countries';
|
|
144
731
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
formatCurrencyNative
|
|
151
|
-
} from '@exyconn/common/data/currencies';
|
|
732
|
+
const usa = getCountryByCode('US');
|
|
733
|
+
const states = getStatesByCountry('US');
|
|
734
|
+
const matches = searchCountries('united');
|
|
735
|
+
const flag = getFlag('US'); // '🇺🇸'
|
|
736
|
+
```
|
|
152
737
|
|
|
153
|
-
|
|
154
|
-
import { phoneCodes, getPhoneCodeByCountry } from '@exyconn/common/data/phone-codes';
|
|
738
|
+
#### Currencies (`@exyconn/common/data/currencies`)
|
|
155
739
|
|
|
156
|
-
|
|
157
|
-
import {
|
|
158
|
-
timezones,
|
|
159
|
-
getTimezoneByCode,
|
|
160
|
-
searchTimezones,
|
|
161
|
-
getCommonTimezones
|
|
162
|
-
} from '@exyconn/common/data/timezones';
|
|
740
|
+
```typescript
|
|
741
|
+
import { getCurrencyByCode, formatCurrency } from '@exyconn/common/data/currencies';
|
|
163
742
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
REGEX,
|
|
167
|
-
EMAIL,
|
|
168
|
-
PASSWORD_STRONG,
|
|
169
|
-
PHONE_INTERNATIONAL,
|
|
170
|
-
UUID,
|
|
171
|
-
URL_STRICT
|
|
172
|
-
} from '@exyconn/common/data/regex';
|
|
173
|
-
|
|
174
|
-
// Brand Identity
|
|
175
|
-
import {
|
|
176
|
-
BRANDS,
|
|
177
|
-
APPS,
|
|
178
|
-
getBrandById,
|
|
179
|
-
getBrandByDomain,
|
|
180
|
-
getThemedLogo,
|
|
181
|
-
createAppConfig
|
|
182
|
-
} from '@exyconn/common/data/brand-identity';
|
|
743
|
+
const usd = getCurrencyByCode('USD');
|
|
744
|
+
formatCurrency(1234.56, 'USD'); // '$1,234.56'
|
|
183
745
|
```
|
|
184
746
|
|
|
185
|
-
|
|
747
|
+
#### Phone Codes (`@exyconn/common/data/phone-codes`)
|
|
186
748
|
|
|
187
749
|
```typescript
|
|
188
|
-
import {
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
ORDER_STATUS,
|
|
193
|
-
PAYMENT_STATUS,
|
|
194
|
-
TASK_STATUS,
|
|
195
|
-
|
|
196
|
-
// Sort options
|
|
197
|
-
SORT,
|
|
198
|
-
SORT_DIRECTION,
|
|
199
|
-
|
|
200
|
-
// Roles & permissions
|
|
201
|
-
ROLE,
|
|
202
|
-
PERMISSION_LEVEL,
|
|
203
|
-
|
|
204
|
-
// UI constants
|
|
205
|
-
BREAKPOINTS,
|
|
206
|
-
MEDIA_QUERIES,
|
|
207
|
-
THEME,
|
|
208
|
-
|
|
209
|
-
// Other
|
|
210
|
-
PRIORITY,
|
|
211
|
-
VISIBILITY,
|
|
212
|
-
NOTIFICATION_TYPE
|
|
213
|
-
} from '@exyconn/common/shared/constants/enums';
|
|
750
|
+
import { getPhoneCodeByCountry } from '@exyconn/common/data/phone-codes';
|
|
751
|
+
|
|
752
|
+
const usCode = getPhoneCodeByCountry('US');
|
|
753
|
+
// { country: 'United States', dial_code: '+1', flag: '🇺🇸' }
|
|
214
754
|
```
|
|
215
755
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
### Countries
|
|
219
|
-
- 50+ countries with full details
|
|
220
|
-
- Nested states/cities for major countries
|
|
221
|
-
- Includes phone codes, currencies, timezones
|
|
222
|
-
- **NEW:** Emoji flags with `getFlag()` and `codeToFlag()` helpers
|
|
223
|
-
|
|
224
|
-
### Currencies
|
|
225
|
-
- 100+ world currencies
|
|
226
|
-
- Symbol, name, and native formatting
|
|
227
|
-
- formatCurrency(amount, code) helper
|
|
228
|
-
|
|
229
|
-
### Phone Codes
|
|
230
|
-
- 190+ international phone codes
|
|
231
|
-
- Country flags (emoji)
|
|
232
|
-
|
|
233
|
-
### Timezones
|
|
234
|
-
- 65+ timezones with UTC offsets
|
|
235
|
-
- Common timezone presets
|
|
236
|
-
- Search and filter functions
|
|
237
|
-
|
|
238
|
-
### Regex Patterns
|
|
239
|
-
100+ common regex patterns organized by category:
|
|
240
|
-
- **Email** - Basic, RFC5322, common domains
|
|
241
|
-
- **Password** - Weak to very strong
|
|
242
|
-
- **Phone** - International, US, India, UK
|
|
243
|
-
- **Postal** - ZIP, PIN, UK, Canada, Germany
|
|
244
|
-
- **IDs** - UUID, MongoDB ObjectId, Aadhaar, PAN, SSN
|
|
245
|
-
- **URL** - Basic, strict, domain, localhost
|
|
246
|
-
- **Credit Card** - Visa, Mastercard, Amex, etc.
|
|
247
|
-
- **Date/Time** - ISO, various formats
|
|
248
|
-
- **Colors** - Hex, RGB, RGBA, HSL
|
|
249
|
-
- **And more!**
|
|
250
|
-
|
|
251
|
-
### Brand Identity
|
|
252
|
-
Complete brand configuration for all 5 Exyconn projects:
|
|
253
|
-
- **Logo variants:** light, dark, logoOnly, darkLogoOnly
|
|
254
|
-
- **Colors:** primary, secondary, accent
|
|
255
|
-
- **Contact:** support email, sales email
|
|
256
|
-
- **Social links:** Twitter, LinkedIn, GitHub, etc.
|
|
257
|
-
- **SEO config:** title, description, keywords
|
|
258
|
-
|
|
259
|
-
Projects:
|
|
260
|
-
- **botify.life** - AI/Bot platform
|
|
261
|
-
- **exyconn.com** - Main Exyconn brand
|
|
262
|
-
- **partywings.fun** - Event platform
|
|
263
|
-
- **sibera.work** - Work management
|
|
264
|
-
- **spentiva.com** - Finance platform
|
|
265
|
-
|
|
266
|
-
### Enums & Breakpoints
|
|
267
|
-
Comprehensive enums for consistent data handling:
|
|
268
|
-
- **STATUS** - active, pending, approved, archived...
|
|
269
|
-
- **USER_STATUS** - active, suspended, banned...
|
|
270
|
-
- **ORDER_STATUS** - pending, confirmed, shipped...
|
|
271
|
-
- **PAYMENT_STATUS** - pending, completed, refunded...
|
|
272
|
-
- **TASK_STATUS** - todo, in_progress, done...
|
|
273
|
-
- **SORT** - newest, oldest, alphabetical, popular...
|
|
274
|
-
- **ROLE** - super_admin, admin, user, viewer...
|
|
275
|
-
- **PRIORITY** - low, medium, high, critical
|
|
276
|
-
- **BREAKPOINTS** - xs, sm, md, lg, xl, 2xl (Tailwind-compatible)
|
|
277
|
-
|
|
278
|
-
## React Hooks
|
|
279
|
-
|
|
280
|
-
| Hook | Description |
|
|
281
|
-
|------|-------------|
|
|
282
|
-
| useLocalStorage | Persist state in localStorage with cross-tab sync |
|
|
283
|
-
| useDebounce | Debounce a value with configurable delay |
|
|
284
|
-
| useCopyToClipboard | Copy text to clipboard with status |
|
|
285
|
-
| usePageTitle | Set document title with suffix |
|
|
286
|
-
| useSnackbar | Toast/snackbar notification state |
|
|
287
|
-
| useMediaQuery | Track media query matches |
|
|
288
|
-
| useIsMobile / useIsDesktop | Responsive breakpoint hooks |
|
|
289
|
-
| useThemeDetector | Detect system light/dark mode |
|
|
290
|
-
| useInterval | Safely run intervals with cleanup |
|
|
291
|
-
| useOnClickOutside | Detect clicks outside element |
|
|
292
|
-
| useWindowSize | Track window dimensions |
|
|
756
|
+
#### Timezones (`@exyconn/common/data/timezones`)
|
|
293
757
|
|
|
294
|
-
|
|
758
|
+
```typescript
|
|
759
|
+
import { getTimezoneByCode, searchTimezones } from '@exyconn/common/data/timezones';
|
|
295
760
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
```
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
761
|
+
const pst = getTimezoneByCode('America/Los_Angeles');
|
|
762
|
+
const matches = searchTimezones('india');
|
|
763
|
+
```
|
|
764
|
+
|
|
765
|
+
#### Regex Patterns (`@exyconn/common/data/regex`)
|
|
766
|
+
|
|
767
|
+
```typescript
|
|
768
|
+
import REGEX from '@exyconn/common/data/regex';
|
|
769
|
+
|
|
770
|
+
REGEX.EMAIL.BASIC.test('test@example.com');
|
|
771
|
+
REGEX.PASSWORD.STRONG.test('Test@123');
|
|
772
|
+
REGEX.CREDIT_CARD.VISA.test('4111111111111111');
|
|
773
|
+
```
|
|
774
|
+
|
|
775
|
+
#### Brand Identity (`@exyconn/common/data/brand-identity`)
|
|
776
|
+
|
|
777
|
+
```typescript
|
|
778
|
+
import { BRANDS, getBrandById, getBrandByDomain, getThemedLogo } from '@exyconn/common/data/brand-identity';
|
|
779
|
+
|
|
780
|
+
const exyconn = getBrandById('exyconn');
|
|
781
|
+
const brand = getBrandByDomain('botify.life');
|
|
782
|
+
const logo = getThemedLogo('exyconn', 'dark');
|
|
309
783
|
```
|
|
310
784
|
|
|
311
|
-
|
|
785
|
+
---
|
|
786
|
+
|
|
787
|
+
## Package Exports Map
|
|
312
788
|
|
|
313
789
|
```
|
|
314
790
|
@exyconn/common
|
|
315
|
-
├── /server
|
|
316
|
-
│ ├── /response
|
|
317
|
-
│ ├── /enums
|
|
318
|
-
│ ├── /logger
|
|
319
|
-
│ ├── /db
|
|
320
|
-
│ ├── /middleware
|
|
321
|
-
│
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
│ ├── /
|
|
325
|
-
│ ├── /
|
|
326
|
-
│
|
|
327
|
-
├── /
|
|
328
|
-
│
|
|
329
|
-
|
|
330
|
-
│ ├── /
|
|
331
|
-
│
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
├── /
|
|
337
|
-
├── /
|
|
338
|
-
├── /
|
|
339
|
-
|
|
791
|
+
├── /server # Server utilities
|
|
792
|
+
│ ├── /response # Response helpers
|
|
793
|
+
│ ├── /enums # Status codes/messages
|
|
794
|
+
│ ├── /logger # Winston logger
|
|
795
|
+
│ ├── /db # Database connections
|
|
796
|
+
│ ├── /middleware # Auth middleware
|
|
797
|
+
│ ├── /utils # Server utilities
|
|
798
|
+
│ └── /configs # Dynamic configurations
|
|
799
|
+
├── /client # Client utilities
|
|
800
|
+
│ ├── /http # Axios HTTP client
|
|
801
|
+
│ ├── /hooks # React hooks (50+)
|
|
802
|
+
│ ├── /logger # Browser logger
|
|
803
|
+
│ ├── /utils # Client utilities
|
|
804
|
+
│ └── /web # Web utilities
|
|
805
|
+
├── /shared # Shared between server/client
|
|
806
|
+
│ ├── /types # TypeScript types
|
|
807
|
+
│ ├── /validation # Validation patterns
|
|
808
|
+
│ ├── /constants # Enums, breakpoints
|
|
809
|
+
│ ├── /config # Environment config
|
|
810
|
+
│ └── /utils # Date-time utilities
|
|
811
|
+
└── /data # Static data modules
|
|
812
|
+
├── /countries # Countries with states/cities
|
|
813
|
+
├── /currencies # World currencies
|
|
814
|
+
├── /phone-codes # Phone codes
|
|
815
|
+
├── /timezones # Timezones
|
|
816
|
+
├── /regex # Regex patterns
|
|
817
|
+
└── /brand-identity # Brand configs
|
|
818
|
+
```
|
|
819
|
+
|
|
820
|
+
---
|
|
821
|
+
|
|
822
|
+
## Peer Dependencies
|
|
823
|
+
|
|
824
|
+
All peer dependencies are **optional**. Install only what you need:
|
|
825
|
+
|
|
826
|
+
| Package | Version | Required For |
|
|
827
|
+
|---------|---------|--------------|
|
|
828
|
+
| `react` | ^17.0.0 \|\| ^18.0.0 \|\| ^19.0.0 | Client hooks |
|
|
829
|
+
| `express` | ^4.18.0 \|\| ^5.0.0 | Server utilities |
|
|
830
|
+
| `express-rate-limit` | ^7.0.0 | Rate limiter configs |
|
|
831
|
+
| `cors` | ^2.8.5 | CORS configs |
|
|
832
|
+
| `mongoose` | ^8.0.0 | Database utilities |
|
|
833
|
+
| `winston` | ^3.11.0 | Server logger |
|
|
834
|
+
| `jsonwebtoken` | ^9.0.0 | Auth middleware |
|
|
835
|
+
| `axios` | ^1.6.0 | HTTP client |
|
|
836
|
+
| `date-fns` | ^3.0.0 | Date utilities |
|
|
837
|
+
|
|
838
|
+
---
|
|
839
|
+
|
|
840
|
+
## TypeScript Support
|
|
841
|
+
|
|
842
|
+
Full TypeScript support with exported types:
|
|
843
|
+
|
|
844
|
+
```typescript
|
|
845
|
+
import type {
|
|
846
|
+
ApiResponse,
|
|
847
|
+
CorsConfig,
|
|
848
|
+
RateLimitTierConfig,
|
|
849
|
+
AuthRequest,
|
|
850
|
+
} from '@exyconn/common/server';
|
|
340
851
|
```
|
|
341
852
|
|
|
853
|
+
---
|
|
854
|
+
|
|
855
|
+
## Node.js Version
|
|
856
|
+
|
|
857
|
+
Requires Node.js >= 18.0.0
|
|
858
|
+
|
|
859
|
+
---
|
|
860
|
+
|
|
861
|
+
## Development
|
|
862
|
+
|
|
863
|
+
### Setup
|
|
864
|
+
|
|
865
|
+
```bash
|
|
866
|
+
# Clone the repository
|
|
867
|
+
git clone https://github.com/exyconn/common.git
|
|
868
|
+
cd common
|
|
869
|
+
|
|
870
|
+
# Install dependencies
|
|
871
|
+
npm install
|
|
872
|
+
|
|
873
|
+
# Build the package
|
|
874
|
+
npm run build
|
|
875
|
+
|
|
876
|
+
# Run tests
|
|
877
|
+
npm run test
|
|
878
|
+
|
|
879
|
+
# Run tests with coverage
|
|
880
|
+
npm run test:coverage
|
|
881
|
+
```
|
|
882
|
+
|
|
883
|
+
### Available Scripts
|
|
884
|
+
|
|
885
|
+
| Command | Description |
|
|
886
|
+
|---------|-------------|
|
|
887
|
+
| `npm run build` | Build the package with tsup |
|
|
888
|
+
| `npm run dev` | Watch mode for development |
|
|
889
|
+
| `npm run test` | Run tests with Vitest |
|
|
890
|
+
| `npm run test:coverage` | Run tests with coverage report |
|
|
891
|
+
| `npm run lint` | Lint the codebase |
|
|
892
|
+
| `npm run type-check` | TypeScript type checking |
|
|
893
|
+
| `npm run docs:dev` | Start docs dev server (port 4006) |
|
|
894
|
+
| `npm run docs:build` | Build documentation |
|
|
895
|
+
| `npm run docs:start` | Start production docs server |
|
|
896
|
+
|
|
897
|
+
### Documentation Development
|
|
898
|
+
|
|
899
|
+
```bash
|
|
900
|
+
# Install docs dependencies
|
|
901
|
+
npm run docs:install
|
|
902
|
+
|
|
903
|
+
# Start documentation site locally
|
|
904
|
+
npm run docs:dev
|
|
905
|
+
|
|
906
|
+
# Open http://localhost:4006
|
|
907
|
+
```
|
|
908
|
+
|
|
909
|
+
### Running with Docker
|
|
910
|
+
|
|
911
|
+
```bash
|
|
912
|
+
# Build the docs Docker image
|
|
913
|
+
cd docs
|
|
914
|
+
docker build -t common-docs .
|
|
915
|
+
|
|
916
|
+
# Run the container
|
|
917
|
+
docker run -p 4006:4006 common-docs
|
|
918
|
+
```
|
|
919
|
+
|
|
920
|
+
---
|
|
921
|
+
|
|
922
|
+
## Contributing
|
|
923
|
+
|
|
924
|
+
Contributions are welcome! Please read our [Contributing Guide](CONTRIBUTING.md) for details.
|
|
925
|
+
|
|
926
|
+
1. Fork the repository
|
|
927
|
+
2. Create your feature branch (`git checkout -b feature/amazing-feature`)
|
|
928
|
+
3. Commit your changes (`git commit -m 'feat: add amazing feature'`)
|
|
929
|
+
4. Push to the branch (`git push origin feature/amazing-feature`)
|
|
930
|
+
5. Open a Pull Request
|
|
931
|
+
|
|
932
|
+
### Commit Convention
|
|
933
|
+
|
|
934
|
+
We use [Conventional Commits](https://www.conventionalcommits.org/):
|
|
935
|
+
|
|
936
|
+
- `feat:` - New features
|
|
937
|
+
- `fix:` - Bug fixes
|
|
938
|
+
- `docs:` - Documentation changes
|
|
939
|
+
- `test:` - Adding or updating tests
|
|
940
|
+
- `refactor:` - Code refactoring
|
|
941
|
+
- `chore:` - Maintenance tasks
|
|
942
|
+
|
|
943
|
+
---
|
|
944
|
+
|
|
342
945
|
## License
|
|
343
946
|
|
|
344
947
|
MIT © Exyconn
|