CON-333 — Update documentation and workflow for database promotion processes, emphasizing the restriction of SQL execution to dev preprod environments only. Enhance clarity on deployment steps and introduce guidelines for removing excess roles and permissions during synchronization. Increment version code.

This commit is contained in:
2026-07-23 09:16:19 -04:00
parent 9697b37263
commit 5ceeec7fdd
5 changed files with 407 additions and 61 deletions

View File

@ -267,31 +267,34 @@ function fxStaticSyncFetchRowsByKey($objDb, array $tabDef) {
return array('rows' => array(), 'duplicate_count' => 0, 'physical_count' => 0, 'error' => null);
}
$sql = !empty($tabDef['join_role_code'])
? ('SELECT erp.role_id, erp.perm_key, r.role_code'
. ' FROM `inscriptions_eve_role_permissions` erp'
. ' INNER JOIN `inscriptions_eve_roles` r ON r.role_id = erp.role_id'
. ' ORDER BY r.role_code, erp.perm_key')
: ('SELECT * FROM `' . $strTable . '` ORDER BY ' . fxStaticSyncSqlOrderByKeys($tabKeys));
if (!empty($tabDef['join_role_code'])) {
if (!fxStaticSyncTableExists($objDb, 'inscriptions_eve_roles')) {
return array('rows' => array(), 'duplicate_count' => 0, 'physical_count' => 0, 'error' => 'bad_keys');
}
$sql = 'SELECT erp.role_id, erp.perm_key, r.role_code'
. ' FROM `inscriptions_eve_role_permissions` erp'
. ' INNER JOIN `inscriptions_eve_roles` r ON r.role_id = erp.role_id'
. ' ORDER BY r.role_code, erp.perm_key';
$tabResults = $objDb->fxGetResults($sql);
} else {
if (!fxStaticSyncKeyColumnsValid($objDb, $strTable, $tabKeys)) {
return array('rows' => array(), 'duplicate_count' => 0, 'physical_count' => 0, 'error' => 'bad_keys');
}
$sql = 'SELECT * FROM `' . $strTable . '` ORDER BY ' . fxStaticSyncSqlOrderByKeys($tabKeys);
$tabResults = $objDb->fxGetResults($sql);
}
// CON-333 — clsMysql::fxGetResults renvoie null si 0 ligne (pas une erreur SQL).
if (!is_array($tabResults)) {
$tabResults = array();
}
$tabKeyed = array();
$intDupes = 0;
$intPhysical = 0;
if (!is_array($tabResults)) {
return array('rows' => array(), 'duplicate_count' => 0, 'physical_count' => 0, 'error' => 'query_failed');
}
foreach ($tabResults as $row) {
if (!is_array($row)) {
continue;
@ -1010,6 +1013,163 @@ function fxStaticSyncRepairInfoPoubelle($objSource, $objTarget, array $tabDef =
);
}
/**
* CON-333 — Retirer les lignes « en trop » (catalogue accès v2).
* info : jamais. Rôles : refuse si encore utilisés dans inscriptions_eve_acces.
*/
function fxStaticSyncRemoveExtras($objSource, $objTarget, $strTable, array $tabDef = null) {
if (!$tabDef || empty($tabDef['allow_remove_extras'])) {
return array('ok' => false, 'message' => 'Retrait des en trop non autorisé pour cette table.');
}
if ($strTable === 'info') {
return array('ok' => false, 'message' => 'Interdit : jamais de DELETE sur info.');
}
$tabPrep = fxStaticSyncPrepareSoftWrite($objSource, $objTarget, $strTable, $tabDef);
if (empty($tabPrep['ok'])) {
return $tabPrep;
}
$tabExtras = array();
foreach ($tabPrep['target_rows'] as $strKey => $tabEntry) {
if (!isset($tabPrep['source_rows'][$strKey])) {
$tabExtras[$strKey] = $tabEntry['row'];
}
}
$intExtra = count($tabExtras);
if ($intExtra === 0) {
return array('ok' => true, 'message' => 'Aucun en trop à retirer.', 'deleted' => 0, 'skipped' => 0);
}
$blnTransactional = (fxStaticSyncGetTableEngine($objTarget, $strTable) === 'INNODB');
if ($blnTransactional) {
$tabTx = fxStaticSyncExecQuery($objTarget, 'START TRANSACTION');
if (!$tabTx['ok']) {
return array('ok' => false, 'message' => 'Transaction : ' . $tabTx['error']);
}
}
$intDeleted = 0;
$intSkipped = 0;
$tabSkipReasons = array();
foreach ($tabExtras as $strKey => $row) {
if ($strTable === 'inscriptions_eve_roles') {
$intRoleId = isset($row['role_id']) ? (int)$row['role_id'] : 0;
$strCode = isset($row['role_code']) ? (string)$row['role_code'] : '?';
if ($intRoleId <= 0) {
$intSkipped++;
$tabSkipReasons[] = $strCode . ' (role_id invalide)';
continue;
}
$intUsed = 0;
if (fxStaticSyncTableExists($objTarget, 'inscriptions_eve_acces')) {
$intUsed = (int)$objTarget->fxGetVar(
'SELECT COUNT(*) FROM inscriptions_eve_acces WHERE role_id=' . $intRoleId
);
}
if ($intUsed > 0) {
$intSkipped++;
$tabSkipReasons[] = $strCode . ' (encore ' . $intUsed . ' accès promoteur — non effacé)';
continue;
}
// Nettoyer dabord les liens matrice orphelins de ce rôle.
if (fxStaticSyncTableExists($objTarget, 'inscriptions_eve_role_permissions')) {
$tabDelMat = fxStaticSyncExecQuery(
$objTarget,
'DELETE FROM `inscriptions_eve_role_permissions` WHERE role_id=' . $intRoleId
);
if (!$tabDelMat['ok']) {
if ($blnTransactional) {
fxStaticSyncExecQuery($objTarget, 'ROLLBACK');
}
return array('ok' => false, 'message' => 'DELETE matrice orpheline : ' . $tabDelMat['error']);
}
}
$strSql = 'DELETE FROM `inscriptions_eve_roles` WHERE role_id=' . $intRoleId
. ' AND role_code=\'' . $objTarget->fxEscape($strCode) . '\' LIMIT 1';
} elseif ($strTable === 'inscriptions_eve_role_permissions') {
$strCode = isset($row['role_code']) ? (string)$row['role_code'] : '';
$strPerm = isset($row['perm_key']) ? (string)$row['perm_key'] : '';
$intRoleId = isset($row['role_id']) ? (int)$row['role_id'] : 0;
if ($strPerm === '' || ($intRoleId <= 0 && $strCode === '')) {
$intSkipped++;
continue;
}
if ($intRoleId <= 0) {
$intRoleId = (int)$objTarget->fxGetVar(
"SELECT role_id FROM inscriptions_eve_roles WHERE role_code='"
. $objTarget->fxEscape($strCode) . "' LIMIT 1"
);
}
if ($intRoleId <= 0) {
$intSkipped++;
$tabSkipReasons[] = $strCode . '+' . $strPerm . ' (rôle introuvable)';
continue;
}
$strSql = 'DELETE FROM `inscriptions_eve_role_permissions` WHERE role_id=' . $intRoleId
. ' AND perm_key=\'' . $objTarget->fxEscape($strPerm) . '\' LIMIT 1';
} elseif ($strTable === 'inscriptions_eve_permissions') {
$strPerm = isset($row['perm_key']) ? (string)$row['perm_key'] : '';
if ($strPerm === '') {
$intSkipped++;
continue;
}
// Si encore référencée dans la matrice cible, ne pas casser.
$intMat = 0;
if (fxStaticSyncTableExists($objTarget, 'inscriptions_eve_role_permissions')) {
$intMat = (int)$objTarget->fxGetVar(
"SELECT COUNT(*) FROM inscriptions_eve_role_permissions WHERE perm_key='"
. $objTarget->fxEscape($strPerm) . "'"
);
}
if ($intMat > 0) {
$intSkipped++;
$tabSkipReasons[] = $strPerm . ' (encore ' . $intMat . ' lien(s) matrice)';
continue;
}
$strSql = 'DELETE FROM `inscriptions_eve_permissions` WHERE perm_key=\''
. $objTarget->fxEscape($strPerm) . '\' LIMIT 1';
} else {
if ($blnTransactional) {
fxStaticSyncExecQuery($objTarget, 'ROLLBACK');
}
return array('ok' => false, 'message' => 'Table non gérée pour retrait en trop.');
}
$tabDel = fxStaticSyncExecQuery($objTarget, $strSql);
if (!$tabDel['ok']) {
if ($blnTransactional) {
fxStaticSyncExecQuery($objTarget, 'ROLLBACK');
}
return array('ok' => false, 'message' => 'DELETE en trop : ' . $tabDel['error']);
}
$intDeleted++;
}
if ($blnTransactional) {
$tabCommit = fxStaticSyncExecQuery($objTarget, 'COMMIT');
if (!$tabCommit['ok']) {
fxStaticSyncExecQuery($objTarget, 'ROLLBACK');
return array('ok' => false, 'message' => 'Commit : ' . $tabCommit['error']);
}
}
$strMessage = $intDeleted . ' en trop retiré(s) (alignement catalogue sur préprod).';
if ($intSkipped > 0) {
$strMessage .= ' ' . $intSkipped . ' conservé(s) (encore utilisés) : '
. implode(' ; ', array_slice($tabSkipReasons, 0, 8));
}
return array(
'ok' => true,
'message' => $strMessage,
'deleted' => $intDeleted,
'skipped' => $intSkipped,
);
}
/**
* CON-333 — Ancien Sync destructif (DELETE + INSERT) : désactivé.
* Conservé pour ne pas casser dappels résiduels — renvoie toujours une erreur.
@ -1150,6 +1310,90 @@ function fxStaticSyncFormatKeyPreview($strKey) {
return $strKey;
}
/**
* CON-333 — Expliquer une ligne « en trop » (présente en cible, absente en préprod).
*/
function fxStaticSyncExplainExtraRow(array $tabDef, array $row = null) {
$strTable = $tabDef['table'];
if (!is_array($row)) {
return 'Présente seulement en cible — absente de la préprod (source).';
}
if ($strTable === 'info') {
$strClef = isset($row['info_clef']) ? (string)$row['info_clef'] : '';
$strLang = isset($row['info_langue']) ? (string)$row['info_langue'] : '';
$strPrg = isset($row['info_prg']) ? (string)$row['info_prg'] : '';
$strTexte = isset($row['info_texte']) ? (string)$row['info_texte'] : '';
$blnPoub = ($strTexte !== '' && $strTexte === $strClef);
if ($blnPoub) {
return 'Auto-créée en cible (texte = clé) — prg « ' . $strPrg . ' » / ' . $strLang
. '. Nexiste pas en préprod ; souvent résidu après trou info.';
}
$strPreview = function_exists('mb_substr') ? mb_substr($strTexte, 0, 80) : substr($strTexte, 0, 80);
return 'Libellé seulement en cible (pas en préprod) — prg « ' . $strPrg . ' » / ' . $strLang
. ' : « ' . $strPreview . ' ». Soit ajouté seulement ici, soit jamais repris en préprod.';
}
if ($strTable === 'inscriptions_eve_roles') {
$strCode = isset($row['role_code']) ? (string)$row['role_code'] : '?';
$strLab = isset($row['role_label_fr']) ? (string)$row['role_label_fr'] : '';
return 'Rôle seulement en cible : « ' . $strCode . ' »'
. ($strLab !== '' ? ' (' . $strLab . ')' : '')
. '. Pas dans la préprod — ancien rôle ou créé hors sync.';
}
if ($strTable === 'inscriptions_eve_role_permissions') {
$strCode = isset($row['role_code']) ? (string)$row['role_code'] : '?';
$strPerm = isset($row['perm_key']) ? (string)$row['perm_key'] : '?';
return 'Droit accordé seulement en cible : rôle « ' . $strCode . ' » → « ' . $strPerm . ' ». '
. 'Absent de la matrice préprod — soit préprod incomplète, soit droit en trop à retirer plus tard.';
}
if ($strTable === 'inscriptions_pages') {
$strCode = isset($row['pag_code']) ? (string)$row['pag_code'] : '?';
return 'Page promoteur seulement en cible : « ' . $strCode . ' ».';
}
if ($strTable === 'inscriptions_eve_permissions') {
$strPerm = isset($row['perm_key']) ? (string)$row['perm_key'] : '?';
return 'Permission catalogue seulement en cible : « ' . $strPerm . ' ».';
}
return 'Ligne seulement en cible (clé métier absente en préprod).';
}
/** CON-333 — Échantillon décart enrichi pour lUI. */
function fxStaticSyncBuildSample(array $tabDef, $strKey, $strType, $tabSrcEntry, $tabTgtEntry) {
$tabSrc = is_array($tabSrcEntry) && isset($tabSrcEntry['row']) ? $tabSrcEntry['row'] : null;
$tabTgt = is_array($tabTgtEntry) && isset($tabTgtEntry['row']) ? $tabTgtEntry['row'] : null;
$tabSample = array(
'key' => $strKey,
'type' => $strType,
'source' => $tabSrc,
'target' => $tabTgt,
'explain' => '',
);
if ($strType === 'extra') {
$tabSample['explain'] = fxStaticSyncExplainExtraRow($tabDef, is_array($tabTgt) ? $tabTgt : null);
} elseif ($strType === 'missing') {
$tabSample['explain'] = 'Absente en cible — présente en préprod (Appliquer manquants).';
} elseif ($strType === 'poubelle') {
$tabSample['explain'] = 'Poubelle en cible (texte = clé) — préprod a un vrai libellé (Appliquer corrections).';
} else {
$tabSample['explain'] = 'Même clé, contenu différent (Appliquer corrections = préprod gagne).';
}
if ($tabDef['table'] === 'info' && ($strType === 'diff' || $strType === 'poubelle' || $strType === 'extra')) {
$tabSample['source_texte'] = is_array($tabSrc) && isset($tabSrc['info_texte']) ? $tabSrc['info_texte'] : '';
$tabSample['target_texte'] = is_array($tabTgt) && isset($tabTgt['info_texte']) ? $tabTgt['info_texte'] : '';
}
return $tabSample;
}
function fxStaticSyncEnsureCsrfToken() {
if (empty($_SESSION['ms1_static_sync_csrf'])) {
$_SESSION['ms1_static_sync_csrf'] = bin2hex(random_bytes(16));
@ -1318,53 +1562,81 @@ function fxStaticSyncGetDiffDetail($strTable, $strEnvId, $tabConnections) {
$tabCompare = fxStaticSyncCompareEnvPair($objSource, $objTarget, $tabDef);
$tabIgnore = isset($tabDef['ignore_cols']) ? $tabDef['ignore_cols'] : array();
$tabSchema = fxStaticSyncResolveCompareColumns($objSource, $objTarget, $tabDef['table'], $tabIgnore);
if (!empty($tabDef['join_role_code'])) {
$tabSchema['compare_cols'] = $tabDef['keys'];
}
$tabSourceData = fxStaticSyncLoadKeyedRows($objSource, $tabDef, $tabSchema['compare_cols']);
$tabTargetData = fxStaticSyncLoadKeyedRows($objTarget, $tabDef, $tabSchema['compare_cols']);
$tabPoubelle = isset($tabCompare['poubelle_keys']) ? $tabCompare['poubelle_keys'] : array();
$intExtra = count($tabCompare['extra_keys']);
$strExtraContext = '';
if ($strTable === 'inscriptions_eve_role_permissions' && $intExtra > 0) {
$strExtraContext = 'La préprod n\'a que ' . (int)$tabCompare['source_count']
. ' lien(s) matrice. Les « en trop » sont des droits présents en cible seulement — '
. 'à revoir un par un (préprod incomplète vs anciens droits).';
} elseif ($strTable === 'info' && $intExtra > 0) {
$strExtraContext = 'Clés info seulement en cible : souvent créées à chaud (afficheTexte) '
. 'ou jamais reprises en préprod. Liste ci-dessous : poubelle vs vrai libellé.';
} elseif ($intExtra > 0) {
$strExtraContext = 'Lignes présentes seulement en cible. Soft sync ne les efface pas — '
. 'décider : garder, reporter en préprod, ou retirer plus tard (mode catalogue).';
}
$tabDetail = array(
'compare' => $tabCompare,
'samples' => array(),
'note' => 'CON-333 — Les clés « en trop » en cible ne sont jamais effacées automatiquement.',
'samples_missing' => array(),
'samples_diff' => array(),
'samples_extra' => array(),
'note' => 'CON-333 — Les « en trop » sont listés et expliqués ; ils ne sont pas oubliés.',
'extra_context' => $strExtraContext,
);
// Priorité daffichage : manquants, poubelles, autres diffs, extras (revue manuelle).
$tabKeysToShow = array_slice(
array_merge(
$tabCompare['missing_keys'],
$tabPoubelle,
array_diff($tabCompare['diff_keys'], $tabPoubelle),
$tabCompare['extra_keys']
),
0,
40
);
foreach ($tabKeysToShow as $strKey) {
$strType = 'diff';
if (in_array($strKey, $tabCompare['extra_keys'], true)) {
$strType = 'extra';
} elseif (in_array($strKey, $tabCompare['missing_keys'], true)) {
$strType = 'missing';
} elseif (in_array($strKey, $tabPoubelle, true)) {
$strType = 'poubelle';
}
$tabSample = array(
'key' => $strKey,
'type' => $strType,
'source' => isset($tabSourceData['rows'][$strKey]['row']) ? $tabSourceData['rows'][$strKey]['row'] : null,
'target' => isset($tabTargetData['rows'][$strKey]['row']) ? $tabTargetData['rows'][$strKey]['row'] : null,
foreach (array_slice($tabCompare['missing_keys'], 0, 80) as $strKey) {
$tabDetail['samples_missing'][] = fxStaticSyncBuildSample(
$tabDef,
$strKey,
'missing',
isset($tabSourceData['rows'][$strKey]) ? $tabSourceData['rows'][$strKey] : null,
null
);
if ($tabDef['table'] === 'info' && ($strType === 'diff' || $strType === 'poubelle')) {
$tabSample['source_texte'] = isset($tabSample['source']['info_texte']) ? $tabSample['source']['info_texte'] : '';
$tabSample['target_texte'] = isset($tabSample['target']['info_texte']) ? $tabSample['target']['info_texte'] : '';
}
$tabDetail['samples'][] = $tabSample;
}
foreach (array_slice(array_diff($tabCompare['diff_keys'], $tabPoubelle), 0, 80) as $strKey) {
$tabDetail['samples_diff'][] = fxStaticSyncBuildSample(
$tabDef,
$strKey,
'diff',
isset($tabSourceData['rows'][$strKey]) ? $tabSourceData['rows'][$strKey] : null,
isset($tabTargetData['rows'][$strKey]) ? $tabTargetData['rows'][$strKey] : null
);
}
foreach (array_slice($tabPoubelle, 0, 40) as $strKey) {
$tabDetail['samples_diff'][] = fxStaticSyncBuildSample(
$tabDef,
$strKey,
'poubelle',
isset($tabSourceData['rows'][$strKey]) ? $tabSourceData['rows'][$strKey] : null,
isset($tabTargetData['rows'][$strKey]) ? $tabTargetData['rows'][$strKey] : null
);
}
foreach (array_slice($tabCompare['extra_keys'], 0, 300) as $strKey) {
$tabDetail['samples_extra'][] = fxStaticSyncBuildSample(
$tabDef,
$strKey,
'extra',
null,
isset($tabTargetData['rows'][$strKey]) ? $tabTargetData['rows'][$strKey] : null
);
}
$tabDetail['samples'] = array_merge(
$tabDetail['samples_missing'],
$tabDetail['samples_diff'],
$tabDetail['samples_extra']
);
return $tabDetail;
}