node-server-engine 2.0.1 → 2.1.0
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/.commitlintrc.js +10 -10
- package/.nycrc +1 -3
- package/.releaserc.js +2 -1
- package/README.md +206 -146
- package/dist/const/metrics.js.map +1 -1
- package/dist/db/models/index.js.map +1 -1
- package/dist/entities/AWSS3/AWSS3.js +12 -12
- package/dist/entities/AWSS3/AWSS3.js.map +1 -1
- package/dist/entities/ElasticSearch/ElasticSearch.js.map +1 -1
- package/dist/entities/Endpoint/Endpoint.types.js.map +1 -1
- package/dist/entities/GoogleCloudStorage/GoogleCloudStorage.js +11 -11
- package/dist/entities/GoogleCloudStorage/GoogleCloudStorage.js.map +1 -1
- package/dist/entities/KeySet/KeySet.js.map +1 -1
- package/dist/entities/PubSub/PubSub.js.map +1 -1
- package/dist/entities/PushNotification/PushNotification.js.map +1 -1
- package/dist/entities/Redis/Redis.js +3 -3
- package/dist/entities/Redis/Redis.js.map +1 -1
- package/dist/entities/SecretManager/SecretManager.js +3 -3
- package/dist/entities/SecretManager/SecretManager.js.map +1 -1
- package/dist/entities/Sequelize/Sequelize.config.js +11 -7
- package/dist/entities/Sequelize/Sequelize.config.js.map +1 -1
- package/dist/entities/Sequelize/Sequelize.js.map +1 -1
- package/dist/entities/Server/Server.js.map +1 -1
- package/dist/entities/Server/Server.types.js.map +1 -1
- package/dist/entities/SocketClient/SocketClient.d.ts +12 -0
- package/dist/entities/SocketClient/SocketClient.js +34 -3
- package/dist/entities/SocketClient/SocketClient.js.map +1 -1
- package/dist/entities/SocketClient/SocketClient.types.d.ts +6 -0
- package/dist/entities/SocketClient/SocketClient.types.js.map +1 -1
- package/dist/entities/index.js.map +1 -1
- package/dist/middleware/checkPermission/checkPermission.js.map +1 -1
- package/dist/middleware/checkPermission/index.js.map +1 -1
- package/dist/middleware/index.js.map +1 -1
- package/dist/middleware/multiPartFileUploader/multiPartFileUploader.js.map +1 -1
- package/dist/middleware/requestMetrics/requestMetrics.js.map +1 -1
- package/dist/middleware/swaggerDocs/swaggerDocs.js.map +1 -1
- package/dist/middleware/verificationToken/verificationToken.js.map +1 -1
- package/dist/utils/createSortedObject/createSortedObject.js.map +1 -1
- package/dist/utils/dbMigration/dbMigration.js.map +1 -1
- package/dist/utils/dbMigration/migrateMetadataTable/migrateMetadataTable.js.map +1 -1
- package/dist/utils/jwt/jwt.js.map +1 -1
- package/dist/utils/jwt/jwt.types.js.map +1 -1
- package/dist/utils/report/output.js.map +1 -1
- package/dist/utils/report/report.js.map +1 -1
- package/dist/utils/semverCompare/semverCompare.js.map +1 -1
- package/dist/utils/verificationToken/verificationToken.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -126,20 +126,22 @@ The server provides structured logging with automatic format detection. In local
|
|
|
126
126
|
### Log Format
|
|
127
127
|
|
|
128
128
|
Logs automatically detect the environment:
|
|
129
|
+
|
|
129
130
|
- **Local Development**: Colorized, concise format with time (HH:MM:SS)
|
|
130
131
|
- **Production**: JSON structured logs for cloud log aggregation
|
|
131
132
|
|
|
132
133
|
### Environment Variables
|
|
133
134
|
|
|
134
|
-
| Variable
|
|
135
|
-
|
|
136
|
-
| `LOG_FORMAT`
|
|
137
|
-
| `DETAILED_LOGS` | `true`, `false` | Show stack traces and verbose details
|
|
138
|
-
| `DEBUG`
|
|
135
|
+
| Variable | Values | Description | Default |
|
|
136
|
+
| --------------- | --------------- | ----------------------------------------- | ----------- |
|
|
137
|
+
| `LOG_FORMAT` | `local`, `json` | Force specific log format | Auto-detect |
|
|
138
|
+
| `DETAILED_LOGS` | `true`, `false` | Show stack traces and verbose details | `false` |
|
|
139
|
+
| `DEBUG` | `namespace:*` | Enable debug logs for specific namespaces | Off |
|
|
139
140
|
|
|
140
141
|
### Examples
|
|
141
142
|
|
|
142
143
|
**Default Local Format** (clean, concise):
|
|
144
|
+
|
|
143
145
|
```
|
|
144
146
|
[21:16:15] INFO SERVER_RUNNING
|
|
145
147
|
[21:16:15] INFO Connected to database successfully
|
|
@@ -149,6 +151,7 @@ Logs automatically detect the environment:
|
|
|
149
151
|
```
|
|
150
152
|
|
|
151
153
|
**Detailed Logs** (`DETAILED_LOGS=true`):
|
|
154
|
+
|
|
152
155
|
```
|
|
153
156
|
[21:16:15] DEBUG POST /auth/login [200] 154ms
|
|
154
157
|
Data:
|
|
@@ -167,6 +170,7 @@ Error: No bearer token found
|
|
|
167
170
|
```
|
|
168
171
|
|
|
169
172
|
**Production Format** (`LOG_FORMAT=json`):
|
|
173
|
+
|
|
170
174
|
```json
|
|
171
175
|
{"severity":"INFO","message":"SERVER_RUNNING","timestamp":"2025-12-09T21:16:15.028Z","serviceContext":{"service":"my-service","version":"1.0.0"}}
|
|
172
176
|
{"severity":"DEBUG","message":"POST /auth/login [200] 154ms","timestamp":"2025-12-09T21:16:15.182Z"}
|
|
@@ -378,22 +382,22 @@ new Endpoint({
|
|
|
378
382
|
The middleware adds a `files` object to the `req` object, which contains information about the uploaded file.
|
|
379
383
|
|
|
380
384
|
```json
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
385
|
+
[
|
|
386
|
+
{
|
|
387
|
+
"fieldname": "avatar",
|
|
388
|
+
"originalname": "profile.png",
|
|
389
|
+
"mimetype": "image/png",
|
|
390
|
+
"size": 204800,
|
|
391
|
+
"buffer": []
|
|
392
|
+
},
|
|
393
|
+
{
|
|
394
|
+
"fieldname": "document",
|
|
395
|
+
"originalname": "resume.pdf",
|
|
396
|
+
"mimetype": "application/pdf",
|
|
397
|
+
"size": 512000,
|
|
398
|
+
"buffer": []
|
|
399
|
+
}
|
|
400
|
+
]
|
|
397
401
|
```
|
|
398
402
|
|
|
399
403
|
##### Features
|
|
@@ -481,6 +485,7 @@ new Endpoint({
|
|
|
481
485
|
}
|
|
482
486
|
});
|
|
483
487
|
```
|
|
488
|
+
|
|
484
489
|
---
|
|
485
490
|
|
|
486
491
|
### Socket Client
|
|
@@ -523,22 +528,24 @@ const server = new Server({
|
|
|
523
528
|
});
|
|
524
529
|
```
|
|
525
530
|
|
|
526
|
-
| Parameter | Type | Description
|
|
527
|
-
| ----------------- | ------------------------------------------ |
|
|
528
|
-
| handlers | Array\<[MessageHandler](#message-handler)> | A list of message handlers to use
|
|
529
|
-
| authCallbacks | Function \| Array\<Function> | Callbacks called when a client successfully authenticates
|
|
530
|
-
| initCallbacks | Function \| Array\<Function> | Callbacks called when the socket client is created
|
|
531
|
-
| shutdownCallbacks | Function \| Array\<Function> | Callbacks called when the socket client is destroyed
|
|
531
|
+
| Parameter | Type | Description | Default |
|
|
532
|
+
| ----------------- | ------------------------------------------ | --------------------------------------------------------------------------------- | ------------ |
|
|
533
|
+
| handlers | Array\<[MessageHandler](#message-handler)> | A list of message handlers to use | **required** |
|
|
534
|
+
| authCallbacks | Function \| Array\<Function> | Callbacks called when a client successfully authenticates | [] |
|
|
535
|
+
| initCallbacks | Function \| Array\<Function> | Callbacks called when the socket client is created | [] |
|
|
536
|
+
| shutdownCallbacks | Function \| Array\<Function> | Callbacks called when the socket client is destroyed | [] |
|
|
537
|
+
| authTimeoutMs | Number | Close the socket with code 4401 if it has not authenticated within this time (ms) | never |
|
|
532
538
|
|
|
533
539
|
#### Client Properties & Methods
|
|
534
540
|
|
|
535
|
-
| Property/Method | Type
|
|
536
|
-
| ----------------- |
|
|
537
|
-
| id | String
|
|
538
|
-
| establishedAt | Date
|
|
539
|
-
| isAuthenticated() | () => Boolean
|
|
540
|
-
| getUser() | () => SocketUser \| undefined | Get authenticated user data (userId, deviceId, tokenId, audience)
|
|
541
|
-
| sendMessage() | (type, payload, options)
|
|
541
|
+
| Property/Method | Type | Description |
|
|
542
|
+
| ----------------- | ----------------------------- | -------------------------------------------------------------------------------------------------------- |
|
|
543
|
+
| id | String | Unique identifier for the connection |
|
|
544
|
+
| establishedAt | Date | Timestamp when connection was established |
|
|
545
|
+
| isAuthenticated() | () => Boolean | Check if client is authenticated |
|
|
546
|
+
| getUser() | () => SocketUser \| undefined | Get authenticated user data (userId, deviceId, tokenId, audience, the token's `user` claim, `expiresAt`) |
|
|
547
|
+
| sendMessage() | (type, payload, options) | Send a message to the client |
|
|
548
|
+
| close() | (code?, reason?) | Close the connection, e.g. `close(4403, 'Not allowed')` from an `authCallbacks` check |
|
|
542
549
|
|
|
543
550
|
#### Client Authentication
|
|
544
551
|
|
|
@@ -546,10 +553,12 @@ Clients authenticate by sending a message:
|
|
|
546
553
|
|
|
547
554
|
```javascript
|
|
548
555
|
// Client-side
|
|
549
|
-
websocket.send(
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
}
|
|
556
|
+
websocket.send(
|
|
557
|
+
JSON.stringify({
|
|
558
|
+
type: 'authenticate',
|
|
559
|
+
payload: { token: 'your-jwt-token' }
|
|
560
|
+
})
|
|
561
|
+
);
|
|
553
562
|
|
|
554
563
|
// Server will:
|
|
555
564
|
// 1. Verify the JWT token
|
|
@@ -561,10 +570,10 @@ websocket.send(JSON.stringify({
|
|
|
561
570
|
|
|
562
571
|
#### Environment Variables
|
|
563
572
|
|
|
564
|
-
| Variable
|
|
565
|
-
|
|
|
566
|
-
| WS_PING_INTERVAL
|
|
567
|
-
| WEBSOCKET_CLOSE_TIMEOUT | Timeout for graceful close (milliseconds)
|
|
573
|
+
| Variable | Description | Default |
|
|
574
|
+
| ----------------------- | ----------------------------------------- | ------- |
|
|
575
|
+
| WS_PING_INTERVAL | Interval between ping checks (seconds) | 30 |
|
|
576
|
+
| WEBSOCKET_CLOSE_TIMEOUT | Timeout for graceful close (milliseconds) | 3000 |
|
|
568
577
|
|
|
569
578
|
---
|
|
570
579
|
|
|
@@ -630,13 +639,13 @@ await Redis.shutdown();
|
|
|
630
639
|
|
|
631
640
|
#### Environment Variables
|
|
632
641
|
|
|
633
|
-
| Variable
|
|
634
|
-
|
|
|
635
|
-
| REDIS_HOST
|
|
636
|
-
| REDIS_PORT
|
|
637
|
-
| REDIS_USERNAME
|
|
638
|
-
| REDIS_PASSWORD
|
|
639
|
-
| TLS_CA
|
|
642
|
+
| Variable | Description | Default | Required |
|
|
643
|
+
| -------------- | ------------------------------------- | ------- | -------- |
|
|
644
|
+
| REDIS_HOST | Redis server hostname or IP | - | ✓ |
|
|
645
|
+
| REDIS_PORT | Redis server port | 6379 | ✗ |
|
|
646
|
+
| REDIS_USERNAME | Username for authentication | - | ✗ |
|
|
647
|
+
| REDIS_PASSWORD | Password for authentication | - | ✗ |
|
|
648
|
+
| TLS_CA | TLS certificate authority for SSL/TLS | - | ✗ |
|
|
640
649
|
|
|
641
650
|
#### Configuration Options
|
|
642
651
|
|
|
@@ -646,10 +655,11 @@ You can customize Redis client creation:
|
|
|
646
655
|
import { createRedisClient } from 'node-server-engine';
|
|
647
656
|
|
|
648
657
|
const client = createRedisClient({
|
|
649
|
-
db: 2,
|
|
650
|
-
lazyConnect: true,
|
|
651
|
-
enableReadyCheck: false,
|
|
652
|
-
redis: {
|
|
658
|
+
db: 2, // Database index (default: 0)
|
|
659
|
+
lazyConnect: true, // Don't connect until first command
|
|
660
|
+
enableReadyCheck: false, // Disable ready check
|
|
661
|
+
redis: {
|
|
662
|
+
// Override any ioredis options
|
|
653
663
|
connectTimeout: 10000,
|
|
654
664
|
commandTimeout: 5000
|
|
655
665
|
}
|
|
@@ -672,10 +682,10 @@ await SecretManager.init({
|
|
|
672
682
|
cache: true,
|
|
673
683
|
fallbackToEnv: true,
|
|
674
684
|
secrets: [
|
|
675
|
-
'SQL_PASSWORD',
|
|
685
|
+
'SQL_PASSWORD', // Simple env variable
|
|
676
686
|
'JWT_SECRET',
|
|
677
687
|
{
|
|
678
|
-
name: 'PRIVATE_KEY',
|
|
688
|
+
name: 'PRIVATE_KEY', // File-based secret
|
|
679
689
|
type: 'file',
|
|
680
690
|
targetEnvVar: 'PRIVATE_KEY_PATH',
|
|
681
691
|
filename: 'private-key.pem'
|
|
@@ -739,25 +749,27 @@ await server.init(); // Secrets loaded before app starts
|
|
|
739
749
|
|
|
740
750
|
#### Configuration Options
|
|
741
751
|
|
|
742
|
-
| Property | Type | Description
|
|
743
|
-
|
|
744
|
-
| enabled | boolean | Enable Secret Manager (typically only in production)
|
|
745
|
-
| projectId | string | GCP project ID
|
|
746
|
-
| cache | boolean | Cache secret values in memory
|
|
747
|
-
| fallbackToEnv | boolean | Fall back to `process.env` if secret loading fails
|
|
748
|
-
| tempDir | string | Directory for file-based secrets
|
|
749
|
-
| secrets | Array<string \| SecretConfig> | List of secrets to load
|
|
752
|
+
| Property | Type | Description | Default |
|
|
753
|
+
| ------------- | ----------------------------- | ---------------------------------------------------- | ------------- |
|
|
754
|
+
| enabled | boolean | Enable Secret Manager (typically only in production) | false |
|
|
755
|
+
| projectId | string | GCP project ID | Required |
|
|
756
|
+
| cache | boolean | Cache secret values in memory | true |
|
|
757
|
+
| fallbackToEnv | boolean | Fall back to `process.env` if secret loading fails | true |
|
|
758
|
+
| tempDir | string | Directory for file-based secrets | `os.tmpdir()` |
|
|
759
|
+
| secrets | Array<string \| SecretConfig> | List of secrets to load | [] |
|
|
750
760
|
|
|
751
761
|
#### Secret Configuration
|
|
752
762
|
|
|
753
763
|
Secrets can be specified as strings (simple env variables) or objects for advanced configuration:
|
|
754
764
|
|
|
755
765
|
**String format** (simple env variable):
|
|
766
|
+
|
|
756
767
|
```javascript
|
|
757
|
-
'SQL_PASSWORD'
|
|
768
|
+
'SQL_PASSWORD'; // Loads GCP secret "SQL_PASSWORD" → process.env.SQL_PASSWORD
|
|
758
769
|
```
|
|
759
770
|
|
|
760
771
|
**Object format** (advanced configuration):
|
|
772
|
+
|
|
761
773
|
```javascript
|
|
762
774
|
{
|
|
763
775
|
name: 'PRIVATE_KEY', // Secret name in GCP Secret Manager
|
|
@@ -771,11 +783,13 @@ Secrets can be specified as strings (simple env variables) or objects for advanc
|
|
|
771
783
|
#### Secret Types
|
|
772
784
|
|
|
773
785
|
**Environment Variable Secrets** (`type: 'env'`):
|
|
786
|
+
|
|
774
787
|
- Loaded directly into `process.env`
|
|
775
788
|
- Good for passwords, API keys, tokens
|
|
776
789
|
- Example: `'SQL_PASSWORD'` → `process.env.SQL_PASSWORD`
|
|
777
790
|
|
|
778
791
|
**File-based Secrets** (`type: 'file'`):
|
|
792
|
+
|
|
779
793
|
- Written to temp files with secure permissions (0o600)
|
|
780
794
|
- Good for certificates, private keys, JSON credentials
|
|
781
795
|
- Environment variable points to file path
|
|
@@ -783,10 +797,10 @@ Secrets can be specified as strings (simple env variables) or objects for advanc
|
|
|
783
797
|
|
|
784
798
|
#### Environment Variables
|
|
785
799
|
|
|
786
|
-
| Variable
|
|
787
|
-
|
|
788
|
-
| GCP_PROJECT_ID
|
|
789
|
-
| NODE_ENV
|
|
800
|
+
| Variable | Description | Required |
|
|
801
|
+
| -------------- | ------------------------------------ | -------- |
|
|
802
|
+
| GCP_PROJECT_ID | GCP project containing secrets | ✓ |
|
|
803
|
+
| NODE_ENV | Environment (production/development) | - |
|
|
790
804
|
|
|
791
805
|
#### Load Results
|
|
792
806
|
|
|
@@ -820,11 +834,11 @@ const secretConfig = {
|
|
|
820
834
|
// Database credentials
|
|
821
835
|
'SQL_PASSWORD',
|
|
822
836
|
'SQL_USER',
|
|
823
|
-
|
|
837
|
+
|
|
824
838
|
// API keys
|
|
825
839
|
'STRIPE_API_KEY',
|
|
826
840
|
'SENDGRID_API_KEY',
|
|
827
|
-
|
|
841
|
+
|
|
828
842
|
// Certificate files
|
|
829
843
|
{
|
|
830
844
|
name: 'TLS_CERT',
|
|
@@ -838,7 +852,7 @@ const secretConfig = {
|
|
|
838
852
|
targetEnvVar: 'TLS_KEY_PATH',
|
|
839
853
|
filename: 'tls.key'
|
|
840
854
|
},
|
|
841
|
-
|
|
855
|
+
|
|
842
856
|
// Service account key
|
|
843
857
|
{
|
|
844
858
|
name: 'GCP_SERVICE_ACCOUNT',
|
|
@@ -868,7 +882,7 @@ import fs from 'fs';
|
|
|
868
882
|
AWSS3.init({
|
|
869
883
|
region: 'us-east-1',
|
|
870
884
|
accessKeyId: 'YOUR_ACCESS_KEY',
|
|
871
|
-
secretAccessKey: 'YOUR_SECRET_KEY'
|
|
885
|
+
secretAccessKey: 'YOUR_SECRET_KEY'
|
|
872
886
|
// For S3-compatible services
|
|
873
887
|
// endpoint: 'http://localhost:4566',
|
|
874
888
|
// forcePathStyle: true
|
|
@@ -907,7 +921,10 @@ const head = await AWSS3.getMetadata('my-bucket', uploaded.Key);
|
|
|
907
921
|
await AWSS3.delete('my-bucket', uploaded.Key);
|
|
908
922
|
|
|
909
923
|
// Generate unique destination path
|
|
910
|
-
const key = AWSS3.generateFileDestination({
|
|
924
|
+
const key = AWSS3.generateFileDestination({
|
|
925
|
+
directory: 'uploads/images',
|
|
926
|
+
mime: 'image/png'
|
|
927
|
+
});
|
|
911
928
|
```
|
|
912
929
|
|
|
913
930
|
#### Features
|
|
@@ -971,14 +988,14 @@ Generates a unique key using UUID with optional directory and extension (from `m
|
|
|
971
988
|
|
|
972
989
|
#### Environment Variables
|
|
973
990
|
|
|
974
|
-
| Variable
|
|
975
|
-
|
|
|
976
|
-
| `AWS_REGION`
|
|
977
|
-
| `AWS_ACCESS_KEY_ID`
|
|
978
|
-
| `AWS_SECRET_ACCESS_KEY`
|
|
979
|
-
| `AWS_SESSION_TOKEN`
|
|
980
|
-
| `AWS_S3_ENDPOINT`
|
|
981
|
-
| `AWS_S3_FORCE_PATH_STYLE` | Use path-style addressing (`true`/`false`)
|
|
991
|
+
| Variable | Description | Required |
|
|
992
|
+
| ------------------------- | ------------------------------------------------ | -------- |
|
|
993
|
+
| `AWS_REGION` | AWS region (e.g., `us-east-1`) | ✗* |
|
|
994
|
+
| `AWS_ACCESS_KEY_ID` | Access key ID | ✗* |
|
|
995
|
+
| `AWS_SECRET_ACCESS_KEY` | Secret access key | ✗* |
|
|
996
|
+
| `AWS_SESSION_TOKEN` | Session token (temporary creds) | ✗ |
|
|
997
|
+
| `AWS_S3_ENDPOINT` | Custom S3-compatible endpoint (MinIO/LocalStack) | ✗ |
|
|
998
|
+
| `AWS_S3_FORCE_PATH_STYLE` | Use path-style addressing (`true`/`false`) | ✗ |
|
|
982
999
|
|
|
983
1000
|
\* Not required if you call `init()` with a config object.
|
|
984
1001
|
|
|
@@ -988,6 +1005,7 @@ Generates a unique key using UUID with optional directory and extension (from `m
|
|
|
988
1005
|
- Other errors bubble up from AWS SDK v3 commands
|
|
989
1006
|
|
|
990
1007
|
**Example:**
|
|
1008
|
+
|
|
991
1009
|
```javascript
|
|
992
1010
|
try {
|
|
993
1011
|
await AWSS3.upload(stream, 'bucket', {}, { maxSize: '1MB' });
|
|
@@ -1012,11 +1030,10 @@ new Endpoint({
|
|
|
1012
1030
|
const file = req.files[0];
|
|
1013
1031
|
const stream = Readable.from(file.buffer);
|
|
1014
1032
|
|
|
1015
|
-
const result = await AWSS3.upload(
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
);
|
|
1033
|
+
const result = await AWSS3.upload(stream, process.env.S3_UPLOAD_BUCKET, {
|
|
1034
|
+
directory: 'user-uploads',
|
|
1035
|
+
mime: file.mimetype
|
|
1036
|
+
});
|
|
1020
1037
|
|
|
1021
1038
|
res.json({ key: result.Key, url: result.Location });
|
|
1022
1039
|
}
|
|
@@ -1040,14 +1057,16 @@ Sequelize.closeSequelize();
|
|
|
1040
1057
|
|
|
1041
1058
|
It can be configured through environment variables
|
|
1042
1059
|
|
|
1043
|
-
| env
|
|
1044
|
-
|
|
|
1045
|
-
| SQL_HOST
|
|
1046
|
-
| SQL_PORT
|
|
1047
|
-
| SQL_PASSWORD
|
|
1048
|
-
| SQL_USER
|
|
1049
|
-
| SQL_DB
|
|
1050
|
-
| SQL_TYPE
|
|
1060
|
+
| env | description | default |
|
|
1061
|
+
| --------------------- | ------------------------------------------------------------------------------------------ | -------- |
|
|
1062
|
+
| SQL_HOST | Host to which connect to | |
|
|
1063
|
+
| SQL_PORT | Port on which SQL is served | 5432 |
|
|
1064
|
+
| SQL_PASSWORD | Password used to authenticate with the SQL server | |
|
|
1065
|
+
| SQL_USER | User used to authenticate with the SQL server | |
|
|
1066
|
+
| SQL_DB | Database to which connect to | |
|
|
1067
|
+
| SQL_TYPE | SQL type which connect to | postgres |
|
|
1068
|
+
| SQL_ENCRYPT | `mssql` only: set to `false` to disable TLS | true |
|
|
1069
|
+
| SQL_TRUST_SERVER_CERT | `mssql` only: `true` skips certificate verification (self-signed development servers only) | false |
|
|
1051
1070
|
|
|
1052
1071
|
---
|
|
1053
1072
|
|
|
@@ -1070,14 +1089,14 @@ import { PubSub } from 'node-server-engine';
|
|
|
1070
1089
|
PubSub.addPublisher(topic, {
|
|
1071
1090
|
enableMessageOrdering: false, // Enable ordering (requires orderingKey when publishing)
|
|
1072
1091
|
batching: {
|
|
1073
|
-
maxMessages: 100,
|
|
1074
|
-
maxBytes: 1024 * 1024,
|
|
1075
|
-
maxMilliseconds: 100
|
|
1092
|
+
maxMessages: 100, // Max messages per batch
|
|
1093
|
+
maxBytes: 1024 * 1024, // Max bytes per batch (1MB)
|
|
1094
|
+
maxMilliseconds: 100 // Max delay before sending batch
|
|
1076
1095
|
},
|
|
1077
1096
|
retry: {
|
|
1078
|
-
initialDelayMillis: 100,
|
|
1079
|
-
maxDelayMillis: 60000,
|
|
1080
|
-
delayMultiplier: 1.3
|
|
1097
|
+
initialDelayMillis: 100, // Initial retry delay
|
|
1098
|
+
maxDelayMillis: 60000, // Max retry delay (60s)
|
|
1099
|
+
delayMultiplier: 1.3 // Exponential backoff multiplier
|
|
1081
1100
|
}
|
|
1082
1101
|
});
|
|
1083
1102
|
|
|
@@ -1091,13 +1110,13 @@ PubSub.addPublisher(topic, {
|
|
|
1091
1110
|
* @param {Object} [options] - Subscriber configuration options
|
|
1092
1111
|
*/
|
|
1093
1112
|
PubSub.addSubscriber(subscription, handler, {
|
|
1094
|
-
first: false,
|
|
1095
|
-
isDebezium: false,
|
|
1096
|
-
ackDeadline: 60,
|
|
1113
|
+
first: false, // Put handler at beginning of chain
|
|
1114
|
+
isDebezium: false, // Handle Debezium CDC events
|
|
1115
|
+
ackDeadline: 60, // Acknowledgement deadline (10-600 seconds)
|
|
1097
1116
|
flowControl: {
|
|
1098
|
-
maxMessages: 1000,
|
|
1117
|
+
maxMessages: 1000, // Max concurrent messages
|
|
1099
1118
|
maxBytes: 100 * 1024 * 1024, // Max concurrent bytes (100MB)
|
|
1100
|
-
allowExcessMessages: true
|
|
1119
|
+
allowExcessMessages: true // Allow excess messages if under maxBytes
|
|
1101
1120
|
}
|
|
1102
1121
|
});
|
|
1103
1122
|
|
|
@@ -1132,10 +1151,10 @@ async function messageHandler(payload, attributes, publishedAt) {
|
|
|
1132
1151
|
// payload: The JSON message content
|
|
1133
1152
|
// attributes: Message attributes (key-value pairs)
|
|
1134
1153
|
// publishedAt: Date when message was published
|
|
1135
|
-
|
|
1154
|
+
|
|
1136
1155
|
// Process the message
|
|
1137
1156
|
await processData(payload);
|
|
1138
|
-
|
|
1157
|
+
|
|
1139
1158
|
// Message will be ack'd automatically after successful processing
|
|
1140
1159
|
// If an error is thrown, message will be nack'd for redelivery
|
|
1141
1160
|
}
|
|
@@ -1203,13 +1222,14 @@ await PushNotification.sendPush(userId, {
|
|
|
1203
1222
|
|
|
1204
1223
|
#### Environment Variables
|
|
1205
1224
|
|
|
1206
|
-
| Variable
|
|
1207
|
-
|
|
|
1208
|
-
| `PUBSUB_PUSH_NOTIFICATION_QUEUE_TOPIC` | Pub/Sub topic name for push notifications | ✓
|
|
1225
|
+
| Variable | Description | Required |
|
|
1226
|
+
| -------------------------------------- | ----------------------------------------- | -------- |
|
|
1227
|
+
| `PUBSUB_PUSH_NOTIFICATION_QUEUE_TOPIC` | Pub/Sub topic name for push notifications | ✓ |
|
|
1209
1228
|
|
|
1210
1229
|
#### Configuration
|
|
1211
1230
|
|
|
1212
1231
|
The PushNotification entity is pre-configured with optimal settings:
|
|
1232
|
+
|
|
1213
1233
|
- **Batching**: Up to 100 messages, 1MB max, 50ms delay for fast delivery
|
|
1214
1234
|
- **Retry**: Exponential backoff with 100ms initial delay, up to 60s max delay
|
|
1215
1235
|
- **No Ordering**: Push notifications don't require strict ordering for better throughput
|
|
@@ -1315,6 +1335,7 @@ export async function migrate(client: Client): Promise<void> {
|
|
|
1315
1335
|
```
|
|
1316
1336
|
|
|
1317
1337
|
Migrations are:
|
|
1338
|
+
|
|
1318
1339
|
- Run automatically on `init()`
|
|
1319
1340
|
- Tracked in the `migrations` index
|
|
1320
1341
|
- Executed once per file
|
|
@@ -1322,17 +1343,18 @@ Migrations are:
|
|
|
1322
1343
|
|
|
1323
1344
|
#### Environment Variables
|
|
1324
1345
|
|
|
1325
|
-
| Variable
|
|
1326
|
-
|
|
|
1327
|
-
| ELASTIC_SEARCH_HOST
|
|
1328
|
-
| ELASTIC_SEARCH_USERNAME
|
|
1329
|
-
| ELASTIC_SEARCH_PASSWORD
|
|
1330
|
-
| ELASTIC_SEARCH_MIGRATION_PATH
|
|
1331
|
-
| TLS_CA
|
|
1346
|
+
| Variable | Description | Required |
|
|
1347
|
+
| ----------------------------- | ------------------------------------- | -------- |
|
|
1348
|
+
| ELASTIC_SEARCH_HOST | Elasticsearch cluster URL | ✓ |
|
|
1349
|
+
| ELASTIC_SEARCH_USERNAME | Username for authentication | ✓ |
|
|
1350
|
+
| ELASTIC_SEARCH_PASSWORD | Password for authentication | ✓ |
|
|
1351
|
+
| ELASTIC_SEARCH_MIGRATION_PATH | Absolute path to migrations directory | ✓ |
|
|
1352
|
+
| TLS_CA | TLS certificate authority for SSL/TLS | ✗ |
|
|
1332
1353
|
|
|
1333
1354
|
#### Configuration
|
|
1334
1355
|
|
|
1335
1356
|
The client is configured with:
|
|
1357
|
+
|
|
1336
1358
|
- **Max Retries**: 3 attempts
|
|
1337
1359
|
- **Request Timeout**: 30 seconds
|
|
1338
1360
|
- **Sniff on Start**: Disabled (use with clusters)
|
|
@@ -1371,12 +1393,18 @@ const result = await GoogleCloudStorage.upload(
|
|
|
1371
1393
|
console.log(result.name); // photos/uuid.jpeg
|
|
1372
1394
|
|
|
1373
1395
|
// Download file as Buffer
|
|
1374
|
-
const { data, metadata } = await GoogleCloudStorage.get(
|
|
1396
|
+
const { data, metadata } = await GoogleCloudStorage.get(
|
|
1397
|
+
'my-bucket',
|
|
1398
|
+
'photos/image.jpg'
|
|
1399
|
+
);
|
|
1375
1400
|
console.log(metadata.size); // File size in bytes
|
|
1376
1401
|
fs.writeFileSync('downloaded.jpg', data);
|
|
1377
1402
|
|
|
1378
1403
|
// Stream a file (for large files)
|
|
1379
|
-
const { stream, metadata } = await GoogleCloudStorage.download(
|
|
1404
|
+
const { stream, metadata } = await GoogleCloudStorage.download(
|
|
1405
|
+
'my-bucket',
|
|
1406
|
+
'videos/video.mp4'
|
|
1407
|
+
);
|
|
1380
1408
|
stream.pipe(res); // Stream to HTTP response
|
|
1381
1409
|
|
|
1382
1410
|
// Get file stream directly (fastest, no metadata)
|
|
@@ -1430,6 +1458,7 @@ GoogleCloudStorage.init({
|
|
|
1430
1458
|
Upload a file to Google Cloud Storage.
|
|
1431
1459
|
|
|
1432
1460
|
**Parameters:**
|
|
1461
|
+
|
|
1433
1462
|
- `stream` (Readable): Node.js readable stream of file content
|
|
1434
1463
|
- `bucket` (string): Bucket name
|
|
1435
1464
|
- `destinationOptions` (object, optional):
|
|
@@ -1444,6 +1473,7 @@ Upload a file to Google Cloud Storage.
|
|
|
1444
1473
|
**Returns:** Promise\<StorageUploadedFile> - Metadata of uploaded file
|
|
1445
1474
|
|
|
1446
1475
|
**Example:**
|
|
1476
|
+
|
|
1447
1477
|
```javascript
|
|
1448
1478
|
const fileStream = fs.createReadStream('document.pdf');
|
|
1449
1479
|
const result = await GoogleCloudStorage.upload(
|
|
@@ -1460,14 +1490,19 @@ const result = await GoogleCloudStorage.upload(
|
|
|
1460
1490
|
Download a file and return its content as a Buffer along with metadata.
|
|
1461
1491
|
|
|
1462
1492
|
**Parameters:**
|
|
1493
|
+
|
|
1463
1494
|
- `bucket` (string): Bucket name
|
|
1464
1495
|
- `path` (string): File path in the bucket
|
|
1465
1496
|
|
|
1466
1497
|
**Returns:** Promise\<{data: Buffer, metadata: StorageUploadedFile}>
|
|
1467
1498
|
|
|
1468
1499
|
**Example:**
|
|
1500
|
+
|
|
1469
1501
|
```javascript
|
|
1470
|
-
const { data, metadata } = await GoogleCloudStorage.get(
|
|
1502
|
+
const { data, metadata } = await GoogleCloudStorage.get(
|
|
1503
|
+
'my-bucket',
|
|
1504
|
+
'photos/image.jpg'
|
|
1505
|
+
);
|
|
1471
1506
|
console.log(metadata.contentType); // 'image/jpeg'
|
|
1472
1507
|
console.log(data.length); // File size in bytes
|
|
1473
1508
|
```
|
|
@@ -1477,14 +1512,19 @@ console.log(data.length); // File size in bytes
|
|
|
1477
1512
|
Get a readable stream for a file along with its metadata. Use this for large files or when you need to stream content.
|
|
1478
1513
|
|
|
1479
1514
|
**Parameters:**
|
|
1515
|
+
|
|
1480
1516
|
- `bucket` (string): Bucket name
|
|
1481
1517
|
- `path` (string): File path in the bucket
|
|
1482
1518
|
|
|
1483
1519
|
**Returns:** Promise\<{stream: Readable, metadata: StorageUploadedFile}>
|
|
1484
1520
|
|
|
1485
1521
|
**Example:**
|
|
1522
|
+
|
|
1486
1523
|
```javascript
|
|
1487
|
-
const { stream, metadata } = await GoogleCloudStorage.download(
|
|
1524
|
+
const { stream, metadata } = await GoogleCloudStorage.download(
|
|
1525
|
+
'my-bucket',
|
|
1526
|
+
'videos/large-video.mp4'
|
|
1527
|
+
);
|
|
1488
1528
|
console.log(metadata.size); // File size
|
|
1489
1529
|
stream.pipe(response); // Stream to HTTP response
|
|
1490
1530
|
```
|
|
@@ -1494,12 +1534,14 @@ stream.pipe(response); // Stream to HTTP response
|
|
|
1494
1534
|
Get a readable stream for a file without fetching metadata. Fastest option when metadata is not needed.
|
|
1495
1535
|
|
|
1496
1536
|
**Parameters:**
|
|
1537
|
+
|
|
1497
1538
|
- `bucket` (string): Bucket name
|
|
1498
1539
|
- `path` (string): File path in the bucket
|
|
1499
1540
|
|
|
1500
1541
|
**Returns:** Readable - Node.js readable stream
|
|
1501
1542
|
|
|
1502
1543
|
**Example:**
|
|
1544
|
+
|
|
1503
1545
|
```javascript
|
|
1504
1546
|
const stream = GoogleCloudStorage.getFileStream('my-bucket', 'audio/song.mp3');
|
|
1505
1547
|
stream.pipe(response); // Direct streaming
|
|
@@ -1510,12 +1552,14 @@ stream.pipe(response); // Direct streaming
|
|
|
1510
1552
|
Delete a file from Google Cloud Storage.
|
|
1511
1553
|
|
|
1512
1554
|
**Parameters:**
|
|
1555
|
+
|
|
1513
1556
|
- `bucket` (string): Bucket name
|
|
1514
1557
|
- `path` (string): File path in the bucket
|
|
1515
1558
|
|
|
1516
1559
|
**Returns:** Promise\<void>
|
|
1517
1560
|
|
|
1518
1561
|
**Example:**
|
|
1562
|
+
|
|
1519
1563
|
```javascript
|
|
1520
1564
|
await GoogleCloudStorage.delete('my-bucket', 'temp/old-file.txt');
|
|
1521
1565
|
```
|
|
@@ -1525,6 +1569,7 @@ await GoogleCloudStorage.delete('my-bucket', 'temp/old-file.txt');
|
|
|
1525
1569
|
Generate a unique file path with optional directory and extension.
|
|
1526
1570
|
|
|
1527
1571
|
**Parameters:**
|
|
1572
|
+
|
|
1528
1573
|
- `options` (object, optional):
|
|
1529
1574
|
- `directory` (string): Subdirectory path
|
|
1530
1575
|
- `mime` (string): MIME type for extension detection
|
|
@@ -1533,6 +1578,7 @@ Generate a unique file path with optional directory and extension.
|
|
|
1533
1578
|
**Returns:** string - Generated file path
|
|
1534
1579
|
|
|
1535
1580
|
**Examples:**
|
|
1581
|
+
|
|
1536
1582
|
```javascript
|
|
1537
1583
|
// UUID only
|
|
1538
1584
|
GoogleCloudStorage.generateFileDestination();
|
|
@@ -1555,10 +1601,10 @@ GoogleCloudStorage.generateFileDestination({
|
|
|
1555
1601
|
|
|
1556
1602
|
#### Environment Variables
|
|
1557
1603
|
|
|
1558
|
-
| Variable
|
|
1559
|
-
|
|
|
1560
|
-
| GC_PROJECT
|
|
1561
|
-
| GOOGLE_APPLICATION_CREDENTIALS
|
|
1604
|
+
| Variable | Description | Required |
|
|
1605
|
+
| ------------------------------ | -------------------------------- | -------- |
|
|
1606
|
+
| GC_PROJECT | Google Cloud Project ID | ✗* |
|
|
1607
|
+
| GOOGLE_APPLICATION_CREDENTIALS | Path to service account key file | ✗* |
|
|
1562
1608
|
|
|
1563
1609
|
\* Not required if you call `init()` with a config object
|
|
1564
1610
|
|
|
@@ -1570,6 +1616,7 @@ The entity throws `WebError` with appropriate status codes:
|
|
|
1570
1616
|
- Other errors are passed through from Google Cloud Storage SDK
|
|
1571
1617
|
|
|
1572
1618
|
**Example:**
|
|
1619
|
+
|
|
1573
1620
|
```javascript
|
|
1574
1621
|
try {
|
|
1575
1622
|
await GoogleCloudStorage.upload(stream, 'bucket', {}, {}, { maxSize: '1MB' });
|
|
@@ -1595,13 +1642,13 @@ new Endpoint({
|
|
|
1595
1642
|
async handler(req, res) {
|
|
1596
1643
|
const file = req.files[0];
|
|
1597
1644
|
const stream = Readable.from(file.buffer);
|
|
1598
|
-
|
|
1645
|
+
|
|
1599
1646
|
const result = await GoogleCloudStorage.upload(
|
|
1600
1647
|
stream,
|
|
1601
1648
|
process.env.UPLOAD_BUCKET,
|
|
1602
1649
|
{ directory: 'user-uploads', mime: file.mimetype }
|
|
1603
1650
|
);
|
|
1604
|
-
|
|
1651
|
+
|
|
1605
1652
|
res.json({ path: result.name, url: result.mediaLink });
|
|
1606
1653
|
}
|
|
1607
1654
|
});
|
|
@@ -1662,6 +1709,7 @@ Errors are a crucial part of the application, they are what helps us to properly
|
|
|
1662
1709
|
The engine automatically adapts log output based on the environment:
|
|
1663
1710
|
|
|
1664
1711
|
**Local Development (Readable Format)**
|
|
1712
|
+
|
|
1665
1713
|
- Colorized output with severity levels
|
|
1666
1714
|
- Formatted timestamps and file locations
|
|
1667
1715
|
- Pretty-printed data objects
|
|
@@ -1669,6 +1717,7 @@ The engine automatically adapts log output based on the environment:
|
|
|
1669
1717
|
- HTTP request context when available
|
|
1670
1718
|
|
|
1671
1719
|
**Production/GCP (JSON Format)**
|
|
1720
|
+
|
|
1672
1721
|
- Structured JSON for log aggregation
|
|
1673
1722
|
- Google Cloud Error Reporting integration
|
|
1674
1723
|
- Kubernetes pod information
|
|
@@ -1687,6 +1736,7 @@ LOG_FORMAT=json npm start
|
|
|
1687
1736
|
```
|
|
1688
1737
|
|
|
1689
1738
|
**Example Local Output:**
|
|
1739
|
+
|
|
1690
1740
|
```
|
|
1691
1741
|
[2025-12-10T10:30:45.123Z] ERROR src/endpoints/users.ts:42 User not found
|
|
1692
1742
|
Error Code: user-not-found
|
|
@@ -1943,15 +1993,15 @@ interface User {
|
|
|
1943
1993
|
|
|
1944
1994
|
```javascript
|
|
1945
1995
|
// Case-insensitive matching
|
|
1946
|
-
checkPermission('ADMIN')
|
|
1947
|
-
checkPermission(['READ', 'write'])
|
|
1996
|
+
checkPermission('ADMIN'); // Matches: 'admin', 'Admin', 'ADMIN'
|
|
1997
|
+
checkPermission(['READ', 'write']); // Matches any case variation
|
|
1948
1998
|
|
|
1949
1999
|
// Namespace-style permissions
|
|
1950
|
-
checkPermission('users:read')
|
|
1951
|
-
checkPermission(['users:write', 'users:delete'])
|
|
2000
|
+
checkPermission('users:read');
|
|
2001
|
+
checkPermission(['users:write', 'users:delete']);
|
|
1952
2002
|
|
|
1953
2003
|
// Role-based permissions
|
|
1954
|
-
checkPermission(['admin', 'moderator'])
|
|
2004
|
+
checkPermission(['admin', 'moderator']);
|
|
1955
2005
|
```
|
|
1956
2006
|
|
|
1957
2007
|
#### Response Codes
|
|
@@ -1975,8 +2025,10 @@ checkPermission(['admin', 'moderator'])
|
|
|
1975
2025
|
"message": "Permission denied"
|
|
1976
2026
|
}
|
|
1977
2027
|
```
|
|
2028
|
+
|
|
1978
2029
|
};
|
|
1979
|
-
|
|
2030
|
+
|
|
2031
|
+
````
|
|
1980
2032
|
|
|
1981
2033
|
---
|
|
1982
2034
|
|
|
@@ -2006,7 +2058,7 @@ new Endpoint({
|
|
|
2006
2058
|
res.json({ ok: true });
|
|
2007
2059
|
}
|
|
2008
2060
|
});
|
|
2009
|
-
|
|
2061
|
+
````
|
|
2010
2062
|
|
|
2011
2063
|
#### Defaults
|
|
2012
2064
|
|
|
@@ -2147,7 +2199,10 @@ The function returns an object with the following status options:
|
|
|
2147
2199
|
Generate and verify a short-lived verification token + OTP for sensitive operations.
|
|
2148
2200
|
|
|
2149
2201
|
```javascript
|
|
2150
|
-
import {
|
|
2202
|
+
import {
|
|
2203
|
+
createVerificationToken,
|
|
2204
|
+
verifyVerificationToken
|
|
2205
|
+
} from 'node-server-engine';
|
|
2151
2206
|
|
|
2152
2207
|
// Generate token + OTP
|
|
2153
2208
|
const { token, otp, expiresAt } = createVerificationToken({
|
|
@@ -2435,17 +2490,17 @@ npm run coverage
|
|
|
2435
2490
|
|
|
2436
2491
|
### Scripts
|
|
2437
2492
|
|
|
2438
|
-
| Script
|
|
2439
|
-
|
|
2440
|
-
| `npm run build`
|
|
2441
|
-
| `npm run lint`
|
|
2442
|
-
| `npm run lint:fix`
|
|
2443
|
-
| `npm run format`
|
|
2444
|
-
| `npm run format:check` | Check code formatting without modifying files
|
|
2445
|
-
| `npm test`
|
|
2446
|
-
| `npm run test:file`
|
|
2447
|
-
| `npm run coverage`
|
|
2448
|
-
| `npm run coverage:ci`
|
|
2493
|
+
| Script | Description |
|
|
2494
|
+
| ---------------------- | -------------------------------------------------- |
|
|
2495
|
+
| `npm run build` | Compile TypeScript and generate distribution files |
|
|
2496
|
+
| `npm run lint` | Check code for linting errors |
|
|
2497
|
+
| `npm run lint:fix` | Automatically fix linting errors |
|
|
2498
|
+
| `npm run format` | Format code with Prettier |
|
|
2499
|
+
| `npm run format:check` | Check code formatting without modifying files |
|
|
2500
|
+
| `npm test` | Run all tests with coverage |
|
|
2501
|
+
| `npm run test:file` | Run a specific test file |
|
|
2502
|
+
| `npm run coverage` | Generate HTML coverage report |
|
|
2503
|
+
| `npm run coverage:ci` | Generate coverage summary for CI |
|
|
2449
2504
|
|
|
2450
2505
|
### Commit Convention
|
|
2451
2506
|
|
|
@@ -2454,6 +2509,7 @@ This project uses [Conventional Commits](https://www.conventionalcommits.org/) f
|
|
|
2454
2509
|
**Format:** `type(scope): subject`
|
|
2455
2510
|
|
|
2456
2511
|
**Types:**
|
|
2512
|
+
|
|
2457
2513
|
- `feat`: New feature (triggers minor version bump)
|
|
2458
2514
|
- `fix`: Bug fix (triggers patch version bump)
|
|
2459
2515
|
- `docs`: Documentation changes
|
|
@@ -2465,6 +2521,7 @@ This project uses [Conventional Commits](https://www.conventionalcommits.org/) f
|
|
|
2465
2521
|
- `ci`: CI/CD changes
|
|
2466
2522
|
|
|
2467
2523
|
**Examples:**
|
|
2524
|
+
|
|
2468
2525
|
```bash
|
|
2469
2526
|
git commit -m "feat(endpoints): add support for GraphQL endpoints"
|
|
2470
2527
|
git commit -m "fix(auth): resolve JWT token validation issue"
|
|
@@ -2472,6 +2529,7 @@ git commit -m "docs(readme): update installation instructions"
|
|
|
2472
2529
|
```
|
|
2473
2530
|
|
|
2474
2531
|
**Breaking Changes:** Add `BREAKING CHANGE:` in the commit body to trigger a major version bump:
|
|
2532
|
+
|
|
2475
2533
|
```bash
|
|
2476
2534
|
git commit -m "feat(api): redesign authentication flow" -m "BREAKING CHANGE: AuthType enum values changed"
|
|
2477
2535
|
```
|
|
@@ -2481,6 +2539,7 @@ git commit -m "feat(api): redesign authentication flow" -m "BREAKING CHANGE: Aut
|
|
|
2481
2539
|
This project uses [semantic-release](https://github.com/semantic-release/semantic-release) for automated version management and package publishing.
|
|
2482
2540
|
|
|
2483
2541
|
**How it works:**
|
|
2542
|
+
|
|
2484
2543
|
1. Commits are analyzed based on conventional commit format
|
|
2485
2544
|
2. Version number is automatically determined (major.minor.patch)
|
|
2486
2545
|
3. Changelog is generated from commit messages
|
|
@@ -2489,6 +2548,7 @@ This project uses [semantic-release](https://github.com/semantic-release/semanti
|
|
|
2489
2548
|
6. GitHub release is created
|
|
2490
2549
|
|
|
2491
2550
|
**When releases happen:**
|
|
2551
|
+
|
|
2492
2552
|
- Automatically on every push to `main` branch
|
|
2493
2553
|
- Only if there are release-worthy commits (feat, fix, perf, BREAKING CHANGE)
|
|
2494
2554
|
- Releases are skipped for commits with `[skip ci]` in the message
|