node-server-engine 2.0.1 → 2.0.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/.commitlintrc.js +10 -10
- package/.nycrc +1 -3
- package/.releaserc.js +2 -1
- package/README.md +194 -138
- 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.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/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,22 @@ 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 | [] |
|
|
532
537
|
|
|
533
538
|
#### Client Properties & Methods
|
|
534
539
|
|
|
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)
|
|
540
|
+
| Property/Method | Type | Description |
|
|
541
|
+
| ----------------- | ----------------------------- | ----------------------------------------------------------------- |
|
|
542
|
+
| id | String | Unique identifier for the connection |
|
|
543
|
+
| establishedAt | Date | Timestamp when connection was established |
|
|
544
|
+
| isAuthenticated() | () => Boolean | Check if client is authenticated |
|
|
545
|
+
| getUser() | () => SocketUser \| undefined | Get authenticated user data (userId, deviceId, tokenId, audience) |
|
|
546
|
+
| sendMessage() | (type, payload, options) | Send a message to the client |
|
|
542
547
|
|
|
543
548
|
#### Client Authentication
|
|
544
549
|
|
|
@@ -546,10 +551,12 @@ Clients authenticate by sending a message:
|
|
|
546
551
|
|
|
547
552
|
```javascript
|
|
548
553
|
// Client-side
|
|
549
|
-
websocket.send(
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
}
|
|
554
|
+
websocket.send(
|
|
555
|
+
JSON.stringify({
|
|
556
|
+
type: 'authenticate',
|
|
557
|
+
payload: { token: 'your-jwt-token' }
|
|
558
|
+
})
|
|
559
|
+
);
|
|
553
560
|
|
|
554
561
|
// Server will:
|
|
555
562
|
// 1. Verify the JWT token
|
|
@@ -561,10 +568,10 @@ websocket.send(JSON.stringify({
|
|
|
561
568
|
|
|
562
569
|
#### Environment Variables
|
|
563
570
|
|
|
564
|
-
| Variable
|
|
565
|
-
|
|
|
566
|
-
| WS_PING_INTERVAL
|
|
567
|
-
| WEBSOCKET_CLOSE_TIMEOUT | Timeout for graceful close (milliseconds)
|
|
571
|
+
| Variable | Description | Default |
|
|
572
|
+
| ----------------------- | ----------------------------------------- | ------- |
|
|
573
|
+
| WS_PING_INTERVAL | Interval between ping checks (seconds) | 30 |
|
|
574
|
+
| WEBSOCKET_CLOSE_TIMEOUT | Timeout for graceful close (milliseconds) | 3000 |
|
|
568
575
|
|
|
569
576
|
---
|
|
570
577
|
|
|
@@ -630,13 +637,13 @@ await Redis.shutdown();
|
|
|
630
637
|
|
|
631
638
|
#### Environment Variables
|
|
632
639
|
|
|
633
|
-
| Variable
|
|
634
|
-
|
|
|
635
|
-
| REDIS_HOST
|
|
636
|
-
| REDIS_PORT
|
|
637
|
-
| REDIS_USERNAME
|
|
638
|
-
| REDIS_PASSWORD
|
|
639
|
-
| TLS_CA
|
|
640
|
+
| Variable | Description | Default | Required |
|
|
641
|
+
| -------------- | ------------------------------------- | ------- | -------- |
|
|
642
|
+
| REDIS_HOST | Redis server hostname or IP | - | ✓ |
|
|
643
|
+
| REDIS_PORT | Redis server port | 6379 | ✗ |
|
|
644
|
+
| REDIS_USERNAME | Username for authentication | - | ✗ |
|
|
645
|
+
| REDIS_PASSWORD | Password for authentication | - | ✗ |
|
|
646
|
+
| TLS_CA | TLS certificate authority for SSL/TLS | - | ✗ |
|
|
640
647
|
|
|
641
648
|
#### Configuration Options
|
|
642
649
|
|
|
@@ -646,10 +653,11 @@ You can customize Redis client creation:
|
|
|
646
653
|
import { createRedisClient } from 'node-server-engine';
|
|
647
654
|
|
|
648
655
|
const client = createRedisClient({
|
|
649
|
-
db: 2,
|
|
650
|
-
lazyConnect: true,
|
|
651
|
-
enableReadyCheck: false,
|
|
652
|
-
redis: {
|
|
656
|
+
db: 2, // Database index (default: 0)
|
|
657
|
+
lazyConnect: true, // Don't connect until first command
|
|
658
|
+
enableReadyCheck: false, // Disable ready check
|
|
659
|
+
redis: {
|
|
660
|
+
// Override any ioredis options
|
|
653
661
|
connectTimeout: 10000,
|
|
654
662
|
commandTimeout: 5000
|
|
655
663
|
}
|
|
@@ -672,10 +680,10 @@ await SecretManager.init({
|
|
|
672
680
|
cache: true,
|
|
673
681
|
fallbackToEnv: true,
|
|
674
682
|
secrets: [
|
|
675
|
-
'SQL_PASSWORD',
|
|
683
|
+
'SQL_PASSWORD', // Simple env variable
|
|
676
684
|
'JWT_SECRET',
|
|
677
685
|
{
|
|
678
|
-
name: 'PRIVATE_KEY',
|
|
686
|
+
name: 'PRIVATE_KEY', // File-based secret
|
|
679
687
|
type: 'file',
|
|
680
688
|
targetEnvVar: 'PRIVATE_KEY_PATH',
|
|
681
689
|
filename: 'private-key.pem'
|
|
@@ -739,25 +747,27 @@ await server.init(); // Secrets loaded before app starts
|
|
|
739
747
|
|
|
740
748
|
#### Configuration Options
|
|
741
749
|
|
|
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
|
|
750
|
+
| Property | Type | Description | Default |
|
|
751
|
+
| ------------- | ----------------------------- | ---------------------------------------------------- | ------------- |
|
|
752
|
+
| enabled | boolean | Enable Secret Manager (typically only in production) | false |
|
|
753
|
+
| projectId | string | GCP project ID | Required |
|
|
754
|
+
| cache | boolean | Cache secret values in memory | true |
|
|
755
|
+
| fallbackToEnv | boolean | Fall back to `process.env` if secret loading fails | true |
|
|
756
|
+
| tempDir | string | Directory for file-based secrets | `os.tmpdir()` |
|
|
757
|
+
| secrets | Array<string \| SecretConfig> | List of secrets to load | [] |
|
|
750
758
|
|
|
751
759
|
#### Secret Configuration
|
|
752
760
|
|
|
753
761
|
Secrets can be specified as strings (simple env variables) or objects for advanced configuration:
|
|
754
762
|
|
|
755
763
|
**String format** (simple env variable):
|
|
764
|
+
|
|
756
765
|
```javascript
|
|
757
|
-
'SQL_PASSWORD'
|
|
766
|
+
'SQL_PASSWORD'; // Loads GCP secret "SQL_PASSWORD" → process.env.SQL_PASSWORD
|
|
758
767
|
```
|
|
759
768
|
|
|
760
769
|
**Object format** (advanced configuration):
|
|
770
|
+
|
|
761
771
|
```javascript
|
|
762
772
|
{
|
|
763
773
|
name: 'PRIVATE_KEY', // Secret name in GCP Secret Manager
|
|
@@ -771,11 +781,13 @@ Secrets can be specified as strings (simple env variables) or objects for advanc
|
|
|
771
781
|
#### Secret Types
|
|
772
782
|
|
|
773
783
|
**Environment Variable Secrets** (`type: 'env'`):
|
|
784
|
+
|
|
774
785
|
- Loaded directly into `process.env`
|
|
775
786
|
- Good for passwords, API keys, tokens
|
|
776
787
|
- Example: `'SQL_PASSWORD'` → `process.env.SQL_PASSWORD`
|
|
777
788
|
|
|
778
789
|
**File-based Secrets** (`type: 'file'`):
|
|
790
|
+
|
|
779
791
|
- Written to temp files with secure permissions (0o600)
|
|
780
792
|
- Good for certificates, private keys, JSON credentials
|
|
781
793
|
- Environment variable points to file path
|
|
@@ -783,10 +795,10 @@ Secrets can be specified as strings (simple env variables) or objects for advanc
|
|
|
783
795
|
|
|
784
796
|
#### Environment Variables
|
|
785
797
|
|
|
786
|
-
| Variable
|
|
787
|
-
|
|
788
|
-
| GCP_PROJECT_ID
|
|
789
|
-
| NODE_ENV
|
|
798
|
+
| Variable | Description | Required |
|
|
799
|
+
| -------------- | ------------------------------------ | -------- |
|
|
800
|
+
| GCP_PROJECT_ID | GCP project containing secrets | ✓ |
|
|
801
|
+
| NODE_ENV | Environment (production/development) | - |
|
|
790
802
|
|
|
791
803
|
#### Load Results
|
|
792
804
|
|
|
@@ -820,11 +832,11 @@ const secretConfig = {
|
|
|
820
832
|
// Database credentials
|
|
821
833
|
'SQL_PASSWORD',
|
|
822
834
|
'SQL_USER',
|
|
823
|
-
|
|
835
|
+
|
|
824
836
|
// API keys
|
|
825
837
|
'STRIPE_API_KEY',
|
|
826
838
|
'SENDGRID_API_KEY',
|
|
827
|
-
|
|
839
|
+
|
|
828
840
|
// Certificate files
|
|
829
841
|
{
|
|
830
842
|
name: 'TLS_CERT',
|
|
@@ -838,7 +850,7 @@ const secretConfig = {
|
|
|
838
850
|
targetEnvVar: 'TLS_KEY_PATH',
|
|
839
851
|
filename: 'tls.key'
|
|
840
852
|
},
|
|
841
|
-
|
|
853
|
+
|
|
842
854
|
// Service account key
|
|
843
855
|
{
|
|
844
856
|
name: 'GCP_SERVICE_ACCOUNT',
|
|
@@ -868,7 +880,7 @@ import fs from 'fs';
|
|
|
868
880
|
AWSS3.init({
|
|
869
881
|
region: 'us-east-1',
|
|
870
882
|
accessKeyId: 'YOUR_ACCESS_KEY',
|
|
871
|
-
secretAccessKey: 'YOUR_SECRET_KEY'
|
|
883
|
+
secretAccessKey: 'YOUR_SECRET_KEY'
|
|
872
884
|
// For S3-compatible services
|
|
873
885
|
// endpoint: 'http://localhost:4566',
|
|
874
886
|
// forcePathStyle: true
|
|
@@ -907,7 +919,10 @@ const head = await AWSS3.getMetadata('my-bucket', uploaded.Key);
|
|
|
907
919
|
await AWSS3.delete('my-bucket', uploaded.Key);
|
|
908
920
|
|
|
909
921
|
// Generate unique destination path
|
|
910
|
-
const key = AWSS3.generateFileDestination({
|
|
922
|
+
const key = AWSS3.generateFileDestination({
|
|
923
|
+
directory: 'uploads/images',
|
|
924
|
+
mime: 'image/png'
|
|
925
|
+
});
|
|
911
926
|
```
|
|
912
927
|
|
|
913
928
|
#### Features
|
|
@@ -971,14 +986,14 @@ Generates a unique key using UUID with optional directory and extension (from `m
|
|
|
971
986
|
|
|
972
987
|
#### Environment Variables
|
|
973
988
|
|
|
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`)
|
|
989
|
+
| Variable | Description | Required |
|
|
990
|
+
| ------------------------- | ------------------------------------------------ | -------- |
|
|
991
|
+
| `AWS_REGION` | AWS region (e.g., `us-east-1`) | ✗* |
|
|
992
|
+
| `AWS_ACCESS_KEY_ID` | Access key ID | ✗* |
|
|
993
|
+
| `AWS_SECRET_ACCESS_KEY` | Secret access key | ✗* |
|
|
994
|
+
| `AWS_SESSION_TOKEN` | Session token (temporary creds) | ✗ |
|
|
995
|
+
| `AWS_S3_ENDPOINT` | Custom S3-compatible endpoint (MinIO/LocalStack) | ✗ |
|
|
996
|
+
| `AWS_S3_FORCE_PATH_STYLE` | Use path-style addressing (`true`/`false`) | ✗ |
|
|
982
997
|
|
|
983
998
|
\* Not required if you call `init()` with a config object.
|
|
984
999
|
|
|
@@ -988,6 +1003,7 @@ Generates a unique key using UUID with optional directory and extension (from `m
|
|
|
988
1003
|
- Other errors bubble up from AWS SDK v3 commands
|
|
989
1004
|
|
|
990
1005
|
**Example:**
|
|
1006
|
+
|
|
991
1007
|
```javascript
|
|
992
1008
|
try {
|
|
993
1009
|
await AWSS3.upload(stream, 'bucket', {}, { maxSize: '1MB' });
|
|
@@ -1012,11 +1028,10 @@ new Endpoint({
|
|
|
1012
1028
|
const file = req.files[0];
|
|
1013
1029
|
const stream = Readable.from(file.buffer);
|
|
1014
1030
|
|
|
1015
|
-
const result = await AWSS3.upload(
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
);
|
|
1031
|
+
const result = await AWSS3.upload(stream, process.env.S3_UPLOAD_BUCKET, {
|
|
1032
|
+
directory: 'user-uploads',
|
|
1033
|
+
mime: file.mimetype
|
|
1034
|
+
});
|
|
1020
1035
|
|
|
1021
1036
|
res.json({ key: result.Key, url: result.Location });
|
|
1022
1037
|
}
|
|
@@ -1070,14 +1085,14 @@ import { PubSub } from 'node-server-engine';
|
|
|
1070
1085
|
PubSub.addPublisher(topic, {
|
|
1071
1086
|
enableMessageOrdering: false, // Enable ordering (requires orderingKey when publishing)
|
|
1072
1087
|
batching: {
|
|
1073
|
-
maxMessages: 100,
|
|
1074
|
-
maxBytes: 1024 * 1024,
|
|
1075
|
-
maxMilliseconds: 100
|
|
1088
|
+
maxMessages: 100, // Max messages per batch
|
|
1089
|
+
maxBytes: 1024 * 1024, // Max bytes per batch (1MB)
|
|
1090
|
+
maxMilliseconds: 100 // Max delay before sending batch
|
|
1076
1091
|
},
|
|
1077
1092
|
retry: {
|
|
1078
|
-
initialDelayMillis: 100,
|
|
1079
|
-
maxDelayMillis: 60000,
|
|
1080
|
-
delayMultiplier: 1.3
|
|
1093
|
+
initialDelayMillis: 100, // Initial retry delay
|
|
1094
|
+
maxDelayMillis: 60000, // Max retry delay (60s)
|
|
1095
|
+
delayMultiplier: 1.3 // Exponential backoff multiplier
|
|
1081
1096
|
}
|
|
1082
1097
|
});
|
|
1083
1098
|
|
|
@@ -1091,13 +1106,13 @@ PubSub.addPublisher(topic, {
|
|
|
1091
1106
|
* @param {Object} [options] - Subscriber configuration options
|
|
1092
1107
|
*/
|
|
1093
1108
|
PubSub.addSubscriber(subscription, handler, {
|
|
1094
|
-
first: false,
|
|
1095
|
-
isDebezium: false,
|
|
1096
|
-
ackDeadline: 60,
|
|
1109
|
+
first: false, // Put handler at beginning of chain
|
|
1110
|
+
isDebezium: false, // Handle Debezium CDC events
|
|
1111
|
+
ackDeadline: 60, // Acknowledgement deadline (10-600 seconds)
|
|
1097
1112
|
flowControl: {
|
|
1098
|
-
maxMessages: 1000,
|
|
1113
|
+
maxMessages: 1000, // Max concurrent messages
|
|
1099
1114
|
maxBytes: 100 * 1024 * 1024, // Max concurrent bytes (100MB)
|
|
1100
|
-
allowExcessMessages: true
|
|
1115
|
+
allowExcessMessages: true // Allow excess messages if under maxBytes
|
|
1101
1116
|
}
|
|
1102
1117
|
});
|
|
1103
1118
|
|
|
@@ -1132,10 +1147,10 @@ async function messageHandler(payload, attributes, publishedAt) {
|
|
|
1132
1147
|
// payload: The JSON message content
|
|
1133
1148
|
// attributes: Message attributes (key-value pairs)
|
|
1134
1149
|
// publishedAt: Date when message was published
|
|
1135
|
-
|
|
1150
|
+
|
|
1136
1151
|
// Process the message
|
|
1137
1152
|
await processData(payload);
|
|
1138
|
-
|
|
1153
|
+
|
|
1139
1154
|
// Message will be ack'd automatically after successful processing
|
|
1140
1155
|
// If an error is thrown, message will be nack'd for redelivery
|
|
1141
1156
|
}
|
|
@@ -1203,13 +1218,14 @@ await PushNotification.sendPush(userId, {
|
|
|
1203
1218
|
|
|
1204
1219
|
#### Environment Variables
|
|
1205
1220
|
|
|
1206
|
-
| Variable
|
|
1207
|
-
|
|
|
1208
|
-
| `PUBSUB_PUSH_NOTIFICATION_QUEUE_TOPIC` | Pub/Sub topic name for push notifications | ✓
|
|
1221
|
+
| Variable | Description | Required |
|
|
1222
|
+
| -------------------------------------- | ----------------------------------------- | -------- |
|
|
1223
|
+
| `PUBSUB_PUSH_NOTIFICATION_QUEUE_TOPIC` | Pub/Sub topic name for push notifications | ✓ |
|
|
1209
1224
|
|
|
1210
1225
|
#### Configuration
|
|
1211
1226
|
|
|
1212
1227
|
The PushNotification entity is pre-configured with optimal settings:
|
|
1228
|
+
|
|
1213
1229
|
- **Batching**: Up to 100 messages, 1MB max, 50ms delay for fast delivery
|
|
1214
1230
|
- **Retry**: Exponential backoff with 100ms initial delay, up to 60s max delay
|
|
1215
1231
|
- **No Ordering**: Push notifications don't require strict ordering for better throughput
|
|
@@ -1315,6 +1331,7 @@ export async function migrate(client: Client): Promise<void> {
|
|
|
1315
1331
|
```
|
|
1316
1332
|
|
|
1317
1333
|
Migrations are:
|
|
1334
|
+
|
|
1318
1335
|
- Run automatically on `init()`
|
|
1319
1336
|
- Tracked in the `migrations` index
|
|
1320
1337
|
- Executed once per file
|
|
@@ -1322,17 +1339,18 @@ Migrations are:
|
|
|
1322
1339
|
|
|
1323
1340
|
#### Environment Variables
|
|
1324
1341
|
|
|
1325
|
-
| Variable
|
|
1326
|
-
|
|
|
1327
|
-
| ELASTIC_SEARCH_HOST
|
|
1328
|
-
| ELASTIC_SEARCH_USERNAME
|
|
1329
|
-
| ELASTIC_SEARCH_PASSWORD
|
|
1330
|
-
| ELASTIC_SEARCH_MIGRATION_PATH
|
|
1331
|
-
| TLS_CA
|
|
1342
|
+
| Variable | Description | Required |
|
|
1343
|
+
| ----------------------------- | ------------------------------------- | -------- |
|
|
1344
|
+
| ELASTIC_SEARCH_HOST | Elasticsearch cluster URL | ✓ |
|
|
1345
|
+
| ELASTIC_SEARCH_USERNAME | Username for authentication | ✓ |
|
|
1346
|
+
| ELASTIC_SEARCH_PASSWORD | Password for authentication | ✓ |
|
|
1347
|
+
| ELASTIC_SEARCH_MIGRATION_PATH | Absolute path to migrations directory | ✓ |
|
|
1348
|
+
| TLS_CA | TLS certificate authority for SSL/TLS | ✗ |
|
|
1332
1349
|
|
|
1333
1350
|
#### Configuration
|
|
1334
1351
|
|
|
1335
1352
|
The client is configured with:
|
|
1353
|
+
|
|
1336
1354
|
- **Max Retries**: 3 attempts
|
|
1337
1355
|
- **Request Timeout**: 30 seconds
|
|
1338
1356
|
- **Sniff on Start**: Disabled (use with clusters)
|
|
@@ -1371,12 +1389,18 @@ const result = await GoogleCloudStorage.upload(
|
|
|
1371
1389
|
console.log(result.name); // photos/uuid.jpeg
|
|
1372
1390
|
|
|
1373
1391
|
// Download file as Buffer
|
|
1374
|
-
const { data, metadata } = await GoogleCloudStorage.get(
|
|
1392
|
+
const { data, metadata } = await GoogleCloudStorage.get(
|
|
1393
|
+
'my-bucket',
|
|
1394
|
+
'photos/image.jpg'
|
|
1395
|
+
);
|
|
1375
1396
|
console.log(metadata.size); // File size in bytes
|
|
1376
1397
|
fs.writeFileSync('downloaded.jpg', data);
|
|
1377
1398
|
|
|
1378
1399
|
// Stream a file (for large files)
|
|
1379
|
-
const { stream, metadata } = await GoogleCloudStorage.download(
|
|
1400
|
+
const { stream, metadata } = await GoogleCloudStorage.download(
|
|
1401
|
+
'my-bucket',
|
|
1402
|
+
'videos/video.mp4'
|
|
1403
|
+
);
|
|
1380
1404
|
stream.pipe(res); // Stream to HTTP response
|
|
1381
1405
|
|
|
1382
1406
|
// Get file stream directly (fastest, no metadata)
|
|
@@ -1430,6 +1454,7 @@ GoogleCloudStorage.init({
|
|
|
1430
1454
|
Upload a file to Google Cloud Storage.
|
|
1431
1455
|
|
|
1432
1456
|
**Parameters:**
|
|
1457
|
+
|
|
1433
1458
|
- `stream` (Readable): Node.js readable stream of file content
|
|
1434
1459
|
- `bucket` (string): Bucket name
|
|
1435
1460
|
- `destinationOptions` (object, optional):
|
|
@@ -1444,6 +1469,7 @@ Upload a file to Google Cloud Storage.
|
|
|
1444
1469
|
**Returns:** Promise\<StorageUploadedFile> - Metadata of uploaded file
|
|
1445
1470
|
|
|
1446
1471
|
**Example:**
|
|
1472
|
+
|
|
1447
1473
|
```javascript
|
|
1448
1474
|
const fileStream = fs.createReadStream('document.pdf');
|
|
1449
1475
|
const result = await GoogleCloudStorage.upload(
|
|
@@ -1460,14 +1486,19 @@ const result = await GoogleCloudStorage.upload(
|
|
|
1460
1486
|
Download a file and return its content as a Buffer along with metadata.
|
|
1461
1487
|
|
|
1462
1488
|
**Parameters:**
|
|
1489
|
+
|
|
1463
1490
|
- `bucket` (string): Bucket name
|
|
1464
1491
|
- `path` (string): File path in the bucket
|
|
1465
1492
|
|
|
1466
1493
|
**Returns:** Promise\<{data: Buffer, metadata: StorageUploadedFile}>
|
|
1467
1494
|
|
|
1468
1495
|
**Example:**
|
|
1496
|
+
|
|
1469
1497
|
```javascript
|
|
1470
|
-
const { data, metadata } = await GoogleCloudStorage.get(
|
|
1498
|
+
const { data, metadata } = await GoogleCloudStorage.get(
|
|
1499
|
+
'my-bucket',
|
|
1500
|
+
'photos/image.jpg'
|
|
1501
|
+
);
|
|
1471
1502
|
console.log(metadata.contentType); // 'image/jpeg'
|
|
1472
1503
|
console.log(data.length); // File size in bytes
|
|
1473
1504
|
```
|
|
@@ -1477,14 +1508,19 @@ console.log(data.length); // File size in bytes
|
|
|
1477
1508
|
Get a readable stream for a file along with its metadata. Use this for large files or when you need to stream content.
|
|
1478
1509
|
|
|
1479
1510
|
**Parameters:**
|
|
1511
|
+
|
|
1480
1512
|
- `bucket` (string): Bucket name
|
|
1481
1513
|
- `path` (string): File path in the bucket
|
|
1482
1514
|
|
|
1483
1515
|
**Returns:** Promise\<{stream: Readable, metadata: StorageUploadedFile}>
|
|
1484
1516
|
|
|
1485
1517
|
**Example:**
|
|
1518
|
+
|
|
1486
1519
|
```javascript
|
|
1487
|
-
const { stream, metadata } = await GoogleCloudStorage.download(
|
|
1520
|
+
const { stream, metadata } = await GoogleCloudStorage.download(
|
|
1521
|
+
'my-bucket',
|
|
1522
|
+
'videos/large-video.mp4'
|
|
1523
|
+
);
|
|
1488
1524
|
console.log(metadata.size); // File size
|
|
1489
1525
|
stream.pipe(response); // Stream to HTTP response
|
|
1490
1526
|
```
|
|
@@ -1494,12 +1530,14 @@ stream.pipe(response); // Stream to HTTP response
|
|
|
1494
1530
|
Get a readable stream for a file without fetching metadata. Fastest option when metadata is not needed.
|
|
1495
1531
|
|
|
1496
1532
|
**Parameters:**
|
|
1533
|
+
|
|
1497
1534
|
- `bucket` (string): Bucket name
|
|
1498
1535
|
- `path` (string): File path in the bucket
|
|
1499
1536
|
|
|
1500
1537
|
**Returns:** Readable - Node.js readable stream
|
|
1501
1538
|
|
|
1502
1539
|
**Example:**
|
|
1540
|
+
|
|
1503
1541
|
```javascript
|
|
1504
1542
|
const stream = GoogleCloudStorage.getFileStream('my-bucket', 'audio/song.mp3');
|
|
1505
1543
|
stream.pipe(response); // Direct streaming
|
|
@@ -1510,12 +1548,14 @@ stream.pipe(response); // Direct streaming
|
|
|
1510
1548
|
Delete a file from Google Cloud Storage.
|
|
1511
1549
|
|
|
1512
1550
|
**Parameters:**
|
|
1551
|
+
|
|
1513
1552
|
- `bucket` (string): Bucket name
|
|
1514
1553
|
- `path` (string): File path in the bucket
|
|
1515
1554
|
|
|
1516
1555
|
**Returns:** Promise\<void>
|
|
1517
1556
|
|
|
1518
1557
|
**Example:**
|
|
1558
|
+
|
|
1519
1559
|
```javascript
|
|
1520
1560
|
await GoogleCloudStorage.delete('my-bucket', 'temp/old-file.txt');
|
|
1521
1561
|
```
|
|
@@ -1525,6 +1565,7 @@ await GoogleCloudStorage.delete('my-bucket', 'temp/old-file.txt');
|
|
|
1525
1565
|
Generate a unique file path with optional directory and extension.
|
|
1526
1566
|
|
|
1527
1567
|
**Parameters:**
|
|
1568
|
+
|
|
1528
1569
|
- `options` (object, optional):
|
|
1529
1570
|
- `directory` (string): Subdirectory path
|
|
1530
1571
|
- `mime` (string): MIME type for extension detection
|
|
@@ -1533,6 +1574,7 @@ Generate a unique file path with optional directory and extension.
|
|
|
1533
1574
|
**Returns:** string - Generated file path
|
|
1534
1575
|
|
|
1535
1576
|
**Examples:**
|
|
1577
|
+
|
|
1536
1578
|
```javascript
|
|
1537
1579
|
// UUID only
|
|
1538
1580
|
GoogleCloudStorage.generateFileDestination();
|
|
@@ -1555,10 +1597,10 @@ GoogleCloudStorage.generateFileDestination({
|
|
|
1555
1597
|
|
|
1556
1598
|
#### Environment Variables
|
|
1557
1599
|
|
|
1558
|
-
| Variable
|
|
1559
|
-
|
|
|
1560
|
-
| GC_PROJECT
|
|
1561
|
-
| GOOGLE_APPLICATION_CREDENTIALS
|
|
1600
|
+
| Variable | Description | Required |
|
|
1601
|
+
| ------------------------------ | -------------------------------- | -------- |
|
|
1602
|
+
| GC_PROJECT | Google Cloud Project ID | ✗* |
|
|
1603
|
+
| GOOGLE_APPLICATION_CREDENTIALS | Path to service account key file | ✗* |
|
|
1562
1604
|
|
|
1563
1605
|
\* Not required if you call `init()` with a config object
|
|
1564
1606
|
|
|
@@ -1570,6 +1612,7 @@ The entity throws `WebError` with appropriate status codes:
|
|
|
1570
1612
|
- Other errors are passed through from Google Cloud Storage SDK
|
|
1571
1613
|
|
|
1572
1614
|
**Example:**
|
|
1615
|
+
|
|
1573
1616
|
```javascript
|
|
1574
1617
|
try {
|
|
1575
1618
|
await GoogleCloudStorage.upload(stream, 'bucket', {}, {}, { maxSize: '1MB' });
|
|
@@ -1595,13 +1638,13 @@ new Endpoint({
|
|
|
1595
1638
|
async handler(req, res) {
|
|
1596
1639
|
const file = req.files[0];
|
|
1597
1640
|
const stream = Readable.from(file.buffer);
|
|
1598
|
-
|
|
1641
|
+
|
|
1599
1642
|
const result = await GoogleCloudStorage.upload(
|
|
1600
1643
|
stream,
|
|
1601
1644
|
process.env.UPLOAD_BUCKET,
|
|
1602
1645
|
{ directory: 'user-uploads', mime: file.mimetype }
|
|
1603
1646
|
);
|
|
1604
|
-
|
|
1647
|
+
|
|
1605
1648
|
res.json({ path: result.name, url: result.mediaLink });
|
|
1606
1649
|
}
|
|
1607
1650
|
});
|
|
@@ -1662,6 +1705,7 @@ Errors are a crucial part of the application, they are what helps us to properly
|
|
|
1662
1705
|
The engine automatically adapts log output based on the environment:
|
|
1663
1706
|
|
|
1664
1707
|
**Local Development (Readable Format)**
|
|
1708
|
+
|
|
1665
1709
|
- Colorized output with severity levels
|
|
1666
1710
|
- Formatted timestamps and file locations
|
|
1667
1711
|
- Pretty-printed data objects
|
|
@@ -1669,6 +1713,7 @@ The engine automatically adapts log output based on the environment:
|
|
|
1669
1713
|
- HTTP request context when available
|
|
1670
1714
|
|
|
1671
1715
|
**Production/GCP (JSON Format)**
|
|
1716
|
+
|
|
1672
1717
|
- Structured JSON for log aggregation
|
|
1673
1718
|
- Google Cloud Error Reporting integration
|
|
1674
1719
|
- Kubernetes pod information
|
|
@@ -1687,6 +1732,7 @@ LOG_FORMAT=json npm start
|
|
|
1687
1732
|
```
|
|
1688
1733
|
|
|
1689
1734
|
**Example Local Output:**
|
|
1735
|
+
|
|
1690
1736
|
```
|
|
1691
1737
|
[2025-12-10T10:30:45.123Z] ERROR src/endpoints/users.ts:42 User not found
|
|
1692
1738
|
Error Code: user-not-found
|
|
@@ -1943,15 +1989,15 @@ interface User {
|
|
|
1943
1989
|
|
|
1944
1990
|
```javascript
|
|
1945
1991
|
// Case-insensitive matching
|
|
1946
|
-
checkPermission('ADMIN')
|
|
1947
|
-
checkPermission(['READ', 'write'])
|
|
1992
|
+
checkPermission('ADMIN'); // Matches: 'admin', 'Admin', 'ADMIN'
|
|
1993
|
+
checkPermission(['READ', 'write']); // Matches any case variation
|
|
1948
1994
|
|
|
1949
1995
|
// Namespace-style permissions
|
|
1950
|
-
checkPermission('users:read')
|
|
1951
|
-
checkPermission(['users:write', 'users:delete'])
|
|
1996
|
+
checkPermission('users:read');
|
|
1997
|
+
checkPermission(['users:write', 'users:delete']);
|
|
1952
1998
|
|
|
1953
1999
|
// Role-based permissions
|
|
1954
|
-
checkPermission(['admin', 'moderator'])
|
|
2000
|
+
checkPermission(['admin', 'moderator']);
|
|
1955
2001
|
```
|
|
1956
2002
|
|
|
1957
2003
|
#### Response Codes
|
|
@@ -1975,8 +2021,10 @@ checkPermission(['admin', 'moderator'])
|
|
|
1975
2021
|
"message": "Permission denied"
|
|
1976
2022
|
}
|
|
1977
2023
|
```
|
|
2024
|
+
|
|
1978
2025
|
};
|
|
1979
|
-
|
|
2026
|
+
|
|
2027
|
+
````
|
|
1980
2028
|
|
|
1981
2029
|
---
|
|
1982
2030
|
|
|
@@ -2006,7 +2054,7 @@ new Endpoint({
|
|
|
2006
2054
|
res.json({ ok: true });
|
|
2007
2055
|
}
|
|
2008
2056
|
});
|
|
2009
|
-
|
|
2057
|
+
````
|
|
2010
2058
|
|
|
2011
2059
|
#### Defaults
|
|
2012
2060
|
|
|
@@ -2147,7 +2195,10 @@ The function returns an object with the following status options:
|
|
|
2147
2195
|
Generate and verify a short-lived verification token + OTP for sensitive operations.
|
|
2148
2196
|
|
|
2149
2197
|
```javascript
|
|
2150
|
-
import {
|
|
2198
|
+
import {
|
|
2199
|
+
createVerificationToken,
|
|
2200
|
+
verifyVerificationToken
|
|
2201
|
+
} from 'node-server-engine';
|
|
2151
2202
|
|
|
2152
2203
|
// Generate token + OTP
|
|
2153
2204
|
const { token, otp, expiresAt } = createVerificationToken({
|
|
@@ -2435,17 +2486,17 @@ npm run coverage
|
|
|
2435
2486
|
|
|
2436
2487
|
### Scripts
|
|
2437
2488
|
|
|
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`
|
|
2489
|
+
| Script | Description |
|
|
2490
|
+
| ---------------------- | -------------------------------------------------- |
|
|
2491
|
+
| `npm run build` | Compile TypeScript and generate distribution files |
|
|
2492
|
+
| `npm run lint` | Check code for linting errors |
|
|
2493
|
+
| `npm run lint:fix` | Automatically fix linting errors |
|
|
2494
|
+
| `npm run format` | Format code with Prettier |
|
|
2495
|
+
| `npm run format:check` | Check code formatting without modifying files |
|
|
2496
|
+
| `npm test` | Run all tests with coverage |
|
|
2497
|
+
| `npm run test:file` | Run a specific test file |
|
|
2498
|
+
| `npm run coverage` | Generate HTML coverage report |
|
|
2499
|
+
| `npm run coverage:ci` | Generate coverage summary for CI |
|
|
2449
2500
|
|
|
2450
2501
|
### Commit Convention
|
|
2451
2502
|
|
|
@@ -2454,6 +2505,7 @@ This project uses [Conventional Commits](https://www.conventionalcommits.org/) f
|
|
|
2454
2505
|
**Format:** `type(scope): subject`
|
|
2455
2506
|
|
|
2456
2507
|
**Types:**
|
|
2508
|
+
|
|
2457
2509
|
- `feat`: New feature (triggers minor version bump)
|
|
2458
2510
|
- `fix`: Bug fix (triggers patch version bump)
|
|
2459
2511
|
- `docs`: Documentation changes
|
|
@@ -2465,6 +2517,7 @@ This project uses [Conventional Commits](https://www.conventionalcommits.org/) f
|
|
|
2465
2517
|
- `ci`: CI/CD changes
|
|
2466
2518
|
|
|
2467
2519
|
**Examples:**
|
|
2520
|
+
|
|
2468
2521
|
```bash
|
|
2469
2522
|
git commit -m "feat(endpoints): add support for GraphQL endpoints"
|
|
2470
2523
|
git commit -m "fix(auth): resolve JWT token validation issue"
|
|
@@ -2472,6 +2525,7 @@ git commit -m "docs(readme): update installation instructions"
|
|
|
2472
2525
|
```
|
|
2473
2526
|
|
|
2474
2527
|
**Breaking Changes:** Add `BREAKING CHANGE:` in the commit body to trigger a major version bump:
|
|
2528
|
+
|
|
2475
2529
|
```bash
|
|
2476
2530
|
git commit -m "feat(api): redesign authentication flow" -m "BREAKING CHANGE: AuthType enum values changed"
|
|
2477
2531
|
```
|
|
@@ -2481,6 +2535,7 @@ git commit -m "feat(api): redesign authentication flow" -m "BREAKING CHANGE: Aut
|
|
|
2481
2535
|
This project uses [semantic-release](https://github.com/semantic-release/semantic-release) for automated version management and package publishing.
|
|
2482
2536
|
|
|
2483
2537
|
**How it works:**
|
|
2538
|
+
|
|
2484
2539
|
1. Commits are analyzed based on conventional commit format
|
|
2485
2540
|
2. Version number is automatically determined (major.minor.patch)
|
|
2486
2541
|
3. Changelog is generated from commit messages
|
|
@@ -2489,6 +2544,7 @@ This project uses [semantic-release](https://github.com/semantic-release/semanti
|
|
|
2489
2544
|
6. GitHub release is created
|
|
2490
2545
|
|
|
2491
2546
|
**When releases happen:**
|
|
2547
|
+
|
|
2492
2548
|
- Automatically on every push to `main` branch
|
|
2493
2549
|
- Only if there are release-worthy commits (feat, fix, perf, BREAKING CHANGE)
|
|
2494
2550
|
- Releases are skipped for commits with `[skip ci]` in the message
|