@strapi/data-transfer 5.10.4 → 5.11.1

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/dist/index.mjs CHANGED
@@ -1,4 +1,5 @@
1
- import { Transform, PassThrough, Writable, Readable, Duplex, pipeline } from 'stream';
1
+ import { Transform, PassThrough, Writable, Readable, Duplex, pipeline as pipeline$1 } from 'stream';
2
+ import { pipeline } from 'stream/promises';
2
3
  import path, { extname, join, posix } from 'path';
3
4
  import { EOL } from 'os';
4
5
  import { chain } from 'stream-chain';
@@ -633,8 +634,7 @@ const TRANSFER_STAGES = Object.freeze([
633
634
  };
634
635
  const DEFAULT_VERSION_STRATEGY = 'ignore';
635
636
  const DEFAULT_SCHEMA_STRATEGY = 'strict';
636
- var _metadata$1 = /*#__PURE__*/ _class_private_field_loose_key$6("_metadata"), _schema = /*#__PURE__*/ _class_private_field_loose_key$6("_schema"), _handlers = /*#__PURE__*/ _class_private_field_loose_key$6("_handlers"), // Save the currently open stream so that we can access it at any time
637
- _currentStream = /*#__PURE__*/ _class_private_field_loose_key$6("_currentStream"), /**
637
+ var _metadata$1 = /*#__PURE__*/ _class_private_field_loose_key$6("_metadata"), _schema = /*#__PURE__*/ _class_private_field_loose_key$6("_schema"), _handlers = /*#__PURE__*/ _class_private_field_loose_key$6("_handlers"), _currentStreamController = /*#__PURE__*/ _class_private_field_loose_key$6("_currentStreamController"), _aborted = /*#__PURE__*/ _class_private_field_loose_key$6("_aborted"), /**
638
638
  * Create and return a transform stream based on the given stage and options.
639
639
  *
640
640
  * Allowed transformations includes 'filter' and 'map'.
@@ -749,11 +749,9 @@ class TransferEngine {
749
749
  }
750
750
  // Cause an ongoing transfer to abort gracefully
751
751
  async abortTransfer() {
752
- const err = new TransferEngineError('fatal', 'Transfer aborted.');
753
- if (!_class_private_field_loose_base$6(this, _currentStream)[_currentStream]) {
754
- throw err;
755
- }
756
- _class_private_field_loose_base$6(this, _currentStream)[_currentStream].destroy(err);
752
+ _class_private_field_loose_base$6(this, _aborted)[_aborted] = true;
753
+ _class_private_field_loose_base$6(this, _currentStreamController)[_currentStreamController]?.abort();
754
+ throw new TransferEngineError('fatal', 'Transfer aborted.');
757
755
  }
758
756
  async init() {
759
757
  // Resolve providers' resource and store
@@ -1063,7 +1061,11 @@ class TransferEngine {
1063
1061
  writable: true,
1064
1062
  value: void 0
1065
1063
  });
1066
- Object.defineProperty(this, _currentStream, {
1064
+ Object.defineProperty(this, _currentStreamController, {
1065
+ writable: true,
1066
+ value: void 0
1067
+ });
1068
+ Object.defineProperty(this, _aborted, {
1067
1069
  writable: true,
1068
1070
  value: void 0
1069
1071
  });
@@ -1073,6 +1075,7 @@ class TransferEngine {
1073
1075
  schemaDiff: [],
1074
1076
  errors: {}
1075
1077
  };
1078
+ _class_private_field_loose_base$6(this, _aborted)[_aborted] = false;
1076
1079
  this.diagnostics = createDiagnosticReporter();
1077
1080
  validateProvider('source', sourceProvider);
1078
1081
  validateProvider('destination', destinationProvider);
@@ -1271,6 +1274,9 @@ function assertSchemasMatching(sourceSchemas, destinationSchemas) {
1271
1274
  }
1272
1275
  }
1273
1276
  async function transferStage(options) {
1277
+ if (_class_private_field_loose_base$6(this, _aborted)[_aborted]) {
1278
+ throw new TransferEngineError('fatal', 'Transfer aborted.');
1279
+ }
1274
1280
  const { stage, source, destination, transform, tracker } = options;
1275
1281
  const updateEndTime = ()=>{
1276
1282
  const stageData = this.progress.data[stage];
@@ -1302,27 +1308,42 @@ async function transferStage(options) {
1302
1308
  return;
1303
1309
  }
1304
1310
  _class_private_field_loose_base$6(this, _emitStageUpdate)[_emitStageUpdate]('start', stage);
1305
- await new Promise((resolve, reject)=>{
1306
- let stream = source;
1311
+ try {
1312
+ const streams = [
1313
+ source
1314
+ ];
1307
1315
  if (transform) {
1308
- stream = stream.pipe(transform);
1316
+ streams.push(transform);
1309
1317
  }
1310
1318
  if (tracker) {
1311
- stream = stream.pipe(tracker);
1312
- }
1313
- _class_private_field_loose_base$6(this, _currentStream)[_currentStream] = stream.pipe(destination).on('error', (e)=>{
1314
- updateEndTime();
1315
- _class_private_field_loose_base$6(this, _emitStageUpdate)[_emitStageUpdate]('error', stage);
1316
- this.reportError(e, 'error');
1317
- destination.destroy(e);
1318
- reject(e);
1319
- }).on('close', ()=>{
1320
- _class_private_field_loose_base$6(this, _currentStream)[_currentStream] = undefined;
1321
- updateEndTime();
1322
- resolve();
1319
+ streams.push(tracker);
1320
+ }
1321
+ streams.push(destination);
1322
+ // NOTE: to debug/confirm backpressure issues from misbehaving stream, uncomment the following lines
1323
+ // source.on('pause', () => console.log(`[${stage}] Source paused due to backpressure`));
1324
+ // source.on('resume', () => console.log(`[${stage}] Source resumed`));
1325
+ // destination.on('drain', () =>
1326
+ // console.log(`[${stage}] Destination drained, resuming data flow`)
1327
+ // );
1328
+ // destination.on('error', (err) => console.error(`[${stage}] Destination error:`, err));
1329
+ const controller = new AbortController();
1330
+ const { signal } = controller;
1331
+ // Store the controller so you can cancel later
1332
+ _class_private_field_loose_base$6(this, _currentStreamController)[_currentStreamController] = controller;
1333
+ await pipeline(streams, {
1334
+ signal
1323
1335
  });
1324
- });
1325
- _class_private_field_loose_base$6(this, _emitStageUpdate)[_emitStageUpdate]('finish', stage);
1336
+ _class_private_field_loose_base$6(this, _emitStageUpdate)[_emitStageUpdate]('finish', stage);
1337
+ } catch (e) {
1338
+ updateEndTime();
1339
+ _class_private_field_loose_base$6(this, _emitStageUpdate)[_emitStageUpdate]('error', stage);
1340
+ this.reportError(e, 'error');
1341
+ if (!destination.destroyed) {
1342
+ destination.destroy(e);
1343
+ }
1344
+ } finally{
1345
+ updateEndTime();
1346
+ }
1326
1347
  }
1327
1348
  async function resolveProviderResource() {
1328
1349
  const sourceMetadata = await this.sourceProvider.getMetadata();
@@ -1894,9 +1915,7 @@ const createLinkQuery = (strapi, trx)=>{
1894
1915
  assignOrderColumns();
1895
1916
  const qb = connection.insert(payload).into(addSchema(joinTable.name));
1896
1917
  if (trx) {
1897
- await trx.transaction(async (nestedTrx)=>{
1898
- await qb.transacting(nestedTrx);
1899
- });
1918
+ await qb.transacting(trx);
1900
1919
  }
1901
1920
  }
1902
1921
  if ('morphColumn' in attribute && attribute.morphColumn) {
@@ -2349,7 +2368,7 @@ class LocalStrapiDestinationProvider {
2349
2368
  const provider = strapi.config.get('plugin::upload').provider;
2350
2369
  const fileId = fileEntitiesMapper?.[uploadData.id];
2351
2370
  if (!fileId) {
2352
- callback(new Error(`File ID not found for ID: ${uploadData.id}`));
2371
+ return callback(new Error(`File ID not found for ID: ${uploadData.id}`));
2353
2372
  }
2354
2373
  try {
2355
2374
  await strapi.plugin('upload').provider.uploadStream(uploadData);
@@ -2400,9 +2419,9 @@ class LocalStrapiDestinationProvider {
2400
2419
  provider
2401
2420
  }
2402
2421
  });
2403
- callback();
2422
+ return callback();
2404
2423
  } catch (error) {
2405
- callback(new Error(`Error while uploading asset ${chunk.filename} ${error}`));
2424
+ return callback(new Error(`Error while uploading asset ${chunk.filename} ${error}`));
2406
2425
  }
2407
2426
  });
2408
2427
  }
@@ -2939,7 +2958,7 @@ function reportError(message, error) {
2939
2958
  }
2940
2959
  function handleStreamError(streamType, err) {
2941
2960
  const { message, stack } = err;
2942
- const errorMessage = `Error in ${streamType} read stream: ${message}`;
2961
+ const errorMessage = `[Data transfer] Error in ${streamType} read stream: ${message}`;
2943
2962
  const formattedError = {
2944
2963
  message: errorMessage,
2945
2964
  stack,
@@ -3111,17 +3130,6 @@ const connectToWebsocket = (address, options, diagnostics)=>{
3111
3130
  const trimTrailingSlash = (input)=>{
3112
3131
  return input.replace(/\/$/, '');
3113
3132
  };
3114
- const wait = (ms)=>{
3115
- return new Promise((resolve)=>{
3116
- setTimeout(resolve, ms);
3117
- });
3118
- };
3119
- const waitUntil = async (test, interval)=>{
3120
- while(!test()){
3121
- await wait(interval);
3122
- }
3123
- return Promise.resolve();
3124
- };
3125
3133
 
3126
3134
  const TRANSFER_PATH = '/transfer/runner';
3127
3135
  const TRANSFER_METHODS = [
@@ -3542,6 +3550,17 @@ class RemoteStrapiSourceProvider {
3542
3550
  });
3543
3551
  // Init the asset map
3544
3552
  const assets = {};
3553
+ // Watch for stalled assets; if we don't receive a chunk within timeout, abort transfer
3554
+ const resetTimeout = (assetID)=>{
3555
+ if (assets[assetID].timeout) {
3556
+ clearTimeout(assets[assetID].timeout);
3557
+ }
3558
+ assets[assetID].timeout = setTimeout(()=>{
3559
+ _class_private_field_loose_base$2(this, _reportInfo$2)[_reportInfo$2](`Asset ${assetID} transfer stalled, aborting.`);
3560
+ assets[assetID].status = 'errored';
3561
+ assets[assetID].stream.destroy(new Error(`Asset ${assetID} transfer timed out`));
3562
+ }, this.options.streamTimeout);
3563
+ };
3545
3564
  stream/**
3546
3565
  * Process a payload of many transfer assets and performs the following tasks:
3547
3566
  * - Start: creates a stream for new assets.
@@ -3552,56 +3571,46 @@ class RemoteStrapiSourceProvider {
3552
3571
  const { action, assetID } = item;
3553
3572
  // Creates the stream to send the incoming asset through
3554
3573
  if (action === 'start') {
3555
- // Ignore the item if a transfer has already been started for the same asset ID
3574
+ // if a transfer has already been started for the same asset ID, something is wrong
3556
3575
  if (assets[assetID]) {
3557
- continue;
3576
+ throw new Error(`Asset ${assetID} already started`);
3558
3577
  }
3578
+ _class_private_field_loose_base$2(this, _reportInfo$2)[_reportInfo$2](`Asset ${assetID} starting`);
3559
3579
  // Register the asset
3560
3580
  assets[assetID] = {
3561
3581
  ...item.data,
3562
3582
  stream: new PassThrough(),
3563
- status: 'idle',
3583
+ status: 'ok',
3564
3584
  queue: []
3565
3585
  };
3586
+ resetTimeout(assetID);
3566
3587
  // Connect the individual asset stream to the main asset stage stream
3567
3588
  // Note: nothing is transferred until data chunks are fed to the asset stream
3568
3589
  await this.writeAsync(pass, assets[assetID]);
3569
- } else if (action === 'stream') {
3570
- // If the asset hasn't been registered, or if it's been closed already, then ignore the message
3590
+ } else if (action === 'stream' || action === 'end') {
3591
+ // If the asset hasn't been registered, or if it's been closed already, something is wrong
3571
3592
  if (!assets[assetID]) {
3572
- continue;
3593
+ throw new Error(`No id matching ${assetID} for stream action`);
3573
3594
  }
3574
- switch(assets[assetID].status){
3575
- // The asset is ready to accept a new chunk, write it now
3576
- case 'idle':
3577
- await writeAssetChunk(assetID, item.data);
3578
- break;
3579
- // The resource is busy, queue the current chunk so that it gets transferred as soon as possible
3580
- case 'busy':
3581
- assets[assetID].queue.push(item);
3582
- break;
3595
+ // On every action, reset the timeout timer
3596
+ if (action === 'stream') {
3597
+ resetTimeout(assetID);
3598
+ } else {
3599
+ clearTimeout(assets[assetID].timeout);
3583
3600
  }
3584
- } else if (action === 'end') {
3585
- // If the asset has already been closed, or if it was never registered, ignore the command
3586
- if (!assets[assetID]) {
3587
- continue;
3601
+ if (assets[assetID].status === 'closed') {
3602
+ throw new Error(`Asset ${assetID} is closed`);
3588
3603
  }
3589
- switch(assets[assetID].status){
3590
- // There's no ongoing activity, the asset is ready to be closed
3591
- case 'idle':
3592
- case 'errored':
3593
- await closeAssetStream(assetID);
3594
- break;
3595
- // The resource is busy, wait for a different state and close the stream.
3596
- case 'busy':
3597
- await Promise.race([
3598
- // Either: wait for the asset to be ready to be closed
3599
- waitUntil(()=>assets[assetID].status !== 'busy', 100),
3600
- // Or: if the last chunks are still not processed after ten seconds
3601
- wait(10000)
3602
- ]);
3603
- await closeAssetStream(assetID);
3604
- break;
3604
+ assets[assetID].queue.push(item);
3605
+ }
3606
+ }
3607
+ // each new payload will start new processQueue calls, which may cause some extra calls
3608
+ // it's essentially saying "start processing this asset again, I added more data to the queue"
3609
+ for(const assetID in assets){
3610
+ if (Object.prototype.hasOwnProperty.call(assets, assetID)) {
3611
+ const asset = assets[assetID];
3612
+ if (asset.queue?.length > 0) {
3613
+ await processQueue(assetID);
3605
3614
  }
3606
3615
  }
3607
3616
  }
@@ -3609,38 +3618,48 @@ class RemoteStrapiSourceProvider {
3609
3618
  pass.end();
3610
3619
  });
3611
3620
  /**
3612
- * Writes a chunk of data for the specified asset with the given id.
3613
- */ const writeAssetChunk = async (id, data)=>{
3621
+ * Start processing the queue for a given assetID
3622
+ *
3623
+ * Even though this is a loop that attempts to process the entire queue, it is safe to call this more than once
3624
+ * for the same asset id because the queue is shared globally, the items are shifted off, and immediately written
3625
+ */ const processQueue = async (id)=>{
3614
3626
  if (!assets[id]) {
3615
3627
  throw new Error(`Failed to write asset chunk for "${id}". Asset not found.`);
3616
3628
  }
3617
- const { status: currentStatus } = assets[id];
3618
- if (currentStatus !== 'idle') {
3629
+ const asset = assets[id];
3630
+ const { status: currentStatus } = asset;
3631
+ if ([
3632
+ 'closed',
3633
+ 'errored'
3634
+ ].includes(currentStatus)) {
3619
3635
  throw new Error(`Failed to write asset chunk for "${id}". The asset is currently "${currentStatus}"`);
3620
3636
  }
3621
- const nextItemInQueue = ()=>assets[id].queue.shift();
3622
- try {
3623
- // Lock the asset
3624
- assets[id].status = 'busy';
3625
- // Save the current chunk
3626
- await unsafe_writeAssetChunk(id, data);
3627
- // Empty the queue if needed
3628
- let item = nextItemInQueue();
3629
- while(item){
3630
- await unsafe_writeAssetChunk(id, item.data);
3631
- item = nextItemInQueue();
3637
+ while(asset.queue.length > 0){
3638
+ const data = asset.queue.shift();
3639
+ if (!data) {
3640
+ throw new Error(`Invalid chunk found for ${id}`);
3641
+ }
3642
+ try {
3643
+ // if this is an end chunk, close the asset stream
3644
+ if (data.action === 'end') {
3645
+ _class_private_field_loose_base$2(this, _reportInfo$2)[_reportInfo$2](`Ending asset stream for ${id}`);
3646
+ await closeAssetStream(id);
3647
+ break; // Exit the loop after closing the stream
3648
+ }
3649
+ // Save the current chunk
3650
+ await writeChunkToStream(id, data);
3651
+ } catch {
3652
+ if (!assets[id]) {
3653
+ throw new Error(`No id matching ${id} for writeAssetChunk`);
3654
+ }
3632
3655
  }
3633
- // Unlock the asset
3634
- assets[id].status = 'idle';
3635
- } catch {
3636
- assets[id].status = 'errored';
3637
3656
  }
3638
3657
  };
3639
3658
  /**
3640
3659
  * Writes a chunk of data to the asset's stream.
3641
3660
  *
3642
3661
  * Only check if the targeted asset exists, no other validation is done.
3643
- */ const unsafe_writeAssetChunk = async (id, data)=>{
3662
+ */ const writeChunkToStream = async (id, data)=>{
3644
3663
  const asset = assets[id];
3645
3664
  if (!asset) {
3646
3665
  throw new Error(`Failed to write asset chunk for "${id}". Asset not found.`);
@@ -3661,9 +3680,11 @@ class RemoteStrapiSourceProvider {
3661
3680
  await new Promise((resolve, reject)=>{
3662
3681
  const { stream } = assets[id];
3663
3682
  stream.on('close', ()=>{
3664
- delete assets[id];
3665
3683
  resolve();
3666
- }).on('error', reject).end();
3684
+ }).on('error', (e)=>{
3685
+ assets[id].status = 'errored';
3686
+ reject(new Error(`Failed to close asset "${id}". Asset stream error: ${e.toString()}`));
3687
+ }).end();
3667
3688
  });
3668
3689
  };
3669
3690
  return pass;
@@ -3778,6 +3799,9 @@ class RemoteStrapiSourceProvider {
3778
3799
  });
3779
3800
  this.name = 'source::remote-strapi';
3780
3801
  this.type = 'source';
3802
+ this.defaultOptions = {
3803
+ streamTimeout: 15000
3804
+ };
3781
3805
  this.writeAsync = (stream, data)=>{
3782
3806
  return new Promise((resolve, reject)=>{
3783
3807
  stream.write(data, (error)=>{
@@ -3788,7 +3812,10 @@ class RemoteStrapiSourceProvider {
3788
3812
  });
3789
3813
  });
3790
3814
  };
3791
- this.options = options;
3815
+ this.options = {
3816
+ ...this.defaultOptions,
3817
+ ...options
3818
+ };
3792
3819
  this.ws = null;
3793
3820
  this.dispatcher = null;
3794
3821
  }
@@ -4721,6 +4748,18 @@ const createPullController = handlerControllerFactory((proto)=>({
4721
4748
  kind: 'warning'
4722
4749
  });
4723
4750
  },
4751
+ onError (error) {
4752
+ this.diagnostics?.report({
4753
+ details: {
4754
+ message: error.message,
4755
+ error,
4756
+ createdAt: new Date(),
4757
+ name: error.name,
4758
+ severity: 'fatal'
4759
+ },
4760
+ kind: 'error'
4761
+ });
4762
+ },
4724
4763
  assertValidTransferAction (action) {
4725
4764
  // Abstract the constant to string[] to allow looser check on the given action
4726
4765
  const validActions = VALID_TRANSFER_ACTIONS;
@@ -4794,6 +4833,15 @@ const createPullController = handlerControllerFactory((proto)=>({
4794
4833
  let batch = [];
4795
4834
  const stream = this.streams?.[stage];
4796
4835
  const batchLength = ()=>Buffer.byteLength(JSON.stringify(batch));
4836
+ const maybeConfirm = async (data)=>{
4837
+ try {
4838
+ await this.confirm(data);
4839
+ } catch (error) {
4840
+ // Handle the error, log it, or take other appropriate actions
4841
+ strapi?.log.error(`[Data transfer] Message confirmation failed: ${error?.message}`);
4842
+ this.onError(error);
4843
+ }
4844
+ };
4797
4845
  const sendBatch = async ()=>{
4798
4846
  await this.confirm({
4799
4847
  type: 'transfer',
@@ -4802,6 +4850,7 @@ const createPullController = handlerControllerFactory((proto)=>({
4802
4850
  error: null,
4803
4851
  id
4804
4852
  });
4853
+ batch = [];
4805
4854
  };
4806
4855
  if (!stream) {
4807
4856
  throw new ProviderTransferError(`No available stream found for ${stage}`);
@@ -4812,7 +4861,6 @@ const createPullController = handlerControllerFactory((proto)=>({
4812
4861
  batch.push(chunk);
4813
4862
  if (batchLength() >= batchSize) {
4814
4863
  await sendBatch();
4815
- batch = [];
4816
4864
  }
4817
4865
  } else {
4818
4866
  await this.confirm({
@@ -4828,7 +4876,6 @@ const createPullController = handlerControllerFactory((proto)=>({
4828
4876
  }
4829
4877
  if (batch.length > 0 && stage !== 'assets') {
4830
4878
  await sendBatch();
4831
- batch = [];
4832
4879
  }
4833
4880
  await this.confirm({
4834
4881
  type: 'transfer',
@@ -4838,7 +4885,8 @@ const createPullController = handlerControllerFactory((proto)=>({
4838
4885
  id
4839
4886
  });
4840
4887
  } catch (e) {
4841
- await this.confirm({
4888
+ // TODO: if this confirm fails, can we abort the whole transfer?
4889
+ await maybeConfirm({
4842
4890
  type: 'transfer',
4843
4891
  data: null,
4844
4892
  ended: true,
@@ -4887,7 +4935,7 @@ const createPullController = handlerControllerFactory((proto)=>({
4887
4935
  };
4888
4936
  const BATCH_MAX_SIZE = 1024 * 1024; // 1MB
4889
4937
  if (!assets) {
4890
- throw new Error('bad');
4938
+ throw new Error('Assets read stream could not be created');
4891
4939
  }
4892
4940
  /**
4893
4941
  * Generates batches of 1MB of data from the assets stream to avoid
@@ -5138,7 +5186,7 @@ class LocalFileSourceProvider {
5138
5186
  });
5139
5187
  const loadAssetMetadata = _class_private_field_loose_base$1(this, _loadAssetMetadata)[_loadAssetMetadata].bind(this);
5140
5188
  _class_private_field_loose_base$1(this, _reportInfo$1)[_reportInfo$1]('creating assets read stream');
5141
- pipeline([
5189
+ pipeline$1([
5142
5190
  inStream,
5143
5191
  new tar.Parse({
5144
5192
  // find only files in the assets/uploads folder
@@ -5249,7 +5297,7 @@ function streamJsonlDirectory(directory) {
5249
5297
  const outStream = new PassThrough({
5250
5298
  objectMode: true
5251
5299
  });
5252
- pipeline([
5300
+ pipeline$1([
5253
5301
  inStream,
5254
5302
  new tar.Parse({
5255
5303
  filter (filePath, entry) {
@@ -5290,7 +5338,7 @@ function streamJsonlDirectory(directory) {
5290
5338
  }
5291
5339
  async function parseJSONFile(fileStream, filePath) {
5292
5340
  return new Promise((resolve, reject)=>{
5293
- pipeline([
5341
+ pipeline$1([
5294
5342
  fileStream,
5295
5343
  // Custom backup archive parsing
5296
5344
  new tar.Parse({