@medplum/mock 0.9.1 → 0.9.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -8,6 +8,38 @@ For example:
8
8
  - `GET fhir/R4/Practitioner/123` returns Dr. Alice Smith
9
9
  - `GET fhir/R4/Organization/123` returns Test Organization
10
10
 
11
+ ## Installation
12
+
13
+ Add as a dependency:
14
+
15
+ ```bash
16
+ npm install @medplum/mock
17
+ ```
18
+
19
+ Note the following peer dependencies:
20
+ - [@medplum/core](https://www.npmjs.com/package/@medplum/core)
21
+ - [@medplum/fhirpath](https://www.npmjs.com/package/@medplum/fhirpath)
22
+
23
+ ## Usage
24
+
25
+ Create a new mock client:
26
+
27
+ ```ts
28
+ const client = new MockClient();
29
+ ```
30
+
31
+ Read a `Patient` resource:
32
+
33
+ ```ts
34
+ const patient = await client.readResource('Patient', '123');
35
+ ```
36
+
37
+ `MockClient` is API compatible with `MedplumClient` in [@medplum/core](https://www.npmjs.com/package/@medplum/core). Please refer to `MedplumClient` for full documentation on client capabilities.
38
+
39
+ ## About Medplum
40
+
41
+ Medplum is a healthcare platform that helps you quickly develop high-quality compliant applications. Medplum includes a FHIR server, React component library, and developer app.
42
+
11
43
  ## License
12
44
 
13
45
  Apache 2.0. Copyright © Medplum 2022
package/dist/cjs/index.js CHANGED
@@ -123,6 +123,11 @@
123
123
  id: '123',
124
124
  name: 'Test Bot',
125
125
  code: 'console.log("hello world");',
126
+ };
127
+ const ExampleClient = {
128
+ resourceType: 'ClientApplication',
129
+ id: '123',
130
+ name: 'Test Client',
126
131
  };
127
132
 
128
133
  const ExampleQuestionnaire = {
@@ -444,6 +449,7 @@
444
449
  },
445
450
  ],
446
451
  specimen: [core.createReference(HomerSimpsonSpecimen)],
452
+ authoredOn: '2020-01-01T12:00:00Z',
447
453
  };
448
454
  const BartSimpson = {
449
455
  resourceType: 'Patient',
@@ -6216,6 +6222,136 @@
6216
6222
  }
6217
6223
  ]
6218
6224
  }
6225
+ },
6226
+ {
6227
+ resourceType: "StructureDefinition",
6228
+ id: "Bot",
6229
+ name: "Bot",
6230
+ description: "Bot account for automated actions.",
6231
+ snapshot: {
6232
+ element: [
6233
+ {
6234
+ id: "Bot.id",
6235
+ path: "Bot.id",
6236
+ definition: "The logical id of the resource, as used in the URL for the resource. Once assigned, this value never changes.",
6237
+ min: 0,
6238
+ max: "1",
6239
+ type: [
6240
+ {
6241
+ code: "string"
6242
+ }
6243
+ ]
6244
+ },
6245
+ {
6246
+ id: "Bot.meta",
6247
+ path: "Bot.meta",
6248
+ definition: "The metadata about the resource. This is content that is maintained by the infrastructure. Changes to the content might not always be associated with version changes to the resource.",
6249
+ min: 0,
6250
+ max: "1",
6251
+ type: [
6252
+ {
6253
+ code: "Meta"
6254
+ }
6255
+ ]
6256
+ },
6257
+ {
6258
+ id: "Bot.implicitRules",
6259
+ path: "Bot.implicitRules",
6260
+ definition: "A reference to a set of rules that were followed when the resource was constructed, and which must be understood when processing the content. Often, this is a reference to an implementation guide that defines the special rules along with other profiles etc.",
6261
+ min: 0,
6262
+ max: "1",
6263
+ type: [
6264
+ {
6265
+ code: "uri"
6266
+ }
6267
+ ]
6268
+ },
6269
+ {
6270
+ id: "Bot.language",
6271
+ path: "Bot.language",
6272
+ definition: "The base language in which the resource is written.",
6273
+ min: 0,
6274
+ max: "1",
6275
+ type: [
6276
+ {
6277
+ code: "code"
6278
+ }
6279
+ ]
6280
+ },
6281
+ {
6282
+ id: "Bot.name",
6283
+ path: "Bot.name",
6284
+ definition: "A name associated with the Bot.",
6285
+ min: 0,
6286
+ max: "1",
6287
+ type: [
6288
+ {
6289
+ code: "string"
6290
+ }
6291
+ ]
6292
+ },
6293
+ {
6294
+ id: "Bot.description",
6295
+ path: "Bot.description",
6296
+ definition: "A summary, characterization or explanation of the Bot.",
6297
+ min: 0,
6298
+ max: "1",
6299
+ type: [
6300
+ {
6301
+ code: "string"
6302
+ }
6303
+ ]
6304
+ },
6305
+ {
6306
+ id: "Bot.runtimeVersion",
6307
+ path: "Bot.runtimeVersion",
6308
+ definition: "The identifier of the bot runtime environment (i.e., vmcontext, awslambda, etc).",
6309
+ min: 0,
6310
+ max: "1",
6311
+ type: [
6312
+ {
6313
+ code: "string"
6314
+ }
6315
+ ]
6316
+ },
6317
+ {
6318
+ id: "Bot.photo",
6319
+ path: "Bot.photo",
6320
+ definition: "Image of the bot.",
6321
+ min: 0,
6322
+ max: "1",
6323
+ type: [
6324
+ {
6325
+ code: "Attachment"
6326
+ }
6327
+ ]
6328
+ },
6329
+ {
6330
+ id: "Bot.code",
6331
+ path: "Bot.code",
6332
+ definition: "Bot logic script.",
6333
+ min: 0,
6334
+ max: "1",
6335
+ type: [
6336
+ {
6337
+ code: "string"
6338
+ }
6339
+ ]
6340
+ },
6341
+ {
6342
+ id: "Bot.runAsUser",
6343
+ path: "Bot.runAsUser",
6344
+ definition: "Optional flag to indicate that the bot should be run as the user.",
6345
+ min: 0,
6346
+ max: "1",
6347
+ type: [
6348
+ {
6349
+ code: "boolean"
6350
+ }
6351
+ ]
6352
+ }
6353
+ ]
6354
+ }
6219
6355
  }
6220
6356
  ];
6221
6357
 
@@ -6311,6 +6447,14 @@
6311
6447
  expression: 'ServiceRequest.subject',
6312
6448
  target: ['Group', 'Device', 'Patient', 'Location'],
6313
6449
  },
6450
+ {
6451
+ resourceType: 'SearchParameter',
6452
+ id: 'ServiceRequest-authored',
6453
+ code: 'authored',
6454
+ base: ['ServiceRequest'],
6455
+ type: 'date',
6456
+ expression: 'ServiceRequest.authoredOn',
6457
+ },
6314
6458
  {
6315
6459
  resourceType: 'SearchParameter',
6316
6460
  id: 'Observation-value-quantity',
@@ -6968,6 +7112,23 @@
6968
7112
  eval(context) {
6969
7113
  return this.impl(ensureArray(context), ...this.args);
6970
7114
  }
7115
+ }
7116
+ class IndexerAtom {
7117
+ constructor(left, expr) {
7118
+ this.left = left;
7119
+ this.expr = expr;
7120
+ }
7121
+ eval(context) {
7122
+ const index = this.expr.eval(context);
7123
+ if (typeof index !== 'number') {
7124
+ throw new Error(`Invalid indexer expression: should return integer}`);
7125
+ }
7126
+ const leftResult = this.left.eval(context);
7127
+ if (!(index in leftResult)) {
7128
+ return [];
7129
+ }
7130
+ return leftResult[index];
7131
+ }
6971
7132
  }
6972
7133
 
6973
7134
  function parseDateString(str) {
@@ -7230,7 +7391,7 @@
7230
7391
  * See: http://hl7.org/fhirpath/#selectprojection-expression-collection
7231
7392
  */
7232
7393
  function select(input, criteria) {
7233
- return ensureArray(criteria.eval(input));
7394
+ return ensureArray(input.map((e) => criteria.eval(e)).flat());
7234
7395
  }
7235
7396
  /**
7236
7397
  * A version of select that will repeat the projection and add it to the output
@@ -8315,7 +8476,11 @@
8315
8476
  refStr = e;
8316
8477
  }
8317
8478
  else if (typeof e === 'object') {
8318
- refStr = e.reference;
8479
+ const ref = e;
8480
+ if (ref.resource) {
8481
+ return ref.resource;
8482
+ }
8483
+ refStr = ref.reference;
8319
8484
  }
8320
8485
  if (!refStr) {
8321
8486
  return undefined;
@@ -8685,7 +8850,7 @@
8685
8850
  prefix(tokenType, precedence, builder) {
8686
8851
  return this.registerPrefix(tokenType, {
8687
8852
  parse(parser, token) {
8688
- const right = parser.parse(precedence);
8853
+ const right = parser.consumeAndParse(precedence);
8689
8854
  return builder(token, right);
8690
8855
  },
8691
8856
  });
@@ -8693,7 +8858,7 @@
8693
8858
  infixLeft(tokenType, precedence, builder) {
8694
8859
  return this.registerInfix(tokenType, {
8695
8860
  parse(parser, left, token) {
8696
- const right = parser.parse(precedence);
8861
+ const right = parser.consumeAndParse(precedence);
8697
8862
  return builder(left, token, right);
8698
8863
  },
8699
8864
  precedence,
@@ -8722,7 +8887,7 @@
8722
8887
  __classPrivateFieldGet(this, _Parser_instances, "m", _Parser_consume).call(this);
8723
8888
  return true;
8724
8889
  }
8725
- parse(precedence = 100 /* MaximumPrecedence */) {
8890
+ consumeAndParse(precedence = 100 /* MaximumPrecedence */) {
8726
8891
  const token = __classPrivateFieldGet(this, _Parser_instances, "m", _Parser_consume).call(this);
8727
8892
  const prefix = __classPrivateFieldGet(this, _Parser_prefixParselets, "f")[token.id];
8728
8893
  if (!prefix) {
@@ -8757,13 +8922,23 @@
8757
8922
  };
8758
8923
  const PARENTHESES_PARSELET = {
8759
8924
  parse(parser) {
8760
- const expr = parser.parse();
8925
+ const expr = parser.consumeAndParse();
8761
8926
  if (!parser.match(')')) {
8762
8927
  throw new Error('Parse error: expected `)`');
8763
8928
  }
8764
8929
  return expr;
8765
8930
  },
8766
8931
  };
8932
+ const INDEXER_PARSELET = {
8933
+ parse(parser, left) {
8934
+ const expr = parser.consumeAndParse();
8935
+ if (!parser.match(']')) {
8936
+ throw new Error('Parse error: expected `]`');
8937
+ }
8938
+ return new IndexerAtom(left, expr);
8939
+ },
8940
+ precedence: 2 /* Indexer */,
8941
+ };
8767
8942
  const FUNCTION_CALL_PARSELET = {
8768
8943
  parse(parser, left) {
8769
8944
  if (!(left instanceof SymbolAtom)) {
@@ -8774,7 +8949,7 @@
8774
8949
  }
8775
8950
  const args = [];
8776
8951
  while (!parser.match(')')) {
8777
- args.push(parser.parse());
8952
+ args.push(parser.consumeAndParse());
8778
8953
  parser.match(',');
8779
8954
  }
8780
8955
  return new FunctionAtom(left.name, args, functions[left.name]);
@@ -8819,6 +8994,7 @@
8819
8994
  })
8820
8995
  .registerPrefix('{}', { parse: () => new EmptySetAtom() })
8821
8996
  .registerPrefix('(', PARENTHESES_PARSELET)
8997
+ .registerInfix('[', INDEXER_PARSELET)
8822
8998
  .registerInfix('(', FUNCTION_CALL_PARSELET)
8823
8999
  .prefix('+', 3 /* UnaryAdd */, (_, right) => new UnaryOperatorAtom(right, (x) => x))
8824
9000
  .prefix('-', 3 /* UnarySubtract */, (_, right) => new ArithemticOperatorAtom(right, right, (_, y) => -y))
@@ -8871,7 +9047,7 @@
8871
9047
  */
8872
9048
  function parseFhirPath(input) {
8873
9049
  try {
8874
- return new FhirPathAtom(input, parserBuilder.construct(input).parse());
9050
+ return new FhirPathAtom(input, parserBuilder.construct(input).consumeAndParse());
8875
9051
  }
8876
9052
  catch (error) {
8877
9053
  throw new Error(`FhirPathError on "${input}": ${error}`);
@@ -8951,6 +9127,12 @@
8951
9127
  total: result.length,
8952
9128
  };
8953
9129
  }
9130
+ deleteResource(resourceType, id) {
9131
+ var _a, _b;
9132
+ if ((_b = (_a = __classPrivateFieldGet$1(this, _MemoryRepository_resources, "f")) === null || _a === void 0 ? void 0 : _a[resourceType]) === null || _b === void 0 ? void 0 : _b[id]) {
9133
+ delete __classPrivateFieldGet$1(this, _MemoryRepository_resources, "f")[resourceType][id];
9134
+ }
9135
+ }
8954
9136
  generateId() {
8955
9137
  return Date.now().toString(36);
8956
9138
  }
@@ -8968,7 +9150,7 @@
8968
9150
  }
8969
9151
  if (searchRequest.filters) {
8970
9152
  for (const filter of searchRequest.filters) {
8971
- if (!matchesSearchFilter(resource, searchRequest, filter)) {
9153
+ if (!matchesSearchFilter(resource, filter)) {
8972
9154
  return false;
8973
9155
  }
8974
9156
  }
@@ -8981,7 +9163,7 @@
8981
9163
  * @param filter One of the filters of a subscription criteria.
8982
9164
  * @returns True if the resource satisfies the search filter.
8983
9165
  */
8984
- function matchesSearchFilter(resource, searchRequest, filter) {
9166
+ function matchesSearchFilter(resource, filter) {
8985
9167
  const expression = filter.code.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
8986
9168
  const values = evalFhirPath(expression, resource);
8987
9169
  const result = values.some((value) => JSON.stringify(value).toLowerCase().includes(filter.value.toLowerCase()));
@@ -8994,19 +9176,15 @@
8994
9176
  baseUrl: 'https://example.com/',
8995
9177
  clientId: 'my-client-id',
8996
9178
  fetch: (url, options) => {
8997
- const method = options.method;
9179
+ const method = options.method || 'GET';
8998
9180
  const path = url.replace('https://example.com/', '');
8999
9181
  if (clientOptions === null || clientOptions === void 0 ? void 0 : clientOptions.debug) {
9000
9182
  console.log('MockClient', method, path);
9001
9183
  }
9002
- const result = mockHandler(method, path, options);
9003
- if ((clientOptions === null || clientOptions === void 0 ? void 0 : clientOptions.debug) && !result) {
9184
+ const response = mockHandler(method, path, options);
9185
+ if ((clientOptions === null || clientOptions === void 0 ? void 0 : clientOptions.debug) && !response) {
9004
9186
  console.log('MockClient: not found', method, path);
9005
9187
  }
9006
- const response = Object.assign({ request: {
9007
- url,
9008
- options,
9009
- } }, result);
9010
9188
  if (clientOptions === null || clientOptions === void 0 ? void 0 : clientOptions.debug) {
9011
9189
  console.log('MockClient', JSON.stringify(response, null, 2));
9012
9190
  }
@@ -9039,7 +9217,7 @@
9039
9217
  }
9040
9218
  return super.getActiveLogin();
9041
9219
  }
9042
- createBinary(data, filename, contentType) {
9220
+ createBinary(_data, filename, contentType) {
9043
9221
  if (filename.endsWith('.exe')) {
9044
9222
  return Promise.reject(core.badRequest('Invalid file type'));
9045
9223
  }
@@ -9065,13 +9243,14 @@
9065
9243
  return null;
9066
9244
  }
9067
9245
  }
9068
- function mockAdminHandler(method, path) {
9246
+ function mockAdminHandler(_method, path) {
9069
9247
  if (path.startsWith('admin/projects/123')) {
9070
9248
  return {
9071
9249
  project: { id: '123', name: 'Project 123' },
9072
9250
  members: [
9073
9251
  { id: '123', profile: { reference: 'Practitioner/123', display: 'Alice Smith' }, role: 'owner' },
9074
9252
  { id: '888', profile: { reference: 'ClientApplication/123', display: 'Test Client' }, role: 'client' },
9253
+ { id: '999', profile: { reference: 'Bot/123', display: 'Test Bot' }, role: 'bot' },
9075
9254
  ],
9076
9255
  };
9077
9256
  }
@@ -9095,9 +9274,14 @@
9095
9274
  if (path.startsWith('auth/resetpassword')) {
9096
9275
  return mockResetPasswordHandler(method, path, options);
9097
9276
  }
9277
+ if (path.startsWith('auth/me')) {
9278
+ return {
9279
+ profile: { reference: 'Practitioner/123' },
9280
+ };
9281
+ }
9098
9282
  return null;
9099
9283
  }
9100
- function mockChangePasswordHandler(method, path, options) {
9284
+ function mockChangePasswordHandler(_method, _path, options) {
9101
9285
  const { body } = options;
9102
9286
  const { oldPassword } = JSON.parse(body);
9103
9287
  if (oldPassword === 'orange') {
@@ -9117,7 +9301,7 @@
9117
9301
  };
9118
9302
  }
9119
9303
  }
9120
- function mockLoginHandler(method, path, options) {
9304
+ function mockLoginHandler(_method, _path, options) {
9121
9305
  const { body } = options;
9122
9306
  const { password } = JSON.parse(body);
9123
9307
  if (password === 'password') {
@@ -9139,7 +9323,7 @@
9139
9323
  };
9140
9324
  }
9141
9325
  }
9142
- function mockSetPasswordHandler(method, path, options) {
9326
+ function mockSetPasswordHandler(_method, _path, options) {
9143
9327
  const { body } = options;
9144
9328
  const { password } = JSON.parse(body);
9145
9329
  if (password === 'orange') {
@@ -9159,7 +9343,7 @@
9159
9343
  };
9160
9344
  }
9161
9345
  }
9162
- function mockRegisterHandler(method, path, options) {
9346
+ function mockRegisterHandler(_method, _path, options) {
9163
9347
  const { body } = options;
9164
9348
  const { email, password } = JSON.parse(body);
9165
9349
  if (email === 'george@example.com' && password === 'password') {
@@ -9178,7 +9362,7 @@
9178
9362
  };
9179
9363
  }
9180
9364
  }
9181
- function mockResetPasswordHandler(method, path, options) {
9365
+ function mockResetPasswordHandler(_method, _path, options) {
9182
9366
  const { body } = options;
9183
9367
  const { email } = JSON.parse(body);
9184
9368
  if (email === 'admin@example.com') {
@@ -9205,6 +9389,7 @@
9205
9389
  mockRepo.createResource(ExampleStatusValueSet);
9206
9390
  mockRepo.createResource(ExampleUserConfiguration);
9207
9391
  mockRepo.createResource(ExampleBot);
9392
+ mockRepo.createResource(ExampleClient);
9208
9393
  mockRepo.createResource(HomerDiagnosticReport);
9209
9394
  mockRepo.createResource(HomerEncounter);
9210
9395
  mockRepo.createResource(HomerCommunication);
@@ -9238,7 +9423,7 @@
9238
9423
  return exampleValueSet;
9239
9424
  }
9240
9425
  else if (path === 'fhir/R4/%24graphql') {
9241
- return GraphQLSchemaResponse;
9426
+ return mockFhirGraphqlHandler(method, path, options);
9242
9427
  }
9243
9428
  else if (path === 'not-found' || id === 'not-found') {
9244
9429
  return core.notFound;
@@ -9268,8 +9453,36 @@
9268
9453
  else if (method === 'PUT') {
9269
9454
  return mockRepo.createResource(JSON.parse(options.body));
9270
9455
  }
9456
+ else if (method === 'DELETE') {
9457
+ if (resourceType && id) {
9458
+ return mockRepo.deleteResource(resourceType, id);
9459
+ }
9460
+ else {
9461
+ return core.notFound;
9462
+ }
9463
+ }
9464
+ }
9465
+ function mockFhirGraphqlHandler(_method, _path, options) {
9466
+ const { body } = options;
9467
+ if (body.includes('ResourceList: ServiceRequestList')) {
9468
+ return {
9469
+ data: {
9470
+ ResourceList: [
9471
+ Object.assign(Object.assign({}, HomerServiceRequest), { ObservationList: [
9472
+ HomerObservation1,
9473
+ HomerObservation2,
9474
+ HomerObservation3,
9475
+ HomerObservation4,
9476
+ HomerObservation5,
9477
+ HomerObservation6,
9478
+ ] }),
9479
+ ],
9480
+ },
9481
+ };
9482
+ }
9483
+ return GraphQLSchemaResponse;
9271
9484
  }
9272
- function mockFhirBatchHandler(method, path, options) {
9485
+ function mockFhirBatchHandler(_method, _path, options) {
9273
9486
  var _a;
9274
9487
  const { body } = options;
9275
9488
  const request = JSON.parse(body);
@@ -9303,6 +9516,7 @@
9303
9516
  exports.ExampleAccessPolicy = ExampleAccessPolicy;
9304
9517
  exports.ExampleAuditEvent = ExampleAuditEvent;
9305
9518
  exports.ExampleBot = ExampleBot;
9519
+ exports.ExampleClient = ExampleClient;
9306
9520
  exports.ExampleQuestionnaire = ExampleQuestionnaire;
9307
9521
  exports.ExampleQuestionnaireResponse = ExampleQuestionnaireResponse;
9308
9522
  exports.ExampleStatusValueSet = ExampleStatusValueSet;