Skip to content
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

[Maybe bug?] Empty array does not show up in nested write #81

Open
steventhan opened this issue Sep 23, 2020 · 2 comments
Open

[Maybe bug?] Empty array does not show up in nested write #81

steventhan opened this issue Sep 23, 2020 · 2 comments

Comments

@steventhan
Copy link

E.g. a Company that has many Employee

company.employees = [];
company.save({ with: 'employees' });

This is the payload which doesn't include the relationships field:

{ "data": { "type": "companies" } }

It seems to be because of this line explicitly setting it to null
https://github.com/graphiti-api/spraypaint.js/blob/master/src/util/write-payload.ts#L115

I just wonder this is intended? (JSON:API spec allows replacement of the entire array: https://jsonapi.org/format/#crud-updating-resource-relationships)

@richmolj
Copy link
Contributor

Hey @steventhan yes this is by design. We want to default the relationship to [], so it introduces a somewhat likely scenario where you don't sideload anything (or sideload then remove) and accidentally send [] to the server wiping away all your data. We want to keep our dirty-tracking system without causing these major accidents.

We do have other ways to delete/disassociate relationships though https://www.graphiti.dev/guides/concepts/resources#sideposting

In theory we could support something like this with a special flag or something, but I tend to think it's not worth the effort as a less-common use case (with a big possible downside). If you have this scenario, consider a one-off separate resource for the special case, or maybe a magic attribute flag (ie company.deleteEmployees = true).

@tklaas
Copy link

tklaas commented Oct 26, 2023

I had the same problem and made a generic solution.
You can define the relations to check and detach when empty directly when loading the model:

Company
      .includes(['employees'])
      .find(123)
      .then(data => data.data.detachWhenEmpty(['employees']))

All you have to do is to extend your Base class with this class:

@Model()
export default class BaseModel extends SpraypaintBaseDetachRelationsWhenEmpty {
  static baseUrl = process.env.API_URL
  static apiNamespace = '/v1'

 // ...
}
import { isArray, isEmpty } from 'lodash';
import { Attr, SpraypaintBase } from 'spraypaint';
import { SaveOptions } from 'spraypaint/lib-esm/model';

export default class SpraypaintBaseDetachRelationsWhenEmpty extends SpraypaintBase {

  /**
   * hack to allow detaching all relations since spraypaint does not send an empty array
   * @see CommonRessourceRequest.php -> after()
   * https://github.com/graphiti-api/spraypaint.js/issues/81
   */

  @Attr() private detachRelationsByName: string[] = []

  detachRelationsWhenEmptyByName: string[] = []

  detachWhenEmpty(names: string|string[]): this {
    if(isArray(names)) {
      for(const name of names) {
        this.detachRelationsWhenEmptyByName.push(name)
      }
    } else {
      this.detachRelationsWhenEmptyByName.push(names)
    }
    return this
  }

  private checkDetachRelationsWhenEmpty() {
    for(const relationName of this.detachRelationsWhenEmptyByName) {
      // eslint-disable-next-line @typescript-eslint/ban-ts-comment
      // @ts-ignore
      const relation = this[relationName]
      if(isEmpty(relation)) {
        if(this.detachRelationsByName === null) {
          this.detachRelationsByName = []
        }
        this.detachRelationsByName.push(relationName)
      }
    }
  }

  save<I extends SpraypaintBase>(options?: SaveOptions<I>): Promise<boolean> {
    this.checkDetachRelationsWhenEmpty()
    return super.save(options);
  }

}

If you are using Laravel JSON:API in your backend, you can check the field dynamically, too:

CompanySchema.php

class CompanySchema extends CommonSchema {
    public function fields(): array
    {
        return self::withDefaults([
              // fields for company
        ]);
    }
}

CommonSchema.php

abstract class CommonSchema extends Schema {
    public static function withDefaults(array $fields): array {
        return array_merge($fields, [
            ArrayList::make('detachRelationsByName') // @see CommonRessourceRequest::after()
        ]);
    }
}

CompanyRequest.php

class CompanyRequest extends CommonRessourceRequest {
   // ...
}

CommonRessourceRequest.php

use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Validation\Validator;
use LaravelJsonApi\Laravel\Http\Requests\ResourceRequest;

abstract class CommonRessourceRequest extends ResourceRequest {

    /**
     * https://laravel.com/docs/10.x/validation#performing-additional-validation-on-form-requests
     *
     * @return array
     */
    public function after(): array {
        return [
            /**
             * hack to allow detaching all relations since spraypaint does not send an empty array
             * @see SpraypaintBaseDetachRelationsWhenEmpty.ts
             * https://github.com/graphiti-api/spraypaint.js/issues/81
             */
            function (Validator $validator) {
                $detachRelationsByName = request()?->input('data.attributes.detachRelationsByName');
                $model = $this->model();
                if(isset($detachRelationsByName, $model)) {
                    foreach ($detachRelationsByName as $relationName) {
                        if($model->isRelation($relationName)) {
                            $relation = $model->{$relationName}();
                            $data = $validator->getData();
                            unset($data[$relationName]); // prevent existing values being merged 
                            $validator->setData($data);
                            if($relation instanceof BelongsToMany) {
                                $relation->detach();
                            }
                            elseif($relation instanceof BelongsTo) {
                                $relation->disassociate();
                            }
                           // add other relation types if needed
                        }
                    }
                }
            }

        ];
    }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants