diff --git a/scripts/feature-setup.mjs b/scripts/feature-setup.mjs index 2d15f29..580e887 100644 --- a/scripts/feature-setup.mjs +++ b/scripts/feature-setup.mjs @@ -1,16 +1,17 @@ #!/usr/bin/env node /** * Portal-Funktionen (News, Terminplaner): legt die Attribute an der Collection portalFeatures an, die der - * Verkauf des Terminplaners braucht (Abschaltgrund, Marker der Abo-Ende-Mail, Marker der Zahlungsmeldung). - * Idempotent: Vorhandenes wird uebersprungen. Vor dem Code-Deploy ausfuehren (activate() schreibt die neuen - * Felder; ohne Schema antwortet Appwrite mit "Unknown attribute"). + * Verkauf des Terminplaners braucht (Abschaltgrund, Marker der Abo-Ende-Mail, Marker der Zahlungsmeldung), + * und die Collection featureConsents (Zustimmungsnachweis zur Bestellung, nur Server-Key) samt Attributen und + * Indizes. Idempotent: Vorhandenes wird uebersprungen. Vor dem Code-Deploy ausfuehren (activate() schreibt die + * neuen Felder; ohne Schema antwortet Appwrite mit "Unknown attribute"). * scripts/setup-abo-collections.mjs bleibt unveraendert (Top-Level-Skript ohne Pruefmodus). * Usage: * node scripts/feature-setup.mjs --check (nur melden, nichts anlegen; Exit-Code 1 bei Luecken) * node scripts/feature-setup.mjs (Fehlendes anlegen) * * config.js und der Appwrite-Client werden erst in main() geladen: test/support/schema.js importiert die - * Attributliste, bevor die Tests ihre Umgebung (useTestEnv) setzen koennen. + * Attributlisten, bevor die Tests ihre Umgebung (useTestEnv) setzen koennen. */ import { pathToFileURL } from 'node:url' @@ -26,6 +27,39 @@ export const PORTAL_FEATURE_ATTRIBUTES = [ { type: 'string', key: 'zahlungMeldung', size: 24, required: false }, ] +const str = (key, size) => ({ key, type: 'string', size, required: false }) + +/** + * Zustimmungsnachweis zur Bestellung einer Funktion (Collection featureConsents, nur Server-Key, wie analyseConsents): + * - fassungen: JSON mit den Fassungen der gezeigten Texte; texte: JSON mit den gezeigten Texten (Appwrite macht aus + * dieser Groesse eine TEXT-Spalte; 20000 Zeichen genuegen fuer Zustimmung, Haken und Belehrung) + * - textHash: SHA-256 (hex) ueber die gespeicherten Texte; stripeSessionId: Stripe-Sitzung, zu der der Nachweis gehoert + * - ip, userAgent, konto: Herkunft der Zustimmung; acceptedAt: Zeitpunkt des Klicks (Anker der Widerrufsfrist) + */ +export const CONSENT_COLLECTION_NAME = 'featureConsents' +export const FEATURE_CONSENT_ATTRIBUTES = [ + str('customerId', 36), + str('projectId', 36), + str('featureKey', 16), + str('stripeSessionId', 128), + { key: 'acceptedAt', type: 'datetime', required: false }, + str('fassungen', 1000), + str('texte', 20000), + str('textHash', 64), + str('ip', 64), + str('userAgent', 300), + str('konto', 255), +] +export const FEATURE_CONSENT_INDEXES = [ + { key: 'idx_session', type: 'key', attributes: ['stripeSessionId'] }, + { key: 'idx_project_feature', type: 'key', attributes: ['projectId', 'featureKey', 'acceptedAt'] }, +] + +/** true, wenn das Ergebnis von main() Luecken meldet (Exit-Code 1 im Pruefmodus) */ +export function hatLuecken({ missing = [], missingCollection = false, missingIndexes = [] } = {}) { + return missing.length > 0 || missingCollection || missingIndexes.length > 0 +} + export async function main({ check = false, client, log = console.log } = {}) { const { config } = await import('../server/config.js') let api = client @@ -34,11 +68,12 @@ export async function main({ check = false, client, log = console.log } = {}) { api = createAppwriteClient() } const featuresId = config.collections.portalFeatures + const consentsId = config.collections.featureConsents const missing = [] - // Attribute einer Collection abgleichen: meldet Vorhandenes bzw. (mit --check) jede Luecke und liefert die fehlenden - async function syncAttributes(collectionId, attributes) { - const have = new Set((await api.listAttributes(collectionId)).map((a) => a.key)) + // Attribute einer Collection abgleichen; `exists` false = Collection fehlt (alle Attribute fehlen) + async function syncAttributes(collectionId, attributes, exists = true) { + const have = exists ? new Set((await api.listAttributes(collectionId)).map((a) => a.key)) : new Set() for (const attr of attributes) { if (have.has(attr.key)) log(`= ${collectionId}.${attr.key} (existiert)`) else if (check) log(`- ${collectionId}.${attr.key} FEHLT`) @@ -49,25 +84,49 @@ export async function main({ check = false, client, log = console.log } = {}) { const featureAbsent = await syncAttributes(featuresId, PORTAL_FEATURE_ATTRIBUTES) featureAbsent.forEach((a) => missing.push({ ...a, collection: featuresId })) + const consentsExist = await api.collectionExists(consentsId) + if (!consentsExist && check) log(`- ${consentsId} FEHLT`) + const consentAbsent = await syncAttributes(consentsId, FEATURE_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 = FEATURE_CONSENT_INDEXES.filter((i) => !haveIndexes.has(i.key)) + if (check) indexAbsent.forEach((i) => log(`- ${consentsId}.${i.key} FEHLT`)) + + const result = { missing, missingCollection: !consentsExist, missingIndexes: indexAbsent } if (check) { - log(missing.length ? `${missing.length} Eintrag/Eintraege fehlen.` : 'Alles vorhanden.') - return { missing } + const open = missing.length + indexAbsent.length + (consentsExist ? 0 : 1) + log(open ? `${open} Eintrag/Eintraege fehlen.` : 'Alles vorhanden.') + return result } for (const attr of featureAbsent) { await api.createAttribute(featuresId, attr) log(`+ Attribut ${featuresId}.${attr.key}`) } + if (!consentsExist) { + await api.ensureCollection(consentsId, CONSENT_COLLECTION_NAME) + log(`+ Collection ${consentsId}`) + } + 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) + // Indizes erst, wenn ihre Attribute verfuegbar sind + for (const index of indexAbsent) { + await api.createIndex(consentsId, index) + log(`+ Index ${consentsId}.${index.key}`) + } log('Fertig.') - return { missing } + return result } 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((result) => process.exit(check && hatLuecken(result) ? 1 : 0)) .catch((e) => { console.error(e.message) process.exit(1) diff --git a/server/config.js b/server/config.js index 520205c..3a16081 100644 --- a/server/config.js +++ b/server/config.js @@ -287,6 +287,8 @@ export const config = { websiteProjects: process.env.APPWRITE_COLLECTION_WEBSITE_PROJECTS || 'websiteProjects', portalFeatures: process.env.APPWRITE_COLLECTION_PORTAL_FEATURES || 'portalFeatures', analyseConsents: process.env.APPWRITE_COLLECTION_ANALYSE_CONSENTS || 'analyseConsents', + // Zustimmungsnachweis zur Bestellung einer Funktion (nur Server-Key; scripts/feature-setup.mjs) + featureConsents: process.env.APPWRITE_COLLECTION_FEATURE_CONSENTS || 'featureConsents', workorders: process.env.APPWRITE_COLLECTION_WORKORDERS || 'workorders', worksheets: process.env.APPWRITE_COLLECTION_WORKSHEETS || 'worksheets', employees: process.env.APPWRITE_COLLECTION_EMPLOYEES || 'employees', diff --git a/test/featureSetup.test.js b/test/featureSetup.test.js index 2c9989b..3de0fef 100644 --- a/test/featureSetup.test.js +++ b/test/featureSetup.test.js @@ -4,9 +4,17 @@ import { startAppwriteMock } from './support/appwriteMock.js' import { useTestEnv } from './support/testEnv.js' import { schemaKeys } from './support/schema.js' // Das Skript laedt config.js erst in main(): schema.js (Testhilfe) importiert es vor useTestEnv() -import { PORTAL_FEATURE_ATTRIBUTES, main } from '../scripts/feature-setup.mjs' +import { + CONSENT_COLLECTION_NAME, + FEATURE_CONSENT_ATTRIBUTES, + FEATURE_CONSENT_INDEXES, + PORTAL_FEATURE_ATTRIBUTES, + hatLuecken, + main, +} from '../scripts/feature-setup.mjs' -// Schema-Skript der Funktions-Attribute (scripts/feature-setup.mjs): Mock-Client wie in analyseSetup.test.js +// Schema-Skript der Funktions-Attribute und des Zustimmungsnachweises (scripts/feature-setup.mjs): +// Mock-Client wie in analyseSetup.test.js const schema = { portalFeatures: [] } // wird im Schema-Test gefuellt (die Analyse-Schluessel kennt erst das dynamisch geladene Skript) const aw = await startAppwriteMock({ schema }) after(() => aw.close()) @@ -14,37 +22,55 @@ useTestEnv({ appwriteUrl: aw.url, stripeUrl: 'http://127.0.0.1:1' }) const { config } = await import('../server/config.js') const { ANALYSE_ATTRIBUTES } = await import('../scripts/analyse-setup.mjs') -function mockClient({ attrs = [] } = {}) { - const calls = { attrs: [], collections: [], indexes: [], waits: [] } +const CONSENT_KEYS = [ + 'customerId', 'projectId', 'featureKey', 'stripeSessionId', 'acceptedAt', 'fassungen', 'texte', 'textHash', 'ip', 'userAgent', 'konto', +] +const CONSENT_INDEX_KEYS = ['idx_session', 'idx_project_feature'] + +// Ohne Angaben ist featureConsents komplett vorhanden (Attribute und Indizes): die Faelle fuer portalFeatures bleiben so +// unberuehrt; die Faelle fuer featureConsents setzen consents/consentAttrs/indexes selbst. +function mockClient({ attrs = [], consents = true, consentAttrs = CONSENT_KEYS, indexes = CONSENT_INDEX_KEYS } = {}) { + const calls = { attrs: [], collections: [], indexes: [], waits: [], order: [] } + const consentsId = config.collections.featureConsents return { calls, - async collectionExists() { - return true + async collectionExists(id) { + return id === consentsId ? consents : true }, async ensureCollection(id, name) { calls.collections.push({ id, name }) + calls.order.push(`collection ${id}`) return true }, async listAttributes(id) { - assert.equal(id, config.collections.portalFeatures, 'liest nur die Funktions-Collection') + if (id === consentsId) { + if (!consents) throw Object.assign(new Error('Collection nicht gefunden'), { status: 404 }) + return consentAttrs.map((key) => ({ key })) + } + assert.equal(id, config.collections.portalFeatures, 'liest nur die beiden Funktions-Collections') return attrs.map((key) => ({ key })) }, async createAttribute(id, attr) { calls.attrs.push({ id, ...attr }) + calls.order.push(`attribute ${id}.${attr.key}`) }, async waitForAttribute(id, key) { calls.waits.push(`${id}.${key}`) + calls.order.push(`wait ${id}.${key}`) }, - async listIndexes() { - return [] + async listIndexes(id) { + assert.equal(id, consentsId, 'Indizes gibt es nur an featureConsents') + if (!consents) throw Object.assign(new Error('Collection nicht gefunden'), { status: 404 }) + return indexes.map((key) => ({ key })) }, async createIndex(id, index) { calls.indexes.push(`${id}.${index.key}`) + calls.order.push(`index ${id}.${index.key}`) }, } } const KEYS = ['disabledReason', 'aboEndeMailAt', 'zahlungMeldung'] -const KEINE = { attrs: [], collections: [], indexes: [], waits: [] } +const KEINE = { attrs: [], collections: [], indexes: [], waits: [], order: [] } test('Schema: drei Attribute mit Typ und Groesse, alle optional', () => { assert.deepEqual(PORTAL_FEATURE_ATTRIBUTES.map((a) => a.key), KEYS) @@ -78,6 +104,8 @@ test('main legt nur Fehlendes an und wartet auf die neuen Attribute', async () = assert.deepEqual(client.calls.waits, ['portalFeatures.aboEndeMailAt', 'portalFeatures.zahlungMeldung']) assert.deepEqual([client.calls.collections, client.calls.indexes], [[], []]) assert.deepEqual(result.missing.map((a) => a.key), ['aboEndeMailAt', 'zahlungMeldung']) + assert.equal(result.missingCollection, false) + assert.deepEqual(result.missingIndexes, []) assert.ok(log.some((zeile) => zeile.includes('+ Attribut portalFeatures.aboEndeMailAt'))) assert.ok(log.some((zeile) => zeile.includes('= portalFeatures.disabledReason (existiert)'))) }) @@ -128,3 +156,157 @@ test('schemaKeys(portalFeatures) enthaelt die drei Schluessel; der Analyse-Aufba assert.equal(neu.disabledReason, 'team') await assert.rejects(() => updateDocument(config.collections.portalFeatures, doc.$id, { gibtEsNicht: 1 }), /Unknown attribute/) }) + +test('Schema featureConsents: 11 Schluessel mit Typ und Groesse, alle optional', () => { + assert.equal(CONSENT_COLLECTION_NAME, 'featureConsents') + assert.equal(config.collections.featureConsents, 'featureConsents') + assert.deepEqual(FEATURE_CONSENT_ATTRIBUTES.map((a) => a.key), CONSENT_KEYS) + const nach = Object.fromEntries(FEATURE_CONSENT_ATTRIBUTES.map((a) => [a.key, a])) + const groessen = Object.fromEntries(FEATURE_CONSENT_ATTRIBUTES.filter((a) => a.type === 'string').map((a) => [a.key, a.size])) + assert.deepEqual(groessen, { + customerId: 36, projectId: 36, featureKey: 16, stripeSessionId: 128, fassungen: 1000, texte: 20000, + textHash: 64, ip: 64, userAgent: 300, konto: 255, + }) + assert.ok(nach.texte.size >= 20000, 'die gezeigten Texte (JSON) passen hinein') + assert.ok(nach.stripeSessionId.size >= 128) + assert.equal(nach.acceptedAt.type, 'datetime') + assert.equal('size' in nach.acceptedAt, false, 'ein datetime-Attribut hat keine Groesse') + assert.ok(FEATURE_CONSENT_ATTRIBUTES.every((a) => a.required === false), 'alle optional') + assert.equal(new Set(CONSENT_KEYS).size, CONSENT_KEYS.length) + // Ein SHA-256 in hex (64 Zeichen) und eine UUID (36) passen + assert.ok(nach.textHash.size >= 64 && nach.customerId.size >= 36 && nach.projectId.size >= 36) +}) + +test('Schema featureConsents: beide Indizes mit den Attributen aus der Vorgabe', () => { + assert.deepEqual(FEATURE_CONSENT_INDEXES, [ + { key: 'idx_session', type: 'key', attributes: ['stripeSessionId'] }, + { key: 'idx_project_feature', type: 'key', attributes: ['projectId', 'featureKey', 'acceptedAt'] }, + ]) + const keys = new Set(CONSENT_KEYS) + for (const index of FEATURE_CONSENT_INDEXES) for (const attr of index.attributes) assert.ok(keys.has(attr), `${index.key}: ${attr}`) +}) + +test('schemaKeys(featureConsents) liefert dieselben Schluessel, der Nachbau nimmt sie an und lehnt andere ab', async () => { + assert.deepEqual(schemaKeys('featureConsents'), CONSENT_KEYS) + assert.deepEqual(schemaKeys('featureConsents'), FEATURE_CONSENT_ATTRIBUTES.map((a) => a.key)) + schema.featureConsents = schemaKeys('featureConsents') + const { createDocument } = await import('../server/services/appwriteAdmin.js') + const doc = await createDocument(config.collections.featureConsents, { + customerId: 'c1', projectId: 'p1', featureKey: 'termine', stripeSessionId: 'cs_1', acceptedAt: '2026-10-04T10:00:00.000+00:00', + fassungen: '{}', texte: '[]', textHash: 'x', ip: '127.0.0.1', userAgent: 'test', konto: 'k@example.com', + }, 'fc_schema') + assert.equal(doc.stripeSessionId, 'cs_1') + await assert.rejects(() => createDocument(config.collections.featureConsents, { gibtEsNicht: 1 }, 'fc_schema2'), /Unknown attribute/) +}) + +test('main legt bei fehlender Collection genau featureConsents mit 11 Attributen und 2 Indizes an', async () => { + const client = mockClient({ attrs: KEYS, consents: false, consentAttrs: [], indexes: [] }) + const log = [] + const result = await main({ client, log: (zeile) => log.push(zeile) }) + assert.deepEqual(client.calls.collections, [{ id: 'featureConsents', name: 'featureConsents' }]) + assert.deepEqual(client.calls.attrs.map((a) => a.id), Array(11).fill('featureConsents'), 'nichts an portalFeatures, da dort alles vorhanden ist') + assert.deepEqual(client.calls.attrs.map((a) => a.key), CONSENT_KEYS) + assert.deepEqual(client.calls.attrs.map((a) => a.type), ['string', 'string', 'string', 'string', 'datetime', 'string', 'string', 'string', 'string', 'string', 'string']) + assert.equal(client.calls.attrs.find((a) => a.key === 'texte').size, 20000) + assert.ok(client.calls.attrs.every((a) => a.required === false)) + assert.deepEqual(client.calls.waits, CONSENT_KEYS.map((key) => `featureConsents.${key}`)) + assert.deepEqual(client.calls.indexes, ['featureConsents.idx_session', 'featureConsents.idx_project_feature']) + // Reihenfolge: Collection, alle Attribute, Warten auf alle Attribute, erst dann die Indizes + const order = client.calls.order + assert.equal(order[0], 'collection featureConsents') + assert.equal(order.findIndex((e) => e.startsWith('index ')), order.length - 2, 'die Indizes kommen zuletzt') + assert.ok(order.lastIndexOf('wait featureConsents.konto') < order.indexOf('index featureConsents.idx_session')) + assert.ok(order.indexOf('attribute featureConsents.konto') < order.indexOf('wait featureConsents.customerId')) + assert.equal(result.missingCollection, true) + assert.equal(result.missing.length, 11) + assert.ok(result.missing.every((a) => a.collection === 'featureConsents')) + assert.deepEqual(result.missingIndexes.map((i) => i.key), ['idx_session', 'idx_project_feature']) + assert.ok(log.includes('+ Collection featureConsents')) + assert.ok(log.includes('+ Index featureConsents.idx_project_feature')) +}) + +test('main legt bei fehlender Collection und fehlenden portalFeatures-Attributen nur das Fehlende an', async () => { + const client = mockClient({ attrs: ['disabledReason'], consents: false, consentAttrs: [], indexes: [] }) + const result = await main({ client, log: () => {} }) + assert.deepEqual(client.calls.collections.map((c) => c.id), ['featureConsents']) + assert.deepEqual(client.calls.attrs.filter((a) => a.id === 'portalFeatures').map((a) => a.key), ['aboEndeMailAt', 'zahlungMeldung']) + assert.equal(client.calls.attrs.filter((a) => a.id === 'featureConsents').length, 11) + assert.equal(result.missing.length, 13) +}) + +test('main ergaenzt an vorhandener Collection nur fehlende Attribute und Indizes', async () => { + const client = mockClient({ + attrs: KEYS, + consentAttrs: CONSENT_KEYS.filter((key) => key !== 'konto' && key !== 'textHash'), + indexes: ['idx_session'], + }) + const result = await main({ client, log: () => {} }) + assert.deepEqual(client.calls.collections, [], 'die Collection wird nicht noch einmal angelegt') + assert.deepEqual(client.calls.attrs.map((a) => `${a.id}.${a.key}`), ['featureConsents.textHash', 'featureConsents.konto']) + assert.deepEqual(client.calls.waits, ['featureConsents.textHash', 'featureConsents.konto']) + assert.deepEqual(client.calls.indexes, ['featureConsents.idx_project_feature']) + assert.equal(result.missingCollection, false) + assert.deepEqual(result.missing.map((a) => a.key), ['textHash', 'konto']) + assert.deepEqual(result.missingIndexes.map((i) => i.key), ['idx_project_feature']) +}) + +test('main ueberspringt Vorhandenes: keine Anlege-Aufrufe, Ergebnis ohne Luecken', async () => { + const client = mockClient({ attrs: KEYS }) + const log = [] + const result = await main({ client, log: (zeile) => log.push(zeile) }) + assert.deepEqual(client.calls, KEINE) + assert.equal(result.missing.length, 0) + assert.equal(result.missingCollection, false) + assert.deepEqual(result.missingIndexes, []) + assert.equal(hatLuecken(result), false) + assert.ok(log.includes('= featureConsents.texte (existiert)')) + assert.ok(!log.some((zeile) => zeile.includes('FEHLT') || zeile.startsWith('+'))) +}) + +test('main --check schreibt nie, meldet featureConsents FEHLT und liefert die Luecken im Ergebnis', async () => { + const client = mockClient({ attrs: KEYS, consents: false, consentAttrs: [], indexes: [] }) + const log = [] + const result = await main({ check: true, client, log: (zeile) => log.push(zeile) }) + assert.deepEqual(client.calls, KEINE, 'weder ensureCollection noch createAttribute noch createIndex noch waitForAttribute') + assert.ok(log.includes('- featureConsents FEHLT')) + for (const key of CONSENT_KEYS) assert.ok(log.includes(`- featureConsents.${key} FEHLT`), key) + assert.ok(log.includes('- featureConsents.idx_session FEHLT')) + assert.ok(log.includes('- featureConsents.idx_project_feature FEHLT')) + assert.ok(!log.some((zeile) => zeile.includes('portalFeatures') && zeile.includes('FEHLT'))) + assert.ok(!log.some((zeile) => zeile.startsWith('+'))) + assert.equal(result.missingCollection, true) + assert.deepEqual(result.missing.map((a) => a.key), CONSENT_KEYS) + assert.equal(result.missingIndexes.length, 2) + assert.equal(hatLuecken(result), true) + assert.equal(log.at(-1), '14 Eintrag/Eintraege fehlen.') +}) + +test('main --check meldet je fehlendes Attribut und je fehlenden Index einer vorhandenen Collection', async () => { + const client = mockClient({ attrs: KEYS, consentAttrs: CONSENT_KEYS.filter((key) => key !== 'texte'), indexes: ['idx_project_feature'] }) + const log = [] + const result = await main({ check: true, client, log: (zeile) => log.push(zeile) }) + assert.deepEqual(client.calls, KEINE) + assert.ok(log.includes('- featureConsents.texte FEHLT')) + assert.ok(log.includes('- featureConsents.idx_session FEHLT')) + assert.ok(!log.includes('- featureConsents FEHLT'), 'die Collection selbst ist da') + assert.ok(!log.includes('- featureConsents.idx_project_feature FEHLT')) + assert.equal(result.missingCollection, false) + assert.equal(hatLuecken(result), true) +}) + +test('main --check ohne Luecken: Alles vorhanden, Exit-Code 0', async () => { + const client = mockClient({ attrs: KEYS }) + const log = [] + const result = await main({ check: true, client, log: (zeile) => log.push(zeile) }) + assert.deepEqual(client.calls, KEINE) + assert.equal(log.at(-1), 'Alles vorhanden.') + assert.equal(hatLuecken(result), false) +}) + +test('hatLuecken zaehlt Attribute, Collection und Indizes', () => { + assert.equal(hatLuecken({ missing: [], missingCollection: false, missingIndexes: [] }), false) + assert.equal(hatLuecken({ missing: [{ key: 'a' }], missingCollection: false, missingIndexes: [] }), true) + assert.equal(hatLuecken({ missing: [], missingCollection: true, missingIndexes: [] }), true) + assert.equal(hatLuecken({ missing: [], missingCollection: false, missingIndexes: [{ key: 'i' }] }), true) + assert.equal(hatLuecken({ missing: [] }), false) +}) diff --git a/test/support/schema.js b/test/support/schema.js index 981137c..89bdbda 100644 --- a/test/support/schema.js +++ b/test/support/schema.js @@ -1,6 +1,6 @@ import { readFileSync } from 'node:fs' import { TERMINE_COLLECTIONS } from '../../server/services/termine/schema.js' -import { PORTAL_FEATURE_ATTRIBUTES } from '../../scripts/feature-setup.mjs' +import { FEATURE_CONSENT_ATTRIBUTES, PORTAL_FEATURE_ATTRIBUTES } from '../../scripts/feature-setup.mjs' // Setup-Skripte, die Collections in Appwrite anlegen (Blockformat: 'id', ... \n ],) const SETUP_SCRIPTS = ['setup-abo-collections.mjs', 'setup-finanzen.mjs'] @@ -10,10 +10,12 @@ const SETUP_SCRIPTS = ['setup-abo-collections.mjs', 'setup-finanzen.mjs'] * scripts/setup-finanzen.mjs in Appwrite anlegen. Tests geben sie dem Appwrite-Nachbau als Schema: * Schreibt der Code ein Feld, das das Setup-Skript nicht anlegt, scheitert der Test wie Appwrite in * Produktion. Die Termine-Collections kommen aus server/services/termine/schema.js (scripts/termine-setup.mjs); - * portalFeatures bekommt zusaetzlich die Attribute aus scripts/feature-setup.mjs (PORTAL_FEATURE_ATTRIBUTES). + * portalFeatures bekommt zusaetzlich die Attribute aus scripts/feature-setup.mjs (PORTAL_FEATURE_ATTRIBUTES); + * featureConsents (Zustimmungsnachweis) steht ganz dort (FEATURE_CONSENT_ATTRIBUTES). */ export function schemaKeys(collection) { if (Object.hasOwn(TERMINE_COLLECTIONS, collection)) return TERMINE_COLLECTIONS[collection].attributes.map((a) => a.key) + if (collection === 'featureConsents') return FEATURE_CONSENT_ATTRIBUTES.map((a) => a.key) for (const script of SETUP_SCRIPTS) { const src = readFileSync(new URL(`../../scripts/${script}`, import.meta.url), 'utf8') const start = src.indexOf(`'${collection}',`)