feat(api): Logo-Endpunkte, Fehlercodes und gemeinsame Helfer
- common.js (aus app.js herausgelöst, Verhalten gleich): MSG, HttpError, Prüfungen, Rechte-Strings, Melder-Hash,
Bildmaße und die Dateiprüfungen checkFileExists/checkFileOwnership. Neu: die Logo-Texte in MSG, codeFor,
likeRowPermissions. app.js exportiert weiter alles wie bisher.
- Fehlerantworten sind jetzt {error, code}: code = MSG-Schlüssel des Textes, dynamische Prüftexte badRequest,
Appwrite-Fehler unavailable (502) bzw. internal (500).
- logos.js: POST /api/logos/hochladen, /herz, /melden, /loeschen und /api/admin/logo-sichtbarkeit nach dem Vorbild der
Karten-Endpunkte (Datei prüfen, Zeile anlegen, Datei auf read("any") festlegen, sonst Rücknahme; likeCount und
reportCount immer aus countRows; ein gemeinsames Rate-Limit für Karten- und Logo-Meldungen).
- appwrite.js: countRows. fake-appwrite.js: Tabellen logos, logo_likes, logo_reports mit Spaltenlängen, Bucket
logo-images und total in den Listen-Antworten.
- Tests: logos.test.js (59), code-Fälle und MSG-Eindeutigkeit in app.test.js (+7), countRows in appwrite.test.js (+2);
alle 111 bisherigen Tests unverändert grün (179 gesamt).
- Doku: api/README.md (Endpunkte, Fehlercodes, Logo-Regeln), deploy/README.md (Betrieb kopiert fünf Dateien).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
88abc2061d
commit
dc5de99611
10 files changed
+2181
-286
No files matched your search
+52
-13
@@ -9,17 +9,20 @@
|
||||
* GET|PUT|DELETE /v1/storage/buckets/{b}/files/{f} PUT {permissions}
|
||||
* GET /v1/storage/buckets/{b}/files/{f}/view Dateiinhalt (Bytes aus `content`)
|
||||
*
|
||||
* Die Tabelle `cards` ist nur zum Lesen da (Zeilen legt der Test mit `addCard` an).
|
||||
* Die Tabelle `cards` ist nur zum Lesen da (Zeilen legt der Test mit `addCard` an). Es gibt außerdem die Tabellen
|
||||
* `gallery`, `reports`, `logos`, `logo_likes` und `logo_reports` und die Buckets `gallery-images`, `card-images`, `logo-images`.
|
||||
*
|
||||
* Abfragen: `queries[]=<json>` mit den Methoden equal, cursorAfter und limit. Zeilen werden gegen die Spaltenlängen der
|
||||
* echten Tabellen geprüft (document_invalid_structure), damit zu lange Werte im Test auffallen.
|
||||
* Abfragen: `queries[]=<json>` mit den Methoden equal, cursorAfter und limit. Die Antwort enthält wie bei Appwrite `total`:
|
||||
* die Zahl aller Treffer der equal-Abfragen, unabhängig von limit und cursorAfter (darauf baut `countRows`).
|
||||
* Zeilen werden gegen die Spaltenlängen der echten Tabellen geprüft (document_invalid_structure), damit zu lange Werte im
|
||||
* Test auffallen.
|
||||
* Dateien werden nach ID gesucht und liefern ihren echten `bucketId` zurück (der Bucket in der URL zählt nicht),
|
||||
* so lässt sich der Fall „Datei liegt in einem anderen Bucket“ nachstellen.
|
||||
*/
|
||||
|
||||
const http = require('node:http');
|
||||
|
||||
const BUCKETS = ['gallery-images', 'card-images'];
|
||||
const BUCKETS = ['gallery-images', 'card-images', 'logo-images'];
|
||||
|
||||
/** PNG-Anfang (Signatur + IHDR) mit den Maßen; reicht für die Größenprüfung der API. */
|
||||
function pngHeader(width, height) {
|
||||
@@ -80,10 +83,43 @@ const COLUMNS = {
|
||||
reason: { enum: ['unangemessen', 'kartendaten', 'urheberrecht', 'spam', 'sonstiges'], required: true },
|
||||
reporterHash: { size: 64, required: true },
|
||||
},
|
||||
// Spalten wie in web/scripts/appwrite-setup.mjs (Community-Logos)
|
||||
logos: {
|
||||
name: { size: 40, required: true },
|
||||
displayName: { size: 30 },
|
||||
fileId: { size: 36, required: true },
|
||||
ownerId: { size: 36, required: true },
|
||||
mono: { type: 'boolean' },
|
||||
likeCount: { type: 'integer', min: 0 },
|
||||
hidden: { type: 'boolean' },
|
||||
reportCount: { type: 'integer', min: 0 },
|
||||
},
|
||||
logo_likes: {
|
||||
logoId: { size: 36, required: true },
|
||||
userId: { size: 36, required: true },
|
||||
},
|
||||
logo_reports: {
|
||||
logoId: { size: 36, required: true },
|
||||
reason: { enum: ['unangemessen', 'kartendaten', 'urheberrecht', 'spam', 'sonstiges'], required: true },
|
||||
reporterHash: { size: 64, required: true },
|
||||
},
|
||||
};
|
||||
|
||||
/** Standardwerte der Spalten, die Appwrite beim Anlegen einsetzt, wenn die Anfrage sie weglässt. */
|
||||
const DEFAULTS = {
|
||||
gallery: { hidden: false, reportCount: 0 },
|
||||
logos: { mono: false, likeCount: 0, hidden: false, reportCount: 0 },
|
||||
};
|
||||
|
||||
async function startFakeAppwrite({ project = 'proj-test', key = 'KEY-test-secret-value', database = 'aircard' } = {}) {
|
||||
const tables = { gallery: new Map(), reports: new Map(), cards: new Map() };
|
||||
const tables = {
|
||||
gallery: new Map(),
|
||||
reports: new Map(),
|
||||
cards: new Map(),
|
||||
logos: new Map(),
|
||||
logo_likes: new Map(),
|
||||
logo_reports: new Map(),
|
||||
};
|
||||
const files = new Map();
|
||||
const contents = new Map();
|
||||
const users = new Map();
|
||||
@@ -150,7 +186,7 @@ async function startFakeAppwrite({ project = 'proj-test', key = 'KEY-test-secret
|
||||
const column = columns[name];
|
||||
if (!column) return `Unknown attribute: ${name}`;
|
||||
if (column.type === 'boolean' && typeof value !== 'boolean') return `Invalid ${name}`;
|
||||
if (column.type === 'integer' && !Number.isInteger(value)) return `Invalid ${name}`;
|
||||
if (column.type === 'integer' && (!Number.isInteger(value) || (column.min !== undefined && value < column.min))) return `Invalid ${name}`;
|
||||
if (column.enum && !column.enum.includes(value)) return `Invalid ${name}`;
|
||||
if (column.size !== undefined) {
|
||||
if (typeof value !== 'string') return `Invalid ${name}`;
|
||||
@@ -231,15 +267,14 @@ async function startFakeAppwrite({ project = 'proj-test', key = 'KEY-test-secret
|
||||
const data = body.data || {};
|
||||
const problem = validateData(table, data, { partial: false });
|
||||
if (problem) return error(400, 'document_invalid_structure', problem);
|
||||
const defaults = table === 'gallery' ? { hidden: false, reportCount: 0 } : {};
|
||||
const row = addRow(table, body.rowId, { ...defaults, ...data }, body.permissions || []);
|
||||
const row = addRow(table, body.rowId, { ...DEFAULTS[table], ...data }, body.permissions || []);
|
||||
return reply(201, row);
|
||||
}
|
||||
|
||||
if (segments.length === 6 && req.method === 'GET') {
|
||||
const rows = runQueries([...store.values()], search);
|
||||
if (rows === null) return error(400, 'general_query_invalid');
|
||||
return reply(200, { total: rows.length, rows });
|
||||
const found = runQueries([...store.values()], search);
|
||||
if (found === null) return error(400, 'general_query_invalid');
|
||||
return reply(200, found);
|
||||
}
|
||||
|
||||
if (segments.length === 7) {
|
||||
@@ -339,7 +374,10 @@ function safeParse(text) {
|
||||
}
|
||||
}
|
||||
|
||||
/** Wendet equal, cursorAfter und limit an (Reihenfolge = Anlegereihenfolge); null bei unbekannter Abfrage. */
|
||||
/**
|
||||
* Wendet equal, cursorAfter und limit an (Reihenfolge = Anlegereihenfolge); null bei unbekannter Abfrage.
|
||||
* Liefert {total, rows}: `total` zählt alle Treffer der equal-Abfragen, `rows` ist die Seite nach cursorAfter und limit.
|
||||
*/
|
||||
function runQueries(rows, search) {
|
||||
let result = rows;
|
||||
let limit = 25;
|
||||
@@ -364,12 +402,13 @@ function runQueries(rows, search) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
const total = result.length;
|
||||
if (cursor !== null) {
|
||||
const index = result.findIndex((row) => row.$id === cursor);
|
||||
if (index === -1) return null;
|
||||
result = result.slice(index + 1);
|
||||
}
|
||||
return result.slice(0, limit);
|
||||
return { total, rows: result.slice(0, limit) };
|
||||
}
|
||||
|
||||
module.exports = { startFakeAppwrite, pngHeader, webpHeader };
|
||||
Reference in new issue
Block a user