BinarCode / laravel-restify

The fastest way to make a powerful JSON:API compatible Rest API with Laravel.
https://restify.binarcode.com
MIT License
604 stars 54 forks source link

Store/Update requests aren't working with JSONAPI payload format #562

Open maicol07 opened 1 year ago

maicol07 commented 1 year ago

Hi,

if I try to send to the POST endpoint (store) the following payload (compliant with JSONAPI specs):

{
  "type": "users",
  "attributes": {
    "username": "test",
    "email": "test@test.tl"
  },
  "relationships": {}
}

I get the following error:

{
  "message": "username is required. (and 1 more error)",
  "errors": {
    "username": [
      "username is required."
    ],
    "email": [
      "email is required."
    ]
  }
}

I've found out Restify only accepts a plain payload of type [attribute => value], like this:

{
    "username": "test",
    "email": "test@test.tl"
}

By the way, this works but due to this Restify isn't fully compliant with JSONAPI (the docs home page says "JSON:API consistency" under features).

Can you support the JSONAPI payload schema?

Thanks

maicol07 commented 1 year ago

As a workaround, I've added methods on my repository to support JSON API payloads, translating them into Restify ones:

/**
     * @psalm-suppress MissingParamType
     */
    public function allowToStore(RestifyRequest $request, $payload = null): RestifyRepository
    {
        $this->adaptJsonApiRequest($request);

        return parent::allowToStore($request, $payload);
    }

    /**
     * @psalm-suppress MissingParamType
     */
    public function allowToPatch(RestifyRequest $request, $payload = null): RestifyRepository
    {
        $this->adaptJsonApiRequest($request, true);

        return parent::allowToPatch($request, $payload);
    }

    public function getStoringRules(RestifyRequest $request): array
    {
        return $this->collectFields($request)->mapWithKeys(static fn (Field $k) => [
            ($k->label ?? $k->attribute) => $k->getStoringRules(),
        ])->toArray();
    }

    public function collectFields(RestifyRequest $request): FieldCollection
    {
        $fields = parent::collectFields($request);
        if ($request->isUpdateRequest()) {
            return $fields->map(static function (Field $field) {
                if (!($field instanceof BelongsTo)) {
                    // Fix to allow updating fields with custom labels
                    $field->label = $field->attribute;
                }

                return $field;
            });
        }

        return $fields;
    }

    /**
     * Adapt JSON:API request to Restify request.
     */
    protected function adaptJsonApiRequest(RestifyRequest $request, bool $snake_attributes = false): void
    {
        /** @var array<string, mixed> $attributes */
        $attributes = $request->input('data.attributes') ?? [];
        // Convert all keys to snake_case using Collections
        if ($snake_attributes) {
            $attributes = collect($attributes)
                ->mapWithKeys(static fn ($value, $key) => [Str::snake($key) => $value])
                ->toArray();
        }
        $relationships = $request->input('data.relationships') ?? [];

        // Get relationships in form of "relationshipName" → relationship_id
        $relationships = array_map(static fn (array $relationship): int => Arr::get($relationship, 'data.id'), $relationships);

        $request->replace([
            ...$attributes,
            ...$relationships,
        ]);
    }

Actually, it supports:

binaryk commented 1 year ago

That's a good idea to include support. Thanks @maicol07 , will make sure we prioritize this.

maicol07 commented 1 year ago

That's a good idea to include support. Thanks @maicol07 , will make sure we prioritize this.

By the way, we'll also how to figure out where to put pivot fields (I've read something on discuss.jsonapi.org where they indicate meta inside the the relationship)