import { Models } from './models'; type Payload = { [key: string]: any; } type UploadProgress = { $id: string; progress: number; sizeUploaded: number; chunksTotal: number; chunksUploaded: number; } type Headers = { [key: string]: string; } class AppcondaException extends Error { code: number; response: string; type: string; constructor(message: string, code: number = 0, type: string = '', response: string = '') { super(message); this.name = 'AppcondaException'; this.message = message; this.code = code; this.type = type; this.response = response; } } type FileLike = { size: number; slice: (start?: number, end?: number) => Blob; name?: string; type?: string; } function hasGlobalFileConstructor(): boolean { return typeof globalThis !== 'undefined' && typeof (globalThis as any).File !== 'undefined'; } function isFileLike(value: unknown): value is FileLike { if (!value || typeof value !== 'object') { return false; } if (hasGlobalFileConstructor() && value instanceof (globalThis as any).File) { return true; } return typeof (value as FileLike).size === 'number' && typeof (value as FileLike).slice === 'function'; } function getFileName(file: FileLike, fallbackName = 'upload.bin'): string { if (typeof file.name === 'string' && file.name.length > 0) { return file.name; } return fallbackName; } function createChunkFile(chunk: Blob, filename: string): Blob | File { if (hasGlobalFileConstructor()) { return new (globalThis as any).File([chunk], filename, { type: chunk.type || 'application/octet-stream', }); } const blob = new Blob([chunk], { type: chunk.type || 'application/octet-stream', }) as Blob & { name?: string }; try { Object.defineProperty(blob, 'name', { value: filename, configurable: true, }); } catch { // Fallback for runtimes where Blob is not extensible. } return blob; } function getUserAgent() { let ua = 'AppcondaNodeJSSDK/14.1.0'; // `process` is a global in Node.js, but not fully available in all runtimes. const platform: string[] = []; if (typeof process !== 'undefined') { if (typeof process.platform === 'string') platform.push(process.platform); if (typeof process.arch === 'string') platform.push(process.arch); } if (platform.length > 0) { ua += ` (${platform.join('; ')})`; } // `navigator.userAgent` is available in Node.js 21 and later. // It's also part of the WinterCG spec, so many edge runtimes provide it. // https://common-min-api.proposal.wintercg.org/#requirements-for-navigatoruseragent // @ts-ignore if (typeof navigator !== 'undefined' && typeof navigator.userAgent === 'string') { // @ts-ignore ua += ` ${navigator.userAgent}`; // @ts-ignore } else if (typeof globalThis.EdgeRuntime === 'string') { ua += ` EdgeRuntime`; // Older Node.js versions don't have `navigator.userAgent`, so we have to use `process.version`. } else if (typeof process !== 'undefined' && typeof process.version === 'string') { ua += ` Node.js/${process.version}`; } return ua; } class Client { static CHUNK_SIZE = 1024 * 1024 * 5; config = { endpoint: 'https://cloud.appconda.io/v1', selfSigned: false, project: '', mode: '', key: '', jwt: '', locale: '', session: '', forwardeduseragent: '', }; headers: Headers = { 'x-sdk-name': 'Node.js', 'x-sdk-platform': 'server', 'x-sdk-language': 'nodejs', 'x-sdk-version': '14.1.0', 'user-agent': getUserAgent(), 'X-Appconda-Response-Format': '1.6.0', }; private fetchImpl?: typeof fetch; /** * Set Endpoint * * Your project endpoint * * @param {string} endpoint * * @returns {this} */ setEndpoint(endpoint: string): this { this.config.endpoint = endpoint; return this; } /** * Set self-signed * * @param {boolean} selfSigned * * @returns {this} */ setSelfSigned(selfSigned: boolean): this { // @ts-ignore if (typeof globalThis.EdgeRuntime !== 'undefined') { console.warn('setSelfSigned is not supported in edge runtimes.'); } this.config.selfSigned = selfSigned; return this; } /** * Add header * * @param {string} header * @param {string} value * * @returns {this} */ addHeader(header: string, value: string): this { this.headers[header.toLowerCase()] = value; return this; } /** * Set custom fetch implementation. * * Useful for Node.js versions without a global fetch. */ setFetch(fetchImpl: typeof fetch): this { this.fetchImpl = fetchImpl; return this; } private getFetch(): typeof fetch { const runtimeFetch = this.fetchImpl ?? (typeof globalThis !== 'undefined' ? globalThis.fetch : undefined); if (!runtimeFetch) { throw new AppcondaException('Fetch API is not available. Use Node.js 18+ or set a custom fetch with client.setFetch().'); } return runtimeFetch.bind(globalThis); } /** * Set Project * * Your project ID * * @param value string * * @return {this} */ setProject(value: string): this { this.headers['X-Appconda-Project'] = value; this.config.project = value; return this; } /** * Set Mode * * @param value string * * @return {this} */ setMode(value: string): this { this.headers['X-Appconda-Mode'] = value; this.config.mode = value; return this; } /** * Set Key * * Your secret API key * * @param value string * * @return {this} */ setKey(value: string): this { this.headers['X-Appconda-Key'] = value; this.config.key = value; return this; } /** * Set JWT * * Your secret JSON Web Token * * @param value string * * @return {this} */ setJWT(value: string): this { this.headers['X-Appconda-JWT'] = value; this.config.jwt = value; return this; } /** * Set Locale * * @param value string * * @return {this} */ setLocale(value: string): this { this.headers['X-Appconda-Locale'] = value; this.config.locale = value; return this; } /** * Set Session * * The user session to authenticate with * * @param value string * * @return {this} */ setSession(value: string): this { this.headers['X-Appconda-Session'] = value; this.config.session = value; return this; } /** * Set ForwardedUserAgent * * The user agent string of the client that made the request * * @param value string * * @return {this} */ setForwardedUserAgent(value: string): this { this.headers['X-Forwarded-User-Agent'] = value; this.config.forwardeduseragent = value; return this; } setFallbackCookies(value: string): this { this.headers['X-Fallback-Cookies'] = value; //this.config.forwardeduseragent = value; return this; } prepareRequest(method: string, url: URL, headers: Headers = {}, params: Payload = {}): { uri: string, options: RequestInit } { method = method.toUpperCase(); headers = Object.assign({}, this.headers, headers); if (typeof window !== 'undefined' && window.localStorage) { const cookieFallback = window.localStorage.getItem('cookieFallback'); if (cookieFallback) { headers['X-Fallback-Cookies'] = cookieFallback; } } let options: RequestInit = { method, headers, }; if (headers['X-Appconda-Dev-Key'] === undefined) { options.credentials = 'include'; } if (method === 'GET') { for (const [key, value] of Object.entries(Client.flatten(params))) { url.searchParams.append(key, value); } } else { switch (headers['content-type']) { case 'application/json': options.body = JSON.stringify(params); break; case 'multipart/form-data': const formData = new FormData(); for (const [key, value] of Object.entries(params)) { if (isFileLike(value)) { formData.append(key, value as unknown as Blob, getFileName(value)); } else if (Array.isArray(value)) { for (const nestedValue of value) { if (isFileLike(nestedValue)) { formData.append(`${key}[]`, nestedValue as unknown as Blob, getFileName(nestedValue)); } else { formData.append(`${key}[]`, nestedValue as any); } } } else { formData.append(key, value as any); } } options.body = formData; delete headers['content-type']; break; } } return { uri: url.toString(), options }; } async chunkedUpload(method: string, url: URL, headers: Headers = {}, originalPayload: Payload = {}, onProgress: (progress: UploadProgress) => void) { const file = Object.values(originalPayload).find((value) => isFileLike(value)) as FileLike | undefined; if (!file) { return await this.call(method, url, headers, originalPayload); } if (file.size <= Client.CHUNK_SIZE) { return await this.call(method, url, headers, originalPayload); } let start = 0; let response: any = null; while (start < file.size) { let end = start + Client.CHUNK_SIZE; // Prepare end for the next chunk if (end >= file.size) { end = file.size; // Adjust for the last chunk to include the last byte } headers['content-range'] = `bytes ${start}-${end - 1}/${file.size}`; const chunk = file.slice(start, end); const payload = { ...originalPayload, file: createChunkFile(chunk, getFileName(file)), }; response = await this.call(method, url, headers, payload); if (onProgress && typeof onProgress === 'function') { onProgress({ $id: response.$id, progress: Math.round((end / file.size) * 100), sizeUploaded: end, chunksTotal: Math.ceil(file.size / Client.CHUNK_SIZE), chunksUploaded: Math.ceil(end / Client.CHUNK_SIZE) }); } if (response && response.$id) { headers['x-appconda-id'] = response.$id; } start = end; } return response; } async redirect(method: string, url: URL, headers: Headers = {}, params: Payload = {}): Promise { const { uri, options } = this.prepareRequest(method, url, headers, params); const runtimeFetch = this.getFetch(); const response = await runtimeFetch(uri, { ...options, redirect: 'manual' }); if (response.status !== 301 && response.status !== 302) { throw new AppcondaException('Invalid redirect', response.status); } return response.headers.get('location') || ''; } async call(method: string, url: URL, headers: Headers = {}, params: Payload = {}, responseType = 'json'): Promise { const { uri, options } = this.prepareRequest(method, url, headers, params); const runtimeFetch = this.getFetch(); let data: any = null; try { const response = await runtimeFetch(uri, options); const warnings = response.headers.get('x-appconda-warning'); if (warnings) { warnings.split(';').forEach((warning: string) => console.warn('Warning: ' + warning)); } if (response.headers.get('content-type')?.includes('application/json')) { data = await response.json(); } else if (responseType === 'arrayBuffer') { data = await response.arrayBuffer(); } else { data = { message: await response.text() }; } if (400 <= response.status) { throw new AppcondaException(data?.message, response.status, data?.type, data); } return data; } catch (e) { console.error(e); throw e; } } static flatten(data: Payload, prefix = ''): Payload { let output: Payload = {}; for (const [key, value] of Object.entries(data)) { let finalKey = prefix ? prefix + '[' + key + ']' : key; if (Array.isArray(value)) { output = { ...output, ...Client.flatten(value, finalKey) }; } else { output[finalKey] = value; } } return output; } } export { Client, AppcondaException }; export { Query } from './query'; export type { Models, Payload, UploadProgress }; export type { QueryTypes, QueryTypesList } from './query';