Skip to content

Commit

Permalink
fix: improves DataPackage fetch method
Browse files Browse the repository at this point in the history
- Refactored the fetch method in DataPackage to return a Promise and handle options correctly.
- Added detailed documentation for fetch method usage.
- Improved error handling and retry logic for fetch method.
- Added unit tests for DataPackage fetch method to ensure proper functionality.
- Fixes the issue of a new resource map ID not being created on update

Closes NCEAS#2607
  • Loading branch information
vchendrix committed Feb 5, 2025
1 parent 1423787 commit d85318d
Show file tree
Hide file tree
Showing 2 changed files with 222 additions and 55 deletions.
137 changes: 82 additions & 55 deletions src/js/collections/DataPackage.js
Original file line number Diff line number Diff line change
Expand Up @@ -573,67 +573,94 @@ define([
},

/**
* Overload fetch calls for a DataPackage
* @param {object} [options] - Optional options for this fetch that get
* sent with the XHR request
* @property {boolean} fetchModels - If false, this fetch will not fetch
* each model in the collection. It will only get the resource map object.
* @property {boolean} fromIndex - If true, the collection will be fetched
* from Solr rather than fetching the system metadata of each model.
* Useful when you only need to retrieve limited information about each
* package member. Set query-specific parameters on the `solrResults`
* SolrResults set on this collection.
* @returns {jqXHR} The jQuery XMLHttpRequest for the request
* Overload fetch calls for a DataPackage
*
* This fetch function will fetch the resource map RDF XML for this package
*
* + Example 1: `this.fetch();`
* + Example 2: `this.fetch({fetchModels: false});`
* + Example 3: `this.fetch({fromIndex: true});`
* + Example 4:
* ```
* this.fetch()
* .then(function() {
* console.log("Fetch complete!");
* })
* .catch(function() {
* console.log("Fetch failed!");
* });
* ```
*
* @param {object} [options] - Optional options for this fetch that get sent with the XHR request
* @property {boolean} fetchModels - If false, this fetch will not fetch
* each model in the collection. It will only get the resource map object.
* @property {boolean} fromIndex - If true, the collection will be fetched from Solr rather than
* fetching the system metadata of each model. Useful when you only need to retrieve limited information about
* each package member. Set query-specific parameters on the `solrResults` SolrResults set on this collection.
* @return {Promise} A promise that resolves when the fetch is complete
*/
fetch(options = {}) {
// Fetch the system metadata for this resource map
this.packageModel.fetch();

const fetchOptions = { dataType: "text", ...options };

// If the fetchModels property is set to false,
if (fetchOptions.fetchModels === false) {
// Save the property to the Collection itself so it is accessible in
// other functions
this.fetchModels = false;
// Remove the property from the options Object since we don't want to
// send it with the XHR
delete fetchOptions.fetchModels;
this.once("reset", this.triggerComplete);
}
// If the fetchFromIndex property is set to true
else if (fetchOptions.fromIndex) {
this.fetchFromIndex();
return null;
}
fetch(options) {
return new Promise((resolve, reject) => {
// Fetch the system metadata for this resource map
this.packageModel.fetch();

if (typeof options === "object") {
// If the fetchModels property is set to false,
if (options.fetchModels === false) {
// Save the property to the Collection itself so it is accessible in other functions
this.fetchModels = false;
// Remove the property from the options Object since we don't want to send it with the XHR
delete options.fetchModels;
this.once("reset", () => {
this.triggerComplete();
resolve();
});
}
// If the fetchFromIndex property is set to true
else if (options.fromIndex) {
this.fetchFromIndex();
resolve();
return;
}
}

const thisPackage = this;
// Set some custom fetch options
const fetchOptions = _.extend({ dataType: "text" }, options);

// Function to retry fetching with user login details if the initial
// fetch fails eslint-disable-next-line func-names
const retryFetch = () => {
// Add the authorization options
const authFetchOptions = _.extend(
fetchOptions,
MetacatUI.appUserModel.createAjaxSettings(),
);
const thisPackage = this;

// Function to retry fetching with user login details if the initial fetch fails
const retryFetch = function () {
// Add the authorization options
const authFetchOptions = _.extend(
fetchOptions,
MetacatUI.appUserModel.createAjaxSettings(),
);

// Fetch the resource map RDF XML with user login details
return Backbone.Collection.prototype.fetch
.call(thisPackage, authFetchOptions)
.fail(() => {
// trigger failure()
console.log("Fetch failed");

thisPackage.trigger("fetchFailed", thisPackage);
reject();
});
};

// Fetch the resource map RDF XML with user login details
return Backbone.Collection.prototype.fetch
.call(thisPackage, authFetchOptions)
// Fetch the resource map RDF XML
Backbone.Collection.prototype.fetch
.call(this, fetchOptions)
.done(() => resolve())
.fail(() => {
// TODO: Handle the fetch failure!
thisPackage.trigger("fetchFailed", thisPackage);
console.log("Fetch failed. Retrying with user login details...");
// If the initial fetch fails, retry with user login details
retryFetch()
.done(() => resolve())
.fail(() => reject());
});
};

// Fetch the resource map RDF XML
return Backbone.Collection.prototype.fetch
.call(this, fetchOptions)
.fail(() =>
// If the initial fetch fails, retry with user login details
retryFetch(),
);
});
},

/**
Expand Down
140 changes: 140 additions & 0 deletions test/js/specs/unit/collections/DataPackage.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,42 @@ define([

describe("DataPackage Test Suite", function () {
let dataPackage;
let originalFetch,
originalTriggerComplete,
originalFetchFromIndex,
originalCreateAjaxSettings;
let fetchCallCount,
triggerCompleteCallCount,
fetchFromIndexCallCount,
createAjaxSettingsCallCount;

beforeEach(function () {
dataPackage = new DataPackage();
originalFetch = dataPackage.packageModel.fetch;
originalTriggerComplete = dataPackage.triggerComplete;
originalFetchFromIndex = dataPackage.fetchFromIndex;
originalCreateAjaxSettings = MetacatUI.appUserModel.createAjaxSettings;

fetchCallCount = 0;
triggerCompleteCallCount = 0;
fetchFromIndexCallCount = 0;
createAjaxSettingsCallCount = 0;

dataPackage.triggerComplete = function () {
triggerCompleteCallCount++;
};
dataPackage.fetchFromIndex = function () {
fetchFromIndexCallCount++;
};
MetacatUI.appUserModel.createAjaxSettings = function () {
createAjaxSettingsCallCount++;
};
});

afterEach(function () {
dataPackage.triggerComplete = originalTriggerComplete;
dataPackage.fetchFromIndex = originalFetchFromIndex;
MetacatUI.appUserModel.createAjaxSettings = originalCreateAjaxSettings;
dataPackage = undefined;
});

Expand Down Expand Up @@ -133,5 +163,115 @@ define([
}, 1000);
});
});

describe("fetch", function () {
let originalFetch;

// Save the original fetch method and mock it to return a resolved promise before each test
beforeEach(function () {
originalFetch = Backbone.Collection.prototype.fetch;
Backbone.Collection.prototype.fetch = function (options) {
// Mocked fetch method that returns a resolved promise
var deferred = $.Deferred();
deferred.resolve();
return deferred.promise();
};
});

// Restore the original fetch method after each test
afterEach(function () {
Backbone.Collection.prototype.fetch = originalFetch;
});

// Test that packageModel.fetch is called with no options
it("should call packageModel.fetch with no options", function (done) {
dataPackage.packageModel.fetch = function () {
// Mocked fetch method that increments fetchCallCount and returns a resolved promise
fetchCallCount++;
return Promise.resolve();
};

dataPackage
.fetch()
.then(function () {
expect(fetchCallCount).to.equal(1);
done(); // Call done() to finish the test
})
.catch(function () {
done(new Error("Expected fetch to succeed"));
});
});

// Test that packageModel.fetch is called with fetchModels: false
it("should call packageModel.fetch with fetchModels: false", function (done) {
dataPackage.packageModel.fetch = function () {
// Mocked fetch method that increments fetchCallCount and returns a resolved promise
fetchCallCount++;
return Promise.resolve();
};

dataPackage
.fetch({ fetchModels: false })
.then(function () {
expect(fetchCallCount).to.equal(1);
expect(fetchFromIndexCallCount).to.equal(0);
done(); // Call done() to finish the test
})
.catch(function () {
done(new Error("Expected fetch to succeed"));
});
});

// Test that fetchFromIndex is called with fromIndex: true
it("should call fetchFromIndex with fromIndex: true", function (done) {
dataPackage
.fetch({ fromIndex: true })
.then(function () {
expect(fetchFromIndexCallCount).to.equal(1);
done(); // Call done() to finish the test
})
.catch(function () {
done(new Error("Expected fetch to succeed"));
});
});

// Test that createAjaxSettings is called
it("should call createAjaxSettings", function (done) {
dataPackage
.fetch()
.then(function () {
expect(createAjaxSettingsCallCount).to.equal(1);
done(); // Call done() to finish the test
})
.catch(function () {
done(new Error("Expected fetch to succeed"));
});
});

// Test that the fetch method handles done and fail callbacks correctly
it("should handle done and fail callbacks", function (done) {
dataPackage.packageModel.fetch = function () {
// Mocked fetch method that increments fetchCallCount and returns a rejected promise
fetchCallCount++;
return Promise.reject();
};
Backbone.Collection.prototype.fetch = function (options) {
// Mocked fetch method that returns a rejected promise
var deferred = $.Deferred();
deferred.reject();
return deferred.promise();
};
dataPackage
.fetch()
.then(function () {
done(new Error("Expected fetch to fail"));
})
.catch(function () {
expect(fetchCallCount).to.equal(1);
expect(fetchFromIndexCallCount).to.equal(0);
done();
});
});
});
});
});

0 comments on commit d85318d

Please sign in to comment.