API platform filters with uuid - filter

I recently set up a project where I use api-platform in correlation with ramsey/uuid-doctrine in accordance with https://api-platform.com/docs/core/identifiers/.
Al the basic CRUD stuff works but I'm getting unexpected behavior on ApiFilter.
The basic setup is this, I have platform objects that in turn contain organisations both relaying on an astract class.
Abstract
class AbstaractEntity
{
/**
* #var \Ramsey\Uuid\UuidInterface
*
* #ORM\Id
* #ORM\Column(type="uuid_binary_ordered_time", unique=true)
* #ORM\GeneratedValue(strategy="CUSTOM")
* #ORM\CustomIdGenerator(class="Ramsey\Uuid\Doctrine\UuidOrderedTimeGenerator")
*
* #Groups({"read", "write"})
* #ApiProperty(iri="https://schema.org/identifier")
*/
protected $id;
* The Platform to which this object belongs.
*
* #ORM\ManyToOne(targetEntity="App\Conduction\PlatformBundle\Entity\Platform")
* #ORM\JoinColumn(name="platform_id", referencedColumnName="id")
* #Groups({"read", "write"})
* #MaxDepth(1)
* #ApiProperty()
*/
protected $platform;
...
}
Organisation
/**
* Organisation entity
*
* #ORM\Table(
* "organisation__organisation"
* )
* #ORM\Entity
* #ORM\HasLifecycleCallbacks
* #ORM\EntityListeners({"App\Conduction\OrganisationBundle\EventListener\OrganisationListener","App\Conduction\AppBundle\EventListener\EntityListener"})
* #ApiResource(attributes={
* "normalization_context"={"groups"={"read"}, "enable_max_depth"="true"},
* "denormalization_context"={"groups"={"write"}}
* })
* #ApiFilter(SearchFilter::class, properties={"id": "exact", "description": "partial", "name": "partial","organisation": "exact","platform": "exact"})
* #ApiFilter(DateFilter::class, properties={"dateCreated","datePublished","dateModified","dateDeleted"})
* #ApiFilter(OrderFilter::class, properties={"id", "name","dateCreated","datePublished","dateModified","dateDeleted"}, arguments={"orderParameterName"="order"})
* #Gedmo\SoftDeleteable(
* fieldName = "dateDeleted",
* timeAware = true
* )
*/
class Organisation extends \App\Conduction\AppBundle\Entity\AbstaractEntity
{
....
}
So all the basics are in order and a api/organisations gives met the following
{
"#context": "/api/contexts/Organisation",
"#id": "/api/organisations",
"#type": "hydra:Collection",
"hydra:member": [
{
"#id": "/api/organisations/ba8dc018-d8d5-11e8-bcb6-5254007d3b24",
"#type": "Organisation",
"slug": null,
"organisation": "/api/organisations/ba8dc018-d8d5-11e8-bcb6-5254007d3b24",
"children": [
"/api/organisations/ba8dc018-d8d5-11e8-bcb6-5254007d3b24"
],
"id": "ba8dc018-d8d5-11e8-bcb6-5254007d3b24",
"name": "Conduction",
"description": "This is the defeault platform",
"platform": {
"#id": "/api/platforms/ba8d9958-d8d5-11e8-a9a5-5254007d3b24",
"#type": "Platform",
"approveUsers": true,
"validateUsers": true,
"organisation": "/api/organisations/ba8dc018-d8d5-11e8-bcb6-5254007d3b24",
"id": "ba8d9958-d8d5-11e8-a9a5-5254007d3b24",
"name": "Conduction",
"description": "This is the defeault platform",
"platform": "/api/platforms/ba8d9958-d8d5-11e8-a9a5-5254007d3b24",
"dateCreated": "2018-10-26T06:15:09+02:00",
"datePublished": "2018-10-26T06:15:09+02:00",
"dateModified": "2018-10-26T06:15:10+02:00"
},
"dateCreated": "2018-10-26T06:15:09+02:00",
"datePublished": "2018-10-26T06:15:09+02:00",
"dateModified": "2018-10-26T06:15:10+02:00"
},
{
"#id": "/api/organisations/5f87e080-d8d6-11e8-9809-5254007d3b24",
"#type": "Organisation",
"slug": null,
"organisation": "/api/organisations/5f87e080-d8d6-11e8-9809-5254007d3b24",
"children": [
"/api/organisations/5f87e080-d8d6-11e8-9809-5254007d3b24"
],
"id": "5f87e080-d8d6-11e8-9809-5254007d3b24",
"name": "test 1",
"description": "<p>test 1 </p>",
"platform": null,
"dateCreated": "2018-10-26T06:19:47+02:00",
"datePublished": "2018-10-26T06:19:47+02:00",
"dateModified": "2018-10-26T06:19:47+02:00"
},
{
"#id": "/api/organisations/f2079af4-d8d6-11e8-ba9a-5254007d3b24",
"#type": "Organisation",
"slug": null,
"organisation": "/api/organisations/f2079af4-d8d6-11e8-ba9a-5254007d3b24",
"children": [
"/api/organisations/f2079af4-d8d6-11e8-ba9a-5254007d3b24"
],
"id": "f2079af4-d8d6-11e8-ba9a-5254007d3b24",
"name": "test 1",
"description": "<p>test 1</p>",
"platform": {
"#id": "/api/platforms/f20723bc-d8d6-11e8-8ffb-5254007d3b24",
"#type": "Platform",
"approveUsers": true,
"validateUsers": true,
"organisation": "/api/organisations/f2079af4-d8d6-11e8-ba9a-5254007d3b24",
"id": "f20723bc-d8d6-11e8-8ffb-5254007d3b24",
"name": "test 1",
"description": "<p>test 1</p>",
"platform": "/api/platforms/f20723bc-d8d6-11e8-8ffb-5254007d3b24",
"dateCreated": "2018-10-26T06:23:52+02:00",
"datePublished": "2018-10-26T06:23:52+02:00",
"dateModified": "2018-10-26T06:23:52+02:00"
},
"dateCreated": "2018-10-26T06:23:52+02:00",
"datePublished": "2018-10-26T06:23:52+02:00",
"dateModified": "2018-10-26T06:23:52+02:00"
}
],
"hydra:totalItems": 3,
"hydra:search": {
"#type": "hydra:IriTemplate",
"hydra:template": "/api/organisations{?id,id[],description,name,organisation,organisation[],platform,platform[],dateCreated[before],dateCreated[strictly_before],dateCreated[after],dateCreated[strictly_after],datePublished[before],datePublished[strictly_before],datePublished[after],datePublished[strictly_after],dateModified[before],dateModified[strictly_before],dateModified[after],dateModified[strictly_after],dateDeleted[before],dateDeleted[strictly_before],dateDeleted[after],dateDeleted[strictly_after],order[id],order[name],order[dateCreated],order[datePublished],order[dateModified],order[dateDeleted]}",
"hydra:variableRepresentation": "BasicRepresentation",
"hydra:mapping": [
{
"#type": "IriTemplateMapping",
"variable": "id",
"property": "id",
"required": false
},
{
"#type": "IriTemplateMapping",
"variable": "id[]",
"property": "id",
"required": false
},
{
"#type": "IriTemplateMapping",
"variable": "description",
"property": "description",
"required": false
},
{
"#type": "IriTemplateMapping",
"variable": "name",
"property": "name",
"required": false
},
{
"#type": "IriTemplateMapping",
"variable": "organisation",
"property": "organisation",
"required": false
},
{
"#type": "IriTemplateMapping",
"variable": "organisation[]",
"property": "organisation",
"required": false
},
{
"#type": "IriTemplateMapping",
"variable": "platform",
"property": "platform",
"required": false
},
{
"#type": "IriTemplateMapping",
"variable": "platform[]",
"property": "platform",
"required": false
},
{
"#type": "IriTemplateMapping",
"variable": "dateCreated[before]",
"property": "dateCreated",
"required": false
},
{
"#type": "IriTemplateMapping",
"variable": "dateCreated[strictly_before]",
"property": "dateCreated",
"required": false
},
{
"#type": "IriTemplateMapping",
"variable": "dateCreated[after]",
"property": "dateCreated",
"required": false
},
{
"#type": "IriTemplateMapping",
"variable": "dateCreated[strictly_after]",
"property": "dateCreated",
"required": false
},
{
"#type": "IriTemplateMapping",
"variable": "datePublished[before]",
"property": "datePublished",
"required": false
},
{
"#type": "IriTemplateMapping",
"variable": "datePublished[strictly_before]",
"property": "datePublished",
"required": false
},
{
"#type": "IriTemplateMapping",
"variable": "datePublished[after]",
"property": "datePublished",
"required": false
},
{
"#type": "IriTemplateMapping",
"variable": "datePublished[strictly_after]",
"property": "datePublished",
"required": false
},
{
"#type": "IriTemplateMapping",
"variable": "dateModified[before]",
"property": "dateModified",
"required": false
},
{
"#type": "IriTemplateMapping",
"variable": "dateModified[strictly_before]",
"property": "dateModified",
"required": false
},
{
"#type": "IriTemplateMapping",
"variable": "dateModified[after]",
"property": "dateModified",
"required": false
},
{
"#type": "IriTemplateMapping",
"variable": "dateModified[strictly_after]",
"property": "dateModified",
"required": false
},
{
"#type": "IriTemplateMapping",
"variable": "dateDeleted[before]",
"property": "dateDeleted",
"required": false
},
{
"#type": "IriTemplateMapping",
"variable": "dateDeleted[strictly_before]",
"property": "dateDeleted",
"required": false
},
{
"#type": "IriTemplateMapping",
"variable": "dateDeleted[after]",
"property": "dateDeleted",
"required": false
},
{
"#type": "IriTemplateMapping",
"variable": "dateDeleted[strictly_after]",
"property": "dateDeleted",
"required": false
},
{
"#type": "IriTemplateMapping",
"variable": "order[id]",
"property": "id",
"required": false
},
{
"#type": "IriTemplateMapping",
"variable": "order[name]",
"property": "name",
"required": false
},
{
"#type": "IriTemplateMapping",
"variable": "order[dateCreated]",
"property": "dateCreated",
"required": false
},
{
"#type": "IriTemplateMapping",
"variable": "order[datePublished]",
"property": "datePublished",
"required": false
},
{
"#type": "IriTemplateMapping",
"variable": "order[dateModified]",
"property": "dateModified",
"required": false
},
{
"#type": "IriTemplateMapping",
"variable": "order[dateDeleted]",
"property": "dateDeleted",
"required": false
}
]
}
}
one would then assume that api/organisations?platform=ba8d9958-d8d5-11e8-a9a5-5254007d3b24 gives all the organizations belonging to that platform, but no. It gives
{
"#context": "/api/contexts/Organisation",
"#id": "/api/organisations",
"#type": "hydra:Collection",
"hydra:member": [],
"hydra:totalItems": 0,
"hydra:view": {
"#id": "/api/organisations?platform=ba8d9958-d8d5-11e8-a9a5-5254007d3b24",
"#type": "hydra:PartialCollectionView"
},
"hydra:search": {
"#type": "hydra:IriTemplate",
"hydra:template": "/api/organisations{?id,id[],description,name,organisation,organisation[],platform,platform[],dateCreated[before],dateCreated[strictly_before],dateCreated[after],dateCreated[strictly_after],datePublished[before],datePublished[strictly_before],datePublished[after],datePublished[strictly_after],dateModified[before],dateModified[strictly_before],dateModified[after],dateModified[strictly_after],dateDeleted[before],dateDeleted[strictly_before],dateDeleted[after],dateDeleted[strictly_after],order[id],order[name],order[dateCreated],order[datePublished],order[dateModified],order[dateDeleted]}",
"hydra:variableRepresentation": "BasicRepresentation",
"hydra:mapping": [
{
"#type": "IriTemplateMapping",
"variable": "id",
"property": "id",
"required": false
},
{
"#type": "IriTemplateMapping",
"variable": "id[]",
"property": "id",
"required": false
},
{
"#type": "IriTemplateMapping",
"variable": "description",
"property": "description",
"required": false
},
{
"#type": "IriTemplateMapping",
"variable": "name",
"property": "name",
"required": false
},
{
"#type": "IriTemplateMapping",
"variable": "organisation",
"property": "organisation",
"required": false
},
{
"#type": "IriTemplateMapping",
"variable": "organisation[]",
"property": "organisation",
"required": false
},
{
"#type": "IriTemplateMapping",
"variable": "platform",
"property": "platform",
"required": false
},
{
"#type": "IriTemplateMapping",
"variable": "platform[]",
"property": "platform",
"required": false
},
{
"#type": "IriTemplateMapping",
"variable": "dateCreated[before]",
"property": "dateCreated",
"required": false
},
{
"#type": "IriTemplateMapping",
"variable": "dateCreated[strictly_before]",
"property": "dateCreated",
"required": false
},
{
"#type": "IriTemplateMapping",
"variable": "dateCreated[after]",
"property": "dateCreated",
"required": false
},
{
"#type": "IriTemplateMapping",
"variable": "dateCreated[strictly_after]",
"property": "dateCreated",
"required": false
},
{
"#type": "IriTemplateMapping",
"variable": "datePublished[before]",
"property": "datePublished",
"required": false
},
{
"#type": "IriTemplateMapping",
"variable": "datePublished[strictly_before]",
"property": "datePublished",
"required": false
},
{
"#type": "IriTemplateMapping",
"variable": "datePublished[after]",
"property": "datePublished",
"required": false
},
{
"#type": "IriTemplateMapping",
"variable": "datePublished[strictly_after]",
"property": "datePublished",
"required": false
},
{
"#type": "IriTemplateMapping",
"variable": "dateModified[before]",
"property": "dateModified",
"required": false
},
{
"#type": "IriTemplateMapping",
"variable": "dateModified[strictly_before]",
"property": "dateModified",
"required": false
},
{
"#type": "IriTemplateMapping",
"variable": "dateModified[after]",
"property": "dateModified",
"required": false
},
{
"#type": "IriTemplateMapping",
"variable": "dateModified[strictly_after]",
"property": "dateModified",
"required": false
},
{
"#type": "IriTemplateMapping",
"variable": "dateDeleted[before]",
"property": "dateDeleted",
"required": false
},
{
"#type": "IriTemplateMapping",
"variable": "dateDeleted[strictly_before]",
"property": "dateDeleted",
"required": false
},
{
"#type": "IriTemplateMapping",
"variable": "dateDeleted[after]",
"property": "dateDeleted",
"required": false
},
{
"#type": "IriTemplateMapping",
"variable": "dateDeleted[strictly_after]",
"property": "dateDeleted",
"required": false
},
{
"#type": "IriTemplateMapping",
"variable": "order[id]",
"property": "id",
"required": false
},
{
"#type": "IriTemplateMapping",
"variable": "order[name]",
"property": "name",
"required": false
},
{
"#type": "IriTemplateMapping",
"variable": "order[dateCreated]",
"property": "dateCreated",
"required": false
},
{
"#type": "IriTemplateMapping",
"variable": "order[datePublished]",
"property": "datePublished",
"required": false
},
{
"#type": "IriTemplateMapping",
"variable": "order[dateModified]",
"property": "dateModified",
"required": false
},
{
"#type": "IriTemplateMapping",
"variable": "order[dateDeleted]",
"property": "dateDeleted",
"required": false
}
]
}
}
Or simply put zero organization. Now for bug hunting purposes I build the entire thing again but now using incremental ID's and then it works like a charm. So the problem seems to be the way ApiFilter works. That would suggest that ApiFilter doesn't actually normalize the UUID before using it in a search...
-- Update --
Okey, so I tried bypassing this with an custom filter
namespace App\Conduction\AppBundle\Filter;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\AbstractContextAwareFilter;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface;
use Doctrine\ORM\QueryBuilder;
use Ramsey\Uuid\Uuid;
final class UuidFilter extends AbstractContextAwareFilter
{
protected function filterProperty(string $property, $value, QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, string $operationName = null)
{
// otherwise filter is applied to order and page as well
if (
!$this->isPropertyEnabled($property, $resourceClass) ||
!$this->isPropertyMapped($property, $resourceClass)
) {
return;
}
$value =pack("h*", str_replace('-', '', $value));
$parameterName = $queryNameGenerator->generateParameterName($property); // Generate a unique parameter name to avoid collisions with other filters
$queryBuilder
->andWhere("$property =:$parameterName")
->setParameter($parameterName, $value);
}
// This function is only used to hook in documentation generators (supported by Swagger and Hydra)
public function getDescription(string $resourceClass): array
{
if (!$this->properties) {
return [];
}
$description = [];
foreach ($this->properties as $property => $strategy) {
$description["uuid"] = [
'property' => $property,
'type' => 'string',
'required' => false,
'swagger' => [
'description' => 'This filter allows to search the api based on object uuids',
'name' => 'UUID',
'type' => 'Primary identifier',
],
];
}
return $description;
}
}
But then I run into an doctrine error "[Semantical Error] line 0, col 76 near 'platform =:platform_p1': Error: 'platform' is not defined."
That of course suggests that platform isn't defined but as far as I can see it is.
--- UPDATE: Solved it! ---
Interesting thought, but unfortunately that also returns zero results. But interesting stuff is happening. So using the IRI doctrine does get the proper intel -> as seen in the logs as:
[2018-10-30 10:00:50] doctrine.DEBUG: SELECT o0_.slug AS slug_0, o0_.id AS id_1, o0_.name AS name_2, o0_.description AS description_3, o0_.date_created AS date_created_4, o0_.date_published AS date_published_5, o0_.date_modified AS date_modified_6, o0_.date_deleted AS date_deleted_7, o0_.date_checkout AS date_checkout_8, o0_.version AS version_9, o0_.address_id AS address_id_10, o0_.organisation_id AS organisation_id_11, o0_.default_tax_id AS default_tax_id_12, o0_.member_group_id AS member_group_id_13, o0_.admin_group_id AS admin_group_id_14, o0_.image_id AS image_id_15, o0_.platform_id AS platform_id_16, o0_.user_owned AS user_owned_17, o0_.user_created AS user_created_18, o0_.user_updated AS user_updated_19, o0_.user_checkout AS user_checkout_20 FROM organisation__organisation o0_ WHERE o0_.platform_id = ? AND o0_.date_deleted IS NULL AND o0_.date_published < ? ORDER BY o0_.id ASC ["[object] (Ramsey\\Uuid\\Uuid: \"13be1406-d9c3-11e8-9d84-5254007d3b24\")","2018-10-30 10:00:50"] []
So here doctrine is apparently aware that it is searching for a Ramsy\Uuid instead of “13be1406-d9c3-11e8-9d84-5254007d3b2” directly. Okey so what if we search without the the platform IRI?
That gives a nice response of 1 object, and the following doctrine log
[2018-10-30 10:09:46] doctrine.DEBUG: SELECT o0_.slug AS slug_0, o0_.id AS id_1, o0_.name AS name_2, o0_.description AS description_3, o0_.date_created AS date_created_4, o0_.date_published AS date_published_5, o0_.date_modified AS date_modified_6, o0_.date_deleted AS date_deleted_7, o0_.date_checkout AS date_checkout_8, o0_.version AS version_9, o0_.address_id AS address_id_10, o0_.organisation_id AS organisation_id_11, o0_.default_tax_id AS default_tax_id_12, o0_.member_group_id AS member_group_id_13, o0_.admin_group_id AS admin_group_id_14, o0_.image_id AS image_id_15, o0_.platform_id AS platform_id_16, o0_.user_owned AS user_owned_17, o0_.user_created AS user_created_18, o0_.user_updated AS user_updated_19, o0_.user_checkout AS user_checkout_20 FROM organisation__organisation o0_ WHERE o0_.date_deleted IS NULL AND o0_.date_published < ? ORDER BY o0_.id ASC ["2018-10-30 10:09:46"] []
So the difference really seams to be in the Ramsy\Uuid, peculiar thing here is that if we get .. /api/platforms/13be1406-d9c3-11e8-9d84-5254007d3b24 we get a nice response of that platform object.So the parsing of the Ramsy\Uuid in doctrine looks to be working, uh okey…. So what about that id field then? If we just grap the database and do a simple join. We are able to join the organization and id, so the platform.id and organization__organistation.platform_id field do contain the same value. And then for the real kicker….
.../api/organisations?platform.id=13be1406-d9c3-11e8-9d84-5254007d3b24
Does work! So that solves our problem “yeah”, just in an unexpected way. It present us with a doctrine log of:
[2018-10-30 10:19:15] doctrine.DEBUG: SELECT t0.editable AS editable_1, t0.memberGroup AS memberGroup_2, t0.id AS id_3, t0.name AS name_4, t0.description AS description_5, t0.date_created AS date_created_6, t0.date_published AS date_published_7, t0.date_modified AS date_modified_8, t0.date_deleted AS date_deleted_9, t0.date_checkout AS date_checkout_10, t0.version AS version_11, t0.organisation_id AS organisation_id_12, t13.slug AS slug_14, t13.id AS id_15, t13.name AS name_16, t13.description AS description_17, t13.date_created AS date_created_18, t13.date_published AS date_published_19, t13.date_modified AS date_modified_20, t13.date_deleted AS date_deleted_21, t13.date_checkout AS date_checkout_22, t13.version AS version_23, t13.address_id AS address_id_24, t13.organisation_id AS organisation_id_25, t13.default_tax_id AS default_tax_id_26, t13.member_group_id AS member_group_id_27, t13.admin_group_id AS admin_group_id_28, t13.image_id AS image_id_29, t13.platform_id AS platform_id_30, t13.user_owned AS user_owned_31, t13.user_created AS user_created_32, t13.user_updated AS user_updated_33, t13.user_checkout AS user_checkout_34, t0.parent_id AS parent_id_35, t0.image_id AS image_id_36, t0.platform_id AS platform_id_37, t0.user_owned AS user_owned_38, t0.user_created AS user_created_39, t0.user_updated AS user_updated_40, t0.user_checkout AS user_checkout_41 FROM organisation__organisation_group t0 LEFT JOIN organisation__organisation t13 ON t0.organisation_id = t13.id INNER JOIN organisation__organisation_group_member ON t0.id = organisation__organisation_group_member.group_id WHERE organisation__organisation_group_member.user_id = ? ["[object] (Ramsey\\Uuid\\Uuid: \"154f07d0-d9c3-11e8-bf04-5254007d3b24\")"] []
Wait wut? Yeah. If we do an api/organisations?platform=/api/platforms/ba8d9958-d8d5-11e8-a9a5-5254007d3b24. Doctrine doesn't do an inner join and can't correctly decode the UUID but when we do api/organisations?platform.id=ba8d9958-d8d5-11e8-a9a5-5254007d3b24. API platform forces an inner join and then it suddenly works...
Okay our problem is solved now, but I still think that this is something the either API Platform (that supports Ramsy\Uuid) or Ramsy\Uuid might want to look into

I came across this issue and was having a similar problem but solved it without a custom filter. You need only update your #ApiFilter() definition.
I changed this line in your organization model from:
* #ApiFilter(SearchFilter::class, properties={"id": "exact", "description": "partial", "name": "partial","organisation": "exact","platform": "exact"})
To
* #ApiFilter(SearchFilter::class, properties={"id": "exact", "description": "partial", "name": "partial","organisation": "exact","platform.uuid": "exact"})
Note the change to the platform definition. This was all I needed to fix my use case.
I see another comment says to use .id but that didn't work from me as that would only search on the id column not the uuid.

I'm pretty sure that the "correct" search would be:
api/organisations?platform=/api/platforms/ba8d9958-d8d5-11e8-a9a5-5254007d3b24.
You need to use the iri, not the id uniquely.

You can create a custom filter:
services.yml
App\Api\Filter\UlidFilter:
tags: [ 'api_platform.filter' ]
App\Api\Filter\UlidFilter.php
namespace App\Api\Filter;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\AbstractContextAwareFilter;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface;
use Doctrine\ORM\QueryBuilder;
use Symfony\Component\Uid\Ulid;
final class UlidFilter extends AbstractContextAwareFilter
{
protected function filterProperty(string $property, $value, QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, string $operationName = null)
{
// otherwise filter is applied to order and page as well
if (
!$this->isPropertyEnabled($property, $resourceClass) ||
!$this->isPropertyMapped($property, $resourceClass, true)
) {
return;
}
// Generate a unique parameter name to avoid collisions with other filters
$parameterName = $queryNameGenerator->generateParameterName($property);
$queryBuilder
->andWhere(sprintf('o.%s = :%s', $property, $parameterName))
->setParameter($parameterName, (new Ulid($value))->toBinary());
}
// This function is only used to hook in documentation generators (supported by Swagger and Hydra)
public function getDescription(string $resourceClass): array
{
if (!$this->properties) {
return [];
}
$description = [];
foreach ($this->properties as $property => $strategy) {
$description[$property] = [
'property' => $property,
'type' => 'string',
'required' => false,
'swagger' => [
'description' => 'Filter Ulid property.',
'name' => 'Ulid Search filter',
'type' => '',
],
];
}
return $description;
}
}
In your entity:
use App\Api\Filter\UlidFilter;
...
/**
* #ApiResource()
* #ApiFilter(UlidFilter::class, properties={"Field": "exact"})
**/

I've been having the same issue and found what worked for me.
I have two entities Export and Listing.
OneToMany on Listing -> Export.
I wanted to filter on Export by Listing Id (UUID).
#[ApiFilter(SearchFilter::class, properties: ['listing' => 'exact'])]
What I found was that if I did a query at api/exports?listing={insert IRI or uuid} it wasn't working, however if I changed the query parameter api/exports?listing.id={insert IRI or uuid} then it worked.
Conversely, if I change the filter to this –
#[ApiFilter(SearchFilter::class, properties: ['listing.id' => 'exact'])]
Then I can just query at /api/exports?listing={insert IRI or uuid} and it works perfectly.
What I didn't realise was that this works on all properties on a relation, so if you were to just list listing as the filter, you can actually filter on any property on a Listing from your query parameters and it will work (eg. listing.id={} or listing.type etc)
Interesting as it opens up other possibilities but for this specific problem it solves the uuid issue.

Related

Any idea how to do custom supportedCookingModes in Alexa discovery?

I'm trying to return a Discovery Response, but the supportedCookingModes only seems to accept standard values and only in the format of ["OFF","BAKE"], not Custom values as indicated by the documentation. Any idea on how to specify custom values?
{
"event": {
"header": {
"namespace": "Alexa.Discovery",
"name": "Discover.Response",
"payloadVersion": "3",
"messageId": "asdf"
},
"payload": {
"endpoints": [
{
"endpointId": "asdf",
"capabilities": [
{
"type": "AlexaInterface",
"interface": "Alexa.Cooking",
"version": "3",
"properties": {
"supported": [
{
"name": "cookingMode"
}
],
"proactivelyReported": true,
"retrievable": true,
"nonControllable": false
},
"configuration": {
"supportsRemoteStart": true,
"supportedCookingModes": [
{
"value": "OFF"
},
{
"value": "BAKE"
},
{
"value": "CUSTOM",
"customName": "FANCY_NANCY_MODE"
}
]
}
}
]
}
]
}
}
}
Custom cooking modes are brand specific. This functionality is not yet publicly available. I recommend you to choose one of the existing cooking modes:
https://developer.amazon.com/en-US/docs/alexa/device-apis/cooking-property-schemas.html#cooking-mode-values

How to get proposed time using Microsoft Graph API?

When the other person declined the meeting and proposed a new time. In Outlook, you can see the proposed time.
Now I am trying to use Microsoft Graph API to get that proposed time.
For example, the original meeting date is 2018-03-08, and the other person declined and proposed a new date 2018-03-12.
I tried
GET /beta/me/messages/{messageId}=?$expand=microsoft.graph.eventMessage/event
However, I cannot find the proposed time from the result returned. How can I get it? Thanks
{
"#odata.context": "https://graph.microsoft.com/beta/$metadata#users('576552d5-3bc0-42a6-a53d-bfceb405db23')/messages/$entity",
"#odata.type": "#microsoft.graph.eventMessage",
"#odata.etag": "W/\"DAAAABYAAACpTc/InBsuTYwTUBb+VIb4AADoRAyI\"",
"id": "AAMkADBlZTUwNTkxLWVmODgtNDVhNC1iZjhlLTdjNjA1ODZlMDI5MgBGAAAAAACUbnk-iwQZRbXMgkfKtmYhBwCpTc-InBsuTYwTUBb_VIb4AAAAAAEMAACpTc-InBsuTYwTUBb_VIb4AADnwc8mAAA=",
"createdDateTime": "2018-03-06T22:29:10Z",
"lastModifiedDateTime": "2018-03-06T22:29:11Z",
"changeKey": "DAAAABYAAACpTc/InBsuTYwTUBb+VIb4AADoRAyI",
"categories": [],
"receivedDateTime": "2018-03-06T22:29:11Z",
"sentDateTime": "2018-03-06T22:29:05Z",
"hasAttachments": false,
"internetMessageId": "<MWHPR15MB18399806CC97C61817C9A2B18BD90#MWHPR15MB1839.namprd15.prod.outlook.com>",
"subject": "New Time Proposed: Test",
"bodyPreview": "",
"importance": "normal",
"parentFolderId": "AAMkADBlZTUwNTkxLWVmODgtNDVhNC1iZjhlLTdjNjA1ODZlMDI5MgAuAAAAAACUbnk-iwQZRbXMgkfKtmYhAQCpTc-InBsuTYwTUBb_VIb4AAAAAAEMAAA=",
"conversationId": "AAQkADBlZTUwNTkxLWVmODgtNDVhNC1iZjhlLTdjNjA1ODZlMDI5MgAQAOnuCMgoRLdGs-1scw6i7EU=",
"conversationIndex": "AdO1mnWL6e4IyChEt0az/WxzDqLsRQAABO5D",
"isDeliveryReceiptRequested": null,
"isReadReceiptRequested": false,
"isRead": false,
"isDraft": false,
"webLink": "https://outlook.office365.com/owa/?ItemID=AAMkADBlZTUwNTkxLWVmODgtNDVhNC1iZjhlLTdjNjA1ODZlMDI5MgBGAAAAAACUbnk%2FiwQZRbXMgkfKtmYhBwCpTc%2FInBsuTYwTUBb%2BVIb4AAAAAAEMAACpTc%2FInBsuTYwTUBb%2BVIb4AADnwc8mAAA%3D&exvsurl=1&viewmodel=ReadMessageItem",
"inferenceClassification": "focused",
"unsubscribeData": [],
"unsubscribeEnabled": false,
"meetingMessageType": "meetingDeclined",
"type": "singleInstance",
"isOutOfDate": false,
"isAllDay": false,
"isDelegated": false,
"body": {
"contentType": "html",
"content": "Hi"
},
"sender": {
"emailAddress": {
"name": "Rose",
"address": "rose#example.com"
}
},
"from": {
"emailAddress": {
"name": "Rose",
"address": "rose#example.com"
}
},
"toRecipients": [
{
"emailAddress": {
"name": "Jack",
"address": "jack#example.com"
}
}
],
"ccRecipients": [],
"bccRecipients": [],
"replyTo": [],
"mentionsPreview": null,
"flag": {
"flagStatus": "notFlagged"
},
"startDateTime": {
"dateTime": "2018-03-08T04:00:00.0000000",
"timeZone": "UTC"
},
"endDateTime": {
"dateTime": "2018-03-08T05:00:00.0000000",
"timeZone": "UTC"
},
"location": {
"displayName": "Test",
"locationType": "default",
"uniqueIdType": "unknown"
},
"recurrence": null,
"event#odata.context": "https://graph.microsoft.com/beta/$metadata#users('576552d5-3bc0-42a6-a53d-bfceb405db23')/messages('AAMkADBlZTUwNTkxLWVmODgtNDVhNC1iZjhlLTdjNjA1ODZlMDI5MgBGAAAAAACUbnk-iwQZRbXMgkfKtmYhBwCpTc-InBsuTYwTUBb_VIb4AAAAAAEMAACpTc-InBsuTYwTUBb_VIb4AADnwc8mAAA%3D')/microsoft.graph.eventMessage/event/$entity",
"event": {
"#odata.etag": "W/\"qU3PyJwbLk2ME1AW/lSG+AAA6EQMeA==\"",
"id": "AAMkADBlZTUwNTkxLWVmODgtNDVhNC1iZjhlLTdjNjA1ODZlMDI5MgBGAAAAAACUbnk-iwQZRbXMgkfKtmYhBwCpTc-InBsuTYwTUBb_VIb4AAAAAAENAACpTc-InBsuTYwTUBb_VIb4AADnwkmiAAA=",
"createdDateTime": "2018-03-06T22:28:32.3852279Z",
"lastModifiedDateTime": "2018-03-06T22:29:11.4604154Z",
"changeKey": "qU3PyJwbLk2ME1AW/lSG+AAA6EQMeA==",
"categories": [],
"originalStartTimeZone": "Pacific Standard Time",
"originalEndTimeZone": "Pacific Standard Time",
"iCalUId": "040000008200E00074C5B7101A82E0080000000000000000000000000000000000000000310000007643616C2D5569640100000033324633333433392D433744452D344338362D393046452D44424639314131363444323900",
"reminderMinutesBeforeStart": 15,
"isReminderOn": true,
"hasAttachments": false,
"subject": "Test",
"bodyPreview": "Test",
"importance": "normal",
"sensitivity": "normal",
"isAllDay": false,
"isCancelled": false,
"isOrganizer": true,
"responseRequested": true,
"seriesMasterId": null,
"showAs": "busy",
"type": "singleInstance",
"webLink": "https://outlook.office365.com/owa/?itemid=AAMkADBlZTUwNTkxLWVmODgtNDVhNC1iZjhlLTdjNjA1ODZlMDI5MgBGAAAAAACUbnk%2FiwQZRbXMgkfKtmYhBwCpTc%2FInBsuTYwTUBb%2BVIb4AAAAAAENAACpTc%2FInBsuTYwTUBb%2BVIb4AADnwkmiAAA%3D&exvsurl=1&path=/calendar/item",
"onlineMeetingUrl": null,
"responseStatus": {
"response": "organizer",
"time": "0001-01-01T00:00:00Z"
},
"body": {
"contentType": "html",
"content": "Hi"
},
"start": {
"dateTime": "2018-03-08T04:00:00.0000000",
"timeZone": "UTC"
},
"end": {
"dateTime": "2018-03-08T05:00:00.0000000",
"timeZone": "UTC"
},
"location": {
"displayName": "Test",
"locationType": "default",
"uniqueId": "Test",
"uniqueIdType": "private"
},
"locations": [
{
"displayName": "Test",
"locationType": "default",
"uniqueId": "Test",
"uniqueIdType": "private"
}
],
"recurrence": null,
"attendees": [
{
"type": "required",
"status": {
"response": "declined",
"time": "2018-03-06T22:29:05Z"
},
"emailAddress": {
"name": "Rose",
"address": "rose#example.com"
}
}
],
"organizer": {
"emailAddress": {
"name": "Jack",
"address": "jack#example.com"
}
}
}
}
The API doesn't expose this information directly. The related entities just don't have the fields defined to show this information. However, the data is obviously there, you just need to know how to get to it. The answer lies in extended properties.
Basically since Graph doesn't expose these values, we need to dig into the MAPI properties that store this data. After a little digging through the Exchange server protocol docs I found PidLidAppointmentProposedStartWhole and PidLidAppointmentProposedEndWhole. We just need to translate those MAPI property definitions into the Graph extended property syntax.
From the doc, those are both in the PSETID_Appointment namespace, which uses the GUID {00062002-0000-0000-C000-000000000046}. PidLidAppointmentProposedStartWhole uses id 0x8250, and PidLidAppointmentProposedEndWhole uses id 0x8251. So those should translate to:
PidLidAppointmentProposedStartWhole: 'SystemTime {00062002-0000-0000-C000-000000000046} Id 0x8250'
PidLidAppointmentProposedEndWhole: 'SystemTime {00062002-0000-0000-C000-000000000046} Id 0x8251'
If we use those in an $expand clause as per Get singleValueLegacyExtendedProperty, we get something like:
GET /me/mailfolders/inbox/messages?$expand=singleValueExtendedProperties($filter=id eq 'SystemTime {00062002-0000-0000-C000-000000000046} Id 0x8250' or id eq 'SystemTime {00062002-0000-0000-C000-000000000046} Id 0x8251')
And the response looks something like this (other properties omitted):
{
"subject": "New Time Proposed: Let's meet",
"singleValueExtendedProperties": [
{
"id": "SystemTime {00062002-0000-0000-c000-000000000046} Id 0x8250",
"value": "2018-03-20T20:00:00Z"
},
{
"id": "SystemTime {00062002-0000-0000-c000-000000000046} Id 0x8251",
"value": "2018-03-20T21:00:00Z"
}
]
}

liquibase defaultvalue sys_guid

how can i make a column default to SYS_GUID()using liquibase? i tried
{
"column": {
"name": "ID",
"type": "varchar(32)",
"constraints": {
"nullable": false,
"primariKey": true,
"unique": true,
"defaultValueComputed": "SYS_GUID()"
}
}
},
also tried changing sys guid to uuid
"column": {
"name": "ID",
"type": "varchar(32)"
"constraints": {
"nullable": false,
"primariKey": true,
"unique": true,
"defaultValueComputed": "UUID"
}
}
sorry for the dumb question, it was like this:
"column": {
"name": "ID",
"type": "varchar(32)",
"defaultValueComputed": "SYS_GUID()",
"constraints": {
"nullable": false,
"primariKey": true,
"unique": true
}
}
i was dumb enough to put the default in the constraints

Fetch all user ids under domain

How do I fetch all user Ids under our company domain using Google API?
I want to get list of all users under our domain.
Then I want to get a list of all emails for each user.
I think you are referring to Retrieve all users in a domain:
To retrieve all users in the same domain, use the following GET request and include the authorization described in Authorize requests. For readability, this example uses line returns:
GET https://www.googleapis.com/admin/directory/v1/users
?domain=primary domain name&pageToken=token for next results page
&maxResults=max number of results per page
&orderBy=email, givenName, or familyName
&sortOrder=ascending or descending
&query=email, givenName, or familyName:the query's value*
By default, the system returns a list of 100 users in the alphabetical order of the user's email address:
GET https://www.googleapis.com/admin/directory/v1/users?domain=example.com&maxResults=2
A successful response returns an HTTP 200 status code. Along with the status code, the response returns 2 user accounts in the example.com domain (maxResults=2):
{
"kind": "directory#users",
"users": [
{
"kind": "directory#user",
"id": "the unique user id",
"primaryEmail": "liz#example.com",
"name": {
"givenName": "Liz",
"familyName": "Smith",
"fullName": "Liz Smith"
},
"isAdmin": true,
"isDelegatedAdmin": false,
"lastLoginTime": "2013-02-05T10:30:03.325Z",
"creationTime": "2010-04-05T17:30:04.325Z",
"agreedToTerms": true,
"hashFunction: "SHA-1",
"suspended": false,
"changePasswordAtNextLogin": false,
"ipWhitelisted": false,
"ims": [
{
"type": "work",
"protocol": "gtalk",
"im": "lizim#talk.example.com",
"primary": true
}
],
"emails": [
{
"address": "liz#example.com",
"type": "work",
"customType": "",
"primary": true
}
],
"addresses": [
{
"type": "work",
"customType": "",
"streetAddress": "1600 Amphitheatre Parkway",
"locality": "Mountain View",
"region": "CA",
"postalCode": "94043"
}
],
"externalIds": [
{
"value": "employee number",
"type": "custom",
"customType": "office"
}
],
"relations": [
{
"value": "susan",
"type": "friend",
"customType": ""
}
],
"organizations": [
{
"name": "Google Inc.",
"title": "SWE",
"primary": true,
"customType": "",
"description": "Software engineer"
}
],
"phones": [
{
"value": "+1 nnn nnn nnnn",
"type": "work"
}
],
"aliases": [
"lizsmith#example.com",
"lsmith#example.com"
],
"nonEditableAliases: [
"liz#test.com"
],
"customerId": "C03az79cb",
"orgUnitPath": "corp/engineering",
"isMailboxSetup": true,
"includeInGlobalAddressList": true
},
{
"kind": "directory#user",
"id": "user unique ID",
"primaryEmail": "admin2#example.com",
"name": {
"givenName": "admin",
"familyName": "two",
"fullName": "admin two"
},
"isAdmin": true,
"isDelegatedAdmin": true,
"lastLoginTime": "2013-02-05T10:30:03.325Z",
"creationTime": "2010-04-05T17:30:04.325Z",
"agreedToTerms": true,
"hashFunction: "SHA-1",
"suspended": true,
"suspensionReason": "ADMIN",
"changePasswordAtNextLogin": false,
"ipWhitelisted": false,
"emails": [
{
"address": "admin2#example.com",
"type": "work",
"customType": "",
"primary": true
}
],
"externalIds": [
{
"value": "contractor license number",
"type": "custom",
"customType": "work"
}
],
"relations": [
{
"value": "liz",
"type": "friend",
"customType": ""
}
],
"aliases": [
"second_admin#example.com"
],
"nonEditableAliases: [
"admin#test.com"
],
"customerId": "C03az79cb",
"orgUnitPath": "corp/engineering",
"isMailboxSetup": true,
"includeInGlobalAddressList": true
}
],
"nextPageToken": "next page token"
}
You can also check out Retrieve all account users
To retrieve all users in an account which can consist of multiple domains, use the following GET request and include the authorization described in Authorize requests.

Combine JSON objects in Ruby, and output to CSV

Let's say I have two JSON objects (call them 'websites' and 'links'). I need to end up with both objects in a single CSV (ideally in separate columns).
I'm working with something like this:
File.open("file.json", "w") do |f|
combined = [websites, links]
f.write(JSON.pretty_generate(combined))
end
And then I'm using the Ruby gem json2csv to convert this file to a CSV. But when I do, I get the following error:
error: undefined method 'keys' for #<Array:0x007fea8a8e33f8>
I can't figure out what's wrong. When I look in file.json, it appears to be structured like this: [{websites}, {links}]. From my limited knowledge of JSON, I think that's right, but I could easily be wrong.
Also, I know this won't get me separate columns in the CSV. If anyone has an answer for that part, major bonus points.
EDIT: JSON examples included below; error message changed after minor fix on my end.
websites:
{
"uri": "https://v1/websites",
"id": 28235674,
"background": null,
"createdDate": 1399585684000,
"lastActivityDate": 1430682494000,
"lastCommunicationDate": 1430682494000,
"lastNonCommunicationChronicleDate": 1430330886000,
"lastModifiedDate": 1449263116000,
"lastViewedDate": 1421429034000,
"preferredContactType": null,
"rss": "",
"emailAddresses": [
{
"email": "",
"type": "Work"
},
{
"email": "",
"type": "Work"
},
{
"email": "",
"type": "Work"
},
{
"email": "not found",
"type": "Work"
}
],
"phoneNumbers": [
],
"streetAddresses": [
],
"socialNetworks": [
{
"profileUrl": "http://twitter.com",
"name": "Twitter"
},
{
"profileUrl": "http://www.facebook.com",
"name": "Facebook"
},
{
"profileUrl": "http://plus.google.com",
"name": "GooglePlus"
},
{
"profileUrl": "http://www.linkedin.com",
"name": "LinkedIn"
},
{
"profileUrl": "http://twitter.com",
"name": "Twitter"
}
],
"contactUrls": [
],
"tags": [
"tag1",
"tag2"
],
"mostRecentActivity": "https://v1/history",
"mostRecentChronicle": "https://v1/history",
"mostRecentCommunication": "https://v1/history",
"mostRecentNonCommunicationChronicle": "https://v1/history",
"projectStates": "https://v1/websites",
"history": "https://v1/history",
"customFieldValues": [
],
"name": "",
"primaryDomain": "",
"domains": [
""
],
"associatedPeople": "https://v1/people",
"payments": "https://payments",
"links": "https://v1/links",
"type": "https://v1/websites"
}
links:
{
"uri": "https://v1/links/custom_fields",
"id": 15529329,
"value": "Name",
"backgroundColor": null,
"customField": "https://v1/links/custom_fields"
}
combined output:
[
{
"uri": "https://v1/websites",
"id": 28235674,
"background": null,
"createdDate": 1399585684000,
"lastActivityDate": 1430682494000,
"lastCommunicationDate": 1430682494000,
"lastNonCommunicationChronicleDate": 1430330886000,
"lastModifiedDate": 1449263116000,
"lastViewedDate": 1421429034000,
"preferredContactType": null,
"rss": "",
"emailAddresses": [
{
"email": "",
"type": "Work"
},
{
"email": "",
"type": "Work"
},
{
"email": "",
"type": "Work"
},
{
"email": "not found",
"type": "Work"
}
],
"phoneNumbers": [
],
"streetAddresses": [
],
"socialNetworks": [
{
"profileUrl": "http://twitter.com/",
"name": "Twitter"
},
{
"profileUrl": "http://www.facebook.com",
"name": "Facebook"
},
{
"profileUrl": "http://plus.google.com",
"name": "GooglePlus"
},
{
"profileUrl": "http://www.linkedin.com/",
"name": "LinkedIn"
},
{
"profileUrl": "http://twitter.com/",
"name": "Twitter"
}
],
"contactUrls": [
],
"tags": [
"tag1",
"tag2"
],
"mostRecentActivity": "https://v1/history/",
"mostRecentChronicle": "https://v1/history/",
"mostRecentCommunication": "https://v1/history/",
"mostRecentNonCommunicationChronicle": "https://v1/history/",
"projectStates": "https://v1/websites/",
"history": "https://v1/history",
"customFieldValues": [
],
"name": "",
"primaryDomain": "",
"domains": [
""
],
"associatedPeople": "https://v1/people",
"links": "https://v1/links",
"type": "https://v1/websites"
},
{
"uri": "https://links/custom_fields",
"id": 15529329,
"value": "Name",
"backgroundColor": null,
"customField": "https://links/custom_fields"
}
]
JSON.pretty_generate() is expecting a hash and you are passing an array of 2 hashes. Start with: combined.map { |c| f.write(JSON.pretty_generate(c)) } and then mapping them into your CSV should be as easy as following the CSV documentation.

Resources