Skip to content

Commit e44c732

Browse files
authored
Merge pull request #179 from Akatenvictor/feat/new-backend-implementation
added backend implementationd
2 parents 04b5df1 + 9086741 commit e44c732

4 files changed

Lines changed: 226 additions & 3 deletions

File tree

backend/src/index.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,17 +13,18 @@ import { jobsRouter } from './routes/jobs.js';
1313
import { healthRouter } from './routes/health.js';
1414
import { queueRouter } from './routes/queue.js';
1515
import { slaRouter } from './routes/sla.js';
16+
import { legacyRouter } from './routes/legacy.js';
1617
import { startJobs, getJobScheduler } from './jobs/index.js';
1718
import { errorHandler, notFoundHandler, AppError } from './middleware/errorHandler.js';
1819
import { messageQueue } from './services/queue.js';
1920
import { registerDefaultProcessors } from './services/queue-producers.js';
2021
import { slaTrackingMiddleware } from './middleware/slaTracking.js';
2122
import { requestIdMiddleware, REQUEST_ID_HEADER } from './middleware/requestId.js';
22-
import { validateEnv, config } from './config/env.js';
23+
import { validateEnv, config as getConfig } from './config/env.js';
2324

2425
// Validate environment variables at startup
2526
validateEnv();
26-
const env = config();
27+
const env = getConfig();
2728

2829
const traceStorage = new AsyncLocalStorage<string>();
2930

@@ -211,6 +212,7 @@ apiV1Router.use('/catalog', catalogRouter);
211212
apiV1Router.use('/jobs', jobsRouter);
212213
apiV1Router.use('/queue', queueRouter);
213214
apiV1Router.use('/sla', slaRouter);
215+
apiV1Router.use('/legacy', legacyRouter);
214216

215217
app.use('/api/v1', apiV1Router);
216218

@@ -222,7 +224,7 @@ app.use('/api', (req: Request, res: Response, next: NextFunction) => {
222224
if (req.apiVersion === 'v1') {
223225
return apiV1Router(req, res, next);
224226
}
225-
227+
226228
next(new AppError(404, `API Version ${req.apiVersion} is not supported`, 'UNSUPPORTED_API_VERSION'));
227229
});
228230

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
import { describe, it, expect, vi, beforeEach } from 'vitest';
2+
import type { Request, Response } from 'express';
3+
import { deprecationMiddleware } from '../deprecation.js';
4+
5+
// ─── Helpers ──────────────────────────────────────────────────────────────────
6+
7+
function makeReq(overrides: Partial<Request> = {}): Request {
8+
return {
9+
method: 'GET',
10+
originalUrl: '/api/v1/test',
11+
ip: '127.0.0.1',
12+
headers: {},
13+
...overrides,
14+
} as unknown as Request;
15+
}
16+
17+
function makeRes(): {
18+
res: Response;
19+
headers: Record<string, string | string[] | number>;
20+
} {
21+
const headers: Record<string, string | string[] | number> = {};
22+
23+
const res = {
24+
setHeader: vi.fn((name: string, value: string | string[] | number) => {
25+
headers[name] = value;
26+
}),
27+
getHeader: vi.fn((name: string) => {
28+
return headers[name];
29+
}),
30+
} as unknown as Response;
31+
32+
return { res, headers };
33+
}
34+
35+
// ─── Tests ────────────────────────────────────────────────────────────────────
36+
37+
describe('deprecationMiddleware()', () => {
38+
let next: ReturnType<typeof vi.fn>;
39+
40+
beforeEach(() => {
41+
next = vi.fn();
42+
// Mock console.warn to avoid cluttering test output
43+
vi.spyOn(console, 'warn').mockImplementation(() => { });
44+
});
45+
46+
it('sets Deprecation header', () => {
47+
const req = makeReq();
48+
const { res, headers } = makeRes();
49+
const deprecationDate = '2023-12-31';
50+
const mw = deprecationMiddleware({ deprecationDate });
51+
52+
mw(req, res, next);
53+
54+
expect(headers['Deprecation']).toBe(new Date(deprecationDate).toUTCString());
55+
expect(next).toHaveBeenCalledOnce();
56+
});
57+
58+
it('sets Sunset header when provided', () => {
59+
const req = makeReq();
60+
const { res, headers } = makeRes();
61+
const deprecationDate = '2023-10-01';
62+
const sunsetDate = '2024-03-31';
63+
const mw = deprecationMiddleware({ deprecationDate, sunsetDate });
64+
65+
mw(req, res, next);
66+
67+
expect(headers['Deprecation']).toBe(new Date(deprecationDate).toUTCString());
68+
expect(headers['Sunset']).toBe(new Date(sunsetDate).toUTCString());
69+
});
70+
71+
it('sets Link header for successor-version', () => {
72+
const req = makeReq();
73+
const { res, headers } = makeRes();
74+
const alternativeUrl = 'https://api.example.com/v2';
75+
const mw = deprecationMiddleware({
76+
deprecationDate: '2023-01-01',
77+
alternativeUrl
78+
});
79+
80+
mw(req, res, next);
81+
82+
expect(headers['Link']).toBe(`<${alternativeUrl}>; rel="successor-version"`);
83+
});
84+
85+
it('appends to existing Link header', () => {
86+
const req = makeReq();
87+
const { res, headers } = makeRes();
88+
89+
// Set existing Link header
90+
const existingLink = '<https://docs.example.com>; rel="help"';
91+
res.setHeader('Link', existingLink);
92+
93+
const alternativeUrl = 'https://api.example.com/v2';
94+
const mw = deprecationMiddleware({
95+
deprecationDate: '2023-01-01',
96+
alternativeUrl
97+
});
98+
99+
mw(req, res, next);
100+
101+
const linkHeader = headers['Link'];
102+
expect(Array.isArray(linkHeader)).toBe(true);
103+
expect(linkHeader).toContain(existingLink);
104+
expect(linkHeader).toContain(`<${alternativeUrl}>; rel="successor-version"`);
105+
});
106+
107+
it('logs a warning with request details', () => {
108+
const req = makeReq({ method: 'POST', originalUrl: '/api/v1/old-endpoint' });
109+
const { res } = makeRes();
110+
const deprecationDate = '2023-06-01';
111+
const mw = deprecationMiddleware({ deprecationDate });
112+
113+
mw(req, res, next);
114+
115+
expect(console.warn).toHaveBeenCalledWith(
116+
expect.stringContaining('[DEPRECATION WARNING]')
117+
);
118+
expect(console.warn).toHaveBeenCalledWith(
119+
expect.stringContaining('POST')
120+
);
121+
expect(console.warn).toHaveBeenCalledWith(
122+
expect.stringContaining('/api/v1/old-endpoint')
123+
);
124+
expect(console.warn).toHaveBeenCalledWith(
125+
expect.stringContaining(deprecationDate)
126+
);
127+
});
128+
});
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { Request, Response, NextFunction } from 'express';
2+
3+
/**
4+
* Options for the deprecation middleware.
5+
*/
6+
interface DeprecationOptions {
7+
/**
8+
* The date when the endpoint was deprecated (ISO 8601 format, e.g., '2023-12-31').
9+
*/
10+
deprecationDate: string;
11+
/**
12+
* The date when the endpoint will be removed (ISO 8601 format, e.g., '2024-06-30').
13+
*/
14+
sunsetDate?: string;
15+
/**
16+
* URL to the new endpoint or documentation.
17+
*/
18+
alternativeUrl?: string;
19+
}
20+
21+
/**
22+
* Middleware to add deprecation headers to a response.
23+
* Follows the draft-ietf-httpapi-deprecation-header and draft-ietf-httpapi-sunset-header.
24+
*
25+
* @param options DeprecationOptions
26+
* @returns Express Middleware
27+
*/
28+
export const deprecationMiddleware = (options: DeprecationOptions) => {
29+
return (req: Request, res: Response, next: NextFunction) => {
30+
// 1. Add Deprecation header
31+
// The Deprecation header indicates that the resource is deprecated.
32+
// It can also include the date when the deprecation started.
33+
res.setHeader('Deprecation', new Date(options.deprecationDate).toUTCString());
34+
35+
// 2. Add Sunset header (if provided)
36+
// The Sunset header indicates when the resource will become unavailable.
37+
if (options.sunsetDate) {
38+
res.setHeader('Sunset', new Date(options.sunsetDate).toUTCString());
39+
}
40+
41+
// 3. Include alternative info (Link header)
42+
// The Link header with rel="successor-version" can point to a newer version.
43+
if (options.alternativeUrl) {
44+
// If there are existing Link headers, we should append to them.
45+
const existingLink = res.getHeader('Link');
46+
const newLink = `<${options.alternativeUrl}>; rel="successor-version"`;
47+
48+
if (existingLink) {
49+
if (Array.isArray(existingLink)) {
50+
res.setHeader('Link', [...existingLink, newLink]);
51+
} else {
52+
res.setHeader('Link', [`${existingLink}`, newLink]);
53+
}
54+
} else {
55+
res.setHeader('Link', newLink);
56+
}
57+
}
58+
59+
// 4. Log deprecation
60+
// This helps server operators identify usage of deprecated endpoints.
61+
console.warn(`[DEPRECATION WARNING] Client ${req.ip} accessed deprecated endpoint ${req.method} ${req.originalUrl}. Deprecated since: ${options.deprecationDate}.`);
62+
63+
next();
64+
};
65+
};

backend/src/routes/legacy.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { Router, Request, Response } from 'express';
2+
import { deprecationMiddleware } from '../middleware/deprecation.js';
3+
4+
export const legacyRouter = Router();
5+
6+
/**
7+
* @openapi
8+
* /api/v1/legacy-data:
9+
* get:
10+
* summary: Get legacy data (Deprecated)
11+
* responses:
12+
* 200:
13+
* description: Returns legacy data with deprecation headers
14+
*/
15+
legacyRouter.get(
16+
'/legacy-data',
17+
deprecationMiddleware({
18+
deprecationDate: '2023-10-01',
19+
sunsetDate: '2024-12-31',
20+
alternativeUrl: 'https://agenticpay.io/docs/api/v2/data'
21+
}),
22+
(req: Request, res: Response) => {
23+
res.json({
24+
message: 'This is legacy data. Please migrate to the new API.',
25+
data: [1, 2, 3, 4, 5]
26+
});
27+
}
28+
);

0 commit comments

Comments
 (0)