@crvouga/mockingbird-service-medplum 0.1.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 ADDED
@@ -0,0 +1,193 @@
1
+ # @crvouga/mockingbird-service-medplum
2
+
3
+ Stateful Medplum "mock" that runs the **real [Medplum](https://www.medplum.com/) server** rather
4
+ than reimplementing its FHIR API. It self-hosts a pinned Medplum build as a child process on
5
+ embedded Postgres and an in-memory Redis, and exposes it through the Mockingbird `FetchAPI`
6
+ contract (`fetch(Request) -> Response`) plus explicit `start` / `reset` / `stop` lifecycle calls.
7
+
8
+ Use it for integration tests that need genuine Medplum/FHIR behaviour (search, validation,
9
+ auth, versioning) without a hosted Medplum project. Because it runs the real server, the full
10
+ Medplum API is available; the vendored contract used for parity covers healthcheck, the login and
11
+ token flow, and Patient search/create/read/update/delete
12
+ ([SUPPORT.md](https://github.com/crvouga/mockingbird/blob/main/packages/service/medplum/SUPPORT.md)).
13
+
14
+ Costs to know before choosing it: the **first** `start()` clones and builds Medplum (several
15
+ minutes, network required); every `start()` boots Postgres, Redis and the server (seconds, not
16
+ milliseconds). Share one instance per test file or suite, not per test.
17
+
18
+ ## Install
19
+
20
+ ```bash
21
+ npm install -D @crvouga/mockingbird-service-medplum
22
+ ```
23
+
24
+ Requirements:
25
+
26
+ - Node.js >= 22.18 (`engines`). Also used under Bun by this repo's own tests.
27
+ - `git` and `npm` on `PATH` for the one-time clone and build
28
+ (`git clone` -> `npm ci` -> `npm run build:fast`, following Medplum's
29
+ [install-from-scratch](https://www.medplum.com/docs/self-hosting/install-from-scratch) flow).
30
+ - `make` for the one-time `redis-server` compile done by `redis-memory-server` (the binary is cached
31
+ afterwards).
32
+ - Postinstall scripts of `embedded-postgres` and `redis-memory-server` must run:
33
+ - **Bun**: list both under `trustedDependencies` in your root `package.json`.
34
+ - **pnpm**: approve them once with `pnpm approve-builds`, or set `REDISMS_DISABLE_POSTINSTALL=true`
35
+ to defer the Redis binary download to the first `start()`.
36
+ - Disk under `~/.cache/mockingbird/medplum-server/<version>` for the Medplum clone and build.
37
+
38
+ ## Usage
39
+
40
+ ### Lifecycle
41
+
42
+ | Call | Behaviour |
43
+ | --- | --- |
44
+ | `await createMedplumAPI(options?)` | Construct a `MedplumAPI` and `await start()` it. |
45
+ | `await medplum.start()` | Pick free ports; boot embedded Postgres (database `medplum`) and Redis; ensure the Medplum build exists in the cache (clone/build on first run); write a per-run `medplum.config.json` into a temp dir; spawn `packages/server/dist/index.js` with the current runtime (`process.execPath`); poll `GET /healthcheck` (up to 5 minutes). Idempotent and safe to call concurrently. |
46
+ | `medplum.fetch(request)` | Proxy the request to the running server. Only the path and query of `request.url` are used, so any origin works. Redirects are returned, not followed. |
47
+ | `await medplum.reset()` | Stop the server, drop and recreate the `medplum` database, restart and wait for healthy. The server re-runs migrations and seeding on boot, so state is pristine. Clears the cached access token. Throws if not started. |
48
+ | `await medplum.stop()` | SIGTERM the server (SIGKILL after 10s), stop Redis and Postgres, delete the temp dir. No-op if not started. |
49
+
50
+ A process exit / SIGINT / SIGTERM guard kills the child processes if your test runner dies. The
51
+ first run's clone/build steps have timeouts of 10 minutes (clone), 30 minutes (`npm ci`) and 60
52
+ minutes (build); progress goes to `onLog` and to `<cacheRoot>/<version>.log`.
53
+
54
+ ### Seeded credentials and tokens
55
+
56
+ Each boot seeds Medplum's example project and super admin (`admin@example.com` /
57
+ `medplum_admin`, exported as `SUPER_ADMIN_EMAIL` / `SUPER_ADMIN_PASSWORD`) and a
58
+ `ClientApplication` for the mock's fixed client id. `getAccessToken()` logs in with those
59
+ credentials (`POST /auth/login`, PKCE `plain`) and exchanges the code at `POST /oauth2/token`,
60
+ returning a bearer token that is cached until `reset()` or `stop()`. Send it as
61
+ `Authorization: Bearer <token>`.
62
+
63
+ ### Example
64
+
65
+ ```ts
66
+ import { createMedplumAPI } from "@crvouga/mockingbird-service-medplum"
67
+
68
+ const medplum = await createMedplumAPI({ onLog: (message) => console.log(message) })
69
+ try {
70
+ const token = await medplum.getAccessToken()
71
+ const headers = { authorization: `Bearer ${token}`, "content-type": "application/fhir+json" }
72
+
73
+ // Direct HTTP against the internal base URL (http://127.0.0.1:<apiPort>/):
74
+ const created = await fetch(new URL("/fhir/R4/Patient", medplum.getBaseUrl()), {
75
+ method: "POST",
76
+ headers,
77
+ body: JSON.stringify({ resourceType: "Patient", name: [{ given: ["Ada"], family: "Lovelace" }] }),
78
+ })
79
+ const patient = (await created.json()) as { id: string }
80
+
81
+ // Through the FetchAPI contract (any origin; path and query are forwarded):
82
+ const read = await medplum.fetch(
83
+ new Request(`https://medplum.test/fhir/R4/Patient/${patient.id}`, { headers }),
84
+ )
85
+ console.log(read.status) // 200
86
+ } finally {
87
+ await medplum.stop()
88
+ }
89
+ ```
90
+
91
+ ### Pointing the Medplum SDK (or any client) at it
92
+
93
+ The server listens on `http://127.0.0.1:<apiPort>/` (`getBaseUrl()`), with CORS open
94
+ (`allowedOrigins: "*"`) and rate limits disabled. For `@medplum/core`'s `MedplumClient`, pass that
95
+ URL as `baseUrl` and sign in with the seeded super admin, or hand it the token:
96
+
97
+ ```js
98
+ import { MedplumClient } from "@medplum/core"
99
+
100
+ const client = new MedplumClient({ baseUrl: medplum.getBaseUrl() })
101
+ client.setAccessToken(await medplum.getAccessToken())
102
+ await client.searchResources("Patient")
103
+ ```
104
+
105
+ ### Resetting between tests
106
+
107
+ `reset()` restarts the server on a fresh database, so it costs roughly one server boot. Prefer
108
+ unique data per test and reset between files or suites:
109
+
110
+ ```ts
111
+ import { afterAll, beforeAll, expect, test } from "bun:test"
112
+ import { MedplumAPI } from "@crvouga/mockingbird-service-medplum"
113
+
114
+ const medplum = new MedplumAPI()
115
+ beforeAll(() => medplum.start(), 15 * 60 * 1000) // first run builds Medplum
116
+ afterAll(() => medplum.stop())
117
+
118
+ test("healthcheck", async () => {
119
+ const response = await medplum.fetch(new Request("http://medplum.test/healthcheck"))
120
+ expect(response.status).toBe(200)
121
+ })
122
+
123
+ test("starts clean after reset", async () => {
124
+ await medplum.reset()
125
+ const token = await medplum.getAccessToken()
126
+ const response = await medplum.fetch(
127
+ new Request("http://medplum.test/fhir/R4/Patient?name=Lovelace", {
128
+ headers: { authorization: `Bearer ${token}` },
129
+ }),
130
+ )
131
+ expect(response.status).toBe(200)
132
+ }, 5 * 60 * 1000)
133
+ ```
134
+
135
+ ## API
136
+
137
+ | Export | Description |
138
+ | --- | --- |
139
+ | `MedplumAPI` | Class. `new MedplumAPI(options?)`; implements `FetchAPI`. Does not start anything until `start()`. |
140
+ | `createMedplumAPI` | `(options?) => Promise<MedplumAPI>` — construct and `start()`. |
141
+ | `SUPER_ADMIN_EMAIL` | `"admin@example.com"` — seeded super admin email. |
142
+ | `SUPER_ADMIN_PASSWORD` | `"medplum_admin"` — seeded super admin password. |
143
+ | `buildServerConfig` | `(input: { apiPort, dbPort, redisPort, dataDir, superAdminEmail?, superAdminPassword? }) => MedplumServerConfig` — the `medplum.config.json` the mock writes. |
144
+ | `resolveMedplumPaths` | `(options?: { version?, cacheDir? }) => MedplumPaths` — where the clone, server entry and build marker live. |
145
+
146
+ `MedplumAPI` members:
147
+
148
+ | Member | Description |
149
+ | --- | --- |
150
+ | `start()` / `stop()` / `reset()` | Lifecycle, see Usage. |
151
+ | `fetch(request)` | Proxy one request to the running server. |
152
+ | `getAccessToken()` | `Promise<string>` — cached bearer token for the seeded super admin. |
153
+ | `getBaseUrl()` | `"http://127.0.0.1:<apiPort>/"`; throws if not started. |
154
+ | `apiPort` | Port of the running server; throws if not started. |
155
+ | `isStarted` | `boolean`. |
156
+
157
+ Options and types:
158
+
159
+ ```text
160
+ type MedplumAPIOptions = {
161
+ version?: string // Medplum git tag; default MOCKINGBIRD_MEDPLUM_VERSION env, else "v5.1.37"
162
+ cacheDir?: string // cache root; default MEDPLUM_MOCK_CACHE_DIR env, else ~/.cache/mockingbird/medplum-server
163
+ onLog?: (message: string) => void // build progress and server stdout/stderr
164
+ email?: string // login for getAccessToken(); default SUPER_ADMIN_EMAIL
165
+ password?: string // default SUPER_ADMIN_PASSWORD
166
+ }
167
+ type MedplumProcessOptions = { version?; cacheDir?; onLog? }
168
+ type MedplumPaths = { version; cacheRoot; cloneDir: "<cacheRoot>/<version>"; serverEntry; buildMarker }
169
+ type MedplumProcessInfo = { apiPort; dbPort; redisPort; baseUrl; dataDir; paths; config }
170
+ type MedplumServerConfig // shape of medplum.config.json (see buildServerConfig)
171
+ ```
172
+
173
+ `email` / `password` only change which credentials `getAccessToken()` sends; the server always
174
+ seeds `SUPER_ADMIN_EMAIL` / `SUPER_ADMIN_PASSWORD`.
175
+
176
+ ## Development
177
+
178
+ For contributors to the mockingbird repo only.
179
+
180
+ Tests are property-based. The always-on properties cover config generation and port picking
181
+ without touching the network. The full self-hosted properties (server boot, self-parity between
182
+ two independent servers, reset semantics, stop/restart lifecycle) run only with
183
+ `MOCKINGBIRD_MEDPLUM_E2E=1`:
184
+
185
+ ```bash
186
+ bun test
187
+ MOCKINGBIRD_MEDPLUM_E2E=1 bun test # first run pays the clone/build cost (several minutes, logged)
188
+ ```
189
+
190
+ In this repo, the root `package.json` already lists `embedded-postgres` and `redis-memory-server`
191
+ under `trustedDependencies`.
192
+
193
+ Part of [mockingbird](https://github.com/crvouga/mockingbird) — agent integration guide: [`@crvouga/mockingbird`](https://github.com/crvouga/mockingbird/tree/main/packages/facade#readme).
@@ -0,0 +1,51 @@
1
+ export type ServerConfigInput = {
2
+ apiPort: number;
3
+ dbPort: number;
4
+ redisPort: number;
5
+ dataDir: string;
6
+ superAdminEmail?: string | undefined;
7
+ superAdminPassword?: string | undefined;
8
+ };
9
+ export type MedplumDatabaseConfig = {
10
+ host: string;
11
+ port: number;
12
+ dbname: string;
13
+ username: string;
14
+ password: string;
15
+ };
16
+ export type MedplumRedisConfig = {
17
+ host: string;
18
+ port: number;
19
+ };
20
+ export type MedplumServerConfig = {
21
+ port: number;
22
+ baseUrl: string;
23
+ appBaseUrl: string;
24
+ binaryStorage: string;
25
+ storageBaseUrl: string;
26
+ supportEmail: string;
27
+ emailProvider: "none";
28
+ botLambdaRoleArn: "";
29
+ botLambdaLayerName: string;
30
+ vmContextBotsEnabled: true;
31
+ defaultBotRuntimeVersion: "vmcontext";
32
+ allowedOrigins: "*";
33
+ introspectionEnabled: true;
34
+ rateLimitsEnabled: false;
35
+ database: MedplumDatabaseConfig;
36
+ redis: MedplumRedisConfig;
37
+ shutdownTimeoutMilliseconds: number;
38
+ defaultSuperAdminEmail?: string | undefined;
39
+ defaultSuperAdminPassword?: string | undefined;
40
+ defaultSuperAdminClientId?: string | undefined;
41
+ defaultSuperAdminClientSecret?: string | undefined;
42
+ superAdminSecurity: {
43
+ blockAdmin: false;
44
+ };
45
+ };
46
+ export declare const SUPER_ADMIN_EMAIL = "admin@example.com";
47
+ export declare const SUPER_ADMIN_PASSWORD = "medplum_admin";
48
+ export declare const SUPER_ADMIN_CLIENT_ID = "6f3f0c17-8bd1-4a56-9d5a-6b21e5b0a101";
49
+ export declare const SUPER_ADMIN_CLIENT_SECRET = "mockingbird-local-secret";
50
+ export declare const buildServerConfig: (input: ServerConfigInput) => MedplumServerConfig;
51
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,iBAAiB,GAAG;IAC9B,OAAO,EAAE,MAAM,CAAA;IACf,MAAM,EAAE,MAAM,CAAA;IACd,SAAS,EAAE,MAAM,CAAA;IACjB,OAAO,EAAE,MAAM,CAAA;IACf,eAAe,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IACpC,kBAAkB,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;CACxC,CAAA;AAED,MAAM,MAAM,qBAAqB,GAAG;IAClC,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,EAAE,MAAM,CAAA;IACd,QAAQ,EAAE,MAAM,CAAA;IAChB,QAAQ,EAAE,MAAM,CAAA;CACjB,CAAA;AAED,MAAM,MAAM,kBAAkB,GAAG;IAC/B,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,CAAA;CACb,CAAA;AAED,MAAM,MAAM,mBAAmB,GAAG;IAChC,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,MAAM,CAAA;IACf,UAAU,EAAE,MAAM,CAAA;IAClB,aAAa,EAAE,MAAM,CAAA;IACrB,cAAc,EAAE,MAAM,CAAA;IACtB,YAAY,EAAE,MAAM,CAAA;IACpB,aAAa,EAAE,MAAM,CAAA;IACrB,gBAAgB,EAAE,EAAE,CAAA;IACpB,kBAAkB,EAAE,MAAM,CAAA;IAC1B,oBAAoB,EAAE,IAAI,CAAA;IAC1B,wBAAwB,EAAE,WAAW,CAAA;IACrC,cAAc,EAAE,GAAG,CAAA;IACnB,oBAAoB,EAAE,IAAI,CAAA;IAC1B,iBAAiB,EAAE,KAAK,CAAA;IACxB,QAAQ,EAAE,qBAAqB,CAAA;IAC/B,KAAK,EAAE,kBAAkB,CAAA;IACzB,2BAA2B,EAAE,MAAM,CAAA;IACnC,sBAAsB,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IAC3C,yBAAyB,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IAC9C,yBAAyB,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IAC9C,6BAA6B,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IAClD,kBAAkB,EAAE;QAAE,UAAU,EAAE,KAAK,CAAA;KAAE,CAAA;CAC1C,CAAA;AAED,eAAO,MAAM,iBAAiB,sBAAsB,CAAA;AACpD,eAAO,MAAM,oBAAoB,kBAAkB,CAAA;AACnD,eAAO,MAAM,qBAAqB,yCAAyC,CAAA;AAC3E,eAAO,MAAM,yBAAyB,6BAA6B,CAAA;AAInE,eAAO,MAAM,iBAAiB,GAAI,OAAO,iBAAiB,KAAG,mBAsC5D,CAAA"}
package/dist/config.js ADDED
@@ -0,0 +1,46 @@
1
+ export const SUPER_ADMIN_EMAIL = "admin@example.com";
2
+ export const SUPER_ADMIN_PASSWORD = "medplum_admin";
3
+ export const SUPER_ADMIN_CLIENT_ID = "6f3f0c17-8bd1-4a56-9d5a-6b21e5b0a101";
4
+ export const SUPER_ADMIN_CLIENT_SECRET = "mockingbird-local-secret";
5
+ const SHUTDOWN_TIMEOUT_MILLISECONDS = 5000;
6
+ const HOST = "127.0.0.1";
7
+ export const buildServerConfig = (input) => {
8
+ const { apiPort, dbPort, redisPort, dataDir } = input;
9
+ const config = {
10
+ port: apiPort,
11
+ baseUrl: `http://${HOST}:${apiPort}/`,
12
+ appBaseUrl: `http://${HOST}:3000/`,
13
+ binaryStorage: `file:${dataDir}/binary/`,
14
+ storageBaseUrl: `http://${HOST}:${apiPort}/storage/`,
15
+ supportEmail: "Medplum Mock <no-reply@example.com>",
16
+ emailProvider: "none",
17
+ botLambdaRoleArn: "",
18
+ botLambdaLayerName: "medplum-bot-layer",
19
+ vmContextBotsEnabled: true,
20
+ defaultBotRuntimeVersion: "vmcontext",
21
+ allowedOrigins: "*",
22
+ introspectionEnabled: true,
23
+ rateLimitsEnabled: false,
24
+ database: {
25
+ host: HOST,
26
+ port: dbPort,
27
+ dbname: "medplum",
28
+ username: "postgres",
29
+ password: "postgres",
30
+ },
31
+ redis: {
32
+ host: HOST,
33
+ port: redisPort,
34
+ },
35
+ shutdownTimeoutMilliseconds: SHUTDOWN_TIMEOUT_MILLISECONDS,
36
+ defaultSuperAdminEmail: input.superAdminEmail ?? SUPER_ADMIN_EMAIL,
37
+ defaultSuperAdminPassword: input.superAdminPassword ?? SUPER_ADMIN_PASSWORD,
38
+ defaultSuperAdminClientId: SUPER_ADMIN_CLIENT_ID,
39
+ defaultSuperAdminClientSecret: SUPER_ADMIN_CLIENT_SECRET,
40
+ superAdminSecurity: {
41
+ blockAdmin: false,
42
+ },
43
+ };
44
+ return config;
45
+ };
46
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AA+CA,MAAM,CAAC,MAAM,iBAAiB,GAAG,mBAAmB,CAAA;AACpD,MAAM,CAAC,MAAM,oBAAoB,GAAG,eAAe,CAAA;AACnD,MAAM,CAAC,MAAM,qBAAqB,GAAG,sCAAsC,CAAA;AAC3E,MAAM,CAAC,MAAM,yBAAyB,GAAG,0BAA0B,CAAA;AACnE,MAAM,6BAA6B,GAAG,IAAI,CAAA;AAC1C,MAAM,IAAI,GAAG,WAAW,CAAA;AAExB,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC,KAAwB,EAAuB,EAAE;IACjF,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,OAAO,EAAE,GAAG,KAAK,CAAA;IACrD,MAAM,MAAM,GAAwB;QAClC,IAAI,EAAE,OAAO;QACb,OAAO,EAAE,UAAU,IAAI,IAAI,OAAO,GAAG;QACrC,UAAU,EAAE,UAAU,IAAI,QAAQ;QAClC,aAAa,EAAE,QAAQ,OAAO,UAAU;QACxC,cAAc,EAAE,UAAU,IAAI,IAAI,OAAO,WAAW;QACpD,YAAY,EAAE,qCAAqC;QACnD,aAAa,EAAE,MAAM;QACrB,gBAAgB,EAAE,EAAE;QACpB,kBAAkB,EAAE,mBAAmB;QACvC,oBAAoB,EAAE,IAAI;QAC1B,wBAAwB,EAAE,WAAW;QACrC,cAAc,EAAE,GAAG;QACnB,oBAAoB,EAAE,IAAI;QAC1B,iBAAiB,EAAE,KAAK;QACxB,QAAQ,EAAE;YACR,IAAI,EAAE,IAAI;YACV,IAAI,EAAE,MAAM;YACZ,MAAM,EAAE,SAAS;YACjB,QAAQ,EAAE,UAAU;YACpB,QAAQ,EAAE,UAAU;SACrB;QACD,KAAK,EAAE;YACL,IAAI,EAAE,IAAI;YACV,IAAI,EAAE,SAAS;SAChB;QACD,2BAA2B,EAAE,6BAA6B;QAC1D,sBAAsB,EAAE,KAAK,CAAC,eAAe,IAAI,iBAAiB;QAClE,yBAAyB,EAAE,KAAK,CAAC,kBAAkB,IAAI,oBAAoB;QAC3E,yBAAyB,EAAE,qBAAqB;QAChD,6BAA6B,EAAE,yBAAyB;QACxD,kBAAkB,EAAE;YAClB,UAAU,EAAE,KAAK;SAClB;KACF,CAAA;IACD,OAAO,MAAM,CAAA;AACf,CAAC,CAAA"}
@@ -0,0 +1,13 @@
1
+ export type EnsureBuildOptions = {
2
+ version?: string | undefined;
3
+ cacheDir?: string | undefined;
4
+ onLog?: ((message: string) => void) | undefined;
5
+ };
6
+ /**
7
+ * Ensure a pinned Medplum monorepo clone is installed and built under the
8
+ * shared cache, following the official install-from-scratch flow
9
+ * (git clone -> npm ci -> npm run build:fast). Idempotent: a marker file
10
+ * short-circuits the expensive steps.
11
+ */
12
+ export declare const ensureMedplumBuild: (options?: EnsureBuildOptions) => Promise<import("./paths.js").MedplumPaths>;
13
+ //# sourceMappingURL=ensure-medplum-build.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ensure-medplum-build.d.ts","sourceRoot":"","sources":["../src/ensure-medplum-build.ts"],"names":[],"mappings":"AAMA,MAAM,MAAM,kBAAkB,GAAG;IAC/B,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IAC5B,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IAC7B,KAAK,CAAC,EAAE,CAAC,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC,GAAG,SAAS,CAAA;CAChD,CAAA;AAkED;;;;;GAKG;AACH,eAAO,MAAM,kBAAkB,GAAU,UAAS,kBAAuB,+CAqExE,CAAA"}
@@ -0,0 +1,109 @@
1
+ import { spawn } from "node:child_process";
2
+ import { existsSync } from "node:fs";
3
+ import { appendFile, mkdir, writeFile } from "node:fs/promises";
4
+ import { join } from "node:path";
5
+ import { joinPath, resolveMedplumPaths } from "./paths.js";
6
+ const BUILD_TIMEOUT_MILLISECONDS = 60 * 60 * 1000;
7
+ const CLONE_TIMEOUT_MILLISECONDS = 10 * 60 * 1000;
8
+ const INSTALL_TIMEOUT_MILLISECONDS = 30 * 60 * 1000;
9
+ const log = (onLog, message) => {
10
+ onLog?.(message);
11
+ };
12
+ const runCommand = async (command, args, options) => {
13
+ return new Promise((resolve, reject) => {
14
+ const child = spawn(command, args, {
15
+ cwd: options.cwd,
16
+ env: process.env,
17
+ stdio: ["ignore", "pipe", "pipe"],
18
+ });
19
+ const tail = [];
20
+ const capture = (chunk) => {
21
+ const text = chunk.toString();
22
+ tail.push(text);
23
+ if (tail.length > 40)
24
+ tail.shift();
25
+ if (options.logPath) {
26
+ void appendFile(options.logPath, text).catch(() => { });
27
+ }
28
+ };
29
+ child.stdout.on("data", capture);
30
+ child.stderr.on("data", capture);
31
+ const timer = setTimeout(() => {
32
+ child.kill("SIGKILL");
33
+ reject(new Error(`${command} ${args.join(" ")} timed out after ${options.timeoutMs}ms`));
34
+ }, options.timeoutMs);
35
+ child.on("error", (error) => {
36
+ clearTimeout(timer);
37
+ reject(error);
38
+ });
39
+ child.on("close", (code) => {
40
+ clearTimeout(timer);
41
+ resolve({ code: code ?? -1, tail });
42
+ });
43
+ });
44
+ };
45
+ const runStep = async (name, command, args, options) => {
46
+ const result = await runCommand(command, args, options);
47
+ if (result.code !== 0) {
48
+ throw new Error(`medplum build step "${name}" failed with exit code ${result.code}. Last output:\n${result.tail.join("")}`);
49
+ }
50
+ };
51
+ /**
52
+ * Ensure a pinned Medplum monorepo clone is installed and built under the
53
+ * shared cache, following the official install-from-scratch flow
54
+ * (git clone -> npm ci -> npm run build:fast). Idempotent: a marker file
55
+ * short-circuits the expensive steps.
56
+ */
57
+ export const ensureMedplumBuild = async (options = {}) => {
58
+ const paths = resolveMedplumPaths({ version: options.version, cacheDir: options.cacheDir });
59
+ const onLog = options.onLog;
60
+ if (existsSync(paths.buildMarker) && existsSync(paths.serverEntry)) {
61
+ return paths;
62
+ }
63
+ if (!process.env.HOME && !paths.cacheRoot.startsWith("/")) {
64
+ throw new Error("Cannot resolve the medplum clone cache: HOME is not set");
65
+ }
66
+ const needsClone = !existsSync(joinPath(paths.cloneDir, "package.json"));
67
+ await mkdir(paths.cloneDir, { recursive: true });
68
+ const logPath = join(paths.cacheRoot, `${paths.version}.log`);
69
+ if (needsClone) {
70
+ log(onLog, `[medplum-mock] cloning medplum ${paths.version} (one-time, cached at ${paths.cloneDir})`);
71
+ await runStep("git clone", "git", [
72
+ "clone",
73
+ "--depth",
74
+ "1",
75
+ "--branch",
76
+ paths.version,
77
+ "https://github.com/medplum/medplum.git",
78
+ paths.cloneDir,
79
+ ], {
80
+ timeoutMs: CLONE_TIMEOUT_MILLISECONDS,
81
+ logPath,
82
+ });
83
+ }
84
+ const nodeModulesReady = existsSync(joinPath(paths.cloneDir, "node_modules", ".package-lock.json"));
85
+ if (!nodeModulesReady) {
86
+ log(onLog, `[medplum-mock] npm ci in ${paths.cloneDir} (one-time, several minutes)`);
87
+ await runStep("npm ci", "npm", ["ci", "--no-audit", "--no-fund"], {
88
+ cwd: paths.cloneDir,
89
+ timeoutMs: INSTALL_TIMEOUT_MILLISECONDS,
90
+ logPath,
91
+ });
92
+ }
93
+ const buildReady = existsSync(paths.serverEntry);
94
+ if (!buildReady) {
95
+ log(onLog, `[medplum-mock] building medplum server (one-time, several minutes)`);
96
+ await runStep("npm run build:fast", "npm", ["run", "build:fast"], {
97
+ cwd: paths.cloneDir,
98
+ timeoutMs: BUILD_TIMEOUT_MILLISECONDS,
99
+ logPath,
100
+ });
101
+ }
102
+ if (!existsSync(paths.serverEntry)) {
103
+ throw new Error(`medplum build finished but the server entry is missing: ${paths.serverEntry}`);
104
+ }
105
+ await writeFile(paths.buildMarker, new Date().toISOString());
106
+ log(onLog, `[medplum-mock] medplum ${paths.version} ready at ${paths.cloneDir}`);
107
+ return paths;
108
+ };
109
+ //# sourceMappingURL=ensure-medplum-build.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ensure-medplum-build.js","sourceRoot":"","sources":["../src/ensure-medplum-build.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,7 @@
1
+ import type { OpenAPIDocument } from "@crvouga/mockingbird-openapi";
2
+ export declare const document: OpenAPIDocument;
3
+ export type OperationId = "GetHealthcheck" | "PostAuthLogin" | "PostOauth2Token" | "SearchPatient" | "CreatePatient" | "ReadPatient" | "UpdatePatient" | "DeletePatient";
4
+ export type SupportedOperationId = "GetHealthcheck" | "PostAuthLogin" | "PostOauth2Token" | "SearchPatient" | "CreatePatient" | "ReadPatient" | "UpdatePatient" | "DeletePatient";
5
+ export declare const operationIds: readonly ["GetHealthcheck", "PostAuthLogin", "PostOauth2Token", "SearchPatient", "CreatePatient", "ReadPatient", "UpdatePatient", "DeletePatient"];
6
+ export declare const supportedOperationIds: readonly ["GetHealthcheck", "PostAuthLogin", "PostOauth2Token", "SearchPatient", "CreatePatient", "ReadPatient", "UpdatePatient", "DeletePatient"];
7
+ //# sourceMappingURL=openapi.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"openapi.d.ts","sourceRoot":"","sources":["../../src/generated/openapi.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,8BAA8B,CAAA;AAEnE,eAAO,MAAM,QAAQ,EAAE,eAA48O,CAAA;AAEn+O,MAAM,MAAM,WAAW,GAAG,gBAAgB,GAAG,eAAe,GAAG,iBAAiB,GAAG,eAAe,GAAG,eAAe,GAAG,aAAa,GAAG,eAAe,GAAG,eAAe,CAAA;AACxK,MAAM,MAAM,oBAAoB,GAAG,gBAAgB,GAAG,eAAe,GAAG,iBAAiB,GAAG,eAAe,GAAG,eAAe,GAAG,aAAa,GAAG,eAAe,GAAG,eAAe,CAAA;AACjL,eAAO,MAAM,YAAY,oJAA8I,CAAA;AACvK,eAAO,MAAM,qBAAqB,oJAA8I,CAAA"}
@@ -0,0 +1,5 @@
1
+ // Generated by @crvouga/mockingbird-openapi-codegen from openapi.yaml. Do not edit.
2
+ export const document = JSON.parse(`{"openapi":"3.1.0","info":{"title":"Medplum API (Mockingbird subset)","description":"Self-hosted Medplum server subset exercised by the Mockingbird differential properties.\\nThe mock runs the real Medplum server (self-hosting: install from scratch,\\nhttps://www.medplum.com/docs/self-hosting/install-from-scratch) on embedded Postgres and Redis.\\n","version":"4.0","x-mockingbird-upstream":{"docs":"https://www.medplum.com/docs/api","selfHosting":"https://www.medplum.com/docs/self-hosting/install-from-scratch","note":"Hand-authored subset covering the auth flow, FHIR Patient CRUD, and project invite."}},"servers":[{"url":"http://localhost:8103"}],"security":[{"bearerAuth":[]}],"paths":{"/healthcheck":{"get":{"operationId":"GetHealthcheck","security":[],"x-mockingbird":{"supported":true,"parity":{"enabled":false,"safe":true,"reason":"Infrastructure endpoint used for boot polling; excluded from differential walks."}},"responses":{"200":{"description":"Server is healthy","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HealthStatus"}}}}}}},"/auth/login":{"post":{"operationId":"PostAuthLogin","security":[],"x-mockingbird":{"supported":true,"parity":{"enabled":false,"safe":true,"reason":"Seeded-admin credential flow; exercised by lifecycle helpers, not by differential walks."}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoginRequest"}}}},"responses":{"200":{"description":"Login accepted; exchange \`code\` at /oauth2/token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoginResponse"}}}},"401":{"description":"Invalid credentials","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OperationOutcome"}}}}}}},"/oauth2/token":{"post":{"operationId":"PostOauth2Token","security":[],"x-mockingbird":{"supported":true,"parity":{"enabled":false,"safe":true,"reason":"One-time token exchange; tokens are minted per login, so walks cannot replay them."}},"requestBody":{"required":true,"content":{"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/TokenRequest"}}}},"responses":{"200":{"description":"Access token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TokenResponse"}}}},"400":{"description":"Invalid grant","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OperationOutcome"}}}}}}},"/fhir/R4/Patient":{"get":{"operationId":"SearchPatient","x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":true}},"parameters":[{"name":"name","in":"query","required":false,"schema":{"type":"string"}},{"name":"_count","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":100}}],"responses":{"200":{"description":"Search bundle","content":{"application/fhir+json":{"schema":{"$ref":"#/components/schemas/Bundle"}}}}}},"post":{"operationId":"CreatePatient","x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":true}},"requestBody":{"required":true,"content":{"application/fhir+json":{"schema":{"$ref":"#/components/schemas/PatientInput"}}}},"responses":{"201":{"description":"Created Patient","content":{"application/fhir+json":{"schema":{"$ref":"#/components/schemas/Patient"}}}},"400":{"description":"Bad Request","content":{"application/fhir+json":{"schema":{"$ref":"#/components/schemas/OperationOutcome"}}}}}}},"/fhir/R4/Patient/{id}":{"get":{"operationId":"ReadPatient","x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":true}},"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","x-mockingbird-resource-ref":{"type":"patient","missing":"not_found"}}}],"responses":{"200":{"description":"Patient","content":{"application/fhir+json":{"schema":{"$ref":"#/components/schemas/Patient"}}}},"404":{"description":"Not Found","content":{"application/fhir+json":{"schema":{"$ref":"#/components/schemas/OperationOutcome"}}}}}},"put":{"operationId":"UpdatePatient","x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":true}},"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","x-mockingbird-resource-ref":{"type":"patient","missing":"not_found"}}}],"requestBody":{"required":true,"content":{"application/fhir+json":{"schema":{"$ref":"#/components/schemas/PatientInput"}}}},"responses":{"200":{"description":"Updated Patient","content":{"application/fhir+json":{"schema":{"$ref":"#/components/schemas/Patient"}}}},"404":{"description":"Not Found","content":{"application/fhir+json":{"schema":{"$ref":"#/components/schemas/OperationOutcome"}}}}}},"delete":{"operationId":"DeletePatient","x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":true}},"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","x-mockingbird-resource-ref":{"type":"patient","missing":"not_found"}}}],"responses":{"200":{"description":"Deleted","content":{"application/fhir+json":{"schema":{"$ref":"#/components/schemas/OperationOutcome"}}}},"404":{"description":"Not Found","content":{"application/fhir+json":{"schema":{"$ref":"#/components/schemas/OperationOutcome"}}}}}}}},"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer"}},"schemas":{"HealthStatus":{"type":"object","properties":{"ok":{"type":"boolean"}}},"LoginRequest":{"type":"object","required":["email","password"],"properties":{"email":{"type":"string"},"password":{"type":"string"},"scope":{"type":"string"},"codeChallenge":{"type":"string"},"codeChallengeMethod":{"type":"string"}}},"LoginResponse":{"type":"object","required":["code"],"properties":{"code":{"type":"string","x-mockingbird-volatile":{"kind":"token"}}}},"TokenRequest":{"type":"object","required":["grant_type","code","client_id"],"properties":{"grant_type":{"type":"string","enum":["authorization_code"]},"code":{"type":"string","x-mockingbird-volatile":{"kind":"token"}},"client_id":{"type":"string"},"code_verifier":{"type":"string"}}},"TokenResponse":{"type":"object","required":["access_token","token_type","expires_in"],"properties":{"access_token":{"type":"string","x-mockingbird-volatile":{"kind":"token"}},"token_type":{"type":"string","enum":["Bearer"]},"expires_in":{"type":"integer"}}},"HumanName":{"type":"object","properties":{"family":{"type":"string"},"given":{"type":"array","items":{"type":"string"}}}},"PatientInput":{"type":"object","required":["resourceType"],"properties":{"resourceType":{"type":"string","enum":["Patient"]},"name":{"type":"array","items":{"$ref":"#/components/schemas/HumanName"}}}},"Patient":{"type":"object","required":["resourceType","id","meta"],"properties":{"resourceType":{"type":"string","enum":["Patient"]},"id":{"type":"string","x-mockingbird-resource":{"type":"patient","identity":true},"x-mockingbird-volatile":{"kind":"id"}},"meta":{"type":"object","properties":{"versionId":{"type":"string","x-mockingbird-volatile":{"kind":"id"}},"lastUpdated":{"type":"string","format":"date-time","x-mockingbird-volatile":{"kind":"timestamp"}}}},"name":{"type":"array","items":{"$ref":"#/components/schemas/HumanName"}}}},"Bundle":{"type":"object","required":["resourceType","type"],"properties":{"resourceType":{"type":"string","enum":["Bundle"]},"type":{"type":"string","enum":["searchset"]},"total":{"type":"integer"},"entry":{"type":"array","items":{"type":"object","properties":{"resource":{"$ref":"#/components/schemas/Patient"}}}}}},"OperationOutcome":{"type":"object","required":["resourceType","issue"],"properties":{"resourceType":{"type":"string","enum":["OperationOutcome"]},"issue":{"type":"array","items":{"type":"object"}}}}}}}`);
3
+ export const operationIds = ["GetHealthcheck", "PostAuthLogin", "PostOauth2Token", "SearchPatient", "CreatePatient", "ReadPatient", "UpdatePatient", "DeletePatient"];
4
+ export const supportedOperationIds = ["GetHealthcheck", "PostAuthLogin", "PostOauth2Token", "SearchPatient", "CreatePatient", "ReadPatient", "UpdatePatient", "DeletePatient"];
5
+ //# sourceMappingURL=openapi.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"openapi.js","sourceRoot":"","sources":["../../src/generated/openapi.ts"],"names":[],"mappings":"AAAA,oFAAoF;AAIpF,MAAM,CAAC,MAAM,QAAQ,GAAoB,IAAI,CAAC,KAAK,CAAC,25OAA25O,CAAoB,CAAA;AAIn+O,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,gBAAgB,EAAC,eAAe,EAAC,iBAAiB,EAAC,eAAe,EAAC,eAAe,EAAC,aAAa,EAAC,eAAe,EAAC,eAAe,CAAU,CAAA;AACvK,MAAM,CAAC,MAAM,qBAAqB,GAAG,CAAC,gBAAgB,EAAC,eAAe,EAAC,iBAAiB,EAAC,eAAe,EAAC,eAAe,EAAC,aAAa,EAAC,eAAe,EAAC,eAAe,CAAU,CAAA"}
@@ -0,0 +1,46 @@
1
+ import type { FetchAPI } from "@crvouga/mockingbird-core";
2
+ import { type MedplumProcessOptions } from "./process.js";
3
+ export type { MedplumServerConfig } from "./config.js";
4
+ export { buildServerConfig, SUPER_ADMIN_EMAIL, SUPER_ADMIN_PASSWORD } from "./config.js";
5
+ export type { MedplumPaths } from "./paths.js";
6
+ export { resolveMedplumPaths } from "./paths.js";
7
+ export type { MedplumProcessInfo, MedplumProcessOptions } from "./process.js";
8
+ export type MedplumAPIOptions = MedplumProcessOptions & {
9
+ /** Username for the seeded super admin. Defaults match Medplum's dev seed. */
10
+ email?: string | undefined;
11
+ /** Password for the seeded super admin. */
12
+ password?: string | undefined;
13
+ };
14
+ /**
15
+ * Stateful Medplum mock backed by the real Medplum server, self-hosted as a
16
+ * child process on embedded Postgres and Redis. Implements the Mockingbird
17
+ * FetchAPI contract by proxying every request to the running server, with
18
+ * explicit async lifecycle calls following the adapter convention.
19
+ */
20
+ export declare class MedplumAPI implements FetchAPI {
21
+ private readonly process;
22
+ private readonly options;
23
+ private cachedToken;
24
+ constructor(options?: MedplumAPIOptions);
25
+ fetch(request: Request): Promise<Response>;
26
+ /** Boot embedded Postgres + Redis, ensure the server build, and start the server. */
27
+ start(): Promise<void>;
28
+ /** Stop the server and tear down Postgres, Redis, and the temp dir. */
29
+ stop(): Promise<void>;
30
+ /** Drop every resource (truncate all tables) and restart the server. */
31
+ reset(): Promise<void>;
32
+ get isStarted(): boolean;
33
+ /** Ephemeral port of the running API server. */
34
+ get apiPort(): number;
35
+ /** Base URL of the running server (internal ephemeral port). */
36
+ getBaseUrl(): string;
37
+ /**
38
+ * OAuth2 password + PKCE login for the seeded super admin, returning a
39
+ * bearer token for API calls. The token is cached until reset()/stop().
40
+ */
41
+ getAccessToken(): Promise<string>;
42
+ private rewriteUrl;
43
+ }
44
+ /** Async factory that boots the server before returning, pairing with the parity runner's async `mock.create`. */
45
+ export declare const createMedplumAPI: (options?: MedplumAPIOptions) => Promise<MedplumAPI>;
46
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,2BAA2B,CAAA;AAOzD,OAAO,EAAE,KAAK,qBAAqB,EAAwB,MAAM,cAAc,CAAA;AAE/E,YAAY,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAA;AACtD,OAAO,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAA;AACxF,YAAY,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AAC9C,OAAO,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAA;AAChD,YAAY,EAAE,kBAAkB,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAA;AAK7E,MAAM,MAAM,iBAAiB,GAAG,qBAAqB,GAAG;IACtD,8EAA8E;IAC9E,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IAC1B,2CAA2C;IAC3C,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;CAC9B,CAAA;AAQD;;;;;GAKG;AACH,qBAAa,UAAW,YAAW,QAAQ;IACzC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAsB;IAC9C,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAmB;IAC3C,OAAO,CAAC,WAAW,CAAoB;gBAE3B,OAAO,GAAE,iBAAsB;IAM3C,KAAK,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC;IAmB1C,qFAAqF;IAC/E,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAI5B,uEAAuE;IACjE,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAK3B,wEAAwE;IAClE,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAK5B,IAAI,SAAS,IAAI,OAAO,CAEvB;IAED,gDAAgD;IAChD,IAAI,OAAO,IAAI,MAAM,CAEpB;IAED,gEAAgE;IAChE,UAAU,IAAI,MAAM;IAIpB;;;OAGG;IACG,cAAc,IAAI,OAAO,CAAC,MAAM,CAAC;IA+CvC,OAAO,CAAC,UAAU;CAQnB;AAED,kHAAkH;AAClH,eAAO,MAAM,gBAAgB,GAAU,UAAS,iBAAsB,KAAG,OAAO,CAAC,UAAU,CAI1F,CAAA"}
package/dist/index.js ADDED
@@ -0,0 +1,126 @@
1
+ import { SUPER_ADMIN_CLIENT_ID, SUPER_ADMIN_CLIENT_SECRET, SUPER_ADMIN_EMAIL, SUPER_ADMIN_PASSWORD, } from "./config.js";
2
+ import { MedplumServerProcess } from "./process.js";
3
+ export { buildServerConfig, SUPER_ADMIN_EMAIL, SUPER_ADMIN_PASSWORD } from "./config.js";
4
+ export { resolveMedplumPaths } from "./paths.js";
5
+ const AUTH_LOGIN_PATH = "/auth/login";
6
+ const OAUTH_TOKEN_PATH = "/oauth2/token";
7
+ /**
8
+ * Stateful Medplum mock backed by the real Medplum server, self-hosted as a
9
+ * child process on embedded Postgres and Redis. Implements the Mockingbird
10
+ * FetchAPI contract by proxying every request to the running server, with
11
+ * explicit async lifecycle calls following the adapter convention.
12
+ */
13
+ export class MedplumAPI {
14
+ process;
15
+ options;
16
+ cachedToken;
17
+ constructor(options = {}) {
18
+ const { email, password, ...processOptions } = options;
19
+ this.options = { email, password, ...processOptions };
20
+ this.process = new MedplumServerProcess(processOptions);
21
+ }
22
+ fetch(request) {
23
+ const target = this.rewriteUrl(request.url);
24
+ const headers = new Headers(request.headers);
25
+ headers.delete("host");
26
+ headers.delete("connection");
27
+ headers.delete("content-length");
28
+ const init = {
29
+ method: request.method,
30
+ headers,
31
+ redirect: "manual",
32
+ };
33
+ if (request.method !== "GET" && request.method !== "HEAD") {
34
+ init.body = request.body;
35
+ init.duplex = "half";
36
+ }
37
+ return fetch(new Request(target, init));
38
+ }
39
+ /** Boot embedded Postgres + Redis, ensure the server build, and start the server. */
40
+ async start() {
41
+ await this.process.start();
42
+ }
43
+ /** Stop the server and tear down Postgres, Redis, and the temp dir. */
44
+ async stop() {
45
+ this.cachedToken = undefined;
46
+ await this.process.stop();
47
+ }
48
+ /** Drop every resource (truncate all tables) and restart the server. */
49
+ async reset() {
50
+ this.cachedToken = undefined;
51
+ await this.process.reset();
52
+ }
53
+ get isStarted() {
54
+ return this.process.isStarted;
55
+ }
56
+ /** Ephemeral port of the running API server. */
57
+ get apiPort() {
58
+ return this.process.apiPort;
59
+ }
60
+ /** Base URL of the running server (internal ephemeral port). */
61
+ getBaseUrl() {
62
+ return this.process.baseUrl;
63
+ }
64
+ /**
65
+ * OAuth2 password + PKCE login for the seeded super admin, returning a
66
+ * bearer token for API calls. The token is cached until reset()/stop().
67
+ */
68
+ async getAccessToken() {
69
+ if (this.cachedToken)
70
+ return this.cachedToken;
71
+ const base = this.process.baseUrl.replace(/\/+$/, "");
72
+ const email = this.options.email ?? SUPER_ADMIN_EMAIL;
73
+ const password = this.options.password ?? SUPER_ADMIN_PASSWORD;
74
+ const loginResponse = await fetch(`${base}${AUTH_LOGIN_PATH}`, {
75
+ method: "POST",
76
+ headers: { "content-type": "application/json" },
77
+ body: JSON.stringify({
78
+ email,
79
+ password,
80
+ clientId: SUPER_ADMIN_CLIENT_ID,
81
+ scope: "openid offline",
82
+ codeChallenge: SUPER_ADMIN_CLIENT_SECRET,
83
+ codeChallengeMethod: "plain",
84
+ }),
85
+ });
86
+ if (!loginResponse.ok) {
87
+ throw new Error(`medplum login failed (${loginResponse.status}): ${await loginResponse.text()}`);
88
+ }
89
+ const login = (await loginResponse.json());
90
+ if (!login.code)
91
+ throw new Error("medplum login did not return a login code");
92
+ const tokenResponse = await fetch(`${base}${OAUTH_TOKEN_PATH}`, {
93
+ method: "POST",
94
+ headers: { "content-type": "application/x-www-form-urlencoded" },
95
+ body: new URLSearchParams({
96
+ grant_type: "authorization_code",
97
+ code: login.code,
98
+ client_id: SUPER_ADMIN_CLIENT_ID,
99
+ code_verifier: SUPER_ADMIN_CLIENT_SECRET,
100
+ }),
101
+ });
102
+ if (!tokenResponse.ok) {
103
+ throw new Error(`medplum token exchange failed (${tokenResponse.status}): ${await tokenResponse.text()}`);
104
+ }
105
+ const token = (await tokenResponse.json()).access_token;
106
+ if (!token)
107
+ throw new Error("medplum token exchange did not return an access token");
108
+ this.cachedToken = token;
109
+ return token;
110
+ }
111
+ rewriteUrl(rawUrl) {
112
+ const base = this.process.baseUrl.replace(/\/+$/, "");
113
+ const incoming = new URL(rawUrl);
114
+ const internal = new URL(base);
115
+ internal.pathname = incoming.pathname;
116
+ internal.search = incoming.search;
117
+ return internal.toString();
118
+ }
119
+ }
120
+ /** Async factory that boots the server before returning, pairing with the parity runner's async `mock.create`. */
121
+ export const createMedplumAPI = async (options = {}) => {
122
+ const api = new MedplumAPI(options);
123
+ await api.start();
124
+ return api;
125
+ };
126
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EACL,qBAAqB,EACrB,yBAAyB,EACzB,iBAAiB,EACjB,oBAAoB,GACrB,MAAM,aAAa,CAAA;AACpB,OAAO,EAA8B,oBAAoB,EAAE,MAAM,cAAc,CAAA;AAG/E,OAAO,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAA;AAExF,OAAO,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAA;AAGhD,MAAM,eAAe,GAAG,aAAa,CAAA;AACrC,MAAM,gBAAgB,GAAG,eAAe,CAAA;AAexC;;;;;GAKG;AACH,MAAM,OAAO,UAAU;IACJ,OAAO,CAAsB;IAC7B,OAAO,CAAmB;IACnC,WAAW,CAAoB;IAEvC,YAAY,UAA6B,EAAE;QACzC,MAAM,EAAE,KAAK,EAAE,QAAQ,EAAE,GAAG,cAAc,EAAE,GAAG,OAAO,CAAA;QACtD,IAAI,CAAC,OAAO,GAAG,EAAE,KAAK,EAAE,QAAQ,EAAE,GAAG,cAAc,EAAE,CAAA;QACrD,IAAI,CAAC,OAAO,GAAG,IAAI,oBAAoB,CAAC,cAAc,CAAC,CAAA;IACzD,CAAC;IAED,KAAK,CAAC,OAAgB;QACpB,MAAM,MAAM,GAAG,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC,GAAG,CAAC,CAAA;QAC3C,MAAM,OAAO,GAAG,IAAI,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,CAAA;QAC5C,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,CAAA;QACtB,OAAO,CAAC,MAAM,CAAC,YAAY,CAAC,CAAA;QAC5B,OAAO,CAAC,MAAM,CAAC,gBAAgB,CAAC,CAAA;QAChC,MAAM,IAAI,GAAgB;YACxB,MAAM,EAAE,OAAO,CAAC,MAAM;YACtB,OAAO;YACP,QAAQ,EAAE,QAAQ;SACnB,CAAA;QACD,IAAI,OAAO,CAAC,MAAM,KAAK,KAAK,IAAI,OAAO,CAAC,MAAM,KAAK,MAAM,EAAE,CAAC;YAC1D,IAAI,CAAC,IAAI,GAAG,OAAO,CAAC,IAAI,CAEvB;YAAC,IAAyC,CAAC,MAAM,GAAG,MAAM,CAAA;QAC7D,CAAC;QACD,OAAO,KAAK,CAAC,IAAI,OAAO,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC,CAAA;IACzC,CAAC;IAED,qFAAqF;IACrF,KAAK,CAAC,KAAK;QACT,MAAM,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,CAAA;IAC5B,CAAC;IAED,uEAAuE;IACvE,KAAK,CAAC,IAAI;QACR,IAAI,CAAC,WAAW,GAAG,SAAS,CAAA;QAC5B,MAAM,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,CAAA;IAC3B,CAAC;IAED,wEAAwE;IACxE,KAAK,CAAC,KAAK;QACT,IAAI,CAAC,WAAW,GAAG,SAAS,CAAA;QAC5B,MAAM,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,CAAA;IAC5B,CAAC;IAED,IAAI,SAAS;QACX,OAAO,IAAI,CAAC,OAAO,CAAC,SAAS,CAAA;IAC/B,CAAC;IAED,gDAAgD;IAChD,IAAI,OAAO;QACT,OAAO,IAAI,CAAC,OAAO,CAAC,OAAO,CAAA;IAC7B,CAAC;IAED,gEAAgE;IAChE,UAAU;QACR,OAAO,IAAI,CAAC,OAAO,CAAC,OAAO,CAAA;IAC7B,CAAC;IAED;;;OAGG;IACH,KAAK,CAAC,cAAc;QAClB,IAAI,IAAI,CAAC,WAAW;YAAE,OAAO,IAAI,CAAC,WAAW,CAAA;QAC7C,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAA;QACrD,MAAM,KAAK,GAAG,IAAI,CAAC,OAAO,CAAC,KAAK,IAAI,iBAAiB,CAAA;QACrD,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,CAAC,QAAQ,IAAI,oBAAoB,CAAA;QAE9D,MAAM,aAAa,GAAG,MAAM,KAAK,CAAC,GAAG,IAAI,GAAG,eAAe,EAAE,EAAE;YAC7D,MAAM,EAAE,MAAM;YACd,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;YAC/C,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC;gBACnB,KAAK;gBACL,QAAQ;gBACR,QAAQ,EAAE,qBAAqB;gBAC/B,KAAK,EAAE,gBAAgB;gBACvB,aAAa,EAAE,yBAAyB;gBACxC,mBAAmB,EAAE,OAAO;aAC7B,CAAC;SACH,CAAC,CAAA;QACF,IAAI,CAAC,aAAa,CAAC,EAAE,EAAE,CAAC;YACtB,MAAM,IAAI,KAAK,CACb,yBAAyB,aAAa,CAAC,MAAM,MAAM,MAAM,aAAa,CAAC,IAAI,EAAE,EAAE,CAChF,CAAA;QACH,CAAC;QACD,MAAM,KAAK,GAAG,CAAC,MAAM,aAAa,CAAC,IAAI,EAAE,CAA4C,CAAA;QACrF,IAAI,CAAC,KAAK,CAAC,IAAI;YAAE,MAAM,IAAI,KAAK,CAAC,2CAA2C,CAAC,CAAA;QAE7E,MAAM,aAAa,GAAG,MAAM,KAAK,CAAC,GAAG,IAAI,GAAG,gBAAgB,EAAE,EAAE;YAC9D,MAAM,EAAE,MAAM;YACd,OAAO,EAAE,EAAE,cAAc,EAAE,mCAAmC,EAAE;YAChE,IAAI,EAAE,IAAI,eAAe,CAAC;gBACxB,UAAU,EAAE,oBAAoB;gBAChC,IAAI,EAAE,KAAK,CAAC,IAAI;gBAChB,SAAS,EAAE,qBAAqB;gBAChC,aAAa,EAAE,yBAAyB;aACzC,CAAC;SACH,CAAC,CAAA;QACF,IAAI,CAAC,aAAa,CAAC,EAAE,EAAE,CAAC;YACtB,MAAM,IAAI,KAAK,CACb,kCAAkC,aAAa,CAAC,MAAM,MAAM,MAAM,aAAa,CAAC,IAAI,EAAE,EAAE,CACzF,CAAA;QACH,CAAC;QACD,MAAM,KAAK,GAAI,CAAC,MAAM,aAAa,CAAC,IAAI,EAAE,CAAmB,CAAC,YAAY,CAAA;QAC1E,IAAI,CAAC,KAAK;YAAE,MAAM,IAAI,KAAK,CAAC,uDAAuD,CAAC,CAAA;QACpF,IAAI,CAAC,WAAW,GAAG,KAAK,CAAA;QACxB,OAAO,KAAK,CAAA;IACd,CAAC;IAEO,UAAU,CAAC,MAAc;QAC/B,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAA;QACrD,MAAM,QAAQ,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,CAAA;QAChC,MAAM,QAAQ,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,CAAA;QAC9B,QAAQ,CAAC,QAAQ,GAAG,QAAQ,CAAC,QAAQ,CAAA;QACrC,QAAQ,CAAC,MAAM,GAAG,QAAQ,CAAC,MAAM,CAAA;QACjC,OAAO,QAAQ,CAAC,QAAQ,EAAE,CAAA;IAC5B,CAAC;CACF;AAED,kHAAkH;AAClH,MAAM,CAAC,MAAM,gBAAgB,GAAG,KAAK,EAAE,UAA6B,EAAE,EAAuB,EAAE;IAC7F,MAAM,GAAG,GAAG,IAAI,UAAU,CAAC,OAAO,CAAC,CAAA;IACnC,MAAM,GAAG,CAAC,KAAK,EAAE,CAAA;IACjB,OAAO,GAAG,CAAA;AACZ,CAAC,CAAA"}
@@ -0,0 +1,14 @@
1
+ export type MedplumPathsOptions = {
2
+ version?: string | undefined;
3
+ cacheDir?: string | undefined;
4
+ };
5
+ export declare const joinPath: (...segments: string[]) => string;
6
+ export type MedplumPaths = {
7
+ version: string;
8
+ cacheRoot: string;
9
+ cloneDir: string;
10
+ serverEntry: string;
11
+ buildMarker: string;
12
+ };
13
+ export declare const resolveMedplumPaths: (options?: MedplumPathsOptions) => MedplumPaths;
14
+ //# sourceMappingURL=paths.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"paths.d.ts","sourceRoot":"","sources":["../src/paths.ts"],"names":[],"mappings":"AAIA,MAAM,MAAM,mBAAmB,GAAG;IAChC,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IAC5B,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;CAC9B,CAAA;AAaD,eAAO,MAAM,QAAQ,GAAI,GAAG,UAAU,MAAM,EAAE,KAAG,MACD,CAAA;AAEhD,MAAM,MAAM,YAAY,GAAG;IACzB,OAAO,EAAE,MAAM,CAAA;IACf,SAAS,EAAE,MAAM,CAAA;IACjB,QAAQ,EAAE,MAAM,CAAA;IAChB,WAAW,EAAE,MAAM,CAAA;IACnB,WAAW,EAAE,MAAM,CAAA;CACpB,CAAA;AAED,eAAO,MAAM,mBAAmB,GAAI,UAAS,mBAAwB,KAAG,YAavE,CAAA"}
package/dist/paths.js ADDED
@@ -0,0 +1,27 @@
1
+ const DEFAULT_MEDPLUM_VERSION = "v5.1.37";
2
+ const CACHE_ROOT_SEGMENTS = ["mockingbird", "medplum-server"];
3
+ const HOME_CACHE_ROOT = ".cache";
4
+ const normalizeVersion = (version) => {
5
+ const trimmed = version.trim();
6
+ return trimmed.startsWith("v") ? trimmed : `v${trimmed}`;
7
+ };
8
+ const defaultCacheRoot = () => {
9
+ const home = process.env.HOME?.trim();
10
+ if (!home)
11
+ throw new Error("Cannot resolve the medplum clone cache: HOME is not set");
12
+ return joinPath(home, HOME_CACHE_ROOT, ...CACHE_ROOT_SEGMENTS);
13
+ };
14
+ export const joinPath = (...segments) => segments.filter((s) => s.length > 0).join("/");
15
+ export const resolveMedplumPaths = (options = {}) => {
16
+ const version = normalizeVersion(options.version ?? process.env.MOCKINGBIRD_MEDPLUM_VERSION ?? DEFAULT_MEDPLUM_VERSION);
17
+ const cacheRoot = options.cacheDir ?? process.env.MEDPLUM_MOCK_CACHE_DIR ?? defaultCacheRoot();
18
+ const cloneDir = joinPath(cacheRoot, version);
19
+ return {
20
+ version,
21
+ cacheRoot,
22
+ cloneDir,
23
+ serverEntry: joinPath(cloneDir, "packages", "server", "dist", "index.js"),
24
+ buildMarker: joinPath(cloneDir, ".mockingbird-build-complete"),
25
+ };
26
+ };
27
+ //# sourceMappingURL=paths.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"paths.js","sourceRoot":"","sources":["../src/paths.ts"],"names":[],"mappings":"AAAA,MAAM,uBAAuB,GAAG,SAAS,CAAA;AACzC,MAAM,mBAAmB,GAAG,CAAC,aAAa,EAAE,gBAAgB,CAAC,CAAA;AAC7D,MAAM,eAAe,GAAG,QAAQ,CAAA;AAOhC,MAAM,gBAAgB,GAAG,CAAC,OAAe,EAAU,EAAE;IACnD,MAAM,OAAO,GAAG,OAAO,CAAC,IAAI,EAAE,CAAA;IAC9B,OAAO,OAAO,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,OAAO,EAAE,CAAA;AAC1D,CAAC,CAAA;AAED,MAAM,gBAAgB,GAAG,GAAW,EAAE;IACpC,MAAM,IAAI,GAAG,OAAO,CAAC,GAAG,CAAC,IAAI,EAAE,IAAI,EAAE,CAAA;IACrC,IAAI,CAAC,IAAI;QAAE,MAAM,IAAI,KAAK,CAAC,yDAAyD,CAAC,CAAA;IACrF,OAAO,QAAQ,CAAC,IAAI,EAAE,eAAe,EAAE,GAAG,mBAAmB,CAAC,CAAA;AAChE,CAAC,CAAA;AAED,MAAM,CAAC,MAAM,QAAQ,GAAG,CAAC,GAAG,QAAkB,EAAU,EAAE,CACxD,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;AAUhD,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC,UAA+B,EAAE,EAAgB,EAAE;IACrF,MAAM,OAAO,GAAG,gBAAgB,CAC9B,OAAO,CAAC,OAAO,IAAI,OAAO,CAAC,GAAG,CAAC,2BAA2B,IAAI,uBAAuB,CACtF,CAAA;IACD,MAAM,SAAS,GAAG,OAAO,CAAC,QAAQ,IAAI,OAAO,CAAC,GAAG,CAAC,sBAAsB,IAAI,gBAAgB,EAAE,CAAA;IAC9F,MAAM,QAAQ,GAAG,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC,CAAA;IAC7C,OAAO;QACL,OAAO;QACP,SAAS;QACT,QAAQ;QACR,WAAW,EAAE,QAAQ,CAAC,QAAQ,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,EAAE,UAAU,CAAC;QACzE,WAAW,EAAE,QAAQ,CAAC,QAAQ,EAAE,6BAA6B,CAAC;KAC/D,CAAA;AACH,CAAC,CAAA"}
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Ask the OS for a free TCP port by binding an ephemeral listener, reading the
3
+ * assigned port, and closing the listener. The port can race between release
4
+ * and reuse, which is acceptable for mock infrastructure: consumers poll the
5
+ * health endpoint until the server is ready.
6
+ */
7
+ export declare const findFreePort: () => Promise<number>;
8
+ //# sourceMappingURL=ports.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ports.d.ts","sourceRoot":"","sources":["../src/ports.ts"],"names":[],"mappings":"AAIA;;;;;GAKG;AACH,eAAO,MAAM,YAAY,QAAa,OAAO,CAAC,MAAM,CAgBnD,CAAA"}
package/dist/ports.js ADDED
@@ -0,0 +1,26 @@
1
+ import { createServer } from "node:net";
2
+ const BIND_HOST = "127.0.0.1";
3
+ /**
4
+ * Ask the OS for a free TCP port by binding an ephemeral listener, reading the
5
+ * assigned port, and closing the listener. The port can race between release
6
+ * and reuse, which is acceptable for mock infrastructure: consumers poll the
7
+ * health endpoint until the server is ready.
8
+ */
9
+ export const findFreePort = async () => {
10
+ return new Promise((resolve, reject) => {
11
+ const server = createServer();
12
+ server.unref();
13
+ server.on("error", reject);
14
+ server.listen(0, BIND_HOST, () => {
15
+ const address = server.address();
16
+ if (address && typeof address === "object") {
17
+ const { port } = address;
18
+ server.close(() => resolve(port));
19
+ return;
20
+ }
21
+ server.close();
22
+ reject(new Error("Ephemeral listener did not report a port"));
23
+ });
24
+ });
25
+ };
26
+ //# sourceMappingURL=ports.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ports.js","sourceRoot":"","sources":["../src/ports.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,UAAU,CAAA;AAEvC,MAAM,SAAS,GAAG,WAAW,CAAA;AAE7B;;;;;GAKG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG,KAAK,IAAqB,EAAE;IACtD,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QACrC,MAAM,MAAM,GAAG,YAAY,EAAE,CAAA;QAC7B,MAAM,CAAC,KAAK,EAAE,CAAA;QACd,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,CAAC,CAAA;QAC1B,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,SAAS,EAAE,GAAG,EAAE;YAC/B,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO,EAAE,CAAA;YAChC,IAAI,OAAO,IAAI,OAAO,OAAO,KAAK,QAAQ,EAAE,CAAC;gBAC3C,MAAM,EAAE,IAAI,EAAE,GAAG,OAAO,CAAA;gBACxB,MAAM,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAA;gBACjC,OAAM;YACR,CAAC;YACD,MAAM,CAAC,KAAK,EAAE,CAAA;YACd,MAAM,CAAC,IAAI,KAAK,CAAC,0CAA0C,CAAC,CAAC,CAAA;QAC/D,CAAC,CAAC,CAAA;IACJ,CAAC,CAAC,CAAA;AACJ,CAAC,CAAA"}
@@ -0,0 +1,50 @@
1
+ import { type MedplumServerConfig } from "./config.js";
2
+ import { type MedplumPaths } from "./paths.js";
3
+ export type MedplumProcessOptions = {
4
+ version?: string | undefined;
5
+ cacheDir?: string | undefined;
6
+ onLog?: ((message: string) => void) | undefined;
7
+ };
8
+ export type MedplumProcessInfo = {
9
+ apiPort: number;
10
+ dbPort: number;
11
+ redisPort: number;
12
+ baseUrl: string;
13
+ dataDir: string;
14
+ paths: MedplumPaths;
15
+ config: MedplumServerConfig;
16
+ };
17
+ export declare class MedplumServerProcess {
18
+ private readonly options;
19
+ private postgres;
20
+ private redis;
21
+ private child;
22
+ private runDir;
23
+ private started;
24
+ private starting;
25
+ private boundApiPort;
26
+ private boundDbPort;
27
+ private boundRedisPort;
28
+ private exitGuard;
29
+ constructor(options?: MedplumProcessOptions);
30
+ get isStarted(): boolean;
31
+ get baseUrl(): string;
32
+ get apiPort(): number;
33
+ get dbPort(): number;
34
+ get redisPort(): number;
35
+ get info(): MedplumProcessInfo;
36
+ start(): Promise<void>;
37
+ private boot;
38
+ private spawnServerProcess;
39
+ /**
40
+ * Stop the server, drop and recreate the medplum database (the server re-runs
41
+ * migrations and seeding on boot, restoring pristine state), and boot it again.
42
+ */
43
+ reset(): Promise<void>;
44
+ stop(): Promise<void>;
45
+ private stopServerProcess;
46
+ private teardown;
47
+ private registerExitGuard;
48
+ private unregisterExitGuard;
49
+ }
50
+ //# sourceMappingURL=process.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"process.d.ts","sourceRoot":"","sources":["../src/process.ts"],"names":[],"mappings":"AAOA,OAAO,EAAqB,KAAK,mBAAmB,EAAE,MAAM,aAAa,CAAA;AAEzE,OAAO,EAAE,KAAK,YAAY,EAAuB,MAAM,YAAY,CAAA;AAQnE,MAAM,MAAM,qBAAqB,GAAG;IAClC,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IAC5B,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IAC7B,KAAK,CAAC,EAAE,CAAC,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC,GAAG,SAAS,CAAA;CAChD,CAAA;AAED,MAAM,MAAM,kBAAkB,GAAG;IAC/B,OAAO,EAAE,MAAM,CAAA;IACf,MAAM,EAAE,MAAM,CAAA;IACd,SAAS,EAAE,MAAM,CAAA;IACjB,OAAO,EAAE,MAAM,CAAA;IACf,OAAO,EAAE,MAAM,CAAA;IACf,KAAK,EAAE,YAAY,CAAA;IACnB,MAAM,EAAE,mBAAmB,CAAA;CAC5B,CAAA;AA0CD,qBAAa,oBAAoB;IAC/B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAuB;IAC/C,OAAO,CAAC,QAAQ,CAA8B;IAC9C,OAAO,CAAC,KAAK,CAA+B;IAC5C,OAAO,CAAC,KAAK,CAA0B;IACvC,OAAO,CAAC,MAAM,CAAoB;IAClC,OAAO,CAAC,OAAO,CAAQ;IACvB,OAAO,CAAC,QAAQ,CAA2B;IAC3C,OAAO,CAAC,YAAY,CAAoB;IACxC,OAAO,CAAC,WAAW,CAAoB;IACvC,OAAO,CAAC,cAAc,CAAoB;IAC1C,OAAO,CAAC,SAAS,CAA0B;gBAE/B,OAAO,GAAE,qBAA0B;IAI/C,IAAI,SAAS,IAAI,OAAO,CAEvB;IAED,IAAI,OAAO,IAAI,MAAM,CAKpB;IAED,IAAI,OAAO,IAAI,MAAM,CAKpB;IAED,IAAI,MAAM,IAAI,MAAM,CAKnB;IAED,IAAI,SAAS,IAAI,MAAM,CAKtB;IAED,IAAI,IAAI,IAAI,kBAAkB,CAuB7B;IAEK,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;YAWd,IAAI;IA2ClB,OAAO,CAAC,kBAAkB;IAsB1B;;;OAGG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAgBtB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;YAKb,iBAAiB;YAiBjB,QAAQ;IA6BtB,OAAO,CAAC,iBAAiB;IAoBzB,OAAO,CAAC,mBAAmB;CAW5B"}
@@ -0,0 +1,283 @@
1
+ import { spawn } from "node:child_process";
2
+ import { existsSync, rmSync } from "node:fs";
3
+ import { mkdtemp, writeFile } from "node:fs/promises";
4
+ import { tmpdir } from "node:os";
5
+ import { join } from "node:path";
6
+ import EmbeddedPostgres from "embedded-postgres";
7
+ import { RedisMemoryServer } from "redis-memory-server";
8
+ import { buildServerConfig } from "./config.js";
9
+ import { ensureMedplumBuild } from "./ensure-medplum-build.js";
10
+ import { resolveMedplumPaths } from "./paths.js";
11
+ import { findFreePort } from "./ports.js";
12
+ const HEALTHCHECK_PATH = "/healthcheck";
13
+ const SERVER_START_TIMEOUT_MS = 5 * 60 * 1000;
14
+ const HEALTHCHECK_POLL_INTERVAL_MS = 500;
15
+ const CHILD_STOP_GRACE_MS = 10000;
16
+ const startEmbeddedPostgres = async (dataDir, port) => {
17
+ const pg = new EmbeddedPostgres({
18
+ databaseDir: dataDir,
19
+ user: "postgres",
20
+ password: "postgres",
21
+ port,
22
+ persistent: false,
23
+ });
24
+ await pg.initialise();
25
+ await pg.start();
26
+ await pg.createDatabase("medplum");
27
+ return pg;
28
+ };
29
+ const resetMedplumDatabase = async (pg) => {
30
+ await pg.dropDatabase("medplum");
31
+ await pg.createDatabase("medplum");
32
+ };
33
+ const healthcheckUrl = (baseUrl) => `${baseUrl.replace(/\/+$/, "")}${HEALTHCHECK_PATH}`;
34
+ const pollHealthcheck = async (baseUrl, timeoutMs) => {
35
+ const deadline = Date.now() + timeoutMs;
36
+ let lastError = "unknown";
37
+ while (Date.now() < deadline) {
38
+ try {
39
+ const response = await fetch(healthcheckUrl(baseUrl), { method: "GET" });
40
+ if (response.ok)
41
+ return;
42
+ lastError = `healthcheck responded with ${response.status}`;
43
+ }
44
+ catch (error) {
45
+ lastError = error;
46
+ }
47
+ await new Promise((resolve) => setTimeout(resolve, HEALTHCHECK_POLL_INTERVAL_MS));
48
+ }
49
+ throw new Error(`medplum server did not become healthy within ${timeoutMs}ms (last error: ${String(lastError)})`);
50
+ };
51
+ export class MedplumServerProcess {
52
+ options;
53
+ postgres;
54
+ redis;
55
+ child;
56
+ runDir;
57
+ started = false;
58
+ starting;
59
+ boundApiPort;
60
+ boundDbPort;
61
+ boundRedisPort;
62
+ exitGuard;
63
+ constructor(options = {}) {
64
+ this.options = options;
65
+ }
66
+ get isStarted() {
67
+ return this.started;
68
+ }
69
+ get baseUrl() {
70
+ const port = this.boundApiPort;
71
+ if (!this.started || port === undefined)
72
+ throw new Error("medplum server is not started; call start() first");
73
+ return `http://127.0.0.1:${port}/`;
74
+ }
75
+ get apiPort() {
76
+ const port = this.boundApiPort;
77
+ if (!this.started || port === undefined)
78
+ throw new Error("medplum server is not started; call start() first");
79
+ return port;
80
+ }
81
+ get dbPort() {
82
+ const port = this.boundDbPort;
83
+ if (!this.started || port === undefined)
84
+ throw new Error("medplum server is not started; call start() first");
85
+ return port;
86
+ }
87
+ get redisPort() {
88
+ const port = this.boundRedisPort;
89
+ if (!this.started || port === undefined)
90
+ throw new Error("medplum server is not started; call start() first");
91
+ return port;
92
+ }
93
+ get info() {
94
+ const paths = resolveMedplumPaths({
95
+ version: this.options.version,
96
+ cacheDir: this.options.cacheDir,
97
+ });
98
+ const dataDir = this.runDir ?? "";
99
+ const apiPort = this.apiPort;
100
+ const dbPort = this.dbPort;
101
+ const redisPort = this.redisPort;
102
+ return {
103
+ apiPort,
104
+ dbPort,
105
+ redisPort,
106
+ baseUrl: this.baseUrl,
107
+ dataDir,
108
+ paths,
109
+ config: buildServerConfig({
110
+ apiPort,
111
+ dbPort,
112
+ redisPort,
113
+ dataDir,
114
+ }),
115
+ };
116
+ }
117
+ async start() {
118
+ if (this.started)
119
+ return;
120
+ if (this.starting)
121
+ return this.starting;
122
+ this.starting = this.boot();
123
+ try {
124
+ await this.starting;
125
+ }
126
+ finally {
127
+ this.starting = undefined;
128
+ }
129
+ }
130
+ async boot() {
131
+ const dataDir = await mkdtemp(join(tmpdir(), "medplum-mock-"));
132
+ this.runDir = dataDir;
133
+ const apiPort = await findFreePort();
134
+ const dbPort = await findFreePort();
135
+ const redisPort = await findFreePort();
136
+ try {
137
+ this.postgres = await startEmbeddedPostgres(dataDir, dbPort);
138
+ this.redis = new RedisMemoryServer({
139
+ instance: { port: redisPort, ip: "127.0.0.1" },
140
+ });
141
+ await this.redis.start();
142
+ const resolvedRedisPort = await this.redis.getPort();
143
+ const paths = await ensureMedplumBuild({
144
+ version: this.options.version,
145
+ cacheDir: this.options.cacheDir,
146
+ onLog: this.options.onLog,
147
+ });
148
+ const config = buildServerConfig({
149
+ apiPort,
150
+ dbPort,
151
+ redisPort: resolvedRedisPort,
152
+ dataDir,
153
+ });
154
+ await writeFile(join(dataDir, "medplum.config.json"), JSON.stringify(config, null, 2));
155
+ this.child = this.spawnServerProcess(paths, dataDir);
156
+ this.boundApiPort = apiPort;
157
+ this.boundDbPort = dbPort;
158
+ this.boundRedisPort = resolvedRedisPort;
159
+ await pollHealthcheck(`http://127.0.0.1:${apiPort}/`, SERVER_START_TIMEOUT_MS);
160
+ this.started = true;
161
+ this.registerExitGuard();
162
+ this.options.onLog?.(`[medplum-mock] medplum server healthy at ${this.baseUrl}`);
163
+ }
164
+ catch (error) {
165
+ await this.teardown();
166
+ throw error;
167
+ }
168
+ }
169
+ spawnServerProcess(paths, dataDir) {
170
+ const child = spawn(process.execPath, [paths.serverEntry, "file:medplum.config.json"], {
171
+ cwd: dataDir,
172
+ env: process.env,
173
+ stdio: ["ignore", "pipe", "pipe"],
174
+ });
175
+ child.stdout?.on("data", (chunk) => this.options.onLog?.(`[medplum-server] ${chunk.toString().trimEnd()}`));
176
+ child.stderr?.on("data", (chunk) => this.options.onLog?.(`[medplum-server] ${chunk.toString().trimEnd()}`));
177
+ child.on("exit", (code, signal) => {
178
+ if (this.started && this.child === child) {
179
+ this.options.onLog?.(`[medplum-mock] medplum server exited unexpectedly (code=${code} signal=${signal})`);
180
+ }
181
+ });
182
+ return child;
183
+ }
184
+ /**
185
+ * Stop the server, drop and recreate the medplum database (the server re-runs
186
+ * migrations and seeding on boot, restoring pristine state), and boot it again.
187
+ */
188
+ async reset() {
189
+ if (!this.started || this.postgres === undefined) {
190
+ throw new Error("medplum server is not started; call start() first");
191
+ }
192
+ const postgres = this.postgres;
193
+ await this.stopServerProcess();
194
+ await resetMedplumDatabase(postgres);
195
+ const paths = resolveMedplumPaths({
196
+ version: this.options.version,
197
+ cacheDir: this.options.cacheDir,
198
+ });
199
+ const dataDir = this.runDir ?? "";
200
+ this.child = this.spawnServerProcess(paths, dataDir);
201
+ await pollHealthcheck(this.baseUrl, SERVER_START_TIMEOUT_MS);
202
+ }
203
+ async stop() {
204
+ if (!this.started)
205
+ return;
206
+ await this.teardown();
207
+ }
208
+ async stopServerProcess() {
209
+ const child = this.child;
210
+ if (!child || child.exitCode !== null)
211
+ return;
212
+ await new Promise((resolve) => {
213
+ const timer = setTimeout(() => {
214
+ child.kill("SIGKILL");
215
+ resolve();
216
+ }, CHILD_STOP_GRACE_MS);
217
+ child.once("exit", () => {
218
+ clearTimeout(timer);
219
+ resolve();
220
+ });
221
+ child.kill("SIGTERM");
222
+ });
223
+ this.child = undefined;
224
+ }
225
+ async teardown() {
226
+ this.unregisterExitGuard();
227
+ await this.stopServerProcess();
228
+ if (this.redis) {
229
+ try {
230
+ await this.redis.stop();
231
+ }
232
+ catch {
233
+ // already stopped
234
+ }
235
+ this.redis = undefined;
236
+ }
237
+ if (this.postgres) {
238
+ try {
239
+ await this.postgres.stop();
240
+ }
241
+ catch {
242
+ // already stopped
243
+ }
244
+ this.postgres = undefined;
245
+ }
246
+ if (this.runDir && existsSync(this.runDir)) {
247
+ rmSync(this.runDir, { recursive: true, force: true });
248
+ }
249
+ this.runDir = undefined;
250
+ this.boundApiPort = undefined;
251
+ this.boundDbPort = undefined;
252
+ this.boundRedisPort = undefined;
253
+ this.started = false;
254
+ }
255
+ registerExitGuard() {
256
+ if (this.exitGuard)
257
+ return;
258
+ this.exitGuard = () => {
259
+ this.child?.kill("SIGKILL");
260
+ try {
261
+ void this.postgres?.stop();
262
+ }
263
+ catch {
264
+ // process is exiting
265
+ }
266
+ void this.redis?.stop();
267
+ };
268
+ const host = process;
269
+ host.on("exit", this.exitGuard);
270
+ host.on("SIGINT", this.exitGuard);
271
+ host.on("SIGTERM", this.exitGuard);
272
+ }
273
+ unregisterExitGuard() {
274
+ if (!this.exitGuard)
275
+ return;
276
+ const host = process;
277
+ host.off("exit", this.exitGuard);
278
+ host.off("SIGINT", this.exitGuard);
279
+ host.off("SIGTERM", this.exitGuard);
280
+ this.exitGuard = undefined;
281
+ }
282
+ }
283
+ //# sourceMappingURL=process.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"process.js","sourceRoot":"","sources":["../src/process.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,67 @@
1
+ {
2
+ "name": "@crvouga/mockingbird-service-medplum",
3
+ "version": "0.1.0",
4
+ "description": "Stateful Medplum mock that self-hosts the real Medplum server as a child process on embedded Postgres and Redis, exposed through the Mockingbird FetchAPI contract.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "sideEffects": false,
8
+ "files": [
9
+ "dist",
10
+ "README.md"
11
+ ],
12
+ "exports": {
13
+ ".": {
14
+ "types": "./dist/index.d.ts",
15
+ "default": "./dist/index.js"
16
+ }
17
+ },
18
+ "publishConfig": {
19
+ "access": "public",
20
+ "provenance": true
21
+ },
22
+ "repository": {
23
+ "type": "git",
24
+ "url": "git+https://github.com/crvouga/mockingbird.git",
25
+ "directory": "packages/service/medplum"
26
+ },
27
+ "homepage": "https://github.com/crvouga/mockingbird/tree/main/packages/service/medplum#readme",
28
+ "keywords": [
29
+ "mockingbird",
30
+ "service",
31
+ "medplum",
32
+ "fhir"
33
+ ],
34
+ "mockingbird": {
35
+ "runtime": "node",
36
+ "layer": "service"
37
+ },
38
+ "engines": {
39
+ "node": ">=22.18.0"
40
+ },
41
+ "scripts": {
42
+ "generate": "bun ../../openapi/codegen/dist/cli.js",
43
+ "generate:check": "bun ../../openapi/codegen/dist/cli.js --check",
44
+ "openapi:check": "bun ../../openapi/codegen/dist/cli.js --validate",
45
+ "build": "tsc -p tsconfig.build.json",
46
+ "typecheck": "tsc -p tsconfig.json --noEmit",
47
+ "lint": "biome check .",
48
+ "test": "bun test",
49
+ "portability": "bun ../../../scripts/portability.ts",
50
+ "pack:check": "bun ../../../scripts/pack-check.ts"
51
+ },
52
+ "dependencies": {
53
+ "@crvouga/mockingbird-core": "0.0.0-development",
54
+ "embedded-postgres": "16.14.0-beta.17",
55
+ "redis-memory-server": "0.17.1"
56
+ },
57
+ "devDependencies": {
58
+ "@crvouga/mockingbird-openapi": "0.0.0-development",
59
+ "@crvouga/mockingbird-openapi-codegen": "0.1.0",
60
+ "@crvouga/mockingbird-openapi-metadata": "0.0.0-development",
61
+ "@crvouga/mockingbird-parity": "0.0.0-development",
62
+ "@crvouga/mockingbird-testing": "0.1.0",
63
+ "@types/bun": "1.4.0",
64
+ "@types/node": "22.20.1",
65
+ "fast-check": "4.9.0"
66
+ }
67
+ }