@mikezimm/fps-core-v7
Version:
Library of reusable core interfaces, types and constants migrated from fps-library-v2
448 lines (442 loc) • 29.2 kB
JavaScript
/**
* 2024-09-15: Migrated to SAME FOLDER in fps-library-v2\src\components\molecules\SpHttp...
* export { IJSFetchReturn, createEmptyFetchReturn, doSpJsFetch }
*/
import { check4This, Check4 } from '../../../../logic/Links/CheckSearch';
import { checkDeepProperty } from '../../../../logic/Objects/deep';
import { makeAbsoluteUrl } from '../../../../logic/Strings/getSiteCollectionUrlFromLink';
import { getSiteCollectionUrlFromLink } from '../../../../logic/Strings/getSiteCollectionUrlFromLink';
import { startPerformOpV2, updatePerformanceEndV2 } from '../../Performance/functions';
import { CheckItemsResultsWDigest } from '../../process-results/CheckWDigest/CheckItemsResultsWDigest';
// import { saveErrorToLog } from '../process-results/Logging';
import { copyHeadersToReturns, createMinHeaders } from '../helpers/headerInitUtilities';
import { check4ThisFPSDigestValue } from '../helpers/check4ThisFPSDigestValue';
import { cleanSearchResults } from '../helpers/cleanSearchedResults';
import { createEmptyFetchReturn } from '../interfaces/IJSFetchReturn';
import { addCatchResponseError, addUnknownFetchError } from '../helpers/SpFetchCommon';
export async function doSpJsFetch(fetchAPI, digestValue, headerContentType) {
const headers = createMinHeaders(fetchAPI, digestValue, headerContentType);
const results = await doSpJsFetchOrPost(fetchAPI, 'GET', headers, null);
return results;
}
// Helper function to introduce a delay
const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms));
/**
*
* Pass in any SharePoint rest api url and it should return a result or a standard error return object
* 2024-12-09: changed from accepting headerContentType to headers.
* old default was: 'Content-Type': headerContentType ? headerContentType : 'application/json;odata=verbose',
* ALSO, NEED TO add requestDigest into customHeaders like this:
* headers[ 'X-RequestDigest' ] = digestValue
*
* for Updates:
* Use 'MERGE' for updating items
*
* customHeaders: HeadersInit update
*
*
* TOP THINGS TO Look for debugging
* 1. __metadata: for ALL SAVE ITEMS
* 2. null for body if there is no body
*
*
* @param fetchAPI
* @returns
*/
export async function doSpJsFetchOrPost(fetchAPI, method, customHeaders, body, forcedHeaders, retryCount = 3, delayTime = 2000) {
// Created for later comparison after the method may be changed in the MERGE logic
const originalMethod = `${method}`;
// Automatically added this because API's usually need full url. - NOT Needed because makeAbsoluteUrl already does this
// if ( fetchAPI.indexOf( CurrentHostName ) === 0 ) fetchAPI = `https://${fetchAPI}`;
// Always make sure this is an absolute url by running through this first. Note, if the previous line executes, it should be good then.
fetchAPI = makeAbsoluteUrl(fetchAPI);
let results = createEmptyFetchReturn(fetchAPI, method);
// Check if the "OData-Version" header is set to "3.0"
const odataVersion = customHeaders ? customHeaders["OData-Version"] : '';
const isOdata3 = odataVersion && odataVersion.indexOf('3') > -1 ? true : false;
let headers = {
Accept: isOdata3 ? 'application/json;odata=nometadata' : 'application/json;odata.metadata=none',
'Content-Type': method === 'MERGE' || method === 'PATCH' ? 'application/json;odata=nometadata' : 'application/json;odata=verbose',
...customHeaders
};
// Add request digest for SharePoint operations if missing
const siteCollectionUrl = getSiteCollectionUrlFromLink(fetchAPI);
const digestValue = check4ThisFPSDigestValue(siteCollectionUrl);
// Added to just check if digestValue exists and if so, use it here
if (digestValue && !headers['X-RequestDigest']) {
headers['X-RequestDigest'] = digestValue;
}
// Adjust headers for specific SharePoint operations
if (method === 'GET') {
if (fetchAPI.indexOf('_api/search/query?') > 0) {
/**
* This is a comparison to the pnpjs call vs mine ( using the exact same endpoint )
*
* pnpjs: application/json;odata=nometadata;streaming=true;charset=utf-8
* mine: application/json;odata.metadata=minimal;odata.streaming=true;IEEE754Compatible=false;charset=utf-8
NOTE: These headers did not work by themselves... had to explicitly set to Odata 3.0 and then adjust
headers['Content-Type'] = 'application/json;odata.metadata=full'; // duplicating exactly the same Content-Type as Pnpjs using the same endpoint
This: "application/json;odata=nometadata" gave error that "The HTTP header ACCEPT is missing or its value is invalid."
headers['Accept'] = 'application/json;odata.metadata=none'; // duplicating exactly the same Content-Type as Pnpjs using the same endpoint
*/
// This: "application/json;odata=nometadata" gave error that "The HTTP header ACCEPT is missing or its value is invalid."
headers['Accept'] = 'application/json;odata=nometadata'; // duplicating exactly the same Content-Type as Pnpjs using the same endpoint
headers['Content-Type'] = 'application/json;odata=verbose'; // duplicating exactly the same Content-Type as Pnpjs using the same endpoint
headers['OData-Version'] = '3.0'; // duplicating exactly the same Content-Type as Pnpjs using the same endpoint
}
}
else if (method === 'PATCH') {
headers['X-HTTP-Method'] = 'MERGE';
// Ensure overwrite behavior
headers['IF-MATCH'] = '*';
// 2024-12-09: Untested -- Moved this further down so it is used for both MERGE (update) and POST (create)
// Ensure returning the updated object in case it's needed: Accept: application/json;odata=verbose
// (headers as Record<string, string>)['Accept'] = 'application/json;odata=verbose';
// Ensure returning the created object in case it's needed: Prefer: return=representation
headers['Prefer'] = 'return=representation';
if (headers['Content-Type'] && headers['Content-Type'].indexOf('verbose') > -1)
headers['Content-Type'] = 'application/json;odata=nometadata';
// method = 'POST'; // SharePoint requires POST for MERGE
}
else if (method === 'MERGE') {
headers['X-HTTP-Method'] = 'MERGE';
// Ensure overwrite behavior
headers['IF-MATCH'] = '*';
// 2024-12-09: Untested -- Moved this further down so it is used for both MERGE (update) and POST (create)
// Ensure returning the updated object in case it's needed: Accept: application/json;odata=verbose
// (headers as Record<string, string>)['Accept'] = 'application/json;odata=verbose';
// Ensure returning the created object in case it's needed: Prefer: return=representation
headers['Prefer'] = 'return=representation';
if (headers['Content-Type'] && headers['Content-Type'].indexOf('verbose') > -1)
headers['Content-Type'] = 'application/json;odata=nometadata';
method = 'POST'; // SharePoint requires POST for MERGE
}
else if (method === 'DELETE') {
headers['IF-MATCH'] = '*'; // Required for DELETE
method = 'POST'; // SharePoint requires POST for DELETE
/**
* HAD TO MAKE THIS else if because it would reset any previous updates to headers for 'MERGE' and 'DELETE'
*/
}
else if (method === 'POST') {
if (fetchAPI.toLocaleLowerCase().indexOf('getbyid') > -1) {
alert(`You are doing a POST to a 'getById' endpoint which could send you in circles :) Been there done that!`);
}
// 2024-12-22: Added this because the docs say it automatically handles defaults
headers = {};
// Ensure returning the created object in case it's needed: Prefer: return=representation
headers['Prefer'] = 'return=representation';
// Ensure returning the updated object in case it's needed: Accept: application/json;odata=verbose
headers['Accept'] = 'application/json;odata=verbose';
// 2024-12-22: Do I need to tweak this to check if it is possibly using SPFx 1.4.1 and the odata heading is different syntax?
// Ensure returning the updated object in case it's needed: Accept: application/json;odata=verbose
// 2024-12-22: This was causing an error: 'The HTTP header ACCEPT is missing or its value is invalid.'
// (headers as Record<string, string>)['Accept'] = 'application/json;odata.metadata=full';
/**
* Trying this after getting error "The parameter Title does not exist in method GetById."
* with just the Prefer header
*/
// (headers as Record<string, string>)['X-HTTP-Method'] = 'MERGE'; // This caused error when doing actual post... BUT helped when doing a Post to an existing item which is wrong.
}
/**
* 2025-01-09: Added this to be able to completely over-ride all the logic if needed (aka SP-API-Tester use case)
*/
const useTheForce = !forcedHeaders || Object.keys(forcedHeaders).length === 0 ? false : true;
const useTheseHeaders = useTheForce === true && forcedHeaders ? forcedHeaders : headers;
if (useTheForce === true) {
console.log(`doSpHttpFetchOrPost expected headers:`, headers);
console.log(`doSpHttpFetchOrPost forcedHeaders:`, forcedHeaders);
console.log(`doSpHttpFetchOrPost body:`, body);
}
results.headers = copyHeadersToReturns(useTheseHeaders);
try {
// Added these hard coded alerts to help in reducing development errors and time - aka PivotTiles Permission Save and Groups APIs
if (method === 'POST' && check4This(Check4.fpsHttpResponse_Eq_true) === true) {
if (body === null || body === undefined) {
console.log(`!!!!! doSpJsFetchOrPost - POST without body !!!! ~ 118`, fetchAPI);
}
else if (!body.__metadata) {
console.log(`!!!!! doSpJsFetchOrPost - POST body NO __metadata !!!! ~ 120`, body, fetchAPI);
}
}
/**
* Status Codes Explanation
204 No Content: This status code means that the server successfully processed the request but is not returning any content. For example, this is common with DELETE or PUT/MERGE requests when updating an item without requesting the updated data.
201 Created: This status code is used when an item is successfully created, and, with the Prefer: return=representation header, SharePoint will include the full response with the item details.
200 OK: This status code may appear for updates or other operations when the Prefer header is not set to return=representation, and the response may include the updated item details.
1. 400 Bad Request
Meaning: The request is malformed or missing required data.
Example: Invalid JSON format, missing mandatory fields, or incorrect request body.
Resolution: Double-check the request payload, headers, and any required parameters.
2. 401 Unauthorized
Meaning: Authentication is required and has failed or not been provided.
Example: Missing or incorrect authentication tokens.
Resolution: Ensure that the request includes a valid authentication token or the correct credentials.
3. 403 Forbidden
Meaning: The user does not have permission to perform the operation.
Example: Attempting to access a list or site for which the user does not have the required permissions.
Resolution: Verify the user's permissions and access rights to the resource.
4. 404 Not Found
Meaning: The requested resource (e.g., a list or item) was not found.
Example: Trying to access a non-existent SharePoint list or item.
Resolution: Check the URL and ensure the resource exists.
4.5 406 - Tyipcally a headers error
A 406 Not Acceptable error in the context of SP (SharePoint) REST API fetch typically occurs when the server cannot provide
a response matching the content type or encoding specified in the request headers, especially the Accept header.
A) Incorrect Accept Header:
If you specify an Accept header that the SharePoint API does not support, such as an unsupported MIME type, SharePoint may return a 406 error.
Example: Sending Accept: application/xml when the API only supports JSON responses.
Invalid OData Query Options:
B) Some OData query options might be malformed or not supported in certain requests, leading to an inability to process the request.
CDE) Unsupported URL or API Call, Content Negotiation Issue; Authentication Issues:
5. 409 Conflict
Meaning: There is a conflict with the current state of the resource.
Example: Trying to create an item with a duplicate value that violates unique constraints.
Resolution: Verify the request data to avoid conflicts (e.g., ensure unique values where required).
6. 412 Precondition Failed
Meaning: A condition specified in the If-Match or If-None-Match header was not met.
Example: Trying to update an item using If-Match with an ETag that does not match the current value.
Resolution: Ensure that the If-Match or If-None-Match header matches the current ETag of the item.
7. 415 Unsupported Media Type
Meaning: The server does not support the media type of the request payload.
Example: Sending a request body with an unsupported Content-Type header.
Resolution: Ensure the Content-Type header matches the format (e.g., application/json for JSON payloads).
8. 429 Too Many Requests
Meaning: The user has sent too many requests in a given amount of time.
Example: Exceeding the SharePoint Online API rate limits.
Resolution: Implement retry logic and respect rate limits. Review Microsoft's API throttling documentation for details.
9. 500 Internal Server Error
Meaning: An error occurred on the server while processing the request.
Example: Server-side issues or unexpected failures.
Resolution: Retry the request after a short delay or check for known outages or service issues.
10. 503 Service Unavailable
Meaning: The server is currently unable to handle the request due to temporary overloading or maintenance.
Example: SharePoint Online service issues or server maintenance.
Resolution: Retry after some time or check the Microsoft 365 Service Health Dashboard for updates.
Common HTTP Headers for Handling Responses
Retry-After: Indicates how long to wait before making another request. This header is often seen with 429 Too Many Requests or 503 Service Unavailable.
*/
const response = await fetch(fetchAPI, {
method: method,
headers: useTheseHeaders,
// Added this during testing of PivotTiles permissionsSave
body: body && body !== undefined ? JSON.stringify(body) : null,
});
// If the response status is 429, retry after a delay
if (response.status === 429 && retryCount > 0) {
console.warn(`429 Too Many Requests. Retrying in ${delayTime}ms...`);
await delay(delayTime); // Wait for the specified delay time
return doSpJsFetchOrPost(fetchAPI, method, headers, body, forcedHeaders, retryCount - 1, delayTime * 2); // Increase delay time exponentially
}
if (response.status === 204) {
/**
* 204 means there were no errors but nothing is returned...
* Some examples from ChatGPT:
* DELETE: When you delete an item or resource, a 204 status indicates that the deletion was successful, but there is no content in the response.
* PUT or MERGE: When updating an item or resource, a 204 status can indicate that the update was successful, but there is no new content to return.
* POST (with certain operations): Sometimes used for operations where the server processes the request but doesn't return any additional content.
*/
console.log('Operation was successful but there is no content to return.');
results.ok = true;
results.statusText = 'No Content Recieved';
results.statusNo = response.status;
results.status = 'Success';
/**
* 2025-01-12: Added this loop in effort to find if the ['Prefer'] = 'return=representation' result if it were avaialble
* However, based on lots of testing, I was never able to get it to return no matter what headers or method I used.
* Only time I got a response back was after POSTing a NEW item
*
*/
if ((originalMethod === 'POST' || originalMethod === 'PATCH' || originalMethod === 'MERGE') && useTheseHeaders['Prefer'] === 'return=representation') {
// Safely handle the body
const responseBody = await response.text(); // Get the raw response as text
if (responseBody) {
try {
const data = JSON.parse(responseBody); // Parse if there's content
results.items = data;
}
catch (error) {
console.warn('Failed to parse JSON:', error);
results.items = []; // Or provide a fallback
}
}
else {
if (check4This(Check4.fpsShowFetchResults_Eq_true) === true)
console.warn('Response body is empty for 204 status.');
if (!results.items)
results.items = []; // No data to return
}
}
// check if the response is OK
}
else if (response.ok) {
const data = await response.json();
const usesSearch = fetchAPI.indexOf(`_api/search`) > -1 ? true : false;
// 2024-12-09: Refactored this code from my original commented out below.... VVVVVV
if (usesSearch) {
results.rawSearchResults = data;
const deepPropValue = checkDeepProperty(data, ['PrimaryQueryResult', 'RelevantResults', 'Table', 'Rows'], 'Actual');
if (deepPropValue) {
results.items = data.PrimaryQueryResult.RelevantResults.Table.Rows;
}
if (results.items && results.items.length > 0) {
results = cleanSearchResults(results);
}
}
else if (data.value) {
results.items = data.value; // Standard SharePoint API response
}
else {
results.items = data; // Non-standard response
}
// 2024-12-09: Refactored ^^^^^^^^ from my original commented out below.... VVVVVV
// const deepPropValue = usesSearch === true ? checkDeepProperty( data, [ 'PrimaryQueryResult', 'RelevantResults', 'Table', 'Rows'], 'Actual' ) : undefined;
// // added logic to solve this: https://github.com/mikezimm/pivottiles7/issues/292
// if ( deepPropValue !== undefined && deepPropValue !== null ) {
// results.items = data.PrimaryQueryResult.RelevantResults.Table.Rows;
// } else if ( fetchAPI.indexOf('GetFolderByServerRelativeUrl') > 0 ) {
// } else {
// if ( usesSearch === true && data.ElapsedTime && !data.PrimaryQueryResult ) {
// // Seems like query did not fail, so do nothing because array of items is already [].
// // This is what happened when using fetchMySubsites and not having any subsites to return.
// } else {
// results.items = data.value ? data.value : data;
// }
// }
results.ok = true;
results.statusText = response.statusText;
results.statusNo = response.status;
results.status = 'Success';
if (check4This(Check4.fpsShowFetchResults_Eq_true) === true)
console.log(`fps-core-v7 Success: doSpJsFetchOrPost ~ 131 results`, results);
}
else {
// Handle cases where the response isn't JSON (e.g., empty or text response)
// these next 2 lines were causing issues in addCatchResponseError because that was trying to do response.text() but it was already done here
// const text = await response.text();
// console.warn('Non-JSON response received:', text);
results = await addCatchResponseError(results, response, null);
// 2024-12-09: Copied this over from updateCommandItems so it will always capture the errors somewhere
// Commented out saveErrorToLog here because there is no results.errorInfo, so it will error out triggering an endless loop.
// Also to note, saveErrorToLog is already being done later during the checkAnyItems part so it's not really needed here.
// saveErrorToLogWDigest( results.errorInfo as IHelpfullOutput, results.errorInput as IHelpfullInput );
}
return results;
}
catch (e) {
// Added these hard coded alerts to help in reducing development errors and time - aka PivotTiles Permission Save and Groups APIs
if (method === 'POST' && check4This(Check4.fpsHttpResponse_Eq_true) === true) {
if (body === null || body === undefined) {
alert(`!!!!! doSpJsFetchOrPost - POST without body !!!! ~ 271<br/>${fetchAPI}`);
}
else if (!body.__metadata) {
alert(`!!!!! doSpJsFetchOrPost - POST body NO __metadata !!!! ~ 273<br/>${fetchAPI}`);
}
}
return addUnknownFetchError(results, e);
}
}
/**
* doSpJsFetchOrPostAndCheck does: the fetch, checkItemsResults, adds fpsContentTypes and performance
*
* Use 'MERGE' for updating items
*
* Steps to use this vs the No Check:
* 1. Update call
const result: IFpsUsersReturn = await doSpJsFetchOrPostAndCheck( fetchAPI, 'GET', digestValue, '', false, true, null ) as unknown as IFpsUsersReturn;
*
* 4. re-assign performance object, update label
result.unifiedPerformanceOps.fetch.label = 'GroupName';
initialResult.fetchOp = initialResult.unifiedPerformanceOps.fetch;
*
* Things to remove from the older version calls
* const performanceSettings: IPerformanceSettings = { label: 'FetchCheck', includeMsStr: true, updateMiliseconds: true, op: 'fetch' };
* const fetchOp = performanceSettings ? startPerformOpV2( performanceSettings ) : null;
* DONE HERE based on 'type' property: move results to the object you want
result.users = result.items ? result.items : [];
DONE HERE based on 'type' property: update fpsContentType
result.fpsContentType = [ 'user' ];
* initialResult.unifiedPerformanceOps.fetch = performanceSettings ?
updatePerformanceEndV2( { op: fetchOp as IPerformanceOp, updateMiliseconds: performanceSettings.updateMiliseconds, count: initialResult.items ? initialResult.items.length : 0 })
: null as any; // 2024-09-29: set null as any to pass build error
*
*
* @param fetchAPI
* @param method
* @param digestValue
* @param headerContentType
* @param type
* @param alertMe
* @param consoleLog
* @param body
* @returns
*/
export async function doSpJsFetchOrPostAndCheck(fetchAPI, method, digestValue, headerContentType, alertMe, consoleLog, type, body, forcedHeaders) {
const performanceSettings = { label: 'FetchCheck', includeMsStr: true, updateMiliseconds: true, op: 'fetch' };
const fetchOp = performanceSettings ? startPerformOpV2(performanceSettings) : null;
const headers = createMinHeaders(fetchAPI, digestValue, headerContentType);
let initialResult = await doSpJsFetchOrPost(fetchAPI, method, headers, body, forcedHeaders);
initialResult.fetchAPI = fetchAPI;
initialResult.method = method;
initialResult = CheckItemsResultsWDigest(initialResult, `fps-core-v7: doSpJsFetchOrPostAndCheck ~ 161`, alertMe, consoleLog);
// Moved outside of type so it can also be used to update the results cound in performance
const values = initialResult.item ? initialResult.item : initialResult.items ? initialResult.items : null;
const isArray = Array.isArray(values) === true ? true : false;
/**
* Added to automatically add fpsContentType based on the type, and results to the appropriate key
*/
if (type) {
if (!initialResult.fpsContentType)
initialResult.fpsContentType = [];
// Need to add if it's a file because it is also an item
if (type === 'file' || type === 'files')
initialResult.fpsContentType.push('item');
// https://github.com/fps-solutions/SP-API-Tester/issues/15 - fixed the type.length -1 instead of -2
const typeNoS = type.endsWith('s') === true ? type.substring(0, type.length - 1) : type;
if (initialResult.fpsContentType.indexOf(typeNoS) < 0)
initialResult.fpsContentType.push(typeNoS);
initialResult[`${typeNoS}${isArray === true && type.endsWith('s') === true ? 's' : ''}`] = values;
}
initialResult.unifiedPerformanceOps.fetch = performanceSettings ?
updatePerformanceEndV2({ op: fetchOp, updateMiliseconds: performanceSettings.updateMiliseconds, count: isArray === true ? values.length : values ? 1 : 0 })
: null; // 2024-09-29: set null as any to pass build error.
// export type IJSFetchMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'MERGE' ;
initialResult.fetchOp = initialResult.unifiedPerformanceOps.fetch;
if (method === 'POST') {
initialResult.saveOp = initialResult.unifiedPerformanceOps.fetch;
}
else if (method === 'DELETE') {
initialResult.deleteOp = initialResult.unifiedPerformanceOps.fetch;
}
else if (method === 'MERGE') {
initialResult.saveOp = initialResult.unifiedPerformanceOps.fetch;
}
return initialResult;
}
// export function checkFpsTypesResults ( type: IFPSBaseContentTypesAll,
// export async function doSpJsFetchOrPostAndCheckTypes(
// fetchAPI: string, method: IJSFetchMethodNoPUT,
// digestValue: string, headerContentType: string,
// alertMe: boolean | undefined, consoleLog: boolean | undefined,
// type: IFPSBaseContentTypesAll, body: any ): Promise<IFpsItemsReturn> {
// const performanceSettings: IPerformanceSettings = { label: 'FetchCheck', includeMsStr: true, updateMiliseconds: true, op: 'fetch' };
// const fetchOp = performanceSettings ? startPerformOpV2( performanceSettings ) : null;
// const headers: HeadersInit = createMinHeaders( digestValue, headerContentType );
// let initialResult: IFpsItemsReturn = await doSpJsFetchOrPost( fetchAPI, method, headers, body );
// initialResult.fetchAPI = fetchAPI;
// initialResult.method = method;
// initialResult = checkItemsResults( initialResult, `fps-core-v7: doSpJsFetchOrPostAndCheck ~ 161`, alertMe, consoleLog );
// const values = initialResult.item ? initialResult.item : initialResult.items ? initialResult.items : null;
// const isArray: boolean = Array.isArray( values ) === true ? true : false;
// initialResult[ `${type}${ isArray === true && type.endsWith('s') === true ? 's' : '' }` as 'item' ] = values;
// initialResult.unifiedPerformanceOps.fetch = performanceSettings ?
// updatePerformanceEndV2( { op: fetchOp as IPerformanceOp, updateMiliseconds: performanceSettings.updateMiliseconds, count: initialResult.items ? initialResult.items.length : 0 })
// : null as any; // 2024-09-29: set null as any to pass build error.
// // export type IJSFetchMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'MERGE' ;
// initialResult.fetchOp = initialResult.unifiedPerformanceOps.fetch;
// if ( method === 'POST' ) { initialResult.saveOp = initialResult.unifiedPerformanceOps.fetch; }
// else if ( method === 'DELETE' ) { initialResult.deleteOp = initialResult.unifiedPerformanceOps.fetch; }
// else if ( method === 'MERGE' ) { initialResult.saveOp = initialResult.unifiedPerformanceOps.fetch; }
// return initialResult;
// }
//# sourceMappingURL=doSpJsFetch.js.map