-
-
Notifications
You must be signed in to change notification settings - Fork 449
How to handle Postgres JSONB? #954
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Comments
I am not 100% sure if this is the best implementation of JSON type, I actually don't even remember where do I have it from, but it works for me. Just define custom scalar type: <?php
namespace App\GraphQL\Scalars;
use GraphQL\Type\Definition\ScalarType;
use GraphQL\Language\AST\BooleanValueNode;
use GraphQL\Language\AST\FloatValueNode;
use GraphQL\Language\AST\IntValueNode;
use GraphQL\Language\AST\ListValueNode;
use GraphQL\Language\AST\ObjectValueNode;
use GraphQL\Language\AST\StringValueNode;
/**
* Read more about scalars here http://webonyx.github.io/graphql-php/type-system/scalar-types/
*/
class JSON extends ScalarType
{
public $description =
'The `JSON` scalar type represents JSON values as specified by
[ECMA-404](http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf).';
/**
* Serializes an internal value to include in a response.
*
* @param mixed $value
* @return mixed
*/
public function serialize($value)
{
// Assuming the internal representation of the value is always correct
return $value;
}
/**
* Parses an externally provided value (query variable) to use as an input
*
* @param mixed $value
* @return mixed
*/
public function parseValue($value)
{
return $value;
}
/**
* Parses an externally provided literal value (hardcoded in GraphQL query) to use as an input.
*
* E.g.
* {
* user(email: "[email protected]")
* }
*
* @param \GraphQL\Language\AST\Node $valueNode
* @param mixed[]|null $variables
* @return mixed
*/
public function parseLiteral($valueNode, ?array $variables = null)
{
switch ($valueNode) {
case ($valueNode instanceof StringValueNode):
case ($valueNode instanceof BooleanValueNode):
return $valueNode->value;
case ($valueNode instanceof IntValueNode):
case ($valueNode instanceof FloatValueNode):
return floatval($valueNode->value);
case ($valueNode instanceof ObjectValueNode): {
$value = [];
foreach ($valueNode->fields as $field) {
$value[$field->name->value] = $this->parseLiteral($field->value);
}
return $value;
}
case ($valueNode instanceof ListValueNode):
return array_map([$this, 'parseLiteral'], $valueNode->values);
default:
return null;
}
}
} And you can use it in your schema: type Model {
attribute: JSON
} |
If I understand it right - hasura is a tool that generates schema.graphql from the postgres DB structure. I think it conflicts with the idea what lighthouse is. Lighthouse helps to write the schema, with useful helpers (directives) and it is actually built to work with eloquent models inside of laravel/lumen framework, instead of directly connect to DB. |
Thanks for the prompt response and the code! Yeah, Hasura basically generates a schema based on your existing database - it's truly fantastic! But this library seems really great too, so thanks guys and gals. Okay, cool. I didn't realise we could create custom types. I'm still in the process of going through all of the documentation and I'm pretty new to GraphQL in general. That seems really promising, though I'm confused why I don't see any I'll have to give it a try. Don't you think it could be good to have such a type built into Lighthouse? |
There is a package that implements a few types (from one of Lighthouse's contributors, Spawnia): https://github.com/mll-lab/graphql-php-scalars |
That is the point. You are also able to create custom resolver, thus you have pretty much freedom, I guess much more in comparison to hasura.
Yes, I was also confused about it. I think outgoing JSON is automatically serialized via eloquent, and that is why it works. Incoming JSON is kinda parsed... I didn't know spawnia has such a repo. Well, @sustained you can also try spawnias implementation: https://github.com/mll-lab/graphql-php-scalars/blob/master/src/JSON.php |
I think spawnia tries to keep lighthouse as light as possible. And JSON type/filed is actually not used in a simple projects - it is more special case, because you have to try to define your database without using JSON. In my case it is just a meta information, that may be very different, so it was easier to choose JSON type for particular field. Anyway, may be it would be cool, if we collect implementations (like list of links) of scalar types in docs? |
@lorado Okay, so here's the problem with your suggestion - I changed I don't specify the shape of the And that kind of goes against the whole GraphQL "the query structure matches the returned data structure" mantra, don't you think? I imagine that with true JSONB support instead the query would look like this - query {
course(id: 1) {
id
author {
name
}
levels {
title
words {
source,
target,
data {
form
type
}
}
}
}
} Do you see what I mean? |
I don't really know the JSONB type, but I think this can help you: I have created a directive to use with the
If it is more or less what you want, you will be able to query exactly the shape you want. For example I have this:
So I can query just what I want. |
That's really cool, I'll definitely have a play with that. Also I need to look into the schemaless attributes library - also seems useful. Thank you very much. |
Yes, but if you want to query only the shape you want, you have to define it in schema. There is no other way. And I think it doesn't matter if it is a JSONB or JSON - in laravel (PHP) it is handled as associative array. In enzonotario example his directive dinamically updates schema, and so you can achieve your goal ;) |
Nice! Let me close this since it seems to be solved. Feel free to re open if needed! |
@enzonotario do you have a copy of this It seems that |
Sure! It's more or less the same:
|
Thanks! Am I using it correctly? The directive doesn't seem to receive a "select" argument so it ends up executing, in effect:
instead of
Here are my configuration and results: type Part @model {
id: ID! @globalId
sku: String!
subgroup_details: PartSubgroupDetails @schemalessAttribute(source: "subgroup_details")
}
type PartSubgroupDetails {
area: Float
height: Float
length: Float
width: Float
} query Goods {
goods(first: 1, subgroup_id: 40) {
edges {
node {
id
part {
sku
subgroup_details {
area
}
}
}
}
}
} {
"data": {
"goods": {
"edges": [
{
"node": {
"id": "R29vZDoyNjMzMA==",
"part": {
"sku": "AB12",
"subgroup_details": null
}
}
}
]
}
}
} |
Did you try it using |
{
"data": {
"goods": {
"edges": [
{
"node": {
"id": "R29vZDoyNjMzMA==",
"part": {
"sku": "GPF1",
"subgroup_details": {
"area": null
}
}
}
}
]
}
}
} I can't wrap my head around how the resolver is supposed to retrieve the sub-selection ( BTW, thanks for you help. Your laravel-websockets example was also a huge time-saver! |
Are you using |
I am using laravel-schemaless-attributes. class Part extends Model
{
use HasSchemalessSubgroupDetails;
/**
* The attributes that should be cast to native types.
*
* @var array
*/
protected $casts = [
'subgroup_details' => 'array',
];
} trait HasSchemalessSubgroupDetails
{
public function getSubgroupDetailsAttribute(): SchemalessAttributes
{
return SchemalessAttributes::createForModel($this, 'subgroup_details');
}
public function scopeWithSubgroupDetails(): Builder
{
return SchemalessAttributes::scopeWithSchemalessAttributes('subgroup_details');
}
} I can also see the resolver retrieving the correct value. if ($root instanceof SchemalessAttributes) {
$value = $root->get($select);
} else {
$value = data_get($root, $select);
}
\Log::debug("\$value: $value"); |
Circling back to show what I ended up with: /**
* Allows definition of schema from attributes in JSON column.
* This means you can filter JSON attributes in your queries like
* you would with normal attributes.
*
* @example
* type Part @model {
* # Option A: Access directly on model
* substrate: String @schemalessAttribute(source: "subgroup_details" select: "area")
* # Option B: Access via sub-selection
* subgroup_details: PartSubgroupDetails @schemalessAttribute(source: "area")
* }
*
* @package App\GraphQL\Directives
*
* @see https://github.com/nuwave/lighthouse/issues/954#issuecomment-598498843 Source
*/
class SchemalessAttributeDirective extends BaseDirective implements FieldResolver
{
/**
* Name of the directive.
*
* @return string
*/
public function name()
{
return 'schemalessAttribute';
}
/**
* Resolve the field directive.
*
* @param FieldValue $value
*
* @return FieldValue
*/
public function resolveField(FieldValue $value)
{
$select = $this->directiveArgValue('select');
$source = $this->directiveArgValue('source', 'extra_attributes');
return $value->setResolver(
function ($root, array $args, GraphQLContext $context, ResolveInfo $resolveInfo) use ($select, $source) {
if ($source) {
$root = $root->{$source};
}
// If no selection is specified in the directive, assume we're moving on to a sub-selection
if (empty($select)) {
return $root;
}
if ($root instanceof SchemalessAttributes) {
$value = $root->get($select);
} else {
$value = data_get($root, $select);
}
return $value;
}
);
}
} |
Are you willing to provide a PR for this issue or aid in developing it?
I mean, in theory yes but PHP is really not my strong suit - I'm mostly a web dev.
Is your feature request related to a problem? Please describe.
I'm using PostgreSQL JSONB columns because they're fantastic but I have to type them as
String
in the GraphQL Schema.Describe the solution you'd like
It would be nice if we could at a minimum have some kind of
JSON
type or something and have the resolver automatically convert to/from strings/JSON for us, both for queries and mutations?But it could be taken much further, like what is possible using Hasura GraphQL engine.
Describe alternatives you've considered
JSON.parse
andJSON.stringify
calls when dealing with JSONB columns? 😦The text was updated successfully, but these errors were encountered: