@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 validateBody = async (schema, data) => {
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 await schema.parseAsync(data);
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 = await validateBody(this.schema, context.req.parsedBody);
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
- // Modified to fix type instantiation error
92
- const bodyValidatorMiddleware = (schema) => ({
93
- before: async (context) => {
94
- context.req.parsedBody = await validateBody(schema, context.req.body);
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
- return {
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: req.routeOptions?.url || req.url,
31
+ path,
15
32
  headers: req.headers,
16
- query: (req.query || {}),
17
- params: (req.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(headers).forEach(([key, value]) => {
55
- reply.header(key, value);
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
- // Ignore RESPONSE_SENT markers (response already sent by middleware)
155
- if (error instanceof Error && error.message === 'RESPONSE_SENT') {
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
- logger_1.logger.error(`${functionName} handler error`, {
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@noony-serverless/core",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
4
4
  "description": "A Middy base framework compatible with Firebase and GCP Cloud Functions with TypeScript",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",