I'm building an API using Sequelize as the ORM. I wanted to incorporate some "listeners" in the core logic to check if certain values have changed and fire off some logic based on that.
One of my main concerns atm is a pivot table between customers and users. (BelongsToMany). The relation has some extra fields in the pivot like "is_admin" or "notify_user". This describes that a user is an admin for a certain customer, and if the system should notify this user on a customers action.
My API supports PATCHING of the pivot tables' data. Now what I want to do for example is if the is_admin value changes, fire off some other logic to update foo elsewhere.
All hooks work on the base object models (User and Customer). But none of my hooks are fired on the pivot object model.
Not sure if this is supported in Sequelize.
Thanks in advance
My relations are setup like this:
private static setupCustomerRelations(sq: Sequelize.Sequelize) {
const user = sq.models['User'] || new UserRepo().getNewInstance().getModel();
const customer = sq.models['Customer'] || new CustomerRepo().getNewInstance().getModel();
const customerUsers = sq.models['customer_users'] || new CustomerUsersRepo().getNewInstance().getModel()
const machine = sq.models['Machine'] || new MachineRepo().getNewInstance().getModel()
customer.belongsToMany(user, {'through': customerUsers});
user.belongsToMany(customer, {'through': customerUsers});
// Machines
machine.belongsTo(customer, {'foreignKey':'customer_id'});
customer.hasMany(machine, {'foreignKey':'customer_id'});
}
And this customerUsers is a proper model
I've had luck with Bulk hooks on join tables. Try using beforeBulkCreate and beforeBulkUpdate on the join table's model.
CustomerUser.addHook('beforeBulkCreate', 'admin-only', async function(customerUsers) {
for (const customerUser of customerUsers) {
const user = await User.findById(customer);
if (user.isAdmin) { /* ... */ }
}
}
Obviously this is going to produce tons of queries and you should think carefully about that and how to avoid that.
Related
I am trying to understand more in depth the difference between filter and item access control.
Basically I understand that Item access control is, sort of, higher order check and will run before the GraphQL filter.
My question is, if I am doing a filter on a specific field while updating, for instance a groupID or something like this, do I need to do the same check in Item Access Control?
This will cause an extra database query that will be part of the filter.
Any thoughts on that?
The TL;DR answer...
if I am doing a filter on a specific field [..] do I need to do the same check in Item Access Control?
No, you only need to apply the restriction in one place or the other.
Generally speaking, if you can describe the restriction using filter access control (ie. as a graphQL-style filter, with the args provided) then that's the best place to do it. But, if your access control needs to behave differently based on values in the current item or the specific changes being made, item access control may be required.
Background
Access control in Keystone can be a little hard to get your head around but it's actually very powerful and the design has good reasons behind it. Let me attempt to clarify:
Filter access control is applied by adding conditions to the queries run against the database.
Imagine a content system with lists for users and posts. Users can author a post but some posts are also editable by everyone. The Post list config might have something like this:
// ..
access: {
filter: {
update: () => ({ isEditable: { equals: true } }),
}
},
// ..
What that's effectively doing is adding a condition to all update queries run for this list. So if you update a post like this:
mutation {
updatePost(where: { id: "123"}, data: { title: "Best Pizza" }) {
id name
}
}
The SQL that runs might look like this:
update "Post"
set title = 'Best Pizza'
where id = 234 and "isEditable" = true;
Note the isEditable condition that's automatically added by the update filter. This is pretty powerful in some ways but also has its limits – filter access control functions can only return GraphQL-style filters which prevents them from operating on things like virtual fields, which can't be filtered on (as they don't exist in the database). They also can't apply different filters depending on the item's current values or the specific updates being performed.
Filter access control functions can access the current session, so can do things like this:
filter: {
// If the current user is an admin don't apply the usual filter for editability
update: (session) => {
return session.isAdmin ? {} : { isEditable: { equals: true } };
},
}
But you couldn't do something like this, referencing the current item data:
filter: {
// ⚠️ this is broken; filter access control functions don't receive the current item ⚠️
// The current user can update any post they authored, regardless of the isEditable flag
update: (session, item) => {
return item.author === session.itemId ? {} : { isEditable: { equals: true } };
},
}
The benefit of filter access control is it doesn't force Keystone to read an item before an operation occurs; the filter is effectively added to the operation itself. This can makes them more efficient for the DB but does limit them somewhat. Note that things like hooks may also cause an item to be read before an operation is performed so this performance difference isn't always evident.
Item access control is applied in the application layer, by evaluating the JS function supplied against the existing item and/or the new data supplied.
This makes them a lot more powerful in some respects. You can, for example, implement the previous use case, where authors are allowed to update their own posts, like this:
item: {
// The current user can update any post they authored, regardless of the isEditable flag
update: (session, item) => {
return item.author === session.itemId || item.isEditable;
},
}
Or add further restrictions based on the specific updates being made, by referencing the inputData argument.
So item access control is arguably more powerful but they can have significant performance implications – not so much for mutations which are likely to be performed in small quantities, but definitely for read operations. In fact, Keystone won't let you define item access control for read operations. If you stop and think about this, you might see why – doing so would require reading all items in the list out of the DB and running the access control function against each one, every time a list was read. As such, the items accessible can only be restricted using filter access control.
Tip: If you think you need item access control for reads, consider putting the relevant business logic in a resolveInput hook that flattens stores the relevant values as fields, then referencing those fields using filter access control.
Hope that helps
I'm using extbase in my extension and so I have *Repository classes where I can do simple queries just like:
public function getRecordsByCondition($config = [],$recordPages = null) {
$recordQuery = $this->createQuery();
$constraints = [];
if ($config['field1']) {
$constraints[] = $recordQuery->equals('field1',$config['field1']));
}
if ($config['field2']) {
$constraints[] = $recordQuery->equals('field2',$config['field2']));
}
if ($config['field3']) {
$constraints[] = $recordQuery->equals('field3',$config['field3']));
}
if (count($constraints)) {
if ($recordPages) {
$constraints[] = $recordQuery->in('pid',$recordPages);
$recordQuery->getQuerySettings()->setRespectStoragePage(false);
}
$recordQuery->matching($recordQuery->logicalAnd($constraints));
} else {
return false;
}
return $recordQuery->execute();
}
this will respect enableFields and other usual conditions.
on the other hand there is the option to do it in this way:
public function getrecords2($config,$recordPages) {
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
->getQueryBuilderForTable('tx_myext_domain_model_records');
$rawquery = $queryBuilder
->select('*')
->from('tx_myext_domain_model_records')
->where(
$queryBuilder->expr()->eq('field1',$config['field1']),
$queryBuilder->expr()->eq('field2',$config['field2']),
$queryBuilder->expr()->eq('field3',$config['field3']),
$queryBuilder->expr()->in('pid', $recordPages),
$queryBuilder->expr()->eq('deleted',0),
$queryBuilder->expr()->eq('hidden',0)
// starttime, endtime, language, workspace, ....
);
return $rawquery->execute()->fetchAll();
}
where I need to care about enablefields by myself but have more options to specify the query.
On the first view you can see that there are other methods (eq vs. equals) and these kind of doing queries have no relation. But both work on the same table.
Now I'm at a point where I need to change all my work from first to second variant as I need a query with a join to another table which can't be done with first variant (as far as I know).
Have I missed something or does the first variant needs some enhancements?
Well, I am not sure exactly the difference but let me try to express things in brief as per my knowledge :D
The main difference between both queries is Individual database queries (Typically I call it Extbase query, I'm not sure I am right or not!) and another is Doctrine DBAL Queries
1. Individual database queries
Here, as per the modern approach extension use Domain modeling. So, TYPO3 already enables a secure connection for model (Typically database table) and you can use relational table connection with Extbase function (Select, operational, join etc..) provided by TYPO3 core.
For more: https://docs.typo3.org/m/typo3/book-extbasefluid/master/en-us/6-Persistence/3-implement-individual-database-queries.html
2. Doctrine DBAL
Here, you enable connection manually for the database table using ConnectionPool class. Also, you have more feasibility to establish a relation (or Join you can say!) according to your need.
For more: https://docs.typo3.org/m/typo3/reference-coreapi/master/en-us/ApiOverview/Database/Index.html
However, you can use restriction for taking care if hidden delete etc.
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
->getQueryBuilderForTable('tx_myext_domain_model_records');
$queryBuilder->getRestrictions()->removeAll()->add(GeneralUtility::makeInstance(DeletedRestriction::class));
$queryBuilder->getRestrictions()->add(GeneralUtility::makeInstance(HiddenRestriction::class));
$rawquery = $queryBuilder
->select('*')
->from('tx_myext_domain_model_records')
->where(
$queryBuilder->expr()->eq('field1',$config['field1']),
$queryBuilder->expr()->eq('field2',$config['field2']),
$queryBuilder->expr()->eq('field3',$config['field3']),
$queryBuilder->expr()->in('pid', $recordPages)
// starttime, endtime, language, workspace, ....
);
See: https://docs.typo3.org/m/typo3/reference-coreapi/master/en-us/ApiOverview/Database/RestrictionBuilder/Index.html
I know this is not a sufficient and 100% correct answer. Everyone can welcome to correct me :)
where I need to care about enablefields by myself
That's not true. By default there are Restrictions active and you can enable or disable every Restriction with a short command.
I use both approaches, but I use the first one only on Extbase extensions, the second one on every other extension. (Yes, there exist extensions without Extbase)
I am using TYPO3 8. In my extension I have a database table "company" in which I store for each company the total number of places (number_places) and the number of occupied places (occupied_places).
Now I want to limit the search to companies which have available places left.
In MySQL it would be like this:
SELECT * FROM company WHERE number_places > occupied_places;
How can I create this query in the extbase repository?
I tried to introduce the virtual property placesLeft in my model but it did not work.
I don't want to use a raw SQL statement as mentioned below, because I already have implemented a filter which uses plenty of different constraints.
Extbase query to compare two fields in same table
You can do it like this in your repository class, please note the comments inside the code:
class CompanyRepository extends \TYPO3\CMS\Extbase\Persistence\Repository
{
public function findWithAvailablePlaces(bool $returnRawQueryResult = false)
{
// Create a QueryBuilder instance
$queryBuilder = $this->objectManager->get(\TYPO3\CMS\Core\Database\ConnectionPool::class)
->getConnectionForTable('company')->createQueryBuilder();
// Create the query
$queryBuilder
->select('*')
->from('company')
->where(
// Note: this string concatenation is needed, because TYPO3's
// QueryBuilder always escapes the value in the ExpressionBuilder's
// methods (eq(), lt(), gt(), ...) and thus render it impossible to
// compare against an identifier.
$queryBuilder->quoteIdentifier('number_places')
. \TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder::GT
. $queryBuilder->quoteIdentifier('occupied_places')
);
// Execute the query
$result = $queryBuilder->execute()->fetchAll();
// Note: this switch is not needed in fact. I just put it here, if you
// like to get the Company model objects instead of an array.
if ($returnRawQueryResult) {
$dataMapper = $this->objectManager->get(\TYPO3\CMS\Extbase\Persistence\Generic\Mapper\DataMapper::class);
return $dataMapper->map($this->objectType, $result);
}
return $result;
}
}
Notes:
If you have lots of records to deal with, I would - for performance reasons - not use the data mapping feature and work with arrays.
If you want to use the fluid pagination widget, be sure you don't and build your own pagination. Because of the way this works (extbase-internally), you'd get a huge system load overhead when the table grows. Better add the support for limited db queries to the repository method, for example:
class CompanyRepository extends \TYPO3\CMS\Extbase\Persistence\Repository
{
public function findWithAvailablePlaces(
int $limit = 10,
int $offset = 0,
bool $returnRawQueryResult = false
) {
// ...
$queryBuilder
->setMaxResults($limit)
->setFirstResult($offset);
$result = $queryBuilder->execute()->fetchAll();
// ...
}
}
I think you cant do this using the default Extbase Query methods like equals() and so on. You may use the function $query->statement() for your specific queries like this.
You also can use the QueryBuilder since TYPO3 8 which has functions to compare fields to each other:
https://docs.typo3.org/typo3cms/CoreApiReference/latest/ApiOverview/Database/QueryBuilder/Index.html#quoteidentifier-and-quoteidentifiers
It's fine to use this QueryBuilder inside Extbase repositories. After this you can use the DataMapper to map the query results to Extbase models.
In case of using "statement()" be aware of escaping every value which may cause any kind of SQL injections.
Based on the current architecture of TYPO3, the data structure is such that comparing of two tables or, mixing results from two tables ought to be done from within the controller, by injecting the two repositories. Optionally, you can construct a Domain Service that can work on the data from the two repositories from within the action itself, in the case of a routine. The service will also have to be injected.
Note:
If you have a foreign relation defined in your table configuration, the results of that foreign relation will show in your defined table repository. So, there's that too.
I have written one extension for making service order.
The issue I am facing here is,
There are FE users belong to three FE user groups namely "client", "Admin" and "Employee".
Here the client can make order and he should be able to see only his orders.
And the admin can see all orders done by different clients.
And the employee should be able to see only some clients orders.
Currently I made a order table with N:1 relation with FE user table. So every order should relate with any one client.
So in controller, I am checking the login user and using custom query in repository, I am accessing order related to loggedin client (FE user)
In file OrdersController.php
public function listAction() {
$orders = $this->ordersRepository->orderForLoginUsr();
$this->view->assign('orders', $orders);
}
In file OrdersRepository.php
public function orderForLoginUsr(){
$loggedInUserId = $GLOBALS ['TSFE']->fe_user->user['uid'];
$query = $this->createQuery();
$query->matching(
$query->equals('user', $loggedInUserId)
);
$query->setOrderings(array('crdate' => \TYPO3\CMS\Extbase\Persistence\QueryInterface::ORDER_DESCENDING));
return $query->execute();
}
But here my question is how to make admin user able to see all the orders done by all clients?
I have to write different template and action that calling function findAll() ?
$orders = $this->ordersRepository->findAll();
And how to set for usergroup Employee ?
Thanks in Advance
I think that the easiest way is to actually implement 3 actions with 3 different plugins, something like: listClientAction, listAdminAction and listEmployeeAction
In each of those action, you implement a method in your repository that fetch the right list of order with the good ordering:
orderForLoginClient(), orderForLoginEmployee(), orderForLoginAdmin()
What does the trick actually is that there will be 3 plugins on your page, one for each action. In each instance of your plugin, you set the access for the right be_group.
Don't forget to add the actions and plugin in the localconf and ext_table files.
I hope it will help!
Olivier
If your view is almost the same for client, admin, employee you should simply add a method like getOrderWithPermissionsForUser($currentUser);
In the method itself you should check for the usergroup and call different queries on your Repo.
If your view is different from usergroup to usergroup, you should use different templates with partials for the same parts.
If the data of the views is the same, just change the template for each usergroup in the action. If not use different actions.
Here is a helper method for easily changing your templatefile.
/**
* This method can change the used template file in an action method.
*
* #param string $templateName Something like "List" or "Foldername/Actionname".
* #param string $templateExtension Default is "html", but for other output types this may be changed as well.
* #param string $controllerName Optionally uses another subfolder of the Templates/ directory
* By default, the current controller name is used. Example value: "JobOffer"
* #param \TYPO3\CMS\Fluid\View\AbstractTemplateView $viewObject The view to set this template to. Default is $this->view
*/
protected function changeTemplateFile($templateName, $templateExtension = 'html', $controllerName = null, AbstractTemplateView $viewObject = null)
{
if (is_null($viewObject)) {
$viewObject = $this->view;
}
if (is_null($controllerName)) {
$controllerName = $this->getControllerContext()->getRequest()->getControllerName();
}
$templatePathAndFilename = $this->getTemplateRootpathForView($controllerName . '/' . $templateName . '.' . $templateExtension);
$viewObject->setTemplatePathAndFilename($templatePathAndFilename);
}
I am new to Laravel and I am trying to add some more information to the user variable I am getting back from Auth::user()
To be more detailed, I have a Many-to-Many relationship to a "Privileges" table. As the name suggests, that table holds specific privileges a user can have. In the pivot table, I just hold the the user_id and privilege_id. I have the necessary models set up and everything works fine if I do this in my before filter:
$user = Auth::user();
$user->priviledges()->get();
Now, I would really like to avoid querying every single time I want to find the privileges and would like to have Laravel's Auth class include the privilege information, so that when I do
$user = Auth::user();
I can do a
$user->privileges;
to get an array of all privileges the user has.
Any suggestions for the best approach?
The link to the answer above is not working. However, I found another solution here which worked for me as follows:
First I created a model called EmailPref.php in my own case;
app/model/EmailPref.php
class EmailPref extends Eloquent {
protected $table = 'email_pref';
public function user()
{
return $this->belongsTo('User');
}
}
and then I created a relationship (in my own case) in the User model like so;
app/model/User.php
public function emailPref()
{
return $this->hasOne('EmailPref');
}
I subsequently referenced it anywhere required within my application like so:
Auth::user()->emailPref;
Hence, I was able to add more information to the Auth user.
I'm no Laravel pro, but i think this will solve your problem: http://forums.laravel.io/viewtopic.php?id=1652