@tumbaland/backend-core 1.16.0 → 1.17.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/README.md CHANGED
@@ -120,54 +120,15 @@ This library is optimized for Docker deployments:
120
120
 
121
121
  ## 📦 Releases & Versioning
122
122
 
123
- This library uses [standard-version](https://github.com/conventional-changelog/standard-version) for automated versioning and follows [Conventional Commits](https://conventionalcommits.org/) specification.
124
-
125
- ### Release Types
126
-
127
- - **PATCH** (`1.0.0` `1.0.1`): Bug fixes
128
- - **MINOR** (`1.0.0` `1.1.0`): New features (backward compatible)
129
- - **MAJOR** (`1.0.0` `2.0.0`): Breaking changes
130
-
131
- ### Commit Message Format
132
-
133
- ```
134
- type(scope): description
135
-
136
- [optional body]
137
-
138
- [optional footer]
139
- ```
140
-
141
- **Types:**
142
- - `feat`: New feature
143
- - `fix`: Bug fix
144
- - `docs`: Documentation
145
- - `style`: Code style changes
146
- - `refactor`: Code refactoring
147
- - `test`: Testing
148
- - `chore`: Maintenance
149
-
150
- **Examples:**
151
- ```
152
- feat(auth): add JWT token refresh
153
- fix(logging): resolve memory leak in Winston transport
154
- docs(api): update health check endpoint documentation
155
- ```
156
-
157
- ### Release Process
158
-
159
- 1. **Make changes** with conventional commit messages
160
- 2. **Run release script**: `./release.sh`
161
- 3. **Push changes**: `git push origin main`
162
- 4. **Publish**: Automated via GitHub Actions
163
-
164
- ### Changelog
165
-
166
- Changelogs are maintained at the project level in the root `CHANGELOG.md` file. Individual library changelogs are not generated to maintain consistency across the monorepo.
167
-
168
- ### Beta Releases
169
-
170
- For beta releases: `npm run release:beta`
123
+ This library is a real published npm package (`@tumbaland/backend-core`), versioned independently
124
+ of the monorepo with [standard-version](https://github.com/conventional-changelog/standard-version)
125
+ and [Conventional Commits](https://conventionalcommits.org/). See the root
126
+ [README's "Commits & Releases" section](../../README.md#commits--releases) for the full process
127
+ and the reasoning behind it — summary: commit with conventional messages, then from this directory
128
+ run `npm run release` (never hand-edit `version` and `npm publish` directly, or the package,
129
+ changelog, and tags drift out of sync with each other). Tags for this package are scoped as
130
+ `backend-core-vX.Y.Z` so they can't collide with `frontend-core`'s, `components`', or the
131
+ monorepo's own `app-vX.Y.Z` tags.
171
132
 
172
133
  ---
173
134
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tumbaland/backend-core",
3
- "version": "1.16.0",
3
+ "version": "1.17.0",
4
4
  "description": "Core shared functionality for Tumbaland backend services",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -25,6 +25,9 @@
25
25
  ],
26
26
  "author": "Tumbaland Team",
27
27
  "license": "MIT",
28
+ "standard-version": {
29
+ "tagPrefix": "backend-core-v"
30
+ },
28
31
  "devDependencies": {
29
32
  "@types/cookie-parser": "^1.4.10",
30
33
  "@types/cors": "^2.8.19",
@@ -54,6 +54,15 @@ describe('createBaseApp', () => {
54
54
  expect(second.status).toBe(429);
55
55
  });
56
56
 
57
+ it('uses the plain request logger (no metrics histogram) when metrics is false', async () => {
58
+ const app = createBaseApp({ metrics: false });
59
+ app.get('/ping', (_req, res) => res.json({ ok: true }));
60
+
61
+ const res = await request(app).get('/ping');
62
+
63
+ expect(res.status).toBe(200);
64
+ });
65
+
57
66
  it('mounts /health/live ahead of the rate limiter, so it never gets 429s a busy service would', async () => {
58
67
  const app = createBaseApp({ rateLimiter: (await import('../middleware/security')).createRateLimiter({ windowMs: 60_000, max: 1 }) });
59
68
  app.get('/ping', (_req, res) => res.json({ ok: true }));
@@ -1,6 +1,6 @@
1
1
  import { Request, Response } from 'express';
2
2
  import mongoose from 'mongoose';
3
- import { healthCheck, healthLive } from './healthController';
3
+ import { healthCheck, healthLive, metricsHandler } from './healthController';
4
4
 
5
5
  function mockRes(): Response {
6
6
  const res: Partial<Response> = {};
@@ -88,4 +88,26 @@ describe('healthCheck', () => {
88
88
  expect(res.status).toHaveBeenCalledWith(503);
89
89
  expect(res.json).toHaveBeenCalledWith(expect.objectContaining({ status: 'error', error: 'Health check failed' }));
90
90
  });
91
+
92
+ it('falls back to default service/version/description in the error body when env vars are unset', async () => {
93
+ delete process.env.SERVICE_NAME;
94
+ delete process.env.npm_package_version;
95
+ delete process.env.SERVICE_DESCRIPTION;
96
+ jest.spyOn(mongoose, 'connection', 'get').mockImplementation(() => {
97
+ throw new Error('boom');
98
+ });
99
+ const res = mockRes();
100
+
101
+ await healthCheck({} as Request, res);
102
+
103
+ expect(res.json).toHaveBeenCalledWith(
104
+ expect.objectContaining({ service: 'unknown-service', version: '1.0.0', description: 'Backend service' })
105
+ );
106
+ });
107
+ });
108
+
109
+ describe('metricsHandler re-export', () => {
110
+ it('re-exports the same metricsHandler function from ../metrics', () => {
111
+ expect(typeof metricsHandler).toBe('function');
112
+ });
91
113
  });
@@ -35,6 +35,21 @@ describe('logger', () => {
35
35
  expect(logger.level).toBe('info');
36
36
  });
37
37
 
38
+ it('defaults to development (debug level) when NODE_ENV is unset', async () => {
39
+ const ORIGINAL_ENV = process.env;
40
+ const merged: Record<string, string | undefined> = { ...ORIGINAL_ENV };
41
+ delete merged.NODE_ENV;
42
+ process.env = merged as NodeJS.ProcessEnv;
43
+
44
+ let loggerModule!: typeof import('./logger').default;
45
+ await jest.isolateModulesAsync(async () => {
46
+ loggerModule = (await import('./logger')).default;
47
+ });
48
+
49
+ process.env = ORIGINAL_ENV;
50
+ expect(loggerModule.level).toBe('debug');
51
+ });
52
+
38
53
  it('does not create a file transport when LOG_TO_FILE is unset', async () => {
39
54
  const logger = await loadLogger({ NODE_ENV: 'development', LOG_TO_FILE: undefined });
40
55
  expect(namedTransports(logger, 'file')).toHaveLength(0);
@@ -81,6 +81,31 @@ describe('metricsMiddleware', () => {
81
81
  });
82
82
  });
83
83
 
84
+ describe('default labels', () => {
85
+ it('falls back to app "unknown-service" and version "1.0.0" when their env vars are unset', async () => {
86
+ const ORIGINAL_ENV = process.env;
87
+ const merged: Record<string, string | undefined> = { ...ORIGINAL_ENV };
88
+ delete merged.SERVICE_NAME;
89
+ delete merged.npm_package_version;
90
+ process.env = merged as NodeJS.ProcessEnv;
91
+
92
+ // isolateModulesAsync sandboxes the whole module registry, including
93
+ // prom-client itself — the spy must target the Registry class required
94
+ // *inside* the sandbox, not the one imported at the top of this file.
95
+ await jest.isolateModulesAsync(async () => {
96
+ const innerPromClient = (await import('prom-client')).default;
97
+ const setDefaultLabelsSpy = jest.spyOn(innerPromClient.Registry.prototype, 'setDefaultLabels');
98
+
99
+ await import('./index');
100
+
101
+ expect(setDefaultLabelsSpy).toHaveBeenCalledWith({ app: 'unknown-service', version: '1.0.0' });
102
+ setDefaultLabelsSpy.mockRestore();
103
+ });
104
+
105
+ process.env = ORIGINAL_ENV;
106
+ });
107
+ });
108
+
84
109
  describe('businessMetrics', () => {
85
110
  it('exposes counters that can be incremented without throwing', () => {
86
111
  expect(() => businessMetrics.albumsCreated.inc()).not.toThrow();
@@ -57,4 +57,16 @@ describe('validate', () => {
57
57
  expect(res.status).toBe(200);
58
58
  expect(res.body.data).toEqual({ name: 'Carol' });
59
59
  });
60
+
61
+ it('omits the field prefix when the zod issue has no path (whole-body validation)', async () => {
62
+ const arraySchema = z.array(z.string());
63
+ const app = buildApp(arraySchema);
64
+ const rawResult = arraySchema.safeParse({ not: 'an array' });
65
+
66
+ const res = await request(app).post('/things').send({ not: 'an array' });
67
+
68
+ expect(res.status).toBe(400);
69
+ expect(rawResult.success).toBe(false);
70
+ expect(res.body.message).toBe(!rawResult.success ? rawResult.error.issues[0].message : undefined);
71
+ });
60
72
  });
@@ -39,6 +39,7 @@ jest.mock('@opentelemetry/resources', () => ({
39
39
  }));
40
40
 
41
41
  import { context } from '@opentelemetry/api';
42
+ import { ATTR_SERVICE_VERSION } from '@opentelemetry/semantic-conventions';
42
43
 
43
44
  async function loadTracing() {
44
45
  let tracing!: typeof import('./index');
@@ -92,6 +93,23 @@ describe('initTracer', () => {
92
93
  expect(exporterCtorMock).toHaveBeenCalledWith({ url: 'http://jaeger-endpoint/v1/traces' });
93
94
  });
94
95
 
96
+ it('falls back to version 1.0.0 and environment "development" when their env vars are unset', async () => {
97
+ const merged: Record<string, string | undefined> = { ...process.env };
98
+ delete merged.npm_package_version;
99
+ delete merged.NODE_ENV;
100
+ process.env = merged as NodeJS.ProcessEnv;
101
+
102
+ const { initTracer } = await loadTracing();
103
+ initTracer('album-service');
104
+
105
+ expect(resourceFromAttributesMock).toHaveBeenCalledWith(
106
+ expect.objectContaining({
107
+ [ATTR_SERVICE_VERSION]: '1.0.0',
108
+ 'deployment.environment': 'development'
109
+ })
110
+ );
111
+ });
112
+
95
113
  it('registers the provider and returns a tracer scoped to the given service name', async () => {
96
114
  const { initTracer } = await loadTracing();
97
115
 
@@ -1,4 +1,7 @@
1
1
  {
2
2
  "extends": "./tsconfig.json",
3
+ "compilerOptions": {
4
+ "types": ["node"]
5
+ },
3
6
  "exclude": ["node_modules", "dist", "**/*.test.ts"]
4
7
  }
package/tsconfig.json CHANGED
@@ -2,13 +2,16 @@
2
2
  "compilerOptions": {
3
3
  "target": "es2023",
4
4
  "module": "commonjs",
5
- "lib": ["ES2020"],
6
5
  "outDir": "./dist",
7
6
  "rootDir": "./src",
8
7
  "strict": true,
9
8
  "esModuleInterop": true,
10
9
  "skipLibCheck": true,
11
10
  "forceConsistentCasingInFileNames": true,
11
+ "moduleDetection": "force",
12
+ "noImplicitReturns": true,
13
+ "noFallthroughCasesInSwitch": true,
14
+ "noImplicitOverride": true,
12
15
  "declaration": true,
13
16
  "declarationMap": true,
14
17
  "sourceMap": true,