first commit

pull/1/head
paidge 4 months ago
commit e022a932df

@ -0,0 +1,17 @@
{
"editor.formatOnType": true,
"editor.formatOnSave": true,
"editor.formatOnPaste": true,
"editor.tabSize": 4,
"[php]": {
"editor.defaultFormatter": "bmewburn.vscode-intelephense-client"
},
"intelephense.telemetry.enabled": false,
"intelephense.files.associations": ["*.php"],
// Indiquer le chemin d'installation de SPIP pour l'autocomplétion et la PHPdoc
// Format Windows
// "intelephense.environment.includePaths": ["..\\.."],
// Format Linux
// "intelephense.environment.includePaths": ["../.."],
"intelephense.format.braces": "k&r"
}

@ -0,0 +1,124 @@
# SPIP GraphQL
## 1. But
Exposer un endpoint graphQL avec les données de SPIP grâce à la bibliothèque [graphql-php](https://github.com/webonyx/graphql-php)
![Client JS inétgré graphiql](./captures/graphiql.png)
## 2. Fonctionnement du plugin
- Création d 'une table `meta_graphql` pour stocker les informations de configuration du plugin
- Expose un point d'entrée unique sur `spip.php?action=graphql` grâce à la librairie [graphql-php](https://github.com/webonyx/graphql-php). Ce fichier prépare le contexte et lance le serveur graphQL en attente des requêtes. Nul besoin du plugin `http` !
- Toute la logique de l'API se situe dans le dossier `/schema`
## 3. Fonctionnement de l'API
### a. Apprendre graphQL
Si vous ne connaissaez pas graphQL, voici [la documentation](https://graphql.org/learn/).
### b. Les types disponibles
Les requêtes graphQL sont fortement typées. Les types sont créés dynamiquement en fonction des objets éditoriaux que vous aurez sélectionnés dans le Back-Office.
Grâce au [schéma d'introspection](https://graphql.org/learn/introspection/), les clients peuvent afficher les types de données disponibles. Voici les types actuellement disponibles :
- Les types de bases (scalaires) : `ID`, `String`, `Int`, `Boolean`
- Le type scalaire personnalisé `Date` qui permet d'indiquer le format des dates attendu
- Le type `MetaList` qui représente les metas exposées
- Le type `ENUM Collection` pour récupérer les collections exposées
- Le type `Interface Node` utilisé par tous les objets. Ce type expose les champs `id`, `slug`, `typeCollection`, `points` et `rang`
- Le type `Interface Objet` utilisé par les objets éditoriaux (sauf `Auteurs` et `Syndic`). Ce type expose les champs `id`, `titre`, `texte`, `slug`, `typeCollection`, `points`, `rang` et `maj`
- Les types `MonObjet` liés aux objets éditoriaux (implémentant `Objet` ou non). Le champs `points` (et non le shampooing :p ) sert lors de la requête `recherche`. Il y a aussi le champ `slug`, le champ `type` (il existe aussi le champ `__typeName` fourni par graphQL) et le champ `rang` qui ont été ajoutés.
### c. Les requêtes disponibles
Les requêtes disponibles sont aussi affichées par les clients grâce au schéma d'introspection.
Le client JS intégré au plugin (ou votre extension navigateur) utilise ce schéma pour afficher les requêtes disponibles.
Pour le moment, les requêtes disponibles sont :
- `getMetas`
- `getCollections`
- `recherche` qui prend un paramètre `texte`
- `maCollection` et `getMonObjet` pour chaque collection exposée
Les requêtes `getMonObjet` reçoivent un `id` en paramètre et les requêtes `maCollection` peuvent recevoir les paramètres suivants :
- `where` (un tableau de string comme `['id_rubrique=1','id_trad=2']`)
- `pagination` pour indiquer le nombre d'items dans la pagination
- `page` pour indiquer la page voulue dans la pagination
## 4. Tester les requêtes
Le endpoint est joignable sur `spip.php?action=graphql`
Vous pouvez utiliser [graphiql](https://github.com/graphql/graphiql) qui est intégré dans le plugin ou une extension pour votre navigateur. Concernant Firefox, j'utilise [Altair GraphQL Client](https://addons.mozilla.org/fr/firefox/addon/altair-graphql-client/). A part pour le client intégré qui pointe directement sur votre endpoint, il faut renseigner l'url complète de votre API : http://domain.local/spip.php?action=graphql
Voici un exemple de requête graphQL que vous pouvez tester :
```
{
getMetas {
nom_site
}
articles(
where: ["id_rubrique=3", "maj>2023-05-19 11:00:00"]
page: 1
pagination: 5
) {
...objetFields
}
getArticle(id:2) {
...objetFields
auteurs (where: ["bio!=''"]) {
nom
email
bio
}
pensebetes {
...objetFields
}
}
}
fragment objetFields on Objet {
id
titre
texte
maj
}
```
Pour la recherche, voici un exemple :
```
query {
recherche(texte: "Hello") {
... on Article {
id
slug
titre
texte
maj
points
}
... on Auteur {
id
nom
points
}
}
}
```
Si vous activez le jeton, vous devez l'intégrer dans l'en-tête de vos requêtes.
Par exemple avec le client graphiql intégré dans le back-office :
![En-têtes](./captures/graphiql-headers.png)
Pour utiliser des variables dans vos clients, voici [la manière recommandée](https://www.apollographql.com/docs/react/data/operation-best-practices/#use-graphql-variables-to-provide-arguments) pour pouvoir facilement injecter, par ex, une saisie utilisateur dans la requête :
![Variables](./captures/graphiql-vars.png)
## 5. Contribuer
N'hésitez pas à contribuer : le code est extrêmement bien commenté
### Pré-requis
- Avoir un site SPIP fonctionnel avec des objets éditoriaux.
- Avoir installé [composer](https://getcomposer.org/).
- Activez le mode debug du plugin pour récupérer les erreurs PHP dans la réponse de l'API.
- En cas d'ajout ou de suppression de fichier dans le dossier `schema`, lancez la commande `composer update` pour prendre en compte vos modifications.
### Lien vers la documentation de la librairie graphql-php
https://webonyx.github.io/graphql-php/

@ -0,0 +1,90 @@
<?php
declare(strict_types=1);
if (!defined('_ECRIRE_INC_VERSION')) {
return;
}
include_spip('inc/config');
include_spip('graphql_fonctions');
include_once _DIR_PLUGIN_GRAPHQL . 'vendor/autoload.php';
use SPIP\GraphQL\SchemaSPIP;
use GraphQL\GraphQL;
use GraphQL\Type\Schema;
use GraphQL\Type\Definition\Type;
use GraphQL\Error\DebugFlag;
// Point d'entrée de l'API GraphQL
function action_graphql_dist() {
try {
// Vérification du jeton
if (!graphql_verif_jeton(getallheaders()))
throw new RuntimeException(_T('graphql:erreur_jeton'));
// Construction du schéma
// https://webonyx.github.io/graphql-php/schema-definition/#lazy-loading-of-types
$schemaSPIP = new SchemaSPIP();
$schema = new Schema([
'query' => $schemaSPIP->get('Query'),
'typeLoader' => static fn (string $name): Type => $schemaSPIP->get($name),
]);
// Récupération de la requête
$rawInput = file_get_contents('php://input');
if ($rawInput === false) {
throw new RuntimeException('Failed to get php://input');
}
$input = json_decode($rawInput, true);
$query = (is_array($input) && array_key_exists('query', $input)) ? $input['query'] : '';
$variableValues = isset($input['variables']) ? $input['variables'] : null;
if (!$query) throw new RuntimeException(_T('graphql:erreur_pas_requete'));
// Préparation de la requête
// https://webonyx.github.io/graphql-php/executing-queries/#using-facade-method
// Permet d'utiliser le resolver par défaut
// https://webonyx.github.io/graphql-php/data-fetching/#default-field-resolver
$rootValue = '';
// Contexte de la requête (pour passer des données communes à toutes les requêtes)
$contexte = [
'rootUrl' => lire_config("adresse_site"),
'request' => $_REQUEST
];
// Exécution de la requête
$output = GraphQL::executeQuery(
$schema,
$query,
$rootValue,
$contexte,
$variableValues,
);
// On vérifie si le mode debug est activé
if (in_array('true', lire_config('/meta_graphql/config/debug', []))) {
$output = $output->toArray(DebugFlag::INCLUDE_DEBUG_MESSAGE | DebugFlag::INCLUDE_TRACE);
} else {
$output = $output->toArray();
}
} catch (\Exception $e) {
// Gestion des erreurs
$output = [
'errors' => [
[
'message' => $e->getMessage()
]
]
];
}
// Envoi de la réponse
header('Content-Type: application/json');
echo json_encode(
$output,
JSON_THROW_ON_ERROR
);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

@ -0,0 +1,10 @@
{
"require": {
"webonyx/graphql-php": "^15.4"
},
"autoload": {
"classmap": [
"schema/"
]
}
}

92
composer.lock generated

@ -0,0 +1,92 @@
{
"_readme": [
"This file locks the dependencies of your project to a known state",
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "1b371bf28124abe412fa0937f28fb61f",
"packages": [
{
"name": "webonyx/graphql-php",
"version": "v15.4.0",
"source": {
"type": "git",
"url": "https://github.com/webonyx/graphql-php.git",
"reference": "99290f7945a5b39ad823f7600fa196de62597e9d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/webonyx/graphql-php/zipball/99290f7945a5b39ad823f7600fa196de62597e9d",
"reference": "99290f7945a5b39ad823f7600fa196de62597e9d",
"shasum": ""
},
"require": {
"ext-json": "*",
"ext-mbstring": "*",
"php": "^7.4 || ^8"
},
"require-dev": {
"amphp/amp": "^2.6",
"amphp/http-server": "^2.1",
"dms/phpunit-arraysubset-asserts": "^0.4",
"ergebnis/composer-normalize": "^2.28",
"mll-lab/php-cs-fixer-config": "^5.0",
"nyholm/psr7": "^1.5",
"phpbench/phpbench": "^1.2",
"phpstan/extension-installer": "^1.1",
"phpstan/phpstan": "1.10.15",
"phpstan/phpstan-phpunit": "1.3.11",
"phpstan/phpstan-strict-rules": "1.5.1",
"phpunit/phpunit": "^9.5",
"psr/http-message": "^1 || ^2",
"react/http": "^1.6",
"react/promise": "^2.9",
"rector/rector": "^0.16.0",
"symfony/polyfill-php81": "^1.23",
"symfony/var-exporter": "^5 || ^6",
"thecodingmachine/safe": "^1.3 || ^2"
},
"suggest": {
"amphp/http-server": "To leverage async resolving with webserver on AMPHP platform",
"psr/http-message": "To use standard GraphQL server",
"react/promise": "To leverage async resolving on React PHP platform"
},
"type": "library",
"autoload": {
"psr-4": {
"GraphQL\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"description": "A PHP port of GraphQL reference implementation",
"homepage": "https://github.com/webonyx/graphql-php",
"keywords": [
"api",
"graphql"
],
"support": {
"issues": "https://github.com/webonyx/graphql-php/issues",
"source": "https://github.com/webonyx/graphql-php/tree/v15.4.0"
},
"funding": [
{
"url": "https://opencollective.com/webonyx-graphql-php",
"type": "open_collective"
}
],
"time": "2023-05-11T10:26:08+00:00"
}
],
"packages-dev": [],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": [],
"prefer-stable": false,
"prefer-lowest": false,
"platform": [],
"platform-dev": [],
"plugin-api-version": "2.3.0"
}

@ -0,0 +1,14 @@
<div class="formulaire_spip formulaire_configurer formulaire_#FORM">
<h3 class="titrem"><:graphql:cfg_api:></h3>
[
<p class="reponse_formulaire reponse_formulaire_ok">(#ENV*{message_ok})</p>
] [
<p class="reponse_formulaire reponse_formulaire_erreur">(#ENV*{message_erreur})</p>
]
<form method="post" action="#ENV{action}">
<div>
#ACTION_FORMULAIRE
<div class="editer-groupe">#GENERER_SAISIES{#ENV{_saisies}}</div>
</div>
</form>
</div>

@ -0,0 +1,176 @@
<?php
if (!defined('_ECRIRE_INC_VERSION')) {
return;
}
include_spip('base/objets');
define('_GRAPHQL_OBJETS_NON_CONFIGURABLES', ['forums']);
function formulaires_configurer_collections_saisies_dist() {
$collections_non = array_merge(_GRAPHQL_OBJETS_NON_CONFIGURABLES, ['depots', 'paquets', 'plugins']);
$saisies = [];
$tables = lister_tables_objets_sql();
ksort($tables);
foreach ($tables as $table => $infos) {
$collection = $infos['table_objet'];
$champ_id = $infos['key']['PRIMARY KEY'];
if (sql_countsel($table) == '0') {
array_push($collections_non, $collection);
}
if (!in_array($collection, $collections_non)) {
$champs = [];
foreach ($infos["field"] as $nom_champ => $def) {
if (
$nom_champ != $champ_id
&& (in_array($collection, GRAPHQL_COLLECTIONS_NON_OBJET)
|| !in_array($nom_champ, GRAPHQL_CHAMPS_INTERFACE_OBJET))
) {
$champs[$nom_champ] = $nom_champ;
}
}
// On n'affiche que les tables auxiliaires finissant par "_liens"
// Qui sont autorisées et qui ont du contenu lié
$collections_autorisees = lire_config("/meta_graphql/objets_editoriaux", []);
$collections_liees = [];
foreach (lister_tables_auxiliaires() as $table_aux => $infos_aux) {
if (
preg_match("#^spip_(\w+)_liens$#", $table_aux, $matches) &&
array_key_exists($matches[1], $collections_autorisees) &&
sql_countsel($table_aux, "objet='" . objet_type($table) . "'") != 0
) {
$collections_liees[$matches[1]] = $matches[1];
}
}
$saisies_fieldset = [
[
'saisie' => 'checkbox',
'options' => [
'nom' => $collection . '_exposer',
'conteneur_class' => 'pleine_largeur',
'data' => [
'actif' => 'Actif',
],
],
],
[
'saisie' => 'input',
'options' => [
'nom' => $collection . '_pagination',
'conteneur_class' => 'pleine_largeur',
'explication' => _T('graphql:pagination'),
'explication_apres' => _T('graphql:desc_arg_pagination'),
'type' => 'number',
'defaut' => '10',
'afficher_si' => '@' . $collection . '_exposer@=="actif"',
],
],
[
'saisie' => 'selection_multiple',
'options' => [
'nom' => $collection . '_champs',
'conteneur_class' => 'pleine_largeur',
'explication' => _T('graphql:champs_objet'),
'afficher_si' => '@' . $collection . '_exposer@=="actif"',
'data' => $champs,
'multiple' => 'oui',
"cacher_option_intro" => "oui",
'size' => 5,
],
],
];
if (!empty($collections_liees)) {
$saisies_fieldset[] = [
'saisie' => 'selection_multiple',
'options' => [
'nom' => $collection . '_liaisons',
'conteneur_class' => 'pleine_largeur',
'explication' => _T('graphql:cfg_collections_liees'),
'afficher_si' => '@' . $collection . '_exposer@=="actif"',
'data' => $collections_liees,
'multiple' => 'oui',
'option_intro' => _T('graphql:aucune'),
'size' => 5,
],
];
}
$saisies_fieldset = array_merge($saisies_fieldset, [
[
'saisie' => 'select_all',
'options' => [
'nom' => $collection . '_select_all',
'conteneur_class' => 'pleine_largeur',
'afficher_si' => '@' . $collection . '_exposer@=="actif"',
],
],
[
'saisie' => 'submit',
'options' => [
'nom' => $collection . '_submit',
'conteneur_class' => 'pleine_largeur',
],
],
]);
$saisies[] = [
'saisie' => 'deplier_collection',
'options' => [
'nom' => $collection . '_deplier',
'conteneur_class' => 'pleine_largeur',
'collection' => $collection
],
];
$saisies[] = [
'saisie' => 'fieldset',
'options' => [
'nom' => $collection,
'conteneur_class' => 'config_' . $collection
],
'saisies' => $saisies_fieldset,
];
}
}
return $saisies;
}
function formulaires_configurer_collections_charger_dist() {
$valeurs = [];
$objets_editoriaux = lire_config('/meta_graphql/objets_editoriaux', array());
foreach ($objets_editoriaux as $collection => $statut) {
$valeurs[$collection . "_exposer"] = "actif";
$valeurs[$collection . "_pagination"] = $statut["pagination"];
$valeurs[$collection . "_champs"] = $statut["champs"];
$valeurs[$collection . "_liaisons"] = $statut["liaisons"];
}
return $valeurs;
}
function formulaires_configurer_collections_traiter_dist() {
$ret = [];
$objets_editoriaux = [];
foreach (lister_tables_objets_sql() as $table => $infos) {
$collection = $infos['table_objet'];
if (_request($collection . '_exposer')) {
$objets_editoriaux[$collection] = [
'pagination' => _request($collection . '_pagination'),
'champs' => _request($collection . '_champs') ? _request($collection . '_champs') : [],
'liaisons' => _request($collection . '_liaisons'),
];
}
}
if (ecrire_config('/meta_graphql/objets_editoriaux', $objets_editoriaux)) {
$ret['message_ok'] = _T('config_info_enregistree');
} else {
$ret['message_erreur'] = _T('erreur_technique_enregistrement_impossible');
}
return $ret;
}

@ -0,0 +1,20 @@
<div class="formulaire_spip formulaire_configurer formulaire_#FORM">
<h3 class="titrem"><:graphql:cfg_general:></h3>
[<p class="reponse_formulaire reponse_formulaire_ok">(#ENV*{message_ok})</p>]
[<p class="reponse_formulaire reponse_formulaire_erreur">(#ENV*{message_erreur})</p>]
<form method="post" action="#ENV{action}">
<div>
#ACTION_FORMULAIRE{#ENV{action}}
<div class="editer-groupe">
[(#SAISIE{checkbox, debug, label=<:graphql:debug_titre:>, explication=<:graphql:debug_desc:>,data=[(#ARRAY{true,<:item_oui:>})]})]
</div>
<input type="hidden" name="_meta_table" value="meta_graphql" />
<input type="hidden" name="_meta_casier" value="config" />
<p class="boutons"><span class="image_loading">&nbsp;</span><input type="submit" class="submit" value="<:bouton_enregistrer:>" /></p>
</div>
</form>
</div>

@ -0,0 +1,46 @@
<div class="formulaire_spip formulaire_configurer formulaire_#FORM">
<h3 class="titrem"><:graphql:cfg_jeton:></h3>
[<p class="reponse_formulaire reponse_formulaire_ok">(#ENV*{message_ok})</p>]
[<p class="reponse_formulaire reponse_formulaire_erreur">(#ENV*{message_erreur})</p>]
<form method="post" action="#ENV{action}">
<div>
#ACTION_FORMULAIRE{#ENV{action}}
<div class="editer-groupe">#GENERER_SAISIES{#ENV{_saisies}}</div>
<div
class="editer afficher_si_visible api_key_buttons"
data-afficher_si='afficher_si({"champ":"api_key_active","total":"","operateur":"==","valeur":"oui"})'
>
<a
class="btn tooltip"
onclick="apiKeyAction(event, 'generate')"
onmouseout="outapiKeyAction(event, 'generate')"
>
<span class="tooltiptext"><:graphql:generate:></span>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
<path
d="M142.9 142.9c62.2-62.2 162.7-62.5 225.3-1L327 183c-6.9 6.9-8.9 17.2-5.2 26.2s12.5 14.8 22.2 14.8H463.5c0 0 0 0 0 0H472c13.3 0 24-10.7 24-24V72c0-9.7-5.8-18.5-14.8-22.2s-19.3-1.7-26.2 5.2L413.4 96.6c-87.6-86.5-228.7-86.2-315.8 1C73.2 122 55.6 150.7 44.8 181.4c-5.9 16.7 2.9 34.9 19.5 40.8s34.9-2.9 40.8-19.5c7.7-21.8 20.2-42.3 37.8-59.8zM16 312v7.6 .7V440c0 9.7 5.8 18.5 14.8 22.2s19.3 1.7 26.2-5.2l41.6-41.6c87.6 86.5 228.7 86.2 315.8-1c24.4-24.4 42.1-53.1 52.9-83.7c5.9-16.7-2.9-34.9-19.5-40.8s-34.9 2.9-40.8 19.5c-7.7 21.8-20.2 42.3-37.8 59.8c-62.2 62.2-162.7 62.5-225.3 1L185 329c6.9-6.9 8.9-17.2 5.2-26.2s-12.5-14.8-22.2-14.8H48.4h-.7H40c-13.3 0-24 10.7-24 24z"
/>
</svg>
</a>
<a
id="copyAPIkeyToClipboard"
class="btn tooltip"
onclick="apiKeyAction(event, 'copy')"
onmouseout="outapiKeyAction(event, 'copy')"
>
<span class="tooltiptext"><:graphql:copier:></span>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
<path
d="M448 384H256c-35.3 0-64-28.7-64-64V64c0-35.3 28.7-64 64-64H396.1c12.7 0 24.9 5.1 33.9 14.1l67.9 67.9c9 9 14.1 21.2 14.1 33.9V320c0 35.3-28.7 64-64 64zM64 128h96v48H64c-8.8 0-16 7.2-16 16V448c0 8.8 7.2 16 16 16H256c8.8 0 16-7.2 16-16V416h48v32c0 35.3-28.7 64-64 64H64c-35.3 0-64-28.7-64-64V192c0-35.3 28.7-64 64-64z"
/>
</svg>
</a>
</div>
<input type="hidden" name="_meta_table" value="meta_graphql" />
<p class="boutons">
<span class="image_loading">&nbsp;</span>
<input type="submit" class="submit" value="<:bouton_enregistrer:>" />
</p>
</div>
</form>
</div>

@ -0,0 +1,78 @@
<?php
if (!defined('_ECRIRE_INC_VERSION')) {
return;
}
function formulaires_configurer_jeton_saisies_dist() {
$saisies = [];
$saisies[] = array(
'saisie' => 'checkbox',
'options' => array(
'nom' => 'api_key_active',
'label' => _T('graphql:activer_api_key'),
'data' => array(
'oui' => _T('graphql:activer'),
),
),
);
$saisies[] = array(
'saisie' => 'input',
'options' => array(
'nom' => "jeton",
'label' => _T('graphql:titre_configurer_jeton'),
'readonly' => 'oui',
'afficher_si' => '@api_key_active@ == "oui"',
'afficher_si_avec_post' => 'oui',
),
);
return $saisies;
}
function formulaires_configurer_jeton_charger_dist() {
$valeurs = array();
$jeton = lire_config('/meta_graphql/jeton', "");
if (empty($jeton)) {
$valeurs["api_key_active"] = "non";
$valeurs["jeton"] = graphql_generate_key();
} else {
$valeurs["api_key_active"] = "oui";
$valeurs["jeton"] = $jeton;
}
return $valeurs;
}
function formulaires_configurer_jeton_traiter_dist() {
$ret = array();
$choix = _request("api_key_active") ? _request("api_key_active") : "";
$jeton = ($choix) ? _request("jeton") : "";
if (ecrire_config('/meta_graphql/jeton', $jeton)) {
$ret['message_ok'] = _T('config_info_enregistree');
} else {
$ret['message_erreur'] = _T('erreur_technique_enregistrement_impossible');
}
// $ret['editable'] = true;
return $ret;
}
function graphql_generate_key() {
$d = time() * 1000;
if (function_exists('hrtime')) {
$hrtime = hrtime();
$d += round($hrtime[1] / 1000000);
} elseif (function_exists('microtime')) {
$microtime = microtime(true);
$d += round($microtime * 1000);
}
$uuid = sprintf('%08s-%04s-4%03x-%04x-%012s', substr($d, -8), substr($d, -12, 4), mt_rand(0, 0xfff), mt_rand(0, 0x3fff) | 0x8000, bin2hex(random_bytes(6)));
return $uuid;
}

@ -0,0 +1,18 @@
<div class="formulaire_spip formulaire_configurer formulaire_#FORM">
<h3 class="titrem"><:graphql:cfg_meta:></h3>
[<p class="reponse_formulaire reponse_formulaire_ok">(#ENV*{message_ok})</p>]
[<p class="reponse_formulaire reponse_formulaire_erreur">(#ENV*{message_erreur})</p>]
<form method="post" action="#ENV{action}">
<div>
#ACTION_FORMULAIRE
<div class="editer-groupe">
#GENERER_SAISIES{#ENV{_saisies}}
</div>
<input type="hidden" name="_meta_table" value="meta_graphql" />
<p class="boutons">
<span class="image_loading">&nbsp;</span>
<input type="submit" class="submit" value="<:bouton_enregistrer:>" />
</p>
</div>
</form>
</div>

@ -0,0 +1,47 @@
<?php
if (!defined('_ECRIRE_INC_VERSION')) {
return;
}
include_spip('base/objets');
function formulaires_configurer_meta_saisies_dist() {
$saisies = [];
$saisies[] = [
'saisie' => 'checkbox',
'options' => [
'nom' => 'meta',
'label' => _T('graphql:options_spip'),
'data' => [
'email_webmaster' => _T('email_webmaster'),
'nom_site' => _T('nom_site'),
'slogan_site' => _T('slogan_site'),
'adresse_site' => _T('adresse_site'),
'descriptif_site' => _T('descriptif_site'),
],
],
];
return $saisies;
}
function formulaires_configurer_meta_charger_dist() {
$valeurs = [];
$meta = lire_config('/meta_graphql/meta', "");
$valeurs["meta"] = $meta;
return $valeurs;
}
function formulaires_configurer_meta_traiter_dist() {
$ret = [];
$metas = is_null(_request('meta')) ? [] : _request('meta');
if (ecrire_config('/meta_graphql/meta', $metas)) {
$ret['message_ok'] = _T('config_info_enregistree');
} else {
$ret['message_erreur'] = _T('erreur_technique_enregistrement_impossible');
}
return $ret;
}

@ -0,0 +1,54 @@
<?php
if (!defined('_ECRIRE_INC_VERSION')) {
return;
}
include_once _DIR_PLUGIN_GRAPHQL . 'vendor/autoload.php';
/**
* Fonction d'installation et de mise à jour du plugin
*
* Vous pouvez :
*
* - créer la structure SQL,
* - insérer du pre-contenu,
* - installer des valeurs de configuration,
* - mettre à jour la structure SQL
*
* @param string $nom_meta_base_version
* Nom de la meta informant de la version du schéma de données du plugin installé dans SPIP
* @param string $version_cible
* Version du schéma de données dans ce plugin (déclaré dans paquet.xml)
* @return void
**/
function graphql_upgrade($nom_meta_base_version, $version_cible) {
$maj = array();
// Création de la table qui stockera la config du plugin
$maj['create'][] = array('installer_table_meta', 'meta_graphql');
// Enregistrement de valeurs par défaut dans la table de config du plugin
$maj['create'][] = array('ecrire_config', '/meta_graphql/jeton', '');
include_spip('base/upgrade');
maj_plugin($nom_meta_base_version, $version_cible, $maj);
}
/**
* Fonction de désinstallation du plugin.
*
* Vous devez :
*
* - nettoyer toutes les données ajoutées par le plugin et son utilisation
* - supprimer les tables et les champs créés par le plugin.
*
* @param string $nom_meta_base_version
* Nom de la meta informant de la version du schéma de données du plugin installé dans SPIP
* @return void
**/
function graphql_vider_tables($nom_meta_base_version) {
// Suppression de la table qui stocke la config du plugin
sql_drop_table('spip_meta_graphql');
// Effacement de la version du schema du plugin dans la table spip_meta
effacer_meta($nom_meta_base_version);
}

@ -0,0 +1,17 @@
<?php
if (!defined('_ECRIRE_INC_VERSION')) {
return;
}
// Obligatoire
function graphql_autoriser() {
}
function autoriser_configurergraphql_menu_dist($faire, $type, $id, $qui, $opt) {
return ($qui['webmestre'] == 'oui');
}
function autoriser_graphql_dist($faire, $type, $id, $qui, $opt) {
return ($qui['webmestre'] == 'oui');
}

@ -0,0 +1,36 @@
<?php
if (!defined('_ECRIRE_INC_VERSION')) {
return;
}
function graphql_verif_jeton($headers) {
$token_param = lire_config('/meta_graphql/jeton', '');
if ($token_param == '') {
return true;
}
$token_envoye = '';
if (isset($headers["X_AUTH_TOKEN"])) {
$token_envoye = $headers["X_AUTH_TOKEN"];
}
return ($token_envoye == $token_param);
}
function graphql_getCollectionInfos(string $collection): array {
// spip_articles
$infos['table_collection'] = table_objet_sql($collection);
// infos SQL de la table
$infos['table_infos'] = lister_tables_objets_sql($infos['table_collection']);
// Champs SQL de la table
$infos['champs'] = $infos['table_infos']['field'];
// id_article
$infos['champ_id'] = id_table_objet($infos['table_collection']);
// article
$infos['type_objet'] = objet_type($collection);
// Article
$infos['nameObjet'] = ucfirst($infos['type_objet']);
return $infos;
}

@ -0,0 +1,9 @@
<?php
if (!defined('_ECRIRE_INC_VERSION')) {
return;
}
define('GRAPHQL_COLLECTIONS_NON_OBJET', ['auteurs', 'syndic', 'documents']);
define('GRAPHQL_CHAMPS_COMMUNS', ['id', 'slug', 'typeCollection', 'points', 'rang']);
define('GRAPHQL_CHAMPS_INTERFACE_OBJET', ['titre', 'texte', 'maj']);

@ -0,0 +1,19 @@
<?php
if (!defined('_ECRIRE_INC_VERSION')) {
return;
}
function graphql_header_prive($flux) {
$flux .= recuperer_fond('prive/js/graphql');
$flux .= '<script
src="https://unpkg.com/react@17/umd/react.development.js"
integrity="sha512-Vf2xGDzpqUOEIKO+X2rgTLWPY+65++WPwCHkX2nFMu9IcstumPsf/uKKRd5prX3wOu8Q0GBylRpsDB26R6ExOg=="
crossorigin="anonymous"></script>';
$flux .= '<script
src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"
integrity="sha512-Wr9OKCTtq1anK0hq5bY3X/AvDI5EflDSAh0mE9gma+4hl+kXdTJPKZ3TwLMBcrgUeoY0s3dq9JjhCQc7vddtFg=="
crossorigin="anonymous"></script>';
$flux .= '<link rel="stylesheet" href="https://unpkg.com/graphiql/graphiql.min.css" />';
return $flux;
}

@ -0,0 +1,71 @@
<?php
if (!defined('_ECRIRE_INC_VERSION')) {
return;
}
$GLOBALS[$GLOBALS['idx_lang']] = array(
// A
'activer' => 'Activer',
'activer_api_key' => 'Clé API',
'aucune' => 'Aucune',
// C
"cfg_api" => 'Sélectionner les collections à exposer sur l\'API',
'cfg_collections_liees' => 'Sélectionner les collections liées à exposer',
'cfg_general' => 'Réglagles généraux',
'cfg_jeton' => 'Sécurisez votre API',
'cfg_meta' => 'Sélectionner les metas à exposer sur l\'API',
'champs_objet' => 'Sélectionner les champs que vous souhaitez exposer sur l\'API',
'collection' => 'Collection',
'copier' => 'Copier dans le presse-papier',
// D
'debug_titre' => 'Mode Debug',
'debug_desc' => 'Afficher les retours d\'erreur',
'desc_arg_id' => 'ID de l\'objet',
'desc_arg_page' => 'Page retournée pour la pagination',
'desc_arg_pagination' => 'Nombre d\'objets maximal retournés',
'desc_arg_texte' => 'Votre recherche',
'desc_arg_where' => 'Clause WHERE de votre requête sous forme de tableau (AND)',
'desc_query_collection' => 'Retourne une collection d\'objets',
'desc_query_collections' => 'Retourne la liste des collections disponibles',
'desc_query_getmeta' => 'Retourne les métas autorisées',
'desc_query_objet' => 'Retourne un objet',
'desc_query_recherche' => 'Résultats de recherche sur les objets',
'desc_type_base' => 'Type de base permettant de partager des champs',
'desc_type_collection' => 'Énumération des collections disponibles',
'desc_type_date' => 'Format : AAAA-MM-JJ HH:MM:SS',
'desc_type_interface' => 'Un objet éditorial de SPIP',
'desc_type_metalist' => 'Métas autorisées',
'desc_type_objet' => 'Un objet',
'desc_type_query' => 'Liste des requêtes disponibles',
'desc_type_searchresult' => 'Résultats de recherche',
// E
'erreur_cache' => 'Le schéma d\'introspection est absent. Veuillez exposer des données',
'erreur_clause_where' => 'Erreur dans la clause where',
'erreur_pas_requete' => 'Aucune requête',
'erreur_jeton' => 'Mauvais Jeton API',
// G
'generate' => 'Re-générer',
// O
'options_spip' => 'Options SPIP',
// P
'pagination' => 'Pagination',
// S
'select_all' => 'Tout sélectionner',
// T
'titre_configurer_jeton' => 'Jeton API',
'titre_graphiql' => 'GraphiQL IDE',
'titre_graphql_configurer' => 'Réglages',
'titre_graphql_expositions' => 'Données à exposer',
'titre_graphql_jeton' => 'Jeton API',
'titre_page_configurer_graphql' => 'GraphQL',
'type_meta_desc' => 'Retourne les metas autorisées'
);

@ -0,0 +1,11 @@
<?php
if (!defined('_ECRIRE_INC_VERSION')) {
return;
}
$GLOBALS[$GLOBALS['idx_lang']] = array(
// C
'graphql_nom' => 'SPIP GraphQL',
'graphql_slogan' => 'Permet d\'exposer un endpoint GraphQL',
);

@ -0,0 +1,27 @@
<paquet
prefix="graphql"
categorie="outil"
version="0.1.0"
etat="dev"
compatibilite="[4.0.0;4.2.*]"
logo="prive/themes/spip/images/graphql-64.png"
documentation=""
schema="1.0.0"
>
<nom>Endpoint graphQL</nom>
<auteur mail="paidge_cs@hotmail.com">Pierre-jean CHANCELLIER</auteur>
<licence>GNU/GPL v3</licence>
<necessite nom="saisies" compatibilite="[4.7.1;]" />
<menu nom="graphql" titre="graphql:titre_page_configurer_graphql" icone="images/graphql-16.png" parent="menu_publication" action="graphiql" />
<onglet nom="graphql_ide" titre="graphql:titre_graphiql" icone="" parent="graphql" action="graphiql" />
<onglet nom="graphql_configurer" titre="graphql:titre_graphql_configurer" icone="" parent="graphql" action="configurer_graphql" />
<onglet nom="graphql_expositions" titre="graphql:titre_graphql_expositions" icone="" parent="graphql" action="graphql_expositions" />
<pipeline nom="header_prive" inclure="graphql_pipelines.php" />
<pipeline nom="autoriser" inclure="graphql_autorisations.php" />
</paquet>

@ -0,0 +1,70 @@
<script type="text/javascript">
// Blocs dépliables
$(function() {
$('.formulaire_configurer_collections .deplier_collection').on('click', function(e) {
e.preventDefault()
const collection = $(this).data('collection')
$('.config_' + collection + ' > fieldset').toggleClass('ouvert')
})
});
function generateUUID() {
var d = new Date().getTime()
if (window.performance && typeof window.performance.now === "function") {
d += performance.now()
}
var uuid = "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function (c) {
var r = (d + Math.random() * 16) % 16 | 0
d = Math.floor(d / 16)
return (c == "x" ? r : (r & 0x3) | 0x8).toString(16)
})
return uuid
}
function apiKeyAction(e, action) {
var jetonField = document.getElementById("champ_jeton")
var tooltip = $(e.target).children("span.tooltiptext")[0]
switch (action) {
case "copy":
jetonField.select()
jetonField.setSelectionRange(0, 99999) // For mobile devices
if (window.isSecureContext && navigator.clipboard) {
navigator.clipboard.writeText(content)
} else {
document.execCommand("copy")
}
$(tooltip).text("<?=_T('graphql:copied') ?>")
break
case "generate":
var text = generateUUID()
$(jetonField).val(text)
$(tooltip).text("<?=_T('graphql:generated') ?>")
break
}
}
function outapiKeyAction(e, action) {
var tooltip = $(e.target).children("span.tooltiptext")[0]
switch (action) {
case "copy":
$(tooltip).text("<?=_T('graphql:copier') ?>")
break
case "generate":
$(tooltip).text("<?=_T('graphql:generate') ?>")
break
}
}
function selectAllFields(e) {
e.preventDefault()
$(e.target).parentsUntil("fieldset").find("select option").prop("selected", true)
}
</script>

@ -0,0 +1,4 @@
[(#AUTORISER{graphql}|sinon_interdire_acces)]
[(#VAL{graphql}|barre_onglets{graphql_configurer})]
<div class="ajax">#FORMULAIRE_CONFIGURER_JETON</div>
<div class="ajax">#FORMULAIRE_CONFIGURER_GRAPHQL</div>

@ -0,0 +1,17 @@
[(#AUTORISER{graphql}|sinon_interdire_acces)]
[(#VAL{graphql}|barre_onglets{graphql_ide})]
<div id="graphiql">Loading...</div>
<script
src="https://unpkg.com/graphiql/graphiql.min.js"
type="application/javascript"></script>
<script>
ReactDOM.render(
React.createElement(GraphiQL, {
fetcher: GraphiQL.createFetcher({
url: "/spip.php?action=graphql",
}),
defaultEditorToolsVisibility: true,
}),
document.getElementById("graphiql")
)
</script>

@ -0,0 +1,5 @@
[(#AUTORISER{graphql}|sinon_interdire_acces)]
[(#VAL{graphql}|barre_onglets{graphql_expositions})]
<div>#FORMULAIRE_CONFIGURER_COLLECTIONS</div>
<div class="ajax">#FORMULAIRE_CONFIGURER_META</div>

@ -0,0 +1 @@
#LARGEUR_ECRAN{pleine_largeur}

@ -0,0 +1,152 @@
[(#REM)
Ce squelette definit les styles de l'espace prive
Note: l'entete "Vary:" sert a repousser l'entete par
defaut "Vary: Cookie,Accept-Encoding", qui est (un peu)
genant en cas de "rotation du cookie de session" apres
un changement d'IP (effet de clignotement).
ATTENTION: il faut absolument le charset sinon Firefox croit que
c'est du text/html !
<style>
]
#CACHE{3600*100,cache-client}
#HTTP_HEADER{Content-Type: text/css; charset=iso-8859-15}
#HTTP_HEADER{Vary: Accept-Encoding}
#graphiql {
min-height: 450px;
height: 70vh;
}
#graphiql .graphiql-doc-explorer-argument,
#graphiql .graphiql-doc-explorer-item {
background: var(--spip-color-theme-lightest);
padding: 0.5rem 1rem;
margin-top: 5px;
border-radius: 7px;
}
.graphiql-markdown-description {
font-style: italic;
color: hsla(var(--spip-color-theme-light--hsl),1);
}
/* Containers */
#graphiql .graphiql-container {
--color-primary: var(--spip-color-theme-dark--hsl);
--editor-background: hsla(var(--spip-color-theme-light--hsl),var(--alpha-background-light));
}
#graphiql .graphiql-container .graphiql-sessions {
--color-neutral: var(--spip-color-theme-darker--hsl);
}
/* Boutons */
#graphiql .graphiql-tab.graphiql-tab-active {
background-color: unset;
}
#graphiql button.graphiql-tab-button {
padding: var(--px-4) var(--px-8);
}
#graphiql button.graphiql-un-styled {
background-color: var(--spip-color-gray-lighter);
color: var(--spip-color-theme-dark);
}
#graphiql button.graphiql-un-styled.active {
background-color: var(--spip-color-theme-dark);
color: var(--spip-color-white);
}
/* svg icons */
#graphiql button.graphiql-tab-add > svg,
#graphiql .graphiql-container .graphiql-chevron-icon,
#graphiql .graphiql-toolbar-icon {
--color-neutral: var(--spip-color-theme-dark--hsl);
}
/* Editeur de texte */
#graphiql .cm-s-graphiql .cm-punctuation {
--color-neutral: var(--spip-color-theme--80);
}
#graphiql .cm-s-graphiql .CodeMirror-linenumber {
--color-neutral: var(--spip-color-theme--70);
}
.loading * {
pointer-events: none;
user-select: none;
}
.api_key_buttons > .btn > svg {
pointer-events: none;
fill: white;
width: 20px;
}
.tooltip {
position: relative;
}
.tooltip .tooltiptext {
visibility: hidden;
width: 140px;
background-color: #555;
color: #fff;
text-align: center;
border-radius: 6px;
padding: 5px;
position: absolute;
z-index: 10;
bottom: 150%;
left: 50%;
margin-left: -75px;
opacity: 0;
transition: opacity 0.3s;
}
.tooltip .tooltiptext::after {
content: "";
position: absolute;
top: 100%;
left: 50%;
margin-left: -5px;
border-width: 5px;
border-style: solid;
border-color: #555 transparent transparent transparent;
}
.tooltip:hover .tooltiptext {
visibility: visible;
opacity: 1;
}
.formulaire_spip div.choix.choix_actif {
border: 0;
padding: 0 0 0 var(--spip-form-input-padding-x);
}
/* Boîtes dépliables */
.formulaire_configurer_collections .avec_sous_saisies > fieldset {
display: grid;
grid-template-rows: 0fr;
transition: grid-template-rows 1s ease;
border: none;
margin: 0;
padding-top: 0;
padding-bottom: 0;
}
.formulaire_configurer_collections .avec_sous_saisies > fieldset.ouvert {
grid-template-rows: 1fr;
}
.formulaire_configurer_collections .avec_sous_saisies > fieldset > .editer-groupe {
overflow: hidden;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 581 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

@ -0,0 +1,3 @@
[<button class="deplier_collection" data-collection="#ENV{collection}">
<:graphql:collection:>&nbsp;(#ENV{collection}|majuscules)
</button>]

@ -0,0 +1 @@
<button class="btn" onclick="selectAllFields(event)"><:graphql:select_all:></button>

@ -0,0 +1,4 @@
<p class="boutons">
<span class="image_loading">&nbsp;</span>
<input type="submit" class="submit" value="<:bouton_enregistrer:>" />
</p>

@ -0,0 +1,11 @@
Exposer dynamiquement chaque collection, ainsi que ses champs, sélectionnés dans le navigateur.
- Le fichier `SchemaSPIP.php` gère toute la logique de construction du schéma
- Le fichier `ReponseSPIP.php` gère les fonctions utilisées dans les réponses.
## TODO
- [x] gérer les collections liées
- [x] Voir pour la requête concernant la recherche
- [x] créer un type de base pour tous les objets éditoriaux avec les champs en commun
- [x] Voir pour mettre en place le schéma en [lazy loading](https://webonyx.github.io/graphql-php/schema-definition/#lazy-loading-of-types) grâce à un `TypeRegistry`

@ -0,0 +1,278 @@
<?php
declare(strict_types=1);
namespace SPIP\GraphQL;
class ReponseSPIP {
public static function findMeta(): array {
$metas = lire_config("/meta_graphql/meta", []);
$where = "nom IN ('" . implode("','", $metas) . "')";
$liste_meta_publique = sql_allfetsel("nom, valeur", "spip_meta", $where);
$retourMeta = [];
if (!empty($liste_meta_publique)) {
foreach ($liste_meta_publique as $meta) {
$retourMeta[$meta["nom"]] = $meta["valeur"];
}
}
return $retourMeta;
}
public static function recherche(string $texte): array {
$retour = [];
$collections_autorisees = lire_config('/meta_graphql/objets_editoriaux');
foreach ($collections_autorisees as $collection => $config) {
$result = self::searchCollection($collection, $texte);
$retour = array_merge(
$retour,
$result
);
}
return $retour;
}
public static function afficheCollections(): array {
$collections_autorisees = lire_config('/meta_graphql/objets_editoriaux');
$reponse = [];
foreach ($collections_autorisees as $collection => $config) {
$reponse[] = strtoupper($collection);
}
return $reponse;
}
// Récupération d'une collection d'objets
public static function findCollection(string $collection, array $where, int $pagination, int $page = 0): ?array {
$retour_objets = [];
$table = table_objet_sql($collection);
$offset = $page * $pagination;
$objets = sql_allfetsel(
self::getSelect($table),
$table . ' AS collection',
self::getWhere($table, $where),
'',
'',
"$offset,$pagination"
);
foreach ($objets as $objet) {
$retour_objets[] = self::findObjet((int) $objet['id'], $collection, $objet);
}
return $retour_objets;
}
// Récupération d'un objet
public static function findObjet(int $id, string $type_objet, array $objet = []): ?array {
include_spip('inc/filtres');
$collection = table_objet($type_objet);
if (preg_match('#^get(\w+)$#', $collection, $matches)) {
$collection = strtolower($matches[1]);
}
$type_objet = objet_type($collection);
$champ_id = id_table_objet($collection);
if (empty($objet)) {
$table = table_objet_sql($collection);
$where = [$champ_id . "=" . $id];
$objet = sql_fetsel(
self::getSelect($table),
$table . ' AS collection',
self::getWhere($table, $where)
);
}
if (!$objet) return [];
// Champ de base pour créer le champ slug
$slug_origin = self::champSlug($collection);
// Récupération des champs
foreach ($objet as $champ => $value) {
switch ($champ) {
case $champ_id:
$objet['id'] = $value;
break;
case "titre":
$objet[$champ] = supprimer_numero($value);
$objet['rang'] = (preg_match("#^([0-9]+)[.][[:space:]]#", $value, $matches)) ?
$matches[1] : '0';
break;
case "fichier":
$objet[$champ] = url_absolue(_DIR_IMG . $value);
break;
case "saisies":
// TODO : transformer le tableau de saisies en html
$objet[$champ] = $value;
break;
case "texte":
case "surtitre":
case "soustitre":
case "descriptif":
case "chapo":
case "bio":
case "credits":
// Transformation des liens et des balises
$objet[$champ] = liens_absolus(trim(str_replace("\n", '<br>', propre($value))));
break;
// Gestion des laisions SQL 1 => N
case "id_secteur":
$objet['secteur'] = ($collection != 'rubriques' || $value != $id) ?
self::findObjet((int) $value, 'rubrique') :
null;
break;
case "id_rubrique":
$objet['rubrique'] = self::findObjet((int) $value, 'rubrique');
break;
case "id_trad":
case "id_parent":
$champ = preg_replace('#(id_)#', '', $champ);
$objet[$champ] = ($value !== 0) ? self::findObjet((int) $value, $type_objet) : null;
break;
case "id_vignette":
$objet['vignette'] = ($value !== 0) ? self::findObjet((int) $value, 'document') : null;
break;
case "id_groupe":
$objet['groupe'] = ($value !== 0) ? self::findObjet((int) $value, 'groupe_mots') : null;
break;
default:
$objet[$champ] = $value;
break;
}
// Récupération du slug de l'objet
if ($champ == $slug_origin) {
$objet['slug'] = identifiant_slug(supprimer_numero($value));
}
}
$objet['typeCollection'] = strtoupper($collection);
// Récupération du logo de l'objet s'il existe
$chercher_logo = charger_fonction('chercher_logo', 'inc');
if ($logo = $chercher_logo($id, $type_objet, 'on')) {
if (!is_null($logo[0])) {
$objet['logo'] = lire_config("adresse_site") . "/" . $logo[0];
}
}
// Récupération des collections liées (liaisons SQL N => N)
$collections_autorisees = lire_config("/meta_graphql/objets_editoriaux");
$config_collection = $collections_autorisees[table_objet($type_objet)];
$liaisons_config = (array_key_exists('liaisons', $config_collection) &&
is_array($config_collection['liaisons'])) ? $config_collection['liaisons'] : [];
foreach ($liaisons_config as $collection_liee) {
include_spip('action/editer_liens');
$objet[$collection_liee] = [];
$table_liee = table_objet_sql($collection_liee);
$type_objet_lie = objet_type($table_liee);
$cle_collection_liee = id_table_objet($table_liee);
if (
array_key_exists($collection_liee, $collections_autorisees)
&& $liaison_col = objet_trouver_liens([$type_objet_lie => '*'], [$type_objet => $id])
) {
foreach ($liaison_col as $l) {
$ids[] = $l[$cle_collection_liee];
}
$where = [sql_in($cle_collection_liee, $ids)];
$objet[$collection_liee] = self::findCollection(
$collection_liee,
$where,
(int) $collections_autorisees[$collection_liee]['pagination']
);
}
}
return $objet;
}
public static function searchCollection(string $collection, string $recherche, array $where = []): array {
include_spip('inc/prepare_recherche');
include_spip('inc/filtres');
$retour_recherche = [];
// Préparation et exécution de la requête
$table = table_objet_sql($collection);
$select = self::getSelect($table);
$prepare_requete = inc_prepare_recherche_dist($recherche, $table);
$select[] = $prepare_requete[0];
$from = [$table . ' as collection', table_objet_sql("resultats") . " as resultats",];
$where[] = $prepare_requete[1];
$where[] = 'collection.' . id_table_objet($table) . '=resultats.id';
$result = sql_allfetsel($select, $from, $where, '', ['points DESC']);
foreach ($result as $objet) {
$retour_recherche[] = self::findObjet((int) $objet['id'], $collection, $objet);
}
return $retour_recherche;
}
public static function champSlug(string $collection) {
switch ($collection) {
case "auteurs":
return "nom";
break;
case "syndic":
return "nom_site";
break;
default:
return "titre";
}
}
private static function getSelect(string $table): array {
$select = ['collection.' . id_table_objet($table) . ' as id'];
if (!in_array(table_objet($table), GRAPHQL_COLLECTIONS_NON_OBJET)) {
foreach (GRAPHQL_CHAMPS_INTERFACE_OBJET as $champ_interface) {
$select[] = 'collection.' . $champ_interface;
}
}
$select = array_merge(
$select,
lire_config('/meta_graphql/objets_editoriaux/' . table_objet($table) . '/champs', [])
);
return $select;
}
private static function getWhere(string $table, array $where = []): array {
$table_infos = lister_tables_objets_sql($table);
$retour_where = [];
if (!empty($where)) {
// Format des dates : 2023-05-19 11:00:00
foreach ($where as $clause) {
if (preg_match('#^(\w+)(=|<|>|<>)((?:\w+|\s|:|-)+)$#', $clause, $matches)) {
$retour_where[] = 'collection.' . $matches[1] . $matches[2] . sql_quote($matches[3]);
} else {
$retour_where[] = $clause;
}
}
}
if ($table != "spip_auteurs" && array_key_exists("statut", $table_infos["field"])) {
$retour_where[] = "collection.statut=" . sql_quote('publie');
}
return $retour_where;
}
}

@ -0,0 +1,397 @@
<?php
declare(strict_types=1);
namespace SPIP\GraphQL;
use GraphQL\Type\Definition\Type;
use GraphQL\Type\Definition\CustomScalarType;
use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\InterfaceType;
use GraphQL\Type\Definition\EnumType;
use GraphQL\Type\Definition\UnionType;
use GraphQL\Type\Definition\ListOfType;
use GraphQL\Type\Definition\NonNull;
use GraphQL\Type\Definition\ResolveInfo;
// Permet de créer des types ré-utilisables dans l'API
// https://webonyx.github.io/graphql-php/schema-definition/#lazy-loading-of-types
class SchemaSPIP {
public array $collections_autorisees = [];
public array $metas_autorisees = [];
private array $types = [];
public function __construct() {
$this->collections_autorisees = lire_config('/meta_graphql/objets_editoriaux', []);
$this->metas_autorisees = lire_config('/meta_graphql/meta', []);
}
// Query, Date, MetaList, Article, SearchResult...
public function get(string $name): Type {
return $this->types[$name] ??= $this->getType($name);
}
// Pour créer un nouveau Type de données, c'est par ici
private function getType(string $name) {
$typeDefinition = ['name' => $name];
switch ($name) {
case 'Node':
// Interface Objet pour mutualiser des champs entre objets
$typeDefinition['description'] = _T('graphql:desc_type_base');
$typeDefinition['fields'] = function () {
return $this->champsCommuns(GRAPHQL_CHAMPS_COMMUNS);
};
$typeDefinition['resolveType'] = function ($value, $context, ResolveInfo $info) {
// TODO : écrire le resolveType
// switch ($info->fieldDefinition->name ?? null) {
// case 'human': return MyTypes::human();
// case 'droid': return MyTypes::droid();
// default: throw new Exception("Unknown Character type: {$value->type ?? null}");
// }
};
return new InterfaceType($typeDefinition);
break;
break;
case 'Collection':
// Liste des collections exposées
$typeDefinition['description'] = _T('graphql:desc_type_collection');
foreach ($this->collections_autorisees as $collection => $config) {
$typeDefinition['values'][] = strtoupper($collection);
}
return new EnumType($typeDefinition);
break;
case 'Date':
// Type scalaire personnalisé
// https://webonyx.github.io/graphql-php/type-definitions/scalars/#writing-custom-scalar-types
// AAAA-MM-JJ HH:MM:SS
$typeDefinition['description'] = _T('graphql:desc_type_date');