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.
@@ -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 {};