fork of hey-api/openapi-ts because I need some additional things
at feat/skip-token 228 lines 5.9 kB view raw
1import { getResolvedInput, sendRequest } from '@hey-api/json-schema-ref-parser'; 2import type { MaybeArray } from '@hey-api/types'; 3 4import type { Input } from './config/input/types'; 5import type { WatchValues } from './types/watch'; 6 7const headersEntries = (headers: Headers): Array<[string, string]> => { 8 const entries: Array<[string, string]> = []; 9 headers.forEach((value, key) => { 10 entries.push([key, value]); 11 }); 12 return entries; 13}; 14 15const mergeHeaders = ( 16 ...headers: Array< 17 | RequestInit['headers'] 18 | Record<string, MaybeArray<string | number | boolean> | null | undefined | unknown> 19 | undefined 20 > 21): Headers => { 22 const mergedHeaders = new Headers(); 23 for (const header of headers) { 24 if (!header) { 25 continue; 26 } 27 28 const iterator = header instanceof Headers ? headersEntries(header) : Object.entries(header); 29 30 for (const [key, value] of iterator) { 31 if (value === null) { 32 mergedHeaders.delete(key); 33 } else if (Array.isArray(value)) { 34 for (const v of value) { 35 mergedHeaders.append(key, v as string); 36 } 37 } else if (value !== undefined) { 38 // assume object headers are meant to be JSON stringified, i.e. their 39 // content value in OpenAPI specification is 'application/json' 40 mergedHeaders.set( 41 key, 42 typeof value === 'object' ? JSON.stringify(value) : (value as string), 43 ); 44 } 45 } 46 } 47 return mergedHeaders; 48}; 49 50type SpecResponse = { 51 arrayBuffer: ArrayBuffer | undefined; 52 error?: never; 53 resolvedInput: ReturnType<typeof getResolvedInput>; 54 response?: never; 55}; 56 57type SpecError = { 58 arrayBuffer?: never; 59 error: 'not-modified' | 'not-ok'; 60 resolvedInput?: never; 61 response: Response; 62}; 63 64/** 65 * @internal 66 */ 67export async function getSpec({ 68 fetchOptions, 69 inputPath, 70 timeout, 71 watch, 72}: { 73 fetchOptions?: RequestInit; 74 inputPath: Input['path']; 75 timeout: number | undefined; 76 watch: WatchValues; 77}): Promise<SpecResponse | SpecError> { 78 const resolvedInput = getResolvedInput({ pathOrUrlOrSchema: inputPath }); 79 80 let arrayBuffer: ArrayBuffer | undefined; 81 // boolean signals whether the file has **definitely** changed 82 let hasChanged: boolean | undefined; 83 let response: Response | undefined; 84 85 if (resolvedInput.type === 'url') { 86 // do NOT send HEAD request on first run or if unsupported 87 if (watch.lastValue && watch.isHeadMethodSupported !== false) { 88 try { 89 const request = await sendRequest({ 90 fetchOptions: { 91 method: 'HEAD', 92 ...fetchOptions, 93 headers: mergeHeaders(fetchOptions?.headers, watch.headers), 94 }, 95 timeout, 96 url: resolvedInput.path, 97 }); 98 99 if (request.response.status >= 300) { 100 return { 101 error: 'not-ok', 102 response: request.response, 103 }; 104 } 105 106 response = request.response; 107 } catch (error) { 108 const message = error instanceof Error ? error.message : String(error); 109 return { 110 error: 'not-ok', 111 response: new Response(message, { status: 500 }), 112 }; 113 } 114 115 if (!response.ok && watch.isHeadMethodSupported) { 116 // assume the server is no longer running 117 // do nothing, it might be restarted later 118 return { 119 error: 'not-ok', 120 response, 121 }; 122 } 123 124 if (watch.isHeadMethodSupported === undefined) { 125 watch.isHeadMethodSupported = response.ok; 126 } 127 128 if (response.status === 304) { 129 return { 130 error: 'not-modified', 131 response, 132 }; 133 } 134 135 if (hasChanged === undefined) { 136 const eTag = response.headers.get('ETag'); 137 if (eTag) { 138 hasChanged = eTag !== watch.headers.get('If-None-Match'); 139 140 if (hasChanged) { 141 watch.headers.set('If-None-Match', eTag); 142 } 143 } 144 } 145 146 if (hasChanged === undefined) { 147 const lastModified = response.headers.get('Last-Modified'); 148 if (lastModified) { 149 hasChanged = lastModified !== watch.headers.get('If-Modified-Since'); 150 151 if (hasChanged) { 152 watch.headers.set('If-Modified-Since', lastModified); 153 } 154 } 155 } 156 157 // we definitely know the input has not changed 158 if (hasChanged === false) { 159 return { 160 error: 'not-modified', 161 response, 162 }; 163 } 164 } 165 166 try { 167 const request = await sendRequest({ 168 fetchOptions: { 169 method: 'GET', 170 ...fetchOptions, 171 }, 172 timeout, 173 url: resolvedInput.path, 174 }); 175 176 if (request.response.status >= 300) { 177 return { 178 error: 'not-ok', 179 response: request.response, 180 }; 181 } 182 183 response = request.response; 184 } catch (error) { 185 const message = error instanceof Error ? error.message : String(error); 186 return { 187 error: 'not-ok', 188 response: new Response(message, { status: 500 }), 189 }; 190 } 191 192 if (!response.ok) { 193 // assume the server is no longer running 194 // do nothing, it might be restarted later 195 return { 196 error: 'not-ok', 197 response, 198 }; 199 } 200 201 arrayBuffer = response.body ? await response.arrayBuffer() : new ArrayBuffer(0); 202 203 if (hasChanged === undefined) { 204 const content = new TextDecoder().decode(arrayBuffer); 205 hasChanged = content !== watch.lastValue; 206 watch.lastValue = content; 207 } 208 } else { 209 // we do not support watch mode for files or raw spec data 210 if (!watch.lastValue) { 211 watch.lastValue = resolvedInput.type; 212 } else { 213 hasChanged = false; 214 } 215 } 216 217 if (hasChanged === false) { 218 return { 219 error: 'not-modified', 220 response: response!, 221 }; 222 } 223 224 return { 225 arrayBuffer, 226 resolvedInput, 227 }; 228}