From 073cc435472c37797db13888f0581463950990b7 Mon Sep 17 00:00:00 2001 From: Steve Boyd Date: Thu, 22 Aug 2024 12:44:30 +1200 Subject: [PATCH] API Remove GraphQL --- _config/graphql_plugins.yml | 12 - _config/graphql_schema.yml | 11 - _graphql/config.yml | 25 -- _graphql/enums.yml | 50 --- _graphql/types.yml | 27 -- composer.json | 1 - .../AbstractPublishOperationCreator.php | 86 ------ src/GraphQL/Operations/CopyToStageCreator.php | 78 ----- src/GraphQL/Operations/PublishCreator.php | 44 --- src/GraphQL/Operations/RollbackCreator.php | 82 ----- src/GraphQL/Operations/UnpublishCreator.php | 44 --- src/GraphQL/Plugins/UnpublishOnDelete.php | 94 ------ src/GraphQL/Plugins/VersionedDataObject.php | 154 --------- src/GraphQL/Plugins/VersionedRead.php | 64 ---- src/GraphQL/Resolvers/VersionFilters.php | 251 --------------- src/GraphQL/Resolvers/VersionedResolver.php | 280 ----------------- tests/php/GraphQL/Fake/Fake.php | 35 --- tests/php/GraphQL/Fake/FakeDataObjectStub.php | 36 --- tests/php/GraphQL/Fake/FakeResolveInfo.php | 34 -- .../Plugins/VersionedDataObjectPluginTest.php | 117 ------- .../php/GraphQL/Plugins/VersionedReadTest.php | 72 ----- .../Resolvers/VersionedFiltersTest.php | 291 ----------------- .../Resolvers/VersionedResolverTest.php | 292 ------------------ 23 files changed, 2180 deletions(-) delete mode 100644 _config/graphql_plugins.yml delete mode 100644 _config/graphql_schema.yml delete mode 100644 _graphql/config.yml delete mode 100644 _graphql/enums.yml delete mode 100644 _graphql/types.yml delete mode 100644 src/GraphQL/Operations/AbstractPublishOperationCreator.php delete mode 100644 src/GraphQL/Operations/CopyToStageCreator.php delete mode 100644 src/GraphQL/Operations/PublishCreator.php delete mode 100644 src/GraphQL/Operations/RollbackCreator.php delete mode 100644 src/GraphQL/Operations/UnpublishCreator.php delete mode 100644 src/GraphQL/Plugins/UnpublishOnDelete.php delete mode 100644 src/GraphQL/Plugins/VersionedDataObject.php delete mode 100644 src/GraphQL/Plugins/VersionedRead.php delete mode 100644 src/GraphQL/Resolvers/VersionFilters.php delete mode 100644 src/GraphQL/Resolvers/VersionedResolver.php delete mode 100644 tests/php/GraphQL/Fake/Fake.php delete mode 100644 tests/php/GraphQL/Fake/FakeDataObjectStub.php delete mode 100644 tests/php/GraphQL/Fake/FakeResolveInfo.php delete mode 100644 tests/php/GraphQL/Plugins/VersionedDataObjectPluginTest.php delete mode 100644 tests/php/GraphQL/Plugins/VersionedReadTest.php delete mode 100644 tests/php/GraphQL/Resolvers/VersionedFiltersTest.php delete mode 100644 tests/php/GraphQL/Resolvers/VersionedResolverTest.php diff --git a/_config/graphql_plugins.yml b/_config/graphql_plugins.yml deleted file mode 100644 index 5f63ffdb..00000000 --- a/_config/graphql_plugins.yml +++ /dev/null @@ -1,12 +0,0 @@ ---- -Name: versioned-graphql-plugins -Only: - moduleexists: 'silverstripe/graphql' - classexists: 'SilverStripe\GraphQL\Schema\Schema' ---- -SilverStripe\Core\Injector\Injector: - SilverStripe\GraphQL\Schema\Registry\PluginRegistry: - constructor: - - 'SilverStripe\Versioned\GraphQL\Plugins\VersionedDataObject' - - 'SilverStripe\Versioned\GraphQL\Plugins\UnpublishOnDelete' - - 'SilverStripe\Versioned\GraphQL\Plugins\VersionedRead' diff --git a/_config/graphql_schema.yml b/_config/graphql_schema.yml deleted file mode 100644 index f313bdb5..00000000 --- a/_config/graphql_schema.yml +++ /dev/null @@ -1,11 +0,0 @@ ---- -Name: versioned-graphql-schema -Only: - moduleexists: 'silverstripe/graphql' - classexists: 'SilverStripe\GraphQL\Schema\Schema' ---- -SilverStripe\GraphQL\Schema\Schema: - schemas: - '*': - src: - versionedSrc: 'silverstripe/versioned: _graphql' diff --git a/_graphql/config.yml b/_graphql/config.yml deleted file mode 100644 index 46ec11eb..00000000 --- a/_graphql/config.yml +++ /dev/null @@ -1,25 +0,0 @@ -modelConfig: - DataObject: - plugins: - versioning: - before: inheritance - operations: - copyToStage: - class: SilverStripe\Versioned\GraphQL\Operations\CopyToStageCreator - publish: - class: SilverStripe\Versioned\GraphQL\Operations\PublishCreator - unpublish: - class: SilverStripe\Versioned\GraphQL\Operations\UnpublishCreator - rollback: - class: SilverStripe\Versioned\GraphQL\Operations\RollbackCreator - read: - plugins: - readVersion: - before: paginateList - readOne: - plugins: - readVersion: - before: firstResult - delete: - plugins: - unpublishOnDelete: true diff --git a/_graphql/enums.yml b/_graphql/enums.yml deleted file mode 100644 index 8ebcb72c..00000000 --- a/_graphql/enums.yml +++ /dev/null @@ -1,50 +0,0 @@ -VersionedStage: - description: The stage to read from or write to - values: - DRAFT: - value: Stage - description: The draft stage - LIVE: - value: Live - description: The live stage - -VersionedQueryMode: - description: The versioned mode to use - values: - ARCHIVE: - value: archive - description: Read from a specific date of the archive - LATEST: - value: latest_versions - description: Read the latest version - ALL_VERSIONS: - value: all_versions - description: Reads all versionse - DRAFT: - value: Stage - description: Read from the draft stage - LIVE: - value: Live - description: Read from the live stage - STATUS: - value: status - description: Read only records with a specific status - VERSION: - value: version - description: Read a specific version - -VersionedStatus: - description: The stage to read from or write to - values: - PUBLISHED: - value: published - description: Only published records - DRAFT: - value: draft - description: Only draft records - ARCHIVED: - value: archived - description: Only records that have been archived - MODIFIED: - value: modified - description: Only records that have unpublished changes diff --git a/_graphql/types.yml b/_graphql/types.yml deleted file mode 100644 index dff0e8ae..00000000 --- a/_graphql/types.yml +++ /dev/null @@ -1,27 +0,0 @@ -CopyToStageInputType: - input: true - fields: - id: - type: ID! - description: The ID of the record to copy - fromVersion: - type: Int - description: The source version number to copy - fromStage: - type: VersionedStage - description: The source stage to copy - toStage: - type: VersionedStage - description: The destination state to copy to - -VersionedInputType: - input: true - fields: - mode: VersionedQueryMode = Stage - archiveDate: - type: String - description: The date to use for archive - status: - type: '[VersionedStatus]' - description: If mode is STATUS, specify which versioned statuses - version: Int diff --git a/composer.json b/composer.json index 81a842a6..a0d81c36 100755 --- a/composer.json +++ b/composer.json @@ -26,7 +26,6 @@ }, "require-dev": { "silverstripe/recipe-testing": "^4", - "silverstripe/graphql": "^6", "squizlabs/php_codesniffer": "^3.7", "silverstripe/standards": "^1", "phpstan/extension-installer": "^1.3" diff --git a/src/GraphQL/Operations/AbstractPublishOperationCreator.php b/src/GraphQL/Operations/AbstractPublishOperationCreator.php deleted file mode 100644 index 21c2d37b..00000000 --- a/src/GraphQL/Operations/AbstractPublishOperationCreator.php +++ /dev/null @@ -1,86 +0,0 @@ -getSourceClass(), Versioned::class)) { - return null; - } - - $plugins = $config['plugins'] ?? []; - $name = $config['name'] ?? null; - if (!$name) { - $name = $this->createOperationName($typeName); - } - return ModelMutation::create($model, $name) - ->setPlugins($plugins) - ->setType($typeName) - ->setResolver([VersionedResolver::class, 'resolvePublishOperation']) - ->addResolverContext('action', $this->getAction()) - ->addResolverContext('dataClass', $model->getSourceClass()) - ->addArg('id', 'ID!'); - } - - abstract protected function createOperationName(string $typeName): string; - - abstract protected function getAction(): string; -} diff --git a/src/GraphQL/Operations/CopyToStageCreator.php b/src/GraphQL/Operations/CopyToStageCreator.php deleted file mode 100644 index b690cda6..00000000 --- a/src/GraphQL/Operations/CopyToStageCreator.php +++ /dev/null @@ -1,78 +0,0 @@ -getSourceClass(), Versioned::class)) { - return null; - } - - $plugins = $config['plugins'] ?? []; - $mutationName = $config['name'] ?? null; - if (!$mutationName) { - $mutationName = 'copy' . ucfirst($typeName ?? '') . 'ToStage'; - } - - return ModelMutation::create($model, $mutationName) - ->setType($typeName) - ->setPlugins($plugins) - ->setResolver([VersionedResolver::class, 'resolveCopyToStage']) - ->addResolverContext('dataClass', $model->getSourceClass()) - ->addArg('input', 'CopyToStageInputType!'); - } -} diff --git a/src/GraphQL/Operations/PublishCreator.php b/src/GraphQL/Operations/PublishCreator.php deleted file mode 100644 index 6d03e882..00000000 --- a/src/GraphQL/Operations/PublishCreator.php +++ /dev/null @@ -1,44 +0,0 @@ -getSourceClass(), Versioned::class)) { - return null; - } - - $defaultPlugins = $this->config()->get('default_plugins'); - $configPlugins = $config['plugins'] ?? []; - $plugins = array_merge($defaultPlugins, $configPlugins); - $mutationName = 'rollback' . ucfirst($typeName ?? ''); - return ModelMutation::create($model, $mutationName) - ->setPlugins($plugins) - ->setType($typeName) - ->setResolver([VersionedResolver::class, 'resolveRollback']) - ->addResolverContext('dataClass', $model->getSourceClass()) - ->addArg('id', [ - 'type' => 'ID!', - 'description' => 'The object ID that needs to be rolled back' - ]) - ->addArg('toVersion', [ - 'type' => 'Int!', - 'description' => 'The version of the object that should be rolled back to', - ]); - } -} diff --git a/src/GraphQL/Operations/UnpublishCreator.php b/src/GraphQL/Operations/UnpublishCreator.php deleted file mode 100644 index f49e9641..00000000 --- a/src/GraphQL/Operations/UnpublishCreator.php +++ /dev/null @@ -1,44 +0,0 @@ -addResolverMiddleware( - [static::class, 'unpublishOnDelete'], - ['dataClass' => $mutation->getModel()->getSourceClass()] - ); - } - - /** - * @param array $context - * @return Closure - */ - public static function unpublishOnDelete(array $context) - { - $dataClass = $context['dataClass'] ?? null; - return function ($objects, array $args, array $context) use ($dataClass) { - if (!$dataClass) { - return; - } - if (!ViewableData::has_extension($dataClass, Versioned::class)) { - return; - } - DB::get_conn()->withTransaction(function () use ($args, $context, $dataClass) { - // Build list to filter - $objects = DataList::create($dataClass) - ->byIDs($args['ids']); - - foreach ($objects as $object) { - /** @var DataObject&Versioned $object */ - if (!$object->hasExtension(Versioned::class) || !$object->isPublished()) { - continue; - } - $member = UserContextProvider::get($context); - if (!$object->canUnpublish($member)) { - throw new Exception(sprintf( - 'Cannot unpublish %s with ID %s', - get_class($object), - $object->ID - )); - } - - $object->doUnpublish(); - } - }); - }; - } -} diff --git a/src/GraphQL/Plugins/VersionedDataObject.php b/src/GraphQL/Plugins/VersionedDataObject.php deleted file mode 100644 index b8c91028..00000000 --- a/src/GraphQL/Plugins/VersionedDataObject.php +++ /dev/null @@ -1,154 +0,0 @@ -addModelbyClassName(Member::class); - // Hack.. we can't add a plugin within a plugin, so we have to add sort - // and pagination manually. This requires ensuring the sort types are added - // to the schema (most of the time this is redundant) - if (!$schema->getType('SortDirection')) { - AbstractQuerySortPlugin::updateSchema($schema); - } - if (!$schema->getType('PageInfo')) { - PaginationPlugin::updateSchema($schema); - } - } - - /** - * @param ModelType $type - * @param Schema $schema - * @param array $config - * @throws SchemaBuilderException - */ - public function apply(ModelType $type, Schema $schema, array $config = []): void - { - $class = $type->getModel()->getSourceClass(); - Schema::invariant( - is_subclass_of($class, DataObject::class), - 'The %s plugin can only be applied to types generated by %s models', - __CLASS__, - DataObject::class - ); - if (!ViewableData::has_extension($class, Versioned::class)) { - return; - } - - $versionName = $type->getModel()->getTypeName() . 'Version'; - $memberType = $schema->getModelByClassName(Member::class); - Schema::invariant( - $memberType, - 'The %s class was not added as a model. Should have been done in %s::%s?', - Member::class, - __CLASS__, - 'updateSchema' - ); - $memberTypeName = $memberType->getModel()->getTypeName(); - $resolver = ['resolver' => [VersionedResolver::class, 'resolveVersionFields']]; - - $type->addField('version', 'Int', function (ModelField $field) { - $field->addResolverAfterware([ScalarDBField::class, 'resolve']); - }); - - $versionType = Type::create($versionName) - ->addField('author', ['type' => $memberTypeName] + $resolver) - ->addField('publisher', ['type' => $memberTypeName] + $resolver) - ->addField('published', ['type' => 'Boolean'] + $resolver) - ->addField('liveVersion', ['type' => 'Boolean'] + $resolver) - ->addField('deleted', ['type' => 'Boolean'] + $resolver) - ->addField('draft', ['type' => 'Boolean'] + $resolver) - ->addField('latestDraftVersion', ['type' => 'Boolean'] + $resolver); - - foreach ($type->getFields() as $field) { - $clone = clone $field; - $versionType->addField($clone->getName(), $clone); - } - foreach ($type->getInterfaces() as $interface) { - $versionType->addInterface($interface); - } - - $schema->addType($versionType); - $type->addField('versions', '[' . $versionName . ']', function (Field $field) use ($type, $schema, $config) { - $field->setResolver([VersionedResolver::class, 'resolveVersionList']) - ->addResolverContext('sourceClass', $type->getModel()->getSourceClass()); - SortPlugin::singleton()->apply($field, $schema, [ - 'resolver' => [static::class, 'sortVersions'], - 'fields' => [ 'version' => true ], - ]); - Paginator::singleton()->apply($field, $schema); - }); - } - - /** - * @param array $config - * @return Closure - */ - public static function sortVersions(array $config): Closure - { - $fieldName = $config['fieldName']; - return function (Sortable $list, array $args) use ($fieldName) { - $versionSort = $args[$fieldName]['version'] ?? null; - if ($versionSort) { - $list = $list->sort('Version', $versionSort); - } - - return $list; - }; - } -} diff --git a/src/GraphQL/Plugins/VersionedRead.php b/src/GraphQL/Plugins/VersionedRead.php deleted file mode 100644 index 847d47cf..00000000 --- a/src/GraphQL/Plugins/VersionedRead.php +++ /dev/null @@ -1,64 +0,0 @@ -getModel()->getSourceClass(); - if (!ViewableData::has_extension($class, Versioned::class)) { - return; - } - - // The versioned argument only affects global reading state. Should not - // apply to nested queries. - $rootQuery = $schema->getQueryType()->getFieldByName($query->getName()); - if (!$rootQuery) { - return; - } - - $query->addResolverAfterware([VersionedResolver::class, 'resolveVersionedRead']); - $query->addArg('versioning', 'VersionedInputType'); - } -} diff --git a/src/GraphQL/Resolvers/VersionFilters.php b/src/GraphQL/Resolvers/VersionFilters.php deleted file mode 100644 index d0e8816d..00000000 --- a/src/GraphQL/Resolvers/VersionFilters.php +++ /dev/null @@ -1,251 +0,0 @@ -validateArgs($versioningArgs); - - $mode = $versioningArgs['mode']; - switch ($mode) { - case Versioned::LIVE: - case Versioned::DRAFT: - Versioned::set_stage($mode); - break; - case 'archive': - $date = $versioningArgs['archiveDate']; - Versioned::set_reading_mode($mode); - Versioned::reading_archived_date($date); - break; - } - } - - /** - * @template T of DataObject - * @param DataList $list - * @param array $versioningArgs - * @throws InvalidArgumentException - * @return DataList - */ - public function applyToList(DataList $list, array $versioningArgs): DataList - { - if ($list instanceof RelationList) { - throw new InvalidArgumentException(sprintf( - 'Version filtering cannot be applied to instances of %s. Are you using the plugin on a nested query?', - get_class($list) - )); - } - if (!isset($versioningArgs['mode'])) { - return $list; - } - - $this->validateArgs($versioningArgs); - - $mode = $versioningArgs['mode']; - switch ($mode) { - case Versioned::LIVE: - case Versioned::DRAFT: - $list = $list - ->setDataQueryParam('Versioned.mode', 'stage') - ->setDataQueryParam('Versioned.stage', $mode); - break; - case 'archive': - $date = $versioningArgs['archiveDate']; - $list = $list - ->setDataQueryParam('Versioned.mode', 'archive') - ->setDataQueryParam('Versioned.date', $date); - break; - case 'all_versions': - $list = $list->setDataQueryParam('Versioned.mode', 'all_versions'); - break; - case 'latest_versions': - $list = $list->setDataQueryParam('Versioned.mode', 'latest_versions'); - break; - case 'status': - // When querying by Status we need to ensure both stage / live tables are present - /* @var DataObject&Versioned $sng */ - $sng = singleton($list->dataClass()); - $baseTable = $sng->baseTable(); - $liveTable = $sng->stageTable($baseTable, Versioned::LIVE); - $statuses = $versioningArgs['status']; - - // If we need to search archived records, we need to manually join draft table - if (in_array('archived', $statuses ?? [])) { - $list = $list - ->setDataQueryParam('Versioned.mode', 'latest_versions'); - // Join a temporary alias BaseTable_Draft, renaming this on execution to BaseTable - // See Versioned::augmentSQL() For reference on this alias - $draftTable = $sng->stageTable($baseTable, Versioned::DRAFT) . '_Draft'; - $list = $list - ->leftJoin( - $draftTable, - "\"{$baseTable}\".\"ID\" = \"{$draftTable}\".\"ID\"" - ); - } else { - // Use draft as base query mode (base join live) - $draftTable = $baseTable; - $list = $list - ->setDataQueryParam('Versioned.mode', 'stage') - ->setDataQueryParam('Versioned.stage', Versioned::DRAFT); - } - - // Always include live table - $list = $list->leftJoin( - $liveTable, - "\"{$baseTable}\".\"ID\" = \"{$liveTable}\".\"ID\"" - ); - - // Add all conditions - $conditions = []; - - // Modified exist on both stages, but differ - if (in_array('modified', $statuses ?? [])) { - $conditions[] = "\"{$liveTable}\".\"ID\" IS NOT NULL AND \"{$draftTable}\".\"ID\" IS NOT NULL" - . " AND \"{$draftTable}\".\"Version\" <> \"{$liveTable}\".\"Version\""; - } - - // Is deleted and sent to archive - if (in_array('archived', $statuses ?? [])) { - // Note: Include items staged for deletion for the time being, as these are effectively archived - // we could split this out into "staged for deletion" in the future - $conditions[] = "\"{$draftTable}\".\"ID\" IS NULL"; - } - - // Is on draft only - if (in_array('draft', $statuses ?? [])) { - $conditions[] = "\"{$liveTable}\".\"ID\" IS NULL AND \"{$draftTable}\".\"ID\" IS NOT NULL"; - } - - if (in_array('published', $statuses ?? [])) { - $conditions[] = "\"{$liveTable}\".\"ID\" IS NOT NULL"; - } - - // Validate that all statuses have been handled - if (empty($conditions) || count($statuses ?? []) !== count($conditions ?? [])) { - throw new InvalidArgumentException("Invalid statuses provided"); - } - $list = $list->whereAny(array_filter($conditions ?? [])); - break; - case 'version': - // Note: Only valid for ReadOne - $list = $list->setDataQueryParam([ - "Versioned.mode" => 'version', - "Versioned.version" => $versioningArgs['version'], - ]); - break; - default: - throw new InvalidArgumentException("Unsupported read mode {$mode}"); - } - - return $list; - } - - /** - * @throws InvalidArgumentException - * @param $versioningArgs - */ - public function validateArgs(array $versioningArgs) - { - $mode = $versioningArgs['mode']; - - switch ($mode) { - case Versioned::LIVE: - case Versioned::DRAFT: - break; - case 'archive': - if (empty($versioningArgs['archiveDate'])) { - throw new InvalidArgumentException(sprintf( - 'You must provide an ArchiveDate parameter when using the "%s" mode', - $mode - )); - } - $date = $versioningArgs['archiveDate']; - if (!$this->isValidDate($date)) { - throw new InvalidArgumentException(sprintf( - 'Invalid date: "%s". Must be YYYY-MM-DD format', - $date - )); - } - break; - case 'all_versions': - break; - case 'latest_versions': - break; - case 'status': - if (empty($versioningArgs['status'])) { - throw new InvalidArgumentException(sprintf( - 'You must provide a Status parameter when using the "%s" mode', - $mode - )); - } - break; - case 'version': - // Note: Only valid for ReadOne - if (!isset($versioningArgs['version'])) { - throw new InvalidArgumentException( - 'When using the "version" mode, you must specify a Version parameter' - ); - } - break; - default: - throw new InvalidArgumentException("Unsupported read mode {$mode}"); - } - } - - /** - * Returns true if date is in proper YYYY-MM-DD format - * @param string $date - * @return bool - */ - protected function isValidDate($date) - { - $dt = DateTime::createFromFormat('Y-m-d', $date); - if ($dt === false) { - return false; - } - // DateTime::getLastErrors() has an undocumented difference pre PHP 8.2 for what's returned - // if there are no errors - // https://www.php.net/manual/en/datetime.getlasterrors.php - $errors = $dt->getLastErrors(); - // PHP 8.2 - will return false if no errors - if ($errors === false) { - return true; - } - // PHP 8.2+ will only return an array containing a count of errors only if there are errors - // PHP < 8.2 will always return an array containing a count of errors even if there are no errors - return array_sum($errors) === 0; - } -} diff --git a/src/GraphQL/Resolvers/VersionedResolver.php b/src/GraphQL/Resolvers/VersionedResolver.php deleted file mode 100644 index bd8a5238..00000000 --- a/src/GraphQL/Resolvers/VersionedResolver.php +++ /dev/null @@ -1,280 +0,0 @@ -fieldName) { - case 'author': - return $obj->Author(); - case 'publisher': - return $obj->Publisher(); - case 'published': - return $obj->WasPublished; - case 'draft': - return $obj->WasDraft; - case 'deleted': - return $obj->WasDeleted; - case 'liveVersion': - return $obj->isLiveVersion(); - case 'latestDraftVersion': - return $obj->isLatestDraftVersion(); - } - - return null; - } - - /** - * @param array $resolverContext - * @return Closure - * @see VersionedDataObject - */ - public static function resolveVersionList(array $resolverContext): Closure - { - $sourceClass = $resolverContext['sourceClass']; - return function ($object, array $args, array $context, ResolveInfo $info) use ($sourceClass) { - /** @var DataObject|Versioned $object */ - if (!$object->hasExtension(Versioned::class)) { - throw new Exception(sprintf( - 'Types using the %s plugin must have the Versioned extension applied. (See %s)', - VersionedDataObject::class, - $sourceClass - )); - } - $member = UserContextProvider::get($context); - if (!$object->canViewStage(Versioned::DRAFT, $member)) { - throw new Exception(sprintf( - 'Cannot view versions on %s', - $sourceClass - )); - } - - // Get all versions - return VersionedResolver::getVersionsList($object); - }; - } - - private static function getVersionsList(DataObject $object) - { - $id = $object->ID ?: $object->OldID; - $class = DataObject::getSchema()->baseDataClass($object); - return Versioned::get_all_versions($class, $id); - } - - /** - * @template T of DataObject - * @param DataList $list - * @param array $args - * @param array $context - * @param ResolveInfo $info - * @return DataList - * @see VersionedRead - */ - public static function resolveVersionedRead(DataList $list, array $args, array $context, ResolveInfo $info) - { - if (!isset($args['versioning'])) { - return $list; - } - - // Set the reading state globally, we don't support mixing versioned states in the same query - Injector::inst()->get(VersionFilters::class) - ->applyToReadingState($args['versioning']); - - // Also set on the specific list - $list = Injector::inst()->get(VersionFilters::class) - ->applyToList($list, $args['versioning']); - - return $list; - } - - /** - * @param array $context - * @return Closure - * @see CopyToStageCreator - */ - public static function resolveCopyToStage(array $context): Closure - { - $dataClass = $context['dataClass'] ?? null; - return function ($object, array $args, $context, ResolveInfo $info) use ($dataClass) { - if (!$dataClass) { - return; - } - - $input = $args['input']; - $id = $input['id']; - $to = $input['toStage']; - /** @var Versioned|DataObject $record */ - $record = null; - if (isset($input['fromVersion'])) { - $from = $input['fromVersion']; - $record = Versioned::get_version($dataClass, $id, $from); - } elseif (isset($input['fromStage'])) { - $from = $input['fromStage']; - $record = Versioned::get_by_stage($dataClass, $from)->byID($id); - } else { - throw new InvalidArgumentException('You must provide either a FromStage or FromVersion argument'); - } - if (!$record) { - throw new InvalidArgumentException("Record {$id} not found"); - } - - $member = UserContextProvider::get($context); - // Permission check object - $can = $to === Versioned::LIVE - ? $record->canPublish($member) - : $record->canEdit($member); - if (!$can) { - throw new InvalidArgumentException(sprintf( - 'Copying %s from %s to %s is not allowed', - $this->getTypeName(), - $from, - $to - )); - } - - /** @var DataObject|Versioned $record */ - $record->copyVersionToStage($from, $to); - return $record; - }; - } - - /** - * @param array $context - * @return Closure - */ - public static function resolvePublishOperation(array $context) - { - $action = $context['action'] ?? null; - $dataClass = $context['dataClass'] ?? null; - $allowedActions = [ - AbstractPublishOperationCreator::ACTION_PUBLISH, - AbstractPublishOperationCreator::ACTION_UNPUBLISH, - ]; - if (!in_array($action, $allowedActions ?? [])) { - throw new InvalidArgumentException(sprintf( - 'Invalid publish action: %s', - $action - )); - } - - $isPublish = $action === AbstractPublishOperationCreator::ACTION_PUBLISH; - - return function ($obj, array $args, array $context, ResolveInfo $info) use ($isPublish, $dataClass) { - if (!$dataClass) { - return; - } - $stage = $isPublish ? Versioned::DRAFT : Versioned::LIVE; - $obj = Versioned::get_by_stage($dataClass, $stage) - ->byID($args['id']); - if (!$obj) { - throw new Exception(sprintf( - '%s with ID %s not found', - $dataClass, - $args['id'] - )); - } - $permissionMethod = $isPublish ? 'canPublish' : 'canUnpublish'; - $member = UserContextProvider::get($context); - if (!$obj->$permissionMethod($member)) { - throw new Exception(sprintf( - 'Not allowed to change published state of this %s', - $dataClass - )); - } - - try { - DB::get_conn()->withTransaction(function () use ($obj, $isPublish) { - if ($isPublish) { - $obj->publishRecursive(); - } else { - $obj->doUnpublish(); - } - }); - } catch (ValidationException $e) { - throw new Exception( - 'Could not changed published state of %s. Got error: %s', - $dataClass, - $e->getMessage() - ); - } - return $obj; - }; - } - - /** - * @param array $context - * @return Closure - * @see RollbackCreator - */ - public static function resolveRollback(array $context) - { - $dataClass = $context['dataClass'] ?? null; - return function ($obj, array $args, array $context, ResolveInfo $info) use ($dataClass) { - if (!$dataClass) { - return; - } - // Get the args - $id = $args['id']; - $rollbackVersion = $args['toVersion']; - - // Pull the latest version of the record - /** @var Versioned|DataObject $record */ - $record = Versioned::get_latest_version($dataClass, $id); - - // Assert permission - $user = UserContextProvider::get($context); - if (!$record->canEdit($user)) { - throw new InvalidArgumentException('Current user does not have permission to roll back this resource'); - } - - // Perform the rollback - $record = $record->rollbackRecursive($rollbackVersion); - - return $record; - }; - } -} diff --git a/tests/php/GraphQL/Fake/Fake.php b/tests/php/GraphQL/Fake/Fake.php deleted file mode 100644 index e2c56831..00000000 --- a/tests/php/GraphQL/Fake/Fake.php +++ /dev/null @@ -1,35 +0,0 @@ - "Varchar", - ]; - - private static $extensions = [ - Versioned::class, - ]; - - public function canView($member = null) - { - $extended = $this->extendedCan(__FUNCTION__, $member); - if ($extended !== null) { - return $extended; - } - return true; - } -} diff --git a/tests/php/GraphQL/Fake/FakeDataObjectStub.php b/tests/php/GraphQL/Fake/FakeDataObjectStub.php deleted file mode 100644 index f3b71d29..00000000 --- a/tests/php/GraphQL/Fake/FakeDataObjectStub.php +++ /dev/null @@ -1,36 +0,0 @@ - 'Varchar', - 'Editable' => 'Boolean', - ]; - - private static $defaults = [ - 'Editable' => true, - ]; - - private static $extensions = [ - Versioned::class, - ]; - - public static $rollbackCalled = false; - - public function canEdit($member = null) - { - return $this->Editable; - } - - public function rollbackRecursive($rollbackVersion) - { - FakeDataObjectStub::$rollbackCalled = true; - } -} diff --git a/tests/php/GraphQL/Fake/FakeResolveInfo.php b/tests/php/GraphQL/Fake/FakeResolveInfo.php deleted file mode 100644 index a171b5fa..00000000 --- a/tests/php/GraphQL/Fake/FakeResolveInfo.php +++ /dev/null @@ -1,34 +0,0 @@ - 'fake', 'type' => Type::string()]), - new \ArrayObject, - new ObjectType(['name' => 'fake']), - [], - new Schema([]), - [], - '', - new OperationDefinitionNode([]), - [] - ); - } -} diff --git a/tests/php/GraphQL/Plugins/VersionedDataObjectPluginTest.php b/tests/php/GraphQL/Plugins/VersionedDataObjectPluginTest.php deleted file mode 100644 index 488e6245..00000000 --- a/tests/php/GraphQL/Plugins/VersionedDataObjectPluginTest.php +++ /dev/null @@ -1,117 +0,0 @@ -markTestSkipped('Skipped GraphQL 4 test ' . __CLASS__); - } - } - - public function testPluginAddsVersionedFields() - { - $config = $this->createSchemaConfig(); - $model = DataObjectModel::create(Fake::class, $config); - $type = ModelType::create($model); - $type->addField('name'); - - $schema = new Schema('test', $config); - $schema->addModel($type); - $plugin = new VersionedDataObject(); - $plugin->updateSchema($schema); - $this->assertInstanceOf(ModelType::class, $schema->getModelByClassName(Member::class)); - - $plugin->apply($type, $schema); - $storableSchema = $schema->createStoreableSchema(); - $types = $storableSchema->getTypes(); - $this->assertArrayHasKey('FakeVersion', $types); - $versionType = $types['FakeVersion']; - $this->assertInstanceOf(Type::class, $versionType); - - $fields = ['author', 'publisher', 'published', 'liveVersion', 'latestDraftVersion']; - foreach ($fields as $fieldName) { - $field = $versionType->getFieldByName($fieldName); - $this->assertInstanceOf(Field::class, $field, 'Field ' . $fieldName . ' not found'); - $this->assertEquals(VersionedResolver::class . '::resolveVersionFields', $field->getResolver()->toString()); - } - - $fields = ['version', 'name']; - foreach ($fields as $fieldName) { - $field = $type->getFieldByName($fieldName); - $this->assertInstanceOf(Field::class, $field, 'Field ' . $fieldName . ' not found'); - // temorarily removed until BuildState API is in graphql ^4 - //$this->assertEquals(Resolver::class . '::resolve', $field->getEncodedResolver()->getRef()->toString()); - } - - $this->assertInstanceOf(Field::class, $type->getFieldByName('versions')); - } - - public function testPluginDoesntAddVersionedFieldsToUnversionedObjects() - { - Fake::remove_extension(Versioned::class); - $config = $this->createSchemaConfig(); - $type = ModelType::create(DataObjectModel::create(Fake::class, $config)); - $type->addField('Name'); - - $schema = new Schema('test', $config); - $schema->addModel($type); - $plugin = new VersionedDataObject(); - $plugin->updateSchema($schema); - - $plugin->apply($type, $schema); - $type = $schema->getType('FakeVersion'); - $this->assertNull($type); - - Fake::add_extension(Versioned::class); - } - - /** - * @return SchemaConfig - */ - private function createSchemaConfig(): SchemaConfig - { - return new SchemaConfig([ - 'modelCreators' => [ModelCreator::class], - ]); - } -} diff --git a/tests/php/GraphQL/Plugins/VersionedReadTest.php b/tests/php/GraphQL/Plugins/VersionedReadTest.php deleted file mode 100644 index 61b7fc01..00000000 --- a/tests/php/GraphQL/Plugins/VersionedReadTest.php +++ /dev/null @@ -1,72 +0,0 @@ -markTestSkipped('Skipped GraphQL 4 test ' . __CLASS__); - } - } - - public function testVersionedRead() - { - $config = $this->createSchemaConfig(); - $model = DataObjectModel::create(Fake::class, $config); - $query = ModelQuery::create($model, 'testQuery'); - $schema = new Schema('test', $config); - $schema->addQuery($query); - $plugin = new VersionedRead(); - $plugin->apply($query, $schema); - $this->assertCount(1, $query->getResolverAfterwares()); - $this->assertEquals( - VersionedResolver::class . '::resolveVersionedRead', - $query->getResolverAfterwares()[0]->getRef()->toString() - ); - $this->assertCount(1, $query->getArgs()); - $this->assertArrayHasKey('versioning', $query->getArgs()); - } - - /** - * @return SchemaConfig - */ - private function createSchemaConfig(): SchemaConfig - { - return new SchemaConfig([ - 'modelCreators' => [ModelCreator::class], - ]); - } -} diff --git a/tests/php/GraphQL/Resolvers/VersionedFiltersTest.php b/tests/php/GraphQL/Resolvers/VersionedFiltersTest.php deleted file mode 100644 index 113a7827..00000000 --- a/tests/php/GraphQL/Resolvers/VersionedFiltersTest.php +++ /dev/null @@ -1,291 +0,0 @@ -markTestSkipped('Skipped GraphQL 4 test ' . __CLASS__); - } - } - - public function testItValidatesArchiveDate() - { - $filter = new VersionFilters(); - $this->expectException(\InvalidArgumentException::class); - $this->expectExceptionMessageMatches('/ArchiveDate parameter/'); - $filter->validateArgs(['mode' => 'archive']); - } - - public function testItValidatesArchiveDateFormat() - { - $filter = new VersionFilters(); - $this->expectException(\InvalidArgumentException::class); - $this->expectExceptionMessageMatches('/Invalid date/'); - $filter->validateArgs(['mode' => 'archive', 'archiveDate' => '01/12/2018']); - } - - public function testItValidatesStatusParameter() - { - $filter = new VersionFilters(); - $this->expectException(\InvalidArgumentException::class); - $this->expectExceptionMessageMatches('/Status parameter/'); - $filter->validateArgs(['mode' => 'status']); - } - - public function testItValidatesVersionParameter() - { - $filter = new VersionFilters(); - $this->expectException(\InvalidArgumentException::class); - $this->expectExceptionMessageMatches('/Version parameter/'); - $filter->validateArgs(['mode' => 'version']); - } - - public function testItSetsReadingStateByMode() - { - Versioned::withVersionedMode(function () { - $filter = new VersionFilters(); - $filter->applyToReadingState(['mode' => Versioned::DRAFT]); - $this->assertEquals(Versioned::DRAFT, Versioned::get_stage()); - }); - } - - public function testItSetsReadingStateByArchiveDate() - { - Versioned::withVersionedMode(function () { - $filter = new VersionFilters(); - $filter->applyToReadingState(['mode' => 'archive', 'archiveDate' => '2018-01-01']); - $this->assertEquals('2018-01-01', Versioned::current_archived_date()); - }); - } - - public function testItFiltersByStageOnApplyToList() - { - $filter = new VersionFilters(); - $record1 = new Fake(); - $record1->Name = 'First version draft'; - $record1->write(); - - $record2 = new Fake(); - $record2->Name = 'First version live'; - $record2->write(); - $record2->copyVersionToStage(Versioned::DRAFT, Versioned::LIVE); - - $list = Fake::get(); - $list = $filter->applyToList($list, ['mode' => Versioned::DRAFT]); - $this->assertCount(2, $list); - - $list = Fake::get(); - $list = $filter->applyToList($list, ['mode' => Versioned::LIVE]); - $this->assertCount(1, $list); - } - - public function testItThrowsIfArchiveAndNoDateOnApplyToList() - { - $filter = new VersionFilters(); - $this->expectException(\InvalidArgumentException::class); - $this->expectExceptionMessageMatches('/ArchiveDate parameter/'); - $list = Fake::get(); - $filter->applyToList($list, ['mode' => 'archive']); - } - - public function testItThrowsIfArchiveAndInvalidDateOnApplyToList() - { - $filter = new VersionFilters(); - $this->expectException(\InvalidArgumentException::class); - $this->expectExceptionMessageMatches('/Invalid date/'); - $list = Fake::get(); - $filter->applyToList($list, ['mode' => 'archive', 'archiveDate' => 'foo']); - } - - - public function testItThrowsIfVersionAndNoVersionOnApplyToList() - { - $filter = new VersionFilters(); - $this->expectException(\InvalidArgumentException::class); - $this->expectExceptionMessageMatches('/Version parameter/'); - $list = Fake::get(); - $filter->applyToList($list, ['mode' => 'version']); - } - - public function testItSetsArchiveQueryParamsOnApplyToList() - { - $filter = new VersionFilters(); - $list = Fake::get(); - $list = $filter->applyToList( - $list, - [ - 'mode' => 'archive', - 'archiveDate' => '2016-11-08', - ] - ); - - $this->assertEquals('archive', $list->dataQuery()->getQueryParam('Versioned.mode')); - $this->assertEquals('2016-11-08', $list->dataQuery()->getQueryParam('Versioned.date')); - } - - public function testItSetsVersionQueryParamsOnApplyToList() - { - $filter = new VersionFilters(); - $list = Fake::get(); - $list = $filter->applyToList( - $list, - [ - 'mode' => 'version', - 'version' => '5', - ] - ); - - $this->assertEquals('version', $list->dataQuery()->getQueryParam('Versioned.mode')); - $this->assertEquals('5', $list->dataQuery()->getQueryParam('Versioned.version')); - } - - public function testItSetsLatestVersionQueryParamsOnApplyToList() - { - $filter = new VersionFilters(); - $list = Fake::get(); - $list = $filter->applyToList( - $list, - [ - 'mode' => 'latest_versions', - ] - ); - - $this->assertEquals('latest_versions', $list->dataQuery()->getQueryParam('Versioned.mode')); - } - - public function testItSetsAllVersionsQueryParamsOnApplyToList() - { - $filter = new VersionFilters(); - $list = Fake::get(); - $list = $filter->applyToList( - $list, - [ - 'mode' => 'all_versions', - ] - ); - - $this->assertEquals('all_versions', $list->dataQuery()->getQueryParam('Versioned.mode')); - } - - public function testItThrowsOnNoStatusOnApplyToList() - { - $filter = new VersionFilters(); - $this->expectException(\InvalidArgumentException::class); - $this->expectExceptionMessageMatches('/Status parameter/'); - $list = Fake::get(); - $filter->applyToList($list, ['mode' => 'status']); - } - - public function testStatusOnApplyToList() - { - $filter = new VersionFilters(); - $record1 = new Fake(); - $record1->Name = 'Only on draft'; - $record1->write(); - - $record2 = new Fake(); - $record2->Name = 'Published'; - $record2->write(); - $record2->copyVersionToStage(Versioned::DRAFT, Versioned::LIVE); - - $record3 = new Fake(); - $record2->Name = 'Will be modified'; - $record3->write(); - $record3->copyVersionToStage(Versioned::DRAFT, Versioned::LIVE); - $record3->Name = 'Modified'; - $record3->write(); - - $record4 = new Fake(); - $record4->Name = 'Will be archived'; - $record4->write(); - $record4->copyVersionToStage(Versioned::DRAFT, Versioned::LIVE); - $oldID = $record4->ID; - $record4->delete(); - $list = Fake::get(); - $list = $filter->applyToList( - $list, - [ - 'mode' => 'status', - 'status' => ['modified'] - ] - ); - $this->assertListEquals([['ID' => $record3->ID]], $list); - - $list = Fake::get(); - $list = $filter->applyToList( - $list, - [ - 'mode' => 'status', - 'status' => ['archived'] - ] - ); - $this->assertCount(1, $list); - $this->assertEquals($oldID, $list->first()->ID); - - $list = Fake::get(); - $list = $filter->applyToList( - $list, - [ - 'mode' => 'status', - 'status' => ['draft'] - ] - ); - - $this->assertCount(1, $list); - $ids = $list->column('ID'); - $this->assertContains($record1->ID, $ids); - - - $list = Fake::get(); - $list = $filter->applyToList( - $list, - [ - 'mode' => 'status', - 'status' => ['draft', 'modified'] - ] - ); - - $this->assertCount(2, $list); - $ids = $list->column('ID'); - - $this->assertContains($record3->ID, $ids); - $this->assertContains($record1->ID, $ids); - - $list = Fake::get(); - $list = $filter->applyToList( - $list, - [ - 'mode' => 'status', - 'status' => ['archived', 'modified'] - ] - ); - - $this->assertCount(2, $list); - $ids = $list->column('ID'); - $this->assertTrue(in_array($record3->ID, $ids ?? [])); - $this->assertTrue(in_array($oldID, $ids ?? [])); - } -} diff --git a/tests/php/GraphQL/Resolvers/VersionedResolverTest.php b/tests/php/GraphQL/Resolvers/VersionedResolverTest.php deleted file mode 100644 index a803fef0..00000000 --- a/tests/php/GraphQL/Resolvers/VersionedResolverTest.php +++ /dev/null @@ -1,292 +0,0 @@ -markTestSkipped('Skipped GraphQL 4 test ' . __CLASS__); - } - } - - public function testVersionedRead() - { - /* @var Fake|Versioned $record */ - $record = new Fake(); - $record->Name = 'First'; - $record->write(); - - $this->logInWithPermission('ADMIN'); - $member = Security::getCurrentUser(); - - $list = Fake::get(); - $resolvedList = VersionedResolver::resolveVersionedRead( - $list, - ['versioning' => [ - 'mode' => Versioned::LIVE - ]], - ['currentUser' => $member], - new FakeResolveInfo() - ); - $this->assertEquals( - $resolvedList->Count(), - 0, - 'Excludes draft records records in live mode' - ); - - $record->publishSingle(); - - $list = Fake::get(); - $resolvedList = VersionedResolver::resolveVersionedRead( - $list, - ['versioning' => [ - 'mode' => Versioned::LIVE - ]], - ['currentUser' => $member], - new FakeResolveInfo() - ); - $this->assertEquals( - $resolvedList->Count(), - 1, - 'Includes live records records in live mode' - ); - $this->assertEquals( - $resolvedList->First()->ID, - $record->ID, - 'Includes live records records in live mode' - ); - } - - public function testCopyToStage() - { - /* @var Fake|Versioned $record */ - $record = new Fake(); - $record->Name = 'First'; - $record->write(); // v1 - - $this->logInWithPermission('ADMIN'); - $member = Security::getCurrentUser(); - $resolve = VersionedResolver::resolveCopyToStage(['dataClass' => Fake::class]); - $resolve( - null, - [ - 'input' => [ - 'fromStage' => Versioned::DRAFT, - 'toStage' => Versioned::LIVE, - 'id' => $record->ID, - ], - ], - [ 'currentUser' => $member ], - new FakeResolveInfo() - ); - $recordLive = Versioned::get_by_stage(Fake::class, Versioned::LIVE) - ->byID($record->ID); - $this->assertNotNull($recordLive); - $this->assertEquals($record->ID, $recordLive->ID); - - $record->Name = 'Second'; - $record->write(); - $newVersion = $record->Version; - - $recordLive = Versioned::get_by_stage(Fake::class, Versioned::LIVE) - ->byID($record->ID); - $this->assertEquals('First', $recordLive->Title); - - // Invoke publish - $resolve( - null, - [ - 'input' => [ - 'fromVersion' => $newVersion, - 'toStage' => Versioned::LIVE, - 'id' => $record->ID, - ], - ], - [ 'currentUser' => $member ], - new FakeResolveInfo() - ); - $recordLive = Versioned::get_by_stage(Fake::class, Versioned::LIVE) - ->byID($record->ID); - $this->assertEquals('Second', $recordLive->Title); - - // Test error - $this->expectException(\InvalidArgumentException::class); - $resolve( - null, - [ - 'input' => [ - 'toStage' => Versioned::DRAFT, - 'id' => $record->ID, - ], - ], - [ 'currentUser' => new Member() ], - new FakeResolveInfo() - ); - } - - public function testPublish() - { - $record = new Fake(); - $record->Name = 'First'; - $record->write(); - - $result = Versioned::get_by_stage(Fake::class, Versioned::LIVE) - ->byID($record->ID); - - $this->assertNull($result); - $this->logInWithPermission('ADMIN'); - $member = Security::getCurrentUser(); - $resolve = VersionedResolver::resolvePublishOperation([ - 'dataClass' => Fake::class, - 'action' => AbstractPublishOperationCreator::ACTION_PUBLISH - ]); - $resolve( - null, - [ - 'id' => $record->ID - ], - [ 'currentUser' => $member ], - new FakeResolveInfo() - ); - $result = Versioned::get_by_stage(Fake::class, Versioned::LIVE) - ->byID($record->ID); - - $this->assertNotNull($result); - $this->assertInstanceOf(Fake::class, $result); - $this->assertEquals('First', $result->Name); - - $this->expectException(Exception::class); - $this->expectExceptionMessageMatches('/^Not allowed/'); - $resolve( - null, - [ - 'id' => $record->ID - ], - [ 'currentUser' => new Member() ], - new FakeResolveInfo() - ); - } - - public function testUnpublish() - { - $record = new Fake(); - $record->Name = 'First'; - $record->write(); - $record->copyVersionToStage(Versioned::DRAFT, Versioned::LIVE); - $result = Versioned::get_by_stage(Fake::class, Versioned::LIVE) - ->byID($record->ID); - - $this->assertNotNull($result); - $this->assertInstanceOf(Fake::class, $result); - $this->assertEquals('First', $result->Name); - - $this->logInWithPermission('ADMIN'); - $member = Security::getCurrentUser(); - $doResolve = VersionedResolver::resolvePublishOperation([ - 'dataClass' => Fake::class, - 'action' => AbstractPublishOperationCreator::ACTION_UNPUBLISH - ]); - $doResolve( - null, - [ - 'id' => $record->ID - ], - [ 'currentUser' => $member ], - new FakeResolveInfo() - ); - $result = Versioned::get_by_stage(Fake::class, Versioned::LIVE) - ->byID($record->ID); - - $this->assertNull($result); - - $record->copyVersionToStage(Versioned::DRAFT, Versioned::LIVE); - $this->expectException(Exception::class); - $this->expectExceptionMessageMatches('/^Not allowed/'); - $doResolve( - null, - [ - 'id' => $record->ID - ], - [ 'currentUser' => new Member() ], - new FakeResolveInfo() - ); - } - - public function testRollbackCannotBePerformedWithoutEditPermission() - { - // Create a fake version of our stub - $this->expectException(\InvalidArgumentException::class); - $this->expectExceptionMessageMatches('/Current user does not have permission to roll back this resource/'); - - $stub = FakeDataObjectStub::create(); - $stub->Name = 'First'; - $stub->Editable = false; - $stub->write(); - - $this->doRollbackMutation($stub); - } - - public function testRollbackRecursiveIsCalled() - { - // Create a fake version of our stub - $stub = FakeDataObjectStub::create(); - $stub->Name = 'First'; - $stub->write(); - - $this->doRollbackMutation($stub); - - $this->assertTrue($stub::$rollbackCalled, 'RollbackRecursive was called'); - } - - protected function doRollbackMutation(DataObject $stub, $toVersion = 1, $member = null) - { - if (!$stub->isInDB()) { - $stub->write(); - } - - $doRollback = VersionedResolver::resolveRollback(['dataClass' => get_class($stub)]); - $args = [ - 'id' => $stub->ID, - 'toVersion' => $toVersion, - ]; - - $doRollback( - null, - $args, - [ 'currentUser' => $member ?: Security::getCurrentUser() ], - new FakeResolveInfo() - ); - } -}