Extend Hydra search context to generate filter fields client side? - symfony

I'm using API Platform 3.1. When you've got an entity with a ApiFilter, Hydra will give some information about all fields with a filter. In this simple example it will give the name of the field (createdAt):
class Entity
{
#[ApiFilter(DateFilter::class, properties: ['createdAt'])]
protected ?DateTimeInterface $createdAt = null;
}
'hydra:search': {
"#type": "hydra:IriTemplate"
"hydra:mapping": {
"#type": "IriTemplateMapping",
"variable": "createdAt[after]",
"property": "createdAt",
"required": false
}
}
This information can be used to populate search fields client side, like Swagger does:
Now I want this information to generate a form with fields that can be used to sort or filter. But I want not only the field names, but some extra information about how the filter can be used. For example: a date field createdAt should render a slightly different datepicker than a birthdate field.
I've already tried extending the Hydra context as described in API Platform's documentation:
#[ApiResource(operations: [
new Get(hydraContext: ['foo' => 'bar'])
])
But I didn't found foo/bar in my /api/items response.
Is there a way to add some information to the Hydra model which I can use in my application?

Related

Use Extension to filter OneToMany relationship

I'm using API Platform 3 and Gedmo's Softdeleteable Extension. For my entities, I'm using Extensions to add $queryBuilder->andWhere('o.deletedAt IS NULL') to all queries.
This is working fine, but when using OneToMany relations, API Platform doesn't use these extensions and therefor shows 'softdeleted' entities.
Let's take a slightly modified example from their documentation:
#[ApiResource]
class User
{
#[ORM\OneToMany]
#[Serializer\Groups(['user'])]
public Collection $offers;
}
/api/offers/1 correctly shows a 404, because it has been softdeleted. But /api/users/1 shows something like this:
{
username: 'foo',
offers:
{
'#id': "/api/offers/1",
deletedAt: "2022-01-27T12:04:45+01:00"
}
}
How can I change the query that API Platform uses to fetch the relationships?
You have two methods of achieving this.
Filter hydrated objects:
Inside the existing (or completely new) getter:
return $this->offers->filter(function(Offer offer){
return $offer->getDeletedAt() !== NULL;
});
Apply criteria to query:
Again, inside the existing (or completely new) getter:
$criteria = Criteria::create()
->andWhere(Criteria::expr()->eq('deletedAt', NULL));
return $this->offers->matching($criteria);
The Criteria method is preferable if your user could have LOTS of deleted offers - the filtering would be performed on a database level.
Hope this helps.
Btw, both of these methods are well explained in SymfonyCasts tutorials:
https://symfonycasts.com/screencast/api-platform-security/filtered-collection
https://symfonycasts.com/screencast/symfony4-doctrine-relations/collection-criteria

Hasura object permission based authorization

I am trying to set a "Row Select" permissions on Hasura. I have a (simplified for brevity) Data Model like below
User
id: UserID
App
id: AppID
App Permissions
user_id: User ID
app_id: App ID
permissions: [ ENUM: Admin, View, Owner ]
Feed
app_id: AppID
feed_data: Some Feed Data
Now, I wish to query all Feed for an authenticated user. The query can be of the form
GET all apps, for which the authenticated user has view permissions
query MyQuery {
feed(limit: 10) {
app_id
feed_data
}
}
GET apps with app_id in the query filter for which the authenticated user has view permissions
query MyQuery {
feed(limit: 10, where: {app_id: {_in: [1, 2]}}) {
app_id
feed_data
}
}
Since feed table does not have user_id information directly in it, I can not use X-Hasura-User-Id attribute directly against feed table. I also tried to use _exists relation against the app_permission table, but I am unable to put app_id filter in the permission clause.
{
"_exists": {
"_where": {
"user_id": {
"_eq": "X-Hasura-User-Id"
}
},
"_table": {
"schema": "public",
"name": "app_permission"
}
}
}
I am not really sure how to proceed with such data modelling with Hasura. Any help is appreciated. Thanks.
Since you dont have a direct relationship, I think you can query via appPermissions Table instead of directly querying feeds table.
When you create a feeds table with appId as foreign key relationship, Hasura lets you track this relationship as shown below
This way you can make nested graphQL queries to appPerms table as shown below
query GetUserFeeds {
test_appPerms {
id
userId
feeds(limit: 10) {
app_id
id
feed_data
}
}
}
Another thing I'd like to suggest is that you could try is by using a session variable like x-hasura-app-id along side a x-hasura-role and build your permissions around that.
https://hasura.io/docs/latest/graphql/core/auth/authorization/roles-variables.html

Getting the right permitted fields of conditions

Currently, I'm building an app with with following similar logic:
...
const user = {
isAdmin: true,
company: '5faa6a847b42bf47b8f785a1',
projects: ['5faa6a847b42bf47b8f785a2']
}
function defineAbilityForUser(user) {
return defineAbility((can) => {
if (user.isAdmin) {
can('create', 'ProjectTime', {
company: user.company,
}
);
}
can(
'create',
'ProjectTime',
["company", "project", "user", "start", "end"],
{
company: user.company,
project: {
$in: user.projects
}
}
);
});
}
const userAbility = defineAbilityForUser(user); //
console.log( permittedFieldsOf(userAbility, 'create', 'ProjectTime') );
// console output: ['company', 'project', 'user', 'start', 'end']
Basically an admin should be allowed to create a project time with no field restrictions.
And a none admin user should only be allowed to set the specified fields for projects to which he belongs.
The problem is that I would expect to get [] as output because an admin should be allowed to set all fields for a project time.
The only solution I found was to set all fields on the admin user condition. But this requires a lot of migration work later when new fields are added to the project time model. (also wrapping the second condition in an else-block is not possible in my case)
Is there any other better way to do this? Or maybe, would it be better if the permittedFieldsOf-function would prioritize the condition with no field restrictions?
There is actually no way for casl to know what means all fields in context of your models. It knows almost nothing about their shapes and relies on conditions you provide it to check that objects later. So, it does not have full information.
What you need to do is to pass the 4th argument to override fieldsFrom callback. Check the api docs and reference implementation in #casl/mongoose
In casl v5, that parameter is mandatory. So, this confusion will disappear very soon

Getting a link to a specific Google Analytics view

I have a GA account, with defined properties and views. Now, I gave viewing rights for a few users to a specific view. How can I construct/get programmatically a direct URL that will bring those users right to that view/report?
Thanks a lot!
First lets take a look at an typical report url for a specific view:
https://analytics.google.com/analytics/web/#report/visitors-actives/a40777649w70913173p73156703/
Notice the pattern:
BASE_URL = 'https://analytics.google.com/analytics/web/#report/'
REPORT_TYPE = 'visitors-actives/'
ACOUNT_ID = '40777649'
WEBPROPERTY_ID = '70913173'
PROFILE_ID = '73156703' # Also called the view Id.
You can retrieve this information programmatically by calling the Account Summaries: list API method which returns a list of Account Summaries:
{
"id": string,
"kind": "analytics#accountSummary",
"name": string,
"starred": boolean,
"webProperties": [
{
"kind": "analytics#webPropertySummary",
"id": string,
"name": string,
"internalWebPropertyId": string,
"level": string,
"websiteUrl": string,
"starred": boolean,
"profiles": [
{
"kind": "analytics#profileSummary",
"id": string,
"name": string,
"type": string,
"starred": boolean
}
]
}
]
}
The ACCOUNT_ID is the top level acountSumaries.id.
The WEBPROPERTY_ID is the accountsumaries.webproperties[X].internalWebPropertyId.
The PROFILE_ID is the accountsumaries.webproperties[X].profiles[X].id
Now with this information you can recustruct the URL link to the report of interest for a particular view.
FULL_URL = BASE_URL + REPORT_TYPE + 'a' + ACCOUNT_ID + 'w' + WEBPROPERTY_ID + 'p' + PROFILE_ID + '/'
Further to Matt's brilliant answer, you can use the "Try this API" section in their documentation here to get this information without writing a line of code:
https://developers.google.com/analytics/devguides/config/mgmt/v3/mgmtReference/management/accountSummaries/list
There is also some code snippets to do this programmatically.
Don't forget you will need to be logged in with an account with GA access for this to work.
I have a few points to add to Matt and Adam's answers:
I chose to build a generic URL for the main view instead of a report. That way a user can navigate to a report of their choosing. The URL structure is https://analytics.google.com/analytics/web/#/report-home/a[account id]w[internal web property id]p[view id] Important: a user has to have at least READ_AND_ANALYZE permissions for the web property in order to access its default view.
internalWebPropertyId is a resource in the web properties collection and can be obtained through various GA Management API calls. For example, I extracted this value from the response object after creating a new web property using insert call.

Referring to a user by several possible identities

My application keeps multiple profile attributes for its users, such as:
An internal userId
Their phone number
Their email
etc. Each attribute is unique to a user; they can all be used as identity information.
I am designing an API with operations that refer to a specific user, eg charge.
I want to allow clients to identify users by any of the available profile attributes. In my specific domain, it is not possible to just enforce clients to use the internal userId, even if they can receive it in a separate call (eg getUserIdFromProfileAttribute).
Assuming the charge operation, it is a POST request with a JSON document inside the body. What would be the best way to identify the users? I am thinking one of the following:
Top-level key/value pairs for both the id and the id type:
{
"userId": <id>,
"userIdType": <idType>
}
Nested key/value pairs inside a user key:
{
"user": {
"id": <id>,
"type": <idType>
}
}
Single key/value pair, using a URI format with (possibly) custom protocols:
{
"user": <uri> # eg id:1234, tel:+19283912000, email:user#mail.com
}
Single key/value pair, using different keys for each id (one key per call):
{
"userId": <id> *OR*
"userMsisdn": <msisdn> *OR*
"userEmail": <email>
}
Same as above, but nested inside a user key:
{
"user": {
"id": <id> *OR*
"msisdn": <msisdn>
}
}
Any suggestions about best practices? Anyone can point me to some standard / widely used APIs with a similar need?
I should repeat that using just the internal userId in all calls is not possible, and using a separate call for each id (eg chargeById, chargeByEmail) is not practical as there are many such calls.

Resources