'use strict'; /** * Schmaler REST-Client für Appwrite 1.8 (Node 22, nur Bordmittel) mit dem Server-Key des API-Dienstes. * * createAppwrite({ endpoint, project, key, fetchImpl?, database?, timeoutMs? }) * getRow(table, id) Zeile oder null (404) * listRows(table, queries) Array von Zeilen; queries = JSON-Strings (queryEqual, queryLimit) * createRow(table, rowId, data, permissions) doppelte ID -> AppwriteError mit status 409 * updateRow(table, id, data, permissions?) Rechte nur ändern, wenn übergeben * deleteRow(table, id) true, false bei 404 (wiederholbar) * getFile(bucket, id) Datei-Metadaten ($permissions, bucketId, mimeType, sizeOriginal) oder null * updateFilePermissions(bucket, id, permissions) Datei oder null (404) * deleteFile(bucket, id) true, false bei 404 * accountFromJwt(jwt) Nutzer ($id, labels) oder null (401); OHNE Server-Key * * Jeder Aufruf bricht nach `timeoutMs` (10 s) ab. Jedes Pfadsegment wird mit encodeURIComponent kodiert; * "." und ".." lehnt der Client ab, weil encodeURIComponent sie nicht verändert und die URL-Normalisierung * sie sonst als Verzeichniswechsel auslegen würde. * * Fehler: AppwriteError mit `status` (0 = keine Antwort), `type` (Appwrite-Typ, "timeout" oder "unreachable"). * Der Text enthält nie Key oder JWT. */ const DEFAULT_TIMEOUT_MS = 10_000; class AppwriteError extends Error { constructor(status, type, message) { super(message); this.name = 'AppwriteError'; this.status = status; this.type = type; } } /** Appwrite-Abfrage als JSON-String. */ function queryEqual(attribute, values) { return JSON.stringify({ method: 'equal', attribute, values }); } function queryLimit(count) { return JSON.stringify({ method: 'limit', values: [count] }); } /** Ein Pfadsegment: nur nichtleere Zeichenketten, nie "." oder "..", immer kodiert. */ function segment(value) { if (typeof value !== 'string' || value === '' || value === '.' || value === '..') { throw new TypeError('Ungültiges Pfadsegment'); } return encodeURIComponent(value); } function createAppwrite({ endpoint, project, key, fetchImpl = globalThis.fetch, database = 'aircard', timeoutMs = DEFAULT_TIMEOUT_MS }) { if (typeof endpoint !== 'string' || endpoint === '') throw new TypeError('endpoint fehlt'); if (typeof project !== 'string' || project === '') throw new TypeError('project fehlt'); if (typeof key !== 'string' || key === '') throw new TypeError('key fehlt'); if (typeof fetchImpl !== 'function') throw new TypeError('fetch fehlt'); const base = endpoint.replace(/\/+$/, ''); const rowsPath = (table) => `/tablesdb/${segment(database)}/tables/${segment(table)}/rows`; const filePath = (bucket, id) => `/storage/buckets/${segment(bucket)}/files/${segment(id)}`; /** Ein HTTP-Aufruf; liefert { status, body } für 2xx und 404, sonst wirft er. */ async function send(method, path, { body, headers = {}, withKey = true, tolerate = [] } = {}) { const requestHeaders = { 'X-Appwrite-Project': project, ...headers }; if (withKey) requestHeaders['X-Appwrite-Key'] = key; const init = { method, headers: requestHeaders, signal: AbortSignal.timeout(timeoutMs) }; if (body !== undefined) { requestHeaders['Content-Type'] = 'application/json'; init.body = JSON.stringify(body); } let response; let text; try { response = await fetchImpl(`${base}${path}`, init); text = await response.text(); } catch (err) { // Fehlertexte von fetch bleiben draußen (sie könnten Adresse und Header enthalten). const timedOut = err && (err.name === 'TimeoutError' || err.name === 'AbortError'); throw timedOut ? new AppwriteError(0, 'timeout', 'Appwrite antwortet nicht rechtzeitig') : new AppwriteError(0, 'unreachable', 'Appwrite nicht erreichbar'); } let parsed = null; if (text !== '') { try { parsed = JSON.parse(text); } catch { parsed = null; } } if (response.status >= 200 && response.status < 300) return { status: response.status, body: parsed }; if (tolerate.includes(response.status)) return { status: response.status, body: null }; const type = parsed && typeof parsed.type === 'string' ? parsed.type : 'unknown'; const message = parsed && typeof parsed.message === 'string' ? parsed.message : `HTTP ${response.status}`; throw new AppwriteError(response.status, type, message); } return { async getRow(table, id) { const result = await send('GET', `${rowsPath(table)}/${segment(id)}`, { tolerate: [404] }); return result.status === 404 ? null : result.body; }, async listRows(table, queries = []) { const search = queries.map((query) => `queries[]=${encodeURIComponent(query)}`).join('&'); const result = await send('GET', `${rowsPath(table)}${search === '' ? '' : `?${search}`}`); return Array.isArray(result.body && result.body.rows) ? result.body.rows : []; }, async createRow(table, rowId, data, permissions) { const result = await send('POST', rowsPath(table), { body: { rowId, data, permissions } }); return result.body; }, async updateRow(table, id, data, permissions) { const body = { data }; if (Array.isArray(permissions)) body.permissions = permissions; const result = await send('PATCH', `${rowsPath(table)}/${segment(id)}`, { body }); return result.body; }, async deleteRow(table, id) { const result = await send('DELETE', `${rowsPath(table)}/${segment(id)}`, { tolerate: [404] }); return result.status !== 404; }, async getFile(bucket, id) { const result = await send('GET', filePath(bucket, id), { tolerate: [404] }); return result.status === 404 ? null : result.body; }, async updateFilePermissions(bucket, id, permissions) { const result = await send('PUT', filePath(bucket, id), { body: { permissions }, tolerate: [404] }); return result.status === 404 ? null : result.body; }, async deleteFile(bucket, id) { const result = await send('DELETE', filePath(bucket, id), { tolerate: [404] }); return result.status !== 404; }, /** Prüft ein Nutzer-JWT: Konto aus GET /account (ohne Server-Key), null bei 401. */ async accountFromJwt(jwt) { try { const result = await send('GET', '/account', { headers: { 'X-Appwrite-JWT': jwt }, withKey: false }); return result.body; } catch (err) { if (err instanceof AppwriteError && err.status === 401) return null; throw err; } }, }; } module.exports = { createAppwrite, AppwriteError, queryEqual, queryLimit };