@medplum/cli 2.0.21 → 2.0.23
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/dist/cjs/index.cjs
CHANGED
|
@@ -4,15 +4,17 @@
|
|
|
4
4
|
var core = require('@medplum/core');
|
|
5
5
|
var commander = require('commander');
|
|
6
6
|
var dotenv = require('dotenv');
|
|
7
|
+
var os = require('os');
|
|
8
|
+
var child_process = require('child_process');
|
|
9
|
+
var http = require('http');
|
|
10
|
+
var fs = require('fs');
|
|
11
|
+
var path = require('path');
|
|
7
12
|
var clientCloudformation = require('@aws-sdk/client-cloudformation');
|
|
8
13
|
var clientCloudfront = require('@aws-sdk/client-cloudfront');
|
|
9
14
|
var clientEcs = require('@aws-sdk/client-ecs');
|
|
10
15
|
var clientS3 = require('@aws-sdk/client-s3');
|
|
11
16
|
var fastGlob = require('fast-glob');
|
|
12
|
-
var fs = require('fs');
|
|
13
17
|
var fetch$1 = require('node-fetch');
|
|
14
|
-
var os = require('os');
|
|
15
|
-
var path = require('path');
|
|
16
18
|
var promises = require('stream/promises');
|
|
17
19
|
var tar = require('tar');
|
|
18
20
|
var clientAcm = require('@aws-sdk/client-acm');
|
|
@@ -20,22 +22,99 @@ var clientSsm = require('@aws-sdk/client-ssm');
|
|
|
20
22
|
var clientSts = require('@aws-sdk/client-sts');
|
|
21
23
|
var crypto = require('crypto');
|
|
22
24
|
var readline = require('readline');
|
|
23
|
-
|
|
24
|
-
|
|
25
|
+
|
|
26
|
+
function createMedplumCommand(name) {
|
|
27
|
+
return new commander.Command(name)
|
|
28
|
+
.option('--client-id <clientId>', 'FHIR server client id')
|
|
29
|
+
.option('--client-secret <clientSecret>', 'FHIR server client secret')
|
|
30
|
+
.option('--base-url <baseUrl>', 'FHIR server base url')
|
|
31
|
+
.option('--token-url <tokenUrl>', 'FHIR server token url')
|
|
32
|
+
.option('--authorize-url <authorizeUrl>', 'FHIR server authorize url')
|
|
33
|
+
.option('--fhir-url-path <fhirUrlPath>', 'FHIR server url path');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
class FileSystemStorage extends core.ClientStorage {
|
|
37
|
+
constructor() {
|
|
38
|
+
super();
|
|
39
|
+
this.dirName = path.resolve(os.homedir(), '.medplum');
|
|
40
|
+
this.fileName = path.resolve(this.dirName, 'credentials');
|
|
41
|
+
}
|
|
42
|
+
clear() {
|
|
43
|
+
this.writeFile({});
|
|
44
|
+
}
|
|
45
|
+
getString(key) {
|
|
46
|
+
return this.readFile()?.[key];
|
|
47
|
+
}
|
|
48
|
+
setString(key, value) {
|
|
49
|
+
const data = this.readFile() || {};
|
|
50
|
+
if (value) {
|
|
51
|
+
data[key] = value;
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
delete data[key];
|
|
55
|
+
}
|
|
56
|
+
this.writeFile(data);
|
|
57
|
+
}
|
|
58
|
+
readFile() {
|
|
59
|
+
if (fs.existsSync(this.fileName)) {
|
|
60
|
+
return JSON.parse(fs.readFileSync(this.fileName, 'utf8'));
|
|
61
|
+
}
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
writeFile(data) {
|
|
65
|
+
if (!fs.existsSync(this.dirName)) {
|
|
66
|
+
fs.mkdirSync(this.dirName);
|
|
67
|
+
}
|
|
68
|
+
fs.writeFileSync(this.fileName, JSON.stringify(data, null, 2), 'utf8');
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function createMedplumClient(options) {
|
|
73
|
+
const baseUrl = options.baseUrl ?? process.env['MEDPLUM_BASE_URL'] ?? 'https://api.medplum.com/';
|
|
74
|
+
const fhirUrlPath = options.fhirUrlPath ?? process.env['MEDPLUM_FHIR_URL_PATH'] ?? '';
|
|
75
|
+
const accessToken = options.accessToken ?? process.env['MEDPLUM_CLIENT_ACCESS_TOKEN'] ?? '';
|
|
76
|
+
const tokenUrl = options.tokenUrl ?? process.env['MEDPLUM_TOKEN_URL'] ?? '';
|
|
77
|
+
const authorizeUrl = options.authorizeUrl ?? process.env['MEDPLUM_AUTHORIZE_URL'] ?? '';
|
|
78
|
+
const fetchApi = options.fetch ?? fetch;
|
|
79
|
+
const medplumClient = new core.MedplumClient({
|
|
80
|
+
fetch: fetchApi,
|
|
81
|
+
baseUrl,
|
|
82
|
+
tokenUrl,
|
|
83
|
+
fhirUrlPath,
|
|
84
|
+
authorizeUrl,
|
|
85
|
+
storage: new FileSystemStorage(),
|
|
86
|
+
onUnauthenticated: onUnauthenticated,
|
|
87
|
+
});
|
|
88
|
+
if (accessToken) {
|
|
89
|
+
medplumClient.setAccessToken(accessToken);
|
|
90
|
+
}
|
|
91
|
+
const clientId = options.clientId || process.env['MEDPLUM_CLIENT_ID'];
|
|
92
|
+
const clientSecret = options.clientSecret || process.env['MEDPLUM_CLIENT_SECRET'];
|
|
93
|
+
if (clientId && clientSecret) {
|
|
94
|
+
medplumClient.setBasicAuth(clientId, clientSecret);
|
|
95
|
+
await medplumClient.startClientLogin(clientId, clientSecret);
|
|
96
|
+
}
|
|
97
|
+
return medplumClient;
|
|
98
|
+
}
|
|
99
|
+
function onUnauthenticated() {
|
|
100
|
+
console.log('Unauthenticated: run `npx medplum login` to sign in');
|
|
101
|
+
}
|
|
25
102
|
|
|
26
103
|
const clientId = 'medplum-cli';
|
|
27
104
|
const redirectUri = 'http://localhost:9615';
|
|
28
|
-
const login =
|
|
29
|
-
const whoami =
|
|
30
|
-
login.action(async () => {
|
|
31
|
-
await
|
|
105
|
+
const login = createMedplumCommand('login');
|
|
106
|
+
const whoami = createMedplumCommand('whoami');
|
|
107
|
+
login.action(async (options) => {
|
|
108
|
+
const medplum = await createMedplumClient(options);
|
|
109
|
+
await startLogin(medplum);
|
|
32
110
|
});
|
|
33
|
-
whoami.action(() => {
|
|
34
|
-
|
|
111
|
+
whoami.action(async (options) => {
|
|
112
|
+
const medplum = await createMedplumClient(options);
|
|
113
|
+
printMe(medplum);
|
|
35
114
|
});
|
|
36
115
|
async function startLogin(medplum) {
|
|
37
116
|
await startWebServer(medplum);
|
|
38
|
-
const loginUrl = new URL(
|
|
117
|
+
const loginUrl = new URL(medplum.getAuthorizeUrl());
|
|
39
118
|
loginUrl.searchParams.set('client_id', clientId);
|
|
40
119
|
loginUrl.searchParams.set('redirect_uri', redirectUri);
|
|
41
120
|
loginUrl.searchParams.set('scope', 'openid');
|
|
@@ -204,7 +283,6 @@ function getEcsServiceName(resource) {
|
|
|
204
283
|
|
|
205
284
|
/**
|
|
206
285
|
* The AWS "describe" command prints details about a Medplum CloudFormation stack.
|
|
207
|
-
*
|
|
208
286
|
* @param tag The Medplum stack tag.
|
|
209
287
|
*/
|
|
210
288
|
async function describeStacksCommand(tag) {
|
|
@@ -343,7 +421,6 @@ function escapeRegex(str) {
|
|
|
343
421
|
* Expanding archive files without controlling resource consumption is security-sensitive
|
|
344
422
|
*
|
|
345
423
|
* See: https://sonarcloud.io/organizations/medplum/rules?open=typescript%3AS5042&rule_key=typescript%3AS5042
|
|
346
|
-
*
|
|
347
424
|
* @param destinationDir The destination directory where all files will be extracted.
|
|
348
425
|
* @returns A tar file extractor.
|
|
349
426
|
*/
|
|
@@ -409,6 +486,8 @@ async function updateAppCommand(tag) {
|
|
|
409
486
|
* Returns NPM package metadata for a given package name.
|
|
410
487
|
* See: https://github.com/npm/registry/blob/master/docs/REGISTRY-API.md#getpackageversion
|
|
411
488
|
* @param packageName The npm package name.
|
|
489
|
+
* @param version The npm package version string.
|
|
490
|
+
* @returns The package.json metadata content.
|
|
412
491
|
*/
|
|
413
492
|
async function getNpmPackageMetadata(packageName, version) {
|
|
414
493
|
const url = `https://registry.npmjs.org/${packageName}/${version}`;
|
|
@@ -508,6 +587,11 @@ async function uploadAppToS3(tmpDir, bucketName) {
|
|
|
508
587
|
/**
|
|
509
588
|
* Uploads a directory of files to S3.
|
|
510
589
|
* @param options The upload options such as bucket name, content type, and cache control.
|
|
590
|
+
* @param options.rootDir The root directory of the upload.
|
|
591
|
+
* @param options.bucketName The destination bucket name.
|
|
592
|
+
* @param options.fileNamePattern The glob file pattern to upload.
|
|
593
|
+
* @param options.contentType The content type MIME type.
|
|
594
|
+
* @param options.cached True to mark as public and cached forever.
|
|
511
595
|
*/
|
|
512
596
|
async function uploadFolderToS3(options) {
|
|
513
597
|
const items = fastGlob.sync(options.fileNamePattern, { cwd: options.rootDir });
|
|
@@ -519,6 +603,10 @@ async function uploadFolderToS3(options) {
|
|
|
519
603
|
* Uploads a file to S3.
|
|
520
604
|
* @param filePath The file path.
|
|
521
605
|
* @param options The upload options such as bucket name, content type, and cache control.
|
|
606
|
+
* @param options.rootDir The root directory of the upload.
|
|
607
|
+
* @param options.bucketName The destination bucket name.
|
|
608
|
+
* @param options.contentType The content type MIME type.
|
|
609
|
+
* @param options.cached True to mark as public and cached forever.
|
|
522
610
|
*/
|
|
523
611
|
async function uploadFileToS3(filePath, options) {
|
|
524
612
|
const fileStream = fs.createReadStream(filePath);
|
|
@@ -561,7 +649,6 @@ async function createInvalidation(distributionId) {
|
|
|
561
649
|
/**
|
|
562
650
|
* The AWS "update-server" command updates the Medplum server in a Medplum CloudFormation stack.
|
|
563
651
|
* @param tag The Medplum stack tag.
|
|
564
|
-
* @returns
|
|
565
652
|
*/
|
|
566
653
|
async function updateServerCommand(tag) {
|
|
567
654
|
const details = await getStackByTag(tag);
|
|
@@ -800,15 +887,26 @@ async function initStackCommand() {
|
|
|
800
887
|
print(' https://www.medplum.com/docs/self-hosting/install-on-aws');
|
|
801
888
|
print('');
|
|
802
889
|
}
|
|
803
|
-
/**
|
|
890
|
+
/**
|
|
891
|
+
* Prints to stdout.
|
|
892
|
+
* @param text The text to print.
|
|
893
|
+
*/
|
|
804
894
|
function print(text) {
|
|
805
895
|
terminal.write(text + '\n');
|
|
806
896
|
}
|
|
807
|
-
/**
|
|
897
|
+
/**
|
|
898
|
+
* Prints a header with extra line spacing.
|
|
899
|
+
* @param text The text to print.
|
|
900
|
+
*/
|
|
808
901
|
function header(text) {
|
|
809
902
|
print('\n' + text + '\n');
|
|
810
903
|
}
|
|
811
|
-
/**
|
|
904
|
+
/**
|
|
905
|
+
* Prints a question and waits for user input.
|
|
906
|
+
* @param text The question text to print.
|
|
907
|
+
* @param defaultValue Optional default value.
|
|
908
|
+
* @returns The selected value, or default value on empty selection.
|
|
909
|
+
*/
|
|
812
910
|
function ask(text, defaultValue = '') {
|
|
813
911
|
return new Promise((resolve) => {
|
|
814
912
|
terminal.question(text + (defaultValue ? ' (' + defaultValue + ')' : '') + ' ', (answer) => {
|
|
@@ -816,7 +914,13 @@ function ask(text, defaultValue = '') {
|
|
|
816
914
|
});
|
|
817
915
|
});
|
|
818
916
|
}
|
|
819
|
-
/**
|
|
917
|
+
/**
|
|
918
|
+
* Prints a question and waits for user to choose one of the provided options.
|
|
919
|
+
* @param text The prompt text to print.
|
|
920
|
+
* @param options The list of options that the user can select.
|
|
921
|
+
* @param defaultValue Optional default value.
|
|
922
|
+
* @returns The selected value, or default value on empty selection.
|
|
923
|
+
*/
|
|
820
924
|
async function choose(text, options, defaultValue = '') {
|
|
821
925
|
const str = text + ' [' + options.map((o) => (o === defaultValue ? '(' + o + ')' : o)).join('|') + ']';
|
|
822
926
|
// eslint-disable-next-line no-constant-condition
|
|
@@ -828,15 +932,28 @@ async function choose(text, options, defaultValue = '') {
|
|
|
828
932
|
print('Please choose one of the following options: ' + options.join(', '));
|
|
829
933
|
}
|
|
830
934
|
}
|
|
831
|
-
/**
|
|
935
|
+
/**
|
|
936
|
+
* Prints a question and waits for the user to choose a valid integer option.
|
|
937
|
+
* @param text The prompt text to print.
|
|
938
|
+
* @param options The list of options that the user can select.
|
|
939
|
+
* @param defaultValue Optional default value.
|
|
940
|
+
* @returns The selected value, or default value on empty selection.
|
|
941
|
+
*/
|
|
832
942
|
async function chooseInt(text, options, defaultValue = 0) {
|
|
833
943
|
return parseInt(await choose(text, options.map((o) => o.toString()), defaultValue.toString()));
|
|
834
944
|
}
|
|
835
|
-
/**
|
|
945
|
+
/**
|
|
946
|
+
* Prints a question and waits for the user to choose yes or no.
|
|
947
|
+
* @param text The question to print.
|
|
948
|
+
* @returns true on accept or false on reject.
|
|
949
|
+
*/
|
|
836
950
|
async function yesOrNo(text) {
|
|
837
951
|
return (await choose(text, ['y', 'n'])).toLowerCase() === 'y';
|
|
838
952
|
}
|
|
839
|
-
/**
|
|
953
|
+
/**
|
|
954
|
+
* Prints a question and waits for the user to confirm yes. Throws error on no, and exits the program.
|
|
955
|
+
* @param text The prompt text to print.
|
|
956
|
+
*/
|
|
840
957
|
async function checkOk(text) {
|
|
841
958
|
if (!(await yesOrNo(text))) {
|
|
842
959
|
print('Exiting...');
|
|
@@ -908,7 +1025,6 @@ async function listCertificates(region) {
|
|
|
908
1025
|
* 1. If the certificate already exists, return the ARN.
|
|
909
1026
|
* 2. If the certificate does not exist, and the user wants to create a new certificate, create it and return the ARN.
|
|
910
1027
|
* 3. If the certificate does not exist, and the user does not want to create a new certificate, return a placeholder.
|
|
911
|
-
*
|
|
912
1028
|
* @param config In-progress config settings.
|
|
913
1029
|
* @param allCerts List of all existing certificates.
|
|
914
1030
|
* @param region The AWS region where the certificate is needed.
|
|
@@ -963,7 +1079,6 @@ async function requestCert(region, domain) {
|
|
|
963
1079
|
* 3. It must be a 2048-bit key pair.
|
|
964
1080
|
*
|
|
965
1081
|
* See: https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/private-content-trusted-signers.html#private-content-creating-cloudfront-key-pairs
|
|
966
|
-
*
|
|
967
1082
|
* @returns A new signing key.
|
|
968
1083
|
*/
|
|
969
1084
|
function generateSigningKey() {
|
|
@@ -1029,31 +1144,37 @@ aws
|
|
|
1029
1144
|
aws.command('update-server').description('Update the server image').argument('<tag>').action(updateServerCommand);
|
|
1030
1145
|
aws.command('update-app').description('Update the app site').argument('<tag>').action(updateAppCommand);
|
|
1031
1146
|
|
|
1032
|
-
const
|
|
1147
|
+
const botSaveCommand = createMedplumCommand('save');
|
|
1148
|
+
const botDeployCommand = createMedplumCommand('deploy');
|
|
1149
|
+
const botCreateCommand = createMedplumCommand('create');
|
|
1150
|
+
const bot = new commander.Command('bot')
|
|
1151
|
+
.addCommand(botSaveCommand)
|
|
1152
|
+
.addCommand(botDeployCommand)
|
|
1153
|
+
.addCommand(botCreateCommand);
|
|
1033
1154
|
// Commands to deprecate
|
|
1034
|
-
const saveBotDeprecate =
|
|
1035
|
-
const deployBotDeprecate =
|
|
1036
|
-
const createBotDeprecate =
|
|
1037
|
-
|
|
1038
|
-
.command('save')
|
|
1155
|
+
const saveBotDeprecate = createMedplumCommand('save-bot');
|
|
1156
|
+
const deployBotDeprecate = createMedplumCommand('deploy-bot');
|
|
1157
|
+
const createBotDeprecate = createMedplumCommand('create-bot');
|
|
1158
|
+
botSaveCommand
|
|
1039
1159
|
.description('Saving the bot')
|
|
1040
1160
|
.argument('<botName>')
|
|
1041
|
-
.action(async (botName) => {
|
|
1042
|
-
await
|
|
1161
|
+
.action(async (botName, options) => {
|
|
1162
|
+
const medplum = await createMedplumClient(options);
|
|
1163
|
+
await botWrapper(medplum, botName);
|
|
1043
1164
|
});
|
|
1044
|
-
|
|
1045
|
-
.command('deploy')
|
|
1165
|
+
botDeployCommand
|
|
1046
1166
|
.description('Deploy the app to AWS')
|
|
1047
1167
|
.argument('<botName>')
|
|
1048
|
-
.action(async (botName) => {
|
|
1049
|
-
|
|
1168
|
+
.action(async (botName, options) => {
|
|
1169
|
+
const medplum = await createMedplumClient(options);
|
|
1170
|
+
await botWrapper(medplum, botName, true);
|
|
1050
1171
|
});
|
|
1051
|
-
|
|
1052
|
-
.command('create')
|
|
1172
|
+
botCreateCommand
|
|
1053
1173
|
.arguments('<botName> <projectId> <sourceFile> <distFile>')
|
|
1054
1174
|
.description('Creating a bot')
|
|
1055
|
-
.action(async (botName, projectId, sourceFile, distFile) => {
|
|
1056
|
-
|
|
1175
|
+
.action(async (botName, projectId, sourceFile, distFile, options) => {
|
|
1176
|
+
const medplum = await createMedplumClient(options);
|
|
1177
|
+
await createBot(medplum, [botName, projectId, sourceFile, distFile]);
|
|
1057
1178
|
});
|
|
1058
1179
|
async function botWrapper(medplum, botName, deploy = false) {
|
|
1059
1180
|
const botConfigs = readBotConfigs(botName);
|
|
@@ -1070,55 +1191,63 @@ async function botWrapper(medplum, botName, deploy = false) {
|
|
|
1070
1191
|
saveBotDeprecate
|
|
1071
1192
|
.description('Saves the bot')
|
|
1072
1193
|
.argument('<botName>')
|
|
1073
|
-
.action(async (botName) => {
|
|
1074
|
-
await
|
|
1194
|
+
.action(async (botName, options) => {
|
|
1195
|
+
const medplum = await createMedplumClient(options);
|
|
1196
|
+
await botWrapper(medplum, botName);
|
|
1075
1197
|
});
|
|
1076
1198
|
deployBotDeprecate
|
|
1077
1199
|
.description('Deploy the bot to AWS')
|
|
1078
1200
|
.argument('<botName>')
|
|
1079
|
-
.action(async (botName) => {
|
|
1080
|
-
|
|
1201
|
+
.action(async (botName, options) => {
|
|
1202
|
+
const medplum = await createMedplumClient(options);
|
|
1203
|
+
await botWrapper(medplum, botName, true);
|
|
1081
1204
|
});
|
|
1082
1205
|
createBotDeprecate
|
|
1083
1206
|
.arguments('<botName> <projectId> <sourceFile> <distFile>')
|
|
1084
1207
|
.description('Creates and saves the bot')
|
|
1085
|
-
.action(async (botName, projectId, sourceFile, distFile) => {
|
|
1086
|
-
|
|
1208
|
+
.action(async (botName, projectId, sourceFile, distFile, options) => {
|
|
1209
|
+
const medplum = await createMedplumClient(options);
|
|
1210
|
+
await createBot(medplum, [botName, projectId, sourceFile, distFile]);
|
|
1087
1211
|
});
|
|
1088
1212
|
|
|
1089
|
-
const
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1213
|
+
const bulkExportCommand = createMedplumCommand('export');
|
|
1214
|
+
const bulkImportCommand = createMedplumCommand('import');
|
|
1215
|
+
const bulk = new commander.Command('bulk').addCommand(bulkExportCommand).addCommand(bulkImportCommand);
|
|
1216
|
+
bulkExportCommand
|
|
1217
|
+
.option('-e, --export-level <exportLevel>', 'Optional export level. Defaults to system level export. "Group/:id" - Group of Patients, "Patient" - All Patients.')
|
|
1093
1218
|
.option('-t, --types <types>', 'optional resource types to export')
|
|
1094
1219
|
.option('-s, --since <since>', 'optional Resources will be included in the response if their state has changed after the supplied time (e.g. if Resource.meta.lastUpdated is later than the supplied _since time).')
|
|
1095
|
-
.action(async (
|
|
1096
|
-
const
|
|
1220
|
+
.action(async (options) => {
|
|
1221
|
+
const { exportLevel, types, since } = options;
|
|
1222
|
+
const medplum = await createMedplumClient(options);
|
|
1223
|
+
const response = await medplum.bulkExport(exportLevel, types, since);
|
|
1097
1224
|
response.output?.forEach(async ({ type, url }) => {
|
|
1098
|
-
const
|
|
1099
|
-
const
|
|
1225
|
+
const fileUrl = new URL(url);
|
|
1226
|
+
const data = await medplum.download(url);
|
|
1227
|
+
const fileName = `${type}_${fileUrl.pathname}`.replace(/[^a-zA-Z0-9]+/g, '_') + '.ndjson';
|
|
1100
1228
|
fs.writeFile(`${fileName}`, await data.text(), () => {
|
|
1101
1229
|
console.log(`${fileName} is created`);
|
|
1102
1230
|
});
|
|
1103
1231
|
});
|
|
1104
1232
|
});
|
|
1105
|
-
|
|
1106
|
-
.command('import')
|
|
1233
|
+
bulkImportCommand
|
|
1107
1234
|
.argument('<filename>', 'File Name')
|
|
1108
|
-
.option('--
|
|
1235
|
+
.option('--num-resources-per-request <numResourcesPerRequest>', 'optional number of resources to import per batch request. Defaults to 25.', '25')
|
|
1236
|
+
.option('--add-extensions-for-missing-values', 'optional flag to add extensions for missing values in a resource', false)
|
|
1109
1237
|
.action(async (fileName, options) => {
|
|
1110
1238
|
const path$1 = path.resolve(process.cwd(), fileName);
|
|
1111
|
-
const { numResourcesPerRequest } = options;
|
|
1112
|
-
await
|
|
1239
|
+
const { numResourcesPerRequest, addExtensionsForMissingValues } = options;
|
|
1240
|
+
const medplum = await createMedplumClient(options);
|
|
1241
|
+
await importFile(path$1, parseInt(numResourcesPerRequest), medplum, addExtensionsForMissingValues);
|
|
1113
1242
|
});
|
|
1114
|
-
async function importFile(path, numResourcesPerRequest) {
|
|
1243
|
+
async function importFile(path, numResourcesPerRequest, medplum, addExtensionsForMissingValues) {
|
|
1115
1244
|
let entries = [];
|
|
1116
1245
|
const fileStream = fs.createReadStream(path);
|
|
1117
1246
|
const rl = readline.createInterface({
|
|
1118
1247
|
input: fileStream,
|
|
1119
1248
|
});
|
|
1120
1249
|
for await (const line of rl) {
|
|
1121
|
-
const resource =
|
|
1250
|
+
const resource = parseResource(line, addExtensionsForMissingValues);
|
|
1122
1251
|
entries.push({
|
|
1123
1252
|
resource: resource,
|
|
1124
1253
|
request: {
|
|
@@ -1127,16 +1256,16 @@ async function importFile(path, numResourcesPerRequest) {
|
|
|
1127
1256
|
},
|
|
1128
1257
|
});
|
|
1129
1258
|
if (entries.length % numResourcesPerRequest === 0) {
|
|
1130
|
-
await sendBatchEntries(entries);
|
|
1259
|
+
await sendBatchEntries(entries, medplum);
|
|
1131
1260
|
entries = [];
|
|
1132
1261
|
}
|
|
1133
1262
|
}
|
|
1134
1263
|
if (entries.length > 0) {
|
|
1135
|
-
await sendBatchEntries(entries);
|
|
1264
|
+
await sendBatchEntries(entries, medplum);
|
|
1136
1265
|
}
|
|
1137
1266
|
}
|
|
1138
|
-
async function sendBatchEntries(entries) {
|
|
1139
|
-
const result = await
|
|
1267
|
+
async function sendBatchEntries(entries, medplum) {
|
|
1268
|
+
const result = await medplum.executeBatch({
|
|
1140
1269
|
resourceType: 'Bundle',
|
|
1141
1270
|
type: 'transaction',
|
|
1142
1271
|
entry: entries,
|
|
@@ -1145,13 +1274,53 @@ async function sendBatchEntries(entries) {
|
|
|
1145
1274
|
prettyPrint(resultEntry.response);
|
|
1146
1275
|
});
|
|
1147
1276
|
}
|
|
1277
|
+
function parseResource(jsonString, addExtensionsForMissingValues) {
|
|
1278
|
+
const resource = JSON.parse(jsonString);
|
|
1279
|
+
if (addExtensionsForMissingValues) {
|
|
1280
|
+
return addExtensionsForMissingValuesResource(resource);
|
|
1281
|
+
}
|
|
1282
|
+
return resource;
|
|
1283
|
+
}
|
|
1284
|
+
function addExtensionsForMissingValuesResource(resource) {
|
|
1285
|
+
if (resource.resourceType === 'ExplanationOfBenefit') {
|
|
1286
|
+
return addExtensionsForMissingValuesExplanationOfBenefits(resource);
|
|
1287
|
+
}
|
|
1288
|
+
return resource;
|
|
1289
|
+
}
|
|
1290
|
+
function addExtensionsForMissingValuesExplanationOfBenefits(resource) {
|
|
1291
|
+
if (!resource.provider) {
|
|
1292
|
+
resource.provider = getUnmappedExtension();
|
|
1293
|
+
}
|
|
1294
|
+
resource.item?.forEach((item) => {
|
|
1295
|
+
if (!item?.productOrService) {
|
|
1296
|
+
item.productOrService = getUnmappedExtension();
|
|
1297
|
+
}
|
|
1298
|
+
});
|
|
1299
|
+
return resource;
|
|
1300
|
+
}
|
|
1301
|
+
function getUnmappedExtension() {
|
|
1302
|
+
return {
|
|
1303
|
+
extension: [
|
|
1304
|
+
{
|
|
1305
|
+
url: 'https://g.co/unmapped-by-bcda',
|
|
1306
|
+
valueString: 'This is a required FHIR R4 Field, but not mapped by BCDA, which is why we expect it to be empty.',
|
|
1307
|
+
},
|
|
1308
|
+
],
|
|
1309
|
+
};
|
|
1310
|
+
}
|
|
1148
1311
|
|
|
1149
|
-
const
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1312
|
+
const projectListCommand = createMedplumCommand('list');
|
|
1313
|
+
const projectCurrentCommand = createMedplumCommand('current');
|
|
1314
|
+
const projectSwitchCommand = createMedplumCommand('switch');
|
|
1315
|
+
const projectInviteCommand = createMedplumCommand('invite');
|
|
1316
|
+
const project = new commander.Command('project')
|
|
1317
|
+
.addCommand(projectListCommand)
|
|
1318
|
+
.addCommand(projectCurrentCommand)
|
|
1319
|
+
.addCommand(projectSwitchCommand)
|
|
1320
|
+
.addCommand(projectInviteCommand);
|
|
1321
|
+
projectListCommand.description('List of current projects').action(async (options) => {
|
|
1322
|
+
const medplum = await createMedplumClient(options);
|
|
1323
|
+
projectList(medplum);
|
|
1155
1324
|
});
|
|
1156
1325
|
function projectList(medplum) {
|
|
1157
1326
|
const logins = medplum.getLogins();
|
|
@@ -1160,25 +1329,22 @@ function projectList(medplum) {
|
|
|
1160
1329
|
.join('\n\n');
|
|
1161
1330
|
console.log(projects);
|
|
1162
1331
|
}
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
.action(() => {
|
|
1167
|
-
const login = exports.medplum.getActiveLogin();
|
|
1332
|
+
projectCurrentCommand.description('Project you are currently on').action(async (options) => {
|
|
1333
|
+
const medplum = await createMedplumClient(options);
|
|
1334
|
+
const login = medplum.getActiveLogin();
|
|
1168
1335
|
if (!login) {
|
|
1169
1336
|
throw new Error('Unauthenticated: run `npx medplum login` to login');
|
|
1170
1337
|
}
|
|
1171
1338
|
console.log(`${login.project.display} (${login.project.reference})`);
|
|
1172
1339
|
});
|
|
1173
|
-
|
|
1174
|
-
.command('switch')
|
|
1340
|
+
projectSwitchCommand
|
|
1175
1341
|
.description('Switching to another project from the current one')
|
|
1176
1342
|
.argument('<projectId>')
|
|
1177
|
-
.action(async (projectId) => {
|
|
1178
|
-
await
|
|
1343
|
+
.action(async (projectId, options) => {
|
|
1344
|
+
const medplum = await createMedplumClient(options);
|
|
1345
|
+
await switchProject(medplum, projectId);
|
|
1179
1346
|
});
|
|
1180
|
-
|
|
1181
|
-
.command('invite')
|
|
1347
|
+
projectInviteCommand
|
|
1182
1348
|
.description('Invite a member to your current project (run npx medplum project current to confirm)')
|
|
1183
1349
|
.arguments('<firstName> <lastName> <email>')
|
|
1184
1350
|
.option('--send-email', 'If you want to send the email when inviting the user')
|
|
@@ -1187,7 +1353,8 @@ project
|
|
|
1187
1353
|
.choices(['Practitioner', 'Patient', 'RelatedPerson'])
|
|
1188
1354
|
.default('Practitioner'))
|
|
1189
1355
|
.action(async (firstName, lastName, email, options) => {
|
|
1190
|
-
const
|
|
1356
|
+
const medplum = await createMedplumClient(options);
|
|
1357
|
+
const login = medplum.getActiveLogin();
|
|
1191
1358
|
if (!login) {
|
|
1192
1359
|
throw new Error('Unauthenticated: run `npx medplum login` to login');
|
|
1193
1360
|
}
|
|
@@ -1203,7 +1370,7 @@ project
|
|
|
1203
1370
|
sendEmail: !!options.sendEmail,
|
|
1204
1371
|
admin: !!options.admin,
|
|
1205
1372
|
};
|
|
1206
|
-
await inviteUser(projectId, inviteBody);
|
|
1373
|
+
await inviteUser(projectId, inviteBody, medplum);
|
|
1207
1374
|
});
|
|
1208
1375
|
async function switchProject(medplum, projectId) {
|
|
1209
1376
|
const logins = medplum.getLogins();
|
|
@@ -1216,9 +1383,9 @@ async function switchProject(medplum, projectId) {
|
|
|
1216
1383
|
console.log(`Switched to project ${projectId}\n`);
|
|
1217
1384
|
}
|
|
1218
1385
|
}
|
|
1219
|
-
async function inviteUser(projectId, inviteBody) {
|
|
1386
|
+
async function inviteUser(projectId, inviteBody, medplum) {
|
|
1220
1387
|
try {
|
|
1221
|
-
await
|
|
1388
|
+
await medplum.invite(projectId, inviteBody);
|
|
1222
1389
|
if (inviteBody.sendEmail) {
|
|
1223
1390
|
console.log('Email sent');
|
|
1224
1391
|
}
|
|
@@ -1229,19 +1396,21 @@ async function inviteUser(projectId, inviteBody) {
|
|
|
1229
1396
|
}
|
|
1230
1397
|
}
|
|
1231
1398
|
|
|
1232
|
-
const deleteObject =
|
|
1233
|
-
const get =
|
|
1234
|
-
const patch =
|
|
1235
|
-
const post =
|
|
1236
|
-
const put =
|
|
1237
|
-
deleteObject.argument('<url>', 'Resource/$id').action(async (url) => {
|
|
1238
|
-
|
|
1399
|
+
const deleteObject = createMedplumCommand('delete');
|
|
1400
|
+
const get = createMedplumCommand('get');
|
|
1401
|
+
const patch = createMedplumCommand('patch');
|
|
1402
|
+
const post = createMedplumCommand('post');
|
|
1403
|
+
const put = createMedplumCommand('put');
|
|
1404
|
+
deleteObject.argument('<url>', 'Resource/$id').action(async (url, options) => {
|
|
1405
|
+
const medplum = await createMedplumClient(options);
|
|
1406
|
+
prettyPrint(await medplum.delete(cleanUrl(url)));
|
|
1239
1407
|
});
|
|
1240
1408
|
get
|
|
1241
1409
|
.argument('<url>', 'Resource/$id')
|
|
1242
1410
|
.option('--as-transaction', 'Print out the bundle as a transaction type')
|
|
1243
1411
|
.action(async (url, options) => {
|
|
1244
|
-
const
|
|
1412
|
+
const medplum = await createMedplumClient(options);
|
|
1413
|
+
const response = await medplum.get(cleanUrl(url));
|
|
1245
1414
|
if (options.asTransaction) {
|
|
1246
1415
|
prettyPrint(core.convertToTransactionBundle(response));
|
|
1247
1416
|
}
|
|
@@ -1249,14 +1418,17 @@ get
|
|
|
1249
1418
|
prettyPrint(response);
|
|
1250
1419
|
}
|
|
1251
1420
|
});
|
|
1252
|
-
patch.arguments('<url> <body>').action(async (url, body) => {
|
|
1253
|
-
|
|
1421
|
+
patch.arguments('<url> <body>').action(async (url, body, options) => {
|
|
1422
|
+
const medplum = await createMedplumClient(options);
|
|
1423
|
+
prettyPrint(await medplum.patch(cleanUrl(url), parseBody(body)));
|
|
1254
1424
|
});
|
|
1255
|
-
post.arguments('<url> <body>').action(async (url, body) => {
|
|
1256
|
-
|
|
1425
|
+
post.arguments('<url> <body>').action(async (url, body, options) => {
|
|
1426
|
+
const medplum = await createMedplumClient(options);
|
|
1427
|
+
prettyPrint(await medplum.post(cleanUrl(url), parseBody(body)));
|
|
1257
1428
|
});
|
|
1258
|
-
put.arguments('<url> <body>').action(async (url, body) => {
|
|
1259
|
-
|
|
1429
|
+
put.arguments('<url> <body>').action(async (url, body, options) => {
|
|
1430
|
+
const medplum = await createMedplumClient(options);
|
|
1431
|
+
prettyPrint(await medplum.put(cleanUrl(url), parseBody(body)));
|
|
1260
1432
|
});
|
|
1261
1433
|
function parseBody(input) {
|
|
1262
1434
|
if (!input) {
|
|
@@ -1279,51 +1451,7 @@ function cleanUrl(input) {
|
|
|
1279
1451
|
return 'fhir/R4/' + input;
|
|
1280
1452
|
}
|
|
1281
1453
|
|
|
1282
|
-
|
|
1283
|
-
constructor() {
|
|
1284
|
-
super();
|
|
1285
|
-
this.dirName = path.resolve(os.homedir(), '.medplum');
|
|
1286
|
-
this.fileName = path.resolve(this.dirName, 'credentials');
|
|
1287
|
-
}
|
|
1288
|
-
clear() {
|
|
1289
|
-
this.writeFile({});
|
|
1290
|
-
}
|
|
1291
|
-
getString(key) {
|
|
1292
|
-
return this.readFile()?.[key];
|
|
1293
|
-
}
|
|
1294
|
-
setString(key, value) {
|
|
1295
|
-
const data = this.readFile() || {};
|
|
1296
|
-
if (value) {
|
|
1297
|
-
data[key] = value;
|
|
1298
|
-
}
|
|
1299
|
-
else {
|
|
1300
|
-
delete data[key];
|
|
1301
|
-
}
|
|
1302
|
-
this.writeFile(data);
|
|
1303
|
-
}
|
|
1304
|
-
readFile() {
|
|
1305
|
-
if (fs.existsSync(this.fileName)) {
|
|
1306
|
-
return JSON.parse(fs.readFileSync(this.fileName, 'utf8'));
|
|
1307
|
-
}
|
|
1308
|
-
return undefined;
|
|
1309
|
-
}
|
|
1310
|
-
writeFile(data) {
|
|
1311
|
-
if (!fs.existsSync(this.dirName)) {
|
|
1312
|
-
fs.mkdirSync(this.dirName);
|
|
1313
|
-
}
|
|
1314
|
-
fs.writeFileSync(this.fileName, JSON.stringify(data, null, 2), 'utf8');
|
|
1315
|
-
}
|
|
1316
|
-
}
|
|
1317
|
-
|
|
1318
|
-
exports.medplum = void 0;
|
|
1319
|
-
async function main(medplumClient, argv) {
|
|
1320
|
-
exports.medplum = medplumClient;
|
|
1321
|
-
// Legacy support for MEDPLUM_CLIENT_ID and MEDPLUM_CLIENT_SECRET environment variables
|
|
1322
|
-
const clientId = process.env['MEDPLUM_CLIENT_ID'];
|
|
1323
|
-
const clientSecret = process.env['MEDPLUM_CLIENT_SECRET'];
|
|
1324
|
-
if (clientId && clientSecret) {
|
|
1325
|
-
await exports.medplum.startClientLogin(clientId, clientSecret);
|
|
1326
|
-
}
|
|
1454
|
+
async function main(argv) {
|
|
1327
1455
|
try {
|
|
1328
1456
|
const index = new commander.Command('medplum').description('Command to access Medplum CLI');
|
|
1329
1457
|
index.version(core.MEDPLUM_VERSION);
|
|
@@ -1338,7 +1466,7 @@ async function main(medplumClient, argv) {
|
|
|
1338
1466
|
index.addCommand(deleteObject);
|
|
1339
1467
|
// Project
|
|
1340
1468
|
index.addCommand(project);
|
|
1341
|
-
//
|
|
1469
|
+
// Bulk Commands
|
|
1342
1470
|
index.addCommand(bulk);
|
|
1343
1471
|
// Bot Commands
|
|
1344
1472
|
index.addCommand(bot);
|
|
@@ -1354,28 +1482,12 @@ async function main(medplumClient, argv) {
|
|
|
1354
1482
|
console.error('Error: ' + core.normalizeErrorString(err));
|
|
1355
1483
|
}
|
|
1356
1484
|
}
|
|
1357
|
-
function run() {
|
|
1485
|
+
async function run() {
|
|
1358
1486
|
dotenv.config();
|
|
1359
|
-
|
|
1360
|
-
const fhirUrlPath = process.env['MEDPLUM_FHIR_URL_PATH'] || '';
|
|
1361
|
-
const accessToken = process.env['MEDPLUM_CLIENT_ACCESS_TOKEN'] || '';
|
|
1362
|
-
const medplumClient = new core.MedplumClient({
|
|
1363
|
-
fetch,
|
|
1364
|
-
baseUrl,
|
|
1365
|
-
fhirUrlPath,
|
|
1366
|
-
storage: new FileSystemStorage(),
|
|
1367
|
-
onUnauthenticated: onUnauthenticated,
|
|
1368
|
-
});
|
|
1369
|
-
if (accessToken) {
|
|
1370
|
-
medplumClient.setAccessToken(accessToken);
|
|
1371
|
-
}
|
|
1372
|
-
main(medplumClient, process.argv).catch((err) => console.error('Unhandled error:', err));
|
|
1487
|
+
await main(process.argv);
|
|
1373
1488
|
}
|
|
1374
1489
|
if (require.main === module) {
|
|
1375
|
-
run();
|
|
1376
|
-
}
|
|
1377
|
-
function onUnauthenticated() {
|
|
1378
|
-
console.log('Unauthenticated: run `npx medplum login` to sign in');
|
|
1490
|
+
run().catch((err) => console.error('Unhandled error:', err));
|
|
1379
1491
|
}
|
|
1380
1492
|
|
|
1381
1493
|
exports.main = main;
|