spraypaint.js
spraypaint.js copied to clipboard
[Maybe bug?] Empty array does not show up in nested write
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)
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
).
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
}
}
}
}
];
}
}