diff --git a/README.md b/README.md index 7c9b0d1..0ae6005 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,8 @@ Der API-Key braucht die Scopes **Databases/Tables (read+write), Storage/Buckets - **`created_at`-Spalten entfallen** – Appwrite pflegt `$createdAt` automatisch (Indizes nutzen `$createdAt` direkt, z. B. `score_events`, `jobs`). - **Enum-Werte ohne Umlaute** (`laeuft` statt `läuft`). +- **`attributes.tags`: Key-Index statt Volltext-Index** – Appwrite verbietet + Fulltext auf Array-Spalten; Tag-Suche läuft über `Query.contains`. - **S3/Hetzner-Backend für Storage** ist eine *instanzweite* Appwrite-Einstellung (`_APP_STORAGE_DEVICE`) und betrifft alle Projekte auf dem Server – wird daher nicht vom Skript gesetzt, sondern muss bewusst am Server konfiguriert werden. diff --git a/scripts/check-appwrite.mjs b/scripts/check-appwrite.mjs new file mode 100644 index 0000000..d06d42e --- /dev/null +++ b/scripts/check-appwrite.mjs @@ -0,0 +1,49 @@ +#!/usr/bin/env node +/** + * BrandLoop – Schema-Check: listet Tabellen (Spalten-/Index-Status), Buckets + * und Teams und meldet alles, was nicht "available" ist. Exit-Code 1 bei Problemen. + * Aufruf wie setup-appwrite.mjs (Key aus env oder /home/webklar/apps/.env). + */ +import { readFileSync } from 'node:fs'; + +const ENDPOINT = process.env.APPWRITE_ENDPOINT || 'https://appwrite.webklar.com/v1'; +const PROJECT = process.env.APPWRITE_PROJECT || '6a5cee34002bb8360c34'; +const ENV_FILE = process.env.ENV_FILE || '/home/webklar/apps/.env'; +const DB_ID = 'brandloop'; + +const API_KEY = process.env.APPWRITE_API_KEY + || readFileSync(ENV_FILE, 'utf8').match(/^BRANDLOOP_APPWRITE_API_KEY=(.+)$/m)?.[1]?.trim(); +if (!API_KEY) { console.error('Kein API-Key gefunden.'); process.exit(1); } + +async function api(path) { + const res = await fetch(`${ENDPOINT}${path}`, { + headers: { 'X-Appwrite-Project': PROJECT, 'X-Appwrite-Key': API_KEY }, + }); + if (!res.ok) throw new Error(`GET ${path}: HTTP ${res.status}`); + return res.json(); +} + +const limit = encodeURIComponent(JSON.stringify({ method: 'limit', values: [200] })); +let problems = 0; + +const tables = await api(`/tablesdb/${DB_ID}/tables?queries[]=${limit}`); +for (const t of tables.tables) { + const cols = await api(`/tablesdb/${DB_ID}/tables/${t.$id}/columns?queries[]=${limit}`); + const idxs = await api(`/tablesdb/${DB_ID}/tables/${t.$id}/indexes?queries[]=${limit}`); + const badC = cols.columns.filter(c => c.status !== 'available'); + const badI = idxs.indexes.filter(i => i.status !== 'available'); + console.log(`${t.$id.padEnd(18)} ${String(cols.columns.length).padStart(2)} Spalten, ${idxs.indexes.length} Indizes` + + `${t.rowSecurity ? '' : ' [nur Team-Rechte]'}`); + for (const c of badC) { problems++; console.log(` ! Spalte ${c.key}: ${c.status} ${c.error || ''}`); } + for (const i of badI) { problems++; console.log(` ! Index ${i.key}: ${i.status} ${i.error || ''}`); } +} +console.log(`\nTabellen: ${tables.total}`); + +const buckets = await api(`/storage/buckets?queries[]=${limit}`); +console.log(`Buckets: ${buckets.total} (${buckets.buckets.map(b => b.$id).join(', ')})`); + +const teams = await api(`/teams?queries[]=${limit}`); +console.log(`Teams: ${teams.total} (${teams.teams.map(t => t.$id).join(', ')})`); + +if (problems) { console.log(`\n${problems} Problem(e) gefunden.`); process.exit(1); } +console.log('\nAlles verfügbar.'); diff --git a/scripts/setup-appwrite.mjs b/scripts/setup-appwrite.mjs index 371ea98..e8a4ce4 100644 --- a/scripts/setup-appwrite.mjs +++ b/scripts/setup-appwrite.mjs @@ -162,7 +162,8 @@ const TABLES = [ { key: 'idx_topn', type: 'key', columns: ['brand_id', 'category_id', 'status', 'score'], orders: ['ASC', 'ASC', 'ASC', 'DESC'] }, { key: 'idx_parent', type: 'key', columns: ['parent_id'] }, { key: 'idx_slug', type: 'key', columns: ['brand_id', 'slug'] }, - { key: 'ft_tags', type: 'fulltext', columns: ['tags'], optional: true }, + // Fulltext ist auf Array-Spalten verboten → Key-Index (reicht für Query.contains) + { key: 'idx_tags', type: 'key', columns: ['tags'] }, ], }, { @@ -340,6 +341,10 @@ async function main() { rowSecurity: t.rowSecurity, }); for (const c of t.columns) { + // Erst GET: Appwrite prüft das Zeilengrößen-Limit VOR dem Duplikat-Check, + // ein erneutes POST auf große Spalten gäbe sonst 400 statt 409. + const { status } = await api('GET', `/tablesdb/${DB_ID}/tables/${t.id}/columns/${c.body.key}`); + if (status === 200) { skipped++; console.log(` = ${t.id}.${c.body.key} (existiert)`); continue; } await ensure(`${t.id}.${c.body.key}`, 'POST', `/tablesdb/${DB_ID}/tables/${t.id}/columns/${c.type}`, c.body); } }