Compare commits
12 Commits
76a71ebc2c
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
13b6385dbc | ||
|
|
e502ca97d8 | ||
|
|
a8ba0ee979 | ||
|
|
3d1bff45a3 | ||
|
|
10c2226481 | ||
|
|
7fc7f7d1de | ||
|
|
393dcbc2f9 | ||
|
|
6fde1aa00f | ||
|
|
8b323f8036 | ||
|
|
19d8d53948 | ||
|
|
44873cdfd8 | ||
|
|
91a0b21c52 |
27
Dockerfile
Normal file
27
Dockerfile
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
FROM node:22-slim
|
||||||
|
|
||||||
|
RUN corepack enable && corepack prepare pnpm@latest --activate
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Installer les dépendances
|
||||||
|
COPY package.json pnpm-lock.yaml ./
|
||||||
|
COPY patches/ ./patches/
|
||||||
|
RUN pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
# Copier les sources
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Compiler le frontend (vite) et le backend (esbuild)
|
||||||
|
RUN pnpm run build
|
||||||
|
|
||||||
|
# Copier les migrations drizzle
|
||||||
|
COPY drizzle/ ./drizzle/
|
||||||
|
COPY drizzle.config.ts ./
|
||||||
|
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
ENV PORT=3000
|
||||||
|
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
CMD ["node", "dist/index.js"]
|
||||||
9
drizzle/0006_rss_settings_scheduled_time.sql
Normal file
9
drizzle/0006_rss_settings_scheduled_time.sql
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
-- Migration: ajout colonnes scheduledTime et autoFetchEnabled dans rss_settings
|
||||||
|
-- Alignement du schéma Drizzle avec la table existante
|
||||||
|
|
||||||
|
ALTER TABLE rss_settings
|
||||||
|
ADD COLUMN IF NOT EXISTS scheduledTime VARCHAR(5) DEFAULT '06:00',
|
||||||
|
ADD COLUMN IF NOT EXISTS autoFetchEnabled TINYINT(1) NOT NULL DEFAULT 1;
|
||||||
|
|
||||||
|
UPDATE rss_settings SET autoFetchEnabled = isEnabled WHERE 1=1;
|
||||||
|
UPDATE rss_settings SET scheduledTime = '06:00' WHERE scheduledTime IS NULL OR scheduledTime = '';
|
||||||
@@ -66,8 +66,8 @@ export const veilleItems = mysqlTable("veille_items", {
|
|||||||
categorie: varchar("categorie", { length: 128 }),
|
categorie: varchar("categorie", { length: 128 }),
|
||||||
niveau: varchar("niveau", { length: 128 }),
|
niveau: varchar("niveau", { length: 128 }),
|
||||||
territoire: varchar("territoire", { length: 255 }),
|
territoire: varchar("territoire", { length: 255 }),
|
||||||
// Liste JSON des territoires (multi-département) ex: ["Isère","Savoie"]
|
// Liste JSON des territoires (multi-département) ex: ["Isère","Savoie"] — stocké en text pour compatibilité Drizzle/MySQL
|
||||||
territoires: json("territoires").$type<string[]>(),
|
territoires: text("territoires"),
|
||||||
resume: text("resume"),
|
resume: text("resume"),
|
||||||
source: varchar("source", { length: 512 }),
|
source: varchar("source", { length: 512 }),
|
||||||
passage: text("passage"),
|
passage: text("passage"),
|
||||||
@@ -91,8 +91,8 @@ export const aapItems = mysqlTable("aap_items", {
|
|||||||
categorie: mysqlEnum("categorie", ["Handicap", "PA", "Enfance", "Précarité", "Sanitaire", "Autre"]).notNull(),
|
categorie: mysqlEnum("categorie", ["Handicap", "PA", "Enfance", "Précarité", "Sanitaire", "Autre"]).notNull(),
|
||||||
region: varchar("region", { length: 255 }),
|
region: varchar("region", { length: 255 }),
|
||||||
departement: varchar("departement", { length: 255 }),
|
departement: varchar("departement", { length: 255 }),
|
||||||
// Liste JSON des départements (multi-département) ex: ["Isère (38)","Savoie (73)"]
|
// Liste JSON des départements (multi-département) ex: ["Isère (38)","Savoie (73)"] — stocké en text pour compatibilité Drizzle/MySQL
|
||||||
departements: json("departements").$type<string[]>(),
|
departements: text("departements"),
|
||||||
dateCloture: timestamp("dateCloture"),
|
dateCloture: timestamp("dateCloture"),
|
||||||
datePublication: timestamp("datePublication"),
|
datePublication: timestamp("datePublication"),
|
||||||
lien: text("lien"),
|
lien: text("lien"),
|
||||||
|
|||||||
@@ -12,7 +12,8 @@ import { runFullImport } from "../importer";
|
|||||||
import uploadRoutes from "../uploadRoutes";
|
import uploadRoutes from "../uploadRoutes";
|
||||||
import scheduledRoutes from "../scheduledRoutes";
|
import scheduledRoutes from "../scheduledRoutes";
|
||||||
import { ensureAdminExists } from "../localAuth";
|
import { ensureAdminExists } from "../localAuth";
|
||||||
import { getSetting } from "../db";
|
import { getSetting, getRssSettings } from "../db";
|
||||||
|
import { runRssFetch } from "../rssEngine";
|
||||||
|
|
||||||
function isPortAvailable(port: number): Promise<boolean> {
|
function isPortAvailable(port: number): Promise<boolean> {
|
||||||
return new Promise(resolve => {
|
return new Promise(resolve => {
|
||||||
@@ -30,7 +31,6 @@ async function findAvailablePort(startPort: number = 3000): Promise<number> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ─── Tâche d'import quotidien ─────────────────────────────────────────────────
|
// ─── Tâche d'import quotidien ─────────────────────────────────────────────────
|
||||||
|
|
||||||
let cronJob: ReturnType<typeof cron.schedule> | null = null;
|
let cronJob: ReturnType<typeof cron.schedule> | null = null;
|
||||||
|
|
||||||
async function scheduleDailyImport() {
|
async function scheduleDailyImport() {
|
||||||
@@ -38,12 +38,10 @@ async function scheduleDailyImport() {
|
|||||||
const importTime = (await getSetting("import_time")) || "06:00";
|
const importTime = (await getSetting("import_time")) || "06:00";
|
||||||
const [hour, minute] = importTime.split(":").map(Number);
|
const [hour, minute] = importTime.split(":").map(Number);
|
||||||
const cronExpr = `0 ${minute ?? 0} ${hour ?? 6} * * *`;
|
const cronExpr = `0 ${minute ?? 0} ${hour ?? 6} * * *`;
|
||||||
|
|
||||||
if (cronJob) {
|
if (cronJob) {
|
||||||
cronJob.stop();
|
cronJob.stop();
|
||||||
cronJob = null;
|
cronJob = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
cronJob = cron.schedule(cronExpr, async () => {
|
cronJob = cron.schedule(cronExpr, async () => {
|
||||||
console.log(`[Cron] Import automatique démarré à ${new Date().toISOString()}`);
|
console.log(`[Cron] Import automatique démarré à ${new Date().toISOString()}`);
|
||||||
try {
|
try {
|
||||||
@@ -53,10 +51,81 @@ async function scheduleDailyImport() {
|
|||||||
console.error("[Cron] Erreur lors de l'import:", e);
|
console.error("[Cron] Erreur lors de l'import:", e);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(`[Cron] Import quotidien planifié à ${importTime} (${cronExpr})`);
|
console.log(`[Cron] Import quotidien planifié à ${importTime} (${cronExpr})`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── Planificateur RSS natif ──────────────────────────────────────────────────
|
||||||
|
let rssCronJob: ReturnType<typeof cron.schedule> | null = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Démarre (ou redémarre) le planificateur RSS en lisant la configuration
|
||||||
|
* depuis la table rss_settings. Peut être appelé au démarrage et à chaque
|
||||||
|
* modification des paramètres RSS via l'interface d'administration.
|
||||||
|
*/
|
||||||
|
export async function scheduleRssFetch() {
|
||||||
|
// Arrêter le cron existant s'il y en a un
|
||||||
|
if (rssCronJob) {
|
||||||
|
rssCronJob.stop();
|
||||||
|
rssCronJob = null;
|
||||||
|
console.log("[RSS Cron] Planificateur RSS arrêté.");
|
||||||
|
}
|
||||||
|
|
||||||
|
let settings = null;
|
||||||
|
try {
|
||||||
|
settings = await getRssSettings();
|
||||||
|
} catch (e) {
|
||||||
|
// En cas d'erreur (ex: colonne manquante lors d'une migration),
|
||||||
|
// utiliser les valeurs par défaut et planifier un retry dans 2 minutes
|
||||||
|
console.warn("[RSS Cron] Impossible de lire les paramètres RSS, utilisation des valeurs par défaut:", (e as Error).message);
|
||||||
|
setTimeout(() => scheduleRssFetch(), 2 * 60 * 1000);
|
||||||
|
// Démarrer quand même avec les valeurs par défaut
|
||||||
|
settings = { autoFetchEnabled: true, fetchMode: "interval" as const, fetchIntervalMinutes: 60, scheduledTime: "06:00" };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!settings || !settings.autoFetchEnabled) {
|
||||||
|
console.log("[RSS Cron] Lecture automatique des flux RSS désactivée.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let cronExpr: string;
|
||||||
|
|
||||||
|
if (settings.fetchMode === "interval") {
|
||||||
|
// Mode intervalle : toutes les N minutes
|
||||||
|
const intervalMin = Math.max(5, settings.fetchIntervalMinutes ?? 60);
|
||||||
|
if (intervalMin < 60) {
|
||||||
|
cronExpr = `*/${intervalMin} * * * *`;
|
||||||
|
} else if (intervalMin % 60 === 0) {
|
||||||
|
const hours = intervalMin / 60;
|
||||||
|
cronExpr = `0 */${hours} * * *`;
|
||||||
|
} else {
|
||||||
|
cronExpr = `*/${intervalMin} * * * *`;
|
||||||
|
}
|
||||||
|
console.log(`[RSS Cron] Mode intervalle — toutes les ${intervalMin} minutes (${cronExpr})`);
|
||||||
|
} else {
|
||||||
|
// Mode planifié : heure fixe quotidienne
|
||||||
|
const scheduledTime = settings.scheduledTime ?? "06:00";
|
||||||
|
const [hour, minute] = scheduledTime.split(":").map(Number);
|
||||||
|
cronExpr = `0 ${minute ?? 0} ${hour ?? 6} * * *`;
|
||||||
|
console.log(`[RSS Cron] Mode planifié — tous les jours à ${scheduledTime} (${cronExpr})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
rssCronJob = cron.schedule(cronExpr, async () => {
|
||||||
|
console.log(`[RSS Cron] Lecture des flux RSS démarrée à ${new Date().toISOString()}`);
|
||||||
|
try {
|
||||||
|
const summary = await runRssFetch();
|
||||||
|
console.log(
|
||||||
|
`[RSS Cron] Lecture terminée — ${summary.totalFeeds} flux, ` +
|
||||||
|
`+${summary.totalNewItems} nouveaux articles, ` +
|
||||||
|
`${summary.errorFeeds} erreur(s)`
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
console.error("[RSS Cron] Erreur lors de la lecture des flux:", e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("[RSS Cron] Planificateur RSS démarré.");
|
||||||
|
}
|
||||||
|
|
||||||
async function startServer() {
|
async function startServer() {
|
||||||
const app = express();
|
const app = express();
|
||||||
const server = createServer(app);
|
const server = createServer(app);
|
||||||
@@ -67,7 +136,6 @@ async function startServer() {
|
|||||||
registerOAuthRoutes(app);
|
registerOAuthRoutes(app);
|
||||||
app.use(uploadRoutes);
|
app.use(uploadRoutes);
|
||||||
app.use(scheduledRoutes);
|
app.use(scheduledRoutes);
|
||||||
|
|
||||||
app.use(
|
app.use(
|
||||||
"/api/trpc",
|
"/api/trpc",
|
||||||
createExpressMiddleware({ router: appRouter, createContext })
|
createExpressMiddleware({ router: appRouter, createContext })
|
||||||
@@ -81,18 +149,17 @@ async function startServer() {
|
|||||||
|
|
||||||
const preferredPort = parseInt(process.env.PORT || "3000");
|
const preferredPort = parseInt(process.env.PORT || "3000");
|
||||||
const port = await findAvailablePort(preferredPort);
|
const port = await findAvailablePort(preferredPort);
|
||||||
|
|
||||||
if (port !== preferredPort) {
|
if (port !== preferredPort) {
|
||||||
console.log(`Port ${preferredPort} is busy, using port ${port} instead`);
|
console.log(`Port ${preferredPort} is busy, using port ${port} instead`);
|
||||||
}
|
}
|
||||||
|
|
||||||
server.listen(port, async () => {
|
server.listen(port, async () => {
|
||||||
console.log(`Server running on http://localhost:${port}/`);
|
console.log(`Server running on http://localhost:${port}/`);
|
||||||
|
|
||||||
// Initialisation post-démarrage
|
// Initialisation post-démarrage
|
||||||
try {
|
try {
|
||||||
await ensureAdminExists();
|
await ensureAdminExists();
|
||||||
await scheduleDailyImport();
|
await scheduleDailyImport();
|
||||||
|
await scheduleRssFetch();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("[Init] Erreur d'initialisation:", e);
|
console.error("[Init] Erreur d'initialisation:", e);
|
||||||
}
|
}
|
||||||
|
|||||||
29
server/db.ts
29
server/db.ts
@@ -1,5 +1,6 @@
|
|||||||
import { eq, desc, and, like, gte, lte, or, sql } from "drizzle-orm";
|
import { eq, desc, and, like, gte, lte, or, sql } from "drizzle-orm";
|
||||||
import { drizzle } from "drizzle-orm/mysql2";
|
import { drizzle } from "drizzle-orm/mysql2";
|
||||||
|
import mysql from "mysql2/promise";
|
||||||
import {
|
import {
|
||||||
InsertUser,
|
InsertUser,
|
||||||
users,
|
users,
|
||||||
@@ -15,17 +16,25 @@ import {
|
|||||||
rssSettings,
|
rssSettings,
|
||||||
type InsertRssFeed,
|
type InsertRssFeed,
|
||||||
type InsertRssSettings,
|
type InsertRssSettings,
|
||||||
|
type ImportLog,
|
||||||
type RssFeed,
|
type RssFeed,
|
||||||
type RssSettings,
|
type RssSettings,
|
||||||
} from "../drizzle/schema";
|
} from "../drizzle/schema";
|
||||||
import { ENV } from "./_core/env";
|
import { ENV } from "./_core/env";
|
||||||
|
|
||||||
let _db: ReturnType<typeof drizzle> | null = null;
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
let _db: any = null;
|
||||||
|
|
||||||
export async function getDb() {
|
export async function getDb() {
|
||||||
if (!_db && process.env.DATABASE_URL) {
|
if (!_db && process.env.DATABASE_URL) {
|
||||||
try {
|
try {
|
||||||
_db = drizzle(process.env.DATABASE_URL);
|
const pool = mysql.createPool({
|
||||||
|
uri: process.env.DATABASE_URL,
|
||||||
|
waitForConnections: true,
|
||||||
|
connectionLimit: 10,
|
||||||
|
enableKeepAlive: true,
|
||||||
|
});
|
||||||
|
_db = drizzle(pool);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn("[Database] Failed to connect:", error);
|
console.warn("[Database] Failed to connect:", error);
|
||||||
_db = null;
|
_db = null;
|
||||||
@@ -174,9 +183,9 @@ export async function getVeilleDistinctValues() {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
categories: cats.map((r) => r.value!).filter(Boolean).sort(),
|
categories: cats.map((r: { value: string | null }) => r.value!).filter(Boolean).sort(),
|
||||||
niveaux: niveaux.map((r) => r.value!).filter(Boolean).sort(),
|
niveaux: niveaux.map((r: { value: string | null }) => r.value!).filter(Boolean).sort(),
|
||||||
territoires: territoires.map((r) => r.value!).filter(Boolean).sort(),
|
territoires: territoires.map((r: { value: string | null }) => r.value!).filter(Boolean).sort(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -241,8 +250,8 @@ export async function getAapDistinctValues() {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
regions: regions.map((r) => r.value!).filter(Boolean).sort(),
|
regions: regions.map((r: { value: string | null }) => r.value!).filter(Boolean).sort(),
|
||||||
departements: departements.map((r) => r.value!).filter(Boolean).sort(),
|
departements: departements.map((r: { value: string | null }) => r.value!).filter(Boolean).sort(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -310,9 +319,9 @@ export async function getImportStats() {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
const total = allLogs.length;
|
const total = allLogs.length;
|
||||||
const success = allLogs.filter(l => l.status === 'success').length;
|
const success = allLogs.filter((l: ImportLog) => l.status === 'success').length;
|
||||||
const errors = allLogs.filter(l => l.status === 'error').length;
|
const errors = allLogs.filter((l: ImportLog) => l.status === 'error').length;
|
||||||
const totalNewRows = allLogs.reduce((sum, l) => sum + (l.newRows ?? 0), 0);
|
const totalNewRows = allLogs.reduce((sum: number, l: ImportLog) => sum + (l.newRows ?? 0), 0);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
totalVeille: Number(veilleCount[0]?.count ?? 0),
|
totalVeille: Number(veilleCount[0]?.count ?? 0),
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ import {
|
|||||||
saveRssSettings,
|
saveRssSettings,
|
||||||
} from "./db";
|
} from "./db";
|
||||||
import { importVeille, importAAP, runFullImport, getImportConfig } from "./importer";
|
import { importVeille, importAAP, runFullImport, getImportConfig } from "./importer";
|
||||||
|
import { scheduleRssFetch } from "./_core/index";
|
||||||
import { loginLocalUser, hashPassword, ensureAdminExists } from "./localAuth";
|
import { loginLocalUser, hashPassword, ensureAdminExists } from "./localAuth";
|
||||||
|
|
||||||
// ─── Middleware admin ─────────────────────────────────────────────────────────
|
// ─── Middleware admin ─────────────────────────────────────────────────────────
|
||||||
@@ -408,6 +409,8 @@ export const appRouter = router({
|
|||||||
}))
|
}))
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
await saveRssSettings(input);
|
await saveRssSettings(input);
|
||||||
|
// Recharger le planificateur RSS avec les nouveaux paramètres
|
||||||
|
await scheduleRssFetch();
|
||||||
return { success: true };
|
return { success: true };
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -364,9 +364,7 @@ async function processFeed(feed: RssFeed): Promise<FetchResult> {
|
|||||||
categorie,
|
categorie,
|
||||||
niveau,
|
niveau,
|
||||||
territoire,
|
territoire,
|
||||||
territoires: territoire !== "France" && territoire !== "Auvergne-Rhône-Alpes"
|
territoires: JSON.stringify(territoire !== "France" && territoire !== "Auvergne-Rhône-Alpes" ? [territoire] : []),
|
||||||
? [territoire]
|
|
||||||
: [],
|
|
||||||
resume: description || null,
|
resume: description || null,
|
||||||
source: feed.name,
|
source: feed.name,
|
||||||
lien: link || null,
|
lien: link || null,
|
||||||
@@ -375,7 +373,7 @@ async function processFeed(feed: RssFeed): Promise<FetchResult> {
|
|||||||
});
|
});
|
||||||
result.newItems++;
|
result.newItems++;
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
if (e?.code === "ER_DUP_ENTRY" || e?.message?.includes("Duplicate entry")) {
|
if (e?.code === "ER_DUP_ENTRY" || e?.cause?.code === "ER_DUP_ENTRY" || e?.message?.includes("Duplicate entry") || e?.cause?.message?.includes("Duplicate entry")) {
|
||||||
// Article existant → ajouter le territoire à la liste si c'est un nouveau département
|
// Article existant → ajouter le territoire à la liste si c'est un nouveau département
|
||||||
if (territoire !== "France" && territoire !== "Auvergne-Rhône-Alpes") {
|
if (territoire !== "France" && territoire !== "Auvergne-Rhône-Alpes") {
|
||||||
await db.execute(
|
await db.execute(
|
||||||
@@ -393,6 +391,7 @@ async function processFeed(feed: RssFeed): Promise<FetchResult> {
|
|||||||
result.skippedItems++;
|
result.skippedItems++;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
console.error(`[RSS DEBUG veille] code=${e?.code} errno=${e?.errno} sqlMsg=${e?.sqlMessage} msg=${String(e?.message).substring(0,200)}`);
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -410,13 +409,13 @@ async function processFeed(feed: RssFeed): Promise<FetchResult> {
|
|||||||
categorie,
|
categorie,
|
||||||
region,
|
region,
|
||||||
departement,
|
departement,
|
||||||
departements: departement ? [departement] : [],
|
departements: JSON.stringify(departement ? [departement] : []),
|
||||||
lien: link || null,
|
lien: link || null,
|
||||||
datePublication: pubDate,
|
datePublication: pubDate,
|
||||||
});
|
});
|
||||||
result.newItems++;
|
result.newItems++;
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
if (e?.code === "ER_DUP_ENTRY" || e?.message?.includes("Duplicate entry")) {
|
if (e?.code === "ER_DUP_ENTRY" || e?.cause?.code === "ER_DUP_ENTRY" || e?.message?.includes("Duplicate entry") || e?.cause?.message?.includes("Duplicate entry")) {
|
||||||
// Article existant → ajouter le département à la liste
|
// Article existant → ajouter le département à la liste
|
||||||
if (departement) {
|
if (departement) {
|
||||||
await db.execute(
|
await db.execute(
|
||||||
@@ -504,18 +503,27 @@ export async function migrateExistingItems(): Promise<MigrationSummary> {
|
|||||||
const normalizedTitle = buildMergeKey(row.titre || "");
|
const normalizedTitle = buildMergeKey(row.titre || "");
|
||||||
const newDedupKey = dedupHash(normalizedTitle + "|veille");
|
const newDedupKey = dedupHash(normalizedTitle + "|veille");
|
||||||
|
|
||||||
|
try {
|
||||||
await db.update(veilleItems)
|
await db.update(veilleItems)
|
||||||
.set({
|
.set({
|
||||||
categorie: newCategorie,
|
categorie: newCategorie,
|
||||||
niveau: newNiveau,
|
niveau: newNiveau,
|
||||||
territoire: newTerritoire,
|
territoire: newTerritoire,
|
||||||
territoires: newTerritoire !== "France" && newTerritoire !== "Auvergne-Rhône-Alpes"
|
territoires: JSON.stringify(newTerritoire !== "France" && newTerritoire !== "Auvergne-Rhône-Alpes" ? [newTerritoire] : []),
|
||||||
? [newTerritoire] : [],
|
|
||||||
titre: row.titre,
|
titre: row.titre,
|
||||||
dedupKey: newDedupKey,
|
dedupKey: newDedupKey,
|
||||||
})
|
})
|
||||||
.where(eq(veilleItems.id, row.id));
|
.where(eq(veilleItems.id, row.id));
|
||||||
veilleUpdated++;
|
veilleUpdated++;
|
||||||
|
} catch (e: any) {
|
||||||
|
// Si le nouveau dedupKey existe déjà → cet article est un doublon, le supprimer
|
||||||
|
if (e?.code === "ER_DUP_ENTRY" || e?.cause?.code === "ER_DUP_ENTRY" || e?.cause?.message?.includes("Duplicate entry")) {
|
||||||
|
await db.delete(veilleItems).where(eq(veilleItems.id, row.id));
|
||||||
|
veilleMerged++;
|
||||||
|
} else {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// Groupe : fusionner en gardant le premier, supprimer les autres
|
// Groupe : fusionner en gardant le premier, supprimer les autres
|
||||||
const sorted = group.sort((a: (typeof veilleRows)[number], b: (typeof veilleRows)[number]) => a.id - b.id);
|
const sorted = group.sort((a: (typeof veilleRows)[number], b: (typeof veilleRows)[number]) => a.id - b.id);
|
||||||
@@ -538,12 +546,13 @@ export async function migrateExistingItems(): Promise<MigrationSummary> {
|
|||||||
const newDedupKey = dedupHash(normalizedTitle + "|veille");
|
const newDedupKey = dedupHash(normalizedTitle + "|veille");
|
||||||
|
|
||||||
// Mettre à jour le principal
|
// Mettre à jour le principal
|
||||||
|
try {
|
||||||
await db.update(veilleItems)
|
await db.update(veilleItems)
|
||||||
.set({
|
.set({
|
||||||
categorie: newCategorie,
|
categorie: newCategorie,
|
||||||
niveau: allTerritoires.length > 1 ? "departemental" : "regional",
|
niveau: allTerritoires.length > 1 ? "departemental" : "regional",
|
||||||
territoire: allTerritoires.length > 0 ? allTerritoires[0] : "Auvergne-Rhône-Alpes",
|
territoire: allTerritoires.length > 0 ? allTerritoires[0] : "Auvergne-Rhône-Alpes",
|
||||||
territoires: allTerritoires,
|
territoires: JSON.stringify(allTerritoires),
|
||||||
titre: primary.titre,
|
titre: primary.titre,
|
||||||
dedupKey: newDedupKey,
|
dedupKey: newDedupKey,
|
||||||
})
|
})
|
||||||
@@ -555,6 +564,17 @@ export async function migrateExistingItems(): Promise<MigrationSummary> {
|
|||||||
veilleMerged++;
|
veilleMerged++;
|
||||||
}
|
}
|
||||||
veilleUpdated++;
|
veilleUpdated++;
|
||||||
|
} catch (e: any) {
|
||||||
|
// Si le newDedupKey existe déjà → supprimer tout le groupe
|
||||||
|
if (e?.code === "ER_DUP_ENTRY" || e?.cause?.code === "ER_DUP_ENTRY" || e?.cause?.message?.includes("Duplicate entry")) {
|
||||||
|
for (const row of sorted) {
|
||||||
|
await db.delete(veilleItems).where(eq(veilleItems.id, row.id));
|
||||||
|
veilleMerged++;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -577,16 +597,26 @@ export async function migrateExistingItems(): Promise<MigrationSummary> {
|
|||||||
const normalizedTitle = buildMergeKey(row.titre || "");
|
const normalizedTitle = buildMergeKey(row.titre || "");
|
||||||
const newDedupKey = dedupHash(normalizedTitle + "|aap");
|
const newDedupKey = dedupHash(normalizedTitle + "|aap");
|
||||||
|
|
||||||
|
try {
|
||||||
await db.update(aapItems)
|
await db.update(aapItems)
|
||||||
.set({
|
.set({
|
||||||
region: newRegion,
|
region: newRegion,
|
||||||
departement: newDept,
|
departement: newDept,
|
||||||
departements: newDept ? [newDept] : [],
|
departements: JSON.stringify(newDept ? [newDept] : []),
|
||||||
titre: row.titre,
|
titre: row.titre,
|
||||||
dedupKey: newDedupKey,
|
dedupKey: newDedupKey,
|
||||||
})
|
})
|
||||||
.where(eq(aapItems.id, row.id));
|
.where(eq(aapItems.id, row.id));
|
||||||
aapUpdated++;
|
aapUpdated++;
|
||||||
|
} catch (e: any) {
|
||||||
|
// Si le nouveau dedupKey existe déjà → cet article est un doublon, le supprimer
|
||||||
|
if (e?.code === "ER_DUP_ENTRY" || e?.cause?.code === "ER_DUP_ENTRY" || e?.cause?.message?.includes("Duplicate entry")) {
|
||||||
|
await db.delete(aapItems).where(eq(aapItems.id, row.id));
|
||||||
|
aapMerged++;
|
||||||
|
} else {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// Fusionner
|
// Fusionner
|
||||||
const sorted = group.sort((a: (typeof aapRows)[number], b: (typeof aapRows)[number]) => a.id - b.id);
|
const sorted = group.sort((a: (typeof aapRows)[number], b: (typeof aapRows)[number]) => a.id - b.id);
|
||||||
@@ -604,11 +634,12 @@ export async function migrateExistingItems(): Promise<MigrationSummary> {
|
|||||||
const normalizedTitle = buildMergeKey(primary.titre || "");
|
const normalizedTitle = buildMergeKey(primary.titre || "");
|
||||||
const newDedupKey = dedupHash(normalizedTitle + "|aap");
|
const newDedupKey = dedupHash(normalizedTitle + "|aap");
|
||||||
|
|
||||||
|
try {
|
||||||
await db.update(aapItems)
|
await db.update(aapItems)
|
||||||
.set({
|
.set({
|
||||||
region: "Auvergne-Rhône-Alpes",
|
region: "Auvergne-Rhône-Alpes",
|
||||||
departement: allDepts.length > 0 ? allDepts[0] : null,
|
departement: allDepts.length > 0 ? allDepts[0] : null,
|
||||||
departements: allDepts,
|
departements: JSON.stringify(allDepts),
|
||||||
titre: primary.titre,
|
titre: primary.titre,
|
||||||
dedupKey: newDedupKey,
|
dedupKey: newDedupKey,
|
||||||
})
|
})
|
||||||
@@ -619,6 +650,17 @@ export async function migrateExistingItems(): Promise<MigrationSummary> {
|
|||||||
aapMerged++;
|
aapMerged++;
|
||||||
}
|
}
|
||||||
aapUpdated++;
|
aapUpdated++;
|
||||||
|
} catch (e: any) {
|
||||||
|
// Si le newDedupKey existe déjà → supprimer tout le groupe
|
||||||
|
if (e?.code === "ER_DUP_ENTRY" || e?.cause?.code === "ER_DUP_ENTRY" || e?.cause?.message?.includes("Duplicate entry")) {
|
||||||
|
for (const row of sorted) {
|
||||||
|
await db.delete(aapItems).where(eq(aapItems.id, row.id));
|
||||||
|
aapMerged++;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user