@openhi/constructs 0.0.45 → 0.0.47
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/lib/rest-api-lambda.handler.js +13907 -5314
- package/lib/rest-api-lambda.handler.js.map +1 -1
- package/lib/rest-api-lambda.handler.mjs +13907 -5314
- package/lib/rest-api-lambda.handler.mjs.map +1 -1
- package/package.json +3 -3
- package/scripts/generate-operations.js +373 -0
- package/scripts/generate-routes.js +286 -0
package/package.json
CHANGED
|
@@ -43,15 +43,15 @@
|
|
|
43
43
|
"express": "^5.2.1",
|
|
44
44
|
"type-fest": "^4",
|
|
45
45
|
"ulid": "^3.0.2",
|
|
46
|
-
"@openhi/
|
|
47
|
-
"@openhi/
|
|
46
|
+
"@openhi/types": "0.0.0",
|
|
47
|
+
"@openhi/config": "0.0.0"
|
|
48
48
|
},
|
|
49
49
|
"main": "lib/index.js",
|
|
50
50
|
"license": "UNLICENSED",
|
|
51
51
|
"publishConfig": {
|
|
52
52
|
"access": "public"
|
|
53
53
|
},
|
|
54
|
-
"version": "0.0.
|
|
54
|
+
"version": "0.0.47",
|
|
55
55
|
"types": "lib/index.d.ts",
|
|
56
56
|
"//": "~~ Generated by projen. To modify, edit .projenrc.js and run \"npx projen\".",
|
|
57
57
|
"scripts": {
|
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* One-off script to generate operation files for the 37 entities that have no operations.
|
|
4
|
+
* Run from repo root: node packages/@openhi/constructs/scripts/generate-operations.js
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const fs = require("fs");
|
|
8
|
+
const path = require("path");
|
|
9
|
+
|
|
10
|
+
const ENTITIES = [
|
|
11
|
+
{ key: "capabilitystatement", type: "CapabilityStatement" },
|
|
12
|
+
{ key: "codesystem", type: "CodeSystem" },
|
|
13
|
+
{ key: "compartmentdefinition", type: "CompartmentDefinition" },
|
|
14
|
+
{ key: "conceptmap", type: "ConceptMap" },
|
|
15
|
+
{ key: "examplescenario", type: "ExampleScenario" },
|
|
16
|
+
{ key: "graphdefinition", type: "GraphDefinition" },
|
|
17
|
+
{ key: "implementationguide", type: "ImplementationGuide" },
|
|
18
|
+
{ key: "medicinalproduct", type: "MedicinalProduct" },
|
|
19
|
+
{ key: "medicinalproductauthorization", type: "MedicinalProductAuthorization" },
|
|
20
|
+
{ key: "medicinalproductcontraindication", type: "MedicinalProductContraindication" },
|
|
21
|
+
{ key: "medicinalproductingredient", type: "MedicinalProductIngredient" },
|
|
22
|
+
{ key: "medicinalproductindication", type: "MedicinalProductIndication" },
|
|
23
|
+
{ key: "medicinalproductinteraction", type: "MedicinalProductInteraction" },
|
|
24
|
+
{ key: "medicinalproductmanufactured", type: "MedicinalProductManufactured" },
|
|
25
|
+
{ key: "medicinalproductpackaged", type: "MedicinalProductPackaged" },
|
|
26
|
+
{ key: "medicinalproductpharmaceutical", type: "MedicinalProductPharmaceutical" },
|
|
27
|
+
{ key: "medicinalproductundesirableeffect", type: "MedicinalProductUndesirableEffect" },
|
|
28
|
+
{ key: "messagedefinition", type: "MessageDefinition" },
|
|
29
|
+
{ key: "namingsystem", type: "NamingSystem" },
|
|
30
|
+
{ key: "observationdefinition", type: "ObservationDefinition" },
|
|
31
|
+
{ key: "operationdefinition", type: "OperationDefinition" },
|
|
32
|
+
{ key: "researchdefinition", type: "ResearchDefinition" },
|
|
33
|
+
{ key: "researchelementdefinition", type: "ResearchElementDefinition" },
|
|
34
|
+
{ key: "searchparameter", type: "SearchParameter" },
|
|
35
|
+
{ key: "specimendefinition", type: "SpecimenDefinition" },
|
|
36
|
+
{ key: "structuredefinition", type: "StructureDefinition" },
|
|
37
|
+
{ key: "structuremap", type: "StructureMap" },
|
|
38
|
+
{ key: "substancenucleicacid", type: "SubstanceNucleicAcid" },
|
|
39
|
+
{ key: "substancepolymer", type: "SubstancePolymer" },
|
|
40
|
+
{ key: "substanceprotein", type: "SubstanceProtein" },
|
|
41
|
+
{ key: "substancereferenceinformation", type: "SubstanceReferenceInformation" },
|
|
42
|
+
{ key: "substancespecification", type: "SubstanceSpecification" },
|
|
43
|
+
{ key: "substancesourcematerial", type: "SubstanceSourceMaterial" },
|
|
44
|
+
{ key: "terminologycapabilities", type: "TerminologyCapabilities" },
|
|
45
|
+
{ key: "testreport", type: "TestReport" },
|
|
46
|
+
{ key: "testscript", type: "TestScript" },
|
|
47
|
+
{ key: "valueset", type: "ValueSet" },
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
function camelCase(str) {
|
|
51
|
+
return str.charAt(0).toLowerCase() + str.slice(1);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function pluralLabel(type) {
|
|
55
|
+
return type + "s";
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function writeFile(filePath, content) {
|
|
59
|
+
const dir = path.dirname(filePath);
|
|
60
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
61
|
+
fs.writeFileSync(filePath, content, "utf8");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const OPS_DIR = path.join(
|
|
65
|
+
__dirname,
|
|
66
|
+
"..",
|
|
67
|
+
"src",
|
|
68
|
+
"data",
|
|
69
|
+
"operations",
|
|
70
|
+
"data"
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
ENTITIES.forEach(({ key, type }) => {
|
|
74
|
+
const c = camelCase(type);
|
|
75
|
+
const createOp = `create${type}Operation`;
|
|
76
|
+
const listOp = `list${pluralLabel(type)}Operation`;
|
|
77
|
+
const getOp = `get${type}ByIdOperation`;
|
|
78
|
+
const updateOp = `update${type}Operation`;
|
|
79
|
+
const deleteOp = `delete${type}Operation`;
|
|
80
|
+
|
|
81
|
+
const dir = path.join(OPS_DIR, key);
|
|
82
|
+
|
|
83
|
+
writeFile(
|
|
84
|
+
path.join(dir, `${key}-create-operation.ts`),
|
|
85
|
+
`import type { ${type}, Meta } from "@openhi/types";
|
|
86
|
+
import { ulid } from "ulid";
|
|
87
|
+
import {
|
|
88
|
+
mergeAuditIntoMeta,
|
|
89
|
+
type MetaWithExtensions,
|
|
90
|
+
} from "../../../audit-meta";
|
|
91
|
+
import { getDynamoDataService } from "../../../dynamo/dynamo-data-service";
|
|
92
|
+
import type { OpenHiContext } from "../../../openhi-context";
|
|
93
|
+
import {
|
|
94
|
+
createDataEntityRecord,
|
|
95
|
+
type SingleResourceResult,
|
|
96
|
+
} from "../../data-operations-common";
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Create a ${type}. Accepts FHIR-like body and OpenHI context; returns created resource.
|
|
100
|
+
*
|
|
101
|
+
* @see sites/www-docs/content/packages/@openhi/constructs/data/shared-data-layer-layout.md
|
|
102
|
+
*/
|
|
103
|
+
export interface Create${type}Params {
|
|
104
|
+
context: OpenHiContext;
|
|
105
|
+
/** FHIR ${type} body (resourceType, id optional, meta optional). */
|
|
106
|
+
body: ${type};
|
|
107
|
+
/** Optional table name override; resolved by data service from DYNAMO_TABLE_NAME when omitted. */
|
|
108
|
+
tableName?: string;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export type Create${type}Result = SingleResourceResult<${type}>;
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Creates a ${type}. Builds put attrs with audit in meta, then ElectroDB put.
|
|
115
|
+
* Throws on service/validation errors; adapters map to HTTP/GraphQL.
|
|
116
|
+
*/
|
|
117
|
+
export async function ${createOp}(
|
|
118
|
+
params: Create${type}Params,
|
|
119
|
+
): Promise<Create${type}Result> {
|
|
120
|
+
const { context, body, tableName } = params;
|
|
121
|
+
const { tenantId, workspaceId, date, actorId, actorName } = context;
|
|
122
|
+
const id = body.id ?? ulid();
|
|
123
|
+
|
|
124
|
+
const meta: Meta = {
|
|
125
|
+
...(body.meta ?? {}),
|
|
126
|
+
lastUpdated: date,
|
|
127
|
+
versionId: "1",
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const resourceWithAudit: ${type} & {
|
|
131
|
+
id: string;
|
|
132
|
+
meta: MetaWithExtensions;
|
|
133
|
+
} = {
|
|
134
|
+
...body,
|
|
135
|
+
resourceType: "${type}",
|
|
136
|
+
id,
|
|
137
|
+
meta: mergeAuditIntoMeta(meta, {
|
|
138
|
+
createdDate: date,
|
|
139
|
+
createdById: actorId,
|
|
140
|
+
createdByName: actorName,
|
|
141
|
+
modifiedDate: date,
|
|
142
|
+
modifiedById: actorId,
|
|
143
|
+
modifiedByName: actorName,
|
|
144
|
+
}),
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
const service = getDynamoDataService(tableName);
|
|
148
|
+
return createDataEntityRecord<${type}>(
|
|
149
|
+
service.entities.${key} as Parameters<typeof createDataEntityRecord>[0],
|
|
150
|
+
tenantId,
|
|
151
|
+
workspaceId,
|
|
152
|
+
id,
|
|
153
|
+
resourceWithAudit,
|
|
154
|
+
date,
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
`
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
writeFile(
|
|
161
|
+
path.join(dir, `${key}-list-operation.ts`),
|
|
162
|
+
`import type { ${type} } from "@openhi/types";
|
|
163
|
+
import { getDynamoDataService } from "../../../dynamo/dynamo-data-service";
|
|
164
|
+
import {
|
|
165
|
+
type ListParams,
|
|
166
|
+
listDataEntitiesByWorkspace,
|
|
167
|
+
type ListResult,
|
|
168
|
+
type ListEntry,
|
|
169
|
+
} from "../../data-operations-common";
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* List ${type}s in a workspace (GSI4). Returns domain result for adapters to map to FHIR Bundle or other formats.
|
|
173
|
+
*
|
|
174
|
+
* @see sites/www-docs/content/packages/@openhi/constructs/data/shared-data-layer-layout.md
|
|
175
|
+
*/
|
|
176
|
+
export type List${pluralLabel(type)}Params = ListParams;
|
|
177
|
+
|
|
178
|
+
export type ${type}ListEntry = ListEntry<${type}>;
|
|
179
|
+
|
|
180
|
+
export type List${pluralLabel(type)}Result = ListResult<${type}>;
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Lists all ${type}s in the workspace. Uses GSI4 (Resource Type Index).
|
|
184
|
+
* Throws on service errors; adapters map to HTTP/GraphQL/Step Function.
|
|
185
|
+
*/
|
|
186
|
+
export async function ${listOp}(
|
|
187
|
+
params: List${pluralLabel(type)}Params,
|
|
188
|
+
): Promise<List${pluralLabel(type)}Result> {
|
|
189
|
+
const { context, tableName } = params;
|
|
190
|
+
const { tenantId, workspaceId } = context;
|
|
191
|
+
const service = getDynamoDataService(tableName);
|
|
192
|
+
return listDataEntitiesByWorkspace<${type}>(
|
|
193
|
+
service.entities.${key} as Parameters<
|
|
194
|
+
typeof listDataEntitiesByWorkspace
|
|
195
|
+
>[0],
|
|
196
|
+
tenantId,
|
|
197
|
+
workspaceId,
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
`
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
writeFile(
|
|
204
|
+
path.join(dir, `${key}-get-by-id-operation.ts`),
|
|
205
|
+
`import type { ${type} } from "@openhi/types";
|
|
206
|
+
import { getDynamoDataService } from "../../../dynamo/dynamo-data-service";
|
|
207
|
+
import {
|
|
208
|
+
type GetByIdParams,
|
|
209
|
+
getDataEntityById,
|
|
210
|
+
type SingleResourceResult,
|
|
211
|
+
} from "../../data-operations-common";
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Get a single ${type} by id. Throws NotFoundError if not found.
|
|
215
|
+
*
|
|
216
|
+
* @see sites/www-docs/content/packages/@openhi/constructs/data/shared-data-layer-layout.md
|
|
217
|
+
*/
|
|
218
|
+
export type Get${type}ByIdParams = GetByIdParams;
|
|
219
|
+
|
|
220
|
+
export type Get${type}ByIdResult = SingleResourceResult<${type}>;
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Returns the current version of a ${type}. Throws NotFoundError when the resource does not exist.
|
|
224
|
+
*/
|
|
225
|
+
export async function ${getOp}(
|
|
226
|
+
params: Get${type}ByIdParams,
|
|
227
|
+
): Promise<Get${type}ByIdResult> {
|
|
228
|
+
const { context, id, tableName } = params;
|
|
229
|
+
const { tenantId, workspaceId } = context;
|
|
230
|
+
const service = getDynamoDataService(tableName);
|
|
231
|
+
return getDataEntityById<${type}>(
|
|
232
|
+
service.entities.${key} as Parameters<typeof getDataEntityById>[0],
|
|
233
|
+
tenantId,
|
|
234
|
+
workspaceId,
|
|
235
|
+
id,
|
|
236
|
+
"${type}",
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
`
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
writeFile(
|
|
243
|
+
path.join(dir, `${key}-update-operation.ts`),
|
|
244
|
+
`import type { ${type} } from "@openhi/types";
|
|
245
|
+
import { getDynamoDataService } from "../../../dynamo/dynamo-data-service";
|
|
246
|
+
import {
|
|
247
|
+
type BaseDataEntityParams,
|
|
248
|
+
buildUpdatedResourceWithAudit,
|
|
249
|
+
updateDataEntityById,
|
|
250
|
+
type SingleResourceResult,
|
|
251
|
+
} from "../../data-operations-common";
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Update a ${type} by id. Throws NotFoundError if the resource does not exist.
|
|
255
|
+
*
|
|
256
|
+
* @see sites/www-docs/content/packages/@openhi/constructs/data/shared-data-layer-layout.md
|
|
257
|
+
*/
|
|
258
|
+
export interface Update${type}Params extends BaseDataEntityParams {
|
|
259
|
+
id: string;
|
|
260
|
+
/** FHIR ${type} body (resourceType, id, meta optional). */
|
|
261
|
+
body: ${type};
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export type Update${type}Result = SingleResourceResult<${type}>;
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Updates the current version of a ${type}. Merges audit from context into meta.
|
|
268
|
+
* Throws NotFoundError when the resource does not exist.
|
|
269
|
+
*/
|
|
270
|
+
export async function ${updateOp}(
|
|
271
|
+
params: Update${type}Params,
|
|
272
|
+
): Promise<Update${type}Result> {
|
|
273
|
+
const { context, id, body, tableName } = params;
|
|
274
|
+
const { tenantId, workspaceId, date, actorId, actorName } = context;
|
|
275
|
+
const service = getDynamoDataService(tableName);
|
|
276
|
+
|
|
277
|
+
return updateDataEntityById<${type}>(
|
|
278
|
+
service.entities.${key} as Parameters<typeof updateDataEntityById>[0],
|
|
279
|
+
tenantId,
|
|
280
|
+
workspaceId,
|
|
281
|
+
id,
|
|
282
|
+
"${type}",
|
|
283
|
+
context,
|
|
284
|
+
(existingResourceStr) =>
|
|
285
|
+
buildUpdatedResourceWithAudit<${type}>(
|
|
286
|
+
body,
|
|
287
|
+
id,
|
|
288
|
+
date,
|
|
289
|
+
actorId,
|
|
290
|
+
actorName,
|
|
291
|
+
existingResourceStr,
|
|
292
|
+
"${type}",
|
|
293
|
+
),
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
`
|
|
297
|
+
);
|
|
298
|
+
|
|
299
|
+
writeFile(
|
|
300
|
+
path.join(dir, `${key}-delete-operation.ts`),
|
|
301
|
+
`import { getDynamoDataService } from "../../../dynamo/dynamo-data-service";
|
|
302
|
+
import {
|
|
303
|
+
type BaseDataEntityParams,
|
|
304
|
+
deleteDataEntityById,
|
|
305
|
+
} from "../../data-operations-common";
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Delete a ${type} by id. No-op if the item does not exist (ElectroDB delete is idempotent).
|
|
309
|
+
*
|
|
310
|
+
* @see sites/www-docs/content/packages/@openhi/constructs/data/shared-data-layer-layout.md
|
|
311
|
+
*/
|
|
312
|
+
export interface Delete${type}Params extends BaseDataEntityParams {
|
|
313
|
+
id: string;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Deletes the current version of a ${type}. Does not throw when the resource does not exist.
|
|
318
|
+
* Throws on service errors; adapters map to HTTP/GraphQL.
|
|
319
|
+
*/
|
|
320
|
+
export async function ${deleteOp}(
|
|
321
|
+
params: Delete${type}Params,
|
|
322
|
+
): Promise<void> {
|
|
323
|
+
const { context, id, tableName } = params;
|
|
324
|
+
const { tenantId, workspaceId } = context;
|
|
325
|
+
const service = getDynamoDataService(tableName);
|
|
326
|
+
await deleteDataEntityById(
|
|
327
|
+
service.entities.${key} as Parameters<typeof deleteDataEntityById>[0],
|
|
328
|
+
tenantId,
|
|
329
|
+
workspaceId,
|
|
330
|
+
id,
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
`
|
|
334
|
+
);
|
|
335
|
+
|
|
336
|
+
writeFile(
|
|
337
|
+
path.join(dir, "index.ts"),
|
|
338
|
+
`/**
|
|
339
|
+
* ${type} data operations. One file per operation; adapters (REST, GraphQL, Step Function) import as needed.
|
|
340
|
+
*
|
|
341
|
+
* @see sites/www-docs/content/packages/@openhi/constructs/data/shared-data-layer-layout.md
|
|
342
|
+
*/
|
|
343
|
+
|
|
344
|
+
export {
|
|
345
|
+
${createOp},
|
|
346
|
+
type Create${type}Params,
|
|
347
|
+
type Create${type}Result,
|
|
348
|
+
} from "./${key}-create-operation";
|
|
349
|
+
export {
|
|
350
|
+
${deleteOp},
|
|
351
|
+
type Delete${type}Params,
|
|
352
|
+
} from "./${key}-delete-operation";
|
|
353
|
+
export {
|
|
354
|
+
${getOp},
|
|
355
|
+
type Get${type}ByIdParams,
|
|
356
|
+
type Get${type}ByIdResult,
|
|
357
|
+
} from "./${key}-get-by-id-operation";
|
|
358
|
+
export {
|
|
359
|
+
${listOp},
|
|
360
|
+
type List${pluralLabel(type)}Params,
|
|
361
|
+
type List${pluralLabel(type)}Result,
|
|
362
|
+
type ${type}ListEntry,
|
|
363
|
+
} from "./${key}-list-operation";
|
|
364
|
+
export {
|
|
365
|
+
${updateOp},
|
|
366
|
+
type Update${type}Params,
|
|
367
|
+
type Update${type}Result,
|
|
368
|
+
} from "./${key}-update-operation";
|
|
369
|
+
`
|
|
370
|
+
);
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
console.log("Generated operations for", ENTITIES.length, "entities.");
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* One-off script to generate REST route files for the 37 resources that have operations but no routes.
|
|
4
|
+
* Run from repo root: node packages/@openhi/constructs/scripts/generate-routes.js
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const fs = require("fs");
|
|
8
|
+
const path = require("path");
|
|
9
|
+
|
|
10
|
+
const RESOURCES = [
|
|
11
|
+
{ key: "capabilitystatement", type: "CapabilityStatement" },
|
|
12
|
+
{ key: "codesystem", type: "CodeSystem" },
|
|
13
|
+
{ key: "compartmentdefinition", type: "CompartmentDefinition" },
|
|
14
|
+
{ key: "conceptmap", type: "ConceptMap" },
|
|
15
|
+
{ key: "examplescenario", type: "ExampleScenario" },
|
|
16
|
+
{ key: "graphdefinition", type: "GraphDefinition" },
|
|
17
|
+
{ key: "implementationguide", type: "ImplementationGuide" },
|
|
18
|
+
{ key: "medicinalproduct", type: "MedicinalProduct" },
|
|
19
|
+
{ key: "medicinalproductauthorization", type: "MedicinalProductAuthorization" },
|
|
20
|
+
{ key: "medicinalproductcontraindication", type: "MedicinalProductContraindication" },
|
|
21
|
+
{ key: "medicinalproductingredient", type: "MedicinalProductIngredient" },
|
|
22
|
+
{ key: "medicinalproductindication", type: "MedicinalProductIndication" },
|
|
23
|
+
{ key: "medicinalproductinteraction", type: "MedicinalProductInteraction" },
|
|
24
|
+
{ key: "medicinalproductmanufactured", type: "MedicinalProductManufactured" },
|
|
25
|
+
{ key: "medicinalproductpackaged", type: "MedicinalProductPackaged" },
|
|
26
|
+
{ key: "medicinalproductpharmaceutical", type: "MedicinalProductPharmaceutical" },
|
|
27
|
+
{ key: "medicinalproductundesirableeffect", type: "MedicinalProductUndesirableEffect" },
|
|
28
|
+
{ key: "messagedefinition", type: "MessageDefinition" },
|
|
29
|
+
{ key: "namingsystem", type: "NamingSystem" },
|
|
30
|
+
{ key: "observationdefinition", type: "ObservationDefinition" },
|
|
31
|
+
{ key: "operationdefinition", type: "OperationDefinition" },
|
|
32
|
+
{ key: "researchdefinition", type: "ResearchDefinition" },
|
|
33
|
+
{ key: "researchelementdefinition", type: "ResearchElementDefinition" },
|
|
34
|
+
{ key: "searchparameter", type: "SearchParameter" },
|
|
35
|
+
{ key: "specimendefinition", type: "SpecimenDefinition" },
|
|
36
|
+
{ key: "structuredefinition", type: "StructureDefinition" },
|
|
37
|
+
{ key: "structuremap", type: "StructureMap" },
|
|
38
|
+
{ key: "substancenucleicacid", type: "SubstanceNucleicAcid" },
|
|
39
|
+
{ key: "substancepolymer", type: "SubstancePolymer" },
|
|
40
|
+
{ key: "substanceprotein", type: "SubstanceProtein" },
|
|
41
|
+
{ key: "substancereferenceinformation", type: "SubstanceReferenceInformation" },
|
|
42
|
+
{ key: "substancespecification", type: "SubstanceSpecification" },
|
|
43
|
+
{ key: "substancesourcematerial", type: "SubstanceSourceMaterial" },
|
|
44
|
+
{ key: "terminologycapabilities", type: "TerminologyCapabilities" },
|
|
45
|
+
{ key: "testreport", type: "TestReport" },
|
|
46
|
+
{ key: "testscript", type: "TestScript" },
|
|
47
|
+
{ key: "valueset", type: "ValueSet" },
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
function toBasePathKey(key) {
|
|
51
|
+
return key.toUpperCase().replace(/-/g, "");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function createRouteName(type, suffix) {
|
|
55
|
+
const base = type.charAt(0).toLowerCase() + type.slice(1);
|
|
56
|
+
if (suffix === "List") return "list" + type + "s";
|
|
57
|
+
if (suffix === "Create") return "create" + type;
|
|
58
|
+
if (suffix === "GetById") return "get" + type + "ById";
|
|
59
|
+
if (suffix === "Update") return "update" + type;
|
|
60
|
+
if (suffix === "Delete") return "delete" + type;
|
|
61
|
+
return base;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function writeFile(filePath, content) {
|
|
65
|
+
const dir = path.dirname(filePath);
|
|
66
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
67
|
+
fs.writeFileSync(filePath, content, "utf8");
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const ROUTES_DIR = path.join(__dirname, "..", "src", "data", "rest-api", "routes", "data");
|
|
71
|
+
|
|
72
|
+
RESOURCES.forEach(({ key, type }) => {
|
|
73
|
+
const basePathKey = toBasePathKey(key);
|
|
74
|
+
const createRoute = "create" + type + "Route";
|
|
75
|
+
const listRoute = "list" + type + "sRoute";
|
|
76
|
+
const getByIdRoute = "get" + type + "ByIdRoute";
|
|
77
|
+
const updateRoute = "update" + type + "Route";
|
|
78
|
+
const deleteRoute = "delete" + type + "Route";
|
|
79
|
+
const routerName = key + "Router";
|
|
80
|
+
|
|
81
|
+
const dir = path.join(ROUTES_DIR, key);
|
|
82
|
+
|
|
83
|
+
writeFile(
|
|
84
|
+
path.join(dir, `${key}-create-route.ts`),
|
|
85
|
+
`import type { ${type} } from "@openhi/types";
|
|
86
|
+
import type { Request, Response } from "express";
|
|
87
|
+
import { create${type}Operation } from "../../../../operations/data/${key}/${key}-create-operation";
|
|
88
|
+
import {
|
|
89
|
+
BASE_PATH,
|
|
90
|
+
requireJsonBodyAs,
|
|
91
|
+
sendOperationOutcome500,
|
|
92
|
+
} from "../../common";
|
|
93
|
+
|
|
94
|
+
/** POST /${type} — create: accepts ${type} in body, persists via data layer, returns 201. */
|
|
95
|
+
export async function ${createRoute}(
|
|
96
|
+
req: Request,
|
|
97
|
+
res: Response,
|
|
98
|
+
): Promise<Response> {
|
|
99
|
+
const bodyResult = requireJsonBodyAs<${type}>(req, res);
|
|
100
|
+
if ("errorResponse" in bodyResult) return bodyResult.errorResponse;
|
|
101
|
+
|
|
102
|
+
const ctx = req.openhiContext!;
|
|
103
|
+
const body = bodyResult.body;
|
|
104
|
+
const resource: ${type} = {
|
|
105
|
+
...body,
|
|
106
|
+
resourceType: "${type}",
|
|
107
|
+
} as ${type};
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
const result = await create${type}Operation({
|
|
111
|
+
context: ctx,
|
|
112
|
+
body: resource,
|
|
113
|
+
});
|
|
114
|
+
return res
|
|
115
|
+
.status(201)
|
|
116
|
+
.location(\`\${BASE_PATH.${basePathKey}}/\${result.id}\`)
|
|
117
|
+
.json(result.resource);
|
|
118
|
+
} catch (err: unknown) {
|
|
119
|
+
return sendOperationOutcome500(res, err, "POST ${type} error:");
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
`
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
writeFile(
|
|
126
|
+
path.join(dir, `${key}-list-route.ts`),
|
|
127
|
+
`import type { Request, Response } from "express";
|
|
128
|
+
import { list${type}sOperation } from "../../../../operations/data/${key}/${key}-list-operation";
|
|
129
|
+
import {
|
|
130
|
+
BASE_PATH,
|
|
131
|
+
buildSearchsetBundle,
|
|
132
|
+
sendOperationOutcome500,
|
|
133
|
+
} from "../../common";
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* GET /${type} — search/list: returns a FHIR Bundle (searchset) from the data store.
|
|
137
|
+
* Uses GSI4 (Resource Type Index) to list all ${type}s in the workspace without a table scan.
|
|
138
|
+
*/
|
|
139
|
+
export async function ${listRoute}(
|
|
140
|
+
req: Request,
|
|
141
|
+
res: Response,
|
|
142
|
+
): Promise<Response> {
|
|
143
|
+
const ctx = req.openhiContext!;
|
|
144
|
+
|
|
145
|
+
try {
|
|
146
|
+
const result = await list${type}sOperation({ context: ctx });
|
|
147
|
+
const bundle = buildSearchsetBundle(BASE_PATH.${basePathKey}, result.entries);
|
|
148
|
+
return res.json(bundle);
|
|
149
|
+
} catch (err: unknown) {
|
|
150
|
+
return sendOperationOutcome500(res, err, "GET /${type} list error:");
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
`
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
writeFile(
|
|
157
|
+
path.join(dir, `${key}-get-by-id-route.ts`),
|
|
158
|
+
`import type { Request, Response } from "express";
|
|
159
|
+
import { NotFoundError, domainErrorToHttpStatus } from "../../../../errors";
|
|
160
|
+
import { get${type}ByIdOperation } from "../../../../operations/data/${key}/${key}-get-by-id-operation";
|
|
161
|
+
import { sendOperationOutcome404, sendOperationOutcome500 } from "../../common";
|
|
162
|
+
|
|
163
|
+
/** GET /${type}/:id — read: returns a single ${type} resource from the data store or 404. */
|
|
164
|
+
export async function ${getByIdRoute}(
|
|
165
|
+
req: Request,
|
|
166
|
+
res: Response,
|
|
167
|
+
): Promise<Response> {
|
|
168
|
+
const id = String(req.params.id);
|
|
169
|
+
const ctx = req.openhiContext!;
|
|
170
|
+
|
|
171
|
+
try {
|
|
172
|
+
const result = await get${type}ByIdOperation({ context: ctx, id });
|
|
173
|
+
return res.json(result.resource);
|
|
174
|
+
} catch (err: unknown) {
|
|
175
|
+
const status = domainErrorToHttpStatus(err);
|
|
176
|
+
if (status === 404) {
|
|
177
|
+
const diagnostics =
|
|
178
|
+
err instanceof NotFoundError ? err.message : \`${type} \${id} not found\`;
|
|
179
|
+
return sendOperationOutcome404(res, diagnostics);
|
|
180
|
+
}
|
|
181
|
+
return sendOperationOutcome500(res, err, "GET ${type} error:");
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
`
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
writeFile(
|
|
188
|
+
path.join(dir, `${key}-update-route.ts`),
|
|
189
|
+
`import type { ${type} } from "@openhi/types";
|
|
190
|
+
import type { Request, Response } from "express";
|
|
191
|
+
import { NotFoundError, domainErrorToHttpStatus } from "../../../../errors";
|
|
192
|
+
import { update${type}Operation } from "../../../../operations/data/${key}/${key}-update-operation";
|
|
193
|
+
import {
|
|
194
|
+
requireJsonBodyAs,
|
|
195
|
+
sendOperationOutcome404,
|
|
196
|
+
sendOperationOutcome500,
|
|
197
|
+
} from "../../common";
|
|
198
|
+
|
|
199
|
+
/** PUT /${type}/:id — update: accepts ${type} in body, persists via data layer, returns 200. */
|
|
200
|
+
export async function ${updateRoute}(
|
|
201
|
+
req: Request,
|
|
202
|
+
res: Response,
|
|
203
|
+
): Promise<Response> {
|
|
204
|
+
const bodyResult = requireJsonBodyAs<${type}>(req, res);
|
|
205
|
+
if ("errorResponse" in bodyResult) return bodyResult.errorResponse;
|
|
206
|
+
|
|
207
|
+
const id = String(req.params.id);
|
|
208
|
+
const ctx = req.openhiContext!;
|
|
209
|
+
const body = bodyResult.body;
|
|
210
|
+
const resource: ${type} = {
|
|
211
|
+
...body,
|
|
212
|
+
resourceType: "${type}",
|
|
213
|
+
id,
|
|
214
|
+
} as ${type};
|
|
215
|
+
|
|
216
|
+
try {
|
|
217
|
+
const result = await update${type}Operation({
|
|
218
|
+
context: ctx,
|
|
219
|
+
id,
|
|
220
|
+
body: resource,
|
|
221
|
+
});
|
|
222
|
+
return res.json(result.resource);
|
|
223
|
+
} catch (err: unknown) {
|
|
224
|
+
const status = domainErrorToHttpStatus(err);
|
|
225
|
+
if (status === 404) {
|
|
226
|
+
const diagnostics =
|
|
227
|
+
err instanceof NotFoundError ? err.message : \`${type} \${id} not found\`;
|
|
228
|
+
return sendOperationOutcome404(res, diagnostics);
|
|
229
|
+
}
|
|
230
|
+
return sendOperationOutcome500(res, err, "PUT ${type} error:");
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
`
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
writeFile(
|
|
237
|
+
path.join(dir, `${key}-delete-route.ts`),
|
|
238
|
+
`import type { Request, Response } from "express";
|
|
239
|
+
import { delete${type}Operation } from "../../../../operations/data/${key}/${key}-delete-operation";
|
|
240
|
+
import { sendOperationOutcome500 } from "../../common";
|
|
241
|
+
|
|
242
|
+
/** DELETE /${type}/:id — delete: removes from data store, returns 204. */
|
|
243
|
+
export async function ${deleteRoute}(
|
|
244
|
+
req: Request,
|
|
245
|
+
res: Response,
|
|
246
|
+
): Promise<Response> {
|
|
247
|
+
const id = String(req.params.id);
|
|
248
|
+
const ctx = req.openhiContext!;
|
|
249
|
+
|
|
250
|
+
try {
|
|
251
|
+
await delete${type}Operation({ context: ctx, id });
|
|
252
|
+
return res.status(204).send();
|
|
253
|
+
} catch (err: unknown) {
|
|
254
|
+
return sendOperationOutcome500(res, err, "DELETE ${type} error:");
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
`
|
|
258
|
+
);
|
|
259
|
+
|
|
260
|
+
writeFile(
|
|
261
|
+
path.join(dir, `${key}.ts`),
|
|
262
|
+
`import express from "express";
|
|
263
|
+
import { ${createRoute} } from "./${key}-create-route";
|
|
264
|
+
import { ${deleteRoute} } from "./${key}-delete-route";
|
|
265
|
+
import { ${getByIdRoute} } from "./${key}-get-by-id-route";
|
|
266
|
+
import { ${listRoute} } from "./${key}-list-route";
|
|
267
|
+
import { ${updateRoute} } from "./${key}-update-route";
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* ${type} REST router: /${type}
|
|
271
|
+
* FHIR R4 ${type} resource.
|
|
272
|
+
*/
|
|
273
|
+
const router: express.Router = express.Router();
|
|
274
|
+
|
|
275
|
+
router.get("/", ${listRoute});
|
|
276
|
+
router.get("/:id", ${getByIdRoute});
|
|
277
|
+
router.post("/", ${createRoute});
|
|
278
|
+
router.put("/:id", ${updateRoute});
|
|
279
|
+
router.delete("/:id", ${deleteRoute});
|
|
280
|
+
|
|
281
|
+
export { router as ${routerName} };
|
|
282
|
+
`
|
|
283
|
+
);
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
console.log("Generated routes for", RESOURCES.length, "resources.");
|