API en inglés: /animals, /species, campo profile (lectura y escritura) y GraphQL
- REST: rutas animals/species, campo «profile» con nombres y valores en inglés
(available/pending/adopted, male/female…), escribible con validación; los datos
comunes se copian a las traducciones y los textos no. Metadatos internos ocultos.
- GraphQL: animals / speciesList, tipos AnimalProfile y enums, filtros
where { language, adoptionStatus }.
- Las peticiones a la API usan el idioma inglés (descripciones, errores).
- Esquema OpenAPI: componente AnimalProfile y cuerpos JSON documentados.
- La web y sus URLs siguen en castellano/euskera; los datos internos no cambian.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
44
README.md
44
README.md
@@ -68,25 +68,51 @@ Cada ficha tiene su propio enlace y botones para compartirla por WhatsApp o Face
|
||||
|
||||
- En *Diseño → Cabecera* se elige cómo sale el logo arriba: símbolo + nombre, logo completo o solo el nombre.
|
||||
|
||||
## APIs
|
||||
## APIs (in English)
|
||||
|
||||
| API | Dirección | Qué incluye |
|
||||
The API is in English (routes, fields, values and messages), whatever the site language. The public website keeps its Spanish/Basque URLs (`/adopta/`, `/especie/perro/`).
|
||||
|
||||
| API | Endpoint | Notes |
|
||||
|---|---|---|
|
||||
| REST | `/wp-json/wp/v2/animales`, `/wp-json/wp/v2/especies` | Animales con el campo `ficha` (valores y textos legibles en su idioma). Se puede filtrar por idioma con `?lang=es` o `?lang=eu`. |
|
||||
| GraphQL (WPGraphQL) | `/graphql` | Tipos `Animal` / `Especie` con el campo `ficha`. Explorador GraphiQL en el panel (menú GraphQL). |
|
||||
| Swagger / OpenAPI 3 | `/rest-api/docs/` (interfaz) y `/rest-api/schema` (esquema) | Documentación interactiva de la API REST. Por defecto solo para usuarios conectados; `API_DOCS_PUBLIC=true` la hace pública. |
|
||||
| REST | `/wp-json/wp/v2/animals`, `/wp-json/wp/v2/species` | Every animal has a `profile` object (read **and write**). Filter by language with `?lang=es` / `?lang=eu`. |
|
||||
| GraphQL (WPGraphQL) | `/graphql` | `animals(where: { language: "eu", adoptionStatus: AVAILABLE })`, `speciesList`, type `AnimalProfile`. GraphiQL IDE in the dashboard. |
|
||||
| OpenAPI 3 / Swagger | `/rest-api/docs/` (UI), `/rest-api/schema` (JSON) | Logged-in users only unless `API_DOCS_PUBLIC=true`. «Try it out» uses your WordPress session. |
|
||||
|
||||
Ejemplo de consulta GraphQL:
|
||||
`profile` fields:
|
||||
|
||||
| Field | Type | Values / notes |
|
||||
|---|---|---|
|
||||
| `status` | string | `available`, `pending`, `adopted` (adopted animals are hidden from public listings) |
|
||||
| `featured` | boolean | shown on the home page |
|
||||
| `sex` | string | `male`, `female` |
|
||||
| `birthDate`, `arrivalDate` | date `YYYY-MM-DD` | nullable |
|
||||
| `age`, `ageUnit` | number, `years`/`months` | approximate age when the birth date is unknown |
|
||||
| `size` | string | `small`, `medium`, `large` (dogs) |
|
||||
| `galleryIds` | int[] | extra photos (attachment IDs); `photos` (read-only) = main photo + gallery URLs |
|
||||
| `video` | URL | YouTube, Vimeo, Instagram or a media file |
|
||||
| `personality`, `needs`, `compatibility`, `idealHome` | string | **per language** (each translation has its own) |
|
||||
| `language`, `labels` | read-only | language of the translation and human-readable values in that language |
|
||||
|
||||
Shared fields (status, dates, size, photos…) are copied to every translation when written; texts are not.
|
||||
|
||||
```graphql
|
||||
{
|
||||
animales(first: 10) {
|
||||
nodes { title uri especies { nodes { name } } ficha { estado edad edadUnidad compatible textos { estado edad } } }
|
||||
animals(first: 10, where: { language: "es", adoptionStatus: AVAILABLE }) {
|
||||
nodes { title uri language speciesList { nodes { name } } profile { status sex featured size personality labels { age } } }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Para escribir en la API REST (crear o editar animales) se usan las *contraseñas de aplicación* de WordPress (Usuarios → Perfil) con autenticación Basic sobre HTTPS; Swagger lo admite desde el botón «Authorize». Los mensajes del formulario no se exponen en ninguna API.
|
||||
```bash
|
||||
# Update an animal (application password: Users → Profile → Application passwords)
|
||||
curl -u 'user:xxxx xxxx xxxx xxxx' -H 'Content-Type: application/json' \
|
||||
-d '{"profile":{"status":"pending","featured":true}}' \
|
||||
https://gureametsabilagun.pireon.dev/wp-json/wp/v2/animals/123
|
||||
```
|
||||
|
||||
Contact-form messages are never exposed by any API.
|
||||
|
||||
> Note: the *WP API SwaggerUI* package on wordpress.org ships without its UI assets, so `/rest-api/docs/` is served by our own page (official Swagger UI, pinned version with SRI). The plugin is still used to generate the OpenAPI schema, which we post-process to document `profile`.
|
||||
|
||||
## Diseño
|
||||
|
||||
|
||||
@@ -37,10 +37,10 @@ add_action( 'init', function () {
|
||||
'menu_position' => 5,
|
||||
'supports' => array( 'title', 'editor', 'thumbnail', 'excerpt' ),
|
||||
'show_in_rest' => true,
|
||||
'rest_base' => 'animales',
|
||||
'rest_base' => 'animals',
|
||||
'show_in_graphql' => true,
|
||||
'graphql_single_name' => 'animal',
|
||||
'graphql_plural_name' => 'animales',
|
||||
'graphql_plural_name' => 'animals',
|
||||
) );
|
||||
|
||||
register_taxonomy( 'especie', 'animal', array(
|
||||
@@ -52,10 +52,10 @@ add_action( 'init', function () {
|
||||
'hierarchical' => true,
|
||||
'show_admin_column' => true,
|
||||
'show_in_rest' => true,
|
||||
'rest_base' => 'especies',
|
||||
'rest_base' => 'species',
|
||||
'show_in_graphql' => true,
|
||||
'graphql_single_name' => 'especie',
|
||||
'graphql_plural_name' => 'especies',
|
||||
'graphql_single_name' => 'species',
|
||||
'graphql_plural_name' => 'speciesList',
|
||||
'rewrite' => array( 'slug' => 'especie', 'with_front' => false ),
|
||||
) );
|
||||
|
||||
@@ -78,10 +78,11 @@ add_action( 'init', function () {
|
||||
) );
|
||||
|
||||
foreach ( array_keys( ga_fields() ) as $key ) {
|
||||
// Los metadatos internos no se exponen tal cual: la API usa el campo «profile» (en inglés, ver api.php).
|
||||
register_post_meta( 'animal', "_ga_$key", array(
|
||||
'type' => 'string',
|
||||
'single' => true,
|
||||
'show_in_rest' => true,
|
||||
'show_in_rest' => false,
|
||||
'auth_callback' => fn() => current_user_can( 'edit_posts' ),
|
||||
) );
|
||||
}
|
||||
|
||||
@@ -1,110 +1,354 @@
|
||||
<?php
|
||||
/**
|
||||
* APIs: REST (/wp-json/wp/v2/animales) y GraphQL (/graphql). Documentación Swagger protegida.
|
||||
* Public API, in English: REST (/wp-json/wp/v2/animals, /wp-json/wp/v2/species) and GraphQL (/graphql).
|
||||
*
|
||||
* Internally the data keeps its original (Spanish) meta keys; this layer maps them to English
|
||||
* names and values so API consumers get a conventional, stable contract.
|
||||
*/
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
/** Ficha de un animal lista para las APIs: valores internos + textos legibles en su idioma. */
|
||||
/** Is this request to one of the APIs (REST, GraphQL, OpenAPI schema/docs)? */
|
||||
function ga_is_api_request() {
|
||||
static $is = null;
|
||||
if ( null === $is ) {
|
||||
$uri = (string) ( $_SERVER['REQUEST_URI'] ?? '' );
|
||||
$path = (string) wp_parse_url( $uri, PHP_URL_PATH );
|
||||
$is = (bool) preg_match( '#^/(?:eu/)?(?:wp-json|graphql|rest-api/(?:schema|docs))(?:/|$)#', $path )
|
||||
|| isset( $_GET['rest_route'] ) || isset( $_GET['graphql'] );
|
||||
}
|
||||
return $is;
|
||||
}
|
||||
|
||||
/* The API speaks English (field descriptions, errors, core schema), whatever the site language. */
|
||||
foreach ( array( 'determine_locale', 'locale' ) as $ga_locale_hook ) {
|
||||
add_filter( $ga_locale_hook, fn( $locale ) => ga_is_api_request() ? 'en_US' : $locale, 1000 );
|
||||
}
|
||||
|
||||
/** Internal value => API value, per field. */
|
||||
function ga_api_enums() {
|
||||
return array(
|
||||
'estado' => array( 'disponible' => 'available', 'proceso' => 'pending', 'adoptado' => 'adopted' ),
|
||||
'sexo' => array( 'macho' => 'male', 'hembra' => 'female' ),
|
||||
'tamano' => array( 'pequeno' => 'small', 'mediano' => 'medium', 'grande' => 'large' ),
|
||||
'edad_unidad' => array( 'anos' => 'years', 'meses' => 'months' ),
|
||||
);
|
||||
}
|
||||
|
||||
/** API property => internal field. Shared fields are common to every translation of the animal. */
|
||||
function ga_api_map() {
|
||||
return array(
|
||||
'status' => 'estado',
|
||||
'featured' => 'destacado',
|
||||
'sex' => 'sexo',
|
||||
'birthDate' => 'nacimiento',
|
||||
'age' => 'edad',
|
||||
'ageUnit' => 'edad_unidad',
|
||||
'size' => 'tamano',
|
||||
'arrivalDate' => 'llegada',
|
||||
'galleryIds' => 'galeria',
|
||||
'video' => 'video',
|
||||
'personality' => 'caracter',
|
||||
'needs' => 'necesidades',
|
||||
'compatibility' => 'convivencia',
|
||||
'idealHome' => 'hogar',
|
||||
);
|
||||
}
|
||||
|
||||
function ga_api_to( $field, $value ) {
|
||||
return ga_api_enums()[ $field ][ $value ] ?? ( '' === $value ? null : $value );
|
||||
}
|
||||
|
||||
function ga_api_from( $field, $value ) {
|
||||
$flip = array_flip( ga_api_enums()[ $field ] ?? array() );
|
||||
return $flip[ $value ] ?? null;
|
||||
}
|
||||
|
||||
/** Animal profile for the APIs (English keys and values, plus readable labels in the animal's language). */
|
||||
function ga_animal_api_data( $post_id ) {
|
||||
$lang = function_exists( 'pll_get_post_language' ) ? ( pll_get_post_language( $post_id ) ?: 'es' ) : 'es';
|
||||
$gallery = array_values( array_filter( array_map( 'absint', explode( ',', ga_get( $post_id, 'galeria' ) ) ) ) );
|
||||
$text = fn( $key ) => '' === ga_get( $post_id, $key ) ? null : ga_get( $post_id, $key );
|
||||
|
||||
$data = array(
|
||||
'estado' => ga_status( $post_id ),
|
||||
'destacado' => (bool) ga_get( $post_id, 'destacado' ),
|
||||
'sexo' => ga_get( $post_id, 'sexo' ),
|
||||
'nacimiento' => ga_get( $post_id, 'nacimiento' ),
|
||||
'edad' => '' === ga_get( $post_id, 'edad' ) ? null : (float) ga_get( $post_id, 'edad' ),
|
||||
'edad_unidad' => ga_get( $post_id, 'edad_unidad' ),
|
||||
'tamano' => ga_get( $post_id, 'tamano' ),
|
||||
'llegada' => ga_get( $post_id, 'llegada' ),
|
||||
'galeria' => array_map( fn( $id ) => wp_get_attachment_image_url( $id, 'large' ), ga_gallery_ids( $post_id ) ),
|
||||
'video' => ga_get( $post_id, 'video' ),
|
||||
'caracter' => ga_get( $post_id, 'caracter' ),
|
||||
'necesidades' => ga_get( $post_id, 'necesidades' ),
|
||||
'convivencia' => ga_get( $post_id, 'convivencia' ),
|
||||
'hogar' => ga_get( $post_id, 'hogar' ),
|
||||
'idioma' => $lang,
|
||||
'status' => ga_api_to( 'estado', ga_status( $post_id ) ),
|
||||
'featured' => (bool) ga_get( $post_id, 'destacado' ),
|
||||
'sex' => ga_api_to( 'sexo', ga_get( $post_id, 'sexo' ) ),
|
||||
'birthDate' => $text( 'nacimiento' ),
|
||||
'age' => '' === ga_get( $post_id, 'edad' ) ? null : (float) ga_get( $post_id, 'edad' ),
|
||||
'ageUnit' => ga_api_to( 'edad_unidad', ga_get( $post_id, 'edad_unidad' ) ),
|
||||
'size' => ga_api_to( 'tamano', ga_get( $post_id, 'tamano' ) ),
|
||||
'arrivalDate' => $text( 'llegada' ),
|
||||
'galleryIds' => $gallery,
|
||||
'photos' => array_values( array_filter( array_map( fn( $id ) => wp_get_attachment_image_url( $id, 'large' ), ga_gallery_ids( $post_id ) ) ) ),
|
||||
'video' => $text( 'video' ),
|
||||
'personality' => $text( 'caracter' ),
|
||||
'needs' => $text( 'necesidades' ),
|
||||
'compatibility' => $text( 'convivencia' ),
|
||||
'idealHome' => $text( 'hogar' ),
|
||||
'language' => $lang,
|
||||
);
|
||||
$data['textos'] = ga_with_lang( $lang, fn() => array(
|
||||
'estado' => ga_label( $post_id, 'estado' ),
|
||||
'sexo' => ga_label( $post_id, 'sexo' ),
|
||||
'edad' => ga_age( $post_id ),
|
||||
'tamano' => ga_label( $post_id, 'tamano' ),
|
||||
$data['labels'] = ga_with_lang( $lang, fn() => array(
|
||||
'status' => ga_label( $post_id, 'estado' ),
|
||||
'sex' => ga_label( $post_id, 'sexo' ) ?: null,
|
||||
'age' => ga_age( $post_id ) ?: null,
|
||||
'size' => ga_label( $post_id, 'tamano' ) ?: null,
|
||||
) );
|
||||
return $data;
|
||||
}
|
||||
|
||||
add_action( 'rest_api_init', function () {
|
||||
register_rest_field( 'animal', 'ficha', array(
|
||||
'get_callback' => fn( $post ) => ga_animal_api_data( $post['id'] ),
|
||||
'schema' => array(
|
||||
'description' => 'Ficha del animal (valores internos y textos legibles en su idioma).',
|
||||
/** Writes an API profile into the animal. Shared fields are copied to its translations. */
|
||||
function ga_animal_api_update( array $profile, $post_id ) {
|
||||
$fields = ga_fields();
|
||||
foreach ( ga_api_map() as $prop => $key ) {
|
||||
if ( ! array_key_exists( $prop, $profile ) ) {
|
||||
continue;
|
||||
}
|
||||
$raw = $profile[ $prop ];
|
||||
switch ( $prop ) {
|
||||
case 'status':
|
||||
case 'sex':
|
||||
case 'size':
|
||||
case 'ageUnit':
|
||||
$value = null === $raw ? '' : ga_api_from( $key, $raw );
|
||||
if ( null === $value ) {
|
||||
return new WP_Error( 'rest_invalid_param', "Invalid value for profile.$prop.", array( 'status' => 400 ) );
|
||||
}
|
||||
break;
|
||||
case 'featured':
|
||||
$value = $raw ? '1' : '';
|
||||
break;
|
||||
case 'age':
|
||||
$value = null === $raw ? '' : (string) absint( $raw );
|
||||
break;
|
||||
case 'birthDate':
|
||||
case 'arrivalDate':
|
||||
$value = (string) $raw;
|
||||
if ( '' !== $value && ! preg_match( '/^\d{4}-\d{2}-\d{2}$/', $value ) ) {
|
||||
return new WP_Error( 'rest_invalid_param', "profile.$prop must be YYYY-MM-DD.", array( 'status' => 400 ) );
|
||||
}
|
||||
break;
|
||||
case 'galleryIds':
|
||||
$value = implode( ',', array_filter( array_map( 'absint', (array) $raw ) ) );
|
||||
break;
|
||||
case 'video':
|
||||
$value = esc_url_raw( (string) $raw );
|
||||
break;
|
||||
default:
|
||||
$value = sanitize_textarea_field( (string) $raw );
|
||||
}
|
||||
if ( ! empty( $fields[ $key ]['shared'] ) ) {
|
||||
ga_update_meta_all_languages( $post_id, "_ga_$key", $value );
|
||||
} else {
|
||||
update_post_meta( $post_id, "_ga_$key", $value );
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/** JSON schema of the profile (used by REST validation and the OpenAPI docs). */
|
||||
function ga_animal_api_schema() {
|
||||
$string_or_null = array( 'type' => array( 'string', 'null' ) );
|
||||
$enum = fn( $field ) => array( 'type' => array( 'string', 'null' ), 'enum' => array_merge( array_values( ga_api_enums()[ $field ] ), array( null ) ) );
|
||||
return array(
|
||||
'description' => 'Adoption profile of the animal. Shared fields are common to all translations; texts are per language.',
|
||||
'type' => 'object',
|
||||
'context' => array( 'view', 'edit' ),
|
||||
'readonly' => true,
|
||||
'properties' => array(
|
||||
'estado' => array( 'type' => 'string', 'enum' => array( 'disponible', 'proceso', 'adoptado' ) ),
|
||||
'destacado' => array( 'type' => 'boolean' ),
|
||||
'sexo' => array( 'type' => 'string' ),
|
||||
'nacimiento' => array( 'type' => 'string' ),
|
||||
'edad' => array( 'type' => array( 'number', 'null' ) ),
|
||||
'edad_unidad' => array( 'type' => 'string' ),
|
||||
'tamano' => array( 'type' => 'string' ),
|
||||
'llegada' => array( 'type' => 'string' ),
|
||||
'galeria' => array( 'type' => 'array', 'items' => array( 'type' => 'string', 'format' => 'uri' ) ),
|
||||
'video' => array( 'type' => 'string' ),
|
||||
'caracter' => array( 'type' => 'string' ),
|
||||
'necesidades' => array( 'type' => 'string' ),
|
||||
'convivencia' => array( 'type' => 'string' ),
|
||||
'hogar' => array( 'type' => 'string' ),
|
||||
'idioma' => array( 'type' => 'string' ),
|
||||
'textos' => array( 'type' => 'object' ),
|
||||
),
|
||||
'status' => array( 'type' => 'string', 'enum' => array_values( ga_api_enums()['estado'] ), 'description' => 'Adopted animals are hidden from public listings.' ),
|
||||
'featured' => array( 'type' => 'boolean', 'description' => 'Shown on the home page.' ),
|
||||
'sex' => $enum( 'sexo' ),
|
||||
'birthDate' => $string_or_null + array( 'format' => 'date' ),
|
||||
'age' => array( 'type' => array( 'number', 'null' ), 'description' => 'Approximate age, used when birthDate is unknown.' ),
|
||||
'ageUnit' => $enum( 'edad_unidad' ),
|
||||
'size' => $enum( 'tamano' ),
|
||||
'arrivalDate' => $string_or_null + array( 'format' => 'date', 'description' => 'Date the animal arrived at the shelter.' ),
|
||||
'galleryIds' => array( 'type' => 'array', 'items' => array( 'type' => 'integer' ), 'description' => 'Attachment IDs of the extra photos (the featured image is the main photo).' ),
|
||||
'photos' => array( 'type' => 'array', 'items' => array( 'type' => 'string', 'format' => 'uri' ), 'readonly' => true, 'description' => 'Main photo followed by the gallery.' ),
|
||||
'video' => $string_or_null + array( 'format' => 'uri' ),
|
||||
'personality' => $string_or_null,
|
||||
'needs' => $string_or_null,
|
||||
'compatibility' => $string_or_null + array( 'description' => 'Observed behaviour with children and other animals, in context.' ),
|
||||
'idealHome' => $string_or_null,
|
||||
'language' => array( 'type' => 'string', 'enum' => array( 'es', 'eu' ), 'readonly' => true ),
|
||||
'labels' => array( 'type' => 'object', 'readonly' => true, 'description' => 'Human-readable values in the animal\'s language.' ),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
add_action( 'rest_api_init', function () {
|
||||
register_rest_field( 'animal', 'profile', array(
|
||||
'get_callback' => fn( $post ) => ga_animal_api_data( $post['id'] ),
|
||||
'update_callback' => fn( $value, $post ) => ga_animal_api_update( (array) $value, $post->ID ),
|
||||
'schema' => ga_animal_api_schema(),
|
||||
) );
|
||||
} );
|
||||
|
||||
add_action( 'graphql_register_types', function () {
|
||||
register_graphql_object_type( 'AnimalFichaTextos', array(
|
||||
'description' => 'Textos legibles de la ficha, en el idioma del animal.',
|
||||
foreach ( array(
|
||||
'AnimalStatusEnum' => array( 'estado', 'Adoption status' ),
|
||||
'AnimalSexEnum' => array( 'sexo', 'Sex' ),
|
||||
'AnimalSizeEnum' => array( 'tamano', 'Size (dogs)' ),
|
||||
'AnimalAgeUnitEnum' => array( 'edad_unidad', 'Unit of the approximate age' ),
|
||||
) as $type => [ $field, $description ] ) {
|
||||
$values = array();
|
||||
foreach ( ga_api_enums()[ $field ] as $api ) {
|
||||
$values[ strtoupper( $api ) ] = array( 'value' => $api );
|
||||
}
|
||||
register_graphql_enum_type( $type, array( 'description' => $description, 'values' => $values ) );
|
||||
}
|
||||
register_graphql_object_type( 'AnimalProfileLabels', array(
|
||||
'description' => 'Human-readable values in the animal\'s language.',
|
||||
'fields' => array(
|
||||
'estado' => array( 'type' => 'String' ),
|
||||
'sexo' => array( 'type' => 'String' ),
|
||||
'edad' => array( 'type' => 'String' ),
|
||||
'tamano' => array( 'type' => 'String' ),
|
||||
'status' => array( 'type' => 'String' ),
|
||||
'sex' => array( 'type' => 'String' ),
|
||||
'age' => array( 'type' => 'String' ),
|
||||
'size' => array( 'type' => 'String' ),
|
||||
),
|
||||
) );
|
||||
register_graphql_object_type( 'AnimalFicha', array(
|
||||
'description' => 'Ficha del animal en adopción.',
|
||||
register_graphql_object_type( 'AnimalProfile', array(
|
||||
'description' => 'Adoption profile of the animal.',
|
||||
'fields' => array(
|
||||
'estado' => array( 'type' => 'String', 'description' => 'disponible, proceso o adoptado' ),
|
||||
'destacado' => array( 'type' => 'Boolean' ),
|
||||
'sexo' => array( 'type' => 'String' ),
|
||||
'nacimiento' => array( 'type' => 'String' ),
|
||||
'edad' => array( 'type' => 'Float' ),
|
||||
'edadUnidad' => array( 'type' => 'String' ),
|
||||
'tamano' => array( 'type' => 'String' ),
|
||||
'llegada' => array( 'type' => 'String' ),
|
||||
'galeria' => array( 'type' => array( 'list_of' => 'String' ) ),
|
||||
'status' => array( 'type' => 'AnimalStatusEnum' ),
|
||||
'featured' => array( 'type' => 'Boolean' ),
|
||||
'sex' => array( 'type' => 'AnimalSexEnum' ),
|
||||
'birthDate' => array( 'type' => 'String' ),
|
||||
'age' => array( 'type' => 'Float' ),
|
||||
'ageUnit' => array( 'type' => 'AnimalAgeUnitEnum' ),
|
||||
'size' => array( 'type' => 'AnimalSizeEnum' ),
|
||||
'arrivalDate' => array( 'type' => 'String' ),
|
||||
'photos' => array( 'type' => array( 'list_of' => 'String' ) ),
|
||||
'video' => array( 'type' => 'String' ),
|
||||
'caracter' => array( 'type' => 'String' ),
|
||||
'necesidades' => array( 'type' => 'String' ),
|
||||
'convivencia' => array( 'type' => 'String' ),
|
||||
'hogar' => array( 'type' => 'String' ),
|
||||
'idioma' => array( 'type' => 'String' ),
|
||||
'textos' => array( 'type' => 'AnimalFichaTextos' ),
|
||||
'personality' => array( 'type' => 'String' ),
|
||||
'needs' => array( 'type' => 'String' ),
|
||||
'compatibility' => array( 'type' => 'String' ),
|
||||
'idealHome' => array( 'type' => 'String' ),
|
||||
'language' => array( 'type' => 'String' ),
|
||||
'labels' => array( 'type' => 'AnimalProfileLabels' ),
|
||||
),
|
||||
) );
|
||||
register_graphql_field( 'Animal', 'ficha', array(
|
||||
'type' => 'AnimalFicha',
|
||||
'resolve' => function ( $post ) {
|
||||
$data = ga_animal_api_data( $post->databaseId );
|
||||
$data['edadUnidad'] = $data['edad_unidad'];
|
||||
return $data;
|
||||
},
|
||||
register_graphql_field( 'Animal', 'profile', array(
|
||||
'type' => 'AnimalProfile',
|
||||
'resolve' => fn( $post ) => ga_animal_api_data( $post->databaseId ),
|
||||
) );
|
||||
register_graphql_field( 'Animal', 'language', array(
|
||||
'type' => 'String',
|
||||
'description' => 'Language of this translation (es or eu).',
|
||||
'resolve' => fn( $post ) => function_exists( 'pll_get_post_language' ) ? pll_get_post_language( $post->databaseId ) : 'es',
|
||||
) );
|
||||
register_graphql_field( 'RootQueryToAnimalConnectionWhereArgs', 'language', array(
|
||||
'type' => 'String',
|
||||
'description' => 'Only animals in this language (es or eu).',
|
||||
) );
|
||||
register_graphql_field( 'RootQueryToAnimalConnectionWhereArgs', 'adoptionStatus', array(
|
||||
'type' => 'AnimalStatusEnum',
|
||||
'description' => 'Only animals with this adoption status.',
|
||||
) );
|
||||
} );
|
||||
|
||||
/* GraphQL: «where: { language, adoptionStatus }» en la consulta de animales («status» ya es el estado de publicación de WordPress). */
|
||||
add_filter( 'graphql_post_object_connection_query_args', function ( $args, $source, $input ) {
|
||||
if ( 'animal' !== ( $args['post_type'] ?? '' ) && array( 'animal' ) !== ( $args['post_type'] ?? '' ) ) {
|
||||
return $args;
|
||||
}
|
||||
$where = $input['where'] ?? array();
|
||||
$args['lang'] = in_array( $where['language'] ?? '', array( 'es', 'eu' ), true ) ? $where['language'] : '';
|
||||
if ( ! empty( $where['adoptionStatus'] ) ) {
|
||||
$internal = ga_api_from( 'estado', $where['adoptionStatus'] );
|
||||
$values = 'proceso' === $internal ? array( 'proceso', 'reservado' ) : array( $internal );
|
||||
$args['meta_query'][] = 'disponible' === $internal
|
||||
? array( 'relation' => 'OR', array( 'key' => '_ga_estado', 'value' => array( 'disponible', 'urgente', 'acogida' ), 'compare' => 'IN' ), array( 'key' => '_ga_estado', 'compare' => 'NOT EXISTS' ) )
|
||||
: array( 'key' => '_ga_estado', 'value' => $values, 'compare' => 'IN' );
|
||||
}
|
||||
return $args;
|
||||
}, 10, 3 );
|
||||
|
||||
/** Converts a WordPress JSON schema into an OpenAPI 3.0 schema (nullable types, readOnly). */
|
||||
function ga_openapi_schema( $schema ) {
|
||||
if ( isset( $schema['type'] ) && is_array( $schema['type'] ) ) {
|
||||
$types = array_values( array_diff( $schema['type'], array( 'null' ) ) );
|
||||
if ( count( $types ) < count( $schema['type'] ) ) {
|
||||
$schema['nullable'] = true;
|
||||
}
|
||||
$schema['type'] = $types[0] ?? 'string';
|
||||
}
|
||||
if ( isset( $schema['enum'] ) ) {
|
||||
$schema['enum'] = array_values( array_filter( $schema['enum'], fn( $v ) => null !== $v ) );
|
||||
}
|
||||
if ( ! empty( $schema['readonly'] ) ) {
|
||||
$schema['readOnly'] = true;
|
||||
}
|
||||
unset( $schema['readonly'], $schema['context'] );
|
||||
foreach ( array( 'properties' ) as $key ) {
|
||||
if ( isset( $schema[ $key ] ) ) {
|
||||
$schema[ $key ] = array_map( 'ga_openapi_schema', $schema[ $key ] );
|
||||
}
|
||||
}
|
||||
if ( isset( $schema['items'] ) ) {
|
||||
$schema['items'] = ga_openapi_schema( $schema['items'] );
|
||||
}
|
||||
return $schema;
|
||||
}
|
||||
|
||||
/*
|
||||
* OpenAPI schema (/rest-api/schema): the plugin flattens object fields to "string".
|
||||
* We post-process its output to document «profile» properly (component AnimalProfile).
|
||||
*/
|
||||
add_action( 'wp', function () {
|
||||
if ( 'schema' !== get_query_var( 'swagger_api' ) ) {
|
||||
return;
|
||||
}
|
||||
ob_start( function ( $json ) {
|
||||
$doc = json_decode( $json, true );
|
||||
if ( ! is_array( $doc ) || empty( $doc['paths'] ) ) {
|
||||
return $json;
|
||||
}
|
||||
$profile = ga_openapi_schema( ga_animal_api_schema() );
|
||||
$ref = array( '$ref' => '#/components/schemas/AnimalProfile' );
|
||||
$doc['components']['schemas']['AnimalProfile'] = $profile;
|
||||
$doc['components']['schemas']['Animal'] = array(
|
||||
'type' => 'object',
|
||||
'properties' => array(
|
||||
'id' => array( 'type' => 'integer' ),
|
||||
'slug' => array( 'type' => 'string' ),
|
||||
'link' => array( 'type' => 'string', 'format' => 'uri' ),
|
||||
'title' => array( 'type' => 'object', 'properties' => array( 'rendered' => array( 'type' => 'string' ) ) ),
|
||||
'content' => array( 'type' => 'object', 'properties' => array( 'rendered' => array( 'type' => 'string' ) ) ),
|
||||
'species' => array( 'type' => 'array', 'items' => array( 'type' => 'integer' ) ),
|
||||
'lang' => array( 'type' => 'string', 'description' => 'Language of this translation (es or eu).' ),
|
||||
'profile' => $ref,
|
||||
),
|
||||
);
|
||||
foreach ( $doc['paths'] as $path => &$methods ) {
|
||||
if ( ! preg_match( '#^/wp/v2/animals(/\{id\})?$#', $path, $m ) ) {
|
||||
continue;
|
||||
}
|
||||
foreach ( $methods as $method => &$op ) {
|
||||
foreach ( $op['requestBody']['content'] ?? array() as $type => $content ) {
|
||||
if ( isset( $content['schema']['properties']['profile'] ) ) {
|
||||
$op['requestBody']['content'][ $type ]['schema']['properties']['profile'] = $ref;
|
||||
}
|
||||
}
|
||||
// JSON first: «profile» is a nested object, which form-encoded bodies can't express well.
|
||||
if ( ! empty( $op['requestBody']['content'] ) && empty( $op['requestBody']['content']['application/json'] ) ) {
|
||||
$first = reset( $op['requestBody']['content'] );
|
||||
$op['requestBody']['content'] = array( 'application/json' => $first ) + $op['requestBody']['content'];
|
||||
}
|
||||
if ( 'get' === $method ) {
|
||||
$item = array( '$ref' => '#/components/schemas/Animal' );
|
||||
$op['responses']['200'] = array(
|
||||
'description' => 'OK',
|
||||
'content' => array( 'application/json' => array( 'schema' => empty( $m[1] ) ? array( 'type' => 'array', 'items' => $item ) : $item ) ),
|
||||
);
|
||||
}
|
||||
}
|
||||
unset( $op );
|
||||
}
|
||||
unset( $methods );
|
||||
return wp_json_encode( $doc );
|
||||
} );
|
||||
}, 0 ); // Antes que el plugin, que emite el esquema en «wp».
|
||||
|
||||
/*
|
||||
* Documentación Swagger (/rest-api/docs/).
|
||||
*
|
||||
@@ -132,7 +376,7 @@ add_action( 'template_redirect', function () {
|
||||
header( 'Content-Type: text/html; charset=utf-8' );
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="es">
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
@@ -143,10 +387,10 @@ add_action( 'template_redirect', function () {
|
||||
</head>
|
||||
<body>
|
||||
<div class="ga-api-bar">
|
||||
<?php echo esc_html( get_bloginfo( 'name' ) ); ?> · API REST ·
|
||||
<a href="<?php echo esc_url( home_url( '/rest-api/schema' ) ); ?>">Esquema OpenAPI</a> ·
|
||||
<?php echo esc_html( get_bloginfo( 'name' ) ); ?> · REST API ·
|
||||
<a href="<?php echo esc_url( home_url( '/rest-api/schema' ) ); ?>">OpenAPI schema</a> ·
|
||||
<a href="<?php echo esc_url( home_url( '/graphql' ) ); ?>">GraphQL</a> ·
|
||||
<a href="<?php echo esc_url( admin_url() ); ?>">Volver al panel</a>
|
||||
<a href="<?php echo esc_url( admin_url() ); ?>">Dashboard</a>
|
||||
</div>
|
||||
<div id="swagger-ui"></div>
|
||||
<script src="<?php echo esc_url( $cdn . '/swagger-ui-bundle.js' ); ?>" integrity="sha384-YDALVcy8kj8yltLBVi1vBiBAUqdxvus673gM8XKwiy6aDUJFXivF/KCufekjYbVf" crossorigin="anonymous"></script>
|
||||
|
||||
Reference in New Issue
Block a user