From 40a8aad3f9111ef95b0487143813a4bd83ad91fa Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 30 Sep 2026 20:28:06 +0000 Subject: [PATCH] Preview: Schema-Skript und Runner-Konfiguration Co-Authored-By: Claude Sonnet 5.5 --- scripts/preview-runner-setup.mjs | 97 ++++++++++++++++++++++++++++++++ server/config.js | 6 ++ test/previewRunnerSetup.test.js | 57 +++++++++++++++++++ 3 files changed, 160 insertions(+) create mode 100644 scripts/preview-runner-setup.mjs create mode 100644 test/previewRunnerSetup.test.js diff --git a/scripts/preview-runner-setup.mjs b/scripts/preview-runner-setup.mjs new file mode 100644 index 0000000..8ca18df --- /dev/null +++ b/scripts/preview-runner-setup.mjs @@ -0,0 +1,97 @@ +#!/usr/bin/env node +/** + * Preview 2.0: legt die Preview-Runner-Attribute an der Collection websiteProjects an. + * Idempotent: vorhandene Attribute werden uebersprungen. + * Usage: + * node scripts/preview-runner-setup.mjs --check (nur melden, nichts anlegen) + * node scripts/preview-runner-setup.mjs (fehlende Attribute anlegen) + */ +import { pathToFileURL } from 'node:url' +import { config } from '../server/config.js' + +export const PREVIEW_RUNNER_ATTRIBUTES = [ + { key: 'previewMode', type: 'string', size: 10, required: false }, + { key: 'previewEnv', type: 'string', size: 20000, required: false }, + { key: 'previewState', type: 'string', size: 20, required: false }, + { key: 'previewDetected', type: 'string', size: 40, required: false }, + { key: 'previewBuiltSha', type: 'string', size: 40, required: false }, + { key: 'previewBuiltAt', type: 'string', size: 40, required: false }, + { key: 'previewError', type: 'string', size: 500, required: false }, +] + +/** Echter Appwrite-Client (REST) fuer die Collection websiteProjects. */ +export function createAppwriteClient() { + const { endpoint, projectId, databaseId, apiKey } = config.appwrite + const collectionId = config.collections.websiteProjects + const base = `${endpoint.replace(/\/$/, '')}/databases/${databaseId}/collections/${collectionId}` + + async function aw(path, { method = 'GET', body } = {}) { + const response = await fetch(`${base}${path}`, { + method, + headers: { + 'Content-Type': 'application/json', + 'X-Appwrite-Project': projectId, + 'X-Appwrite-Key': apiKey, + }, + body: body ? JSON.stringify(body) : undefined, + }) + const data = await response.json().catch(() => ({})) + if (!response.ok) throw new Error(data.message || `Appwrite ${response.status}`) + return data + } + + return { + async listAttributes() { + // Ohne limit liefert Appwrite nur 25 Attribute - die Sammlung hat mehr + const q = encodeURIComponent(JSON.stringify({ method: 'limit', values: [200] })) + const data = await aw(`/attributes?queries[]=${q}`) + return data.attributes || [] + }, + async createAttribute({ type, ...body }) { + await aw(`/attributes/${type}`, { method: 'POST', body }) + }, + async waitForAttribute(key, maxSec = 30) { + for (let i = 0; i < maxSec; i += 1) { + const attr = await aw(`/attributes/${key}`).catch(() => null) + if (attr?.status === 'available') return + await new Promise((r) => setTimeout(r, 1000)) + } + throw new Error(`Attribut ${key} wurde nicht verfuegbar`) + }, + } +} + +export async function main({ check = false, client, log = console.log } = {}) { + const api = client || createAppwriteClient() + const existing = new Set((await api.listAttributes()).map((a) => a.key)) + const missing = PREVIEW_RUNNER_ATTRIBUTES.filter((a) => !existing.has(a.key)) + + for (const attr of PREVIEW_RUNNER_ATTRIBUTES) { + if (existing.has(attr.key)) log(`= Attribut ${attr.key} (existiert)`) + else if (check) log(`- Attribut ${attr.key} FEHLT`) + } + + if (check) { + log(missing.length ? `${missing.length} Attribut(e) fehlen.` : 'Alle Attribute vorhanden.') + return { missing } + } + + for (const attr of missing) { + await api.createAttribute(attr) + log(`+ Attribut ${attr.key}`) + } + for (const attr of missing) await api.waitForAttribute(attr.key) + log('Fertig.') + return { missing } +} + +const invokedDirectly = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href +if (invokedDirectly) { + const check = process.argv.includes('--check') + main({ check }) + .then(({ missing }) => process.exit(check && missing.length ? 1 : 0)) + .catch((e) => { + console.error(e.message) + process.exit(1) + }) +} diff --git a/server/config.js b/server/config.js index a8e28c0..9b242fa 100644 --- a/server/config.js +++ b/server/config.js @@ -303,6 +303,12 @@ export const config = { sessionMaxAge: Number(process.env.PREVIEW_SESSION_MAX_AGE || 60 * 60 * 24 * 7), }, + previewRunner: { + enabled: process.env.PREVIEW_RUNNER === 'on', + url: process.env.PREVIEW_RUNNER_URL || 'http://webklar-preview-runner:8080', + token: process.env.PREVIEW_RUNNER_TOKEN || '', + }, + invoiceNinja: { url: (process.env.INVOICE_NINJA_URL || 'https://invoice.webklar.com').replace(/\/$/, ''), apiToken: process.env.INVOICE_NINJA_API_TOKEN || '', diff --git a/test/previewRunnerSetup.test.js b/test/previewRunnerSetup.test.js new file mode 100644 index 0000000..bb556b7 --- /dev/null +++ b/test/previewRunnerSetup.test.js @@ -0,0 +1,57 @@ +import { test } from 'node:test' +import assert from 'node:assert/strict' +import { PREVIEW_RUNNER_ATTRIBUTES, main } from '../scripts/preview-runner-setup.mjs' +import { config } from '../server/config.js' + +function mockClient(existing = []) { + const created = [] + return { + created, + async listAttributes() { + return existing.map((key) => ({ key })) + }, + async createAttribute(attr) { + created.push(attr.key) + }, + async waitForAttribute() {}, + } +} + +test('Schema: genau die 7 Attribute mit Groessen, alle optional', () => { + assert.deepEqual( + PREVIEW_RUNNER_ATTRIBUTES.map((a) => a.key), + ['previewMode', 'previewEnv', 'previewState', 'previewDetected', 'previewBuiltSha', 'previewBuiltAt', 'previewError'] + ) + assert.ok(PREVIEW_RUNNER_ATTRIBUTES.every((a) => a.required === false && a.type === 'string')) + const sizes = Object.fromEntries(PREVIEW_RUNNER_ATTRIBUTES.map((a) => [a.key, a.size])) + assert.deepEqual(sizes, { + previewMode: 10, + previewEnv: 20000, + previewState: 20, + previewDetected: 40, + previewBuiltSha: 40, + previewBuiltAt: 40, + previewError: 500, + }) +}) + +test('main legt nur fehlende Attribute an', async () => { + const client = mockClient(['previewMode']) + const result = await main({ check: false, client, log: () => {} }) + assert.equal(client.created.length, 6) + assert.ok(!client.created.includes('previewMode')) + assert.equal(result.missing.length, 6) +}) + +test('main --check legt nichts an und meldet Fehlende', async () => { + const client = mockClient(['previewMode']) + const result = await main({ check: true, client, log: () => {} }) + assert.equal(client.created.length, 0) + assert.equal(result.missing.length, 6) +}) + +test('Konfiguration: previewRunner ist standardmaessig aus', () => { + assert.equal(config.previewRunner.enabled, false) + assert.equal(config.previewRunner.url, 'http://webklar-preview-runner:8080') + assert.equal(config.previewRunner.token, '') +})