vintasend-medplum 0.4.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 +506 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +11 -0
- package/dist/medplum-adapter.d.ts +19 -0
- package/dist/medplum-adapter.js +49 -0
- package/dist/medplum-attachment-manager.d.ts +82 -0
- package/dist/medplum-attachment-manager.js +228 -0
- package/dist/medplum-backend.d.ts +86 -0
- package/dist/medplum-backend.js +831 -0
- package/dist/medplum-logger.d.ts +8 -0
- package/dist/medplum-logger.js +18 -0
- package/package.json +33 -0
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MedplumAttachmentFile = exports.MedplumAttachmentManager = void 0;
|
|
4
|
+
const base_attachment_manager_1 = require("vintasend/dist/services/attachment-manager/base-attachment-manager");
|
|
5
|
+
/**
|
|
6
|
+
* Medplum AttachmentManager implementation.
|
|
7
|
+
*
|
|
8
|
+
* This implementation uses FHIR resources for file storage:
|
|
9
|
+
* - Binary resources store the actual file data
|
|
10
|
+
* - Media resources store file metadata and link to Binary resources
|
|
11
|
+
*
|
|
12
|
+
* Files are uploaded to Medplum's storage and can be accessed via URLs.
|
|
13
|
+
*/
|
|
14
|
+
class MedplumAttachmentManager extends base_attachment_manager_1.BaseAttachmentManager {
|
|
15
|
+
constructor(medplum) {
|
|
16
|
+
super();
|
|
17
|
+
this.medplum = medplum;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Upload a file to Medplum storage using Binary and Media resources.
|
|
21
|
+
*
|
|
22
|
+
* @param file - The file data (Buffer, ReadableStream, or file path)
|
|
23
|
+
* @param filename - The filename to use for storage
|
|
24
|
+
* @param contentType - Optional MIME type (auto-detected if not provided)
|
|
25
|
+
* @returns AttachmentFileRecord with metadata about the uploaded file
|
|
26
|
+
*/
|
|
27
|
+
async uploadFile(file, filename, contentType) {
|
|
28
|
+
var _a, _b;
|
|
29
|
+
// Convert file to Buffer
|
|
30
|
+
const buffer = await this.fileToBuffer(file);
|
|
31
|
+
// Calculate checksum for deduplication
|
|
32
|
+
const checksum = this.calculateChecksum(buffer);
|
|
33
|
+
// Detect content type if not provided
|
|
34
|
+
const finalContentType = contentType || this.detectContentType(filename);
|
|
35
|
+
// Create Binary resource with the file data
|
|
36
|
+
const binary = {
|
|
37
|
+
resourceType: 'Binary',
|
|
38
|
+
contentType: finalContentType,
|
|
39
|
+
data: buffer.toString('base64'),
|
|
40
|
+
};
|
|
41
|
+
const createdBinary = await this.medplum.createResource(binary);
|
|
42
|
+
const binaryUrl = `Binary/${createdBinary.id}`;
|
|
43
|
+
// Create Media resource with metadata
|
|
44
|
+
const media = {
|
|
45
|
+
resourceType: 'Media',
|
|
46
|
+
status: 'completed',
|
|
47
|
+
content: {
|
|
48
|
+
contentType: finalContentType,
|
|
49
|
+
url: binaryUrl,
|
|
50
|
+
size: buffer.length,
|
|
51
|
+
title: filename,
|
|
52
|
+
creation: new Date().toISOString(),
|
|
53
|
+
},
|
|
54
|
+
identifier: [
|
|
55
|
+
{
|
|
56
|
+
system: 'http://vintasend.com/fhir/attachment-checksum',
|
|
57
|
+
value: checksum,
|
|
58
|
+
},
|
|
59
|
+
],
|
|
60
|
+
meta: {
|
|
61
|
+
tag: [
|
|
62
|
+
{ code: 'attachment-file' },
|
|
63
|
+
],
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
const createdMedia = await this.medplum.createResource(media);
|
|
67
|
+
return {
|
|
68
|
+
id: createdMedia.id,
|
|
69
|
+
filename,
|
|
70
|
+
contentType: finalContentType,
|
|
71
|
+
size: buffer.length,
|
|
72
|
+
checksum,
|
|
73
|
+
storageMetadata: {
|
|
74
|
+
url: binaryUrl,
|
|
75
|
+
creation: createdMedia.content.creation,
|
|
76
|
+
},
|
|
77
|
+
createdAt: ((_a = createdMedia.meta) === null || _a === void 0 ? void 0 : _a.lastUpdated) ? new Date(createdMedia.meta.lastUpdated) : new Date(),
|
|
78
|
+
updatedAt: ((_b = createdMedia.meta) === null || _b === void 0 ? void 0 : _b.lastUpdated) ? new Date(createdMedia.meta.lastUpdated) : new Date(),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Retrieve a file record by its Media resource ID.
|
|
83
|
+
*
|
|
84
|
+
* @param fileId - The Media resource ID
|
|
85
|
+
* @returns The file record or null if not found
|
|
86
|
+
*/
|
|
87
|
+
async getFile(fileId) {
|
|
88
|
+
var _a, _b, _c, _d;
|
|
89
|
+
try {
|
|
90
|
+
const media = await this.medplum.readResource('Media', fileId);
|
|
91
|
+
if (!media.content || !media.id) {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
id: media.id,
|
|
96
|
+
filename: media.content.title || 'untitled',
|
|
97
|
+
contentType: media.content.contentType || 'application/octet-stream',
|
|
98
|
+
size: media.content.size || 0,
|
|
99
|
+
checksum: ((_b = (_a = media.identifier) === null || _a === void 0 ? void 0 : _a[0]) === null || _b === void 0 ? void 0 : _b.value) || '',
|
|
100
|
+
storageMetadata: {
|
|
101
|
+
url: media.content.url,
|
|
102
|
+
creation: media.content.creation,
|
|
103
|
+
},
|
|
104
|
+
createdAt: ((_c = media.meta) === null || _c === void 0 ? void 0 : _c.lastUpdated) ? new Date(media.meta.lastUpdated) : new Date(),
|
|
105
|
+
updatedAt: ((_d = media.meta) === null || _d === void 0 ? void 0 : _d.lastUpdated) ? new Date(media.meta.lastUpdated) : new Date(),
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
catch (_e) {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Delete a file from Medplum storage.
|
|
114
|
+
*
|
|
115
|
+
* Deletes both the Media resource (metadata) and Binary resource (file data).
|
|
116
|
+
*
|
|
117
|
+
* @param fileId - The Media resource ID
|
|
118
|
+
*/
|
|
119
|
+
async deleteFile(fileId) {
|
|
120
|
+
var _a;
|
|
121
|
+
// Get Media resource to find the Binary URL
|
|
122
|
+
const media = await this.medplum.readResource('Media', fileId);
|
|
123
|
+
// Extract Binary ID from URL
|
|
124
|
+
const binaryUrl = (_a = media.content) === null || _a === void 0 ? void 0 : _a.url;
|
|
125
|
+
if (binaryUrl) {
|
|
126
|
+
const match = binaryUrl.match(/Binary\/([^/]+)/);
|
|
127
|
+
if (match) {
|
|
128
|
+
const binaryId = match[1];
|
|
129
|
+
// Delete Binary resource
|
|
130
|
+
try {
|
|
131
|
+
await this.medplum.deleteResource('Binary', binaryId);
|
|
132
|
+
}
|
|
133
|
+
catch (error) {
|
|
134
|
+
// eslint-disable-next-line no-console
|
|
135
|
+
console.warn(`Failed to delete Binary resource ${binaryId}:`, error);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
// Delete Media resource
|
|
140
|
+
await this.medplum.deleteResource('Media', fileId);
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Reconstruct an AttachmentFile from storage metadata.
|
|
144
|
+
*
|
|
145
|
+
* @param storageMetadata - Metadata containing either 'url' (Binary URL) or 'binaryId'
|
|
146
|
+
* @returns AttachmentFile instance for accessing the file
|
|
147
|
+
*/
|
|
148
|
+
reconstructAttachmentFile(storageMetadata) {
|
|
149
|
+
// Support both 'url' (from upload) and 'binaryId' (direct ID)
|
|
150
|
+
let binaryUrl;
|
|
151
|
+
if (storageMetadata.binaryId && typeof storageMetadata.binaryId === 'string') {
|
|
152
|
+
// Direct Binary ID format
|
|
153
|
+
binaryUrl = `Binary/${storageMetadata.binaryId}`;
|
|
154
|
+
}
|
|
155
|
+
else if (storageMetadata.url && typeof storageMetadata.url === 'string') {
|
|
156
|
+
// Binary URL format (from uploadFile)
|
|
157
|
+
binaryUrl = storageMetadata.url;
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
throw new Error('Storage metadata must contain binaryId for Medplum files');
|
|
161
|
+
}
|
|
162
|
+
return new MedplumAttachmentFile(this.medplum, binaryUrl);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
exports.MedplumAttachmentManager = MedplumAttachmentManager;
|
|
166
|
+
/**
|
|
167
|
+
* Medplum AttachmentFile implementation.
|
|
168
|
+
*
|
|
169
|
+
* Provides access to files stored in Medplum Binary resources.
|
|
170
|
+
*/
|
|
171
|
+
class MedplumAttachmentFile {
|
|
172
|
+
constructor(medplum, binaryUrl) {
|
|
173
|
+
this.medplum = medplum;
|
|
174
|
+
// Extract Binary ID from URL (format: "Binary/{id}")
|
|
175
|
+
const match = binaryUrl.match(/Binary\/([^/]+)/);
|
|
176
|
+
if (!match) {
|
|
177
|
+
throw new Error(`Invalid Binary URL format: ${binaryUrl}`);
|
|
178
|
+
}
|
|
179
|
+
this.binaryId = match[1];
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Read the entire file into memory as a Buffer.
|
|
183
|
+
*/
|
|
184
|
+
async read() {
|
|
185
|
+
const binary = await this.medplum.readResource('Binary', this.binaryId);
|
|
186
|
+
if (!binary.data) {
|
|
187
|
+
throw new Error('Binary resource has no data');
|
|
188
|
+
}
|
|
189
|
+
return Buffer.from(binary.data, 'base64');
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Get a readable stream for the file.
|
|
193
|
+
*
|
|
194
|
+
* Note: For Medplum, we read the entire file and convert to stream.
|
|
195
|
+
* For true streaming, you'd need to use Medplum's binary download API.
|
|
196
|
+
*/
|
|
197
|
+
async stream() {
|
|
198
|
+
const buffer = await this.read();
|
|
199
|
+
return new ReadableStream({
|
|
200
|
+
start(controller) {
|
|
201
|
+
controller.enqueue(buffer);
|
|
202
|
+
controller.close();
|
|
203
|
+
},
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Generate a URL for accessing the file.
|
|
208
|
+
*
|
|
209
|
+
* For Medplum, this returns the Binary resource URL.
|
|
210
|
+
* In production, you might want to generate a presigned URL.
|
|
211
|
+
*
|
|
212
|
+
* @param expiresIn - Seconds until the URL expires (not used for Medplum)
|
|
213
|
+
* @returns URL for file access
|
|
214
|
+
*/
|
|
215
|
+
async url(expiresIn = 3600) {
|
|
216
|
+
// For Medplum, we can use the Binary resource URL
|
|
217
|
+
// In a production setup, you might want to use Medplum's authentication
|
|
218
|
+
// or generate a time-limited access token
|
|
219
|
+
return `Binary/${this.binaryId}`;
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Delete this file from storage.
|
|
223
|
+
*/
|
|
224
|
+
async delete() {
|
|
225
|
+
await this.medplum.deleteResource('Binary', this.binaryId);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
exports.MedplumAttachmentFile = MedplumAttachmentFile;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { MedplumClient } from '@medplum/core';
|
|
2
|
+
import type { BaseAttachmentManager } from 'vintasend/dist/services/attachment-manager/base-attachment-manager';
|
|
3
|
+
import type { BaseNotificationBackend } from 'vintasend/dist/services/notification-backends/base-notification-backend';
|
|
4
|
+
import type { AttachmentFileRecord, StoredAttachment } from 'vintasend/dist/types/attachment';
|
|
5
|
+
import type { InputJsonValue } from 'vintasend/dist/types/json-values';
|
|
6
|
+
import type { AnyDatabaseNotification, DatabaseNotification, NotificationInput } from 'vintasend/dist/types/notification';
|
|
7
|
+
import type { BaseNotificationTypeConfig } from 'vintasend/dist/types/notification-type-config';
|
|
8
|
+
import type { DatabaseOneOffNotification, OneOffNotificationInput } from 'vintasend/dist/types/one-off-notification';
|
|
9
|
+
type MedplumNotificationBackendOptions = {
|
|
10
|
+
emailNotificationSubjectExtensionUrl?: string;
|
|
11
|
+
};
|
|
12
|
+
export declare class MedplumNotificationBackend<Config extends BaseNotificationTypeConfig> implements BaseNotificationBackend<Config> {
|
|
13
|
+
private medplum;
|
|
14
|
+
private options;
|
|
15
|
+
private attachmentManager?;
|
|
16
|
+
constructor(medplum: MedplumClient, options?: MedplumNotificationBackendOptions);
|
|
17
|
+
/**
|
|
18
|
+
* Inject attachment manager (called by VintaSend when both service and backend exist)
|
|
19
|
+
*/
|
|
20
|
+
injectAttachmentManager(manager: BaseAttachmentManager): void;
|
|
21
|
+
/**
|
|
22
|
+
* Get attachment manager with null check
|
|
23
|
+
*/
|
|
24
|
+
private getAttachmentManager;
|
|
25
|
+
private mapToDatabaseNotification;
|
|
26
|
+
getAllPendingNotifications(): Promise<AnyDatabaseNotification<Config>[]>;
|
|
27
|
+
getPendingNotifications(page: number, pageSize: number): Promise<AnyDatabaseNotification<Config>[]>;
|
|
28
|
+
getAllFutureNotifications(): Promise<AnyDatabaseNotification<Config>[]>;
|
|
29
|
+
getFutureNotifications(page: number, pageSize: number): Promise<AnyDatabaseNotification<Config>[]>;
|
|
30
|
+
getAllFutureNotificationsFromUser(referenceString: Config['UserIdType']): Promise<DatabaseNotification<Config>[]>;
|
|
31
|
+
getFutureNotificationsFromUser(referenceString: Config['UserIdType'], page: number, pageSize: number): Promise<DatabaseNotification<Config>[]>;
|
|
32
|
+
persistNotification(notification: NotificationInput<Config>): Promise<DatabaseNotification<Config>>;
|
|
33
|
+
persistNotificationUpdate(notificationId: Config['NotificationIdType'], notification: Partial<Omit<DatabaseNotification<Config>, 'id'>>): Promise<DatabaseNotification<Config>>;
|
|
34
|
+
getAllNotifications(): Promise<AnyDatabaseNotification<Config>[]>;
|
|
35
|
+
getNotifications(page: number, pageSize: number): Promise<AnyDatabaseNotification<Config>[]>;
|
|
36
|
+
bulkPersistNotifications(notifications: Omit<NotificationInput<Config>, 'id'>[]): Promise<Config['NotificationIdType'][]>;
|
|
37
|
+
markAsSent(notificationId: Config['NotificationIdType'], checkIsPending?: boolean): Promise<AnyDatabaseNotification<Config>>;
|
|
38
|
+
markAsFailed(notificationId: Config['NotificationIdType'], checkIsPending?: boolean): Promise<AnyDatabaseNotification<Config>>;
|
|
39
|
+
markAsRead(notificationId: Config['NotificationIdType'], checkIsSent?: boolean): Promise<DatabaseNotification<Config>>;
|
|
40
|
+
cancelNotification(notificationId: Config['NotificationIdType']): Promise<void>;
|
|
41
|
+
getNotification(notificationId: Config['NotificationIdType'], _forUpdate: boolean): Promise<AnyDatabaseNotification<Config> | null>;
|
|
42
|
+
filterAllInAppUnreadNotifications(refenrenceString: Config['UserIdType']): Promise<DatabaseNotification<Config>[]>;
|
|
43
|
+
filterInAppUnreadNotifications(refenrenceString: Config['UserIdType'], page: number, pageSize: number): Promise<DatabaseNotification<Config>[]>;
|
|
44
|
+
getUserEmailFromNotification(notificationId: Config['NotificationIdType']): Promise<string | undefined>;
|
|
45
|
+
storeContextUsed(notificationId: Config['NotificationIdType'], context: InputJsonValue): Promise<void>;
|
|
46
|
+
persistOneOffNotification(notification: Omit<OneOffNotificationInput<Config>, 'id'>): Promise<DatabaseOneOffNotification<Config>>;
|
|
47
|
+
persistOneOffNotificationUpdate(notificationId: Config['NotificationIdType'], notification: Partial<Omit<DatabaseOneOffNotification<Config>, 'id'>>): Promise<DatabaseOneOffNotification<Config>>;
|
|
48
|
+
getOneOffNotification(notificationId: Config['NotificationIdType'], forUpdate: boolean): Promise<DatabaseOneOffNotification<Config> | null>;
|
|
49
|
+
getAllOneOffNotifications(): Promise<DatabaseOneOffNotification<Config>[]>;
|
|
50
|
+
getOneOffNotifications(page: number, pageSize: number): Promise<DatabaseOneOffNotification<Config>[]>;
|
|
51
|
+
/**
|
|
52
|
+
* Helper: Convert FileAttachment to Buffer
|
|
53
|
+
*/
|
|
54
|
+
private fileToBuffer;
|
|
55
|
+
/**
|
|
56
|
+
* Helper: Convert FHIR Media resource to AttachmentFileRecord
|
|
57
|
+
*/
|
|
58
|
+
private mediaToAttachmentFileRecord;
|
|
59
|
+
/**
|
|
60
|
+
* Helper: Create FHIR Attachment from file record
|
|
61
|
+
*/
|
|
62
|
+
private createFhirAttachment;
|
|
63
|
+
/**
|
|
64
|
+
* Helper: Create MedplumAttachmentFile from storage metadata
|
|
65
|
+
*/
|
|
66
|
+
private createMedplumAttachmentFile;
|
|
67
|
+
getAttachmentFile(fileId: string): Promise<AttachmentFileRecord | null>;
|
|
68
|
+
findAttachmentFileByChecksum(checksum: string): Promise<AttachmentFileRecord | null>;
|
|
69
|
+
/**
|
|
70
|
+
* Find multiple attachment files by their checksums in a single query
|
|
71
|
+
*/
|
|
72
|
+
private findAttachmentFilesByChecksums;
|
|
73
|
+
/**
|
|
74
|
+
* Helper: Process attachments and return payload items
|
|
75
|
+
* Optimizes by batching checksum lookups
|
|
76
|
+
*/
|
|
77
|
+
private processAttachments;
|
|
78
|
+
deleteAttachmentFile(fileId: string): Promise<void>;
|
|
79
|
+
getOrphanedAttachmentFiles(): Promise<AttachmentFileRecord[]>;
|
|
80
|
+
getAttachments(notificationId: Config['NotificationIdType']): Promise<StoredAttachment[]>;
|
|
81
|
+
deleteNotificationAttachment(notificationId: Config['NotificationIdType'], attachmentId: string): Promise<void>;
|
|
82
|
+
}
|
|
83
|
+
export declare class MedplumNotificationBackendFactory<Config extends BaseNotificationTypeConfig> {
|
|
84
|
+
create(medplum: MedplumClient): MedplumNotificationBackend<Config>;
|
|
85
|
+
}
|
|
86
|
+
export {};
|