Projekt-Ablauf im Ticketsystem: Anlegen, Archivieren, Import repariert

- Neues Projekt ohne Subdomain: subdomain null statt '' (Unique-Index
  subdomain_unique, jedes weitere scheiterte mit "Document with the
  requested ID already exists"); ebenso Typ "Internes Projekt"
- Anlegen unter derselben Repo-Sperre wie der Webhook repository/created;
  upsertWebsiteProjectByRepo macht aus einem 409 ein Update
- Gitea-Beschreibung = Anzeigename; Art (kind) beim Anlegen aus dem Ticket
- Reaktivieren setzt status zurueck (vorher blieb 'archived' stehen)
- Import: archivierte Repos als archiviert, Archiv-Stand aus Gitea wird
  gespiegelt, ein fehlerhaftes Repo bricht den Lauf nicht mehr ab
- Abgleich ueberschreibt Projektnamen nicht mehr
- CORS erlaubt PUT (Preview-Variablen speichern)
- Vorlage zieht beim Zuweisen nie in die Org um
- Erinnerungs-Lauf ueberspringt archivierte Projekte
- kaputte Umlaute (U+FFFD) in drei Fehlermeldungen

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Opus 5.5 committed 2026-10-04 13:22:53 +00:00
1 parent 3a0b0089dd
commit e43cdb667b
13 files changed
+498 -83

No files matched your search

+41 -9
View File
@@ -28,6 +28,10 @@ import { randomUUID } from 'node:crypto'
export async function startFakeAppwrite(options = {}) {
const calls = []
const pendingFailures = []
// Optionale Unique-Indizes wie in Appwrite/MariaDB: { websiteProjects: ['repoFullName', 'subdomain'] }.
// Strings vergleichen ohne Gross-/Kleinschreibung, null/undefined kollidieren nie, '' schon.
const uniqueAttrs = options.unique || {}
let beforeCreate = null
/** @type {Map<string, Map<string, object>>} Sammlungsname -> (Dokument-ID -> Dokument). */
const store = new Map()
@@ -114,11 +118,24 @@ export async function startFakeAppwrite(options = {}) {
send(res, 200, { total, documents: result })
}
function handleCreateDocument(match, searchParams, body, headers, res) {
function uniqueConflict(col, data, ownId) {
for (const attr of uniqueAttrs[col] || []) {
const value = data?.[attr]
if (value === null || value === undefined) continue
const norm = (v) => (typeof v === 'string' ? v.toLowerCase() : v)
for (const doc of collectionOf(col).values()) {
if (doc.$id !== ownId && doc[attr] !== null && doc[attr] !== undefined && norm(doc[attr]) === norm(value)) return true
}
}
return false
}
async function handleCreateDocument(match, searchParams, body, headers, res) {
if (beforeCreate) await beforeCreate(match[2], body?.data || {})
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.' })
if (map.has(documentId) || uniqueConflict(match[2], body?.data, documentId)) {
return send(res, 409, { message: 'Document with the requested ID already exists.', type: 'document_already_exists' })
}
const now = new Date().toISOString()
const doc = { $id: documentId, $createdAt: now, $updatedAt: now, ...(body?.data || {}) }
@@ -135,6 +152,9 @@ export async function startFakeAppwrite(options = {}) {
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' })
if (uniqueConflict(match[2], body?.data, doc.$id)) {
return send(res, 409, { message: 'Document with the requested ID already exists.', type: 'document_already_exists' })
}
Object.assign(doc, body?.data || {})
doc.$updatedAt = new Date().toISOString()
send(res, 200, doc)
@@ -212,11 +232,9 @@ export async function startFakeAppwrite(options = {}) {
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' })
}
Promise.resolve()
.then(() => route.handler(match, url.searchParams, parsedBody, req.headers, res))
.catch((err) => send(res, 500, { message: err?.message || 'fakeAppwrite: interner Fehler' }))
})
})
@@ -245,10 +263,24 @@ export async function startFakeAppwrite(options = {}) {
pendingFailures.push({ method, pathIncludes, status })
}
/** fn(collection, data) laeuft vor jeder Anlage (z. B. um einen gleichzeitigen Schreiber nachzustellen). */
function setBeforeCreate(fn) {
beforeCreate = fn || null
}
/** Dokument direkt in den Speicher legen (ohne HTTP), z. B. aus einem setBeforeCreate-Haken. */
function insert(col, doc) {
const now = new Date().toISOString()
const id = doc.$id || randomUUID()
const full = { $createdAt: now, $updatedAt: now, ...doc, $id: id }
collectionOf(col).set(id, full)
return full
}
function close() {
for (const socket of sockets) socket.destroy()
return new Promise((resolve) => server.close(() => resolve()))
}
return { endpoint, all, get, calls, failNext, close }
return { endpoint, all, get, calls, failNext, setBeforeCreate, insert, close }
}