diff --git a/scripts/projektarten-setup.mjs b/scripts/projektarten-setup.mjs new file mode 100644 index 0000000..b12fd2a --- /dev/null +++ b/scripts/projektarten-setup.mjs @@ -0,0 +1,96 @@ +#!/usr/bin/env node +/** + * Projektarten: legt die neuen Attribute an der Collection websiteProjects an. + * Idempotent: vorhandene Attribute werden uebersprungen. + * Usage: + * node scripts/projektarten-setup.mjs --check (nur melden, nichts anlegen) + * node scripts/projektarten-setup.mjs (fehlende Attribute anlegen) + */ +import { pathToFileURL } from 'node:url' +import { config } from '../server/config.js' + +export const PROJECT_KIND_ATTRIBUTES = [ + { key: 'kind', type: 'string', size: 20, required: false }, + { key: 'forCustomer', type: 'boolean', required: false }, + { key: 'kindSetBy', type: 'string', size: 64, required: false }, + { key: 'kindSetAt', type: 'string', size: 40, required: false }, + { key: 'createdByGitea', type: 'string', size: 64, required: false }, + { key: 'assignMailSentAt', type: 'string', size: 40, required: false }, + { key: 'assignReminderSentAt', type: 'string', size: 40, required: false }, + { key: 'assignMailAttempts', type: 'integer', 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() { + const data = await aw('/attributes') + 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 = PROJECT_KIND_ATTRIBUTES.filter((a) => !existing.has(a.key)) + + for (const attr of PROJECT_KIND_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/test/projektartenSetup.test.js b/test/projektartenSetup.test.js new file mode 100644 index 0000000..a0b228d --- /dev/null +++ b/test/projektartenSetup.test.js @@ -0,0 +1,47 @@ +import { test } from 'node:test' +import assert from 'node:assert/strict' +import { PROJECT_KIND_ATTRIBUTES, main } from '../scripts/projektarten-setup.mjs' + +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 8 Attribute, alle optional', () => { + assert.deepEqual( + PROJECT_KIND_ATTRIBUTES.map((a) => a.key), + ['kind', 'forCustomer', 'kindSetBy', 'kindSetAt', 'createdByGitea', 'assignMailSentAt', 'assignReminderSentAt', 'assignMailAttempts'] + ) + assert.ok(PROJECT_KIND_ATTRIBUTES.every((a) => a.required === false)) + const byKey = Object.fromEntries(PROJECT_KIND_ATTRIBUTES.map((a) => [a.key, a])) + assert.equal(byKey.kind.size, 20) + assert.equal(byKey.kindSetBy.size, 64) + assert.equal(byKey.createdByGitea.size, 64) + assert.equal(byKey.kindSetAt.size, 40) + assert.equal(byKey.forCustomer.type, 'boolean') + assert.equal(byKey.assignMailAttempts.type, 'integer') +}) + +test('main legt nur fehlende Attribute an (kind existiert schon)', async () => { + const client = mockClient(['kind']) + const result = await main({ check: false, client, log: () => {} }) + assert.equal(client.created.length, 7) + assert.ok(!client.created.includes('kind')) + assert.deepEqual(result.missing.length, 7) +}) + +test('main --check legt nichts an und meldet Fehlende', async () => { + const client = mockClient(['kind']) + const result = await main({ check: true, client, log: () => {} }) + assert.equal(client.created.length, 0) + assert.equal(result.missing.length, 7) +})