0

I am using gqlgen, sqlx and pgx. Trying to use custom scalar to store as jonb type in postgres database.

// graph/model/item.go

type Attributes types.JSONText

// Marshal here
...

func (a *Attributes) UnmarshalGQL(v interface{}) error {
    switch v := v.(type) {
    case []byte:
        log.Println(" >> unmarshal.byte:", v)
        json.Unmarshal(v, &a)
        return nil
    case string:
        log.Println(" >> unmarshal.string:", v) // >> unmarshal.string: {"target": "localhost"}

        json.Unmarshal([]byte(v), a) // This gives `null` in postgres

        log.Println(" >> unmarshal.aT:", reflect.TypeOf(a)) // >> unmarshal.aT: *model.Attributes
        log.Println(" >> unmarshal.aV:", reflect.ValueOf(a)) // >> unmarshal.aV: &[]

        return nil
    default:
        return errors.New(fmt.Sprintf("Unsupported type: %T", v))
    }
}

The desired result of a *Attributes should be {"target": "localhost"}, to store as jsonb in postgres:

| id | quantity | attributes              |
|----|----------|-------------------------|
| 1  | 5        | {"target": "localhost"} |

What I am doing wrong?


Edit: Add sample mutation.

This is the sample mutation:

mutation itemCreate {
  itemCreate(input: {
    quantity: 5,
    attributes: "{\"target\": \"localhost\"}"
  })
}

Edit: Add sqlx query.

The query to insert:

func (d *ItemDb) ItemCreate(i *model.ItemInput) (*model.Item, error) {

    log.Println(" >> i.Attributes:", i.Attributes) // >> i.Attributes: &[123 34 116 97 114 103 101 116 34 58 32 34 108 111 99 97 108 104 111 115 116 34 125]

    item := &model.Item{}

    if err := d.Get(item, `INSERT INTO items
    (quantity, attributes)
    VALUES ($1, $2)
    RETURNING *`, i.Quantity, i.Attributes); err != nil {
        return nil, err
    }

    return item, nil
}
Swix
  • 1,883
  • 7
  • 33
  • 50
  • 1
    `string` can also be converted to `[]byte`, hence also to `Attributes`. If `v` represents literal json data, i.e. no need to be marshaled, then all you need to do is conversion, i.e. `*a = Attributes(v)` in both `[]byte` and `string` cases. – mkopriva Oct 21 '20 at 11:41
  • Oh, hello again @mkopriva Sorry, I've updated with sample mutation. I just tried with `*a = Attributes(v)` but it inserted `"eyJ0YXJnZXQiOiAibG9jYWxob3N0In0="` in database. – Swix Oct 21 '20 at 11:49
  • 1
    That would seem to be a problem with how you then store the data, not with how you unmarshal it. If you do `fmt.Println(string(*a))` *after* `*a = Attributes(v)` do you see the weird string, or do you see the json? – mkopriva Oct 21 '20 at 11:51
  • `fmt.Println(string(*a))` prints out `{"target": "localhost"}` – Swix Oct 21 '20 at 11:56
  • 1
    So it's correct, that's what you want right? As far as db storage, the problem is *probably* that the `Attributes` type is implementing the `driver.Valuer` interface incorrectly. – mkopriva Oct 21 '20 at 11:57
  • 1
    If your `Attributes` type has a `Value() (driver.Value, error)` method you should share it, it's probably invoking `json.Marshal` for no reason... at least your other code so far has been invoking `json.Marshal` and `json.Unmarshal` with raw json as the target argument which is causing you the problems you're seeing. Once you have json you don't need to marshal it, or unmarshal it if json is what you want. – mkopriva Oct 21 '20 at 12:03
  • I don't have `Value()` method. I think I have to convert it again before inserting into database. I have updated with sqlx query. – Swix Oct 21 '20 at 12:11
  • 1
    Inside `ItemCreate` if you do `log.Println(" >> i.Attributes:", string(*i.Attributes))` what do you get? – mkopriva Oct 21 '20 at 12:13
  • Bingo! It works. Thank you soooo much @mkopriva – Swix Oct 21 '20 at 12:17
  • 1
    Note that, whether you have to do the conversion before insert or not might depend on the driver being used. To ensure your custom type is always stored as raw bytes, regardless of the driver, you should implement the `Value` method which would do the conversion from `Attributes` to `[]byte`. i.e. `func (a Attributes) Value() (driver.Value, error) { return []byte(a), nil }`. – mkopriva Oct 21 '20 at 12:19
  • 1
    Thanks :) I just added `Value()` method as you suggested. – Swix Oct 21 '20 at 12:25

1 Answers1

1

If v contains literal json in either []byte or string then there's no need for json.Unmarshal, all that should be needed is a conversion.

func (a *Attributes) UnmarshalGQL(v interface{}) error {
    switch v := v.(type) {
    case []byte:
        *a = Attributes(v)
    case string:
        *a = Attributes(v)
    default:
        return errors.New(fmt.Sprintf("Unsupported type: %T", v))
    }
    return nil
}
mkopriva
  • 35,176
  • 4
  • 57
  • 71