@vereign/core 1.0.0 → 1.2.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 +2 -1
- package/dist/index.d.mts +3 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +241 -28
- package/dist/index.mjs +243 -27
- package/package.json +8 -3
package/README.md
CHANGED
package/dist/index.d.mts
CHANGED
|
@@ -8,6 +8,9 @@ declare class App {
|
|
|
8
8
|
constructor(logger: Logger, appVersion: string);
|
|
9
9
|
private initializeMiddlewares;
|
|
10
10
|
private initializeRoutes;
|
|
11
|
+
/**
|
|
12
|
+
* General error handler for all errors, so we can throw from anywhere in the project
|
|
13
|
+
*/
|
|
11
14
|
private initializeErrorHandling;
|
|
12
15
|
listen(port: number): void;
|
|
13
16
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -8,6 +8,9 @@ declare class App {
|
|
|
8
8
|
constructor(logger: Logger, appVersion: string);
|
|
9
9
|
private initializeMiddlewares;
|
|
10
10
|
private initializeRoutes;
|
|
11
|
+
/**
|
|
12
|
+
* General error handler for all errors, so we can throw from anywhere in the project
|
|
13
|
+
*/
|
|
11
14
|
private initializeErrorHandling;
|
|
12
15
|
listen(port: number): void;
|
|
13
16
|
}
|
package/dist/index.js
CHANGED
|
@@ -3,8 +3,22 @@ var __create = Object.create;
|
|
|
3
3
|
var __defProp = Object.defineProperty;
|
|
4
4
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
5
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getOwnPropSymbols = Object.getOwnPropertySymbols;
|
|
6
7
|
var __getProtoOf = Object.getPrototypeOf;
|
|
7
8
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __propIsEnum = Object.prototype.propertyIsEnumerable;
|
|
10
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
11
|
+
var __spreadValues = (a, b) => {
|
|
12
|
+
for (var prop in b || (b = {}))
|
|
13
|
+
if (__hasOwnProp.call(b, prop))
|
|
14
|
+
__defNormalProp(a, prop, b[prop]);
|
|
15
|
+
if (__getOwnPropSymbols)
|
|
16
|
+
for (var prop of __getOwnPropSymbols(b)) {
|
|
17
|
+
if (__propIsEnum.call(b, prop))
|
|
18
|
+
__defNormalProp(a, prop, b[prop]);
|
|
19
|
+
}
|
|
20
|
+
return a;
|
|
21
|
+
};
|
|
8
22
|
var __export = (target, all) => {
|
|
9
23
|
for (var name in all)
|
|
10
24
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
@@ -26,6 +40,26 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
26
40
|
mod
|
|
27
41
|
));
|
|
28
42
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
43
|
+
var __async = (__this, __arguments, generator) => {
|
|
44
|
+
return new Promise((resolve, reject) => {
|
|
45
|
+
var fulfilled = (value) => {
|
|
46
|
+
try {
|
|
47
|
+
step(generator.next(value));
|
|
48
|
+
} catch (e) {
|
|
49
|
+
reject(e);
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
var rejected = (value) => {
|
|
53
|
+
try {
|
|
54
|
+
step(generator.throw(value));
|
|
55
|
+
} catch (e) {
|
|
56
|
+
reject(e);
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
var step = (x) => x.done ? resolve(x.value) : Promise.resolve(x.value).then(fulfilled, rejected);
|
|
60
|
+
step((generator = generator.apply(__this, __arguments)).next());
|
|
61
|
+
});
|
|
62
|
+
};
|
|
29
63
|
|
|
30
64
|
// src/index.ts
|
|
31
65
|
var index_exports = {};
|
|
@@ -43,7 +77,7 @@ var HealthController = class {
|
|
|
43
77
|
constructor(logger, appVersion) {
|
|
44
78
|
this.check = (_, res) => {
|
|
45
79
|
this.logger.info("Health check endpoint called");
|
|
46
|
-
res.
|
|
80
|
+
res.json({ status: "ok", appVersion: this.appVersion });
|
|
47
81
|
};
|
|
48
82
|
this.logger = logger;
|
|
49
83
|
this.appVersion = appVersion;
|
|
@@ -63,40 +97,175 @@ var HealthRouter = class {
|
|
|
63
97
|
// src/chmed16/router.ts
|
|
64
98
|
var import_express2 = __toESM(require("express"));
|
|
65
99
|
|
|
100
|
+
// src/chmed16/controller.ts
|
|
101
|
+
var import_chmed_parser = require("@vereign/chmed-parser");
|
|
102
|
+
var import_pdf_generator = require("@vereign/pdf-generator");
|
|
103
|
+
|
|
104
|
+
// src/app-error.ts
|
|
105
|
+
var z = __toESM(require("zod/v4"));
|
|
106
|
+
var AppError = class extends Error {
|
|
107
|
+
constructor(status, message, code, details) {
|
|
108
|
+
super(message);
|
|
109
|
+
this.status = status;
|
|
110
|
+
this.code = code;
|
|
111
|
+
this.details = details;
|
|
112
|
+
Error.captureStackTrace(this, this.constructor);
|
|
113
|
+
}
|
|
114
|
+
toJSON() {
|
|
115
|
+
return __spreadValues({
|
|
116
|
+
status: this.status,
|
|
117
|
+
code: this.code,
|
|
118
|
+
message: this.message
|
|
119
|
+
}, this.details ? { errors: this.details } : {});
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
var errorDetailsSchema = z.object({
|
|
123
|
+
path: z.string(),
|
|
124
|
+
message: z.string(),
|
|
125
|
+
code: z.string().optional()
|
|
126
|
+
});
|
|
127
|
+
var appErrorSchema = z.object({
|
|
128
|
+
status: z.number().int(),
|
|
129
|
+
code: z.string(),
|
|
130
|
+
message: z.string(),
|
|
131
|
+
errors: z.array(errorDetailsSchema).optional()
|
|
132
|
+
});
|
|
133
|
+
|
|
66
134
|
// src/chmed16/controller.ts
|
|
67
135
|
var CHMED16AController = class {
|
|
68
136
|
constructor(logger) {
|
|
69
|
-
this.process = (req, res) => {
|
|
70
|
-
const { chmed } = req.body;
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
137
|
+
this.process = (req, res, next) => __async(this, null, function* () {
|
|
138
|
+
const { chmed, lang } = req.body;
|
|
139
|
+
const start = Date.now();
|
|
140
|
+
try {
|
|
141
|
+
this.logger.info("Processing started");
|
|
142
|
+
const json = yield this.parseChmedData(chmed, lang);
|
|
143
|
+
yield this.generatePdfResponse(json, res);
|
|
144
|
+
this.logger.info(`Total request time: ${Date.now() - start} ms`);
|
|
145
|
+
} catch (error) {
|
|
146
|
+
next(error);
|
|
147
|
+
}
|
|
148
|
+
});
|
|
75
149
|
this.logger = logger;
|
|
150
|
+
this.sds = new import_chmed_parser.SDS("https://sds-test.hin.ch", logger);
|
|
151
|
+
this.parser = new import_chmed_parser.CHMEDParser(logger);
|
|
152
|
+
}
|
|
153
|
+
parseChmedData(chmed, lang) {
|
|
154
|
+
return __async(this, null, function* () {
|
|
155
|
+
try {
|
|
156
|
+
const parseStart = Date.now();
|
|
157
|
+
const json = yield this.parser.parse(chmed, this.sds, lang);
|
|
158
|
+
this.logger.info(`Parsing took ${Date.now() - parseStart} ms`);
|
|
159
|
+
return json;
|
|
160
|
+
} catch (error) {
|
|
161
|
+
this.logger.error("Parsing failed:", error);
|
|
162
|
+
throw new AppError(
|
|
163
|
+
400,
|
|
164
|
+
"Failed to parse CHMED data. Please check the input format.",
|
|
165
|
+
"PARSE_ERROR",
|
|
166
|
+
[
|
|
167
|
+
{
|
|
168
|
+
path: "chmed",
|
|
169
|
+
message: "Invalid CHMED format or structure",
|
|
170
|
+
code: "invalid_format"
|
|
171
|
+
}
|
|
172
|
+
]
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
generatePdfResponse(json, res) {
|
|
178
|
+
return __async(this, null, function* () {
|
|
179
|
+
const pdfStart = Date.now();
|
|
180
|
+
try {
|
|
181
|
+
const pdf = new import_pdf_generator.PDFLib(this.logger);
|
|
182
|
+
const pdfBuffer = yield pdf.generateBuffer(json);
|
|
183
|
+
res.setHeader("Content-Type", "application/pdf");
|
|
184
|
+
res.setHeader("Content-Disposition", 'inline; filename="chmed16a.pdf"');
|
|
185
|
+
res.send(pdfBuffer);
|
|
186
|
+
this.logger.info(`PDF generation took ${Date.now() - pdfStart} ms`);
|
|
187
|
+
} catch (error) {
|
|
188
|
+
this.logger.error("PDF generation failed:", error);
|
|
189
|
+
throw new AppError(
|
|
190
|
+
500,
|
|
191
|
+
"Failed to generate PDF. The parsed data may be invalid.",
|
|
192
|
+
"PDF_GENERATION_ERROR",
|
|
193
|
+
[
|
|
194
|
+
{
|
|
195
|
+
path: "pdf_generation",
|
|
196
|
+
message: "PDF creation failed due to invalid or corrupted data",
|
|
197
|
+
code: "generation_failed"
|
|
198
|
+
}
|
|
199
|
+
]
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
});
|
|
76
203
|
}
|
|
77
204
|
};
|
|
78
205
|
|
|
79
206
|
// src/chmed16/middleware.ts
|
|
80
|
-
var
|
|
207
|
+
var import_zod = require("zod");
|
|
81
208
|
|
|
82
209
|
// src/chmed16/request.ts
|
|
83
|
-
var
|
|
84
|
-
var
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
210
|
+
var z2 = __toESM(require("zod/v4"));
|
|
211
|
+
var chmedPattern = /^CHMED[0-9A-Za-z]+/;
|
|
212
|
+
var bodySchema = z2.object({
|
|
213
|
+
chmed: z2.string().min(5, "string is required").refine(
|
|
214
|
+
(val) => {
|
|
215
|
+
if (chmedPattern.test(val)) return true;
|
|
216
|
+
try {
|
|
217
|
+
const url = new URL(val);
|
|
218
|
+
const hash = url.hash.slice(1);
|
|
219
|
+
return chmedPattern.test(hash);
|
|
220
|
+
} catch (e) {
|
|
221
|
+
return false;
|
|
222
|
+
}
|
|
223
|
+
},
|
|
224
|
+
{ message: "chmed must be a valid CHMED string" }
|
|
225
|
+
).meta({
|
|
226
|
+
description: "A valid CHMED string",
|
|
227
|
+
example: "CHMED16A1H4sIAAAAAAAACr2WX2/iOBDAv4rl1wtZ/yEh8LTlaHuVll1Eu620qz4YGIiV4CDHqbbb49v0M9zTvfHFbhwaWvpvV6eqgIMzGTv+zYzHc0NHymkwjvZu6NFntQTao4PCKhrQT3e3x1ZNwKKgP0A1yrvtqMV5S0gUHYOZ4bOeCOipswBeoQ/WlM6qsgTidb7pFUolYwxv/tTu+k7Hv8Is8GYG2B2lhfFvY50uSZIEG/5QfjhUOvdKuKhwUS/l4wR0udKQh9MUNU5mJe19v6Fn1yuoV3Ku/AAeyjAOedjGJriMO6FgHC9hFCby7+HB8fBwwBin68uADmHmDTD4VLohGBzsF2RhSXssoONpPf0JqrTXwbYToRilMY8uG5HYiqJOeyfid6JuZyeSTSdu1PnuWWe7FPUQhzc4XW/Kr0ajhYUf8Jg2jnbP+fpyva6h9BRdaNx2Qv8KKqSUIo5rs22nkIhabDUG7sgWCE0FE6zFui3hJx24s2JrkhMzhrK2yQD1WcgCjs3/Y7v0q98uAGMhQ/WzbDEuvTVPi9nEgjFo2oAerFYnPj5QfqYmOTgHBK5cHhJtSFakOZhyc1tZmFvQsCQXPpAsUdU83/xTggkJTjIuFI4fffFdWNXWPK0mfm2NMSlnIolrqzSk7f9L+oDyF6QDWFkoS108Ih0WdoFcpDJqkm5uzUIvCG4cMlRp/hO0Q6x9qic0URRLlryF336fpp9XLi2m6cxW0+zDX2B/7lN9fZmGPIsT0M+TkZpmD/YBJoZIdCL5lmT8l2QnZg6Ze+KmV4CeCTrZcPEdjOi2u51Evq+bRspm2pSFIR8IzuByjMAcFvto54VFpCU5naa5moMJfj8Wa1j+3A6LuiwRb5VLmsZfhR1WZQb5FdhypYyp8ADZ32jqh16qnEjyg7jN7SLX05SAxtyTLl/YZPw+KsWOLZEJa0dv5cemve7Hc4RyxWr+BKoPmvRhpuw8JDwiLSIZGWpToavI1Z1fDzFL4rZr+Dm5wI0LRKHH3ebfafaiER7lz3tj3DtaRDxm3fh9t6hPNza9dumyKh/5WLuHyHKLakiuzILwFmthC8hMGYW+v4BpWkLuz4/6GX6fDW/xNL7buJfbbfa+2C+dlZzsTkuOLq2w7tr3/b2D6et4+OrREVZPddnkbzAlwNLrozYWDTvU2ghYttWf1jOX5oPjDvCkQO1OzDgWVTIWss6C30q/duzUpSOaivvSkYkzHvek6MnkD8Z79QTjJYYc7WsP6LPRXV2KNYHKcyC+mMnqYgaMB1UelCwqR8Bm+eYWTebdisVQ39eXdP0fL5lXGtoKAAA"
|
|
228
|
+
}),
|
|
229
|
+
lang: z2.enum(["en", "de", "fr", "it"]).optional().meta({
|
|
230
|
+
description: "Language code",
|
|
231
|
+
example: "en"
|
|
232
|
+
})
|
|
233
|
+
}).meta({ description: "Request body for CHMED endpoint" });
|
|
88
234
|
|
|
89
235
|
// src/chmed16/middleware.ts
|
|
90
|
-
var validateBody = (
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
236
|
+
var validateBody = (logger) => {
|
|
237
|
+
return (req, res, next) => {
|
|
238
|
+
try {
|
|
239
|
+
req.body = bodySchema.parse(req.body);
|
|
240
|
+
next();
|
|
241
|
+
} catch (e) {
|
|
242
|
+
logger.debug("Body validation failed", { error: e });
|
|
243
|
+
if (e instanceof import_zod.ZodError) {
|
|
244
|
+
const details = e.issues.map((err) => ({
|
|
245
|
+
path: err.path.join("."),
|
|
246
|
+
message: err.message,
|
|
247
|
+
code: err.code
|
|
248
|
+
}));
|
|
249
|
+
logger.warn("Validation failed", { details });
|
|
250
|
+
return res.status(400).json(
|
|
251
|
+
new AppError(
|
|
252
|
+
400,
|
|
253
|
+
"Invalid request body",
|
|
254
|
+
"BAD_REQUEST",
|
|
255
|
+
details
|
|
256
|
+
).toJSON()
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
logger.error("Unexpected error during body validation", { error: e });
|
|
260
|
+
return res.status(500).json(
|
|
261
|
+
new AppError(
|
|
262
|
+
500,
|
|
263
|
+
"Internal Server Error",
|
|
264
|
+
"INTERNAL_SERVER_ERROR"
|
|
265
|
+
).toJSON()
|
|
266
|
+
);
|
|
97
267
|
}
|
|
98
|
-
|
|
99
|
-
}
|
|
268
|
+
};
|
|
100
269
|
};
|
|
101
270
|
|
|
102
271
|
// src/chmed16/router.ts
|
|
@@ -104,12 +273,17 @@ var CHMED16ARouter = class {
|
|
|
104
273
|
static getRouter(logger) {
|
|
105
274
|
const router = import_express2.default.Router();
|
|
106
275
|
const controller = new CHMED16AController(logger);
|
|
107
|
-
router.post(
|
|
276
|
+
router.post(
|
|
277
|
+
"/chmed",
|
|
278
|
+
validateBody(logger),
|
|
279
|
+
controller.process.bind(controller)
|
|
280
|
+
);
|
|
108
281
|
return router;
|
|
109
282
|
}
|
|
110
283
|
};
|
|
111
284
|
|
|
112
285
|
// src/index.ts
|
|
286
|
+
var import_zod2 = require("zod");
|
|
113
287
|
var App = class {
|
|
114
288
|
constructor(logger, appVersion) {
|
|
115
289
|
this.logger = logger;
|
|
@@ -127,14 +301,53 @@ var App = class {
|
|
|
127
301
|
});
|
|
128
302
|
}
|
|
129
303
|
initializeRoutes() {
|
|
130
|
-
this.app.use("/
|
|
304
|
+
this.app.use("/", HealthRouter.getRouter(this.logger, this.appVersion));
|
|
131
305
|
this.app.use("/v1", CHMED16ARouter.getRouter(this.logger));
|
|
132
306
|
}
|
|
307
|
+
/**
|
|
308
|
+
* General error handler for all errors, so we can throw from anywhere in the project
|
|
309
|
+
*/
|
|
133
310
|
initializeErrorHandling() {
|
|
134
|
-
this.app.use(
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
311
|
+
this.app.use(
|
|
312
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
313
|
+
(e, req, res, next) => {
|
|
314
|
+
this.logger.debug("General Error Handler");
|
|
315
|
+
this.logger.debug(e);
|
|
316
|
+
if (e instanceof import_zod2.ZodError) {
|
|
317
|
+
const details = e.issues.map((err) => ({
|
|
318
|
+
path: err.path.join("."),
|
|
319
|
+
message: err.message,
|
|
320
|
+
code: err.code
|
|
321
|
+
}));
|
|
322
|
+
return res.status(400).json(
|
|
323
|
+
new AppError(
|
|
324
|
+
400,
|
|
325
|
+
"Invalid request body",
|
|
326
|
+
"BAD_REQUEST",
|
|
327
|
+
details
|
|
328
|
+
).toJSON()
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
if (e instanceof AppError) {
|
|
332
|
+
if (e.code === "PARSE_ERROR") {
|
|
333
|
+
this.logger.warn(`Parse error: ${e.message}`);
|
|
334
|
+
} else if (e.code === "PDF_GENERATION_ERROR") {
|
|
335
|
+
this.logger.error(`PDF generation error: ${e.message}`);
|
|
336
|
+
} else {
|
|
337
|
+
this.logger.error(`App error: ${e.message}`);
|
|
338
|
+
}
|
|
339
|
+
return res.status(e.status).json(e.toJSON());
|
|
340
|
+
}
|
|
341
|
+
this.logger.error("Unexpected error:", e);
|
|
342
|
+
return res.status(500).json(
|
|
343
|
+
new AppError(
|
|
344
|
+
500,
|
|
345
|
+
"Internal Server Error",
|
|
346
|
+
"INTERNAL_SERVER_ERROR"
|
|
347
|
+
).toJSON()
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
);
|
|
138
351
|
}
|
|
139
352
|
listen(port) {
|
|
140
353
|
this.app.listen(port, () => {
|
package/dist/index.mjs
CHANGED
|
@@ -1,3 +1,40 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __getOwnPropSymbols = Object.getOwnPropertySymbols;
|
|
3
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
4
|
+
var __propIsEnum = Object.prototype.propertyIsEnumerable;
|
|
5
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
6
|
+
var __spreadValues = (a, b) => {
|
|
7
|
+
for (var prop in b || (b = {}))
|
|
8
|
+
if (__hasOwnProp.call(b, prop))
|
|
9
|
+
__defNormalProp(a, prop, b[prop]);
|
|
10
|
+
if (__getOwnPropSymbols)
|
|
11
|
+
for (var prop of __getOwnPropSymbols(b)) {
|
|
12
|
+
if (__propIsEnum.call(b, prop))
|
|
13
|
+
__defNormalProp(a, prop, b[prop]);
|
|
14
|
+
}
|
|
15
|
+
return a;
|
|
16
|
+
};
|
|
17
|
+
var __async = (__this, __arguments, generator) => {
|
|
18
|
+
return new Promise((resolve, reject) => {
|
|
19
|
+
var fulfilled = (value) => {
|
|
20
|
+
try {
|
|
21
|
+
step(generator.next(value));
|
|
22
|
+
} catch (e) {
|
|
23
|
+
reject(e);
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
var rejected = (value) => {
|
|
27
|
+
try {
|
|
28
|
+
step(generator.throw(value));
|
|
29
|
+
} catch (e) {
|
|
30
|
+
reject(e);
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
var step = (x) => x.done ? resolve(x.value) : Promise.resolve(x.value).then(fulfilled, rejected);
|
|
34
|
+
step((generator = generator.apply(__this, __arguments)).next());
|
|
35
|
+
});
|
|
36
|
+
};
|
|
37
|
+
|
|
1
38
|
// src/index.ts
|
|
2
39
|
import express3 from "express";
|
|
3
40
|
|
|
@@ -9,7 +46,7 @@ var HealthController = class {
|
|
|
9
46
|
constructor(logger, appVersion) {
|
|
10
47
|
this.check = (_, res) => {
|
|
11
48
|
this.logger.info("Health check endpoint called");
|
|
12
|
-
res.
|
|
49
|
+
res.json({ status: "ok", appVersion: this.appVersion });
|
|
13
50
|
};
|
|
14
51
|
this.logger = logger;
|
|
15
52
|
this.appVersion = appVersion;
|
|
@@ -29,16 +66,109 @@ var HealthRouter = class {
|
|
|
29
66
|
// src/chmed16/router.ts
|
|
30
67
|
import express2 from "express";
|
|
31
68
|
|
|
69
|
+
// src/chmed16/controller.ts
|
|
70
|
+
import { CHMEDParser, SDS } from "@vereign/chmed-parser";
|
|
71
|
+
import { PDFLib } from "@vereign/pdf-generator";
|
|
72
|
+
|
|
73
|
+
// src/app-error.ts
|
|
74
|
+
import * as z from "zod/v4";
|
|
75
|
+
var AppError = class extends Error {
|
|
76
|
+
constructor(status, message, code, details) {
|
|
77
|
+
super(message);
|
|
78
|
+
this.status = status;
|
|
79
|
+
this.code = code;
|
|
80
|
+
this.details = details;
|
|
81
|
+
Error.captureStackTrace(this, this.constructor);
|
|
82
|
+
}
|
|
83
|
+
toJSON() {
|
|
84
|
+
return __spreadValues({
|
|
85
|
+
status: this.status,
|
|
86
|
+
code: this.code,
|
|
87
|
+
message: this.message
|
|
88
|
+
}, this.details ? { errors: this.details } : {});
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
var errorDetailsSchema = z.object({
|
|
92
|
+
path: z.string(),
|
|
93
|
+
message: z.string(),
|
|
94
|
+
code: z.string().optional()
|
|
95
|
+
});
|
|
96
|
+
var appErrorSchema = z.object({
|
|
97
|
+
status: z.number().int(),
|
|
98
|
+
code: z.string(),
|
|
99
|
+
message: z.string(),
|
|
100
|
+
errors: z.array(errorDetailsSchema).optional()
|
|
101
|
+
});
|
|
102
|
+
|
|
32
103
|
// src/chmed16/controller.ts
|
|
33
104
|
var CHMED16AController = class {
|
|
34
105
|
constructor(logger) {
|
|
35
|
-
this.process = (req, res) => {
|
|
36
|
-
const { chmed } = req.body;
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
106
|
+
this.process = (req, res, next) => __async(this, null, function* () {
|
|
107
|
+
const { chmed, lang } = req.body;
|
|
108
|
+
const start = Date.now();
|
|
109
|
+
try {
|
|
110
|
+
this.logger.info("Processing started");
|
|
111
|
+
const json = yield this.parseChmedData(chmed, lang);
|
|
112
|
+
yield this.generatePdfResponse(json, res);
|
|
113
|
+
this.logger.info(`Total request time: ${Date.now() - start} ms`);
|
|
114
|
+
} catch (error) {
|
|
115
|
+
next(error);
|
|
116
|
+
}
|
|
117
|
+
});
|
|
41
118
|
this.logger = logger;
|
|
119
|
+
this.sds = new SDS("https://sds-test.hin.ch", logger);
|
|
120
|
+
this.parser = new CHMEDParser(logger);
|
|
121
|
+
}
|
|
122
|
+
parseChmedData(chmed, lang) {
|
|
123
|
+
return __async(this, null, function* () {
|
|
124
|
+
try {
|
|
125
|
+
const parseStart = Date.now();
|
|
126
|
+
const json = yield this.parser.parse(chmed, this.sds, lang);
|
|
127
|
+
this.logger.info(`Parsing took ${Date.now() - parseStart} ms`);
|
|
128
|
+
return json;
|
|
129
|
+
} catch (error) {
|
|
130
|
+
this.logger.error("Parsing failed:", error);
|
|
131
|
+
throw new AppError(
|
|
132
|
+
400,
|
|
133
|
+
"Failed to parse CHMED data. Please check the input format.",
|
|
134
|
+
"PARSE_ERROR",
|
|
135
|
+
[
|
|
136
|
+
{
|
|
137
|
+
path: "chmed",
|
|
138
|
+
message: "Invalid CHMED format or structure",
|
|
139
|
+
code: "invalid_format"
|
|
140
|
+
}
|
|
141
|
+
]
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
generatePdfResponse(json, res) {
|
|
147
|
+
return __async(this, null, function* () {
|
|
148
|
+
const pdfStart = Date.now();
|
|
149
|
+
try {
|
|
150
|
+
const pdf = new PDFLib(this.logger);
|
|
151
|
+
const pdfBuffer = yield pdf.generateBuffer(json);
|
|
152
|
+
res.setHeader("Content-Type", "application/pdf");
|
|
153
|
+
res.setHeader("Content-Disposition", 'inline; filename="chmed16a.pdf"');
|
|
154
|
+
res.send(pdfBuffer);
|
|
155
|
+
this.logger.info(`PDF generation took ${Date.now() - pdfStart} ms`);
|
|
156
|
+
} catch (error) {
|
|
157
|
+
this.logger.error("PDF generation failed:", error);
|
|
158
|
+
throw new AppError(
|
|
159
|
+
500,
|
|
160
|
+
"Failed to generate PDF. The parsed data may be invalid.",
|
|
161
|
+
"PDF_GENERATION_ERROR",
|
|
162
|
+
[
|
|
163
|
+
{
|
|
164
|
+
path: "pdf_generation",
|
|
165
|
+
message: "PDF creation failed due to invalid or corrupted data",
|
|
166
|
+
code: "generation_failed"
|
|
167
|
+
}
|
|
168
|
+
]
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
});
|
|
42
172
|
}
|
|
43
173
|
};
|
|
44
174
|
|
|
@@ -46,23 +176,65 @@ var CHMED16AController = class {
|
|
|
46
176
|
import { ZodError } from "zod";
|
|
47
177
|
|
|
48
178
|
// src/chmed16/request.ts
|
|
49
|
-
import
|
|
50
|
-
var
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
179
|
+
import * as z2 from "zod/v4";
|
|
180
|
+
var chmedPattern = /^CHMED[0-9A-Za-z]+/;
|
|
181
|
+
var bodySchema = z2.object({
|
|
182
|
+
chmed: z2.string().min(5, "string is required").refine(
|
|
183
|
+
(val) => {
|
|
184
|
+
if (chmedPattern.test(val)) return true;
|
|
185
|
+
try {
|
|
186
|
+
const url = new URL(val);
|
|
187
|
+
const hash = url.hash.slice(1);
|
|
188
|
+
return chmedPattern.test(hash);
|
|
189
|
+
} catch (e) {
|
|
190
|
+
return false;
|
|
191
|
+
}
|
|
192
|
+
},
|
|
193
|
+
{ message: "chmed must be a valid CHMED string" }
|
|
194
|
+
).meta({
|
|
195
|
+
description: "A valid CHMED string",
|
|
196
|
+
example: "CHMED16A1H4sIAAAAAAAACr2WX2/iOBDAv4rl1wtZ/yEh8LTlaHuVll1Eu620qz4YGIiV4CDHqbbb49v0M9zTvfHFbhwaWvpvV6eqgIMzGTv+zYzHc0NHymkwjvZu6NFntQTao4PCKhrQT3e3x1ZNwKKgP0A1yrvtqMV5S0gUHYOZ4bOeCOipswBeoQ/WlM6qsgTidb7pFUolYwxv/tTu+k7Hv8Is8GYG2B2lhfFvY50uSZIEG/5QfjhUOvdKuKhwUS/l4wR0udKQh9MUNU5mJe19v6Fn1yuoV3Ku/AAeyjAOedjGJriMO6FgHC9hFCby7+HB8fBwwBin68uADmHmDTD4VLohGBzsF2RhSXssoONpPf0JqrTXwbYToRilMY8uG5HYiqJOeyfid6JuZyeSTSdu1PnuWWe7FPUQhzc4XW/Kr0ajhYUf8Jg2jnbP+fpyva6h9BRdaNx2Qv8KKqSUIo5rs22nkIhabDUG7sgWCE0FE6zFui3hJx24s2JrkhMzhrK2yQD1WcgCjs3/Y7v0q98uAGMhQ/WzbDEuvTVPi9nEgjFo2oAerFYnPj5QfqYmOTgHBK5cHhJtSFakOZhyc1tZmFvQsCQXPpAsUdU83/xTggkJTjIuFI4fffFdWNXWPK0mfm2NMSlnIolrqzSk7f9L+oDyF6QDWFkoS108Ih0WdoFcpDJqkm5uzUIvCG4cMlRp/hO0Q6x9qic0URRLlryF336fpp9XLi2m6cxW0+zDX2B/7lN9fZmGPIsT0M+TkZpmD/YBJoZIdCL5lmT8l2QnZg6Ze+KmV4CeCTrZcPEdjOi2u51Evq+bRspm2pSFIR8IzuByjMAcFvto54VFpCU5naa5moMJfj8Wa1j+3A6LuiwRb5VLmsZfhR1WZQb5FdhypYyp8ADZ32jqh16qnEjyg7jN7SLX05SAxtyTLl/YZPw+KsWOLZEJa0dv5cemve7Hc4RyxWr+BKoPmvRhpuw8JDwiLSIZGWpToavI1Z1fDzFL4rZr+Dm5wI0LRKHH3ebfafaiER7lz3tj3DtaRDxm3fh9t6hPNza9dumyKh/5WLuHyHKLakiuzILwFmthC8hMGYW+v4BpWkLuz4/6GX6fDW/xNL7buJfbbfa+2C+dlZzsTkuOLq2w7tr3/b2D6et4+OrREVZPddnkbzAlwNLrozYWDTvU2ghYttWf1jOX5oPjDvCkQO1OzDgWVTIWss6C30q/duzUpSOaivvSkYkzHvek6MnkD8Z79QTjJYYc7WsP6LPRXV2KNYHKcyC+mMnqYgaMB1UelCwqR8Bm+eYWTebdisVQ39eXdP0fL5lXGtoKAAA"
|
|
197
|
+
}),
|
|
198
|
+
lang: z2.enum(["en", "de", "fr", "it"]).optional().meta({
|
|
199
|
+
description: "Language code",
|
|
200
|
+
example: "en"
|
|
201
|
+
})
|
|
202
|
+
}).meta({ description: "Request body for CHMED endpoint" });
|
|
54
203
|
|
|
55
204
|
// src/chmed16/middleware.ts
|
|
56
|
-
var validateBody = (
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
205
|
+
var validateBody = (logger) => {
|
|
206
|
+
return (req, res, next) => {
|
|
207
|
+
try {
|
|
208
|
+
req.body = bodySchema.parse(req.body);
|
|
209
|
+
next();
|
|
210
|
+
} catch (e) {
|
|
211
|
+
logger.debug("Body validation failed", { error: e });
|
|
212
|
+
if (e instanceof ZodError) {
|
|
213
|
+
const details = e.issues.map((err) => ({
|
|
214
|
+
path: err.path.join("."),
|
|
215
|
+
message: err.message,
|
|
216
|
+
code: err.code
|
|
217
|
+
}));
|
|
218
|
+
logger.warn("Validation failed", { details });
|
|
219
|
+
return res.status(400).json(
|
|
220
|
+
new AppError(
|
|
221
|
+
400,
|
|
222
|
+
"Invalid request body",
|
|
223
|
+
"BAD_REQUEST",
|
|
224
|
+
details
|
|
225
|
+
).toJSON()
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
logger.error("Unexpected error during body validation", { error: e });
|
|
229
|
+
return res.status(500).json(
|
|
230
|
+
new AppError(
|
|
231
|
+
500,
|
|
232
|
+
"Internal Server Error",
|
|
233
|
+
"INTERNAL_SERVER_ERROR"
|
|
234
|
+
).toJSON()
|
|
235
|
+
);
|
|
63
236
|
}
|
|
64
|
-
|
|
65
|
-
}
|
|
237
|
+
};
|
|
66
238
|
};
|
|
67
239
|
|
|
68
240
|
// src/chmed16/router.ts
|
|
@@ -70,12 +242,17 @@ var CHMED16ARouter = class {
|
|
|
70
242
|
static getRouter(logger) {
|
|
71
243
|
const router = express2.Router();
|
|
72
244
|
const controller = new CHMED16AController(logger);
|
|
73
|
-
router.post(
|
|
245
|
+
router.post(
|
|
246
|
+
"/chmed",
|
|
247
|
+
validateBody(logger),
|
|
248
|
+
controller.process.bind(controller)
|
|
249
|
+
);
|
|
74
250
|
return router;
|
|
75
251
|
}
|
|
76
252
|
};
|
|
77
253
|
|
|
78
254
|
// src/index.ts
|
|
255
|
+
import { ZodError as ZodError2 } from "zod";
|
|
79
256
|
var App = class {
|
|
80
257
|
constructor(logger, appVersion) {
|
|
81
258
|
this.logger = logger;
|
|
@@ -93,14 +270,53 @@ var App = class {
|
|
|
93
270
|
});
|
|
94
271
|
}
|
|
95
272
|
initializeRoutes() {
|
|
96
|
-
this.app.use("/
|
|
273
|
+
this.app.use("/", HealthRouter.getRouter(this.logger, this.appVersion));
|
|
97
274
|
this.app.use("/v1", CHMED16ARouter.getRouter(this.logger));
|
|
98
275
|
}
|
|
276
|
+
/**
|
|
277
|
+
* General error handler for all errors, so we can throw from anywhere in the project
|
|
278
|
+
*/
|
|
99
279
|
initializeErrorHandling() {
|
|
100
|
-
this.app.use(
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
280
|
+
this.app.use(
|
|
281
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
282
|
+
(e, req, res, next) => {
|
|
283
|
+
this.logger.debug("General Error Handler");
|
|
284
|
+
this.logger.debug(e);
|
|
285
|
+
if (e instanceof ZodError2) {
|
|
286
|
+
const details = e.issues.map((err) => ({
|
|
287
|
+
path: err.path.join("."),
|
|
288
|
+
message: err.message,
|
|
289
|
+
code: err.code
|
|
290
|
+
}));
|
|
291
|
+
return res.status(400).json(
|
|
292
|
+
new AppError(
|
|
293
|
+
400,
|
|
294
|
+
"Invalid request body",
|
|
295
|
+
"BAD_REQUEST",
|
|
296
|
+
details
|
|
297
|
+
).toJSON()
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
if (e instanceof AppError) {
|
|
301
|
+
if (e.code === "PARSE_ERROR") {
|
|
302
|
+
this.logger.warn(`Parse error: ${e.message}`);
|
|
303
|
+
} else if (e.code === "PDF_GENERATION_ERROR") {
|
|
304
|
+
this.logger.error(`PDF generation error: ${e.message}`);
|
|
305
|
+
} else {
|
|
306
|
+
this.logger.error(`App error: ${e.message}`);
|
|
307
|
+
}
|
|
308
|
+
return res.status(e.status).json(e.toJSON());
|
|
309
|
+
}
|
|
310
|
+
this.logger.error("Unexpected error:", e);
|
|
311
|
+
return res.status(500).json(
|
|
312
|
+
new AppError(
|
|
313
|
+
500,
|
|
314
|
+
"Internal Server Error",
|
|
315
|
+
"INTERNAL_SERVER_ERROR"
|
|
316
|
+
).toJSON()
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
);
|
|
104
320
|
}
|
|
105
321
|
listen(port) {
|
|
106
322
|
this.app.listen(port, () => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vereign/core",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "core api library for e prescription project",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -25,7 +25,9 @@
|
|
|
25
25
|
"lint": "pnpm eslint",
|
|
26
26
|
"test": "vitest run --coverage",
|
|
27
27
|
"test:watch": "vitest --coverage",
|
|
28
|
-
"semantic-release": "semantic-release"
|
|
28
|
+
"semantic-release": "semantic-release",
|
|
29
|
+
"dev": "ts-node-dev ./src/dev-server.ts",
|
|
30
|
+
"generate:swagger": "npx ts-node ./src/generate-swagger.ts"
|
|
29
31
|
},
|
|
30
32
|
"devDependencies": {
|
|
31
33
|
"@commitlint/cli": "^19.8.1",
|
|
@@ -50,13 +52,16 @@
|
|
|
50
52
|
"prettier": "^3.6.2",
|
|
51
53
|
"semantic-release": "^24.2.7",
|
|
52
54
|
"supertest": "^7.1.4",
|
|
55
|
+
"ts-node-dev": "^2.0.0",
|
|
53
56
|
"tsup": "^8.5.0",
|
|
54
57
|
"typescript": "^5.8.3",
|
|
55
58
|
"typescript-eslint": "^8.37.0",
|
|
56
59
|
"vitest": "^3.2.4",
|
|
57
|
-
"zod-
|
|
60
|
+
"zod-openapi": "^5.3.1"
|
|
58
61
|
},
|
|
59
62
|
"dependencies": {
|
|
63
|
+
"@vereign/chmed-parser": "^1.4.6",
|
|
64
|
+
"@vereign/pdf-generator": "^1.2.1",
|
|
60
65
|
"express": "^5.1.0",
|
|
61
66
|
"pdfkit": "^0.17.1",
|
|
62
67
|
"winston": "^3.17.0",
|