Vue authorization with Pinia - How do I get my functions to return values and not promises? - axios

I'm working on front end authorization with Vue and Pinia. My goal is to fetch a list of permissions belonging to the current user into my component file and then check if the User has the permission to see certain aspects of my component.
The problem arises when I use v-if to call my methods and check whether the user has the required permission. My function values are never returned and it always defaults to true.
This is what my auth.js file looks like:
export const useAuthStore = defineStore({
id: "authentication",
state: () => ({
...,
...,
userPermissions: fetchUserPerm(), //!!!
)},
const fetchUserPerm = async () => {
const res = await axios.get("api/users/me");
// My Laravel back end returns a User object with a Role property
which contains a list of all the permissions tied to this user:
return res.data.role.permissions;
};
And this is code contained within the component that I want to fetch the permissions to:
<script>
import router from "../../../router";
import axios from "axios";
import Modal from "../../UI/Modal.vue";
import { useAuthStore } from "../../../stores/auth";
export default {
setup() {
const authStore = useAuthStore();
return { authStore };
},
data() {
return {
...,
...,
userPermissions: this.authStore.userPermissions,
filteredPermissions: null,
};
},
I tried the following in "methods" and "computed" within my component:
methods:{
async checkUserPermFn(value) {
this.userPermissions = await this.userPermissions;
this.filteredPermissions = this.userPermissions.filter((permission) => {
return permission.name.includes(value);
});
console.log(this.filteredPermissions); //CORRECTLY LOGS AFTER FILTERING:
Proxy { <target>: [], <handler>: {…} }
<target>: Array []
return this.filteredPermissions.length > 0; //Gets ignored
},
}
computed:{
async checkPermission() {
this.userPermissions = await this.userPermissions;
console.log(this.userPermissions,"computed"); //CORRECTLY LOGS :
Proxy { <target>: (22) […], <handler>: {…} }
target>: Array(22)
this.filteredPermissions = this.userPermissions.filter(
(permission) => {
return permission.name.includes('permission.name');
}
);
console.log(this.filteredPermissions,"computed"); // CORRECTLY LOGS AFTER FILTERING:
Proxy { <target>: [], <handler>: {…} }
<target>: Array []
console.log(this.filteredPermissions.length)// CORRECTLY LOGS 0
return this.filteredPermissions.length > 0;
},
},
Now in my component I try check what gets returned from my methods and why does it still render them:
<div v-if="checkUserPermFn(value)"><p>Hi!</p></div>
This div is still shown even though the method should return false, and when I console log what my method returns I just get a promise:
Promise { <state>: "pending" }
​
<state>: "fulfilled"
Doing the same thing but with the computed method:
<div v-if="checkUserPermFn(value)"><p>Hi!</p></div>
The div is shown again even though the method should return false, and when I console log what my computed method returns I get a promise again but this time it contains the correct value too:
Promise { <state>: "fulfilled", <value>: false }
​
<state>: "fulfilled"
What am I missing here? I tried resolving promises but that only works within the method, when I try to return the resolved value I get a promise again. Thanks everyone!

Related

Suspense returns data too fast in #tanstack/react-query v4

I am upgrading a React app from react-query v3 to #tanstack/react-query v4.
Almost everything works, but I'm having a problem with Suspense.
I have a react component:
const WrapperPageEdit: React.FC<MyProps> = ({
pageUuid,
redirect,
}: MyProps) => {
const FormPage = React.lazy(() => import('./FormPage'));
const { data } = usePageView(pageUuid);
if (data?.[0]) {
const pageObjectToEdit= data[0];
const content = pageObjectToEdit.myStuff.content;
return (
<Suspense
fallback={<Trans id="loading.editor">Loading the editor...</Trans>}
>
<FormPage
id={uuid}
content={content}
redirect={redirect}
/>
</Suspense>
);
}
return <p>No data.</p>;
};
And here's my query:
export function usePageView(
uuid: string,
): UseQueryResult<DrupalPage[], Error> {
return useQuery<DrupalPage[], Error>(
queryKeyUsePageView(uuid),
async () => {
return fetchAnon(getPageByPageUuid(uuid));
},
{
cacheTime: YEAR_MILLISECONDS,
staleTime: YEAR_MILLISECONDS,
onSuccess: (data) => {
if (data?.[0]) {
data.map((element) => processResult(element));
}
},
},
);
}
This works in v3 but fails in v4 with the following error:
TypeError: Cannot read properties of undefined (reading 'content')
The reason the property is undefined is because that property is set by the processing in onSuccess (data.map).
The issue appears to be that in v4, the component WrapperPageEdit is refreshed before onSuccess in the usePageView query has finished processing, whereas in v3, the component WrapperPageEdit is not refreshed until the onSuccess data.map is complete.
How can I correctly fix this? I can write some additional code to try to check whether the onSuccess data.map is complete, but since react-query handled this automatically in v3, I'd like to rewrite my code in v4 so that it is the same.
The problem is likely that you are mutating the data in onSuccess. Directly modifying data in callbacks is not a good idea. Instead, do your transformation for example directly in the queryFn:
async () => {
const data = fetchAnon(getPageByPageUuid(uuid));
if (data?.[0]) {
data.map((element) => processResult(element));
}
return data
},
other good places to do data transformation is e.g. the select option, but it should always happen in an immutable way, because otherwise, you are overwriting the cached data inadvertently. React prefers updates to be immutable.

FindOne never gets executed Meteor js

I have been on this for a while. The problem with is is that this line of code never get executed let userSchool = SchoolDb.findOne({slug: Session.get('ReceivedSlug')}); When I logged on the console I see the the slug is dynamic as it is suppose to be pull the record from the db. What am I to do right?
The oncreated template
Template.view.onCreated(function () {
Session.set('ReceivedSlug', FlowRouter.getParam('myslug'));
this.autorun(function () {
Meteor.subscribe('SingleSchool', Session.get('ReceivedSlug'));
});
});
The helper function
singleSchool: function () {
if (Meteor.userId()) {
console.log('reactive this ---- ' +Session.get('ReceivedSlug'));
let userSchool = SchoolDb.findOne({slug: Session.get('ReceivedSlug')});
if (!userSchool) {
Bert.alert('School not present', 'danger', 'growl-top-right');
} else {
console.log('school name ----' +userSchool.slug);
return userSchool;
}
}
},
Can you please check whether the subscription has fetched data. Also console out inside publish that whether data gets published when slug changed.
Use below code to check if subscription is working
Meteor.subscribe('SingleSchool', Session.get('ReceivedSlug'), {
onReady: function(){
console.log(SchoolDb.find({}).fetch());
}
});

What's the correct Protractor's syntax for Page Objects?

I've come across different types of syntax for Protractor's Page Objects and I was wondering, what's their background and which way is suggested.
This is the official PageObject syntax from Protractor's tutorial. I like it the most, because it's clear and readable:
use strict;
var AngularHomepage = function() {
var nameInput = element(by.model('yourName'));
var greeting = element(by.binding('yourName'));
this.get = function() {
browser.get('http://www.angularjs.org');
};
this.setName = function(name) {
nameInput.sendKeys(name);
};
this.getGreeting = function() {
return greeting.getText();
};
};
module.exports = AngularHomepage;
However, I've also found this kind:
'use strict';
var AngularPage = function () {
browser.get('http://www.angularjs.org');
};
AngularPage.prototype = Object.create({}, {
todoText: { get: function () { return element(by.model('todoText')); }},
addButton: { get: function () { return element(by.css('[value="add"]')); }},
yourName: { get: function () { return element(by.model('yourName')); }},
greeting: { get: function () { return element(by.binding('yourName')).getText(); }},
todoList: { get: function () { return element.all(by.repeater('todo in todos')); }},
typeName: { value: function (keys) { return this.yourName.sendKeys(keys); }} ,
todoAt: { value: function (idx) { return this.todoList.get(idx).getText(); }},
addTodo: { value: function (todo) {
this.todoText.sendKeys(todo);
this.addButton.click();
}}
});
module.exports = AngularPage;
What are the pros/cons of those two approaches (apart from readability)? Is the second one up-to-date? I've seen that WebdriverIO uses that format.
I've also heard from one guy on Gitter that the first entry is inefficient. Can someone explain to me why?
Page Object Model framework becomes popular mainly because of:
Less code duplicate
Easy to maintain for long
High readability
So, generally we develop test framework(pom) for our convenience based on testing scope and needs by following suitable framework(pom) patterns. There are NO such rules which says that, strictly we should follow any framework.
NOTE: Framework is, to make our task easy, result oriented and effective
In your case, 1st one looks good and easy. And it does not leads to confusion or conflict while in maintenance phase of it.
Example: 1st case-> element locator's declaration happens at top of each page. It would be easy to change in case any element locator changed in future.
Whereas in 2nd case, locators declared in block level(scatter across the page). It would be a time taking process to identify and change the locators if required in future.
So, Choose which one you feel comfortable based on above points.
I prefer to use ES6 class syntax (http://es6-features.org/#ClassDefinition). Here, i prepared some simple example how i work with page objects using ES6 classes and some helpful tricks.
var Page = require('../Page')
var Fragment = require('../Fragment')
class LoginPage extends Page {
constructor() {
super('/login');
this.emailField = $('input.email');
this.passwordField = $('input.password');
this.submitButton = $('button.login');
this.restorePasswordButton = $('button.restore');
}
login(username, password) {
this.email.sendKeys(username);
this.passwordField.sendKeys(password);
this.submit.click();
}
restorePassword(email) {
this.restorePasswordButton.click();
new RestorePasswordModalWindow().submitEmail(email);
}
}
class RestorePasswordModalWindow extends Fragment {
constructor() {
//Passing element that will be used as this.fragment;
super($('div.modal'));
}
submitEmail(email) {
//This how you can use methods from super class, just example - it is not perfect.
this.waitUntilAppear(2000, 'Popup should appear before manipulating');
//I love to use fragments, because they provides small and reusable parts of page.
this.fragment.$('input.email').sendKeys(email);
this.fragment.$('button.submit')click();
this.waitUntilDisappear(2000, 'Popup should disappear before manipulating');
}
}
module.exports = LoginPage;
// Page.js
class Page {
constructor(url){
//this will be part of page to add to base URL.
this.url = url;
}
open() {
//getting baseURL from params object in config.
browser.get(browser.params.baseURL + this.url);
return this; // this will allow chaining methods.
}
}
module.exports = Page;
// Fragment.js
class Fragment {
constructor(fragment) {
this.fragment = fragment;
}
//Example of some general methods for all fragments. Notice that default method parameters will work only in node.js 6.x
waitUntilAppear(timeout=5000, message) {
browser.wait(this.EC.visibilityOf(this.fragment), timeout, message);
}
waitUntilDisappear(timeout=5000, message) {
browser.wait(this.EC.invisibilityOf(this.fragment), timeout, message);
}
}
module.exports = Fragment;
// Then in your test:
let loginPage = new LoginPage().open(); //chaining in action - getting LoginPage instance in return.
loginPage.restorePassword('batman#gmail.com'); // all logic is hidden in Fragment object
loginPage.login('superman#gmail.com')

AngularJS ng-repeat with data from service

Originally in my app, I created controllers with very basic $http calls to get a resource by getting the ID of an object from the url ($routeParams). Ng-repeat display the results correctly.
However, I noticed refreshing in a later view (different controller) wiped out the data and broke the page. So, I created a function on the service to be used in multiple controllers, to check whether the data has is available and to react as follows:
1) If the resource is defined, return it (no API call)
2) If the resource is not defined, get the id from the url and get it from the API
3) If the resource is not defined & you can't get the ID, just return false.
However, this broke the code: the template rendered before the service returned the data, and ng-repeat did not update. The code looks like this:
angular.module('myApp', ['ngCookies'])
.config(...)
.service('myService', ['$cookies', '$http', function($cookies, $http) {
myData = {};
return {
getData:function(dataID) {
if(myData.name) {return myData);
else if (dataID && dataID !== '') {
$http.get('/api/data/' + dataID)
.success(function(data) {
myData = data.object;
$cookies.dataID = data.object.id;
return myData;
}
}
else { return false; }
}
}
}]);
function myCtrl($scope, $http, $routeParams, myService) {
$scope.data = myService.getData($routeParams.dataID);
...
}
And here's the template. It's in jade, which means rather than angle brackets, you just list the element with parameters in parenthesis right after, and content after the parenthesis.
h2 My heading
ul
li(ng-repeat='option in data')
a(href="#", ng-click='someFuncInCtrl(option.name)') {{ option.name }}
When the controller did the $http.get itself, the ng-repeat worked fine because the $scope was updated in the ".success" callback. Now that there's a service that returns the data after a slight delay, "$scope.data" is just undefined, the ng-repeat list is empty.
I used a console.log to check myData right before return "return myData", and the myData is working, it just isn't returned in time, and for whatever reason the list is not updating whenever $scope does get the data.
I looked a using $routeProvider's resolve... but that makes getting the ID from the url challenging, as the resolve object doesn't seem to have access to $routeParams. I know that $scope.$apply is supposed to help update the scope when it's altered by outside functions... but I have no clue where to put it. The most similar problem on SO didn't use a service.
I tried:
$scope.$apply($scope.data = myService.getData($routeParams.dataID));
And
$scope.$apply(function() {
$scope.data = myService($routeParams.dataID);
});
Both times I only got Error: $digest already in progress.
The problem is on the way you interact with the service. Since your getData function can return both synchronous and/or asynchronous information, you can't just use normal return(s).
$http.get('/api/data/' + dataID)
.success(function(data) {
myData = data.object;
$cookies.dataID = data.object.id;
return myData;
});
The return on the above snippet will not return anything from getData because it will be executed on the context of the $http.get success callback (and not on the getData call stack).
The best approach for handling sync and async service requests is to use promises.
Your getData function should look something like this:
getData:function(dataID) {
var deferred = $q.defer();
if(myData.name) {
deferred.resolve(myData);
} else if (dataID && dataID !== '') {
$http.get('/api/data/' + dataID)
.success(function(data) {
myData = data.object;
$cookies.dataID = data.object.id;
deferred.resolve(myData);
// update angular's scopes
$rootScope.$$phase || $rootScope.$apply();
});
} else {
deferred.reject();
}
return deferred.promise;
}
Note: You need to inject the $rootScope on your service.
And on your controller:
function myCtrl($scope, $http, $routeParams, myService) {
myService.getData($routeParams.dataID).then(function(data) {
// request was successful
$scope.data = data;
}, function() {
// request failed (same as your 'return false')
$scope.data = undefined;
});
}

Backbone.js Model different url for create and update?

lets say I have a Backbone Model and I create an instance of a model like this:
var User = Backbone.Model.extend({ ... });
var John = new User({ name : 'John', age : 33 });
I wonder if it is possible when I use John.save() to target /user/create when I use John.save() on second time (update/PUT) to target /user/update when I use John.fetch() to target /user/get and when I use John.remove() to target /user/remove
I know that I could define John.url each time before I trigger any method but I'm wondering if it could be happen automatically some how without overriding any Backbone method.
I know that I could use one url like /user/handle and handle the request based on request method (GET/POST/PUT/DELETE) but I'm just wondering if there is a way to have different url per action in Backbone.
Thanks!
Methods .fetch(), .save() and .destroy() on Backbone.Model are checking if the model has .sync() defined and if yes it will get called otherwise Backbone.sync() will get called (see the last lines of the linked source code).
So one of the solutions is to implement .sync() method.
Example:
var User = Backbone.Model.extend({
// ...
methodToURL: {
'read': '/user/get',
'create': '/user/create',
'update': '/user/update',
'delete': '/user/remove'
},
sync: function(method, model, options) {
options = options || {};
options.url = model.methodToURL[method.toLowerCase()];
return Backbone.sync.apply(this, arguments);
}
}
To abstract dzejkej's solution one level further, you might wrap the Backbone.sync function to query the model for method-specific URLs.
function setDefaultUrlOptionByMethod(syncFunc)
return function sync (method, model, options) {
options = options || {};
if (!options.url)
options.url = _.result(model, method + 'Url'); // Let Backbone.sync handle model.url fallback value
return syncFunc.call(this, method, model, options);
}
}
Then you could define the model with:
var User = Backbone.Model.extend({
sync: setDefaultUrlOptionByMethod(Backbone.sync),
readUrl: '/user/get',
createUrl: '/user/create',
updateUrl: '/user/update',
deleteUrl: '/user/delete'
});
Are you dealing with a REST implementation that isn't to spec or needs some kind of workaround?
Instead, consider using the emulateHTTP option found here:
http://documentcloud.github.com/backbone/#Sync
Otherwise, you'll probably just need to override the default Backbone.sync method and you'll be good to go if you want to get real crazy with that... but I don't suggest that. It'd be best to just use a true RESTful interface.
No you can't do this by default with backbone. What you could to is to add to the model that will change the model url on every event the model trigger. But then you have always the problem that bckbone will use POST add the first time the model was saved and PUT for every call afterward. So you need to override the save() method or Backbone.sync as well.
After all it seems not a good idea to do this cause it break the REST pattern Backbone is build on.
I got inspired by this solution, where you just create your own ajax call for the methods that are not for fetching the model. Here is a trimmed down version of it:
var Backbone = require("backbone");
var $ = require("jquery");
var _ = require("underscore");
function _request(url, method, data, callback) {
$.ajax({
url: url,
contentType: "application/json",
dataType: "json",
type: method,
data: JSON.stringify( data ),
success: function (response) {
if ( !response.error ) {
if ( callback && _.isFunction(callback.success) ) {
callback.success(response);
}
} else {
if ( callback && _.isFunction(callback.error) ) {
callback.error(response);
}
}
},
error: function(mod, response){
if ( callback && _.isFunction(callback.error) ) {
callback.error(response);
}
}
});
}
var User = Backbone.Model.extend({
initialize: function() {
_.bindAll(this, "login", "logout", "signup");
},
login: function (data, callback) {
_request("api/auth/login", "POST", data, callback);
},
logout: function (callback) {
if (this.isLoggedIn()) {
_request("api/auth/logout", "GET", null, callback);
}
},
signup: function (data, callback) {
_request(url, "POST", data, callback);
},
url: "api/auth/user"
});
module.exports = User;
And then you can use it like this:
var user = new User();
// user signup
user.signup(data, {
success: function (response) {
// signup success
}
});
// user login
user.login(data, {
success: function (response) {
// login success
}
});
// user logout
user.login({
success: function (response) {
// logout success
}
});
// fetch user details
user.fetch({
success: function () {
// logged in, go to home
window.location.hash = "";
},
error: function () {
// logged out, go to signin
window.location.hash = "signin";
}
});