CRUD API powered by Doctrine mapping for Symfony
composer require wernerdweight/doctrine-crud-api-bundle
Enable the bundle in your kernel:
<?php
// config/bundles.php
return [
// ...
WernerDweight\DoctrineCrudApiBundle\DoctrineCrudApiBundle::class => ['all' => true],
];
Must be used to mark entity available for the API.
If used, the marked property can be retrieved when listing the entity.
If used with default=true
, the marked property will be retrieved when listing the entity without response structure specified (see below for response structure explanation).
If used, the marked property can be set when creating the entity.
If used with nested=true
, the properties of marked property (if property is an entity or a collection of entities) can also be set when creating the entity.
If used, the marked property can be set when updating the entity.
If used with nested=true
, the properties of marked property (if property is an entity or a collection of entities) can also be set when updating the entity.
Allows to specify additional parameters of the property.
If used with type=entity|collection
, the respective type will be expected by the API (default type is deducted from ORM mapping).
If used with class=App\Some\Entity
, the respective class will be expected by the API (default class is deducted from ORM mapping).
If used with payload=["argument1", "argument2"]
, the arguments provided will be passed to the getter when retrieving the property.
Payload arguments can reference public services by using @
prefix (e.g. @request_stack.currentRequest.query.tagThreshold
).
Allows to specify additional fields that are not mapped to the database.
This way, you can add custom fields to the API response (e.g. to synthetise data from multiple other fields).
Warning: Entity must implement ApiEntityInterface to be available to the API!
use Doctrine\ORM\Mapping as ORM;
use WernerDweight\DoctrineCrudApiBundle\Entity\ApiEntityInterface;
use WernerDweight\DoctrineCrudApiBundle\Mapping\Annotation as WDS;
#[ORM\Table(name: "app_artist")]
#[ORM\Entity(repositoryClass: "App\Repository\ArtistRepository")]
#[WDS\Accessible()]
class Artist implements ApiEntityInterface
{
#[ORM\Column(name: "id", type: "guid")]
#[ORM\Id]
#[ORM\GeneratedValue(strategy: "UUID")]
private string $id;
#[ORM\Column(name: "name", type: "string", nullable: false)]
#[WDS\Listable(default: true)]
#[WDS\Creatable()]
#[WDS\Updatable()]
private string $name;
/**
* @var ArrayCollection|PersistentCollection
*/
#[ORM\OneToMany(targetEntity: "App\Entity\Track", mappedBy: "artist")]
#[WDS\Listable(default: true)]
#[WDS\Creatable(nested: true)]
#[WDS\Updatable(nested: true)]
private $tracks;
#[ORM\Column(name: "tags", type: "json", nullable: false, options: ["jsonb" => true])]
#[WDS\Listable(default: true)]
#[WDS\Metadata(payload: ["@request_stack.currentRequest.query.tagThreshold"])]
private array $tags;
#[WDS\Listable(default: true)]
private string $primaryTag; // this is unmapped, it doesn't have to be populated
...
public function getId(): string
{
return $this->id;
}
...
public function getTags(?string $tagThreshold = null): array
{
// only return tags with score higher than the provided threshold
if (null !== $tagThreshold) {
return array_filter(
$this->tags,
fn (string $tag): bool => $tag['score'] >= (float)$tagThreshold
);
}
return $this->tags;
}
public function getPrimaryTag(): string
{
// return the first tag (for simplicity, any logic can be here)
return $this->tags[0]['value'] ?? '';
}
...
}
<?xml version="1.0" encoding="utf-8"?>
<doctrine-mapping
xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping"
xmlns:wds="http://schemas.wds.blue/orm/doctrine-crud-api-bundle-mapping"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://doctrine-project.org/schemas/orm/doctrine-mapping https://www.doctrine-project.org/schemas/orm/doctrine-mapping.xsd"
>
<entity repository-class="App\Repository\ArtistRepository" name="App\Entity\Artist" table="app_artist">
<wds:accessible/>
<id name="id" type="guid">
<generator strategy="UUID" />
</id>
<field name="name" type="string" nullable="false">
<wds:listable default="true"/>
<wds:creatable/>
<wds:updatable/>
</field>
<one-to-many field="tracks" target-entity="App\Entity\Track" mapped-by="artist" fetch="LAZY">
<wds:listable default="true"/>
<wds:creatable nested="true"/>
<wds:updatable nested="true"/>
</one-to-many>
<field name="tags" type="json" nullable="false">
<options>
<option name="jsonb">true</option>
<option name="default">[]</option>
</options>
<wds:listable default="true" />
<wds:metadata>
<wds:payload>
<wds:argument>@request_stack.currentRequest.query.tagThreshold</wds:argument>
</wds:payload>
</wds:metadata>
</field>
<wds:unmapped name="primaryTag">
<wds:listable default="true" />
</wds:unmapped>
...
</entity>
</doctrine-mapping>
- [GET | POST]
/{entity-name}/list/
- lists entities of type EntityName (see below for filtering, ordering etc.); - [GET]
/{entity-name}/length/
- returns the count of entities of type EntityName that would be listed for given criteria; - [GET | POST]
/{entity-name}/detail/{id}/
- returns entity of type EntityName with primary key id - [POST]
/{entity-name}/create/
- creates entity of type EntityName (validation is executed on submitted data - you need to add "api" validation group to your validation constraints) - [POST]
/{entity-name}/update/{id}/
- updates entity of type EntityName with primary key id (validation is executed on submitted data - you need to add "api" validation group to your validation constraints) - [DELETE]
/{entity-name}/delete/{id}/
- deletes entity of type EntityName with primary key id
Available for list
, detail
, create
, update
.
The API lets you decide the structure of the response. You should set some default structure using Listable(default=true)
(see above) - properties marked as default will automatically be returned if no response structure is specified in the request. Furthermore, if you decide to specify response structure with nesting (you require a property that is a related entity or collection of entities - e.g. artist.tracks
) but will not specify response structure for the nested entity, the defaults will be used to output the nested entity.
WARNING: If you specify response structure, default properties on the specified level will be ignored! You have to specify all required fields in the request!
Pass response structure with your request as query parameter like this:
GET /artist/list/?responseStructure%5Bname%5D=true&responseStructure%5Btracks%5D%5Btitle%5D=true&responseStructure%5Btracks%5D%5Bdifficulty%5D=true&responseStructure%5Btracks%5D%5Bgenre%5D%5Btitle%5D=true HTTP/1.1
Host: your-api-host.com
...
of like this:
POST /artist/list/ HTTP/1.1
Content-Type: application/json
Host: your-api-host.com
...
{
"responseStructure": {
"name": true,
"tracks": {
"title": true,
"difficulty: true
}
}
}
For clarity, the structure parameter is as follows:
{
responseStructure: {
name: true,
tracks: {
title: true,
difficulty: true,
genre: {
title: true
}
}
}
}
You may reference part of the response structure to allow hierarchical output (e.g. tree structure) as follows:
{
responseStructure: {
name: true,
nodes: {
name: true,
depth: true,
nodes: 'nodes' // this line references its parent (nodes)
}
}
}
{
responseStructure: {
company: {
name: true,
employees: {
name: true,
address: true,
previousCompany: {
dateLeft: true,
ceo: 'company.employees' // this line references the employees structure
}
}
}
}
}
Available for list
, length
.
The API lets you filter listed data using query parameter filter
. The filter must respect entity relations (so if you want to filter by name of the artist (root entity in this example - root entity is always referenced as this
), the key for the filter will be this.name
, but if you want to filter by title of related tracks (a 1:N relation), the filter key will be this.tracks.title
).
Filters can be nested and support AND
and OR
logic.
Following operators are supported:
- eq - is equal to (equivalent of
=
in SQL), - neq - is not equal to (equivalent of
!=
in SQL), - gt - is greater than (equivalent of
>
in SQL), - gte - is greater than or equal (equivalent of
>=
in SQL), - gten - is greater than or equal or NULL (equivalent of
>= OR IS NULL
in SQL), - lt - is lower than (equivalent of
<
in SQL), - lte - is lower than or equal (equivalent of
<=
in SQL), - begins - begins with (equivalent of
LIKE '...%'
in SQL; textual properties only, case-insensitive), - contains - contains (equivalent of
LIKE '%...%'
in SQL; textual properties only, case-insensitive), - not-contains - does not contain (equivalent of
NOT LIKE '%...%'
in SQL; textual properties only, case-insensitive), - ends - ends with (equivalent of
LIKE '%...'
in SQL; textual properties only, case-insensitive), - null - is null (equivalent of
IS NULL
in SQL), - not-null - is not null (equivalent of
IS NOT NULL
in SQL), - empty - is empty (equivalent of
IS NULL OR = ''
in SQL; textual properties only), - not-empty - is not empty (equivalent of
IS NOT NULL AND != ''
in SQL; textual properties only), - in - is contained in (equivalent of
IN
in SQL).
Generally, the filter structure is as follows:
{
filter: {
logic: "and|or",
conditions: [
{
// regular filter
field: "path.to.field",
operator: "eq|neq|...",
value: "filtering value"
},
{
// nested filter
logic: "and|or",
conditions: [ /*...*/ ]
},
...
]
}
}
Pass filter settings with your request as query parameter like this:
GET /artist/list/?filter%5Blogic%5D=or&filter%5Bconditions%5D%5B0%5D%5Blogic%5D=and&filter%5Bconditions%5D%5B0%5D%5Bconditions%5D%5B0%5D%5Bfield%5D=this.name&filter%5Bconditions%5D%5B0%5D%5Bconditions%5D%5B0%5D%5Boperator%5D=contains&filter%5Bconditions%5D%5B0%5D%5Bconditions%5D%5B0%5D%5Bvalue%5D=radio&filter%5Bconditions%5D%5B0%5D%5Bconditions%5D%5B1%5D%5Bfield%5D=this.tracks.title&filter%5Bconditions%5D%5B0%5D%5Bconditions%5D%5B1%5D%5Boperator%5D=contains&filter%5Bconditions%5D%5B0%5D%5Bconditions%5D%5B1%5D%5Bvalue%5D=creep&filter%5Bconditions%5D%5B1%5D%5Blogic%5D=and&filter%5Bconditions%5D%5B1%5D%5Bconditions%5D%5B0%5D%5Bfield%5D=this.name&filter%5Bconditions%5D%5B1%5D%5Bconditions%5D%5B0%5D%5Boperator%5D=contains&filter%5Bconditions%5D%5B1%5D%5Bconditions%5D%5B0%5D%5Bvalue%5D=pink&filter%5Bconditions%5D%5B1%5D%5Bconditions%5D%5B1%5D%5Bfield%5D=this.tracks.title&filter%5Bconditions%5D%5B1%5D%5Bconditions%5D%5B1%5D%5Boperator%5D=contains&filter%5Bconditions%5D%5B1%5D%5Bconditions%5D%5B1%5D%5Bvalue%5D=wish HTTP/1.1
Host: your-api-host.com
or like this:
POST /artist/list/ HTTP/1.1
Content-Type: application/json
Host: your-api-host.com
...
{
"filter": {
"logic": "and",
"conditions": [
{
"field": "this.name",
"operator": "contains",
"value": "radio"
}
]
}
}
For clarity, the filter parameter is as follows:
{
filter: {
logic: "or",
conditions: [
{
logic: "and",
conditions: [
{
field: "this.name",
operator: "contains",
value: "radio"
},
{
field: "this.tracks.title",
operator: "contains",
value: "creep"
},
]
},
{
logic: "and",
conditions: [
{
field: "this.name",
operator: "contains",
value: "pink"
},
{
field: "this.tracks.title",
operator: "contains",
value: "wish"
},
],
}
]
}
}
Available for list
, length
.
The API lets you paginate the listed data using a zero-indexed offset
and limit
parameters.
Pass pagination settings with your request as query parameters like this:
GET /artist/list/?offset=100&limit=20 HTTP/1.1
Host: your-api-host.com
Available for list
.
The API lets you sort listed data using query parameter orderBy
.
Generally, the orderBy structure is as follows (conditions will be applied in specified order):
{
orderBy: [
{
field: "path.to.field",
direction: "asc|desc"
},
...
]
}
Pass ordering settings with your request as query parameter like this:
GET /artist/list/?orderBy%5B0%5D%5Bfield%5D=name&orderBy%5B0%5D%5Bdirection%5D=desc HTTP/1.1
Host: your-api-host.com
For clarity, the orderBy parameter is as follows:
{
orderBy: [
{
field: "name",
direction: "desc"
}
]
}
Available for list
, length
.
The API lets you list groupped data using query parameter groupBy
with optional aggregates. Supported aggregate functions are avg
, count
, min
, max
, and sum
.
Generally, the groupBy structure is as follows (groupping will be applied in specified order; aggregates are optional):
{
groupBy: [
{
field: "path.to.field",
direction: "asc|desc",
aggregates: [
{
function: "avg|count|min|max|sum",
field: "path.to.field",
},
...
]
},
...
]
}
Pass groupping settings with your request as query parameter like this:
GET /track/list/?groupBy%5B0%5D%5Bfield%5D=this.difficulty&groupBy%5B0%5D%5Bdirection%5D=desc&groupBy%5B0%5D%5Baggregates%5D%5B0%5D%5Bfunction%5D=count&groupBy%5B0%5D%5Baggregates%5D%5B0%5D%5Bfield%5D=id&groupBy%5B0%5D%5Baggregates%5D%5B1%5D%5Bfunction%5D=sum&groupBy%5B0%5D%5Baggregates%5D%5B1%5D%5Bfield%5D=difficulty HTTP/1.1
Host: your-api-host.com
For clarity, the groupBy parameter is as follows:
{
groupBy: [
{
field: "this.difficulty",
direction: "desc",
aggregates: [
{
function: "count",
field: "id"
},
{
function: "sum",
field: "difficulty"
}
]
}
]
}
Available for create
, update
.
When creating/updating an entity, the API needs to know, which properties to change. You must specify this using the fields
request parameter.
Pass fields with your request as form data like this:
POST /track/create/?= HTTP/1.1
Content-Type: multipart/form-data; boundary=---011000010111000001101001
Host: your-api-host.com
Content-Length: 576
-----011000010111000001101001
Content-Disposition: form-data; name="fields[title]"
New Track
-----011000010111000001101001
Content-Disposition: form-data; name="fields[difficulty]"
3
-----011000010111000001101001
Content-Disposition: form-data; name="fields[artist][id]"
e00ea3a8-bb91-4639-880c-10ce67a92987
-----011000010111000001101001
Content-Disposition: form-data; name="fields[genre][title]"
New Genre
-----011000010111000001101001
Content-Disposition: form-data; name="fields[chords]"
Am Em\nSome Lyrics\n
-----011000010111000001101001--
For clarity, the fields parameter is as follows:
{
fields: [
{
title: "New Track",
difficulty: 3,
artist:{
id: "e00ea3a8-bb91-4639-880c-10ce67a92987"
},
genre: {
title: "New Genre"
},
chords: "Am Em\nSome Lyrics\n"
}
]
}
Please note the difference between how artist
and genre
are specified. An ID is specified for artist - API will thus look for an existing artist with this ID (and will fail if it doesn't exist). The genre, on the other hand, doesn't have an ID specified, but (imagine) it is set as Creatable(nested=true)
, co it can be created together with the track (the same applies to Updatable(nested=true)
). The resulting track will therefore be assigned an existing artist and a newly created genre.
NOTE: If you specify the ID of the related entity, specifing any other fields for the related entity is a no-op.**
NOTE: Specifying the ID value right as a value for the related entity key is equivalent to specifying the ID value as a key under the related entity:
{
// these are equivalent
artist: "e00ea3a8-bb91-4639-880c-10ce67a92987",
artist: {
id: "e00ea3a8-bb91-4639-880c-10ce67a92987"
}
}
The API dispatches events during certain operations, so you can hook in the process. For general info on how to use events, please consult the official Symfony documentation.
PrePersistEvent (wds.doctrine_crud_api_bundle.item.pre_persist
)
Issued during create
endpoint call, right before the newly created entity is persisted. Contains the item being created.
PostCreateEvent (wds.doctrine_crud_api_bundle.item.post_create
)
Issued during create
endpoint call, right after the newly created entity is flushed to the database. Contains the item being created.
PreUpdateEvent (wds.doctrine_crud_api_bundle.item.pre_update
)
Issued during update
endpoint call, right before the updated entity is applied the data from request. Contains the item being updated.
PostUpdateEvent (wds.doctrine_crud_api_bundle.item.post_update
)
Issued during update
endpoint call, right after the updated entity is flushed to the database. Contains the item being updated.
PreDeleteEvent (wds.doctrine_crud_api_bundle.item.pre_delete
)
Issued during delete
endpoint call, right before the entity is deleted. Contains the item being deleted.
PostDeleteEvent (wds.doctrine_crud_api_bundle.item.post_delete
)
Issued during delete
endpoint call, right after the entity is deleted. Contains the item being deleted.
PreValidateEvent (wds.doctrine_crud_api_bundle.item.pre_validate
)
Issued during create
and update
endpoint call, right before the validation is executed on the entity. Contains the item being created/updated.
PreSetPropertyEvent (wds.doctrine_crud_api_bundle.item.pre_set_property
)
Issued during create
and update
endpoint call, right before a value is applied to particular property of the item being created/updated. Contains the item being created/updated, the field being updated, and the value being applied.
Correct configuration of validation constraints:
Entity data is validated during create and update calls. API uses validator from symfony, so if you need your data to be validated, follow the official symfony documentation.
WARNING: All validation constraints that should be used by the API must be assigned the "api" validation group! (see validation groups documentation)
Correct configuration of cascading:
The API will persist all newly created entities (including the nested ones), but it will not check for possible orphaned relations when deleting an item. You should correctly set cascading for any entity, that should be available for delete operation (see doctrine documentation for cascading configuration).
CORS:
If your FE app (or any other app that communicates with the API) sends preflight requests, you have to handle their resolving (an easy option is to listen on kernel.request
event and filter requests by method (OPTIONS
) and controller (by implemented interface DoctrineCrudApiControllerInterface
)).
HEADS UP: You can use CORSBundle to handle this use-case.
Access control:
You should protect your API with some credentials (API secret, client id, OAuth. ...). Since the methods are numerous and requirements may vary, the API does not force you to use a specific method. You can use symfony security options, integrate a 3rd-party bundle, or use your custom solution.
HEADS UP: You can use ApiAuthBundle to handle this use-case.
FYI: The data shown are taken from various chord libraries.
Basic listing:
curl --request GET \
--url http://your-api-host.com/artist/list/
[
{
"name": "Radiohead",
"tracks": [
{
"title": "Karma Police"
},
{
"title": "Polyethylene"
},
{
"title": "Creep"
},
{
"title": "You and Whose Army"
},
{
"title": "Lucky"
}
]
},
{
"name": "Pink Floyd",
"tracks": [
{
"title": "Wish You Were Here"
},
{
"title": "Time"
},
{
"title": "Money"
}
]
}
]
Partial response structure:
curl --request GET \
--url 'http://your-api-host.com/artist/list/?responseStructure%5Bname%5D=true&responseStructure%5Btracks%5D%5Btitle%5D=true&responseStructure%5Btracks%5D%5Bdifficulty%5D=true&responseStructure%5Btracks%5D%5Bgenre%5D=true'
[
{
"name": "Radiohead",
"tracks": [
{
"title": "Karma Police",
"difficulty": 3,
"genre": {
"title": "Psychedelic Rock"
}
},
{
"title": "Polyethylene",
"difficulty": 4,
"genre": {
"title": "Psychedelic Rock"
}
},
{
"title": "Creep",
"difficulty": 1,
"genre": {
"title": "Psychedelic Rock"
}
},
{
"title": "You and Whose Army",
"difficulty": 5,
"genre": {
"title": "Psychedelic Rock"
}
},
{
"title": "Lucky",
"difficulty": 2,
"genre": {
"title": "Psychedelic Rock"
}
}
]
},
{
"name": "Pink Floyd",
"tracks": [
{
"title": "Wish You Were Here",
"difficulty": 2,
"genre": {
"title": "Psychedelic Rock"
}
},
{
"title": "Time",
"difficulty": 3,
"genre": {
"title": "Psychedelic Rock"
}
},
{
"title": "Money",
"difficulty": 3,
"genre": {
"title": "Psychedelic Rock"
}
}
]
}
]
Full response structure specified:
curl --request GET \
--url 'http://your-api-host.com/artist/list/?responseStructure%5Bname%5D=true&responseStructure%5Btracks%5D%5Btitle%5D=true&responseStructure%5Btracks%5D%5Bdifficulty%5D=true&responseStructure%5Btracks%5D%5Bgenre%5D%5Btitle%5D=true'
[
{
"name": "Radiohead",
"tracks": [
{
"title": "Karma Police",
"difficulty": 3,
"genre": {
"title": "Psychedelic Rock"
}
},
{
"title": "Polyethylene",
"difficulty": 4,
"genre": {
"title": "Psychedelic Rock"
}
},
{
"title": "Creep",
"difficulty": 1,
"genre": {
"title": "Psychedelic Rock"
}
},
{
"title": "You and Whose Army",
"difficulty": 5,
"genre": {
"title": "Psychedelic Rock"
}
},
{
"title": "Lucky",
"difficulty": 2,
"genre": {
"title": "Psychedelic Rock"
}
}
]
},
{
"name": "Pink Floyd",
"tracks": [
{
"title": "Wish You Were Here",
"difficulty": 2,
"genre": {
"title": "Psychedelic Rock"
}
},
{
"title": "Time",
"difficulty": 3,
"genre": {
"title": "Psychedelic Rock"
}
},
{
"title": "Money",
"difficulty": 3,
"genre": {
"title": "Psychedelic Rock"
}
}
]
}
]
Artists like "radio" having track like "Creep" or artists like "pink" having track like "Wish":
curl --request GET \
--url 'http://your-api-host.com/artist/list/?filter%5Blogic%5D=or&filter%5Bconditions%5D%5B0%5D%5Blogic%5D=and&filter%5Bconditions%5D%5B0%5D%5Bconditions%5D%5B0%5D%5Bfield%5D=this.name&filter%5Bconditions%5D%5B0%5D%5Bconditions%5D%5B0%5D%5Boperator%5D=contains&filter%5Bconditions%5D%5B0%5D%5Bconditions%5D%5B0%5D%5Bvalue%5D=radio&filter%5Bconditions%5D%5B0%5D%5Bconditions%5D%5B1%5D%5Bfield%5D=this.tracks.title&filter%5Bconditions%5D%5B0%5D%5Bconditions%5D%5B1%5D%5Boperator%5D=contains&filter%5Bconditions%5D%5B0%5D%5Bconditions%5D%5B1%5D%5Bvalue%5D=creep&filter%5Bconditions%5D%5B1%5D%5Blogic%5D=and&filter%5Bconditions%5D%5B1%5D%5Bconditions%5D%5B0%5D%5Bfield%5D=this.name&filter%5Bconditions%5D%5B1%5D%5Bconditions%5D%5B0%5D%5Boperator%5D=contains&filter%5Bconditions%5D%5B1%5D%5Bconditions%5D%5B0%5D%5Bvalue%5D=pink&filter%5Bconditions%5D%5B1%5D%5Bconditions%5D%5B1%5D%5Bfield%5D=this.tracks.title&filter%5Bconditions%5D%5B1%5D%5Bconditions%5D%5B1%5D%5Boperator%5D=contains&filter%5Bconditions%5D%5B1%5D%5Bconditions%5D%5B1%5D%5Bvalue%5D=wish'
[
{
"name": "Pink Floyd",
"tracks": [
{
"title": "Wish You Were Here"
},
{
"title": "Time"
},
{
"title": "Money"
}
]
},
{
"name": "Radiohead",
"tracks": [
{
"title": "Karma Police"
},
{
"title": "Polyethylene"
},
{
"title": "Creep"
},
{
"title": "You and Whose Army"
},
{
"title": "Lucky"
}
]
}
]
Tracks by title descending:
curl --request GET \
--url 'http://your-api-host.com/track/list/?orderBy%5B0%5D%5Bfield%5D=title&orderBy%5B0%5D%5Bdirection%5D=desc'
[
{
"title": "You and Whose Army"
},
{
"title": "Wish You Were Here"
},
{
"title": "Time"
},
{
"title": "Polyethylene"
},
{
"title": "Money"
},
{
"title": "Lucky"
},
{
"title": "Karma Police"
},
{
"title": "Creep"
}
]
Groupping with aggregates, filtering, and response structure:
curl --request GET \
--url 'http://your-api-host.com/track/list/?groupBy%5B0%5D%5Bfield%5D=this.difficulty&groupBy%5B0%5D%5Bdirection%5D=desc&groupBy%5B0%5D%5Baggregates%5D%5B0%5D%5Bfunction%5D=count&groupBy%5B0%5D%5Baggregates%5D%5B0%5D%5Bfield%5D=id&groupBy%5B0%5D%5Baggregates%5D%5B1%5D%5Bfunction%5D=sum&groupBy%5B0%5D%5Baggregates%5D%5B1%5D%5Bfield%5D=difficulty&filter%5Blogic%5D=and&filter%5Bconditions%5D%5B0%5D%5Bfield%5D=this.artist.name&filter%5Bconditions%5D%5B0%5D%5Boperator%5D=eq&filter%5Bconditions%5D%5B0%5D%5Bvalue%5D=Pink%20Floyd&responseStructure%5Btitle%5D=true&responseStructure%5Bartist%5D%5Bname%5D=true'
[
{
"aggregates": [
{
"id": {
"count": 2
}
},
{
"difficulty": {
"sum": 6
}
}
],
"field": "difficulty",
"value": "3",
"hasGroups": false,
"items": [
{
"title": "Time",
"artist": {
"name": "Pink Floyd"
}
},
{
"title": "Money",
"artist": {
"name": "Pink Floyd"
}
}
]
},
{
"aggregates": [
{
"id": {
"count": 1
}
},
{
"difficulty": {
"sum": 2
}
}
],
"field": "difficulty",
"value": "2",
"hasGroups": false,
"items": [
{
"title": "Wish You Were Here",
"artist": {
"name": "Pink Floyd"
}
}
]
}
]
Pagination:
curl --request GET \
--url 'http://your-api-host.com/track/list/?orderBy%5B0%5D%5Bfield%5D=title&orderBy%5B0%5D%5Bdirection%5D=desc&offset=3&limit=2'
[
{
"title": "Polyethylene"
},
{
"title": "Money"
}
]
Length with groupping:
curl --request GET \
--url 'http://your-api-host.com/track/length/?groupBy%5B0%5D%5Bfield%5D=this.difficulty&groupBy%5B0%5D%5Bdirection%5D=desc&groupBy%5B0%5D%5Baggregates%5D%5B0%5D%5Bfunction%5D=count&groupBy%5B0%5D%5Baggregates%5D%5B0%5D%5Bfield%5D=id&groupBy%5B0%5D%5Baggregates%5D%5B1%5D%5Bfunction%5D=sum&groupBy%5B0%5D%5Baggregates%5D%5B1%5D%5Bfield%5D=difficulty&filter%5Blogic%5D=and&filter%5Bconditions%5D%5B0%5D%5Bfield%5D=this.artist.name&filter%5Bconditions%5D%5B0%5D%5Boperator%5D=eq&filter%5Bconditions%5D%5B0%5D%5Bvalue%5D=Pink%20Floyd&responseStructure%5Btitle%5D=true&responseStructure%5Bartist%5D%5Bname%5D=true'
{
"length": 2
}
Detail with response structure:
curl --request GET \
--url 'http://your-api-host.com/artist/detail/e00ea3a8-bb91-4639-880c-10ce67a92987?responseStructure%5Bname%5D=true&responseStructure%5Btracks%5D%5Btitle%5D=true&responseStructure%5Btracks%5D%5Bdifficulty%5D=true&responseStructure%5Btracks%5D%5Bgenre%5D=true'
{
"name": "Radiohead",
"tracks": [
{
"title": "Karma Police",
"difficulty": 3,
"genre": {
"title": "Psychedelic Rock"
}
},
{
"title": "Polyethylene",
"difficulty": 4,
"genre": {
"title": "Psychedelic Rock"
}
},
{
"title": "Creep",
"difficulty": 1,
"genre": {
"title": "Psychedelic Rock"
}
},
{
"title": "You and Whose Army",
"difficulty": 5,
"genre": {
"title": "Psychedelic Rock"
}
},
{
"title": "Lucky",
"difficulty": 2,
"genre": {
"title": "Psychedelic Rock"
}
}
]
}
Delete track:
curl --request DELETE \
--url http://your-api-host.com/track/delete/3cf54bb9-9e98-46fe-834c-298c4cb3763c
{
"title": "New Track"
}
Update track:
curl --request POST \
--url 'http://your-api-host.com/track/update/c9e5c46d-94eb-4cbc-a778-992d115271d3?=' \
--header 'content-type: multipart/form-data; boundary=---011000010111000001101001' \
--form 'fields[title]=New Track (modified)' \
--form 'fields[difficulty]=1' \
--form 'fields[artist][id]=10401146-c83b-48dc-91f0-64abe93e84f4' \
--form 'fields[genre][id]=fc3e17e3-96f2-4947-9567-ca053a557acd' \
--form 'fields[chords]=C Dm\nCompletely Different'
{
"title": "New Track (modified)"
}
Update artist with existing nested tracks with existing nested genre:
curl --request POST \
--url 'http://your-api-host.com/artist/update/10401146-c83b-48dc-91f0-64abe93e84f4?=' \
--header 'content-type: multipart/form-data; boundary=---011000010111000001101001' \
--form 'fields[tracks][0][id]=9df932d7-b832-48ba-9884-a0cdecc017e9' \
--form 'fields[tracks][0][title]=New Track 75' \
--form 'fields[tracks][0][difficulty]=1' \
--form 'fields[tracks][0][genre][title]=New Genre 81' \
--form 'fields[tracks][1][title]=New Track 20' \
--form 'fields[tracks][1][difficulty]=1' \
--form 'fields[tracks][1][chords]=Em Am\nSome other lyrics\n' \
--form 'fields[tracks][1][genre][id]=8247ac0f-17bb-46b5-8f5e-b52fb77792b5'
{
"name": "New Artist (modified)",
"tracks": [
{
"title": "New Track 75"
},
{
"title": "New Track 20"
}
]
}
Create track:
curl --request POST \
--url 'http://your-api-host.com/track/create/?=' \
--header 'content-type: multipart/form-data; boundary=---011000010111000001101001' \
--form 'fields[title]=New Track' \
--form 'fields[difficulty]=3' \
--form 'fields[artist][id]=e00ea3a8-bb91-4639-880c-10ce67a92987' \
--form 'fields[genre][id]=8247ac0f-17bb-46b5-8f5e-b52fb77792b5' \
--form 'fields[chords]=Am Em\nSome Lyrics\n'
{
"title": "New Track"
}
Create artist with nested tracks with nested genre:
curl --request POST \
--url 'http://your-api-host.com/artist/create/?=' \
--header 'content-type: multipart/form-data; boundary=---011000010111000001101001' \
--form 'fields[name]=New Artist' \
--form 'fields[tracks][0][title]=New Track 2' \
--form 'fields[tracks][0][difficulty]=5' \
--form 'fields[tracks][0][genre][title]=New Genre 2' \
--form 'fields[tracks][0][chords]=Am Em\nSome Lyrics\n' \
--form 'fields[tracks][1][title]=New Track 3' \
--form 'fields[tracks][1][difficulty]=1' \
--form 'fields[tracks][1][chords]=Em Am\nSome other lyrics\n' \
--form 'fields[tracks][1][genre][id]=8247ac0f-17bb-46b5-8f5e-b52fb77792b5'
{
"name": "New Artist",
"tracks": [
{
"title": "New Track 2"
},
{
"title": "New Track 3"
}
]
}
This bundle is under the MIT license. See the complete license in the root directiory of the bundle.