diff --git a/_config/assets.yml b/_config/assets.yml index ecd1e7bf..c596dd70 100644 --- a/_config/assets.yml +++ b/_config/assets.yml @@ -3,20 +3,6 @@ Name: graphqlassets Only: moduleexists: 'silverstripe/assets' --- -SilverStripe\GraphQL\Schema\Schema: - schemas: - '*': - types: - DBFile: - fields: - filename: String - hash: String - variant: String - url: String - -SilverStripe\Assets\Storage\DBFile: - graphql_type: DBFile - SilverStripe\Assets\File: allowed_extensions: - graphql diff --git a/_config/dbargs.yml b/_config/dbargs.yml new file mode 100644 index 00000000..f09ff5b6 --- /dev/null +++ b/_config/dbargs.yml @@ -0,0 +1,18 @@ +--- +Name: graphql-db-args +--- +# For the DBFieldArgsPlugin, assign each DBField type an args factory +SilverStripe\ORM\FieldType\DBText: + graphql_args: SilverStripe\GraphQL\Schema\DataObject\Plugin\DBFieldArgs\DBTextArgs +SilverStripe\ORM\FieldType\DBHTMLText: + graphql_args: SilverStripe\GraphQL\Schema\DataObject\Plugin\DBFieldArgs\DBHTMLTextArgs +SilverStripe\ORM\FieldType\DBDate: + graphql_args: SilverStripe\GraphQL\Schema\DataObject\Plugin\DBFieldArgs\DBDateArgs +SilverStripe\ORM\FieldType\DBDateTime: + graphql_args: SilverStripe\GraphQL\Schema\DataObject\Plugin\DBFieldArgs\DBDatetimeArgs +SilverStripe\ORM\FieldType\DBTime: + graphql_args: SilverStripe\GraphQL\Schema\DataObject\Plugin\DBFieldArgs\DBTimeArgs +SilverStripe\ORM\FieldType\DBDecimal: + graphql_args: SilverStripe\GraphQL\Schema\DataObject\Plugin\DBFieldArgs\DBDecimalArgs +SilverStripe\ORM\FieldType\DBFloat: + graphql_args: SilverStripe\GraphQL\Schema\DataObject\Plugin\DBFieldArgs\DBFloatArgs diff --git a/_config/plugins.yml b/_config/plugins.yml index 611d7c43..3e9a23ea 100644 --- a/_config/plugins.yml +++ b/_config/plugins.yml @@ -12,4 +12,7 @@ SilverStripe\Core\Injector\Injector: - 'SilverStripe\GraphQL\Schema\DataObject\Plugin\CanViewPermission' - 'SilverStripe\GraphQL\Schema\DataObject\Plugin\FirstResult' - 'SilverStripe\GraphQL\Schema\DataObject\Plugin\InheritedPlugins' + - 'SilverStripe\GraphQL\Schema\DataObject\Plugin\DBFieldArgs\DBFieldArgsPlugin' + - 'SilverStripe\GraphQL\Schema\DataObject\Plugin\DBFieldTypes' - 'SilverStripe\GraphQL\Schema\Plugin\SortPlugin' + - 'SilverStripe\GraphQL\Schema\DataObject\Plugin\ScalarDBField' diff --git a/_config/schema-default.yml b/_config/schema-default.yml index 71fc5bb2..22b937f9 100644 --- a/_config/schema-default.yml +++ b/_config/schema-default.yml @@ -17,6 +17,7 @@ SilverStripe\GraphQL\Schema\Schema: config: modelConfig: DataObject: + parseShortcodes: true operations: read: plugins: diff --git a/_config/schema-global.yml b/_config/schema-global.yml index ef4bbd97..03fdfec7 100644 --- a/_config/schema-global.yml +++ b/_config/schema-global.yml @@ -22,11 +22,16 @@ SilverStripe\GraphQL\Schema\Schema: base_fields: ID: ID! plugins: + dbFieldArgs: true + dbFieldTypes: + ignore: + className: true + before: scalarDBField inheritance: useUnionQueries: false - hideAncestors: - - SilverStripe\CMS\Model\SiteTree after: 'versioning' + scalarDBField: + after: dbFieldArgs inheritedPlugins: after: '*' operations: diff --git a/src/Schema/DataObject/DataObjectModel.php b/src/Schema/DataObject/DataObjectModel.php index 901e2cdd..75b0cb22 100644 --- a/src/Schema/DataObject/DataObjectModel.php +++ b/src/Schema/DataObject/DataObjectModel.php @@ -11,6 +11,7 @@ use SilverStripe\GraphQL\Schema\Field\ModelQuery; use SilverStripe\GraphQL\Schema\Interfaces\BaseFieldsProvider; use SilverStripe\GraphQL\Schema\Interfaces\DefaultFieldsProvider; +use SilverStripe\GraphQL\Schema\Interfaces\ExtraTypeProvider; use SilverStripe\GraphQL\Schema\Interfaces\ModelBlacklist; use SilverStripe\GraphQL\Schema\Resolver\ResolverReference; use SilverStripe\GraphQL\Schema\SchemaConfig; @@ -114,18 +115,28 @@ public function getField(string $fieldName, array $config = []): ?ModelField return null; } + $hasExplicitType = isset($config['type']); + if ($result instanceof DBField) { $fieldConfig = array_merge([ 'type' => $result->config()->get('graphql_type'), ], $config); - return ModelField::create($fieldName, $fieldConfig, $this); + $modelField = ModelField::create($fieldName, $fieldConfig, $this); + if (!$hasExplicitType) { + $this->applyMetadataClass($modelField, get_class($result)); + } + return $modelField; } $class = $this->getModelClass($result); if (!$class) { if ($this->isList($result)) { - return ModelField::create($fieldName, $config, $this); + $modelField = ModelField::create($fieldName, $config, $this); + if (!$hasExplicitType) { + $this->applyMetadataClass($modelField, $class); + } + return $modelField; } return null; } @@ -136,6 +147,7 @@ public function getField(string $fieldName, array $config = []): ?ModelField ], $config); $query = ModelQuery::create($this, $fieldName, $queryConfig); $query->setDefaultPlugins($this->getModelConfiguration()->getNestedQueryPlugins()); + return $query; } return ModelField::create($fieldName, $type, $this); @@ -399,4 +411,15 @@ private function isList($result): bool { return $result instanceof SS_List || $result instanceof UnsavedRelationList; } + + /** + * @param ModelField $field + * @param string | null $class + * @throws SchemaBuilderException + */ + private function applyMetadataClass(ModelField $field, ?string $class = null): void + { + $field->getMetadata() + ->set('dataClass', $class); + } } diff --git a/src/Schema/DataObject/InterfaceBuilder.php b/src/Schema/DataObject/InterfaceBuilder.php index c4dc48e4..aaa07634 100644 --- a/src/Schema/DataObject/InterfaceBuilder.php +++ b/src/Schema/DataObject/InterfaceBuilder.php @@ -34,13 +34,20 @@ class InterfaceBuilder */ private $schema; + /** + * @var array + */ + private $hideAncestors = []; + /** * InterfaceBuilderTest constructor. * @param Schema $schema + * @param array $hideAncestors */ - public function __construct(Schema $schema) + public function __construct(Schema $schema, array $hideAncestors = []) { $this->setSchema($schema); + $this->hideAncestors = $hideAncestors; } /** @@ -86,7 +93,9 @@ public function createInterfaces(ModelType $modelType, array $interfaceStack = [ $interfaceStack[] = $interface; $modelType->addInterface($interface->getName()); - $chain = InheritanceChain::create($modelType->getModel()->getSourceClass()); + $chain = InheritanceChain::create($modelType->getModel()->getSourceClass()) + ->hideAncestors($this->hideAncestors); + foreach ($chain->getDirectDescendants() as $class) { if ($childType = $this->getSchema()->getModelByClassName($class)) { $this->createInterfaces($childType, $interfaceStack); @@ -130,6 +139,7 @@ public function applyBaseInterface(): InterfaceBuilder /** * @param ModelType $type + * @throws ReflectionException * @throws SchemaBuilderException * @return $this */ @@ -157,6 +167,14 @@ public function applyInterfacesToQueries(ModelType $type): InterfaceBuilder $this->schema->eagerLoad($modelType->getName()); } } + $chain = InheritanceChain::create($type->getModel()->getSourceClass()) + ->hideAncestors($this->hideAncestors); + + foreach ($chain->getDirectDescendants() as $class) { + if ($modelType = $schema->getModelByClassName($class)) { + $this->applyInterfacesToQueries($modelType); + } + } return $this; } diff --git a/src/Schema/DataObject/Plugin/DBFieldArgs/DBDateArgs.php b/src/Schema/DataObject/Plugin/DBFieldArgs/DBDateArgs.php new file mode 100644 index 00000000..fffd648b --- /dev/null +++ b/src/Schema/DataObject/Plugin/DBFieldArgs/DBDateArgs.php @@ -0,0 +1,92 @@ +getValues(), + 'Formatting options for fields that map to DBDate data types' + ); + } + + public function applyToField(ModelField $field): void + { + $field + ->addArg('format', [ + 'type' => $this->getEnum()->getName(), + 'description' => 'Formatting options for this field', + ]) + ->addArg('customFormat', [ + 'type' => 'String', + 'description' => 'If format is CUSTOM, the format string, e.g. "y-MM-dd HH:mm:ss"', + ]) + ->addResolverAfterware($this->getResolver()); + } + + /** + * @return callable + */ + protected function getResolver(): callable + { + return [static::class, 'resolve']; + } + + /** + * @param mixed $obj + * @param array $args + * @return DBField | string + * @throws Exception + */ + public static function resolve($obj, array $args) + { + if (!$obj instanceof DBDate) { + return $obj; + } + $format = $args['format'] ?? null; + $custom = $args['customFormat'] ?? null; + + if ($format === 'Format') { + if (!$custom) { + throw new Exception('The "custom" option requires a value for "customFormat"'); + } + return $obj->Format($custom); + } + if ($custom) { + throw new Exception('The "customFormat" argument should not be set for formats that are not "custom"'); + } + + if ($obj->hasMethod($format)) { + return $obj->obj($format); + } + + return $obj; + } + + public function getValues(): array + { + return [ + 'TIMESTAMP' => 'Timestamp', + 'NICE' => 'Nice', + 'DAY_OF_WEEK' => 'DayOfWeek', + 'MONTH' => 'Month', + 'YEAR' => 'Year', + 'SHORT_MONTH' => 'ShortMonth', + 'DAY_OF_MONTH' => 'DayOfMonth', + 'SHORT' => 'Short', + 'LONG' => 'Long', + 'FULL' => 'Full', + 'CUSTOM' => 'Format', + ]; + } +} diff --git a/src/Schema/DataObject/Plugin/DBFieldArgs/DBDatetimeArgs.php b/src/Schema/DataObject/Plugin/DBFieldArgs/DBDatetimeArgs.php new file mode 100644 index 00000000..c45a784e --- /dev/null +++ b/src/Schema/DataObject/Plugin/DBFieldArgs/DBDatetimeArgs.php @@ -0,0 +1,31 @@ +getValues(), + 'Formatting options for fields that map to DBDatetime data types' + ); + } + + public function getValues(): array + { + return array_merge( + parent::getValues(), + [ + 'DATE' => 'Date', + 'TIME' => 'Time', + 'TIME12' => 'Time12', + 'TIME24' => 'Time24', + ] + ); + } +} diff --git a/src/Schema/DataObject/Plugin/DBFieldArgs/DBDecimalArgs.php b/src/Schema/DataObject/Plugin/DBFieldArgs/DBDecimalArgs.php new file mode 100644 index 00000000..ed222a08 --- /dev/null +++ b/src/Schema/DataObject/Plugin/DBFieldArgs/DBDecimalArgs.php @@ -0,0 +1,45 @@ +getValues(), + 'Formatting options for fields that map to DBDecimal data types' + ); + } + + public function applyToField(ModelField $field): void + { + $field->addArg('format', [ + 'type' => $this->getEnum()->getName(), + 'description' => 'Formatting options for this field', + ])->addResolverAfterware( + $this->getResolver() + ); + } + + /** + * @return callable + */ + protected function getResolver(): callable + { + return [DBFieldArgs::class, 'baseFormatResolver']; + } + + + public function getValues(): array + { + return [ + 'INT' => 'Int', + ]; + } +} diff --git a/src/Schema/DataObject/Plugin/DBFieldArgs/DBFieldArgs.php b/src/Schema/DataObject/Plugin/DBFieldArgs/DBFieldArgs.php new file mode 100644 index 00000000..1ff09652 --- /dev/null +++ b/src/Schema/DataObject/Plugin/DBFieldArgs/DBFieldArgs.php @@ -0,0 +1,48 @@ +hasMethod($format)) { + return $obj->obj($format); + } + } + + return $obj; + } +} diff --git a/src/Schema/DataObject/Plugin/DBFieldArgs/DBFieldArgsPlugin.php b/src/Schema/DataObject/Plugin/DBFieldArgs/DBFieldArgsPlugin.php new file mode 100644 index 00000000..07e3e9ca --- /dev/null +++ b/src/Schema/DataObject/Plugin/DBFieldArgs/DBFieldArgsPlugin.php @@ -0,0 +1,66 @@ +addEnum(DBTextArgs::create()->getEnum()) + ->addEnum(DBHTMLTextArgs::create()->getEnum()) + ->addEnum(DBDecimalArgs::create()->getEnum()) + ->addEnum(DBFloatArgs::create()->getEnum()) + ->addEnum(DBDateArgs::create()->getEnum()) + ->addEnum(DBDatetimeArgs::create()->getEnum()) + ->addEnum(DBTimeArgs::create()->getEnum()); + } + + /** + * @param ModelType $type + * @param Schema $schema + * @param array $config + * @throws SchemaBuilderException + */ + public function apply(ModelType $type, Schema $schema, array $config = []): void + { + foreach ($type->getFields() as $field) { + if ($field instanceof ModelField && $field->getModel() instanceof DataObjectModel) { + $dataClass = $field->getMetadata()->get('dataClass'); + if (!$dataClass) { + continue; + } + $argFactory = Config::forClass($dataClass)->get('graphql_args'); + if ($argFactory) { + /* @var DBFieldArgs $inst */ + $inst = Injector::inst()->create($argFactory); + $inst->applyToField($field); + } + } + } + } +} diff --git a/src/Schema/DataObject/Plugin/DBFieldArgs/DBFloatArgs.php b/src/Schema/DataObject/Plugin/DBFieldArgs/DBFloatArgs.php new file mode 100644 index 00000000..1d9737d5 --- /dev/null +++ b/src/Schema/DataObject/Plugin/DBFieldArgs/DBFloatArgs.php @@ -0,0 +1,44 @@ +getValues(), + 'Formatting options for fields that map to DBFloat data types' + ); + } + + public function applyToField(ModelField $field): void + { + $field->addArg('format', [ + 'type' => $this->getEnum()->getName(), + 'description' => 'Formatting options for this field', + ])->addResolverAfterware( + $this->getResolver() + ); + } + + protected function getResolver(): callable + { + return [DBFieldArgs::class, 'baseFormatResolver']; + } + + public function getValues(): array + { + return [ + 'NICE' => 'Nice', + 'ROUND' => 'Round', + 'NICE_ROUND' => 'NiceRound', + ]; + } +} diff --git a/src/Schema/DataObject/Plugin/DBFieldArgs/DBHTMLTextArgs.php b/src/Schema/DataObject/Plugin/DBFieldArgs/DBHTMLTextArgs.php new file mode 100644 index 00000000..6e96f7b5 --- /dev/null +++ b/src/Schema/DataObject/Plugin/DBFieldArgs/DBHTMLTextArgs.php @@ -0,0 +1,51 @@ +addArg('parseShortcodes', [ + 'type' => 'Boolean', + 'description' => 'Parse shortcodes if true, do not parse if false. + If null, fallback on schema config setting', + ]); + } + + /** + * @param mixed $obj + * @param array $args + * @param array $context + * @return DBField + * @throws Exception + */ + public static function resolve($obj, array $args, array $context) + { + $result = parent::resolve($obj, $args, $context); + if (!$result instanceof DBHTMLText) { + return $result; + } + + /* @var DBHTMLText $obj */ + $parse = $args['parseShortcodes'] ?? null; + if ($parse === null) { + $config = SchemaConfigProvider::get($context); + if ($config) { + $parse = $config->getModelConfiguration('DataObject')->get('parseShortcodes', true); + } + } + $obj->setProcessShortcodes($parse); + + return $obj->RAW(); + } +} diff --git a/src/Schema/DataObject/Plugin/DBFieldArgs/DBTextArgs.php b/src/Schema/DataObject/Plugin/DBFieldArgs/DBTextArgs.php new file mode 100644 index 00000000..4523330e --- /dev/null +++ b/src/Schema/DataObject/Plugin/DBFieldArgs/DBTextArgs.php @@ -0,0 +1,95 @@ +getValues(), + 'Formatting options for fields that map to DBText data types' + ); + } + + public function getValues(): array + { + return [ + 'CONTEXT_SUMMARY' => 'ContextSummary', + 'FIRST_PARAGRAPH' => 'FirstParagraph', + 'LIMIT_SENTENCES' => 'LimitSentences', + 'SUMMARY' => 'Summary', + ]; + } + + public function applyToField(ModelField $field): void + { + $field + ->addArg('format', [ + 'type' => $this->getEnum()->getName(), + 'description' => 'Formatting options for this field', + ]) + ->addArg('limit', [ + 'type' => 'Int', + 'description' => 'An optional limit for the formatting option', + ]) + ->addResolverAfterware( + $this->getResolver() + ); + } + + /** + * @return callable + */ + protected function getResolver(): callable + { + return [static::class, 'resolve']; + } + + /** + * @param mixed $obj + * @param array $args + * @param array $context + */ + public static function resolve($obj, array $args, array $context) + { + if (!$obj instanceof DBText) { + return $obj; + } + $format = $args['format'] ?? null; + $limit = $args['limit'] ?? null; + + if (!$format) { + return $obj; + } + + $noArgMethods = ['FirstParagraph']; + + if ($limit && in_array($format, $noArgMethods)) { + throw new Exception(sprintf('Arg "limit" is not allowed for format "%s"', $format)); + } + + $result = DBFieldArgs::baseFormatResolver($obj, $args); + + // If no referential equality, the parent did something, so we're done. + if ($result !== $obj) { + return $result; + } + + if ($format) { + $args = $limit === null ? [] : [$limit]; + if ($obj->hasMethod($format)) { + return $obj->obj($format, $args); + } + } + + return $obj; + } +} diff --git a/src/Schema/DataObject/Plugin/DBFieldArgs/DBTimeArgs.php b/src/Schema/DataObject/Plugin/DBFieldArgs/DBTimeArgs.php new file mode 100644 index 00000000..e4045b52 --- /dev/null +++ b/src/Schema/DataObject/Plugin/DBFieldArgs/DBTimeArgs.php @@ -0,0 +1,83 @@ +getValues(), + 'Formatting options for fields that map to DBTime data types' + ); + } + + public function applyToField(ModelField $field): void + { + $field + ->addArg('format', [ + 'type' => $this->getEnum()->getName(), + 'description' => 'Formatting options for this field', + ]) + ->addArg('customFormat', [ + 'type' => 'String', + 'description' => 'If format is CUSTOM, the format string, e.g. "HH:mm:ss"', + ]) + ->addResolverAfterware($this->getResolver()); + } + + protected function getResolver(): callable + { + return [static::class, 'resolve']; + } + + /** + * @param mixed $obj + * @param array $args + * @return DBField | string + * @throws Exception + */ + public static function resolve($obj, array $args) + { + if (!$obj instanceof DBTime) { + return $obj; + } + $format = $args['format'] ?? null; + $custom = $args['customFormat'] ?? null; + + if ($format === 'Format') { + if (!$custom) { + throw new Exception('The "custom" option requires a value for "customFormat"'); + } + return $obj->Format($custom); + } + if ($custom) { + throw new Exception('The "customFormat" argument should not be set for formats that are not "custom"'); + } + + if ($obj->hasMethod($format)) { + return $obj->obj($format); + } + + return $obj; + } + + public function getValues(): array + { + return [ + 'TIMESTAMP' => 'Timestamp', + 'NICE' => 'Nice', + 'SHORT' => 'Short', + 'CUSTOM' => 'CUSTOM', + ]; + } +} diff --git a/src/Schema/DataObject/Plugin/DBFieldTypes.php b/src/Schema/DataObject/Plugin/DBFieldTypes.php new file mode 100644 index 00000000..ec060d28 --- /dev/null +++ b/src/Schema/DataObject/Plugin/DBFieldTypes.php @@ -0,0 +1,155 @@ +getFields() as $field) { + if ($field instanceof ModelField && $field->getModel() instanceof DataObjectModel) { + $ignored = $ignore[$type->getName()][$field->getName()] ?? null; + if (!!$ignored) { + continue; + } + $dataClass = $field->getMetadata()->get('dataClass'); + if (!$dataClass) { + continue; + } + if ($dataClass === DBEnum::class || is_subclass_of($dataClass, DBEnum::class)) { + $customName = $mapping[$type->getName()][$field->getName()] ?? null; + $this->applyEnum($type, $field, $schema, $customName); + } elseif ($dataClass === DBComposite::class || is_subclass_of($dataClass, DBComposite::class)) { + $this->applyComposite($field, $schema); + } + } + } + } + + /** + * @param ModelType $type + * @param ModelField $field + * @param Schema $schema + * @param string | null $customName + * @throws SchemaBuilderException + */ + private function applyEnum( + ModelType $type, + ModelField $field, + Schema $schema, + ?string $customName = null + ): void { + $sng = Injector::inst()->get($field->getModel()->getSourceClass()); + /* @var DBEnum $enumField */ + $enumField = $sng->dbObject($field->getPropertyName()); + if (!$enumField) { + return; + } + + $values = $enumField->enumValues(); + + // If another enum exists with the same values, recycle it. + $hash = md5(json_encode($values)); + $enum = null; + foreach ($schema->getEnums() as $candidate) { + $candidateHash = md5(json_encode($candidate->getValues())); + if ($candidateHash === $hash) { + $enum = $candidate; + break; + } + } + + if (!$enum) { + $enum = Enum::create( + $customName ?: sprintf('%sEnum', $field->getName()), + $values + ); + + // Name collision detection. If already exists, prefix with type. + $enums = $schema->getEnums(); + $existing = $enums[$enum->getName()] ?? null; + if ($existing) { + $enum->setName($type->getName() . $enum->getName()); + } + + $schema->addEnum($enum); + } + + $field->setType($enum->getName()); + } + + /** + * @param ModelField $field + * @param Schema $schema + * @throws SchemaBuilderException + */ + private function applyComposite(ModelField $field, Schema $schema): void + { + $sng = Injector::inst()->get($field->getModel()->getSourceClass()); + /* @var DBComposite $compositeField */ + $compositeField = $sng->dbObject($field->getPropertyName()); + if ($compositeField) { + $name = ClassInfo::shortName(get_class($compositeField)) . 'Composite'; + if (!$schema->getType($name)) { + $nestedDBFields = $compositeField->compositeDatabaseFields(); + $compositeType = Type::create($name); + foreach ($nestedDBFields as $nestedFieldName => $nestedFieldType) { + $graphqlType = Injector::inst()->get($nestedFieldType)->config()->get('graphql_type'); + $fieldName = FieldAccessor::formatField($nestedFieldName); + $compositeType->addField($fieldName, $graphqlType); + } + $schema->addType($compositeType); + } + + $field->setType($name); + $field->addResolverAfterware([static::class, 'resolveComposite']); + } + } + + public static function resolveComposite($obj, array $args) + { + if ($obj instanceof DBComposite) { + $result = []; + foreach ($obj->compositeDatabaseFields() as $fieldName => $type) { + $result[FieldAccessor::formatField($fieldName)] = $obj->$fieldName; + } + + return $result; + } + + return $obj; + } +} diff --git a/src/Schema/DataObject/Plugin/Inheritance.php b/src/Schema/DataObject/Plugin/Inheritance.php index cca0fc61..a68b8abc 100644 --- a/src/Schema/DataObject/Plugin/Inheritance.php +++ b/src/Schema/DataObject/Plugin/Inheritance.php @@ -57,8 +57,12 @@ public function apply(ModelType $type, Schema $schema, array $config = []): void $useUnions = $config['useUnionQueries'] ?? false; $hideAncestors = $config['hideAncestors'] ?? []; + if (in_array($type->getModel()->getSourceClass(), $hideAncestors)) { + return; + } + $inheritance = InheritanceBuilder::create($schema, $hideAncestors); - $interfaces = InterfaceBuilder::create($schema); + $interfaces = InterfaceBuilder::create($schema, $hideAncestors); $class = $type->getModel()->getSourceClass(); if ($inheritance->isLeafModel($class)) { @@ -66,14 +70,13 @@ public function apply(ModelType $type, Schema $schema, array $config = []): void } elseif ($inheritance->isBaseModel($class)) { $inheritance->fillDescendants($type); $interfaces->createInterfaces($type); - } - - if ($useUnions) { - InheritanceUnionBuilder::create($schema) - ->createUnions($type) - ->applyUnionsToQueries($type); - } else { - $interfaces->applyInterfacesToQueries($type); + if ($useUnions) { + InheritanceUnionBuilder::create($schema) + ->createUnions($type) + ->applyUnionsToQueries($type); + } else { + $interfaces->applyInterfacesToQueries($type); + } } } } diff --git a/src/Schema/DataObject/Plugin/ScalarDBField.php b/src/Schema/DataObject/Plugin/ScalarDBField.php new file mode 100644 index 00000000..34c3b8dd --- /dev/null +++ b/src/Schema/DataObject/Plugin/ScalarDBField.php @@ -0,0 +1,55 @@ +getFields() as $field) { + if (!$field instanceof ModelField || !$field->getModel() instanceof DataObjectModel) { + continue; + } + if (!$field->isList()) { + $field->addResolverAfterware([static::class, 'resolve']); + } + } + } + + /** + * @param $obj + * @return mixed + */ + public static function resolve($obj) + { + if ($obj instanceof DBField) { + return $obj->getValue(); + } + + return $obj; + } +} diff --git a/src/Schema/DataObject/ReadCreator.php b/src/Schema/DataObject/ReadCreator.php index fd68a42d..54545060 100644 --- a/src/Schema/DataObject/ReadCreator.php +++ b/src/Schema/DataObject/ReadCreator.php @@ -44,7 +44,7 @@ public function createOperation( } $query = ModelQuery::create($model, $queryName) - ->setType("[$typeName]") + ->setType("[$typeName!]!") ->setPlugins($plugins) ->setResolver([static::class, 'resolve']) ->setResolverContext([ diff --git a/src/Schema/DataObject/Resolver.php b/src/Schema/DataObject/Resolver.php index 6be094bc..f3dabe51 100644 --- a/src/Schema/DataObject/Resolver.php +++ b/src/Schema/DataObject/Resolver.php @@ -6,6 +6,7 @@ use GraphQL\Type\Definition\ResolveInfo; use SilverStripe\GraphQL\QueryHandler\SchemaConfigProvider; use SilverStripe\GraphQL\Schema\Exception\SchemaBuilderException; +use SilverStripe\GraphQL\Schema\SchemaConfig; use SilverStripe\ORM\DataList; use SilverStripe\ORM\DataObject; use SilverStripe\ORM\FieldType\DBField; @@ -27,23 +28,9 @@ class Resolver public static function resolve($obj, $args = [], $context = [], ?ResolveInfo $info = null) { $fieldName = $info->fieldName; - $context = SchemaConfigProvider::get($context); - $class = get_class($obj); - $resolvedField = null; - while (!$resolvedField && $class !== DataObject::class) { - $resolvedField = $context->mapFieldByClassName($class, $fieldName); - $class = get_parent_class($class); - } - - if (!$resolvedField) { - return null; - } - $result = FieldAccessor::singleton()->accessField($obj, $resolvedField[1]); - if ($result instanceof DBField) { - return $result->getValue(); - } + $config = SchemaConfigProvider::get($context); - return $result; + return static::getResolvedField($obj, $fieldName, $config); } /** @@ -66,4 +53,27 @@ public static function baseResolve($obj, $args = [], $context = [], ?ResolveInfo return $result; } + + /** + * @param $obj + * @param string $fieldName + * @param SchemaConfig $config + * @return mixed|null + * @throws SchemaBuilderException + */ + public static function getResolvedField($obj, string $fieldName, SchemaConfig $config) + { + $class = get_class($obj); + $resolvedField = null; + while (!$resolvedField && $class && $class !== DataObject::class) { + $resolvedField = $config->mapFieldByClassName($class, $fieldName); + $class = get_parent_class($class); + } + + if (!$resolvedField) { + return null; + } + + return FieldAccessor::singleton()->accessField($obj, $resolvedField[1]); + } } diff --git a/src/Schema/Field/ModelField.php b/src/Schema/Field/ModelField.php index deeaf065..c609a795 100644 --- a/src/Schema/Field/ModelField.php +++ b/src/Schema/Field/ModelField.php @@ -3,6 +3,7 @@ namespace SilverStripe\GraphQL\Schema\Field; +use SilverStripe\GraphQL\Config\Configuration; use SilverStripe\GraphQL\Schema\Exception\SchemaBuilderException; use SilverStripe\GraphQL\Schema\Interfaces\FieldPlugin; use SilverStripe\GraphQL\Schema\Interfaces\ModelFieldPlugin; @@ -27,6 +28,11 @@ class ModelField extends Field */ private $property; + /** + * @var Configuration + */ + private $metadata; + /** * ModelField constructor. * @param string $name @@ -36,6 +42,7 @@ class ModelField extends Field */ public function __construct(string $name, $config, SchemaModelInterface $model) { + $this->metadata = new Configuration(); $this->setModel($model); Schema::invariant( is_array($config) || is_string($config) || $config === true, @@ -137,6 +144,14 @@ public function getPropertyName(): string return $this->getProperty() ?: $this->getModel()->getPropertyForField($this->getName()); } + /** + * @return Configuration + */ + public function getMetadata(): Configuration + { + return $this->metadata; + } + /** * @param string $pluginName * @param $plugin diff --git a/src/Schema/Plugin/PaginationPlugin.php b/src/Schema/Plugin/PaginationPlugin.php index b676b731..26ed7321 100644 --- a/src/Schema/Plugin/PaginationPlugin.php +++ b/src/Schema/Plugin/PaginationPlugin.php @@ -13,6 +13,7 @@ use SilverStripe\GraphQL\Schema\Schema; use SilverStripe\GraphQL\Schema\Type\Type; use Countable; +use SilverStripe\GraphQL\Schema\Type\TypeReference; /** * Generic pagination functionality for a query that can be customised in subclasses @@ -91,8 +92,13 @@ public static function updateSchema(Schema $schema): void */ public function apply(Field $field, Schema $schema, array $config = []): void { + // Set the new return type + $plainType = $field->getNamedType(); + $fullType = $field->getType(); + $ref = TypeReference::create($fullType); + $defaultLimit = $config['defaultLimit'] ?? $this->config()->get('default_limit'); - $connectionName = $config['connection'] ?? $field->getName(); + $connectionName = $config['connection'] ?? $plainType; $max = $this->config()->get('max_limit'); $limit = min($defaultLimit, $max); $field->addArg('limit', "Int = $limit") @@ -102,15 +108,31 @@ public function apply(Field $field, Schema $schema, array $config = []): void ['maxLimit' => $max] ); - // Set the new return type - $plainType = $field->getNamedType(); + $existing = $schema->getState()->get(['connections', $fullType]); + if ($existing) { + $field->setType($existing, $ref->isRequired()); + return; + } + $connectionName = ucfirst($connectionName) . 'Connection'; - $field->setType($connectionName, true); + + // Dedupe. If the connection exists for the same type + // (possibly with different wrapper type, e.g. not required) + if ($existing = $schema->getType($connectionName)) { + $i = 1; + $rootConnectionName = $connectionName; + while ($schema->getType($connectionName)) { + $connectionName = $rootConnectionName . $i; + $i++; + } + } + + $field->setType($connectionName, $ref->isRequired()); // Create the edge type for this query $edgeType = Type::create($connectionName . 'Edge') ->setDescription('The collections edge') - ->addField('node', $plainType, function (Field $field) { + ->addField('node', "{$plainType}!", function (Field $field) { $field->setResolver([static::class, 'noop']) ->setDescription('The node at the end of the collections edge'); }); @@ -119,10 +141,13 @@ public function apply(Field $field, Schema $schema, array $config = []): void // Create the connection type for this query $connectionType = Type::create($connectionName) ->addField('edges', "[{$edgeType->getName()}!]!") - ->addField('nodes', "[{$plainType}!]!") + ->addField('nodes', $fullType) ->addField('pageInfo', 'PageInfo!'); $schema->addType($connectionType); + + // Cache the connection for this type so it can be reused + $schema->getState()->set(['connections', $fullType], $connectionType->getName()); } /** diff --git a/tests/Fake/DataObjectFake.php b/tests/Fake/DataObjectFake.php index 1d7a40c4..a30da95d 100644 --- a/tests/Fake/DataObjectFake.php +++ b/tests/Fake/DataObjectFake.php @@ -20,7 +20,12 @@ class DataObjectFake extends DataObject implements TestOnly private static $db = [ 'MyField' => 'Varchar', - 'MyInt' => 'Int' + 'MyInt' => 'Int', + 'MyDate' => 'Datetime', + 'MyCurrency' => 'Currency', + 'MyText' => 'Text', + 'MyEnum' => "Enum('ONE, TWO')", + 'MyMoney' => 'Money', ]; private static $has_one = [ diff --git a/tests/Schema/DataObject/Plugin/DBFieldArgs/DBDateArgsTest.php b/tests/Schema/DataObject/Plugin/DBFieldArgs/DBDateArgsTest.php new file mode 100644 index 00000000..b0b1ae78 --- /dev/null +++ b/tests/Schema/DataObject/Plugin/DBFieldArgs/DBDateArgsTest.php @@ -0,0 +1,55 @@ +applyToField($field); + $args = $field->getArgs(); + + $this->assertArrayHasKey('format', $args); + $arg = $args['format']; + $this->assertEquals($factory->getEnum()->getName(), $arg->getType()); + + $this->assertArrayHasKey('customFormat', $args); + $arg = $args['customFormat']; + $this->assertEquals('String', $arg->getType()); + } + + public function testResolve() + { + $fake = $this->getMockBuilder(DBDate::class) + ->setMethods(['Nice']) + ->getMock(); + $fake->expects($this->once()) + ->method('Nice'); + + DBDateArgs::resolve($fake, ['format' => 'Nice']); + + $date = DBField::create_field('Date', '123445789'); + $result = DBDateArgs::resolve($date, ['format' => 'FAIL']); + // Referential equality if method not found + $this->assertEquals($result, $date); + + $this->expectExceptionMessage('The "custom" option requires a value for "customFormat"'); + + DBDateArgs::resolve($date, ['format' => 'Custom']); + + $this->expectExceptionMessage('The "customFormat" argument should not be set for formats that are not "custom"'); + + DBDateArgs::resolve($date, ['customFormat' => 'test']); + } +} diff --git a/tests/Schema/DataObject/Plugin/DBFieldArgs/DBDatetimeArgsTest.php b/tests/Schema/DataObject/Plugin/DBFieldArgs/DBDatetimeArgsTest.php new file mode 100644 index 00000000..80a9350b --- /dev/null +++ b/tests/Schema/DataObject/Plugin/DBFieldArgs/DBDatetimeArgsTest.php @@ -0,0 +1,56 @@ +applyToField($field); + $args = $field->getArgs(); + + $this->assertArrayHasKey('format', $args); + $arg = $args['format']; + $this->assertEquals($factory->getEnum()->getName(), $arg->getType()); + + $this->assertArrayHasKey('customFormat', $args); + $arg = $args['customFormat']; + $this->assertEquals('String', $arg->getType()); + } + + public function testResolve() + { + $fake = $this->getMockBuilder(DBDatetime::class) + ->setMethods(['Time']) + ->getMock(); + $fake->expects($this->once()) + ->method('Time'); + + DBDatetimeArgs::resolve($fake, ['format' => 'Time']); + + $date = DBField::create_field('Datetime', '123445789'); + $result = DBDateArgs::resolve($date, ['format' => 'FAIL']); + // Referential equality if method not found + $this->assertEquals($result, $date); + + $this->expectExceptionMessage('The "custom" option requires a value for "customFormat"'); + + DBDateArgs::resolve($date, ['format' => 'Custom']); + + $this->expectExceptionMessage('The "customFormat" argument should not be set for formats that are not "custom"'); + + DBDateArgs::resolve($date, ['customFormat' => 'test']); + } +} diff --git a/tests/Schema/DataObject/Plugin/DBFieldArgs/DBDecimalArgsTest.php b/tests/Schema/DataObject/Plugin/DBFieldArgs/DBDecimalArgsTest.php new file mode 100644 index 00000000..b1c86902 --- /dev/null +++ b/tests/Schema/DataObject/Plugin/DBFieldArgs/DBDecimalArgsTest.php @@ -0,0 +1,25 @@ +applyToField($field); + $args = $field->getArgs(); + + $this->assertArrayHasKey('format', $args); + $arg = $args['format']; + $this->assertEquals($factory->getEnum()->getName(), $arg->getType()); + } +} diff --git a/tests/Schema/DataObject/Plugin/DBFieldArgs/DBFieldArgsPluginTest.php b/tests/Schema/DataObject/Plugin/DBFieldArgs/DBFieldArgsPluginTest.php new file mode 100644 index 00000000..740ec15f --- /dev/null +++ b/tests/Schema/DataObject/Plugin/DBFieldArgs/DBFieldArgsPluginTest.php @@ -0,0 +1,55 @@ +setFieldAccessor(new FieldAccessor()); + + $field1 = new ModelField('test1', [], $model); + $field1->getMetadata()->set('dataClass', DBText::class); + $field2 = new ModelField('test2', [], $model); + $field2->getMetadata()->set('dataClass', DBDatetime::class); + $field3 = new ModelField('test3', [], $model); + $field3->getMetadata()->set('dataClass', null); + + $type = new ModelType($model, [ + 'fields' => [ + 'test1' => $field1, + 'test2' => $field2, + 'test3' => $field3, + ] + ]); + $plugin = new DBFieldArgsPlugin(); + $plugin->apply($type, new Schema('test')); + + $field = $type->getFieldByName('test1'); + $args = $field->getArgs(); + $this->assertArrayHasKey('format', $args); + $this->assertArrayHasKey('limit', $args); + + $field = $type->getFieldByName('test2'); + $args = $field->getArgs(); + $this->assertArrayHasKey('format', $args); + $this->assertArrayHasKey('customFormat', $args); + + $field = $type->getFieldByName('test3'); + $args = $field->getArgs(); + $this->assertEmpty($args); + } +} diff --git a/tests/Schema/DataObject/Plugin/DBFieldArgs/DBFieldArgsTest.php b/tests/Schema/DataObject/Plugin/DBFieldArgs/DBFieldArgsTest.php new file mode 100644 index 00000000..f7fea636 --- /dev/null +++ b/tests/Schema/DataObject/Plugin/DBFieldArgs/DBFieldArgsTest.php @@ -0,0 +1,29 @@ +getMockBuilder(DBText::class) + ->setMethods(['FirstSentence']) + ->getMock(); + $fake->expects($this->once()) + ->method('FirstSentence'); + + DBFieldArgs::baseFormatResolver($fake, ['format' => 'FirstSentence']); + + $test = DBField::create_field('Text', 'test'); + $result = DBFieldArgs::baseFormatResolver($test, ['format' => 'FAIL']); + + // Referential equality if method not found + $this->assertEquals($result, $test); + } +} diff --git a/tests/Schema/DataObject/Plugin/DBFieldArgs/DBFloatArgsTest.php b/tests/Schema/DataObject/Plugin/DBFieldArgs/DBFloatArgsTest.php new file mode 100644 index 00000000..d8d0552e --- /dev/null +++ b/tests/Schema/DataObject/Plugin/DBFieldArgs/DBFloatArgsTest.php @@ -0,0 +1,25 @@ +applyToField($field); + $args = $field->getArgs(); + + $this->assertArrayHasKey('format', $args); + $arg = $args['format']; + $this->assertEquals($factory->getEnum()->getName(), $arg->getType()); + } +} diff --git a/tests/Schema/DataObject/Plugin/DBFieldArgs/DBHTMLTextArgsTest.php b/tests/Schema/DataObject/Plugin/DBFieldArgs/DBHTMLTextArgsTest.php new file mode 100644 index 00000000..8f06380e --- /dev/null +++ b/tests/Schema/DataObject/Plugin/DBFieldArgs/DBHTMLTextArgsTest.php @@ -0,0 +1,52 @@ +applyToField($field); + $args = $field->getArgs(); + + $this->assertArrayHasKey('format', $args); + $arg = $args['format']; + $this->assertEquals($factory->getEnum()->getName(), $arg->getType()); + + $this->assertArrayHasKey('limit', $args); + $arg = $args['limit']; + $this->assertEquals('Int', $arg->getType()); + } + + public function testResolve() + { + $fake = $this->getMockBuilder(DBHTMLText::class) + ->setMethods(['setProcessShortcodes']) + ->getMock(); + $fake->expects($this->exactly(4)) + ->method('setProcessShortcodes') + ->withConsecutive([true], [false], [false], [true]); + + $trueConfig = new SchemaConfig(); + $trueConfig->set('modelConfig.DataObject', ['parseShortcodes' => true]); + $falseConfig = new SchemaConfig(); + $falseConfig->set('modelConfig.DataObject', ['parseShortcodes' => false]); + + DBHTMLTextArgs::resolve($fake, ['parseShortcodes' => true], ['schemaConfig' => $falseConfig]); + DBHTMLTextArgs::resolve($fake, [], ['schemaConfig' => $falseConfig]); + DBHTMLTextArgs::resolve($fake, ['parseShortcodes' => false], ['schemaConfig' => $trueConfig]); + DBHTMLTextArgs::resolve($fake, [], ['schemaConfig' => $trueConfig]); + } +} diff --git a/tests/Schema/DataObject/Plugin/DBFieldArgs/DBTextArgsTest.php b/tests/Schema/DataObject/Plugin/DBFieldArgs/DBTextArgsTest.php new file mode 100644 index 00000000..173caa3b --- /dev/null +++ b/tests/Schema/DataObject/Plugin/DBFieldArgs/DBTextArgsTest.php @@ -0,0 +1,55 @@ +applyToField($field); + $args = $field->getArgs(); + + $this->assertArrayHasKey('format', $args); + $arg = $args['format']; + $this->assertEquals($factory->getEnum()->getName(), $arg->getType()); + + $this->assertArrayHasKey('parseShortcodes', $args); + $arg = $args['parseShortcodes']; + $this->assertEquals('Boolean', $arg->getType()); + } + + public function testResolve() + { + $fake = $this->getMockBuilder(DBText::class) + ->setMethods(['FirstParagraph']) + ->getMock(); + $fake->expects($this->once()) + ->method('FirstParagraph'); + + DBTextArgs::resolve($fake, ['format' => 'FirstParagraph'], []); + DBTextArgs::resolve($fake, [], []); + + $this->expectExceptionMessage('Arg "limit" is not allowed for format "FirstParagraph"'); + DBTextArgs::resolve($fake, ['format' => 'FirstParagraph', 'limit' => 5], []); + + $fake = $this->getMockBuilder(DBText::class) + ->setMethods(['LimitSentences']) + ->getMock(); + $fake->expects($this->once()) + ->method('LimitSentences') + ->with([5]); + + DBTextArgs::resolve($fake, ['format' => 'LimitSentences', 'limit' => 5], []); + } +} diff --git a/tests/Schema/DataObject/Plugin/DBFieldArgs/DBTimeArgsTest.php b/tests/Schema/DataObject/Plugin/DBFieldArgs/DBTimeArgsTest.php new file mode 100644 index 00000000..db24f901 --- /dev/null +++ b/tests/Schema/DataObject/Plugin/DBFieldArgs/DBTimeArgsTest.php @@ -0,0 +1,56 @@ +applyToField($field); + $args = $field->getArgs(); + + $this->assertArrayHasKey('format', $args); + $arg = $args['format']; + $this->assertEquals($factory->getEnum()->getName(), $arg->getType()); + + $this->assertArrayHasKey('customFormat', $args); + $arg = $args['customFormat']; + $this->assertEquals('String', $arg->getType()); + } + + public function testResolve() + { + $fake = $this->getMockBuilder(DBTime::class) + ->setMethods(['Nice']) + ->getMock(); + $fake->expects($this->once()) + ->method('Nice'); + + DBTimeArgs::resolve($fake, ['format' => 'Nice']); + + $time = DBField::create_field('Time', '123445789'); + $result = DBTimeArgs::resolve($time, ['format' => 'FAIL']); + // Referential equality if method not found + $this->assertEquals($result, $time); + + $this->expectExceptionMessage('The "custom" option requires a value for "customFormat"'); + + DBTimeArgs::resolve($time, ['format' => 'Custom']); + + $this->expectExceptionMessage('The "customFormat" argument should not be set for formats that are not "custom"'); + + DBTimeArgs::resolve($time, ['customFormat' => 'test']); + } +} diff --git a/tests/Schema/DataObject/Plugin/DBFieldTypesTest.php b/tests/Schema/DataObject/Plugin/DBFieldTypesTest.php new file mode 100644 index 00000000..019aa120 --- /dev/null +++ b/tests/Schema/DataObject/Plugin/DBFieldTypesTest.php @@ -0,0 +1,143 @@ +setFieldAccessor(new FieldAccessor()); + + $type = new ModelType($model); + $type->addAllFields(); + + $plugin = new DBFieldTypes(); + $plugin->apply($type, $schema = new Schema('test')); + + $enum = $type->getFieldByName('myEnum'); + $this->assertNotNull($enum); + $this->assertEquals('MyEnumEnum', $enum->getType()); + $this->assertNotNull($schema->getEnum('MyEnumEnum')); + + $composite = $type->getFieldByName('myMoney'); + $this->assertNotNull($composite); + $this->assertEquals('DBMoneyComposite', $composite->getType()); + $type = $schema->getType('DBMoneyComposite'); + $this->assertNotNull($type); + $this->assertNotNull($type->getFieldByName('currency')); + $this->assertNotNull($type->getFieldByName('amount')); + } + + public function testDeduplication() + { + $model = new DataObjectModel(DataObjectFake::class, new SchemaConfig()); + $model->setFieldAccessor(new FieldAccessor()); + + $type = new ModelType($model); + $type->addAllFields(); + + $plugin = new DBFieldTypes(); + $schema = new Schema('test'); + $schema->addEnum(Enum::create( + 'MyEnumEnum', + ['Test' => 'Test'] + )); + $plugin->apply($type, $schema); + + $enum = $type->getFieldByName('myEnum'); + $this->assertNotNull($enum); + $this->assertEquals('DataObjectFakeMyEnumEnum', $enum->getType()); + $this->assertNotNull($schema->getEnum('DataObjectFakeMyEnumEnum')); + $this->assertNotNull($schema->getEnum('MyEnumEnum')); + } + + public function testEnumReuse() + { + $model = new DataObjectModel(DataObjectFake::class, new SchemaConfig()); + $model->setFieldAccessor(new FieldAccessor()); + + $type = new ModelType($model); + $type->addAllFields(); + + $plugin = new DBFieldTypes(); + $schema = new Schema('test'); + $schema->addEnum(Enum::create( + 'MyEnumEnum', + [ + 'ONE' => 'ONE', + 'TWO' => 'TWO', + ] + )); + $plugin->apply($type, $schema); + + $enum = $type->getFieldByName('myEnum'); + $this->assertNotNull($enum); + $this->assertEquals('MyEnumEnum', $enum->getType()); + $this->assertNotNull($schema->getEnum('MyEnumEnum')); + // Did not create a new type because it found an existing one with the same signature. + $this->assertNull($schema->getEnum('DataObjectFakeMyEnumEnum')); + } + + public function testEnumCustomName() + { + $model = new DataObjectModel(DataObjectFake::class, new SchemaConfig()); + $model->setFieldAccessor(new FieldAccessor()); + + $type = new ModelType($model); + $type->addAllFields(); + + $plugin = new DBFieldTypes(); + $schema = new Schema('test'); + $plugin->apply($type, $schema, [ + 'enumTypeMapping' => [ + 'DataObjectFake' => [ + 'myEnum' => 'CustomTypeName', + ] + ] + ]); + + $enum = $type->getFieldByName('myEnum'); + $this->assertNotNull($enum); + $this->assertEquals('CustomTypeName', $enum->getType()); + $this->assertNotNull($schema->getEnum('CustomTypeName')); + $this->assertNull($schema->getEnum('DataObjectFakeMyEnumEnum')); + $this->assertNull($schema->getEnum('MyEnumEnum')); + } + + public function testEnumIgnore() + { + $model = new DataObjectModel(DataObjectFake::class, new SchemaConfig()); + $model->setFieldAccessor(new FieldAccessor()); + + $type = new ModelType($model); + $type->addAllFields(); + + $plugin = new DBFieldTypes(); + $schema = new Schema('test'); + $plugin->apply($type, $schema, [ + 'ignore' => [ + 'DataObjectFake' => [ + 'myEnum' => true, + ] + ] + ]); + + $notEnum = $type->getFieldByName('myEnum'); + $this->assertNotNull($notEnum); + $this->assertEquals('String', $notEnum->getType()); + $this->assertNull($schema->getEnum('DataObjectFakeMyEnumEnum')); + $this->assertNull($schema->getEnum('MyEnumEnum')); + } +} diff --git a/tests/Schema/DataObject/Plugin/InheritanceTest.php b/tests/Schema/DataObject/Plugin/InheritanceTest.php index cbe6e85d..b61fed44 100644 --- a/tests/Schema/DataObject/Plugin/InheritanceTest.php +++ b/tests/Schema/DataObject/Plugin/InheritanceTest.php @@ -3,6 +3,7 @@ namespace SilverStripe\GraphQL\Tests\Schema\DataObject\Plugin; +use SilverStripe\Core\ClassInfo; use SilverStripe\Core\Injector\Factory; use SilverStripe\Core\Injector\Injector; use SilverStripe\Dev\SapphireTest; @@ -33,6 +34,7 @@ use SilverStripe\GraphQL\Tests\Schema\DataObject\FakeInheritanceUnionBuilder; use SilverStripe\GraphQL\Tests\Schema\DataObject\FakeInterfaceBuilder; use SilverStripe\GraphQL\Tests\Schema\DataObject\TestSchema; +use SilverStripe\ORM\DataObject; class InheritanceTest extends SapphireTest { @@ -72,10 +74,19 @@ public function testInheritance($unions = false) } $schema->createStoreableSchema(); - $allModels = array_map(function ($class) use ($schema) { + $baseClasses = array_filter(static::$extra_dataobjects, function ($class) { + return DataObject::singleton($class)->baseClass() === $class; + }); + $leafClasses = array_filter(static::$extra_dataobjects, function ($class) { + return empty(ClassInfo::subclassesFor($class, false)); + }); + $leafModels = array_map(function ($class) use ($schema) { return $schema->getConfig()->getTypeNameForClass($class); - }, static::$extra_dataobjects); + }, $leafClasses); + $baseModels = array_map(function ($class) use ($schema) { + return $schema->getConfig()->getTypeNameForClass($class); + }, $baseClasses); FakeInheritanceUnionBuilder::reset(); FakeInterfaceBuilder::reset(); @@ -108,11 +119,11 @@ public function testInheritance($unions = false) if ($unions) { $this->assertCalls( - $allModels, + $baseModels, FakeInheritanceUnionBuilder::$applyCalls ); $this->assertCalls( - $allModels, + $baseModels, FakeInheritanceUnionBuilder::$createCalls ); $this->assertEmpty(FakeInterfaceBuilder::$applyCalls); @@ -120,26 +131,26 @@ public function testInheritance($unions = false) $this->assertEmpty(FakeInheritanceUnionBuilder::$createCalls); $this->assertEmpty(FakeInheritanceUnionBuilder::$applyCalls); $this->assertCalls( - $allModels, + $baseModels, FakeInterfaceBuilder::$applyCalls ); $this->assertCalls( - ['A', 'B', 'C'], + $baseModels, FakeInterfaceBuilder::$createCalls ); } $this->assertCalls( - ['A1a', 'A1b', 'A2a', 'B1a', 'B1b', 'B2', 'C1', 'C2a'], + $leafModels, FakeInheritanceBuilder::$ancestryCalls ); $this->assertCalls( - ['A', 'B', 'C'], + $baseModels, FakeInheritanceBuilder::$descendantCalls ); $this->assertCalls( - ['A', 'B', 'C'], + $baseModels, FakeInterfaceBuilder::$createCalls ); } @@ -153,8 +164,8 @@ private function assertCalls(array $expected, array $actual) $expected = array_map('strtolower', $expected); $compare = array_map('strtolower', array_keys($actual)); - $this->assertEmpty(array_diff($expected, $compare)); - $this->assertEmpty(array_diff($compare, $expected)); + $this->assertEmpty(array_diff($expected, $compare), 'Expected calls exceed the actual calls'); + $this->assertEmpty(array_diff($compare, $expected), 'Actual calls exceed the expected calls'); } public function provideUnionOption() diff --git a/tests/Schema/IntegrationTest.php b/tests/Schema/IntegrationTest.php index ec68be1d..1fe360f7 100644 --- a/tests/Schema/IntegrationTest.php +++ b/tests/Schema/IntegrationTest.php @@ -992,6 +992,42 @@ public function testQueriesAndMutations() } } + + public function testDBFieldArgs() + { + $schema = $this->createSchema(new TestSchemaBuilder(['_' . __FUNCTION__])); + $this->assertSchemaHasType($schema, 'DataObjectFake'); + $obj = DataObjectFake::create([ + 'MyField' => 'This is a varchar field', + 'MyDate' => '1582995600', // 29 Feb 2020 17h + 'MyCurrency' => '204.75', + 'MyText' => 'This is a really long text field. It has a few sentences. Just filling some space now.', + ]); + $obj->write(); + + $query = <<querySchema($schema, $query); + $this->assertSuccess($result); + $node = $result['data']['readOneDataObjectFake'] ?? null; + $this->assertEquals('This is a varchar field', $node['myField']); + $this->assertEquals('Saturday', $node['date1']); + $this->assertEquals('2/29/20, 5:00 PM', $node['date2']); + $this->assertEquals('5:00:00 PM', $node['date3']); + $this->assertEquals('2020', $node['date4']); + $this->assertEquals('This is a really long text field. It has a few sentences.', $node['myText']); + } + /** * @param TestSchemaBuilder $factory * @return Schema diff --git a/tests/Schema/_testDBFieldArgs/models.yml b/tests/Schema/_testDBFieldArgs/models.yml new file mode 100644 index 00000000..f2abe02b --- /dev/null +++ b/tests/Schema/_testDBFieldArgs/models.yml @@ -0,0 +1,4 @@ +SilverStripe\GraphQL\Tests\Fake\DataObjectFake: + fields: '*' + operations: + readOne: true