Date : 25/12/2025 Projet : QwikPress Status : VALIDÉ ✅
QwikPress fonctionne actuellement avec un système de fichiers JSON :
data/config.json → Configuration globaledata/pages/*/meta.json → Métadonnées pagesdata/pages/*/content.json → Contenu des pages (modules)| Problème | Impact |
|---|---|
| Pas de cache | Relecture fichiers à chaque requête |
| Multi-sites impossible | 1 instance = 1 site |
| Pas de collaboration | Accès filesystem requis |
| Médias en local | Pas de CDN, difficile à scale |
| Backup manuel | Risque de perte |
Créer une architecture hybride permettant :
┌─────────────────────────────────────────────────────────────────┐
│ QwikPress │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ DATA SERVICE │ │
│ │ (Interface abstraite unique) │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │ │
│ ┌───────────────┴───────────────┐ │
│ │ │ │
│ ▼ ▼ │
│ ┌──────────────────┐ ┌──────────────────────────┐ │
│ │ FILE ADAPTER │ │ SUPABASE ADAPTER │ │
│ │ │ │ │ │
│ │ data/ │ │ ┌─────────────────────┐ │ │
│ │ ├─ config.json │ │ │ REDIS │ │ │
│ │ ├─ pages/ │ │ │ (Cache Layer) │ │ │
│ │ └─ medias/ │ │ │ - Config: 5min TTL │ │ │
│ │ │ │ │ - Pages: 1min TTL │ │ │
│ │ │ │ │ - Invalidation │ │ │
│ │ │ │ └─────────┬───────────┘ │ │
│ │ │ │ │ │ │
│ │ │ │ ▼ │ │
│ │ │ │ ┌─────────────────────┐ │ │
│ │ │ │ │ SUPABASE │ │ │
│ │ │ │ │ │ │ │
│ │ │ │ │ PostgreSQL: │ │ │
│ │ │ │ │ - sites │ │ │
│ │ │ │ │ - pages │ │ │
│ │ │ │ │ - modules │ │ │
│ │ │ │ │ - medias │ │ │
│ │ │ │ │ │ │ │
│ │ │ │ │ Storage (Bucket): │ │ │
│ │ │ │ │ - images │ │ │
│ │ │ │ │ - assets │ │ │
│ │ │ │ └─────────────────────┘ │ │
│ └──────────────────┘ └──────────────────────────┘ │
│ │
│ ENV: DATA_MODE=file ENV: DATA_MODE=supabase │
│ │
└─────────────────────────────────────────────────────────────────┘
supabase/Tout le nécessaire pour reconstruire l'environnement Supabase :
qwikpress/
└── supabase/
├── README.md # Guide de setup
├── config.toml # Config Supabase CLI (optionnel)
│
├── migrations/ # Migrations SQL versionnées
│ ├── 00001_init_schema.sql
│ ├── 00002_create_sites.sql
│ ├── 00003_create_pages.sql
│ ├── 00004_create_modules.sql
│ ├── 00005_create_medias.sql
│ ├── 00006_create_indexes.sql
│ ├── 00007_create_functions.sql
│ └── 00008_create_policies.sql
│
├── seeds/ # Données initiales
│ ├── 01_default_site.sql # Site par défaut
│ └── 02_demo_pages.sql # Pages démo (optionnel)
│
├── storage/ # Config buckets
│ └── buckets.sql # Création buckets + policies
│
├── types/ # Types TypeScript générés
│ └── database.types.ts # Auto-généré par Supabase CLI
│
└── scripts/ # Scripts utilitaires
├── setup.sh # Setup complet (migrations + seeds)
├── migrate.sh # Appliquer migrations
├── seed.sh # Injecter seeds
├── reset.sh # Reset complet (dev only)
├── generate-types.sh # Regénérer types TS
└── migrate-from-files.ts # Migration fichiers → Supabase
-- supabase/migrations/00001_init_schema.sql
-- Extension UUID
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
-- Schema dédié QwikPress
CREATE SCHEMA IF NOT EXISTS qwikpress;
-- Fonction updated_at automatique
CREATE OR REPLACE FUNCTION qwikpress.update_updated_at()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- supabase/migrations/00002_create_sites.sql
CREATE TABLE qwikpress.sites (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
-- Identification
slug VARCHAR(100) UNIQUE NOT NULL, -- "rhinov", "monsite"
name VARCHAR(200) NOT NULL,
domain VARCHAR(200), -- "monsite.com" (optionnel)
-- Configuration globale (reprend config.json)
config JSONB NOT NULL DEFAULT '{}'::jsonb,
/*
Structure config:
{
"site": { "name": "...", "logo": "...", "favicon": "..." },
"navigation": { "main": [...], "footer": [...] },
"theme": { "colors": {...}, "fonts": {...} },
"seo": { "defaultTitle": "...", "defaultDescription": "..." },
"footer": { "tagline": "...", "social": [...] }
}
*/
-- Status
is_active BOOLEAN DEFAULT true,
-- Timestamps
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
-- Index
CREATE INDEX idx_sites_slug ON qwikpress.sites(slug);
CREATE INDEX idx_sites_domain ON qwikpress.sites(domain);
CREATE INDEX idx_sites_active ON qwikpress.sites(is_active) WHERE is_active = true;
-- Trigger updated_at
CREATE TRIGGER sites_updated_at
BEFORE UPDATE ON qwikpress.sites
FOR EACH ROW EXECUTE FUNCTION qwikpress.update_updated_at();
-- supabase/migrations/00003_create_pages.sql
CREATE TABLE qwikpress.pages (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
site_id UUID NOT NULL REFERENCES qwikpress.sites(id) ON DELETE CASCADE,
-- Identification
slug VARCHAR(200) NOT NULL, -- "home", "about", "services/web"
-- Métadonnées (reprend meta.json)
meta JSONB NOT NULL DEFAULT '{}'::jsonb,
/*
Structure meta:
{
"title": "Page Title",
"description": "SEO description",
"template": "default",
"showInNav": true,
"navOrder": 1,
"ogImage": "/images/og.jpg"
}
*/
-- Contenu (reprend content.json)
content JSONB NOT NULL DEFAULT '{"modules": []}'::jsonb,
/*
Structure content:
{
"modules": [
{ "id": "uuid", "type": "hero", "data": {...} },
{ "id": "uuid", "type": "features", "data": {...} }
]
}
*/
-- Publication
is_published BOOLEAN DEFAULT false,
published_at TIMESTAMPTZ,
-- Timestamps
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
-- Contraintes
UNIQUE(site_id, slug)
);
-- Index
CREATE INDEX idx_pages_site ON qwikpress.pages(site_id);
CREATE INDEX idx_pages_slug ON qwikpress.pages(slug);
CREATE INDEX idx_pages_published ON qwikpress.pages(site_id, is_published)
WHERE is_published = true;
-- Index GIN pour recherche JSONB
CREATE INDEX idx_pages_meta ON qwikpress.pages USING GIN (meta);
CREATE INDEX idx_pages_content ON qwikpress.pages USING GIN (content);
-- Trigger updated_at
CREATE TRIGGER pages_updated_at
BEFORE UPDATE ON qwikpress.pages
FOR EACH ROW EXECUTE FUNCTION qwikpress.update_updated_at();
-- supabase/migrations/00004_create_modules.sql
CREATE TABLE qwikpress.shared_modules (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
site_id UUID NOT NULL REFERENCES qwikpress.sites(id) ON DELETE CASCADE,
-- Identification
name VARCHAR(100) NOT NULL, -- "header-principal", "footer-contact"
type VARCHAR(50) NOT NULL, -- "hero", "cta", "features"
-- Contenu
data JSONB NOT NULL DEFAULT '{}'::jsonb,
-- Timestamps
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
-- Contraintes
UNIQUE(site_id, name)
);
-- Index
CREATE INDEX idx_shared_modules_site ON qwikpress.shared_modules(site_id);
CREATE INDEX idx_shared_modules_type ON qwikpress.shared_modules(type);
-- Trigger updated_at
CREATE TRIGGER shared_modules_updated_at
BEFORE UPDATE ON qwikpress.shared_modules
FOR EACH ROW EXECUTE FUNCTION qwikpress.update_updated_at();
-- supabase/migrations/00005_create_medias.sql
CREATE TABLE qwikpress.medias (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
site_id UUID NOT NULL REFERENCES qwikpress.sites(id) ON DELETE CASCADE,
-- Fichier
filename VARCHAR(200) NOT NULL,
original_name VARCHAR(200),
storage_path TEXT NOT NULL, -- Chemin dans bucket Supabase
public_url TEXT, -- URL publique CDN
-- Métadonnées
mime_type VARCHAR(100),
size_bytes INTEGER,
width INTEGER, -- Pour images
height INTEGER, -- Pour images
alt_text VARCHAR(500),
-- Organisation
folder VARCHAR(200) DEFAULT '/', -- "/images", "/documents"
tags TEXT[],
-- Timestamps
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
-- Index
CREATE INDEX idx_medias_site ON qwikpress.medias(site_id);
CREATE INDEX idx_medias_folder ON qwikpress.medias(site_id, folder);
CREATE INDEX idx_medias_mime ON qwikpress.medias(mime_type);
CREATE INDEX idx_medias_tags ON qwikpress.medias USING GIN (tags);
-- Trigger updated_at
CREATE TRIGGER medias_updated_at
BEFORE UPDATE ON qwikpress.medias
FOR EACH ROW EXECUTE FUNCTION qwikpress.update_updated_at();
-- supabase/migrations/00007_create_functions.sql
-- Récupérer config + pages d'un site (optimisé)
CREATE OR REPLACE FUNCTION qwikpress.get_site_data(p_slug VARCHAR)
RETURNS JSONB AS $$
DECLARE
result JSONB;
BEGIN
SELECT jsonb_build_object(
'site', jsonb_build_object(
'id', s.id,
'slug', s.slug,
'name', s.name,
'config', s.config
),
'pages', COALESCE(
(SELECT jsonb_agg(
jsonb_build_object(
'id', p.id,
'slug', p.slug,
'meta', p.meta,
'content', p.content
)
)
FROM qwikpress.pages p
WHERE p.site_id = s.id AND p.is_published = true),
'[]'::jsonb
)
) INTO result
FROM qwikpress.sites s
WHERE s.slug = p_slug AND s.is_active = true;
RETURN result;
END;
$$ LANGUAGE plpgsql;
-- Récupérer une page spécifique
CREATE OR REPLACE FUNCTION qwikpress.get_page(p_site_slug VARCHAR, p_page_slug VARCHAR)
RETURNS JSONB AS $$
DECLARE
result JSONB;
BEGIN
SELECT jsonb_build_object(
'id', p.id,
'slug', p.slug,
'meta', p.meta,
'content', p.content,
'site', jsonb_build_object(
'id', s.id,
'slug', s.slug,
'name', s.name,
'config', s.config
)
) INTO result
FROM qwikpress.pages p
JOIN qwikpress.sites s ON s.id = p.site_id
WHERE s.slug = p_site_slug
AND p.slug = p_page_slug
AND s.is_active = true
AND p.is_published = true;
RETURN result;
END;
$$ LANGUAGE plpgsql;
-- Générer slug unique pour page
CREATE OR REPLACE FUNCTION qwikpress.generate_page_slug(
p_site_id UUID,
p_base_slug VARCHAR
)
RETURNS VARCHAR AS $$
DECLARE
v_slug VARCHAR;
v_count INTEGER := 0;
BEGIN
v_slug := p_base_slug;
WHILE EXISTS (
SELECT 1 FROM qwikpress.pages
WHERE site_id = p_site_id AND slug = v_slug
) LOOP
v_count := v_count + 1;
v_slug := p_base_slug || '-' || v_count;
END LOOP;
RETURN v_slug;
END;
$$ LANGUAGE plpgsql;
-- supabase/migrations/00008_create_policies.sql
-- Activer RLS
ALTER TABLE qwikpress.sites ENABLE ROW LEVEL SECURITY;
ALTER TABLE qwikpress.pages ENABLE ROW LEVEL SECURITY;
ALTER TABLE qwikpress.shared_modules ENABLE ROW LEVEL SECURITY;
ALTER TABLE qwikpress.medias ENABLE ROW LEVEL SECURITY;
-- Policies pour lecture publique (sites actifs, pages publiées)
CREATE POLICY "Sites publics lisibles" ON qwikpress.sites
FOR SELECT USING (is_active = true);
CREATE POLICY "Pages publiées lisibles" ON qwikpress.pages
FOR SELECT USING (is_published = true);
CREATE POLICY "Modules partagés lisibles" ON qwikpress.shared_modules
FOR SELECT USING (true);
CREATE POLICY "Médias lisibles" ON qwikpress.medias
FOR SELECT USING (true);
-- Note: Les policies d'écriture seront ajoutées
-- quand le système d'auth admin sera implémenté
-- supabase/storage/buckets.sql
-- Bucket pour les médias QwikPress
INSERT INTO storage.buckets (id, name, public, file_size_limit, allowed_mime_types)
VALUES (
'qwikpress-medias',
'qwikpress-medias',
true, -- Public pour CDN
52428800, -- 50MB max
ARRAY[
'image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/svg+xml',
'video/mp4', 'video/webm',
'application/pdf',
'font/woff', 'font/woff2'
]
);
-- Bucket pour les certificats/exports (privé)
INSERT INTO storage.buckets (id, name, public)
VALUES ('qwikpress-exports', 'qwikpress-exports', false);
-- Policies Storage
CREATE POLICY "Médias publics lisibles"
ON storage.objects FOR SELECT
USING (bucket_id = 'qwikpress-medias');
CREATE POLICY "Upload médias authentifié"
ON storage.objects FOR INSERT
WITH CHECK (
bucket_id = 'qwikpress-medias'
AND auth.role() = 'authenticated'
);
NAMESPACE: qwikpress
CLÉS:
qwikpress:site:{slug} → Config site complète (JSONB)
qwikpress:site:{slug}:nav → Navigation (extrait de config)
qwikpress:site:{slug}:page:{slug} → Page complète (meta + content)
qwikpress:site:{slug}:pages → Liste des pages (meta only)
TTL:
Config site: 300s (5 min)
Navigation: 300s (5 min)
Page: 60s (1 min)
Liste pages: 120s (2 min)
// src/services/redis-cache.service.ts
import Redis from 'ioredis';
export class RedisCacheService {
private redis: Redis;
private prefix = 'qwikpress';
constructor() {
this.redis = new Redis({
host: process.env.REDIS_HOST,
port: parseInt(process.env.REDIS_PORT || '6379'),
password: process.env.REDIS_PASSWORD,
});
}
// Clés
private siteKey(slug: string) { return `${this.prefix}:site:${slug}`; }
private pageKey(siteSlug: string, pageSlug: string) {
return `${this.prefix}:site:${siteSlug}:page:${pageSlug}`;
}
private navKey(slug: string) { return `${this.prefix}:site:${slug}:nav`; }
// GET avec fallback
async getSite(slug: string): Promise<SiteConfig | null> {
const cached = await this.redis.get(this.siteKey(slug));
return cached ? JSON.parse(cached) : null;
}
async getPage(siteSlug: string, pageSlug: string): Promise<Page | null> {
const cached = await this.redis.get(this.pageKey(siteSlug, pageSlug));
return cached ? JSON.parse(cached) : null;
}
// SET avec TTL
async setSite(slug: string, data: SiteConfig): Promise<void> {
await this.redis.setex(this.siteKey(slug), 300, JSON.stringify(data));
}
async setPage(siteSlug: string, pageSlug: string, data: Page): Promise<void> {
await this.redis.setex(this.pageKey(siteSlug, pageSlug), 60, JSON.stringify(data));
}
// Invalidation
async invalidateSite(slug: string): Promise<void> {
const pattern = `${this.prefix}:site:${slug}*`;
const keys = await this.redis.keys(pattern);
if (keys.length > 0) {
await this.redis.del(...keys);
}
}
async invalidatePage(siteSlug: string, pageSlug: string): Promise<void> {
await this.redis.del(this.pageKey(siteSlug, pageSlug));
}
}
Plutôt que des Edge Functions Supabase, l'invalidation est gérée directement dans le serveur Qwik :
// src/services/data/supabase-data.service.ts
// L'invalidation se fait automatiquement lors des opérations CRUD
async updatePage(siteSlug: string, pageSlug: string, data: UpdatePageInput): Promise<Page> {
// 1. Update Supabase
const { data: page, error } = await this.supabase
.schema('qwikpress')
.from('pages')
.update({ content: data.content, meta: data.meta, updated_at: new Date() })
.eq('slug', pageSlug)
.select()
.single();
if (error) throw error;
// 2. Invalider cache Redis immédiatement
await this.cache.invalidatePage(siteSlug, pageSlug);
return page;
}
async deletePage(siteSlug: string, pageSlug: string): Promise<void> {
await this.supabase
.schema('qwikpress')
.from('pages')
.delete()
.eq('slug', pageSlug);
// Invalider cache
await this.cache.invalidatePage(siteSlug, pageSlug);
await this.cache.invalidateSiteNav(siteSlug); // Navigation aussi
}
// src/routes/api/webhooks/supabase/index.ts
import type { RequestHandler } from '@builder.io/qwik-city';
import { cacheService } from '~/services/redis-cache.service';
export const onPost: RequestHandler = async ({ request, json }) => {
// Vérifier secret webhook
const secret = request.headers.get('x-supabase-webhook-secret');
if (secret !== process.env.SUPABASE_WEBHOOK_SECRET) {
return json(401, { error: 'Unauthorized' });
}
const payload = await request.json();
const { type, table, record, old_record } = payload;
// Invalider selon la table modifiée
switch (table) {
case 'sites':
const siteSlug = record?.slug || old_record?.slug;
if (siteSlug) await cacheService.invalidateSite(siteSlug);
break;
case 'pages':
// Récupérer site_id → slug puis invalider
const siteId = record?.site_id || old_record?.site_id;
if (siteId) {
const site = await getSiteById(siteId);
if (site) {
await cacheService.invalidatePage(site.slug, record?.slug);
}
}
break;
}
return json(200, { success: true });
};
// src/routes/admin/cache/index.ts
import type { RequestHandler } from '@builder.io/qwik-city';
import { cacheService } from '~/services/redis-cache.service';
// POST /admin/cache/invalidate
export const onPost: RequestHandler = async ({ request, json }) => {
const { siteSlug, pageSlug, type } = await request.json();
switch (type) {
case 'page':
await cacheService.invalidatePage(siteSlug, pageSlug);
break;
case 'site':
await cacheService.invalidateSite(siteSlug);
break;
case 'all':
await cacheService.invalidateAll();
break;
}
return json(200, { success: true, invalidated: type });
};
Avantage : Tout le code reste dans le projet Qwik, pas de fragmentation avec Supabase Functions.
// src/services/data/data-service.interface.ts
export interface IDataService {
// Sites
getSiteBySlug(slug: string): Promise<SiteConfig | null>;
getSiteByDomain(domain: string): Promise<SiteConfig | null>;
// Pages
getPage(siteSlug: string, pageSlug: string): Promise<Page | null>;
getPages(siteSlug: string, options?: { published?: boolean }): Promise<PageMeta[]>;
// Navigation
getNavigation(siteSlug: string): Promise<Navigation>;
// Modules partagés
getSharedModule(siteSlug: string, moduleName: string): Promise<Module | null>;
// Médias
getMediaUrl(siteSlug: string, path: string): string;
// Admin (CRUD)
createPage(siteSlug: string, data: CreatePageInput): Promise<Page>;
updatePage(siteSlug: string, pageSlug: string, data: UpdatePageInput): Promise<Page>;
deletePage(siteSlug: string, pageSlug: string): Promise<void>;
// Mode info
getMode(): 'file' | 'supabase';
}
// src/services/data/data-service.factory.ts
import { FileDataService } from './file-data.service';
import { SupabaseDataService } from './supabase-data.service';
import type { IDataService } from './data-service.interface';
export function createDataService(): IDataService {
const mode = process.env.DATA_MODE || 'file';
if (mode === 'supabase') {
return new SupabaseDataService();
}
return new FileDataService();
}
// Singleton export
export const dataService = createDataService();
// src/services/data/supabase-data.service.ts
import { createClient } from '@supabase/supabase-js';
import { RedisCacheService } from '../redis-cache.service';
import type { IDataService } from './data-service.interface';
export class SupabaseDataService implements IDataService {
private supabase;
private cache: RedisCacheService;
constructor() {
this.supabase = createClient(
process.env.SUPABASE_URL!,
process.env.SUPABASE_SERVICE_ROLE_KEY!
);
this.cache = new RedisCacheService();
}
async getSiteBySlug(slug: string): Promise<SiteConfig | null> {
// 1. Check cache
const cached = await this.cache.getSite(slug);
if (cached) return cached;
// 2. Query Supabase
const { data, error } = await this.supabase
.schema('qwikpress')
.from('sites')
.select('*')
.eq('slug', slug)
.eq('is_active', true)
.single();
if (error || !data) return null;
// 3. Cache result
await this.cache.setSite(slug, data);
return data;
}
async getPage(siteSlug: string, pageSlug: string): Promise<Page | null> {
// 1. Check cache
const cached = await this.cache.getPage(siteSlug, pageSlug);
if (cached) return cached;
// 2. Query via fonction PostgreSQL (optimisé)
const { data, error } = await this.supabase
.rpc('get_page', {
p_site_slug: siteSlug,
p_page_slug: pageSlug
});
if (error || !data) return null;
// 3. Cache result
await this.cache.setPage(siteSlug, pageSlug, data);
return data;
}
getMediaUrl(siteSlug: string, path: string): string {
// URL publique du bucket Supabase Storage
return `${process.env.SUPABASE_URL}/storage/v1/object/public/qwikpress-medias/${siteSlug}/${path}`;
}
getMode(): 'file' | 'supabase' {
return 'supabase';
}
// ... autres méthodes
}
#!/bin/bash
# supabase/scripts/setup.sh
set -e
echo "=== QwikPress Supabase Setup ==="
# Variables
SUPABASE_URL="${SUPABASE_URL:-}"
SUPABASE_SERVICE_KEY="${SUPABASE_SERVICE_ROLE_KEY:-}"
if [ -z "$SUPABASE_URL" ] || [ -z "$SUPABASE_SERVICE_KEY" ]; then
echo "Error: SUPABASE_URL et SUPABASE_SERVICE_ROLE_KEY requis"
exit 1
fi
# 1. Appliquer migrations
echo ">> Applying migrations..."
for file in ./migrations/*.sql; do
echo " - $file"
psql "$DATABASE_URL" -f "$file"
done
# 2. Setup storage
echo ">> Setting up storage buckets..."
psql "$DATABASE_URL" -f ./storage/buckets.sql
# 3. Seeds (optionnel)
if [ "$1" = "--with-seeds" ]; then
echo ">> Applying seeds..."
for file in ./seeds/*.sql; do
echo " - $file"
psql "$DATABASE_URL" -f "$file"
done
fi
# 4. Générer types
echo ">> Generating TypeScript types..."
./scripts/generate-types.sh
echo "=== Setup complete ==="
// supabase/scripts/migrate-from-files.ts
import { createClient } from '@supabase/supabase-js';
import * as fs from 'fs';
import * as path from 'path';
const supabase = createClient(
process.env.SUPABASE_URL!,
process.env.SUPABASE_SERVICE_ROLE_KEY!
);
async function migrateFromFiles(dataDir: string, siteSlug: string) {
console.log(`Migrating ${dataDir} to site: ${siteSlug}`);
// 1. Lire config.json
const configPath = path.join(dataDir, 'config.json');
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
// 2. Créer le site
const { data: site, error: siteError } = await supabase
.schema('qwikpress')
.from('sites')
.insert({
slug: siteSlug,
name: config.site?.name || siteSlug,
config: config
})
.select()
.single();
if (siteError) throw siteError;
console.log(`Site created: ${site.id}`);
// 3. Migrer les pages
const pagesDir = path.join(dataDir, 'pages');
const pageFolders = fs.readdirSync(pagesDir);
for (const folder of pageFolders) {
const pagePath = path.join(pagesDir, folder);
if (!fs.statSync(pagePath).isDirectory()) continue;
const metaPath = path.join(pagePath, 'meta.json');
const contentPath = path.join(pagePath, 'content.json');
if (!fs.existsSync(metaPath) || !fs.existsSync(contentPath)) {
console.warn(`Skipping ${folder}: missing meta or content`);
continue;
}
const meta = JSON.parse(fs.readFileSync(metaPath, 'utf-8'));
const content = JSON.parse(fs.readFileSync(contentPath, 'utf-8'));
const { error: pageError } = await supabase
.schema('qwikpress')
.from('pages')
.insert({
site_id: site.id,
slug: meta.slug || folder,
meta: meta,
content: content,
is_published: true
});
if (pageError) {
console.error(`Error migrating page ${folder}:`, pageError);
} else {
console.log(`Page migrated: ${folder}`);
}
}
// 4. Migrer les médias (TODO: upload vers bucket)
console.log('Media migration: TODO');
console.log('Migration complete!');
}
// CLI
const [,, dataDir, siteSlug] = process.argv;
if (!dataDir || !siteSlug) {
console.log('Usage: bun migrate-from-files.ts <data-dir> <site-slug>');
process.exit(1);
}
migrateFromFiles(dataDir, siteSlug);
# .env.example
# Mode de données
DATA_MODE=file # "file" ou "supabase"
# Supabase (requis si DATA_MODE=supabase)
SUPABASE_URL=https://supabase-api-prod.33800.nowhere84.com
SUPABASE_ANON_KEY=eyJ...
SUPABASE_SERVICE_ROLE_KEY=eyJ...
# Redis (requis si DATA_MODE=supabase)
REDIS_HOST=prod-redis.local
REDIS_PORT=6379
REDIS_PASSWORD=xxx
# File mode
DATA_DIR=./data
supabase/ avec structure complèteIDataServiceFileDataServiceSupabaseDataServiceRedisCacheServiceSupabaseDataService| Chemin | Description |
|---|---|
supabase/README.md |
Guide setup |
supabase/migrations/*.sql |
8 fichiers migration |
supabase/seeds/*.sql |
2 fichiers seeds |
supabase/storage/buckets.sql |
Config buckets |
supabase/scripts/*.sh |
5 scripts bash |
supabase/scripts/migrate-from-files.ts |
Script migration |
src/services/data/data-service.interface.ts |
Interface |
src/services/data/file-data.service.ts |
Adapter fichiers |
src/services/data/supabase-data.service.ts |
Adapter Supabase |
src/services/data/data-service.factory.ts |
Factory |
src/services/redis-cache.service.ts |
Service Redis |
src/routes/api/webhooks/supabase/index.ts |
Webhook invalidation (optionnel) |
src/routes/admin/cache/index.ts |
API invalidation manuelle |
En attente de validation pour commencer l'implémentation.