@noony-serverless/core 0.4.0 → 0.4.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
CHANGED
|
@@ -432,11 +432,210 @@ EXPOSE 8080
|
|
|
432
432
|
CMD ["npm", "start"]
|
|
433
433
|
```
|
|
434
434
|
|
|
435
|
+
## Publishing to npm
|
|
436
|
+
|
|
437
|
+
This package is published as `@noony-serverless/core` on npm. Follow these steps to publish a new version:
|
|
438
|
+
|
|
439
|
+
### Prerequisites
|
|
440
|
+
|
|
441
|
+
1. **npm Account**: You need an npm account. Create one at [npmjs.com/signup](https://www.npmjs.com/signup)
|
|
442
|
+
2. **Organization Access**: You must have access to the `@noony-serverless` organization on npm
|
|
443
|
+
- If you own the organization, you're all set
|
|
444
|
+
- If not, you need to [create the organization](https://www.npmjs.com/org/create) first
|
|
445
|
+
3. **Two-Factor Authentication**: Highly recommended for security
|
|
446
|
+
|
|
447
|
+
### Publishing Steps
|
|
448
|
+
|
|
449
|
+
#### 1. Login to npm
|
|
450
|
+
|
|
451
|
+
```bash
|
|
452
|
+
npm login
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
You'll be prompted for:
|
|
456
|
+
|
|
457
|
+
- Username
|
|
458
|
+
- Password
|
|
459
|
+
- Email
|
|
460
|
+
- One-time password (if 2FA is enabled)
|
|
461
|
+
|
|
462
|
+
Verify you're logged in:
|
|
463
|
+
|
|
464
|
+
```bash
|
|
465
|
+
npm whoami
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
#### 2. Prepare the Package
|
|
469
|
+
|
|
470
|
+
Ensure all changes are committed and tests pass:
|
|
471
|
+
|
|
472
|
+
```bash
|
|
473
|
+
# Run tests
|
|
474
|
+
npm test
|
|
475
|
+
|
|
476
|
+
# Run linter
|
|
477
|
+
npm run lint
|
|
478
|
+
|
|
479
|
+
# Build the package
|
|
480
|
+
npm run build
|
|
481
|
+
```
|
|
482
|
+
|
|
483
|
+
#### 3. Update Version
|
|
484
|
+
|
|
485
|
+
Update the version in `package.json` following [Semantic Versioning](https://semver.org/):
|
|
486
|
+
|
|
487
|
+
```bash
|
|
488
|
+
# For bug fixes (0.4.0 -> 0.4.1)
|
|
489
|
+
npm version patch
|
|
490
|
+
|
|
491
|
+
# For new features (0.4.0 -> 0.5.0)
|
|
492
|
+
npm version minor
|
|
493
|
+
|
|
494
|
+
# For breaking changes (0.4.0 -> 1.0.0)
|
|
495
|
+
npm version major
|
|
496
|
+
```
|
|
497
|
+
|
|
498
|
+
This will:
|
|
499
|
+
|
|
500
|
+
- Update `package.json` version
|
|
501
|
+
- Create a git commit
|
|
502
|
+
- Create a git tag
|
|
503
|
+
|
|
504
|
+
#### 4. Publish to npm
|
|
505
|
+
|
|
506
|
+
For scoped packages (like `@noony-serverless/core`), you must specify public access:
|
|
507
|
+
|
|
508
|
+
```bash
|
|
509
|
+
npm publish --access public
|
|
510
|
+
```
|
|
511
|
+
|
|
512
|
+
**For the first publish only**, you need the `--access public` flag. Subsequent publishes can use:
|
|
513
|
+
|
|
514
|
+
```bash
|
|
515
|
+
npm publish
|
|
516
|
+
```
|
|
517
|
+
|
|
518
|
+
#### 5. Push to Git
|
|
519
|
+
|
|
520
|
+
Don't forget to push the version commit and tags:
|
|
521
|
+
|
|
522
|
+
```bash
|
|
523
|
+
git push && git push --tags
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
### Publishing Checklist
|
|
527
|
+
|
|
528
|
+
Before publishing, verify:
|
|
529
|
+
|
|
530
|
+
- [ ] All tests pass (`npm test`)
|
|
531
|
+
- [ ] No linting errors (`npm run lint`)
|
|
532
|
+
- [ ] Build succeeds (`npm run build`)
|
|
533
|
+
- [ ] Version number updated in `package.json`
|
|
534
|
+
- [ ] CHANGELOG.md updated (if applicable)
|
|
535
|
+
- [ ] README.md is up to date
|
|
536
|
+
- [ ] All changes committed to git
|
|
537
|
+
- [ ] Logged into npm (`npm whoami`)
|
|
538
|
+
|
|
539
|
+
### Troubleshooting
|
|
540
|
+
|
|
541
|
+
#### Error: "Access token expired or revoked"
|
|
542
|
+
|
|
543
|
+
**Solution**: Run `npm login` to re-authenticate
|
|
544
|
+
|
|
545
|
+
#### Error: "404 Not Found - Not in this registry"
|
|
546
|
+
|
|
547
|
+
**Solution**: For first publish of a scoped package, use:
|
|
548
|
+
|
|
549
|
+
```bash
|
|
550
|
+
npm publish --access public
|
|
551
|
+
```
|
|
552
|
+
|
|
553
|
+
#### Error: "You do not have permission to publish"
|
|
554
|
+
|
|
555
|
+
**Solution**:
|
|
556
|
+
|
|
557
|
+
- Verify you're logged in as the correct user
|
|
558
|
+
- Check you have publish access to the `@noony-serverless` organization
|
|
559
|
+
- Create the organization if it doesn't exist
|
|
560
|
+
|
|
561
|
+
#### Error: "Cannot publish over existing version"
|
|
562
|
+
|
|
563
|
+
**Solution**: Update the version number in `package.json` or use:
|
|
564
|
+
|
|
565
|
+
```bash
|
|
566
|
+
npm version patch # or minor/major
|
|
567
|
+
```
|
|
568
|
+
|
|
569
|
+
#### Error: "403 Forbidden"
|
|
570
|
+
|
|
571
|
+
**Solutions**:
|
|
572
|
+
|
|
573
|
+
- Ensure you're logged in: `npm whoami`
|
|
574
|
+
- Verify you own the package or have collaborator access
|
|
575
|
+
- If this is a new scoped package, verify the organization exists
|
|
576
|
+
|
|
577
|
+
### Automated Publishing with GitHub Actions
|
|
578
|
+
|
|
579
|
+
For automated publishing, create `.github/workflows/publish.yml`:
|
|
580
|
+
|
|
581
|
+
```yaml
|
|
582
|
+
name: Publish to npm
|
|
583
|
+
|
|
584
|
+
on:
|
|
585
|
+
release:
|
|
586
|
+
types: [created]
|
|
587
|
+
|
|
588
|
+
jobs:
|
|
589
|
+
publish:
|
|
590
|
+
runs-on: ubuntu-latest
|
|
591
|
+
steps:
|
|
592
|
+
- uses: actions/checkout@v3
|
|
593
|
+
- uses: actions/setup-node@v3
|
|
594
|
+
with:
|
|
595
|
+
node-version: '20'
|
|
596
|
+
registry-url: 'https://registry.npmjs.org'
|
|
597
|
+
- run: npm ci
|
|
598
|
+
- run: npm test
|
|
599
|
+
- run: npm run build
|
|
600
|
+
- run: npm publish --access public
|
|
601
|
+
env:
|
|
602
|
+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
603
|
+
```
|
|
604
|
+
|
|
605
|
+
To use this:
|
|
606
|
+
|
|
607
|
+
1. Create an npm access token at [npmjs.com/settings/tokens](https://www.npmjs.com/settings/tokens)
|
|
608
|
+
2. Add it as a GitHub secret named `NPM_TOKEN`
|
|
609
|
+
3. Create a GitHub release to trigger publishing
|
|
610
|
+
|
|
611
|
+
### Version Management
|
|
612
|
+
|
|
613
|
+
This package follows [Semantic Versioning](https://semver.org/):
|
|
614
|
+
|
|
615
|
+
- **MAJOR** version (1.0.0 → 2.0.0): Breaking changes
|
|
616
|
+
- **MINOR** version (1.0.0 → 1.1.0): New features, backwards compatible
|
|
617
|
+
- **PATCH** version (1.0.0 → 1.0.1): Bug fixes, backwards compatible
|
|
618
|
+
|
|
619
|
+
Current version: **0.4.0**
|
|
620
|
+
|
|
621
|
+
### Package Distribution
|
|
622
|
+
|
|
623
|
+
When published, the package includes only the `build/` directory contents:
|
|
624
|
+
|
|
625
|
+
- `build/core/**/*.js` and `*.d.ts`
|
|
626
|
+
- `build/middlewares/**/*.js` and `*.d.ts`
|
|
627
|
+
- `build/utils/**/*.js` and `*.d.ts`
|
|
628
|
+
- `build/index.js` and `index.d.ts`
|
|
629
|
+
- `README.md`
|
|
630
|
+
|
|
631
|
+
Source TypeScript files are not included in the npm package.
|
|
632
|
+
|
|
435
633
|
## Community & Support
|
|
436
634
|
|
|
437
635
|
- 📖 [Documentation](https://github.com/noony-org/noony-serverless)
|
|
438
636
|
- 🐛 [Issue Tracker](https://github.com/noony-org/noony-serverless/issues)
|
|
439
637
|
- 💬 [Discussions](https://github.com/noony-org/noony-serverless/discussions)
|
|
638
|
+
- 📦 [npm Package](https://www.npmjs.com/package/@noony-serverless/core)
|
|
440
639
|
|
|
441
640
|
## License
|
|
442
641
|
|
|
@@ -3,9 +3,26 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.bodyValidatorMiddleware = exports.BodyValidationMiddleware = void 0;
|
|
4
4
|
const zod_1 = require("zod");
|
|
5
5
|
const errors_1 = require("../core/errors");
|
|
6
|
-
const
|
|
6
|
+
const fastify_wrapper_1 = require("../utils/fastify-wrapper");
|
|
7
|
+
// Reusable error objects for common cases (avoid allocations)
|
|
8
|
+
const VALIDATION_ERROR_INVALID_JSON = new errors_1.ValidationError('Invalid JSON in request body', [
|
|
9
|
+
{
|
|
10
|
+
code: 'custom',
|
|
11
|
+
path: [],
|
|
12
|
+
message: 'Request body must be valid JSON',
|
|
13
|
+
},
|
|
14
|
+
]);
|
|
15
|
+
const VALIDATION_ERROR_MISSING_BODY = new errors_1.ValidationError('Request body is required', [
|
|
16
|
+
{
|
|
17
|
+
code: 'custom',
|
|
18
|
+
path: [],
|
|
19
|
+
message: 'Request body is missing or empty',
|
|
20
|
+
},
|
|
21
|
+
]);
|
|
22
|
+
// Use synchronous parse for better performance (Zod's parseAsync adds overhead)
|
|
23
|
+
const validateWithZod = (schema, data) => {
|
|
7
24
|
try {
|
|
8
|
-
return
|
|
25
|
+
return schema.parse(data);
|
|
9
26
|
}
|
|
10
27
|
catch (error) {
|
|
11
28
|
if (error instanceof zod_1.z.ZodError) {
|
|
@@ -14,6 +31,36 @@ const validateBody = async (schema, data) => {
|
|
|
14
31
|
throw error;
|
|
15
32
|
}
|
|
16
33
|
};
|
|
34
|
+
/**
|
|
35
|
+
* Comprehensive body validation function that handles retrieval from multiple sources,
|
|
36
|
+
* JSON parsing, and Zod schema validation.
|
|
37
|
+
* @internal
|
|
38
|
+
*/
|
|
39
|
+
const validateBody = (schema, context) => {
|
|
40
|
+
// Try to get body from the original Fastify request via WeakMap
|
|
41
|
+
// This fixes the issue where req.body becomes undefined after Handler.executeGeneric copies properties
|
|
42
|
+
const originalReq = fastify_wrapper_1.requestBodyMap.get(context.req);
|
|
43
|
+
// Fast path: check most common case first (originalReq.body)
|
|
44
|
+
let bodyData = originalReq?.body;
|
|
45
|
+
if (!bodyData) {
|
|
46
|
+
bodyData =
|
|
47
|
+
context.req.validatedBody ?? context.req.parsedBody ?? context.req.body;
|
|
48
|
+
}
|
|
49
|
+
// If no parsed body found, try the raw body string (from Cloud Functions wrapper) and parse it
|
|
50
|
+
if (!bodyData && originalReq?.__rawBody) {
|
|
51
|
+
try {
|
|
52
|
+
bodyData = JSON.parse(originalReq.__rawBody);
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
throw VALIDATION_ERROR_INVALID_JSON;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (!bodyData) {
|
|
59
|
+
throw VALIDATION_ERROR_MISSING_BODY;
|
|
60
|
+
}
|
|
61
|
+
// Validate with Zod schema
|
|
62
|
+
return validateWithZod(schema, bodyData);
|
|
63
|
+
};
|
|
17
64
|
/**
|
|
18
65
|
* Body validation middleware using Zod schemas for runtime type checking.
|
|
19
66
|
* Validates the parsed request body against a provided Zod schema and sets
|
|
@@ -52,7 +99,7 @@ class BodyValidationMiddleware {
|
|
|
52
99
|
this.schema = schema;
|
|
53
100
|
}
|
|
54
101
|
async before(context) {
|
|
55
|
-
context.req.validatedBody =
|
|
102
|
+
context.req.validatedBody = validateBody(this.schema, context);
|
|
56
103
|
}
|
|
57
104
|
}
|
|
58
105
|
exports.BodyValidationMiddleware = BodyValidationMiddleware;
|
|
@@ -88,11 +135,12 @@ exports.BodyValidationMiddleware = BodyValidationMiddleware;
|
|
|
88
135
|
* .handle(handleLogin);
|
|
89
136
|
* ```
|
|
90
137
|
*/
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
}
|
|
138
|
+
const bodyValidatorMiddleware = (schema) => {
|
|
139
|
+
return {
|
|
140
|
+
before: async (context) => {
|
|
141
|
+
context.req.validatedBody = validateBody(schema, context);
|
|
142
|
+
},
|
|
143
|
+
};
|
|
144
|
+
};
|
|
97
145
|
exports.bodyValidatorMiddleware = bodyValidatorMiddleware;
|
|
98
146
|
//# sourceMappingURL=bodyValidationMiddleware.js.map
|
|
@@ -1,74 +1,5 @@
|
|
|
1
1
|
import type { FastifyRequest, FastifyReply } from 'fastify';
|
|
2
2
|
import { Handler } from '../core/handler';
|
|
3
|
-
|
|
4
|
-
* Create a Fastify route handler wrapper for a Noony handler
|
|
5
|
-
*
|
|
6
|
-
* Wraps a Noony handler into a Fastify route handler for use with Fastify server.
|
|
7
|
-
* This pattern enables running Noony handlers with Fastify's high-performance HTTP framework.
|
|
8
|
-
*
|
|
9
|
-
* @param noonyHandler - The Noony handler to wrap (contains middleware chain and controller)
|
|
10
|
-
* @param functionName - Name for error logging purposes
|
|
11
|
-
* @param initializeDependencies - Async function that initializes dependencies (database, services, etc.)
|
|
12
|
-
* Uses singleton pattern to prevent re-initialization across requests
|
|
13
|
-
* @returns Fastify route handler: `(req: FastifyRequest, reply: FastifyReply) => Promise<void>`
|
|
14
|
-
*
|
|
15
|
-
* @remarks
|
|
16
|
-
* This wrapper ensures:
|
|
17
|
-
* - Dependencies are initialized before handler execution (singleton pattern for efficiency)
|
|
18
|
-
* - Noony handlers work seamlessly with Fastify routing
|
|
19
|
-
* - Errors are caught and returned as proper HTTP responses
|
|
20
|
-
* - Response is not sent twice (`reply.sent` check)
|
|
21
|
-
* - `RESPONSE_SENT` errors are ignored (response already sent by middleware)
|
|
22
|
-
* - Real errors return 500 with generic message for security
|
|
23
|
-
*
|
|
24
|
-
* @example
|
|
25
|
-
* Creating Fastify app with multiple routes:
|
|
26
|
-
* ```typescript
|
|
27
|
-
* import Fastify from 'fastify';
|
|
28
|
-
* import { createFastifyHandler } from '@noony-serverless/core';
|
|
29
|
-
* import { loginHandler, getConfigHandler } from './handlers';
|
|
30
|
-
*
|
|
31
|
-
* // Initialize dependencies once per app startup
|
|
32
|
-
* let initialized = false;
|
|
33
|
-
* async function initializeDependencies(): Promise<void> {
|
|
34
|
-
* if (initialized) return;
|
|
35
|
-
* const db = await databaseService.connect();
|
|
36
|
-
* await initializeServices(db);
|
|
37
|
-
* initialized = true;
|
|
38
|
-
* }
|
|
39
|
-
*
|
|
40
|
-
* const server = Fastify({ logger: true });
|
|
41
|
-
*
|
|
42
|
-
* // Helper shorthand
|
|
43
|
-
* const adapt = (handler, name) => createFastifyHandler(handler, name, initializeDependencies);
|
|
44
|
-
*
|
|
45
|
-
* // Auth routes
|
|
46
|
-
* server.post('/api/auth/login', adapt(loginHandler, 'login'));
|
|
47
|
-
*
|
|
48
|
-
* // Config routes
|
|
49
|
-
* server.get('/api/config', adapt(getConfigHandler, 'getConfig'));
|
|
50
|
-
*
|
|
51
|
-
* // Start server
|
|
52
|
-
* server.listen({ port: 3000 }, (err) => {
|
|
53
|
-
* if (err) throw err;
|
|
54
|
-
* console.log('Server running on port 3000');
|
|
55
|
-
* });
|
|
56
|
-
* ```
|
|
57
|
-
*
|
|
58
|
-
* @example
|
|
59
|
-
* Fastify routing with path parameters:
|
|
60
|
-
* ```typescript
|
|
61
|
-
* const server = Fastify();
|
|
62
|
-
*
|
|
63
|
-
* // Routes with path parameters work seamlessly
|
|
64
|
-
* server.get('/api/users/:userId', adapt(getUserHandler, 'getUser'));
|
|
65
|
-
* server.patch('/api/config/sections/:sectionId', adapt(updateSectionHandler, 'updateSection'));
|
|
66
|
-
*
|
|
67
|
-
* // Path parameters available in Noony handler via context.req.params
|
|
68
|
-
* ```
|
|
69
|
-
*
|
|
70
|
-
* @see {@link createHttpFunction} for Cloud Functions Framework integration (production deployment)
|
|
71
|
-
* @see {@link wrapNoonyHandler} for Express integration
|
|
72
|
-
*/
|
|
3
|
+
export declare const requestBodyMap: WeakMap<any, FastifyRequest<import("fastify").RouteGenericInterface, import("fastify").RawServerDefault, import("http").IncomingMessage, import("fastify").FastifySchema, import("fastify").FastifyTypeProviderDefault, unknown, import("fastify").FastifyBaseLogger, import("fastify/types/type-provider").ResolveFastifyRequestType<import("fastify").FastifyTypeProviderDefault, import("fastify").FastifySchema, import("fastify").RouteGenericInterface>>>;
|
|
73
4
|
export declare function createFastifyHandler(noonyHandler: Handler<unknown>, functionName: string, initializeDependencies: () => Promise<void>): (req: FastifyRequest, reply: FastifyReply) => Promise<void>;
|
|
74
5
|
//# sourceMappingURL=fastify-wrapper.d.ts.map
|
|
@@ -1,26 +1,45 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.requestBodyMap = void 0;
|
|
3
4
|
exports.createFastifyHandler = createFastifyHandler;
|
|
4
5
|
const logger_1 = require("../core/logger");
|
|
6
|
+
// Global WeakMap to store the original Fastify request for each GenericRequest
|
|
7
|
+
// This allows middlewares to access the original request and its body
|
|
8
|
+
// even after the GenericRequest properties have been copied by Handler.executeGeneric
|
|
9
|
+
exports.requestBodyMap = new WeakMap();
|
|
10
|
+
// Pre-allocated empty objects to avoid allocations in hot path
|
|
11
|
+
const EMPTY_QUERY = Object.freeze({});
|
|
12
|
+
const EMPTY_PARAMS = Object.freeze({});
|
|
5
13
|
/**
|
|
6
14
|
* Adapt Fastify Request to GenericRequest for Noony handlers
|
|
7
15
|
*
|
|
16
|
+
* IMPORTANT: Stores the original Fastify request in a WeakMap so middlewares
|
|
17
|
+
* can access the body even after properties are copied by Handler.executeGeneric
|
|
18
|
+
*
|
|
8
19
|
* @internal
|
|
9
20
|
*/
|
|
10
21
|
function adaptFastifyRequest(req) {
|
|
11
|
-
|
|
22
|
+
// Fast path: use pre-allocated empty objects when no query/params
|
|
23
|
+
const query = req.query || EMPTY_QUERY;
|
|
24
|
+
const params = req.params || EMPTY_PARAMS;
|
|
25
|
+
// Inline path extraction to avoid optional chaining overhead
|
|
26
|
+
const routeUrl = req.routeOptions?.url;
|
|
27
|
+
const path = routeUrl || req.url;
|
|
28
|
+
const genericReq = {
|
|
12
29
|
method: req.method,
|
|
13
30
|
url: req.url,
|
|
14
|
-
path
|
|
31
|
+
path,
|
|
15
32
|
headers: req.headers,
|
|
16
|
-
query:
|
|
17
|
-
params:
|
|
33
|
+
query: query,
|
|
34
|
+
params: params,
|
|
18
35
|
body: req.body,
|
|
19
|
-
// Fastify already parses the body, so set parsedBody for BodyValidationMiddleware
|
|
20
36
|
parsedBody: req.body,
|
|
21
37
|
ip: req.ip,
|
|
22
38
|
userAgent: req.headers['user-agent'],
|
|
23
39
|
};
|
|
40
|
+
// Store the original Fastify request in the WeakMap for middleware access
|
|
41
|
+
exports.requestBodyMap.set(genericReq, req);
|
|
42
|
+
return genericReq;
|
|
24
43
|
}
|
|
25
44
|
/**
|
|
26
45
|
* Adapt Fastify Reply to GenericResponse for Noony handlers
|
|
@@ -37,11 +56,17 @@ function adaptFastifyResponse(reply) {
|
|
|
37
56
|
return response;
|
|
38
57
|
},
|
|
39
58
|
json(data) {
|
|
59
|
+
// Early return if already sent (avoid duplicate sends)
|
|
60
|
+
if (reply.sent)
|
|
61
|
+
return response;
|
|
40
62
|
headersSent = true;
|
|
41
63
|
reply.send(data);
|
|
42
64
|
return response;
|
|
43
65
|
},
|
|
44
66
|
send(data) {
|
|
67
|
+
// Early return if already sent (avoid duplicate sends)
|
|
68
|
+
if (reply.sent)
|
|
69
|
+
return response;
|
|
45
70
|
headersSent = true;
|
|
46
71
|
reply.send(data);
|
|
47
72
|
return response;
|
|
@@ -51,12 +76,16 @@ function adaptFastifyResponse(reply) {
|
|
|
51
76
|
return response;
|
|
52
77
|
},
|
|
53
78
|
headers(headers) {
|
|
54
|
-
Object.entries
|
|
55
|
-
|
|
56
|
-
|
|
79
|
+
// Optimized header setting - direct iteration instead of Object.entries
|
|
80
|
+
for (const key in headers) {
|
|
81
|
+
reply.header(key, headers[key]);
|
|
82
|
+
}
|
|
57
83
|
return response;
|
|
58
84
|
},
|
|
59
85
|
end() {
|
|
86
|
+
// Early return if already sent (avoid duplicate sends)
|
|
87
|
+
if (reply.sent)
|
|
88
|
+
return;
|
|
60
89
|
headersSent = true;
|
|
61
90
|
reply.send();
|
|
62
91
|
},
|
|
@@ -139,7 +168,19 @@ function adaptFastifyResponse(reply) {
|
|
|
139
168
|
* @see {@link createHttpFunction} for Cloud Functions Framework integration (production deployment)
|
|
140
169
|
* @see {@link wrapNoonyHandler} for Express integration
|
|
141
170
|
*/
|
|
171
|
+
// Pre-allocated error response object to avoid allocations
|
|
172
|
+
const INTERNAL_ERROR_RESPONSE = Object.freeze({
|
|
173
|
+
success: false,
|
|
174
|
+
error: Object.freeze({
|
|
175
|
+
code: 'INTERNAL_SERVER_ERROR',
|
|
176
|
+
message: 'An unexpected error occurred',
|
|
177
|
+
}),
|
|
178
|
+
});
|
|
179
|
+
// Constant for performance-critical string comparison
|
|
180
|
+
const RESPONSE_SENT_MESSAGE = 'RESPONSE_SENT';
|
|
142
181
|
function createFastifyHandler(noonyHandler, functionName, initializeDependencies) {
|
|
182
|
+
// Pre-bind the error log prefix to avoid string concatenation in hot path
|
|
183
|
+
const errorLogPrefix = `${functionName} handler error`;
|
|
143
184
|
return async (req, reply) => {
|
|
144
185
|
try {
|
|
145
186
|
// Ensure dependencies are initialized
|
|
@@ -151,23 +192,18 @@ function createFastifyHandler(noonyHandler, functionName, initializeDependencies
|
|
|
151
192
|
await noonyHandler.executeGeneric(genericReq, genericRes);
|
|
152
193
|
}
|
|
153
194
|
catch (error) {
|
|
154
|
-
//
|
|
155
|
-
if (error instanceof Error && error.message ===
|
|
195
|
+
// Fast path: check RESPONSE_SENT first (most common error to ignore)
|
|
196
|
+
if (error instanceof Error && error.message === RESPONSE_SENT_MESSAGE) {
|
|
156
197
|
return;
|
|
157
198
|
}
|
|
158
|
-
|
|
199
|
+
// Log error with pre-allocated prefix
|
|
200
|
+
logger_1.logger.error(errorLogPrefix, {
|
|
159
201
|
error: error instanceof Error ? error.message : 'Unknown error',
|
|
160
202
|
stack: error instanceof Error ? error.stack : undefined,
|
|
161
203
|
});
|
|
162
204
|
// Graceful error handling - only send if response not already sent
|
|
163
205
|
if (!reply.sent) {
|
|
164
|
-
reply.code(500).send(
|
|
165
|
-
success: false,
|
|
166
|
-
error: {
|
|
167
|
-
code: 'INTERNAL_SERVER_ERROR',
|
|
168
|
-
message: 'An unexpected error occurred',
|
|
169
|
-
},
|
|
170
|
-
});
|
|
206
|
+
reply.code(500).send(INTERNAL_ERROR_RESPONSE);
|
|
171
207
|
}
|
|
172
208
|
}
|
|
173
209
|
};
|
package/package.json
CHANGED