import http from 'node:http' import { randomUUID } from 'node:crypto' /** * Nachgebautes Appwrite fuer Tests: nur der REST-Ausschnitt, den `appwriteAdmin.js` und * `appwriteStaff.js` nutzen (Dokumente einer Sammlung, `/account` per JWT). Laeuft auf * 127.0.0.1 mit einem vom Betriebssystem vergebenen Port; `endpoint` endet auf `/v1`, * genau wie `config.appwrite.endpoint`. * * @param {object} [options] * @param {Record} [options.seed] * Anfangsbestand je Sammlung (Sammlungsname -> Dokumente). Jedes Dokument darf `$id` * mitbringen (sonst wird eins erzeugt); `$createdAt`/`$updatedAt` werden sonst mit dem * Startzeitpunkt gefuellt. * @returns {Promise<{ * endpoint: string, * all: (collection: string) => object[], * get: (collection: string, id: string) => (object|undefined), * calls: Array<{method: string, path: string, query: Record, headers: Record, body: any}>, * close: () => Promise, * }>} `all`/`get` lesen den LIVE-Stand (nach create/patch/delete ueber HTTP). Die * Datenbank-ID im Pfad wird nicht geprueft - jede wird akzeptiert. */ export async function startFakeAppwrite(options = {}) { const calls = [] /** @type {Map>} Sammlungsname -> (Dokument-ID -> Dokument). */ const store = new Map() function collectionOf(name) { let map = store.get(name) if (!map) { map = new Map() store.set(name, map) } return map } function all(col) { return [...collectionOf(col).values()] } function get(col, id) { return collectionOf(col).get(id) } function send(res, status, body) { if (body === undefined) { res.writeHead(status) res.end() return } const text = JSON.stringify(body) res.writeHead(status, { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(text), }) res.end(text) } function parseQueries(searchParams) { return searchParams .getAll('queries[]') .map((s) => { try { return JSON.parse(s) } catch { return null } }) .filter(Boolean) } /* ------------------------------------------------------------- Routen --- */ function handleAccount(match, searchParams, body, headers, res) { if (headers['x-appwrite-jwt'] === 'jwt-admin') { return send(res, 200, { $id: 'staff1', labels: ['admin'] }) } send(res, 401, { message: 'Unauthorized' }) } function handleListDocuments(match, searchParams, body, headers, res) { const docs = [...collectionOf(match[2]).values()] const queries = parseQueries(searchParams) let result = docs.filter((doc) => queries.every((q) => { if (q.method === 'equal') return q.values.includes(doc[q.attribute]) if (q.method === 'greaterThan') return doc[q.attribute] > q.values[0] return true }) ) const orderAsc = queries.find((q) => q.method === 'orderAsc') const orderDesc = queries.find((q) => q.method === 'orderDesc') const sortAttr = orderAsc?.attribute || orderDesc?.attribute if (sortAttr) { const dir = orderAsc ? 1 : -1 result = [...result].sort((a, b) => { if (a[sortAttr] === b[sortAttr]) return 0 return a[sortAttr] > b[sortAttr] ? dir : -dir }) } const total = result.length const limit = queries.find((q) => q.method === 'limit') if (limit) result = result.slice(0, Number(limit.values[0])) send(res, 200, { total, documents: result }) } function handleCreateDocument(match, searchParams, body, headers, res) { const map = collectionOf(match[2]) const documentId = body?.documentId || randomUUID() if (map.has(documentId)) { return send(res, 409, { message: 'Document with the requested ID already exists.' }) } const now = new Date().toISOString() const doc = { $id: documentId, $createdAt: now, $updatedAt: now, ...(body?.data || {}) } map.set(documentId, doc) send(res, 201, doc) } function handleGetDocument(match, searchParams, body, headers, res) { const doc = collectionOf(match[2]).get(match[3]) if (!doc) return send(res, 404, { message: 'Document not found' }) send(res, 200, doc) } function handlePatchDocument(match, searchParams, body, headers, res) { const doc = collectionOf(match[2]).get(match[3]) if (!doc) return send(res, 404, { message: 'Document not found' }) Object.assign(doc, body?.data || {}) doc.$updatedAt = new Date().toISOString() send(res, 200, doc) } function handleDeleteDocument(match, searchParams, body, headers, res) { const map = collectionOf(match[2]) if (!map.has(match[3])) return send(res, 404, { message: 'Document not found' }) map.delete(match[3]) send(res, 204) } const ROUTES = [ { method: 'GET', re: /^\/v1\/account$/, handler: handleAccount }, { method: 'GET', re: /^\/v1\/databases\/([^/]+)\/collections\/([^/]+)\/documents$/, handler: handleListDocuments, }, { method: 'POST', re: /^\/v1\/databases\/([^/]+)\/collections\/([^/]+)\/documents$/, handler: handleCreateDocument, }, { method: 'GET', re: /^\/v1\/databases\/([^/]+)\/collections\/([^/]+)\/documents\/([^/]+)$/, handler: handleGetDocument, }, { method: 'PATCH', re: /^\/v1\/databases\/([^/]+)\/collections\/([^/]+)\/documents\/([^/]+)$/, handler: handlePatchDocument, }, { method: 'DELETE', re: /^\/v1\/databases\/([^/]+)\/collections\/([^/]+)\/documents\/([^/]+)$/, handler: handleDeleteDocument, }, ] /* --------------------------------------------------------- Server --- */ const sockets = new Set() const server = http.createServer((req, res) => { const chunks = [] req.on('data', (c) => chunks.push(c)) req.on('end', () => { const raw = Buffer.concat(chunks).toString('utf8') const url = new URL(req.url, 'http://fake-appwrite.local') let parsedBody = null if (raw) { try { parsedBody = JSON.parse(raw) } catch { parsedBody = null } } calls.push({ method: req.method, path: url.pathname, query: Object.fromEntries(url.searchParams), headers: { ...req.headers }, body: parsedBody, }) const route = ROUTES.find((r) => r.method === req.method && r.re.test(url.pathname)) if (!route) return send(res, 404, { message: 'Not Found' }) const match = route.re.exec(url.pathname) try { route.handler(match, url.searchParams, parsedBody, req.headers, res) } catch (err) { send(res, 500, { message: err?.message || 'fakeAppwrite: interner Fehler' }) } }) }) server.on('connection', (socket) => { sockets.add(socket) socket.on('close', () => sockets.delete(socket)) }) await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve)) const { port } = server.address() const endpoint = `http://127.0.0.1:${port}/v1` /* --------------------------------------------------------- Seeding --- */ const seededAt = new Date().toISOString() for (const [col, docs] of Object.entries(options.seed || {})) { const map = collectionOf(col) for (const doc of docs || []) { const { $id, $createdAt, $updatedAt, ...fields } = doc const id = $id || randomUUID() map.set(id, { $id: id, $createdAt: $createdAt || seededAt, $updatedAt: $updatedAt || seededAt, ...fields }) } } function close() { for (const socket of sockets) socket.destroy() return new Promise((resolve) => server.close(() => resolve())) } return { endpoint, all, get, calls, close } }