Skip to content

feat(data-service): add json-patch support#1023

Merged
mediremi merged 13 commits intomasterfrom
feat/add-json-patch-support
Mar 15, 2022
Merged

feat(data-service): add json-patch support#1023
mediremi merged 13 commits intomasterfrom
feat/add-json-patch-support

Conversation

@HendrikThePendric
Copy link
Copy Markdown
Contributor

@HendrikThePendric HendrikThePendric commented Sep 22, 2021

This introduces JSON Patch support for the useDataMutation hook, via an explicit type. The data property needs to be a valid patch array. It is entirely up to the user to ensure this patch is valid, we do no form of validation in app-runtime.

A JSON Patch mutation object would as follows:

const updateMutation = {
    type: 'jsonPatch',  // The new type
    resource: 'userGroups',
    id: 'xyz123',
    data: [ // patch array
        { op: 'replace', path: '/baz', value: 'boo' },
        { op: 'add', path: '/hello', value: ['world'] },
        { op: 'remove', path: '/foo' },
    ],
}

@HendrikThePendric HendrikThePendric force-pushed the feat/add-json-patch-support branch from 573dfa9 to aeda8dd Compare October 14, 2021 12:03
Comment thread services/data/src/links/RestAPILink/queryToRequestOptions.test.ts Outdated
@dhis2 dhis2 deleted a comment from Mohammer5 Oct 14, 2021
@HendrikThePendric
Copy link
Copy Markdown
Contributor Author

FYI: I placed a few comments before that referred to my original implementation and @Mohammer5 replied to one of these comments. By now I've gone with a different type of implementation and the comments did not make much sense anymore. So I removed my comments and the one from Jan-Gerke as well.

Comment thread services/data/src/links/RestAPILink/queryToRequestOptions.ts Outdated
Copy link
Copy Markdown
Contributor

@Mohammer5 Mohammer5 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

Copy link
Copy Markdown
Contributor

@mediremi mediremi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The data engine's validation helpers need to be updated so that jsonPatch is a valid type:

const validTypes = ['read', 'create', 'update', 'replace', 'delete']

@mediremi mediremi self-assigned this Nov 3, 2021
@mediremi mediremi marked this pull request as draft November 3, 2021 16:46
@varl varl self-requested a review November 4, 2021 08:02
@varl
Copy link
Copy Markdown
Contributor

varl commented Nov 4, 2021

Is this intentionally still a draft ?

@mediremi mediremi marked this pull request as ready for review November 4, 2021 16:22
@mediremi
Copy link
Copy Markdown
Contributor

mediremi commented Nov 4, 2021

@varl should be good to rereview now

Comment thread docs/types/Mutation.md Outdated
| Property | Type | | Description |
| :----------: | :----------------------------: | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **type** | _string_ | **required** | The type of mutation to perform, must be one of `create`, `update`, or `delete`. |
| **type** | _string_ | **required** | The type of mutation to perform, must be one of `create`, `update`, `jsonPatch`\*, or `delete`. |
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These types were intentionally kept abstract here (CRUD vs. POST/PUT/PATCH/DELETE/GET) and jsonPatch goes against that intent, as it goes beyond saying what to do by saying how to do it.

The "update" type could be either a PUT or a PATCH under the hood, or, in this case, a "PATCH + json-patch+json".

So conceptually I'm not sure about the jsonPatch type. It doesn't belong as a type on the same level as the others.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I keep flip-flopping on if I consider json-patch a type of mutation in the same vein as CRUD operations.

Conceptually I want it under the "update" mutation, however the implementation is convincing in its simplicity.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So you would prefer useDataQuery to remain at its current level of abstraction and hide the format of the data being sent to the server? And I guess if/when we support graphql we also wouldn't want a type of graphql so makes sense.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mediremi yeah, that's correct.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can see why you wouldn't want jsonPatch as its own type as it doesn't really match with the CRUD types.

But the CRUD types are declarative, which means that the actual implementation is hidden/inaccessible/uncontrollable.
jsonPatch on the other hand is an imperative instruction that tells the server what exactly it should do, so my preference would be to not "hide" it behind a declarative type, I think that it would even violate consistency as we'd be mixing declarative CRUD types with an imperative "handle".

When it comes to translating this to the graphql world, then there's nothing that'd stop us from using jsonPatch as a type. Graphql only knows query and mutation. What kind of mutations exist depends on the schema. Normally there'd be a create/update/delete mutation for every type (e.g. createDataElement, updateDataElement and deleteDataElement; actually there can be a lot more, like linkDataElementWithDataSet for example). There's nothing that prevents us from adding patchDataElement/jsonPatchDataElement.

Copy link
Copy Markdown
Contributor

@varl varl Nov 12, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right. I think where we see things differently is if the request payload is part of the Data Engine's (our) API or the Server's (their) API.

My perspective is that the payload itself is not part of our API. It's determined by the server in all cases.

So on our side, the developer will declare what they want to do (e.g. update), and pass along a payload. That's declarative as far as our API goes.

If that payload is an "sequential instruction set" we pass that along to the server in the proper way.

If that payload is something else (e.g. GQL), we pass that along in the proper way without the user having to think about the specifics of how a request is sent to the server.

In this case, the trade-off is that a developer has to know that we support json-patch instruction sets when using the update type. This is consistent with what the developer currently needs to know when s/he works with the API through the Data Engine so I am OK with the trade-off.

if (type === 'delete' && query.data) {
errors.push("Mutation type 'delete' does not support property 'data'")
}
if (type === 'jsonPatch' && !Array.isArray(query.data)) {
Copy link
Copy Markdown
Contributor

@varl varl Nov 5, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are there other introspective checks we can do on the data to make sure that we are dealing with a json-patch request body if the type is 'update' ?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess we know the shape of a patch array, so we could make sure that the children are sane.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yep we could check if query.data is an array, whose values are objects with an op key containing one of the following values: "add", "remove", "replace", "move", "copy", "test" (see https://datatracker.ietf.org/doc/html/rfc6902/#section-4)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice, that seems like a solid validation to do.

@amcgee
Copy link
Copy Markdown
Contributor

amcgee commented Nov 10, 2021

@HendrikThePendric thanks for putting this together!

Our mutation types are all CRUD verbs (create, read, update, delete) so I'm hesitant to add jsonPatch. @varl and I were thinking that we should prefer consistency, if possible. Could we maybe send a json-patch HTTP request when passing an array to data for a update mutation? We could perhaps do a shallow validation of the array to make sure all are valid json-patch operations. The risk, of course, is that there might be hidden somewhere DEEP in our API a PUT request somewhere which expects a top-level array body, though I don't know of any and would probably consider it a bug if I found one.

@HendrikThePendric
Copy link
Copy Markdown
Contributor Author

HendrikThePendric commented Nov 16, 2021

@HendrikThePendric thanks for putting this together!

Our mutation types are all CRUD verbs (create, read, update, delete) so I'm hesitant to add jsonPatch. @varl and I were thinking that we should prefer consistency, if possible. Could we maybe send a json-patch HTTP request when passing an array to data for a update mutation? We could perhaps do a shallow validation of the array to make sure all are valid json-patch operations. The risk, of course, is that there might be hidden somewhere DEEP in our API a PUT request somewhere which expects a top-level array body, though I don't know of any and would probably consider it a bug if I found one.

I thought this had been discussed already and that we preferred an explicit approach. Perhaps I have misremembered.

I would be on board with this alternative approach too, but I do see some potential downside with determining the request-type based on the shape of the data property. Personally I imagined that in the future we could add some tooling to the data-service that would actually produce patch arrays for the app. For example (this is a very rough idea which I haven't really thought through) we could retain the original object and the app could send us an updated object, and we could produce the patch array based on the diff.

The type of thing I outline above would not be possible if we expect the data to already be a patch array when it is sent to the data-service. But I guess we could always expose other hooks/helpers to aid the creation of patch arrays....

@amcgee
Copy link
Copy Markdown
Contributor

amcgee commented Nov 16, 2021

The type of thing I outline above would not be possible if we expect the data to already be a patch array when it is sent to the data-service. But I guess we could always expose other hooks/helpers to aid the creation of patch arrays....

Why would it not be possible? If we see an update with an object data body, we could tehn generate the necessary patch array right?

@HendrikThePendric
Copy link
Copy Markdown
Contributor Author

Why would it not be possible? If we see an update with an object data body, we could then generate the necessary patch array right?

Just so it doesn't look like I simply ignored Austin's question.... Austin and I had a chat about this on Slack and there was a slight misunderstanding on my side: If we assume all supported DHIS2-core versions have working JSON Patch support then automatically converting the data of an update is possible, and actually a great idea. As a byproduct of my misunderstanding Austin also realised that we shouldn't start doing this automatic conversion before all supported DHIS2 Core versions have full support for JSON patch 😊 .

@amcgee amcgee self-assigned this Nov 23, 2021
@amcgee
Copy link
Copy Markdown
Contributor

amcgee commented Nov 23, 2021

I'll take a stab at this while @mediremi is off - if anyone has opinions on my comments above let me know

@amcgee
Copy link
Copy Markdown
Contributor

amcgee commented Dec 4, 2021

Possible alternative name... instead of a mutation with type: 'jsonPatch' and instead of inspecting the data type let's keep it simple but also explicit with a new verb type: 'patch' - patch is also a verb which makes just as much sense as update or delete (update still has its place when you want to replace an entire object... maybe replace could be added as an alias for update...)

@varl
Copy link
Copy Markdown
Contributor

varl commented Dec 6, 2021

I thought the API design choice for using "CRUD" (create, read, update, delete) is that it is very easy for the consumer to understand what they do, and therefor when they should be used. Adding "patch" into that makes it less so.

"Patch" is basically "Edit", which is covered under "Update", so it pushes the complexity to the user for the sake of less complexity in the implementation.

If less complexity in the implementation is the design goal, then "CRUD" isn't the optimal convention; instead we can pass through the HTTP verbs and deprecate the CRUD pattern.

@amcgee
Copy link
Copy Markdown
Contributor

amcgee commented Dec 6, 2021 via email

@varl
Copy link
Copy Markdown
Contributor

varl commented Dec 7, 2021

The problem is that "update" as a term covers both "full updates" and "partial updates" so having "update" as a verb and having "patch" doesn't respect the abstraction we have chosen.

That level of abstraction is "create - read - update - delete", and "patch" is a lower level of abstraction because "patch" is a kind of update.

I don't see how they can live on the same abstraction level conceptually.

If we go with a new type for json-patch, I think the original proposal of using jsonPatch is a good choice with the least surprises from a consumer perspective.

The partial concept is not bad to me either. Since updates can be done in many ways, having a specifier for how to update is warranted. A boolean for partial isn't ideal though.

Some examples below for how it can fit into our abstraction level, and how it can look if we decide to lower the abstraction level.

With our current level of abstraction:

Default (replace):

{
  type: 'update',
  partial: false, // PUT + json
  data: {},
}

Standard PATCH + json (edit):

{
  type: 'update',
  partial: true, // PATCH + json
  data: {},
}

JSON Patch + json (edit):

{
  type: 'update',
  partial: 'json-patch', // PATCH + json-patch+json
  data: [{}, {}, {}],
}

Lower level of abstraction

Default:

{
  type: 'put',
  data: {},
}

Standard PATCH + json:

{
  type: 'patch',
  data: {},
}

JSON Patch + json:

{
  type: 'json-patch',
  data: [{}, {}, {}],
}

@amcgee
Copy link
Copy Markdown
Contributor

amcgee commented Dec 8, 2021 via email

@mediremi mediremi requested a review from varl March 14, 2022 10:53
@varl
Copy link
Copy Markdown
Contributor

varl commented Mar 14, 2022

Related to dhis2/notes#330

@mediremi mediremi merged commit cdcdf24 into master Mar 15, 2022
@mediremi mediremi deleted the feat/add-json-patch-support branch March 15, 2022 08:54
dhis2-bot added a commit that referenced this pull request Mar 15, 2022
# [3.4.0](v3.3.0...v3.4.0) (2022-03-15)

### Features

* **data-service:** add json-patch support ([#1023](#1023)) ([cdcdf24](cdcdf24))
@dhis2-bot
Copy link
Copy Markdown
Contributor

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants