I'm basing my question on How to handle many-to-many relationships in a RESTful API? but want to continue from the accepted answer.
Let's suppose we have a many-to-many relationship between players and teams (just like in the question mentioned above).
As I understand it, there are several options to model this with REST resources:
The payload contains references to the related resources
GET /players/john
yields
{
"name": "John",
"_links": [
{
"rel": "team",
"href": "/teams/1"
},
{
"rel": "team",
"href": "/teams/4"
}
]
}
and
GET /teams/1
yields
{
"name": "Team 1",
"_links": [
{
"rel": "player",
"href": "/players/john"
},
...
]
}
This forces me to update a player-resource when I just want to add a player to the team. Furthermore, when I add a player to the team using a player-resource, the corresponding team-resource gets automatically updated. According to How to handle many-to-many relationships in a RESTful API?:
you don't want the alternate URL /players/5/teams/ to remain cached
In this case, teams/1 might remain cached when I update player "John" to remove team "Team 1" from it!
The relationship is modelled as another resource
GET /teams/1
yields
{
"name": "Team 1",
}
and
GET /players/john
yields
{
"name": "John",
}
Finally,
GET /relationships
yields
[
{
"_links": [
{
"rel": "player",
"href": "/players/john"
},
{
"rel": "team",
"href": "/teams/1"
}
]
},
...
]
This way, I can create and delete relationships without affecting both player-resources and team-resources. But when I delete /players/john, should the matching relationships be automatically deleted as well? If this is the case, the same rule as above is violated. If this is not the case we need the manually delete these relationships which is a lot of work I do not want to burden the consumers of my API with.
Furthermore, if we want to update the teams a certain player "John" is in, we need to delete some relationships and add others. We open ourselves up to merge conflicts (and race conditions) when someone else is editing the player "John" or the team "Team 1".
Each side of the relationship gets its own relationship-object
GET /teams/1/players
yields something like
{
"_links": [
{
"rel": "player",
"href": "/players/john"
},
...
]
}
and
GET /players/john/teams
something like
{
"_links": [
{
"rel": "team",
"href": "/teams/1"
},
...
]
}
But adding or removing one might still affect a resource that is located at a different URL (which does not share a root element)
My questions
Is there away around the problems I have mentioned in both cases?
Which of both approaches is 'preferable' or more pure REST?
How serious should I take the contstraint mentioned in How to handle many-to-many relationships in a RESTful API?:
you don't want the alternate URL /players/5/teams/ to remain cached
Thank you in advance!
You could have the following
Team
GET /teams/dream
{
"_links": {
"self": {
"href": "/teams/dream"
}
"players": {
"href": "/players?team=dream"
}
},
"name": "Dream"
}
Player
GET /player/john
{
"_links": {
"self": {
"href": "/player/john"
},
"teams": {
"href": "/teams?player=john"
},
},
"name": "John",
}
John's teams
GET /teams?player=john
{
"_links": {
},
"items": [
{
"href": "/teams/a-team"
}
],
}
Adding john to dream team, (using json patch for example) (query string on patch post...etc though rare, is valid)
PATCH /teams?player=john
[{
"op": "add",
"path": "/-",
"value": {
"href": "/team/dream"
}
}]
Get john's teams
GET /teams?player=john
{
"_links": {
},
"items": [
{
"href": "/teams/a-team"
},
{
"href": "/teams/dream"
}
]
}
John leaves A Team :
PATCH /teams?player=john
[{
"op": "remove",
"path": "/0"
}]
Related
I am fetching Git changes from Azure DevOps REST Api and want to distinguish if a specific commit was a merge/branch operation. The default changeTypes are: add, edit, delete and I couldn't find any reference for "merge" or "branch".
Is there a built in way to achieve this?
My query uses the following syntax from the official doc:
GET https://{instance}/{collection}/_apis/git/repositories/{repositoryId}/commits/{commitId}?api-version=4.1
And the response is like:
{
"parents": [],
"treeId": "7fa1a3523ffef51c525ea476bffff7d648b8cb3d",
"push": {
"pushedBy": {
"id": "8c8c7d32-6b1b-47f4-b2e9-30b477b5ab3d",
"displayName": "Chuck Reinhart",
"uniqueName": "fabrikamfiber3#hotmail.com",
"url": "https://fabrikam:8080/tfs/_apis/Identities/8c8c7d32-6b1b-47f4-b2e9-30b477b5ab3d",
"imageUrl": "https://fabrikam:8080/tfs/_api/_common/identityImage?id=8c8c7d32-6b1b-47f4-b2e9-30b477b5ab3d"
},
"pushId": 1,
"date": "2014-01-29T23:33:15.2434002Z"
},
"commitId": "be67f8871a4d2c75f13a51c1d3c30ac0d74d4ef4",
"author": {
"name": "Chuck Reinhart",
"email": "fabrikamfiber3#hotmail.com",
"date": "2014-01-29T23:32:09Z"
},
"committer": {
"name": "Chuck Reinhart",
"email": "fabrikamfiber3#hotmail.com",
"date": "2014-01-29T23:32:09Z"
},
"comment": "First cut\n",
"changeCounts": {
"Add": 2
},
"changes": [
{
"item": {
"gitObjectType": "blob",
"path": "/.gitattributes",
"url": "https://fabrikam:8080/tfs/_apis/git/repositories/278d5cd2-584d-4b63-824a-2ba458937249/items/.gitattributes?versionType=Commit"
},
"changeType": "add"
},
{
"item": {
"gitObjectType": "blob",
"path": "/.gitignore",
"url": "https://fabrikam:8080/tfs/_apis/git/repositories/278d5cd2-584d-4b63-824a-2ba458937249/items/.gitignore?versionType=Commit"
},
"changeType": "add"
}
],
"url": "https://fabrikam:8080/tfs/_apis/git/repositories/278d5cd2-584d-4b63-824a-2ba458937249/commits/be67f8871a4d2c75f13a51c1d3c30ac0d74d4ef4",
"remoteUrl": "https://fabrikam:8080/tfs/_git/Fabrikam-Fiber-Git/commit/be67f8871a4d2c75f13a51c1d3c30ac0d74d4ef4",
"_links": {
"self": {
"href": "https://fabrikam:8080/tfs/_apis/git/repositories/278d5cd2-584d-4b63-824a-2ba458937249/commits/be67f8871a4d2c75f13a51c1d3c30ac0d74d4ef4"
},
"repository": {
"href": "https://fabrikam:8080/tfs/_apis/git/repositories/278d5cd2-584d-4b63-824a-2ba458937249"
},
"changes": {
"href": "https://fabrikam:8080/tfs/_apis/git/repositories/278d5cd2-584d-4b63-824a-2ba458937249/commits/be67f8871a4d2c75f13a51c1d3c30ac0d74d4ef4/changes"
},
"web": {
"href": "https://fabrikam:8080/tfs/_git/Fabrikam-Fiber-Git/commit/be67f8871a4d2c75f13a51c1d3c30ac0d74d4ef4"
},
"tree": {
"href": "https://fabrikam:8080/tfs/_apis/git/repositories/278d5cd2-584d-4b63-824a-2ba458937249/trees/7fa1a3523ffef51c525ea476bffff7d648b8cb3d"
}
}
}
If there is no built in way to achieve this, should the parent be used as a guidance? Like if it is empty it is a 'branch' operation and if there is two entries in it it is a 'merge'?
Update 1
I can see in the documentation referenced above that there are merge and branch changeTypes, but when I query a change that was merged the result only contains edit or add.
You will have to use the parents collection to determine whether it was a merge. Since normal commits just have 1 parent, more than 1 indicates a merge.
From the git documentation here
A commit object may have any number of parents. With exactly one parent, it is an ordinary commit. Having more than one parent makes the commit a merge between several lines of history. Initial (root) commits have no parents.
world! We are trying to use the NetSuite SuiteQL REST API to pull down data. It works perfectly fine for most of the records we are sourcing, but we hit an interesting snag when we hit large tables.
Since the known limit of a single page of data is 1,000 rows, we are simply calling it with a limit of 1,000 and setting the offsets in 1,000 row increments. Something interesting happens when we get to an offset of 100,000 rows for a record that is larger than 100,000 rows.
If we call https://myinstance.suitetalk.api.netsuite.com/services/rest/query/v1/suiteql?limit=1000&offset=98000, we get all the links that we expect.
{
"links": [
{
"rel": "previous",
"href": "https://myinstance.suitetalk.api.netsuite.com/services/rest/query/v1/suiteql?limit=1000&offset=97000"
},
{
"rel": "first",
"href": "https://myinstance.suitetalk.api.netsuite.com/services/rest/query/v1/suiteql?limit=1000&offset=0"
},
{
"rel": "next",
"href": "https://myinstance.suitetalk.api.netsuite.com/services/rest/query/v1/suiteql?limit=1000&offset=99000"
},
{
"rel": "last",
"href": "https://myinstance.suitetalk.api.netsuite.com/services/rest/query/v1/suiteql?limit=1000&offset=753000"
},
{
"rel": "self",
"href": "https://myinstance.suitetalk.api.netsuite.com/services/rest/query/v1/suiteql?limit=1000&offset=98000"
}
],
"count": 1000,
"hasMore": true,
"items": [
{
[heres my data]...
Setting it to 99,000, a lot of that information disappears. It's almost as if it thinks this is the last page.
{
"links": [
{
"rel": "previous",
"href": "https://myinstance.suitetalk.api.netsuite.com/services/rest/query/v1/suiteql?limit=1000&offset=98000"
},
{
"rel": "first",
"href": "https://myinstance.suitetalk.api.netsuite.com/services/rest/query/v1/suiteql?limit=1000&offset=0"
},
{
"rel": "self",
"href": "https://myinstance.suitetalk.api.netsuite.com/services/rest/query/v1/suiteql?limit=1000&offset=99000"
}
],
"count": 1000,
"hasMore": false,
"items": [
{
[heres my data]...
Setting it to 100,000 flat out gives me an error.
{
"type": "https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.5",
"title": "Not Found",
"status": 404,
"o:errorDetails": [
{
"detail": "The specified query parameter 'offset' is out of bounds. Provide value between 0 and 753000.",
"o:errorQueryParam": "offset",
"o:errorCode": "INVALID_PARAMETER"
}
]
Has anyone seen this kind of behavior before? I scoured the documentation and couldn't find any mention of limitation in page offsets, so I'm thinking this might be a bug of some sort (the fact that it even tells you the maximum bounds and it's clearly higher than the offset specified makes me think it's a bug), but hoping someone may have seen this before, and even better, has ideas on how to get around this!
the official documentation for NetSuite REST Web Services mentions this limitation [1]:
Using SuiteQL queries, you can return a maximum of 100,000 results. For more information, see query.runSuiteQLPaged(options).
I have 2 jpa entities Document and DispatchDetail which have one-to-many relationship. i.e. a document can have a list of dispatchDetails. I have created 2 repositories for each entity.
Now I'm gonna try a document GET.
http://localhost:7070/booking-documents-service/docs/5999571
{
"docType": "SAP_ACCOUNTS_PAYABLE",
"docStoreId": 456651,
"qualityChecked": true,
"format": "pdf",
"bookingId": -1,
"_links": {
"self": {
"href": "http://localhost:7070/booking-documents-service/docs/5999571"
},
"generatedDocument": {
"href": "http://localhost:7070/booking-documents-service/docs/5999571"
},
"dispatchDetails": {
"href": "http://localhost:7070/booking-documents-service/docs/5999571/dispatchDetails"
}
}
}
Now when I try GET request for the link listed as dispatchDetails. It is like this.
http://localhost:7070/booking-documents-service/docs/5999571/dispatchDetails
{
"_embedded": {
"dispatchDetails": [
{
"dispatchQueId": 207443,
"dispatchStatus": "S",
"recipient": "fldcvisla12678.wdw.disney.com|#|/opt/apps/shared/shuttle/SAP/OUT/|#|f-tbxshuttlenp|#|D1$NeY984|#|SFTP|#|22|#|null",
"description": "Upload :FileUploadDispatcher; FTP:null/null;\n2d89df3d-ca51-4d35-9528-439923fa48d4..",
"dispatcher": "AD",
"_links": {
"self": {
"href": "http://localhost:7070/booking-documents-service/dispatchDetails/1"
},
"dispatchDetail": {
"href": "http://localhost:7070/booking-documents-service/dispatchDetails/1"
},
"generatedDocument": {
"href": "http://localhost:7070/booking-documents-service/dispatchDetails/1/generatedDocument"
}
}
}
]
},
"_links": {
"self": {
"href": "http://localhost:7070/booking-documents-service/docs/5999571/dispatchDetails"
}
}
}
But I don't want dispatch details as a stand alone resource (listed in links above).
i.e. I don't want this endpoint
http://localhost:7070/booking-documents-service/dispatchDetails
Instead I only need this.
http://localhost:7070/booking-documents-service/docs/5999571/dispatchDetails
How to achieve this? i.e. allow only sub resource level operations.
I have two entities 'persons' and 'businesses' and both of them have a sub-resource (e.g locations)
So I have endpoints:
GET /persons/{id}/locations
GET /businesses/{id}/locations
Now I want an endpoint to filter locations of a main resource.
If I do:
GET /persons/{id}/locations?country={...}
GET /businesses/{id}/locations?country={...}
I will search locations of a specific person/business.
What is the best practice to filter locations of all persons
I have some ideas:
1. GET /persons/locations?country={...}
2. GET /locations?entity=persons&country={...}
But not sure these are fine.
You can work with the expand concept where you can be used with the relationship between the entities.
GET /persons
{
"data": [
{"person_id": 1},
{"person_id": 2}
],
"links": [
{
"rel": "locations",
"href": "/person/1/locations"
},
{
"rel": "locations",
"href": "/person/2/locations"
}
]
}
GET /persons?expand=locations&country={...}
{
"data": [
{"person_id": 1, "locations": [...]},
{"person_id": 2, "locations": [...]}
]
}
Imagine I have an API for a school. One of the resources is Department, which has various resources on it, such as a collection of Professors, and a "Head Professor".
Department looks like this:
{
"_links": {
"self": "http://myapi.com/department/math"
}
"name": "Math Department",
"headProfessor": {
"_links": {
"self": "http://myapi.com/professor/id/2",
"headProfessor": "http://myapi.com/headprofessor/department/math"
},
"name": "George Patton",
"id": "2"
}
"professors": {
"_links": {
"self": "http://myapi.com/professors/department/math"
},
"_collectionData": [
{
"_links": {
"self": "http://myapi.com/professor/id/1"
},
"name": "John Doe",
"id": "1"
},
{
"_links": {
"self": "http://myapi.com/professor/id/2"
},
"name": "George Patton",
"id": "2"
},
{
"_links": {
"self": "http://myapi.com/professor/id/3"
},
"name": "Paul Simon",
"id": "3"
}
]
}
}
My question is regarding "headProfessor" and the links. What is the canonical link for the "head professor"? Is it http://myapi.com/professor/id/1 or is it http://myapi.com/headprofessor/department/math? Should I have both in there? Or is only one necessary? Is there a better way to represent the "head" or the "top" of something, basically a url whose resource could change because it represents a relationship and not a static resource?
NOTE
Yes, I do prefer the resource designator first in the url as it gives the resource designator the same location in every url. But my question is not about that. That's just a matter of taste and style.
First of all Department has professors and not vice-versa so your APIs should be designed like this
http://myapi.com/departments -> GET all departments
http://myapi.com/departments/{departmentId}/professors ---> POST to add a professor to a department , body of POST has the rank of professor has HOD, or staff
http://myapi.com/departments/{departmentId}/professors ---> GET should get all professors of that department
http://myapi.com/departments/{departmentId}/professors?rank=hod ---> Should give you the HOD
http://myapi.com/departments/{departmentId}/professors/{professorId} ---> PUT to change rank of Professor
http://myapi.com/departments/{departmentId}/professors/{professorId} ---> DELETE to remove professor from Department if he retires or moves to another college,etc.