UNPKG

@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
/** * 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