diff --git a/projects/js-packages/publicize-components/changelog/update-social-to-use-unified-settings-endpoint b/projects/js-packages/publicize-components/changelog/update-social-to-use-unified-settings-endpoint new file mode 100644 index 0000000000000..af28eeceb5f49 --- /dev/null +++ b/projects/js-packages/publicize-components/changelog/update-social-to-use-unified-settings-endpoint @@ -0,0 +1,4 @@ +Significance: patch +Type: changed + +Update the settings endppoint to use existing endpoints diff --git a/projects/js-packages/publicize-components/src/components/admin-page/header/index.js b/projects/js-packages/publicize-components/src/components/admin-page/header/index.js index edbaee4243cfa..e73826fa73259 100644 --- a/projects/js-packages/publicize-components/src/components/admin-page/header/index.js +++ b/projects/js-packages/publicize-components/src/components/admin-page/header/index.js @@ -22,7 +22,7 @@ const Header = () => { const store = select( socialStore ); return { hasConnections: store.getConnections().length > 0, - isModuleEnabled: select( socialStore ).getSocialPluginSettings().publicize_active, + isModuleEnabled: select( socialStore ).getSocialModuleSettings().publicize, }; } ); diff --git a/projects/js-packages/publicize-components/src/components/admin-page/index.tsx b/projects/js-packages/publicize-components/src/components/admin-page/index.tsx index 127ac4a7b5200..ed3481a00b745 100644 --- a/projects/js-packages/publicize-components/src/components/admin-page/index.tsx +++ b/projects/js-packages/publicize-components/src/components/admin-page/index.tsx @@ -33,12 +33,12 @@ export const SocialAdminPage = () => { const { isModuleEnabled, showPricingPage, isUpdatingJetpackSettings } = useSelect( select => { const store = select( socialStore ); - const settings = store.getSocialPluginSettings(); + const settings = store.getSocialModuleSettings(); return { - isModuleEnabled: settings.publicize_active, + isModuleEnabled: settings.publicize, showPricingPage: store.getSocialSettings().showPricingPage, - isUpdatingJetpackSettings: store.isSavingSocialPluginSettings(), + isUpdatingJetpackSettings: store.isSavingSocialModuleSettings(), }; }, [] ); diff --git a/projects/js-packages/publicize-components/src/components/admin-page/test/index.test.jsx b/projects/js-packages/publicize-components/src/components/admin-page/test/index.test.jsx index 59088c28fc2dc..0eb0df7149e20 100644 --- a/projects/js-packages/publicize-components/src/components/admin-page/test/index.test.jsx +++ b/projects/js-packages/publicize-components/src/components/admin-page/test/index.test.jsx @@ -13,6 +13,7 @@ describe( 'load the app', () => { beforeEach( () => { window.JetpackScriptData = { social: { + api_paths: {}, plugin_info: { social: { version, diff --git a/projects/js-packages/publicize-components/src/components/admin-page/toggles/social-module-toggle/index.tsx b/projects/js-packages/publicize-components/src/components/admin-page/toggles/social-module-toggle/index.tsx index 8e7c86f69565f..61f76741588aa 100644 --- a/projects/js-packages/publicize-components/src/components/admin-page/toggles/social-module-toggle/index.tsx +++ b/projects/js-packages/publicize-components/src/components/admin-page/toggles/social-module-toggle/index.tsx @@ -21,11 +21,11 @@ const SocialModuleToggle: React.FC = () => { const { isModuleEnabled, isUpdating } = useSelect( select => { const store = select( socialStore ); - const settings = store.getSocialPluginSettings(); + const settings = store.getSocialModuleSettings(); return { - isModuleEnabled: settings.publicize_active, - isUpdating: store.isSavingSocialPluginSettings(), + isModuleEnabled: settings.publicize, + isUpdating: store.isSavingSocialModuleSettings(), }; }, [] ); @@ -36,19 +36,19 @@ const SocialModuleToggle: React.FC = () => { const useAdminUiV1 = feature_flags.useAdminUiV1; - const { updateSocialPluginSettings } = useDispatch( socialStore ); + const { updateSocialModuleSettings } = useDispatch( socialStore ); const toggleModule = useCallback( async () => { const newOption = { - publicize_active: ! isModuleEnabled, + publicize: ! isModuleEnabled, }; - await updateSocialPluginSettings( newOption ); + await updateSocialModuleSettings( newOption ); // If the module was enabled, we need to refresh the connection list - if ( newOption.publicize_active && ! getSocialScriptData().is_publicize_enabled ) { + if ( newOption.publicize && ! getSocialScriptData().is_publicize_enabled ) { window.location.reload(); } - }, [ isModuleEnabled, updateSocialPluginSettings ] ); + }, [ isModuleEnabled, updateSocialModuleSettings ] ); const [ isSmall ] = useBreakpointMatch( 'sm' ); diff --git a/projects/js-packages/publicize-components/src/social-store/actions/index.ts b/projects/js-packages/publicize-components/src/social-store/actions/index.ts index 40463ed09d6b2..204fdcb9b992d 100644 --- a/projects/js-packages/publicize-components/src/social-store/actions/index.ts +++ b/projects/js-packages/publicize-components/src/social-store/actions/index.ts @@ -2,8 +2,8 @@ import * as connectionData from './connection-data'; import * as pricingPageSettings from './pricing-page'; import * as shareStatus from './share-status'; import * as sigActions from './social-image-generator'; +import * as socialModuleSettings from './social-module-settings'; import * as socialNoteSettings from './social-notes'; -import * as socialPluginSettings from './social-plugin-settings'; import * as utmActions from './utm-settings'; const actions = { @@ -13,7 +13,7 @@ const actions = { ...utmActions, ...socialNoteSettings, ...pricingPageSettings, - ...socialPluginSettings, + ...socialModuleSettings, }; export default actions; diff --git a/projects/js-packages/publicize-components/src/social-store/actions/social-module-settings.ts b/projects/js-packages/publicize-components/src/social-store/actions/social-module-settings.ts new file mode 100644 index 0000000000000..eba5e65137f36 --- /dev/null +++ b/projects/js-packages/publicize-components/src/social-store/actions/social-module-settings.ts @@ -0,0 +1,20 @@ +import { store as coreStore } from '@wordpress/core-data'; +import { getSocialScriptData } from '../../utils'; +import { SocialModuleSettings } from '../types'; + +/** + * Saves the Social module settings. + * + * @param data - The data to save. + * + * @return A thunk. + */ +export function updateSocialModuleSettings( data: Partial< SocialModuleSettings > ) { + return async function ( { registry } ) { + const { socialToggleBase } = getSocialScriptData().api_paths; + + const { saveEntityRecord } = registry.dispatch( coreStore ); + + await saveEntityRecord( 'jetpack/v4', socialToggleBase, data ); + }; +} diff --git a/projects/js-packages/publicize-components/src/social-store/actions/social-plugin-settings.ts b/projects/js-packages/publicize-components/src/social-store/actions/social-plugin-settings.ts deleted file mode 100644 index 00954ea2d871a..0000000000000 --- a/projects/js-packages/publicize-components/src/social-store/actions/social-plugin-settings.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { store as coreStore } from '@wordpress/core-data'; -import { SocialPluginSettings } from '../types'; - -/** - * Saves the Social plugin settings. - * - * @param {Partial< SocialPluginSettings >} data - The data to save. - * - * @return {Function} A thunk. - */ -export function updateSocialPluginSettings( data: Partial< SocialPluginSettings > ) { - return async function ( { registry } ) { - const { saveEntityRecord } = registry.dispatch( coreStore ); - - await saveEntityRecord( 'jetpack/v4', 'social/settings', data ); - }; -} diff --git a/projects/js-packages/publicize-components/src/social-store/resolvers.ts b/projects/js-packages/publicize-components/src/social-store/resolvers.ts index bf3a59bb02330..3c6ed36280062 100644 --- a/projects/js-packages/publicize-components/src/social-store/resolvers.ts +++ b/projects/js-packages/publicize-components/src/social-store/resolvers.ts @@ -78,20 +78,22 @@ export function getPostShareStatus( _postId ) { } /** - * Resolves the social plugin settings to ensure the core-data entities are registered. + * Resolves the social module settings to ensure the core-data entities are registered. * * @return {Function} Resolver */ -export function getSocialPluginSettings() { +export function getSocialModuleSettings() { return async ( { registry } ) => { + const { socialToggleBase } = getSocialScriptData().api_paths; + const jetpackEntities = registry.select( coreStore ).getEntitiesConfig( 'jetpack/v4' ); - if ( ! jetpackEntities.some( ( { name } ) => name === 'social/settings' ) ) { + if ( ! jetpackEntities.some( ( { name } ) => name === socialToggleBase ) ) { await registry.dispatch( coreStore ).addEntities( [ { kind: 'jetpack/v4', - name: 'social/settings', - baseURL: '/jetpack/v4/social/settings', + name: socialToggleBase, + baseURL: `/jetpack/v4/${ socialToggleBase }`, label: __( 'Social Settings', 'jetpack-publicize-components' ), }, ] ); @@ -102,5 +104,5 @@ export function getSocialPluginSettings() { export default { getConnections, getPostShareStatus, - getSocialPluginSettings, + getSocialModuleSettings, }; diff --git a/projects/js-packages/publicize-components/src/social-store/selectors/index.ts b/projects/js-packages/publicize-components/src/social-store/selectors/index.ts index 278112c2c925c..97f790c3b6595 100644 --- a/projects/js-packages/publicize-components/src/social-store/selectors/index.ts +++ b/projects/js-packages/publicize-components/src/social-store/selectors/index.ts @@ -1,12 +1,12 @@ import * as connectionDataSelectors from './connection-data'; import * as shareStatusSelectors from './share-status'; -import * as socialPluginSelectors from './social-plugin-settings'; +import * as socialModuleSelectors from './social-module-settings'; import * as socialSettingsSelectors from './social-settings'; const selectors = { ...connectionDataSelectors, ...shareStatusSelectors, - ...socialPluginSelectors, + ...socialModuleSelectors, ...socialSettingsSelectors, }; diff --git a/projects/js-packages/publicize-components/src/social-store/selectors/social-module-settings.ts b/projects/js-packages/publicize-components/src/social-store/selectors/social-module-settings.ts new file mode 100644 index 0000000000000..3d11ae54b1093 --- /dev/null +++ b/projects/js-packages/publicize-components/src/social-store/selectors/social-module-settings.ts @@ -0,0 +1,26 @@ +import { store as coreStore } from '@wordpress/core-data'; +import { createRegistrySelector } from '@wordpress/data'; +import { getSocialScriptData } from '../../utils'; +import { SocialModuleSettings } from '../types'; + +/** + * Returns the Social module settings. + */ +export const getSocialModuleSettings = createRegistrySelector( select => () => { + const { socialToggleBase } = getSocialScriptData().api_paths; + + const data = select( coreStore ).getEntityRecord( 'jetpack/v4', socialToggleBase, undefined ); + + return ( data ?? { + publicize: getSocialScriptData().is_publicize_enabled, + } ) as SocialModuleSettings; +} ); + +/** + * Returns whether the Social module settings are being saved + */ +export const isSavingSocialModuleSettings = createRegistrySelector( select => () => { + const { socialToggleBase } = getSocialScriptData().api_paths; + + return select( coreStore ).isSavingEntityRecord( 'jetpack/v4', socialToggleBase, undefined ); +} ); diff --git a/projects/js-packages/publicize-components/src/social-store/selectors/social-plugin-settings.ts b/projects/js-packages/publicize-components/src/social-store/selectors/social-plugin-settings.ts deleted file mode 100644 index 9e6fa6d3f98ea..0000000000000 --- a/projects/js-packages/publicize-components/src/social-store/selectors/social-plugin-settings.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { store as coreStore } from '@wordpress/core-data'; -import { createRegistrySelector } from '@wordpress/data'; -import { getSocialScriptData } from '../../utils'; -import { SocialPluginSettings } from '../types'; - -/** - * Returns the Social plugin settings. - */ -export const getSocialPluginSettings = createRegistrySelector( select => () => { - const data = select( coreStore ).getEntityRecord( 'jetpack/v4', 'social/settings', undefined ); - - return data ?? { publicize_active: getSocialScriptData().is_publicize_enabled }; -} ) as ( state: object ) => SocialPluginSettings; - -/** - * Returns whether the Social plugin settings are being saved - */ -export const isSavingSocialPluginSettings = createRegistrySelector( select => () => { - return select( coreStore ).isSavingEntityRecord( 'jetpack/v4', 'social/settings', undefined ); -} ); diff --git a/projects/js-packages/publicize-components/src/social-store/types.ts b/projects/js-packages/publicize-components/src/social-store/types.ts index 28e394bffd1b7..585199856568e 100644 --- a/projects/js-packages/publicize-components/src/social-store/types.ts +++ b/projects/js-packages/publicize-components/src/social-store/types.ts @@ -142,8 +142,8 @@ export type SocialNotesSettings = { config: SocialNotesConfig; }; -export type SocialPluginSettings = { - publicize_active: boolean; +export type SocialModuleSettings = { + publicize: boolean; }; export type SocialSettingsFields = { diff --git a/projects/js-packages/publicize-components/src/types.ts b/projects/js-packages/publicize-components/src/types.ts index d810e2e83205f..2a55d0182295b 100644 --- a/projects/js-packages/publicize-components/src/types.ts +++ b/projects/js-packages/publicize-components/src/types.ts @@ -34,6 +34,7 @@ export type ConnectionService = { export interface ApiPaths { refreshConnections: string; resharePost: string; + socialToggleBase: 'settings' | 'social/settings'; } export type SocialSettings = { diff --git a/projects/packages/publicize/changelog/update-social-to-use-unified-settings-endpoint b/projects/packages/publicize/changelog/update-social-to-use-unified-settings-endpoint new file mode 100644 index 0000000000000..af28eeceb5f49 --- /dev/null +++ b/projects/packages/publicize/changelog/update-social-to-use-unified-settings-endpoint @@ -0,0 +1,4 @@ +Significance: patch +Type: changed + +Update the settings endppoint to use existing endpoints diff --git a/projects/packages/publicize/src/class-publicize-script-data.php b/projects/packages/publicize/src/class-publicize-script-data.php index 6c5bafcec2419..abbba41c296e5 100644 --- a/projects/packages/publicize/src/class-publicize-script-data.php +++ b/projects/packages/publicize/src/class-publicize-script-data.php @@ -117,7 +117,7 @@ public static function get_admin_script_data() { } $basic_data = array( - 'api_paths' => array(), + 'api_paths' => self::get_api_paths(), 'is_publicize_enabled' => Utils::is_publicize_active(), 'feature_flags' => self::get_feature_flags(), 'supported_services' => array(), @@ -141,7 +141,6 @@ public static function get_admin_script_data() { return array_merge( $basic_data, array( - 'api_paths' => self::get_api_paths(), 'supported_services' => self::get_supported_services(), 'shares_data' => self::get_shares_data(), 'urls' => self::get_urls(), @@ -306,6 +305,8 @@ public static function get_api_paths() { $commom_paths = array( 'refreshConnections' => '/wpcom/v2/publicize/connections?test_connections=1', + // The complete path will be like `/jetpack/v4/social/settings`. + 'socialToggleBase' => class_exists( 'Jetpack' ) ? 'settings' : 'social/settings', ); $specific_paths = array(); diff --git a/projects/packages/publicize/src/class-publicize-setup.php b/projects/packages/publicize/src/class-publicize-setup.php index d5db75ad397e8..eb4a8e1331e00 100644 --- a/projects/packages/publicize/src/class-publicize-setup.php +++ b/projects/packages/publicize/src/class-publicize-setup.php @@ -51,9 +51,6 @@ public static function pre_initialization() { } } - // This doesn't need to be active on WPCOM. - new REST_API\Settings_Controller(); - Social_Admin_Page::init(); } diff --git a/projects/packages/publicize/src/rest-api/class-settings-controller.php b/projects/packages/publicize/src/rest-api/class-settings-controller.php deleted file mode 100644 index 6a2f2517e4c5c..0000000000000 --- a/projects/packages/publicize/src/rest-api/class-settings-controller.php +++ /dev/null @@ -1,167 +0,0 @@ - WP_REST_Server::READABLE, - 'callback' => array( $this, 'get_item' ), - 'permission_callback' => array( $this, 'require_admin_privilege_callback' ), - 'args' => $this->get_endpoint_args_for_item_schema(), - ), - array( - 'methods' => WP_REST_Server::EDITABLE, - 'callback' => array( $this, 'update_item' ), - 'permission_callback' => array( $this, 'require_admin_privilege_callback' ), - 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), - ), - ) - ); - } - - /** - * Only administrators can access the API. - * - * @return bool|WP_Error True if a blog token was used to sign the request, WP_Error otherwise. - */ - public function require_admin_privilege_callback() { - if ( current_user_can( 'manage_options' ) ) { - return true; - } - - return new WP_Error( - 'rest_forbidden', - esc_html__( 'You are not allowed to perform this action.', 'jetpack-publicize-pkg' ), - array( 'status' => rest_authorization_required_code() ) - ); - } - - /** - * Updates the settings. - * - * @param WP_REST_Request $request Full details about the request. - * @return array|WP_Error Array on success, or error object on failure. - */ - public function get_item( $request ) { - $fields = $this->get_fields_for_response( $request ); - $data = array(); - - if ( rest_is_field_included( 'publicize_active', $fields ) ) { - $data['publicize_active'] = Publicize_Utils::is_publicize_active(); - } - - return $this->prepare_item_for_response( $data, $request ); - } - - /** - * POST `jetpack/v4/social/settings` - * - * @param WP_REST_Request $request - REST request. - */ - public function update_item( $request ) { - $params = $request->get_params(); - $settings = $this->get_endpoint_args_for_item_schema( $request->get_method() ); - - foreach ( array_keys( $settings ) as $name ) { - if ( ! array_key_exists( $name, $params ) ) { - continue; - } - - switch ( $name ) { - case 'publicize_active': - $updated = ( new Modules() )->update_status( self::JETPACK_PUBLICIZE_MODULE_SLUG, (bool) $params[ $name ], false, false ); - if ( is_wp_error( $updated ) ) { - return $updated; - } - break; - } - } - - return $this->get_item( $request ); - } - - /** - * Prepares the settings data to return from the endpoint. - * Includes checking the values against the schema. - * - * @param array $settings The settings data to prepare. - * @param WP_REST_Request $request REST request. - * @return array|WP_Error The prepared settings or a WP_Error on failure. - */ - public function prepare_item_for_response( $settings, $request ) { - $args = $this->get_endpoint_args_for_item_schema( $request->get_method() ); - $return = array(); - foreach ( $settings as $name => $value ) { - if ( empty( $args[ $name ] ) ) { - // This setting shouldn't be returned. - continue; - } - $is_valid = rest_validate_value_from_schema( $value, $args[ $name ], $name ); - if ( is_wp_error( $is_valid ) ) { - return $is_valid; - } - $sanitized = rest_sanitize_value_from_schema( $value, $args[ $name ] ); - if ( is_wp_error( $sanitized ) ) { - return $sanitized; - } - $return[ $name ] = $sanitized; - } - return rest_ensure_response( $return ); - } - - /** - * Get the settings schema, conforming to JSON Schema. - * - * @return array - */ - public function get_item_schema() { - $schema = array( - '$schema' => 'http://json-schema.org/draft-04/schema#', - 'title' => 'system_status', - 'type' => 'object', - 'properties' => array( - 'publicize_active' => array( - 'description' => __( 'Is the publicize module enabled?', 'jetpack-publicize-pkg' ), - 'type' => 'boolean', - 'context' => array( 'view', 'edit' ), - ), - ), - ); - return $this->add_additional_fields_schema( $schema ); - } -} diff --git a/projects/plugins/jetpack/_inc/lib/core-api/class.jetpack-core-api-module-endpoints.php b/projects/plugins/jetpack/_inc/lib/core-api/class.jetpack-core-api-module-endpoints.php index 775f9c42b4126..05acfe82e78db 100644 --- a/projects/plugins/jetpack/_inc/lib/core-api/class.jetpack-core-api-module-endpoints.php +++ b/projects/plugins/jetpack/_inc/lib/core-api/class.jetpack-core-api-module-endpoints.php @@ -635,6 +635,11 @@ public function update_data( $request ) { $not_updated[ $option ] = $error; } + if ( $updated ) { + // Return the module state. + $response[ $option ] = $value; + } + // Remove module from list so we don't go through it again. unset( $params[ $option ] ); } diff --git a/projects/plugins/jetpack/changelog/update-social-to-use-unified-settings-endpoint b/projects/plugins/jetpack/changelog/update-social-to-use-unified-settings-endpoint new file mode 100644 index 0000000000000..c4c37c22d68b0 --- /dev/null +++ b/projects/plugins/jetpack/changelog/update-social-to-use-unified-settings-endpoint @@ -0,0 +1,4 @@ +Significance: patch +Type: other + +Update the settings endpoint to return the updated toggle state in the success response diff --git a/projects/plugins/social/changelog/update-social-to-use-unified-settings-endpoint b/projects/plugins/social/changelog/update-social-to-use-unified-settings-endpoint new file mode 100644 index 0000000000000..af28eeceb5f49 --- /dev/null +++ b/projects/plugins/social/changelog/update-social-to-use-unified-settings-endpoint @@ -0,0 +1,4 @@ +Significance: patch +Type: changed + +Update the settings endppoint to use existing endpoints diff --git a/projects/plugins/social/src/class-rest-settings-controller.php b/projects/plugins/social/src/class-rest-settings-controller.php index 6280e29891adb..afd4db3c2eb09 100644 --- a/projects/plugins/social/src/class-rest-settings-controller.php +++ b/projects/plugins/social/src/class-rest-settings-controller.php @@ -8,6 +8,7 @@ namespace Automattic\Jetpack\Social; +use Automattic\Jetpack\Modules; use Jetpack_Social; use WP_Error; use WP_REST_Controller; @@ -25,6 +26,24 @@ class REST_Settings_Controller extends WP_REST_Controller { * @static */ public function register_rest_routes() { + register_rest_route( + 'jetpack/v4', + '/social/settings', + array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_item' ), + 'permission_callback' => array( $this, 'require_admin_privilege_callback' ), + 'args' => $this->get_endpoint_args_for_item_schema(), + ), + array( + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => array( $this, 'update_item' ), + 'permission_callback' => array( $this, 'require_admin_privilege_callback' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), + ), + ) + ); register_rest_route( 'jetpack/v4', '/social/review-dismiss', @@ -73,6 +92,50 @@ public function require_publish_posts_permission_callback() { ); } + /** + * Updates the settings. + * + * @param WP_REST_Request $request Full details about the request. + * @return array|WP_Error Array on success, or error object on failure. + */ + public function get_item( $request ) { + $fields = $this->get_fields_for_response( $request ); + $data = array(); + + if ( rest_is_field_included( 'publicize', $fields ) ) { + $data['publicize'] = Jetpack_Social::is_publicize_active(); + } + + return $this->prepare_item_for_response( $data, $request ); + } + + /** + * POST `jetpack/v4/social/settings` + * + * @param WP_REST_Request $request - REST request. + */ + public function update_item( $request ) { + $params = $request->get_params(); + $settings = $this->get_endpoint_args_for_item_schema( $request->get_method() ); + + foreach ( array_keys( $settings ) as $name ) { + if ( ! array_key_exists( $name, $params ) ) { + continue; + } + + switch ( $name ) { + case 'publicize': + $updated = ( new Modules() )->update_status( \Jetpack_Social::JETPACK_PUBLICIZE_MODULE_SLUG, (bool) $params[ $name ], false, false ); + if ( is_wp_error( $updated ) ) { + return $updated; + } + break; + } + } + + return $this->get_item( $request ); + } + /** * Updates the boolean value that dismisses the request to review the plugin * @@ -122,4 +185,25 @@ public function prepare_item_for_response( $settings, $request ) { } return rest_ensure_response( $return ); } + + /** + * Get the settings schema, conforming to JSON Schema. + * + * @return array + */ + public function get_item_schema() { + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'system_status', + 'type' => 'object', + 'properties' => array( + 'publicize' => array( + 'description' => __( 'Is the publicize module enabled?', 'jetpack-social' ), + 'type' => 'boolean', + 'context' => array( 'view', 'edit' ), + ), + ), + ); + return $this->add_additional_fields_schema( $schema ); + } }