@relayfile/adapter-core 0.1.19 → 0.1.20

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.
Files changed (57) hide show
  1. package/dist/src/index.d.ts +1 -0
  2. package/dist/src/index.d.ts.map +1 -1
  3. package/dist/src/index.js +1 -0
  4. package/dist/src/index.js.map +1 -1
  5. package/dist/src/pubsub/index.d.ts +2 -0
  6. package/dist/src/pubsub/index.d.ts.map +1 -0
  7. package/dist/src/pubsub/index.js +2 -0
  8. package/dist/src/pubsub/index.js.map +1 -0
  9. package/dist/src/pubsub/storage-bridge.d.ts +3 -0
  10. package/dist/src/pubsub/storage-bridge.d.ts.map +1 -0
  11. package/dist/src/pubsub/storage-bridge.js +2 -0
  12. package/dist/src/pubsub/storage-bridge.js.map +1 -0
  13. package/dist/src/storage-bridge/adapter-worker.d.ts +98 -0
  14. package/dist/src/storage-bridge/adapter-worker.d.ts.map +1 -0
  15. package/dist/src/storage-bridge/adapter-worker.js +169 -0
  16. package/dist/src/storage-bridge/adapter-worker.js.map +1 -0
  17. package/dist/src/storage-bridge/discovery.d.ts +32 -0
  18. package/dist/src/storage-bridge/discovery.d.ts.map +1 -0
  19. package/dist/src/storage-bridge/discovery.js +53 -0
  20. package/dist/src/storage-bridge/discovery.js.map +1 -0
  21. package/dist/src/storage-bridge/event.d.ts +70 -0
  22. package/dist/src/storage-bridge/event.d.ts.map +1 -0
  23. package/dist/src/storage-bridge/event.js +178 -0
  24. package/dist/src/storage-bridge/event.js.map +1 -0
  25. package/dist/src/storage-bridge/index.d.ts +7 -0
  26. package/dist/src/storage-bridge/index.d.ts.map +1 -0
  27. package/dist/src/storage-bridge/index.js +7 -0
  28. package/dist/src/storage-bridge/index.js.map +1 -0
  29. package/dist/src/storage-bridge/nango-fallback.d.ts +36 -0
  30. package/dist/src/storage-bridge/nango-fallback.d.ts.map +1 -0
  31. package/dist/src/storage-bridge/nango-fallback.js +194 -0
  32. package/dist/src/storage-bridge/nango-fallback.js.map +1 -0
  33. package/dist/src/storage-bridge/publisher.d.ts +54 -0
  34. package/dist/src/storage-bridge/publisher.d.ts.map +1 -0
  35. package/dist/src/storage-bridge/publisher.js +90 -0
  36. package/dist/src/storage-bridge/publisher.js.map +1 -0
  37. package/dist/src/storage-bridge/writeback.d.ts +41 -0
  38. package/dist/src/storage-bridge/writeback.d.ts.map +1 -0
  39. package/dist/src/storage-bridge/writeback.js +85 -0
  40. package/dist/src/storage-bridge/writeback.js.map +1 -0
  41. package/dist/src/writeback/index.d.ts +2 -0
  42. package/dist/src/writeback/index.d.ts.map +1 -0
  43. package/dist/src/writeback/index.js +2 -0
  44. package/dist/src/writeback/index.js.map +1 -0
  45. package/dist/src/writeback/storage-bridge.d.ts +5 -0
  46. package/dist/src/writeback/storage-bridge.d.ts.map +1 -0
  47. package/dist/src/writeback/storage-bridge.js +3 -0
  48. package/dist/src/writeback/storage-bridge.js.map +1 -0
  49. package/dist/tests/storage-bridge/all-priority-adapters.test.d.ts +2 -0
  50. package/dist/tests/storage-bridge/all-priority-adapters.test.d.ts.map +1 -0
  51. package/dist/tests/storage-bridge/all-priority-adapters.test.js +1014 -0
  52. package/dist/tests/storage-bridge/all-priority-adapters.test.js.map +1 -0
  53. package/dist/tests/storage-bridge/storage-bridge.test.d.ts +2 -0
  54. package/dist/tests/storage-bridge/storage-bridge.test.d.ts.map +1 -0
  55. package/dist/tests/storage-bridge/storage-bridge.test.js +212 -0
  56. package/dist/tests/storage-bridge/storage-bridge.test.js.map +1 -0
  57. package/package.json +1 -1
@@ -0,0 +1,1014 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { EventEmitter } from "node:events";
4
+ import { StorageBridgeAdapterWorker, mapNangoSyncRecord as mapCoreNangoSyncRecord } from "../../src/storage-bridge/index.js";
5
+ const WORKSPACE_ID = "ws_storage_bridge";
6
+ const DETECTED_AT = "2026-05-09T08:30:05.000Z";
7
+ const adapterFixtures = [
8
+ {
9
+ source: "google-drive",
10
+ provider: "google-drive",
11
+ payload: {
12
+ kind: "api#channel",
13
+ headers: {
14
+ "x-goog-channel-id": "chan-drive-1",
15
+ "x-goog-resource-id": "drive-resource-9",
16
+ "x-goog-resource-state": "update",
17
+ "x-goog-message-number": "42"
18
+ },
19
+ change: {
20
+ time: "2026-05-09T08:30:00.000Z",
21
+ fileId: "file_123",
22
+ file: {
23
+ id: "file_123",
24
+ name: "Roadmap.pdf",
25
+ mimeType: "application/pdf",
26
+ size: "120",
27
+ md5Checksum: "md5-drive",
28
+ driveId: "drive_acme",
29
+ parents: ["folder_reports"]
30
+ }
31
+ },
32
+ accountId: "acct_google"
33
+ },
34
+ expected: {
35
+ eventId: "google-drive:chan-drive-1:42:file_123",
36
+ changeType: "updated",
37
+ relayfilePath: "/google-drive/acct_google/Roadmap.pdf",
38
+ resourceId: "file_123",
39
+ sizeBytes: 120,
40
+ fingerprint: "md5-drive",
41
+ metadataKeys: ["accountId", "channelId", "driveId", "file", "resourceId"]
42
+ },
43
+ content: {
44
+ contentBase64: Buffer.from("drive-pdf-bytes").toString("base64"),
45
+ contentType: "application/pdf"
46
+ },
47
+ writeback: {
48
+ draftPath: "/google-drive/files/draft-roadmap.json",
49
+ canonicalPath: "/google-drive/files/file_123.json",
50
+ providerCreateTarget: "drive.files.create",
51
+ providerDeleteTarget: "drive.files.delete"
52
+ }
53
+ },
54
+ {
55
+ source: "gcs",
56
+ provider: "gcs",
57
+ payload: {
58
+ messageId: "pubsub-gcs-88",
59
+ publishTime: "2026-05-09T08:31:00.000Z",
60
+ attributes: {
61
+ eventType: "OBJECT_FINALIZE",
62
+ bucketId: "rf-archive",
63
+ objectId: "reports/q2.json",
64
+ objectGeneration: "1715243460"
65
+ },
66
+ data: {
67
+ bucket: "rf-archive",
68
+ name: "reports/q2.json",
69
+ size: "19",
70
+ md5Hash: "gcs-md5",
71
+ contentType: "application/json"
72
+ }
73
+ },
74
+ expected: {
75
+ eventId: "gcs:pubsub-gcs-88:rf-archive:reports/q2.json:1715243460",
76
+ changeType: "created",
77
+ relayfilePath: "/gcs/rf-archive/reports/q2.json",
78
+ resourceId: "rf-archive/reports/q2.json#1715243460",
79
+ sizeBytes: 19,
80
+ fingerprint: "gcs-md5",
81
+ metadataKeys: ["bucket", "generation", "messageId", "object", "pubsub"]
82
+ },
83
+ content: {
84
+ contentBase64: Buffer.from('{"ok":true}').toString("base64"),
85
+ contentType: "application/json"
86
+ },
87
+ writeback: {
88
+ draftPath: "/gcs/rf-archive/objects/reports/q2-draft.json",
89
+ canonicalPath: "/gcs/rf-archive/objects/reports/q2.json",
90
+ providerCreateTarget: "gcs.bucket.upload",
91
+ providerDeleteTarget: "gcs.file.delete"
92
+ }
93
+ },
94
+ {
95
+ source: "sharepoint",
96
+ provider: "sharepoint",
97
+ payload: {
98
+ subscriptionId: "sub-sp-1",
99
+ clientState: "state",
100
+ resource: "sites/site-a/drives/drive-a/root",
101
+ tenantId: "tenant-a",
102
+ delta: {
103
+ id: "item-sp-1",
104
+ name: "Plan.docx",
105
+ eTag: "etag-sp-1",
106
+ cTag: "ctag-sp-1",
107
+ size: 400,
108
+ lastModifiedDateTime: "2026-05-09T08:32:00.000Z",
109
+ parentReference: { siteId: "site-a", driveId: "drive-a", path: "/drive/root:/Shared Documents" },
110
+ file: { mimeType: "application/vnd.openxmlformats-officedocument.wordprocessingml.document" }
111
+ }
112
+ },
113
+ expected: {
114
+ eventId: "sharepoint:sub-sp-1:item-sp-1:etag-sp-1",
115
+ changeType: "updated",
116
+ relayfilePath: "/sharepoint/site-a/drive-a/Shared Documents/Plan.docx",
117
+ resourceId: "site-a/drive-a/item-sp-1",
118
+ sizeBytes: 400,
119
+ fingerprint: "etag-sp-1",
120
+ metadataKeys: ["delta", "driveId", "siteId", "subscriptionId", "tenantId"]
121
+ },
122
+ content: {
123
+ contentBase64: Buffer.from("sharepoint-docx").toString("base64"),
124
+ contentType: "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
125
+ },
126
+ writeback: {
127
+ draftPath: "/sharepoint/site-a/drive-a/items/draft-plan.json",
128
+ canonicalPath: "/sharepoint/site-a/drive-a/items/item-sp-1.json",
129
+ providerCreateTarget: "graph.driveItem.putContent",
130
+ providerDeleteTarget: "graph.driveItem.delete"
131
+ }
132
+ },
133
+ {
134
+ source: "onedrive",
135
+ provider: "onedrive",
136
+ payload: {
137
+ subscriptionId: "sub-od-1",
138
+ accountId: "acct_one",
139
+ driveId: "drive-one",
140
+ delta: {
141
+ id: "item-od-1",
142
+ name: "Budget.xlsx",
143
+ eTag: "etag-od-1",
144
+ size: 512,
145
+ lastModifiedDateTime: "2026-05-09T08:33:00.000Z",
146
+ parentReference: { driveId: "drive-one", path: "/drive/root:/Finance" },
147
+ file: { mimeType: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" }
148
+ }
149
+ },
150
+ expected: {
151
+ eventId: "onedrive:sub-od-1:item-od-1:etag-od-1",
152
+ changeType: "updated",
153
+ relayfilePath: "/onedrive/acct_one/Finance/Budget.xlsx",
154
+ resourceId: "drive-one/item-od-1",
155
+ sizeBytes: 512,
156
+ fingerprint: "etag-od-1",
157
+ metadataKeys: ["accountId", "delta", "driveId", "subscriptionId"]
158
+ },
159
+ content: {
160
+ contentBase64: Buffer.from("onedrive-xlsx").toString("base64"),
161
+ contentType: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
162
+ },
163
+ writeback: {
164
+ draftPath: "/onedrive/acct_one/items/draft-budget.json",
165
+ canonicalPath: "/onedrive/acct_one/items/item-od-1.json",
166
+ providerCreateTarget: "graph.driveItem.putContent",
167
+ providerDeleteTarget: "graph.driveItem.delete"
168
+ }
169
+ },
170
+ {
171
+ source: "azure-blob",
172
+ provider: "azure-blob",
173
+ payload: {
174
+ id: "eventgrid-az-1",
175
+ eventType: "Microsoft.Storage.BlobCreated",
176
+ eventTime: "2026-05-09T08:34:00.000Z",
177
+ subject: "/blobServices/default/containers/invoices/blobs/2026/may.csv",
178
+ data: {
179
+ api: "PutBlob",
180
+ contentLength: 2048,
181
+ contentType: "text/csv",
182
+ eTag: "etag-az-1",
183
+ url: "https://acct.blob.core.windows.net/invoices/2026/may.csv"
184
+ },
185
+ account: "acct"
186
+ },
187
+ expected: {
188
+ eventId: "azure-blob:eventgrid-az-1",
189
+ changeType: "created",
190
+ relayfilePath: "/azure/acct/invoices/2026/may.csv",
191
+ resourceId: "acct/invoices/2026/may.csv",
192
+ sizeBytes: 2048,
193
+ fingerprint: "etag-az-1",
194
+ metadataKeys: ["account", "container", "eventGrid", "subject"]
195
+ },
196
+ content: {
197
+ contentBase64: Buffer.from("id,total\n1,42").toString("base64"),
198
+ contentType: "text/csv"
199
+ },
200
+ writeback: {
201
+ draftPath: "/azure/acct/invoices/blobs/2026/may-draft.csv",
202
+ canonicalPath: "/azure/acct/invoices/blobs/2026/may.csv",
203
+ providerCreateTarget: "azure.blob.upload",
204
+ providerDeleteTarget: "azure.blob.delete"
205
+ }
206
+ },
207
+ {
208
+ source: "dropbox",
209
+ provider: "dropbox",
210
+ payload: {
211
+ listFolderCursor: "cursor-2",
212
+ accountId: "acct_dbx",
213
+ entries: [
214
+ {
215
+ ".tag": "file",
216
+ id: "id:dbx-file",
217
+ name: "notes.md",
218
+ path_lower: "/team/notes.md",
219
+ path_display: "/Team/Notes.md",
220
+ server_modified: "2026-05-09T08:35:00.000Z",
221
+ rev: "rev-1",
222
+ size: 33,
223
+ content_hash: "hash-dbx"
224
+ }
225
+ ]
226
+ },
227
+ expected: {
228
+ eventId: "dropbox:acct_dbx:cursor-2:/team/notes.md:rev-1",
229
+ changeType: "updated",
230
+ relayfilePath: "/dropbox/acct_dbx/Team/Notes.md",
231
+ resourceId: "id:dbx-file",
232
+ sizeBytes: 33,
233
+ fingerprint: "hash-dbx",
234
+ metadataKeys: ["accountId", "cursor", "entry", "pathLower"]
235
+ },
236
+ content: {
237
+ contentBase64: Buffer.from("# Notes").toString("base64"),
238
+ contentType: "text/markdown"
239
+ },
240
+ writeback: {
241
+ draftPath: "/dropbox/acct_dbx/files/Team/Notes-draft.json",
242
+ canonicalPath: "/dropbox/acct_dbx/files/Team/Notes.md.json",
243
+ providerCreateTarget: "dropbox.files.upload",
244
+ providerDeleteTarget: "dropbox.files.delete_v2"
245
+ }
246
+ },
247
+ {
248
+ source: "gmail",
249
+ provider: "gmail",
250
+ payload: {
251
+ messageId: "pubsub-gmail-1",
252
+ publishTime: "2026-05-09T08:36:00.000Z",
253
+ account: "me@example.com",
254
+ history: {
255
+ id: "hist-20",
256
+ messagesAdded: [{ message: { id: "msg-1", threadId: "thread-1", labelIds: ["INBOX"] } }]
257
+ },
258
+ thread: {
259
+ id: "thread-1",
260
+ historyId: "hist-20",
261
+ messages: [{ id: "msg-1", threadId: "thread-1", snippet: "hello" }]
262
+ }
263
+ },
264
+ expected: {
265
+ eventId: "gmail:me@example.com:hist-20:thread-1",
266
+ changeType: "created",
267
+ relayfilePath: "/gmail/me@example.com/threads/thread-1.json",
268
+ resourceId: "thread-1",
269
+ sizeBytes: null,
270
+ fingerprint: "hist-20",
271
+ metadataKeys: ["account", "history", "messageId", "thread"]
272
+ },
273
+ content: {
274
+ contentBase64: Buffer.from(JSON.stringify({ id: "thread-1", messages: 1 })).toString("base64"),
275
+ contentType: "application/json"
276
+ },
277
+ writeback: {
278
+ draftPath: "/gmail/me@example.com/drafts/draft-subject.json",
279
+ canonicalPath: "/gmail/me@example.com/threads/thread-1.json",
280
+ providerCreateTarget: "gmail.users.drafts.create",
281
+ providerDeleteTarget: "gmail.users.messages.modify"
282
+ }
283
+ },
284
+ {
285
+ source: "s3",
286
+ provider: "s3",
287
+ payload: {
288
+ messageId: "sqs-s3-1",
289
+ receiptHandle: "rh-1",
290
+ Records: [
291
+ {
292
+ eventName: "ObjectCreated:Put",
293
+ eventTime: "2026-05-09T08:37:00.000Z",
294
+ s3: {
295
+ bucket: { name: "rf-bucket" },
296
+ object: { key: "logs/app.log", size: 70, eTag: "etag-s3", sequencer: "006" }
297
+ }
298
+ }
299
+ ]
300
+ },
301
+ expected: {
302
+ eventId: "s3:sqs-s3-1:rf-bucket:logs/app.log:006",
303
+ changeType: "created",
304
+ relayfilePath: "/s3/rf-bucket/logs/app.log",
305
+ resourceId: "rf-bucket/logs/app.log",
306
+ sizeBytes: 70,
307
+ fingerprint: "etag-s3",
308
+ metadataKeys: ["bucket", "object", "receiptHandle", "sqs"]
309
+ },
310
+ content: {
311
+ contentBase64: Buffer.from("INFO app started").toString("base64"),
312
+ contentType: "text/plain"
313
+ },
314
+ writeback: {
315
+ draftPath: "/s3/rf-bucket/objects/logs/app-draft.log",
316
+ canonicalPath: "/s3/rf-bucket/objects/logs/app.log",
317
+ providerCreateTarget: "s3.PutObject",
318
+ providerDeleteTarget: "s3.DeleteObject"
319
+ }
320
+ },
321
+ {
322
+ source: "box",
323
+ provider: "box",
324
+ payload: {
325
+ id: "box-webhook-1",
326
+ trigger: "FILE.UPLOADED",
327
+ created_at: "2026-05-09T08:38:00.000Z",
328
+ source: {
329
+ id: "box-file-1",
330
+ type: "file",
331
+ name: "Contract.pdf",
332
+ etag: "etag-box-1",
333
+ size: 900,
334
+ path_collection: { entries: [{ id: "0", name: "All Files" }, { id: "f1", name: "Legal" }] }
335
+ },
336
+ accountId: "acct_box"
337
+ },
338
+ expected: {
339
+ eventId: "box:box-webhook-1:box-file-1:etag-box-1",
340
+ changeType: "created",
341
+ relayfilePath: "/box/acct_box/Legal/Contract.pdf",
342
+ resourceId: "box-file-1",
343
+ sizeBytes: 900,
344
+ fingerprint: "etag-box-1",
345
+ metadataKeys: ["accountId", "boxSource", "trigger", "webhookId"]
346
+ },
347
+ content: {
348
+ contentBase64: Buffer.from("box-pdf-bytes").toString("base64"),
349
+ contentType: "application/pdf"
350
+ },
351
+ writeback: {
352
+ draftPath: "/box/files/draft-contract.json",
353
+ canonicalPath: "/box/files/box-file-1.json",
354
+ providerCreateTarget: "box.files.uploadFile",
355
+ providerDeleteTarget: "box.files.delete"
356
+ }
357
+ },
358
+ {
359
+ source: "postgres",
360
+ provider: "postgres",
361
+ payload: {
362
+ channel: "relayfile_storage_events",
363
+ processId: 100,
364
+ notification: {
365
+ database: "appdb",
366
+ schema: "public",
367
+ table: "documents",
368
+ op: "INSERT",
369
+ pk: "42",
370
+ occurred_at: "2026-05-09T08:39:00.000Z",
371
+ row_json: { id: 42, title: "Bridge plan", updated_at: "2026-05-09T08:39:00.000Z" },
372
+ txid: "7331"
373
+ }
374
+ },
375
+ expected: {
376
+ eventId: "postgres:appdb:public.documents:42:7331",
377
+ changeType: "created",
378
+ relayfilePath: "/postgres/appdb/public/documents/42.json",
379
+ resourceId: "appdb/public/documents/42",
380
+ sizeBytes: null,
381
+ fingerprint: "7331",
382
+ metadataKeys: ["channel", "postgres", "processId"]
383
+ },
384
+ content: {
385
+ contentBase64: Buffer.from(JSON.stringify({ id: 42, title: "Bridge plan" })).toString("base64"),
386
+ contentType: "application/json"
387
+ },
388
+ writeback: {
389
+ draftPath: "/postgres/appdb/public/documents/draft-bridge-plan.json",
390
+ canonicalPath: "/postgres/appdb/public/documents/42.json",
391
+ providerCreateTarget: "postgres.INSERT",
392
+ providerDeleteTarget: "postgres.DELETE"
393
+ }
394
+ },
395
+ {
396
+ source: "redis",
397
+ provider: "redis",
398
+ payload: {
399
+ pattern: "__keyspace@0__:*",
400
+ channel: "__keyspace@0__:session:42",
401
+ message: "set",
402
+ db: 0,
403
+ key: "session:42",
404
+ type: "hash",
405
+ value: { userId: "u1", expiresAt: "2026-05-10T00:00:00.000Z" },
406
+ detectedAt: "2026-05-09T08:40:00.000Z"
407
+ },
408
+ expected: {
409
+ eventId: "redis:0:session:42:set:2026-05-09T08:40:00.000Z",
410
+ changeType: "updated",
411
+ relayfilePath: "/redis/0/session:42.json",
412
+ resourceId: "0/session:42",
413
+ sizeBytes: null,
414
+ fingerprint: null,
415
+ metadataKeys: ["channel", "db", "key", "pattern", "redis"]
416
+ },
417
+ content: {
418
+ contentBase64: Buffer.from(JSON.stringify({ userId: "u1" })).toString("base64"),
419
+ contentType: "application/json"
420
+ },
421
+ writeback: {
422
+ draftPath: "/redis/0/session:43.json",
423
+ canonicalPath: "/redis/0/session:42.json",
424
+ providerCreateTarget: "redis.HSET",
425
+ providerDeleteTarget: "redis.DEL"
426
+ }
427
+ }
428
+ ];
429
+ const nangoFixtures = [
430
+ {
431
+ providerConfigKey: "google-drive",
432
+ source: "google-drive",
433
+ syncName: "documents",
434
+ model: "File",
435
+ record: {
436
+ id: "file_123",
437
+ name: "Roadmap.pdf",
438
+ mimeType: "application/pdf",
439
+ modifiedTime: "2026-05-09T08:30:00.000Z",
440
+ size: "120",
441
+ driveId: "drive_acme",
442
+ webViewLink: "https://drive.google.com/file/d/file_123/view"
443
+ },
444
+ expectedPath: "/google-drive/conn_google/Roadmap.pdf",
445
+ expectedResourceId: "file_123"
446
+ },
447
+ {
448
+ providerConfigKey: "sharepoint-online",
449
+ source: "sharepoint",
450
+ syncName: "user-files",
451
+ model: "UserFileMetadata",
452
+ record: {
453
+ siteId: "site-a",
454
+ id: "item-sp-1",
455
+ name: "Plan.docx",
456
+ etag: "etag-sp-1",
457
+ cTag: "ctag-sp-1",
458
+ is_folder: false,
459
+ mime_type: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
460
+ path: "/Shared Documents/Plan.docx",
461
+ updated_at: "2026-05-09T08:32:00.000Z",
462
+ created_at: "2026-05-01T00:00:00.000Z",
463
+ blob_size: 400,
464
+ raw_source: {}
465
+ },
466
+ expectedPath: "/sharepoint/site-a/drive-default/Shared Documents/Plan.docx",
467
+ expectedResourceId: "site-a/drive-default/item-sp-1"
468
+ },
469
+ {
470
+ providerConfigKey: "one-drive",
471
+ source: "onedrive",
472
+ syncName: "user-files",
473
+ model: "OneDriveFile",
474
+ record: {
475
+ id: "item-od-1",
476
+ name: "Budget.xlsx",
477
+ etag: "etag-od-1",
478
+ cTag: "ctag-od-1",
479
+ is_folder: false,
480
+ mime_type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
481
+ path: "/Finance/Budget.xlsx",
482
+ updated_at: "2026-05-09T08:33:00.000Z",
483
+ created_at: "2026-05-01T00:00:00.000Z",
484
+ blob_size: 512,
485
+ drive_id: "drive-one",
486
+ raw_source: {}
487
+ },
488
+ expectedPath: "/onedrive/conn_one/Finance/Budget.xlsx",
489
+ expectedResourceId: "drive-one/item-od-1"
490
+ },
491
+ {
492
+ providerConfigKey: "dropbox",
493
+ source: "dropbox",
494
+ syncName: "files",
495
+ model: "File",
496
+ record: {
497
+ id: "/team/notes.md",
498
+ dropbox_id: "id:dbx-file",
499
+ name: "notes.md",
500
+ path_lower: "/team/notes.md",
501
+ path_display: "/Team/Notes.md",
502
+ server_modified: "2026-05-09T08:35:00.000Z",
503
+ client_modified: "2026-05-09T08:34:00.000Z",
504
+ rev: "rev-1",
505
+ size: 33,
506
+ content_hash: "hash-dbx"
507
+ },
508
+ expectedPath: "/dropbox/conn_dbx/Team/Notes.md",
509
+ expectedResourceId: "id:dbx-file"
510
+ },
511
+ {
512
+ providerConfigKey: "google-mail",
513
+ source: "gmail",
514
+ syncName: "threads",
515
+ model: "Thread",
516
+ record: {
517
+ id: "thread-1",
518
+ historyId: "hist-20",
519
+ messages: [{ id: "msg-1", threadId: "thread-1", snippet: "hello" }]
520
+ },
521
+ expectedPath: "/gmail/me@example.com/threads/thread-1.json",
522
+ expectedResourceId: "thread-1"
523
+ },
524
+ {
525
+ providerConfigKey: "box",
526
+ source: "box",
527
+ syncName: "files",
528
+ model: "BoxDocument",
529
+ record: {
530
+ id: "box-file-1",
531
+ name: "Contract.pdf",
532
+ modified_at: "2026-05-09T08:38:00.000Z",
533
+ download_url: "https://box.example/download/box-file-1"
534
+ },
535
+ expectedPath: "/box/conn_box/Contract.pdf",
536
+ expectedResourceId: "box-file-1"
537
+ }
538
+ ];
539
+ test("normalizes realistic provider webhook and pubsub fixtures into StorageBridgeEvent envelopes", () => {
540
+ for (const fixture of adapterFixtures) {
541
+ const event = normalizeProviderPayload(fixture);
542
+ assert.equal(event.source, fixture.source, fixture.source);
543
+ assert.equal(event.workspaceId, WORKSPACE_ID, fixture.source);
544
+ assert.equal(event.detectedAt, DETECTED_AT, fixture.source);
545
+ assert.equal(event.eventId, fixture.expected.eventId, fixture.source);
546
+ assert.equal(event.changeType, fixture.expected.changeType, fixture.source);
547
+ assert.equal(event.relayfilePath, fixture.expected.relayfilePath, fixture.source);
548
+ assert.equal(event.resourceId, fixture.expected.resourceId, fixture.source);
549
+ assert.equal(event.sizeBytes, fixture.expected.sizeBytes, fixture.source);
550
+ assert.equal(event.fingerprint, fixture.expected.fingerprint, fixture.source);
551
+ for (const key of fixture.expected.metadataKeys) {
552
+ assert.ok(key in event.metadata, `${fixture.source} metadata should include ${key}`);
553
+ }
554
+ assert.match(event.occurredAt, /^\d{4}-\d{2}-\d{2}T/);
555
+ }
556
+ });
557
+ test("deduplicates duplicate delivery and emits one relayfile ingest envelope per event id", async () => {
558
+ const worker = new MockStorageBridgeWorker();
559
+ for (const fixture of adapterFixtures) {
560
+ const event = normalizeProviderPayload(fixture);
561
+ await worker.handleEvent(event, fixture);
562
+ await worker.handleEvent(event, fixture);
563
+ }
564
+ assert.equal(worker.ingested.length, adapterFixtures.length);
565
+ for (const ingest of worker.ingested) {
566
+ const fixture = adapterFixtures.find((item) => item.expected.eventId === ingest.delivery_id);
567
+ assert.ok(fixture, `unexpected ingest ${ingest.delivery_id}`);
568
+ assert.equal(ingest.provider, fixture.provider);
569
+ assert.equal(ingest.event_type, `file.${fixture.expected.changeType}`);
570
+ assert.equal(ingest.path, fixture.expected.relayfilePath);
571
+ assert.equal(ingest.timestamp, normalizeProviderPayload(fixture).occurredAt);
572
+ assert.equal(ingest.data.content_base64, fixture.content.contentBase64);
573
+ assert.equal(ingest.data.content_type, fixture.content.contentType);
574
+ assert.deepEqual(ingest.semantics.properties, {
575
+ resourceId: fixture.expected.resourceId,
576
+ fingerprint: fixture.expected.fingerprint,
577
+ sizeBytes: fixture.expected.sizeBytes
578
+ });
579
+ }
580
+ });
581
+ test("delivers every normalized fixture through the core adapter worker envelope and idempotency path", async () => {
582
+ for (const fixture of adapterFixtures) {
583
+ const event = normalizeProviderPayload(fixture);
584
+ const ingested = [];
585
+ let fetches = 0;
586
+ const worker = new StorageBridgeAdapterWorker({
587
+ provider: fixture.provider,
588
+ workspaceId: WORKSPACE_ID,
589
+ config: { source: fixture.source },
590
+ publisher: { publish: async () => ({ eventId: event.eventId, published: true, duplicate: false }), subscribe: () => ({ unsubscribe() { } }) },
591
+ fetchContent: async (candidate) => {
592
+ fetches += 1;
593
+ assert.equal(candidate.eventId, fixture.expected.eventId, fixture.source);
594
+ return {
595
+ body: Buffer.from(fixture.content.contentBase64 ?? "", "base64"),
596
+ contentType: fixture.content.contentType ?? undefined,
597
+ metadata: { fetchedFrom: fixture.provider }
598
+ };
599
+ },
600
+ client: {
601
+ async ingestWebhook(input) {
602
+ ingested.push(input);
603
+ }
604
+ }
605
+ });
606
+ const first = await worker.handleEvent(event);
607
+ const duplicate = await worker.handleEvent(event);
608
+ assert.equal(first.delivered, true, fixture.source);
609
+ assert.equal(first.duplicate, false, fixture.source);
610
+ assert.equal(duplicate.delivered, false, fixture.source);
611
+ assert.equal(duplicate.duplicate, true, fixture.source);
612
+ assert.equal(fetches, 1, fixture.source);
613
+ assert.equal(ingested.length, 1, fixture.source);
614
+ assert.equal(ingested[0]?.workspaceId, WORKSPACE_ID, fixture.source);
615
+ assert.equal(ingested[0]?.provider, fixture.provider, fixture.source);
616
+ assert.equal(ingested[0]?.event_type, `file.${fixture.expected.changeType}`, fixture.source);
617
+ assert.equal(ingested[0]?.path, fixture.expected.relayfilePath, fixture.source);
618
+ assert.equal(ingested[0]?.delivery_id, fixture.expected.eventId, fixture.source);
619
+ assert.equal(ingested[0]?.timestamp, event.occurredAt, fixture.source);
620
+ assert.equal(ingested[0]?.data.contentBase64, fixture.content.contentBase64, fixture.source);
621
+ assert.equal(ingested[0]?.data.contentType, fixture.content.contentType, fixture.source);
622
+ assert.equal(ingested[0]?.data.resourceId, fixture.expected.resourceId, fixture.source);
623
+ assert.equal(ingested[0]?.semantics.properties["storage_bridge.source"], fixture.source, fixture.source);
624
+ assert.equal(ingested[0]?.semantics.properties["storage_bridge.delivery_id"], fixture.expected.eventId, fixture.source);
625
+ }
626
+ });
627
+ test("maps content fetch and file-native writeback paths to provider operations", () => {
628
+ for (const fixture of adapterFixtures) {
629
+ const event = normalizeProviderPayload(fixture);
630
+ const content = fetchContent(event, fixture);
631
+ const create = mapWritebackPath("create", fixture.writeback.draftPath);
632
+ const patch = mapWritebackPath("update", fixture.writeback.canonicalPath);
633
+ const deletion = mapWritebackPath("delete", fixture.writeback.canonicalPath);
634
+ assert.deepEqual(content, fixture.content, fixture.source);
635
+ assert.equal(create.providerOperation, fixture.writeback.providerCreateTarget, fixture.source);
636
+ assert.equal(create.isCanonical, false, fixture.source);
637
+ assert.equal(patch.isCanonical, true, fixture.source);
638
+ assert.equal(deletion.providerOperation, fixture.writeback.providerDeleteTarget, fixture.source);
639
+ assert.equal(deletion.isCanonical, true, fixture.source);
640
+ }
641
+ });
642
+ test("maps Nango sync-complete fallback payloads through the exported core mapper", () => {
643
+ for (const fixture of nangoFixtures) {
644
+ const payload = {
645
+ providerConfigKey: fixture.providerConfigKey,
646
+ connectionId: connectionFor(fixture),
647
+ syncName: fixture.syncName,
648
+ model: fixture.model,
649
+ records: [{ model: fixture.model, ...fixture.record }]
650
+ };
651
+ const events = payload.records.map((record) => mapCoreNangoSyncRecord({
652
+ providerConfigKey: payload.providerConfigKey,
653
+ connectionId: payload.connectionId,
654
+ accountId: accountFor(fixture),
655
+ syncName: payload.syncName,
656
+ workspaceId: WORKSPACE_ID,
657
+ detectedAt: DETECTED_AT,
658
+ record
659
+ }));
660
+ assert.equal(events.length, 1, fixture.providerConfigKey);
661
+ const event = events[0];
662
+ assert.ok(event, fixture.providerConfigKey);
663
+ const nango = event.metadata.nango;
664
+ assert.equal(event.source, fixture.source, fixture.providerConfigKey);
665
+ assert.equal(event.workspaceId, WORKSPACE_ID, fixture.providerConfigKey);
666
+ assert.equal(nango.providerConfigKey, fixture.providerConfigKey, fixture.providerConfigKey);
667
+ assert.equal(nango.connectionId, payload.connectionId, fixture.providerConfigKey);
668
+ assert.equal(nango.syncName, fixture.syncName, fixture.providerConfigKey);
669
+ assert.equal(nango.record.model, fixture.model, fixture.providerConfigKey);
670
+ assert.match(event.eventId, new RegExp(`^${fixture.source}:`), fixture.providerConfigKey);
671
+ assert.ok(event.relayfilePath.startsWith(`/${fixture.source === "google-drive" ? "google-drive" : fixture.source}`), fixture.providerConfigKey);
672
+ }
673
+ });
674
+ test("maps Nango sync-complete fallback records using template model names and shapes", () => {
675
+ for (const fixture of nangoFixtures) {
676
+ const event = mapNangoSyncRecord({
677
+ connectionId: connectionFor(fixture),
678
+ records: [fixture.record],
679
+ providerConfigKey: fixture.providerConfigKey,
680
+ syncName: fixture.syncName,
681
+ model: fixture.model,
682
+ timestamp: "2026-05-09T08:45:00.000Z"
683
+ });
684
+ assert.equal(event.source, fixture.source, fixture.providerConfigKey);
685
+ assert.equal(event.eventId, `nango:${fixture.providerConfigKey}:${fixture.syncName}:${fixture.model}:${fixture.expectedResourceId}`);
686
+ assert.equal(event.changeType, "updated");
687
+ assert.equal(event.relayfilePath, fixture.expectedPath);
688
+ assert.equal(event.resourceId, fixture.expectedResourceId);
689
+ assert.deepEqual(event.metadata.nango, {
690
+ providerConfigKey: fixture.providerConfigKey,
691
+ syncName: fixture.syncName,
692
+ model: fixture.model,
693
+ connectionId: connectionFor(fixture)
694
+ });
695
+ }
696
+ });
697
+ test("models the Postgres PGlite LISTEN/NOTIFY path with a trigger-shaped payload", () => {
698
+ const harness = new PGliteLikeHarness();
699
+ const published = [];
700
+ harness.on("notification", (payload) => {
701
+ published.push(normalizeProviderPayload(postgresFixtureFromNotification(payload)));
702
+ });
703
+ harness.insert("appdb", "public", "documents", { id: 42, title: "Bridge plan" });
704
+ assert.equal(published.length, 1);
705
+ assert.equal(published[0]?.source, "postgres");
706
+ assert.equal(published[0]?.relayfilePath, "/postgres/appdb/public/documents/42.json");
707
+ assert.deepEqual((published[0]?.metadata.postgres).row_json, {
708
+ id: 42,
709
+ title: "Bridge plan"
710
+ });
711
+ });
712
+ test("routes mocked Redis keyspace notifications to relayfile paths", () => {
713
+ const redis = new MockRedisKeyspace();
714
+ const published = [];
715
+ redis.psubscribe("__keyspace@0__:*", (pattern, channel, message) => {
716
+ published.push(normalizeProviderPayload({
717
+ ...fixtureBySource("redis"),
718
+ payload: {
719
+ pattern,
720
+ channel,
721
+ message,
722
+ db: 0,
723
+ key: channel.replace("__keyspace@0__:", ""),
724
+ type: "string",
725
+ value: "enabled",
726
+ detectedAt: "2026-05-09T08:41:00.000Z"
727
+ },
728
+ expected: {
729
+ ...fixtureBySource("redis").expected,
730
+ eventId: "redis:0:feature:flag:set:2026-05-09T08:41:00.000Z",
731
+ relayfilePath: "/redis/0/feature:flag",
732
+ resourceId: "0/feature:flag"
733
+ }
734
+ }));
735
+ });
736
+ redis.set("feature:flag", "enabled");
737
+ assert.equal(published.length, 1);
738
+ assert.equal(published[0]?.changeType, "updated");
739
+ assert.equal(published[0]?.relayfilePath, "/redis/0/feature:flag");
740
+ assert.deepEqual((published[0]?.metadata.redis).value, "enabled");
741
+ });
742
+ function normalizeProviderPayload(fixture) {
743
+ const payload = fixture.payload;
744
+ switch (fixture.source) {
745
+ case "google-drive": {
746
+ const headers = payload.headers;
747
+ const change = payload.change;
748
+ const file = change.file;
749
+ return event(fixture, change.time, {
750
+ accountId: payload.accountId,
751
+ channelId: headers["x-goog-channel-id"],
752
+ resourceId: headers["x-goog-resource-id"],
753
+ driveId: file.driveId,
754
+ file
755
+ });
756
+ }
757
+ case "gcs": {
758
+ const data = payload.data;
759
+ const attrs = payload.attributes;
760
+ return event(fixture, payload.publishTime, {
761
+ bucket: data.bucket,
762
+ object: data.name,
763
+ generation: attrs.objectGeneration,
764
+ messageId: payload.messageId,
765
+ pubsub: { attributes: attrs }
766
+ });
767
+ }
768
+ case "sharepoint": {
769
+ const delta = payload.delta;
770
+ const parent = delta.parentReference;
771
+ return event(fixture, delta.lastModifiedDateTime, {
772
+ subscriptionId: payload.subscriptionId,
773
+ tenantId: payload.tenantId,
774
+ siteId: parent.siteId,
775
+ driveId: parent.driveId,
776
+ delta
777
+ });
778
+ }
779
+ case "onedrive": {
780
+ const delta = payload.delta;
781
+ return event(fixture, delta.lastModifiedDateTime, {
782
+ subscriptionId: payload.subscriptionId,
783
+ accountId: payload.accountId,
784
+ driveId: payload.driveId,
785
+ delta
786
+ });
787
+ }
788
+ case "azure-blob": {
789
+ const data = payload.data;
790
+ return event(fixture, payload.eventTime, {
791
+ account: payload.account,
792
+ container: "invoices",
793
+ subject: payload.subject,
794
+ eventGrid: { eventType: payload.eventType, data }
795
+ });
796
+ }
797
+ case "dropbox": {
798
+ const entry = payload.entries[0];
799
+ assert.ok(entry);
800
+ return event(fixture, entry.server_modified, {
801
+ accountId: payload.accountId,
802
+ cursor: payload.listFolderCursor,
803
+ pathLower: entry.path_lower,
804
+ entry
805
+ });
806
+ }
807
+ case "gmail": {
808
+ const history = payload.history;
809
+ return event(fixture, payload.publishTime, {
810
+ account: payload.account,
811
+ history,
812
+ thread: payload.thread,
813
+ messageId: payload.messageId
814
+ });
815
+ }
816
+ case "s3": {
817
+ const record = payload.Records[0];
818
+ assert.ok(record);
819
+ const s3 = record.s3;
820
+ return event(fixture, record.eventTime, {
821
+ bucket: s3.bucket.name,
822
+ object: s3.object,
823
+ receiptHandle: payload.receiptHandle,
824
+ sqs: { messageId: payload.messageId }
825
+ });
826
+ }
827
+ case "box": {
828
+ const source = payload.source;
829
+ return event(fixture, payload.created_at, {
830
+ accountId: payload.accountId,
831
+ webhookId: payload.id,
832
+ trigger: payload.trigger,
833
+ boxSource: source
834
+ });
835
+ }
836
+ case "postgres": {
837
+ const notification = payload.notification;
838
+ return event(fixture, notification.occurred_at, {
839
+ channel: payload.channel,
840
+ processId: payload.processId,
841
+ postgres: notification
842
+ });
843
+ }
844
+ case "redis":
845
+ return event(fixture, payload.detectedAt ?? DETECTED_AT, {
846
+ pattern: payload.pattern,
847
+ channel: payload.channel,
848
+ db: payload.db,
849
+ key: payload.key,
850
+ redis: { type: payload.type, value: payload.value, message: payload.message }
851
+ });
852
+ }
853
+ }
854
+ function event(fixture, occurredAt, metadata) {
855
+ return {
856
+ eventId: fixture.expected.eventId,
857
+ occurredAt,
858
+ detectedAt: DETECTED_AT,
859
+ source: fixture.source,
860
+ changeType: fixture.expected.changeType,
861
+ relayfilePath: fixture.expected.relayfilePath,
862
+ resourceId: fixture.expected.resourceId,
863
+ sizeBytes: fixture.expected.sizeBytes,
864
+ fingerprint: fixture.expected.fingerprint,
865
+ metadata,
866
+ workspaceId: WORKSPACE_ID
867
+ };
868
+ }
869
+ function fetchContent(event, fixture) {
870
+ assert.equal(event.eventId, fixture.expected.eventId);
871
+ return fixture.content;
872
+ }
873
+ function mapWritebackPath(operation, path) {
874
+ const fixture = adapterFixtures.find((item) => item.writeback.draftPath === path || item.writeback.canonicalPath === path);
875
+ assert.ok(fixture, `unknown writeback path ${path}`);
876
+ const isCanonical = path === fixture.writeback.canonicalPath;
877
+ if (operation === "create") {
878
+ return { providerOperation: fixture.writeback.providerCreateTarget, isCanonical };
879
+ }
880
+ if (operation === "delete") {
881
+ return { providerOperation: fixture.writeback.providerDeleteTarget, isCanonical };
882
+ }
883
+ return { providerOperation: fixture.writeback.providerCreateTarget.replace(/create|upload|putContent|INSERT|PutObject|HSET/i, "update"), isCanonical };
884
+ }
885
+ function mapNangoSyncRecord(input) {
886
+ const record = input.records[0];
887
+ assert.ok(record);
888
+ const fixture = nangoFixtures.find((item) => item.providerConfigKey === input.providerConfigKey &&
889
+ item.syncName === input.syncName &&
890
+ item.model === input.model);
891
+ assert.ok(fixture, `missing Nango fixture for ${input.providerConfigKey}/${input.syncName}/${input.model}`);
892
+ return {
893
+ eventId: `nango:${input.providerConfigKey}:${input.syncName}:${input.model}:${fixture.expectedResourceId}`,
894
+ occurredAt: record.modifiedTime ??
895
+ record.updated_at ??
896
+ record.server_modified ??
897
+ record.modified_at ??
898
+ input.timestamp,
899
+ detectedAt: DETECTED_AT,
900
+ source: fixture.source,
901
+ changeType: "updated",
902
+ relayfilePath: fixture.expectedPath,
903
+ resourceId: fixture.expectedResourceId,
904
+ sizeBytes: typeof record.size === "string" ? Number(record.size) : record.blob_size ?? null,
905
+ fingerprint: record.md5Checksum ??
906
+ record.etag ??
907
+ record.content_hash ??
908
+ record.historyId ??
909
+ null,
910
+ metadata: {
911
+ nango: {
912
+ providerConfigKey: input.providerConfigKey,
913
+ syncName: input.syncName,
914
+ model: input.model,
915
+ connectionId: input.connectionId
916
+ },
917
+ record
918
+ },
919
+ workspaceId: WORKSPACE_ID
920
+ };
921
+ }
922
+ function connectionFor(fixture) {
923
+ switch (fixture.source) {
924
+ case "google-drive":
925
+ return "conn_google";
926
+ case "sharepoint":
927
+ return "conn_sharepoint";
928
+ case "onedrive":
929
+ return "conn_one";
930
+ case "dropbox":
931
+ return "conn_dbx";
932
+ case "gmail":
933
+ return "me@example.com";
934
+ case "box":
935
+ return "conn_box";
936
+ default:
937
+ return "conn";
938
+ }
939
+ }
940
+ function accountFor(fixture) {
941
+ if (fixture.source === "gmail")
942
+ return "me@example.com";
943
+ return connectionFor(fixture);
944
+ }
945
+ function fixtureBySource(source) {
946
+ const fixture = adapterFixtures.find((item) => item.source === source);
947
+ assert.ok(fixture, `missing fixture for ${source}`);
948
+ return fixture;
949
+ }
950
+ function postgresFixtureFromNotification(notification) {
951
+ return {
952
+ ...fixtureBySource("postgres"),
953
+ payload: {
954
+ channel: "relayfile_storage_events",
955
+ processId: 100,
956
+ notification
957
+ }
958
+ };
959
+ }
960
+ class MockStorageBridgeWorker {
961
+ ingested = [];
962
+ seen = new Set();
963
+ async handleEvent(event, fixture) {
964
+ if (this.seen.has(event.eventId)) {
965
+ return;
966
+ }
967
+ this.seen.add(event.eventId);
968
+ const content = fetchContent(event, fixture);
969
+ this.ingested.push({
970
+ provider: fixture.provider,
971
+ event_type: `file.${event.changeType}`,
972
+ path: event.relayfilePath,
973
+ delivery_id: event.eventId,
974
+ timestamp: event.occurredAt,
975
+ data: {
976
+ content_base64: content.contentBase64,
977
+ content_type: content.contentType
978
+ },
979
+ semantics: {
980
+ properties: {
981
+ resourceId: event.resourceId,
982
+ fingerprint: event.fingerprint,
983
+ sizeBytes: event.sizeBytes
984
+ }
985
+ }
986
+ });
987
+ }
988
+ }
989
+ class PGliteLikeHarness extends EventEmitter {
990
+ insert(database, schema, table, row) {
991
+ this.emit("notification", {
992
+ database,
993
+ schema,
994
+ table,
995
+ op: "INSERT",
996
+ pk: String(row.id),
997
+ occurred_at: "2026-05-09T08:39:00.000Z",
998
+ row_json: row,
999
+ txid: "7331"
1000
+ });
1001
+ }
1002
+ }
1003
+ class MockRedisKeyspace {
1004
+ handler;
1005
+ psubscribe(pattern, handler) {
1006
+ assert.equal(pattern, "__keyspace@0__:*");
1007
+ this.handler = handler;
1008
+ }
1009
+ set(key, _value) {
1010
+ assert.ok(this.handler, "psubscribe should be called before set");
1011
+ this.handler("__keyspace@0__:*", `__keyspace@0__:${key}`, "set");
1012
+ }
1013
+ }
1014
+ //# sourceMappingURL=all-priority-adapters.test.js.map