Analyse: Einwilligungs-Historie, Nachweis beim Einschalten, Sperre ohne Verantwortlichen-Angaben, Textform für Mitarbeiter
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
c6824f83a6
commit
3d05472b25
14 files changed
+585
-115
No files matched your search
+106
-30
@@ -1,10 +1,11 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Website-Analyse: legt die Analyse-Attribute an der Collection portalFeatures an.
|
||||
* Idempotent: vorhandene Attribute werden uebersprungen.
|
||||
* Website-Analyse: legt die Analyse-Attribute an der Collection portalFeatures an und die
|
||||
* Collection analyseConsents (Einwilligungs-Historie, nur Server-Key). Idempotent: Vorhandenes
|
||||
* wird uebersprungen. Vor dem Einschalten der Analyse (Livegang) ausfuehren.
|
||||
* Usage:
|
||||
* node scripts/analyse-setup.mjs --check (nur melden, nichts anlegen)
|
||||
* node scripts/analyse-setup.mjs (fehlende Attribute anlegen)
|
||||
* node scripts/analyse-setup.mjs (Fehlendes anlegen)
|
||||
*/
|
||||
import { pathToFileURL } from 'node:url'
|
||||
import { config } from '../server/config.js'
|
||||
@@ -18,16 +19,37 @@ export const ANALYSE_ATTRIBUTES = [
|
||||
{ key: 'offSince', type: 'string', size: 40, required: false },
|
||||
{ key: 'activatedMailAt', type: 'string', size: 40, required: false },
|
||||
{ key: 'privacyTicketId', type: 'string', size: 40, required: false },
|
||||
{ key: 'avvConsentDate', type: 'string', size: 10, required: false },
|
||||
{ key: 'avvConsentChannel', type: 'string', size: 10, required: false },
|
||||
{ key: 'removalTicketId', type: 'string', size: 40, required: false },
|
||||
{ key: 'publishTicketId', type: 'string', size: 40, required: false },
|
||||
]
|
||||
|
||||
/** Echter Appwrite-Client (REST) fuer die Collection portalFeatures. */
|
||||
const str = (key, size) => ({ key, type: 'string', size, required: false })
|
||||
|
||||
/** Einwilligungs-Historie: nur anfuegen, nie aendern; kein Client-Zugriff. */
|
||||
export const CONSENT_COLLECTION_NAME = 'analyseConsents'
|
||||
export const CONSENT_ATTRIBUTES = [
|
||||
str('customerId', 36),
|
||||
str('projectId', 36),
|
||||
str('siteKey', 40),
|
||||
str('event', 8),
|
||||
str('avvVersion', 20),
|
||||
str('via', 8),
|
||||
str('acceptedAt', 40),
|
||||
str('acceptedBy', 100),
|
||||
str('textformDate', 10),
|
||||
str('textformChannel', 10),
|
||||
]
|
||||
export const CONSENT_INDEXES = [{ key: 'projectId_idx', type: 'key', attributes: ['projectId'] }]
|
||||
|
||||
/** Echter Appwrite-Client (REST); jede Methode nimmt die Collection-ID als ersten Parameter. */
|
||||
export function createAppwriteClient() {
|
||||
const { endpoint, projectId, databaseId, apiKey } = config.appwrite
|
||||
const collectionId = config.collections.portalFeatures
|
||||
const base = `${endpoint.replace(/\/$/, '')}/databases/${databaseId}/collections/${collectionId}`
|
||||
const db = `${endpoint.replace(/\/$/, '')}/databases/${databaseId}/collections`
|
||||
|
||||
async function aw(path, { method = 'GET', body } = {}) {
|
||||
const response = await fetch(`${base}${path}`, {
|
||||
const response = await fetch(`${db}${path}`, {
|
||||
method,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -37,60 +59,114 @@ export function createAppwriteClient() {
|
||||
body: body ? JSON.stringify(body) : undefined,
|
||||
})
|
||||
const data = await response.json().catch(() => ({}))
|
||||
if (!response.ok) throw new Error(data.message || `Appwrite ${response.status}`)
|
||||
if (!response.ok) {
|
||||
const err = new Error(data.message || `Appwrite ${response.status}`)
|
||||
err.status = response.status
|
||||
throw err
|
||||
}
|
||||
return data
|
||||
}
|
||||
// Ohne limit liefert Appwrite nur 25 Eintraege
|
||||
const q = encodeURIComponent(JSON.stringify({ method: 'limit', values: [200] }))
|
||||
|
||||
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 collectionExists(id) {
|
||||
try {
|
||||
await aw(`/${id}`)
|
||||
return true
|
||||
} catch (err) {
|
||||
if (err.status === 404) return false
|
||||
throw err
|
||||
}
|
||||
},
|
||||
async createAttribute({ type, ...body }) {
|
||||
await aw(`/attributes/${type}`, { method: 'POST', body })
|
||||
/** Legt die Collection ohne Rechte an (nur Server-Key); true = neu angelegt. */
|
||||
async ensureCollection(id, name) {
|
||||
if (await this.collectionExists(id)) return false
|
||||
await aw('', { method: 'POST', body: { collectionId: id, name, permissions: [], documentSecurity: false } })
|
||||
return true
|
||||
},
|
||||
async waitForAttribute(key, maxSec = 30) {
|
||||
async listAttributes(id) {
|
||||
return (await aw(`/${id}/attributes?queries[]=${q}`)).attributes || []
|
||||
},
|
||||
async createAttribute(id, { type, ...body }) {
|
||||
await aw(`/${id}/attributes/${type}`, { method: 'POST', body })
|
||||
},
|
||||
async waitForAttribute(id, key, maxSec = 30) {
|
||||
for (let i = 0; i < maxSec; i += 1) {
|
||||
const attr = await aw(`/attributes/${key}`).catch(() => null)
|
||||
const attr = await aw(`/${id}/attributes/${key}`).catch(() => null)
|
||||
if (attr?.status === 'available') return
|
||||
await new Promise((r) => setTimeout(r, 1000))
|
||||
}
|
||||
throw new Error(`Attribut ${key} wurde nicht verfuegbar`)
|
||||
},
|
||||
async listIndexes(id) {
|
||||
return (await aw(`/${id}/indexes?queries[]=${q}`)).indexes || []
|
||||
},
|
||||
async createIndex(id, index) {
|
||||
await aw(`/${id}/indexes`, { method: 'POST', body: index })
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
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 = ANALYSE_ATTRIBUTES.filter((a) => !existing.has(a.key))
|
||||
const featuresId = config.collections.portalFeatures
|
||||
const consentsId = config.collections.analyseConsents
|
||||
const missing = []
|
||||
|
||||
for (const attr of ANALYSE_ATTRIBUTES) {
|
||||
if (existing.has(attr.key)) log(`= Attribut ${attr.key} (existiert)`)
|
||||
else if (check) log(`- Attribut ${attr.key} FEHLT`)
|
||||
// Attribute einer Collection abgleichen; `exists` false = Collection fehlt (alle Attribute fehlen)
|
||||
async function syncAttributes(collectionId, attributes, exists) {
|
||||
const have = exists ? new Set((await api.listAttributes(collectionId)).map((a) => a.key)) : new Set()
|
||||
const absent = attributes.filter((a) => !have.has(a.key))
|
||||
for (const attr of attributes) {
|
||||
if (have.has(attr.key)) log(`= ${collectionId}.${attr.key} (existiert)`)
|
||||
else if (check) log(`- ${collectionId}.${attr.key} FEHLT`)
|
||||
}
|
||||
return absent
|
||||
}
|
||||
|
||||
const featureAbsent = await syncAttributes(featuresId, ANALYSE_ATTRIBUTES, true)
|
||||
featureAbsent.forEach((a) => missing.push({ ...a, collection: featuresId }))
|
||||
|
||||
const consentsExist = await api.collectionExists(consentsId)
|
||||
if (!consentsExist) log(`${check ? '-' : '+'} Collection ${consentsId}${check ? ' FEHLT' : ''}`)
|
||||
const consentAbsent = await syncAttributes(consentsId, CONSENT_ATTRIBUTES, consentsExist)
|
||||
consentAbsent.forEach((a) => missing.push({ ...a, collection: consentsId }))
|
||||
const haveIndexes = consentsExist ? new Set((await api.listIndexes(consentsId)).map((i) => i.key)) : new Set()
|
||||
const indexAbsent = CONSENT_INDEXES.filter((i) => !haveIndexes.has(i.key))
|
||||
if (check) indexAbsent.forEach((i) => log(`- Index ${consentsId}.${i.key} FEHLT`))
|
||||
|
||||
if (check) {
|
||||
log(missing.length ? `${missing.length} Attribut(e) fehlen.` : 'Alle Attribute vorhanden.')
|
||||
return { missing }
|
||||
const open = missing.length + indexAbsent.length + (consentsExist ? 0 : 1)
|
||||
log(open ? `${open} Eintrag/Eintraege fehlen.` : 'Alles vorhanden.')
|
||||
return { missing, missingCollection: !consentsExist, missingIndexes: indexAbsent }
|
||||
}
|
||||
|
||||
for (const attr of missing) {
|
||||
await api.createAttribute(attr)
|
||||
log(`+ Attribut ${attr.key}`)
|
||||
for (const attr of featureAbsent) {
|
||||
await api.createAttribute(featuresId, attr)
|
||||
log(`+ Attribut ${featuresId}.${attr.key}`)
|
||||
}
|
||||
if (!consentsExist) await api.ensureCollection(consentsId, CONSENT_COLLECTION_NAME)
|
||||
for (const attr of consentAbsent) {
|
||||
await api.createAttribute(consentsId, attr)
|
||||
log(`+ Attribut ${consentsId}.${attr.key}`)
|
||||
}
|
||||
for (const attr of featureAbsent) await api.waitForAttribute(featuresId, attr.key)
|
||||
for (const attr of consentAbsent) await api.waitForAttribute(consentsId, attr.key)
|
||||
for (const index of indexAbsent) {
|
||||
await api.createIndex(consentsId, index)
|
||||
log(`+ Index ${consentsId}.${index.key}`)
|
||||
}
|
||||
for (const attr of missing) await api.waitForAttribute(attr.key)
|
||||
log('Fertig.')
|
||||
return { missing }
|
||||
return { missing, missingCollection: !consentsExist, missingIndexes: indexAbsent }
|
||||
}
|
||||
|
||||
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))
|
||||
.then(({ missing, missingCollection, missingIndexes }) =>
|
||||
process.exit(check && (missing.length || missingCollection || missingIndexes.length) ? 1 : 0))
|
||||
.catch((e) => {
|
||||
console.error(e.message)
|
||||
process.exit(1)
|
||||
|
||||
Reference in new issue
Block a user