ki integration
This commit is contained in:
@@ -40,7 +40,7 @@ function resolveApiKey() {
|
||||
}
|
||||
const API_KEY = resolveApiKey();
|
||||
|
||||
let created = 0, skipped = 0, warned = 0;
|
||||
let created = 0, skipped = 0, warned = 0, updated = 0;
|
||||
|
||||
async function api(method, path, body) {
|
||||
const res = await fetch(`${ENDPOINT}${path}`, {
|
||||
@@ -81,13 +81,63 @@ const bool = (key, opts = {}) => ({ type: 'boolean', body: { key, required: fals
|
||||
const dt = (key, opts = {}) => ({ type: 'datetime', body: { key, required: false, ...opts } });
|
||||
const enm = (key, elements, opts = {}) => ({ type: 'enum', body: { key, elements, required: false, ...opts } });
|
||||
|
||||
const P_KEYS = Array.from({ length: 18 }, (_, i) => `P${i + 1}`);
|
||||
const P_KEYS = Array.from({ length: 22 }, (_, i) => `P${i + 1}`);
|
||||
|
||||
// Feste Branchenliste (Onboarding-Pflichtfrage 3, Feed-Filter, Kopier-Kompatibilität)
|
||||
// und Motiv-Tags des Bild-Modus. Beides sind Enums – eine spätere Änderung ist eine
|
||||
// Migration mit Datenverlustrisiko, nicht ein Anhängen (programmier-plan.md §2.6).
|
||||
const NISCHEN = [
|
||||
'mode', 'beauty_kosmetik', 'fitness_sport', 'food_getraenke', 'gastronomie',
|
||||
'gesundheit', 'handwerk_bau', 'immobilien', 'finanzen_versicherung', 'reisen_hotel',
|
||||
'auto_mobilitaet', 'technik_software', 'moebel_interior', 'schmuck_accessoires',
|
||||
'haustier', 'bildung_coaching', 'dienstleistung', 'handel_ecommerce', 'kunst_kultur',
|
||||
'sonstiges',
|
||||
];
|
||||
const TYP_TAGS = [
|
||||
'portrait', 'ganzkoerper', 'gruppe', 'produkt_freisteller', 'produkt_inszeniert',
|
||||
'detail_makro', 'flatlay', 'innenraum', 'aussen', 'studio', 'natur', 'urban',
|
||||
'bewegung', 'stillleben', 'text_overlay', 'logo', 'sonstiges',
|
||||
];
|
||||
|
||||
const INTERNAL = 'team:internal';
|
||||
const internalOnly = [
|
||||
`read("${INTERNAL}")`, `create("${INTERNAL}")`,
|
||||
`update("${INTERNAL}")`, `delete("${INTERNAL}")`,
|
||||
];
|
||||
|
||||
/**
|
||||
* Tabellen-Recht "anlegen darf jeder Angemeldete".
|
||||
*
|
||||
* Warum das nötig ist: bei `rowSecurity: true` regeln Zeilenrechte *lesen,
|
||||
* ändern, löschen* – aber eine Zeile, die es noch nicht gibt, hat keine Rechte.
|
||||
* Ohne ein Tabellen-Recht zum Anlegen kann der Client also gar nichts
|
||||
* schreiben, auch nicht seine eigenen Daten.
|
||||
*
|
||||
* Warum das trotzdem dicht ist: `create` erlaubt nur das Anlegen. Gelesen wird
|
||||
* ausschließlich, was die Zeilenrechte hergeben – und die setzt der Client beim
|
||||
* Anlegen auf sein eigenes Team. Eine fremde Zeile wird dadurch nicht sichtbar.
|
||||
*/
|
||||
const userCreate = ['create("users")'];
|
||||
|
||||
/**
|
||||
* Wer legt in welcher Tabelle Zeilen an.
|
||||
*
|
||||
* NICHT in dieser Liste und das mit Absicht:
|
||||
* - `score_events`, `video_metrics`, `post_metrics`, `usage_records`
|
||||
* – append-only, geschrieben von Functions mit Server-Key. Dürfte der Client
|
||||
* hier schreiben, könnte er seine eigenen Elo-Werte und Feed-Signale
|
||||
* fälschen (projekt-uebersicht.md §11, "Manipulation des Feed-Scores").
|
||||
* - `rules`, `prompt_templates` – Betriebsgeheimnis, nur Team `internal`.
|
||||
*/
|
||||
const CLIENT_CREATE = new Set([
|
||||
'brands', 'folders', 'categories', 'attributes', 'attribute_scores',
|
||||
'assets', 'asset_versions', 'scenes', 'videos', 'votes', 'questions',
|
||||
'posts', 'post_images', 'post_folders', 'follows',
|
||||
'jobs', // jede KI-Aktion wird vom Client eingereiht (app-aufbau.md §2.1)
|
||||
]);
|
||||
|
||||
const tablePermissions = (t) => t.permissions || (CLIENT_CREATE.has(t.id) ? userCreate : []);
|
||||
|
||||
// ---- Tabellen-Definitionen (Spalten + Indizes) ----------------------------
|
||||
const TABLES = [
|
||||
{
|
||||
@@ -101,8 +151,18 @@ const TABLES = [
|
||||
str('stripe_customer_id', 64),
|
||||
int('default_video_count', { min: 3, max: 6, default: 4 }),
|
||||
enm('status', ['trial', 'aktiv', 'pausiert'], { default: 'trial' }),
|
||||
// Feed & Folgen: öffentliches Profil der Brand
|
||||
str('anzeigename', 255),
|
||||
enm('nische', NISCHEN), // Onboarding-Pflichtfrage 3, filtert den Feed
|
||||
id('avatar_file_id'),
|
||||
int('follower_count', { default: 0 }),
|
||||
bool('ist_oeffentlich', { default: false }),
|
||||
enm('plan', ['trial', 'starter', 'pro', 'business'], { default: 'trial' }),
|
||||
],
|
||||
indexes: [
|
||||
{ key: 'idx_team', type: 'key', columns: ['team_id'] },
|
||||
{ key: 'idx_oeffentlich', type: 'key', columns: ['ist_oeffentlich', 'nische'] },
|
||||
],
|
||||
indexes: [{ key: 'idx_team', type: 'key', columns: ['team_id'] }],
|
||||
},
|
||||
{
|
||||
// 🔒 Dev-Modus: nur internes Team
|
||||
@@ -139,8 +199,12 @@ const TABLES = [
|
||||
str('name', 64, { required: true }), // location, licht, farben, kamera, voice, geraeusche, texte-hooks, handlungen
|
||||
int('avg_score', { min: 0, max: 10000, default: 5000 }), // Cache – per Function aktualisiert
|
||||
int('attribute_count', { default: 0 }),
|
||||
id('folder_id'), // null = brand-weite Ebene; Kategorie-Ø wird je Ordner geführt
|
||||
],
|
||||
indexes: [
|
||||
{ key: 'idx_brand_name', type: 'key', columns: ['brand_id', 'name'] },
|
||||
{ key: 'idx_folder', type: 'key', columns: ['folder_id'] },
|
||||
],
|
||||
indexes: [{ key: 'idx_brand_name', type: 'key', columns: ['brand_id', 'name'] }],
|
||||
},
|
||||
{
|
||||
id: 'attributes', name: 'Attributes', rowSecurity: true,
|
||||
@@ -151,22 +215,21 @@ const TABLES = [
|
||||
str('name', 255, { required: true }),
|
||||
str('slug', 255, { required: true }),
|
||||
enm('status', ['aktiv', 'archiviert'], { default: 'aktiv' }), // nie löschen
|
||||
int('score', { min: 0, max: 10000, default: 5000 }),
|
||||
int('k_factor', { default: 32 }), // 32 neu → 8 etabliert
|
||||
int('start_value', { min: 0, max: 10000 }), // Kategorie-Ø bei Anlage
|
||||
int('used_count', { default: 0 }), int('wins', { default: 0 }), int('losses', { default: 0 }),
|
||||
dt('last_used_at'),
|
||||
// Score, k_factor, start_value, used_count, wins, losses, last_used_at liegen
|
||||
// NICHT mehr hier, sondern in `attribute_scores` – ein Attribut hat je Ordner
|
||||
// einen eigenen Score. Diese Tabelle hält nur noch die Definition.
|
||||
str('tags', 255, { array: true }),
|
||||
md('beschreibung_md'), md('essenz_md'), md('details_md'),
|
||||
str('prompt_bausteine', 1024, { array: true }),
|
||||
str('negativ_prompts', 1024, { array: true }),
|
||||
],
|
||||
indexes: [
|
||||
{ key: 'idx_topn', type: 'key', columns: ['brand_id', 'category_id', 'status', 'score'], orders: ['ASC', 'ASC', 'ASC', 'DESC'] },
|
||||
{ key: 'idx_parent', type: 'key', columns: ['parent_id'] },
|
||||
{ key: 'idx_slug', type: 'key', columns: ['brand_id', 'slug'] },
|
||||
// Fulltext ist auf Array-Spalten verboten → Key-Index (reicht für Query.contains)
|
||||
{ key: 'idx_tags', type: 'key', columns: ['tags'] },
|
||||
// Der Top-N-Index liegt jetzt auf attribute_scores – dort steht der Score.
|
||||
{ key: 'idx_brand_cat', type: 'key', columns: ['brand_id', 'category_id', 'status'] },
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -175,22 +238,40 @@ const TABLES = [
|
||||
columns: [
|
||||
id('attribute_id', { required: true }),
|
||||
id('video_id'), id('opponent_attribute_id'),
|
||||
enm('event_type', ['vote', 'vote_favorit_bestaetigt', 'performance', 'gezielte_frage', 'llm_vergleich'], { required: true }),
|
||||
enm('event_type', [
|
||||
'vote', 'vote_favorit_bestaetigt', 'performance', 'gezielte_frage', 'llm_vergleich',
|
||||
'feed_import', 'post_reichweite', // Bild-Modus: Import beim Kopieren, Rückfluss ×0,3
|
||||
], { required: true }),
|
||||
int('delta'), int('new_score', { min: 0, max: 10000 }),
|
||||
flt('weight', { default: 1 }), // ×0,3 / ×1 / ×2
|
||||
str('kommentar', 1024), // z. B. P12-Erkenntnis
|
||||
id('folder_id'), // in welchem Wissens-Scope die Änderung galt
|
||||
id('post_id'), // bei feed_import / post_reichweite
|
||||
],
|
||||
indexes: [
|
||||
{ key: 'idx_attr_created', type: 'key', columns: ['attribute_id', '$createdAt'] },
|
||||
{ key: 'idx_folder_created', type: 'key', columns: ['folder_id', '$createdAt'] },
|
||||
],
|
||||
indexes: [{ key: 'idx_attr_created', type: 'key', columns: ['attribute_id', '$createdAt'] }],
|
||||
},
|
||||
{
|
||||
id: 'assets', name: 'Assets', rowSecurity: true,
|
||||
columns: [
|
||||
id('brand_id', { required: true }),
|
||||
enm('typ', ['gesicht', 'produkt', 'logo', 'sonstiges'], { required: true }),
|
||||
enm('typ', ['gesicht', 'produkt', 'logo', 'sonstiges', 'kulisse'], { required: true }),
|
||||
str('name', 255, { required: true }),
|
||||
id('released_version_id'), // nur diese Version wird in Videos verwendet
|
||||
// Feed: Modelle werden nie geteilt (Marken-/Designrecht, Konsistenz), erscheinen
|
||||
// aber mit eigenem Feed-Score in der Modell-Rangliste.
|
||||
enm('nische', NISCHEN),
|
||||
enm('typ_tags', TYP_TAGS, { array: true }),
|
||||
flt('feed_score', { default: 0 }),
|
||||
dt('feed_score_updated_at'),
|
||||
bool('ist_teilbar', { default: false }),
|
||||
],
|
||||
indexes: [
|
||||
{ key: 'idx_brand', type: 'key', columns: ['brand_id'] },
|
||||
{ key: 'idx_teilbar', type: 'key', columns: ['ist_teilbar', 'nische'] },
|
||||
],
|
||||
indexes: [{ key: 'idx_brand', type: 'key', columns: ['brand_id'] }],
|
||||
},
|
||||
{
|
||||
id: 'asset_versions', name: 'Asset Versions', rowSecurity: true,
|
||||
@@ -216,6 +297,7 @@ const TABLES = [
|
||||
md('script_md'), md('script_final_md'), // Diff = P6-Signal
|
||||
enm('status', ['entwurf', 'script', 'generiert', 'voting', 'fertig'], { default: 'entwurf' }), // Realtime-Kanal fürs UI
|
||||
int('video_count', { min: 1, default: 4 }),
|
||||
id('folder_id'), // Ordner wird VOR der Generierung gewählt; P4 zieht nur dessen Attribute
|
||||
],
|
||||
indexes: [{ key: 'idx_brand_status', type: 'key', columns: ['brand_id', 'status'] }],
|
||||
},
|
||||
@@ -277,7 +359,10 @@ const TABLES = [
|
||||
id: 'jobs', name: 'Jobs', rowSecurity: true,
|
||||
columns: [
|
||||
id('brand_id', { required: true }),
|
||||
enm('typ', ['bild_gen', 'video_gen', 'tagging', 'vergleich', 'script'], { required: true }),
|
||||
enm('typ', [
|
||||
'bild_gen', 'video_gen', 'tagging', 'vergleich', 'script',
|
||||
'slot_analyse', 'ordner_vorschlag', 'werbetext', // P19, P20, P21
|
||||
], { required: true }),
|
||||
str('prompt_template_key', 16), // welcher Prompt …
|
||||
int('prompt_template_version'), // … in welcher Version lief
|
||||
enm('status', ['wartend', 'laeuft', 'fertig', 'fehler'], { default: 'wartend' }),
|
||||
@@ -301,13 +386,158 @@ const TABLES = [
|
||||
],
|
||||
indexes: [{ key: 'uq_brand_periode', type: 'unique', columns: ['brand_id', 'periode'] }],
|
||||
},
|
||||
|
||||
// ---- Wissens-Scope: Ordner + Scores je Ordner ---------------------------
|
||||
{
|
||||
// Ein Ordner ist ein privater Wissens-Scope, kein Sortier-Ordner.
|
||||
id: 'folders', name: 'Folders', rowSecurity: true,
|
||||
columns: [
|
||||
id('brand_id', { required: true }),
|
||||
str('name', 255, { required: true }),
|
||||
md('theme_md'),
|
||||
str('zweck', 1024),
|
||||
enm('startwert_modus', ['erben', 'aus_posts', 'neutral'], { default: 'erben' }),
|
||||
bool('ist_default', { default: false }),
|
||||
int('post_count', { default: 0 }),
|
||||
int('signal_count', { default: 0 }), // Reifegrad des Ordners
|
||||
],
|
||||
indexes: [
|
||||
{ key: 'idx_brand', type: 'key', columns: ['brand_id'] },
|
||||
{ key: 'idx_brand_default', type: 'key', columns: ['brand_id', 'ist_default'] },
|
||||
],
|
||||
},
|
||||
{
|
||||
// Herzstück: ein Attribut hat je Ordner einen eigenen Score.
|
||||
// folder_id = null ist die brand-weite Ebene.
|
||||
id: 'attribute_scores', name: 'Attribute Scores', rowSecurity: true,
|
||||
columns: [
|
||||
id('brand_id', { required: true }),
|
||||
id('attribute_id', { required: true }),
|
||||
id('folder_id'), // null = brand-weit
|
||||
id('category_id', { required: true }),
|
||||
int('score', { min: 0, max: 10000, default: 5000 }),
|
||||
int('k_factor', { default: 32 }), // 32 neu → 8 etabliert
|
||||
int('start_value', { min: 0, max: 10000 }), // Kategorie-Ø bei Anlage
|
||||
enm('start_quelle', ['kategorie_schnitt', 'geerbt', 'aus_posts', 'feed_import', 'neutral'], { default: 'neutral' }),
|
||||
int('used_count', { default: 0 }), int('wins', { default: 0 }), int('losses', { default: 0 }),
|
||||
dt('last_used_at'),
|
||||
],
|
||||
indexes: [
|
||||
// der Top-N-Index, wegen dem Referenzen indizierte Strings sind (siehe Kopf)
|
||||
{ key: 'idx_topn', type: 'key', columns: ['brand_id', 'folder_id', 'category_id', 'score'], orders: ['ASC', 'ASC', 'ASC', 'DESC'] },
|
||||
{ key: 'uq_attr_folder', type: 'unique', columns: ['attribute_id', 'folder_id'] },
|
||||
{ key: 'idx_folder', type: 'key', columns: ['folder_id'] },
|
||||
],
|
||||
},
|
||||
|
||||
// ---- Bild-Modus & Feed --------------------------------------------------
|
||||
{
|
||||
// Ein Post ist ein Slot-Rezept, kein Freitext-Prompt – nur deshalb kopierbar.
|
||||
// prompt_sent ist das Betriebsgeheimnis und darf nie an fremde Konten gehen.
|
||||
id: 'posts', name: 'Posts', rowSecurity: true,
|
||||
columns: [
|
||||
id('brand_id', { required: true }),
|
||||
id('folder_id'),
|
||||
str('titel', 255),
|
||||
md('user_prompt'),
|
||||
jsonCol('slots', 65535), // das Rezept: Kulisse, Modelle, Licht/Kamera/Farbe, Werbetext
|
||||
jsonCol('slot_summary', 65535), // die abstrahierten Chips (P19) – das sieht der Kopierer
|
||||
md('prompt_sent'), // 🔒 nur eigenes Konto + Team internal
|
||||
str('format', 32), // 1:1 | 4:5 | 9:16
|
||||
int('bild_count', { default: 1 }),
|
||||
enm('status', ['entwurf', 'generiert', 'fehler'], { default: 'entwurf' }),
|
||||
enm('sichtbarkeit', ['privat', 'oeffentlich'], { default: 'privat' }), // Veröffentlichen ist ein aktiver Schritt
|
||||
flt('feed_score', { default: 0 }), // gewichtete Summe + Decay, NICHT Elo
|
||||
dt('feed_score_updated_at'),
|
||||
dt('veroeffentlicht_at'),
|
||||
id('copied_from_post_id'),
|
||||
int('kopien_count', { default: 0 }), // Kopien ×1 im Feed-Score
|
||||
enm('nische', NISCHEN),
|
||||
],
|
||||
indexes: [
|
||||
{ key: 'idx_brand_status', type: 'key', columns: ['brand_id', 'status'] },
|
||||
{ key: 'idx_feed', type: 'key', columns: ['sichtbarkeit', 'feed_score'], orders: ['ASC', 'DESC'] },
|
||||
// Explorations-Slot: neue Posts chronologisch, gegen Rich-get-richer
|
||||
{ key: 'idx_feed_neu', type: 'key', columns: ['sichtbarkeit', 'veroeffentlicht_at'], orders: ['ASC', 'DESC'] },
|
||||
{ key: 'idx_copied', type: 'key', columns: ['copied_from_post_id'] },
|
||||
],
|
||||
},
|
||||
{
|
||||
// Bilderkette: nur Position und Winkel variieren. Das Text-Overlay ist ein
|
||||
// eigenes Bild, keine Ebene – so lässt sich Text ohne Neugenerierung tauschen.
|
||||
id: 'post_images', name: 'Post Images', rowSecurity: true,
|
||||
columns: [
|
||||
id('post_id', { required: true }),
|
||||
id('brand_id', { required: true }),
|
||||
int('position', { min: 1, default: 1 }),
|
||||
enm('typ', ['motiv', 'text_overlay'], { default: 'motiv' }),
|
||||
id('storage_file_id'), // → Bucket generated-images
|
||||
md('prompt_sent'), // 🔒
|
||||
id('asset_version_ids', { array: true }),
|
||||
enm('typ_tags', TYP_TAGS, { array: true }), // P22-Tagger, Enum-Zwang
|
||||
int('breite'), int('hoehe'),
|
||||
],
|
||||
indexes: [{ key: 'idx_post_position', type: 'key', columns: ['post_id', 'position'] }],
|
||||
},
|
||||
{
|
||||
// m:n – ein Post darf in mehreren Ordnern liegen, eigene wie fremde
|
||||
id: 'post_folders', name: 'Post ↔ Folder', rowSecurity: true,
|
||||
columns: [
|
||||
id('post_id', { required: true }),
|
||||
id('folder_id', { required: true }),
|
||||
id('brand_id', { required: true }),
|
||||
bool('ist_fremd', { default: false }),
|
||||
enm('quelle', ['eigen', 'feed_import', 'kopie'], { default: 'eigen' }),
|
||||
],
|
||||
indexes: [
|
||||
{ key: 'uq_post_folder', type: 'unique', columns: ['post_id', 'folder_id'] },
|
||||
{ key: 'idx_folder', type: 'key', columns: ['folder_id'] },
|
||||
],
|
||||
},
|
||||
{
|
||||
// Feed-Signale – append-only. Views ×0,1 · Likes ×0,5 · Kopien ×1 · Social ×2
|
||||
id: 'post_metrics', name: 'Post Metrics', rowSecurity: true,
|
||||
columns: [
|
||||
id('post_id', { required: true }),
|
||||
dt('fetched_at'),
|
||||
int('views', { default: 0 }), int('likes', { default: 0 }), int('kopien', { default: 0 }),
|
||||
int('social_reichweite', { default: 0 }),
|
||||
str('social_plattform', 32),
|
||||
],
|
||||
indexes: [{ key: 'idx_post_fetched', type: 'key', columns: ['post_id', 'fetched_at'] }],
|
||||
},
|
||||
{
|
||||
id: 'follows', name: 'Follows', rowSecurity: true,
|
||||
columns: [
|
||||
id('follower_brand_id', { required: true }),
|
||||
id('followed_brand_id', { required: true }),
|
||||
],
|
||||
indexes: [
|
||||
{ key: 'uq_follow', type: 'unique', columns: ['follower_brand_id', 'followed_brand_id'] },
|
||||
{ key: 'idx_follower', type: 'key', columns: ['follower_brand_id'] },
|
||||
{ key: 'idx_followed', type: 'key', columns: ['followed_brand_id'] },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* Buckets. `fileSecurity` überall an – gelesen wird nur, was die Datei-Rechte
|
||||
* hergeben.
|
||||
*
|
||||
* `create("users")` aus demselben Grund wie bei den Tabellen: eine Datei, die
|
||||
* noch nicht existiert, hat keine Rechte. Ohne Bucket-Recht zum Anlegen kann
|
||||
* der Client nichts hochladen.
|
||||
*
|
||||
* **Ohne** `create`: `generated-videos` und `generated-images` – dort schreibt
|
||||
* ausschließlich der Job-Dispatcher mit Server-Key. Dürfte der Client das,
|
||||
* könnte er Bilder unterschieben, die nie durch eine Generierung gelaufen sind.
|
||||
*/
|
||||
const BUCKETS = [
|
||||
{ id: 'uploads', name: 'Uploads (Onboarding)' },
|
||||
{ id: 'asset-references', name: 'Asset-Referenzbilder' },
|
||||
{ id: 'uploads', name: 'Uploads (Onboarding)', permissions: userCreate },
|
||||
{ id: 'asset-references', name: 'Asset-Referenzbilder', permissions: userCreate },
|
||||
{ id: 'generated-videos', name: 'Generierte Videos' },
|
||||
{ id: 'consents', name: 'Einwilligungen', permissions: [`read("${INTERNAL}")`] },
|
||||
{ id: 'consents', name: 'Einwilligungen', permissions: [`read("${INTERNAL}")`, ...userCreate] },
|
||||
{ id: 'generated-images', name: 'Generierte Bilder' },
|
||||
];
|
||||
|
||||
// ---- Ablauf ---------------------------------------------------------------
|
||||
@@ -338,11 +568,27 @@ async function main() {
|
||||
|
||||
for (const t of TABLES) {
|
||||
console.log(`Tabelle ${t.id}:`);
|
||||
await ensure(t.id, 'POST', `/tablesdb/${DB_ID}/tables`, {
|
||||
const perms = tablePermissions(t);
|
||||
const fresh = await ensure(t.id, 'POST', `/tablesdb/${DB_ID}/tables`, {
|
||||
tableId: t.id, name: t.name,
|
||||
permissions: t.permissions || [],
|
||||
permissions: perms,
|
||||
rowSecurity: t.rowSecurity,
|
||||
});
|
||||
// Existierte die Tabelle schon, hat POST nur ein 409 geliefert – die Rechte
|
||||
// wären dann nie angefasst worden. Deshalb hier abgleichen und angleichen:
|
||||
// sonst driftet das Rechte-Modell genauso auseinander wie zuvor das Schema.
|
||||
if (!fresh) {
|
||||
const { status, json } = await api('GET', `/tablesdb/${DB_ID}/tables/${t.id}`);
|
||||
const same = status === 200
|
||||
&& JSON.stringify([...(json.$permissions ?? [])].sort()) === JSON.stringify([...perms].sort())
|
||||
&& !!json.rowSecurity === !!t.rowSecurity;
|
||||
if (!same) {
|
||||
await ensure(`${t.id} Rechte`, 'PUT', `/tablesdb/${DB_ID}/tables/${t.id}`, {
|
||||
name: t.name, permissions: perms, rowSecurity: t.rowSecurity,
|
||||
});
|
||||
updated++; created--; // ensure() zählt als "angelegt" – hier ist es eine Änderung
|
||||
}
|
||||
}
|
||||
for (const c of t.columns) {
|
||||
// Erst GET: Appwrite prüft das Zeilengrößen-Limit VOR dem Duplikat-Check,
|
||||
// ein erneutes POST auf große Spalten gäbe sonst 400 statt 409.
|
||||
@@ -367,15 +613,29 @@ async function main() {
|
||||
|
||||
console.log('Storage-Buckets:');
|
||||
for (const b of BUCKETS) {
|
||||
await ensure(b.id, 'POST', '/storage/buckets', {
|
||||
const perms = b.permissions || [];
|
||||
const fresh = await ensure(b.id, 'POST', '/storage/buckets', {
|
||||
bucketId: b.id, name: b.name,
|
||||
fileSecurity: true,
|
||||
permissions: b.permissions || [],
|
||||
permissions: perms,
|
||||
enabled: true,
|
||||
});
|
||||
// Gleiches Nachziehen wie bei den Tabellen – ein 409 lässt die Rechte sonst
|
||||
// auf dem Stand von damals stehen.
|
||||
if (!fresh) {
|
||||
const { status, json } = await api('GET', `/storage/buckets/${b.id}`);
|
||||
const same = status === 200
|
||||
&& JSON.stringify([...(json.$permissions ?? [])].sort()) === JSON.stringify([...perms].sort());
|
||||
if (!same) {
|
||||
await ensure(`${b.id} Rechte`, 'PUT', `/storage/buckets/${b.id}`, {
|
||||
name: b.name, fileSecurity: true, permissions: perms, enabled: true,
|
||||
});
|
||||
updated++; created--;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`\nFertig: ${created} angelegt, ${skipped} existierten schon, ${warned} Warnung(en).`);
|
||||
console.log(`\nFertig: ${created} angelegt, ${updated} geändert, ${skipped} existierten schon, ${warned} Warnung(en).`);
|
||||
}
|
||||
|
||||
main().catch(e => { console.error(`\nAbbruch: ${e.message}`); process.exit(1); });
|
||||
|
||||
Reference in New Issue
Block a user