I have a model "User" that has a Many-to-One relationship with a "Subject".
User.js
attributes: {
subject: { model: 'subject' },
}
Subject.js
attributes: {
name: { type: 'string', unique: true, required: true },
}
When I call the blueprint create function for a User "/user" and pass in the data:
{
"name":"Test",
"subject":{"name":"Do Not Allow"}
}
It creates the user and also creates the Subject. However I do not want to allow the subject to be created, I only want to be able to attach an existing one. For example I would like it to reject the subject being created using the above data but allow the subject to be attached by using the below data.
{
"name":"Test",
"subject":1
}
I tried adding a policy (shown below) but this only stops the subject from being created using the URL "/subject" and not the nested create shown above.
'SubjectController':{
'create':false
}
Edit
To help understand what is going on here this is the lifecycle process it is going through:
Before Validation of Subject
After Validation of Subject
Before Creating Subject
After Creating Subject
Before Validation of User
After Validation of User
Before Creating User
Before Validation of User
After Validation of User
After Creating User
As you can see it is validating and creating the subject before it even gets to validating or creating the user.
You want to avoid the creation of an associated object when calling the blueprint creation route.
Create a policy (I've named it checkSubjectAndHydrate) and add it into the policies.js file:
// checkSubjectAndHydrate.js
module.exports = function (req, res, next) {
// We can create a user without a subject
if (_.isUndefined(req.body.subject)) {
return next();
}
// Check that the subject exists
Subject
.findOne(req.body.subject)
.exec(function (err, subject) {
if (err) return next(err);
// The subject does not exist, send an error message
if (!subject) return res.forbidden('You are not allowed to do that');
// The subject does exist, replace the body param with its id
req.body.subject = subject.id;
return next();
});
};
// policies.js
module.exports.policies = {
UserController: {
create: 'checkSubjectAndHydrate',
update: 'checkSubjectAndHydrate',
}
};
You should be passing the subject id (e.g. 1) instead of an object (e.g. { name: 'Hello, World!' }) containing the name of the subject as it's not necessarily unique.
If it is unique, you should replace the object by its id inside a beforeValidate for example.
// User.js
module.exports = {
...
beforeValidate: function (users, callback) {
// users = [{
// "name":"Test",
// "subject":{"name":"Do Not Allow"}
// }]
async.each(users, function replaceSubject(user, next) {
var where = {};
if (_.isObject(user.subject) && _.isString(user.subject.name)) {
where.name = user.subject.name;
} else if(_.isInteger(user.subject)) {
where.id = user.subject;
} else {
return next();
}
// Check the existence of the subject
Subject
.findOne(where)
.exec(function (err, subject) {
if (err) return next(err);
// Create a user without a subject if it does not exist
user.subject = subject? subject.id : null;
next();
});
}, callback);
// users = [{
// "name":"Test",
// "subject":1
// }]
}
};
You can create custom type for subject, and add your logic inside model. I'm not 100% sure I understood the attach sometimes part but maybe this could help:
models/User.js
module.exports = {
schema: true,
attributes: {
name: {
type: 'string'
},
subject: {
type: 'json',
myValidation: true
}
},
types: {
myValidation: function(value) {
// add here any kind of logic...
// for example... reject if someone passed name key
return !value.name;
}
}
};
You can find more info here http://sailsjs.org/documentation/concepts/models-and-orm/validations at the bottom of the page.
If I totally missed the point... The second option would be to add beforeCreate and beforeUpdate lifecycle callback to your model like this:
models/User.js
module.exports = {
schema: true,
attributes: {
name: {
type: 'string'
},
subject: {
type: 'json'
}
},
beforeCreate: function (values, cb) {
// for example... reject creating of subject if anything else then value of 1
if (values.subject && values.subject !== 1) return cb('make error obj...');
cb();
},
beforeUpdate: function (values, cb) {
// here you can add any kind of logic to check existing user or current update values that are going to be updated
// and allow it or not
return cb();
}
};
By using this you can use one logic for creating and another one for updating... etc...
You can find more info here: http://sailsjs.org/documentation/concepts/models-and-orm/lifecycle-callbacks
EDIT
Realized you have trouble with relation, and in above examples I thought you are handling type json...
module.exports = {
schema: true,
attributes: {
name: {
type: 'string'
},
subject: {
model: 'subject'
}
},
beforeValidate: function (values, cb) {
// subject is not sent at all, so we just go to next lifecycle
if (!values.subject) return cb();
// before we update or create... we will check if subject by id exists...
Subject.findOne(values.subject).exec(function (err, subject) {
// subject is not existing, return an error
if (err || !subject) return cb(err || 'no subject');
//
// you can also remove subject key instead of sending error like this:
// delete values.subject;
//
// subject is existing... continue with update
cb();
});
}
};
Related
I'm using cognito authentication,
I create a middlware
const { email } = payload;
req.headers['user-email'] = email as string;
I want to write this kind of function
public async httpCheck(query: any, args: any, context: any,
resolveInfo: any) {
console.log('authhealth');
console.log("context "+ context.userEmail);
console.log("query : "+ query.userEmail);
(context.userEmail === query.userEmail ) ? console.log("authorized successfully") : console.log("authorization failed");
return 'OK';
}
This is my file structure, I want to write wrap resolver
From your example, it looks like you are wanting to reject the whole request if the email from the request header does not match an email being provided as an argument to a field in the GraphQL query.
So given the following query:
query MyQuery($userEmail:String!) {
userByEmail(email: $userEmail) {
id
email
familyName
givenName
}
}
If you want to check that the header email equals the email argument of userByEmail BEFORE Postgraphile executes the operation, you need to use a Postgraphile Server Plugin which adds a dynamic validation rule that implements the check:
import type { PostGraphilePlugin } from "postgraphile";
import type { ValidationRule } from "graphql";
import { GraphQLError } from "graphql";
import type { IncomingMessage } from "http";
import type { Plugin } from "graphile-build";
// Defines a graphile plugin that uses a field argument build hook to add
// metadata as an extension to the "email" argument of the "userByEmail" field
const AddEmailMatchPlugin: Plugin = (builder) => {
builder.hook(
"GraphQLObjectType:fields:field:args",
(args, build, context) => {
// access whatever data you need from the field context. The scope contains
// basically any information you might desire including the database metadata
// e.g table name, primary key.
const {
scope: { fieldName, isRootQuery },
} = context;
if (!isRootQuery && fieldName !== "userByEmail") {
return args;
}
if (args.email) {
return {
...args,
email: {
...args.email,
// add an extensions object to the email argument
// this will be accessible from the finalized GraphQLSchema object
extensions: {
// include any existing extension data
...args.email.extensions,
// this can be whatetever you want, but it's best to create
// an object using a consistent key for any
// GraphQL fields/types/args that you modify
myApp: {
matchToUserEmail: true,
},
},
},
};
}
return args;
}
);
};
// define the server plugin
const matchRequestorEmailWithEmailArgPlugin: PostGraphilePlugin = {
// this hook enables the addition of dynamic validation rules
// where we can access the underlying http request
"postgraphile:validationRules": (
rules,
context: { req: IncomingMessage; variables?: Record<string, unknown> }
) => {
const {
variables,
// get your custom user context/jwt/headers from the request object
// this example assumes you've done this in some upstream middleware
req: { reqUser },
} = context;
if (!reqUser) {
throw Error("No user found!");
}
const { email, role } = reqUser;
const vr: ValidationRule = (validationContext) => {
return {
Argument: {
// this fires when an argument node has been found in query AST
enter(node, key) {
if (typeof key === "number") {
// get the schema definition of the argument
const argDef = validationContext.getFieldDef()?.args[key];
if (argDef?.extensions?.myApp?.matchToUserEmail) {
// restrict check to a custom role
if (role === "standard") {
const nodeValueKind = node.value.kind;
let emailsMatch = false;
// basic case
if (nodeValueKind === "StringValue") {
if (node.value.value === email) {
emailsMatch = true;
}
}
// must account for the value being provided by a variable
else if (nodeValueKind === "Variable") {
const varName = node.value.name.value;
if (variables && variables[varName] === email) {
emailsMatch = true;
}
}
if (!emailsMatch) {
validationContext.reportError(
new GraphQLError(
`Field "${
validationContext.getFieldDef()?.name
}" argument "${
argDef.name
}" must match your user email.`,
node
)
);
}
}
}
}
},
},
};
};
return [...rules, vr];
},
// This hook appends the AddEmailMatchPlugin graphile plugin that
// this server plugin depends on for its custom extension.
"postgraphile:options": (options) => {
return {
...options,
appendPlugins: [...(options.appendPlugins || []), AddEmailMatchPlugin],
};
},
};
export default matchRequestorEmailWithEmailArgPlugin;
Then you need to register the server plugin in the Postgraphile middleware options:
const pluginHook = makePluginHook([MatchRequestorEmailWithEmailArgPlugin]);
const postGraphileMiddleware = postgraphile(databaseUrl, "my_schema", {
pluginHook,
// ...
});
If you just want to reject the userByEmail field in the query and don't care about rejecting before any resolution of any other parts of the request occur, you can use the makeWrapResolversPlugin to wrap the resolver and do the check there.
Node js with mongoose.
model has fields that are not selected by default queries(select:false in model). In some scenarios I need to test the result to have the field selected. I have tried sinon-mongoose but it just tells me if a populate or select method was called, but for more flexibility I need to check the result data. And a requirement is not to use any real db connection, as it is a unit test.
The model
var DoctorSchema = new Schema({
firstName: {
type: String,
required: true,
},
lastName: {
type: String,
required: true
},
middleName: {
type: String
},
institutionName: {
type: String,
required: true,
select: false
}
});
The Service
module.exports = function (Doctor) {
service.getById = function(req, res, next){
Doctor.findOne({"_id" : req.params.id}).select('+institutionName').exec(function (err, doc) {
if (err) {
console.log(err);
return
};
if (!doc) {
res.send(404);
}
else {
res.json(doc);
}
});
}
}
So this can be tested
describe('doctorService getById', function () {
it('presentationService should call getById', function (done) {
var DoctorMock = sinon.mock(DoctorModel);
DoctorMock
.expects('findOne').withArgs({ "_id": "123" })
.chain("select").withArgs('+institutionName')
.chain('exec')
.yields(null, seed.doctor);
var serviceMock = doctorService(DoctorModel);
var res = {
json: function (data) {
DoctorMock.verify();
DoctorMock.restore();
assert.equal(seed.doctor, data, "Test fails due to unexpected result");
done();
}
};
serviceMock.getById({param:{id:"123"}}, res);
});
});
But in my example the test is bound to the chain. My intuition tells me that this is not a good approach.
I want to send value of result from child to parent element. I used Session.set and Session.get and it works fine but I know that is not good practice because Sessions are global. So, I wanted to try something like reactive var or reactive dict but both of them are giving me only object as a result. What should I do or how should I take specific things from that object? (I am storing JSON inside that ReactiveVar or Dict and I know that they are really bad with JSON. Thank you for help!
Template.companyCreate.helpers({
CompanyName : function () {
if (Meteor.user() || Roles.userIsInRole(Meteor.user(),['admin','adminCreator'], 'companyAdmin')) {
Meteor.call('findCompany', function(err, result) {
if (err) {
console.log(err.reason)
}
else {
//this is where I want to take result and give it to parent function
}
});
return //this is where I want to take result that was given from child function and return it to CompanyName
}
else {
Router.go('/nemate-prava')
}
},
UPDATED CODE
Template.companyCreate.onCreated(function Poruke() {
this.message = new ReactiveVar(' ');
let self = this;
let user = Meteor.user();
let companyNameHandler = Template.currentData().companyNameHandler;
self.companyName = new ReactiveVar();
if (user && Roles.userIsInRole(user,['admin','adminCreator'], 'companyAdmin')) {
Meteor.call('findCompany', function(err, result) {
if (err) {
console.log(err.reason)
}
else {
self.companyName.set(result);
companyNameHandler(result);
}
});
}
else {
Router.go('/nemate-prava')
}
});
Template.companyCreate.helpers({
message: () => { return Template.instance().message.get() },
isNotInRole : function() {
if (!Meteor.user() || !Roles.userIsInRole(Meteor.user(),['admin','adminCreator'], 'companyAdmin')) {
return true;
}
else {
return false;
}
},
CompanyName : function () {
return Template.instance().companyName.get();
}
});
Template.companyCreate.events({
'submit form': function(event, template) {
var Ime = event.target.Ime.value;
event.preventDefault();
Meteor.call('companyCheck', Ime, function(error, result) {
if (error) {
console.log(error.reason);
template.message.set(error.reason);
alert(error.reason);
}
else {
event.target.Ime.value = "";
console.log('Kompanija je uspesno kreirana!');
template.message.set("Uspesno!");
}
})
},
});
Method:
'findCompany'(){
ImeKompanije = firma.findOne({AdminID: this.userId}).ImeKompanije
if (typeof ImeKompanije == 'undefind') {
throw new Meteor.Error(err, "Greska!");
}
return ImeKompanije;
},
});
Router:
Router.route('/comp/:ImeKompanije', {
name: 'companyProfile',
template: 'companyProfile',
waitOn: function() {
return Meteor.subscribe('bazaFirmi', this.params.ImeKompanije)
},
action: function() {
this.render('companyProfile', {
data: function() {
return firma.findOne({ImeKompanije: this.params.ImeKompanije});
}
});
},
});
ok, there's a lot to unwind here. let's start with something small.
if (Meteor.user() || Roles.userIsInRole(Meteor.user(),['admin','adminCreator'], 'companyAdmin')) {
i think this line is meant to say, "if the user is an admin". but it's really saying, "if the user is logged in." if you meant the first one, then change the "||" to an "&&".
bigger issue is you're making a server call in a helper. helpers can get called over and over, so think of them as something that simply returns data. it should not have any side effects, such as making a server call or (yikes) re-routing the user.
so let's move all that side effect code to the onCreated() and capture the company name so it can be returned from the helper. We'll also get set up to return the company name to the parent.
Template.companyCreate.onCreated(function() {
let self = this;
let user = Meteor.user();
let companyNameHandler = Template.currentData().companyNameHandler;
self.companyName = new ReactiveVar();
if (user && Roles.userIsInRole(user,['admin','adminCreator'], 'companyAdmin')) {
Meteor.call('findCompany', function(err, result) {
if (err) {
console.log(err.reason)
}
else {
self.companyName.set(result);
companyNameHandler(result);
}
});
}
else {
Router.go('/nemate-prava')
}
});
now the helper is really simple, it just returns the data that was saved to the template's reactive var:
Template.companyCreate.helpers({
CompanyName : function () {
return Template.instance().companyName.get();
}
});
the last part is setting up the handler to return the data to the parent. it's bad form to have the client reaching back up to its parent, so i usually have the parent give to the child a function it can call. usually i'll do that when the child says, "i've done my work," but here we can use it to provide that data. i'll have to make some assumptions on what your parent looks like.
<template name="Parent">
{{> companyCreate companyNameHandler=getCompanyNameHandler}}
</template>
Template.Parent.helpers({
getCompanyNameHandler() {
let template = Template.instance();
return function(companyName) {
console.log(companyName);
// you can also access the parent template through the closure "template"
}
}
});
the parent's helper returns a function that is passed to the client. when the client calls it, it will execute in the parent's closure. you can see i set up a variable called "template" that would allow you to, say, access reactive vars belonging to the parent.
UPDATE: in case the handler isn't known as is inside the Meteor.call() scope, we can try using it through a reactive var.
Template.companyCreate.onCreated(function() {
let self = this;
let user = Meteor.user();
self.companyNameHandler = new ReactiveVar(Template.currentData().companyNameHandler);
self.companyName = new ReactiveVar();
if (user && Roles.userIsInRole(user,['admin','adminCreator'], 'companyAdmin')) {
Meteor.call('findCompany', function(err, result) {
if (err) {
console.log(err.reason)
}
else {
self.companyName.set(result);
let fn = self.companyNameHandler.get();
fn(result);
}
});
}
else {
Router.go('/nemate-prava')
}
});
I'm using publish-composite to perform a reactive join (I'm sure the specific package does not matter). And I am seeing that the intermediate data gets pushed to the client.
In the following example:
Meteor.publishComposite('messages', function(userId) {
return {
find: function() {
return Meteor.users.find(
{ 'profile.connections.$': userId }
);
},
children: [{
find: function(user) {
return Messages.find({author: user._id});
}
}]
}
});
All the users that has userId in profile.connections get exposed to the client. I know that can create a mongodb projection so the sensitive stuff is not exposed. But I was wondering if I can just prevent the first find() query cursor from getting to the client at all.
Are you trying to only publish messages for a particular user if that user is a connection with the logged on user? If so, maybe something like this would work:
Meteor.publishComposite('messages', function(userId) {
return {
find: function() {
return Meteor.users.find(this.userId);
},
children: [{
find: function(user) {
return Meteor.users.find(
{ 'profile.connections.$': userid }
);
},
children: [{
find: function(connection, user) {
return Messages.find({author: connection._id});
}
}]
}]
};
});
That would be equivalent to something like :
Meteor.publish('message',function(userId) {
var user = Meteor.users.find({_id : this.userId, 'profile.connections.$' : userId});
if (!!user) {
return Messages.find({author: userId});
}
this.ready();
});
I am trying to insert a documents into collections which are all related to each other: Posts, Comments, and Categories. Each document in Comments and Categories must have a PostId field.
I have created a method named insertSamplePost, which should return the id of the post after inserting a document into Posts. I have assigned this method call to a variable like so:
var postId = Meteor.call('insertSamplePost', samplePost, function(error, id) {
if (error) {
console.log(error);
} else {
return id;
}
});
However, when I try to use postId later to insert related comments and categories, it appears to be undefined! Does anyone know what is happening?
Here is my full code:
if (Meteor.isClient) {
Template.post.events({
'click .new-sample-post' : function (e) {
var samplePost = {
title: "This is a title",
description: "This is a description"
};
// Insert image stub
var postId = Meteor.call('insertSamplePost', samplePost, function(error, id) {
if (error) {
console.log(error);
} else {
return id;
}
});
// This returned undefined. :-()
console.log(postId);
var sampleComment = {
body: "This is a comment",
postId: postId
};
var sampleCategory = {
tag: "Sample Category",
postId: postId
};
Comments.insert(sampleComment);
Categories.insert(sampleCategory);
}
});
}
// Collections
Posts = new Meteor.Collection('posts');
Comments = new Meteor.Collection('comments');
Categories = new Meteor.Collection('categories');
// Methods
Meteor.methods({
insertSamplePost: function(postAttributes) {
var post = _.extend(postAttributes, {
userId: "John Doe",
submitted: new Date().getTime()
});
return Posts.insert(post);
}
});
When you do:
var myVar = Meteor.call("methodName", methodArg, function(error, result) {
return result;
}
Your myVar variable will actually be whatever Meteor.call() returns, not what your callback function returns. Instead, what you can do is:
var postId;
Meteor.call('insertSamplePost', samplePost, function(error, id) {
if (error) {
console.log(error);
} else {
postId = id;
}
});
However, as Akshat mentions, by the time the callback function actually runs and asynchronously sets the postId, your insert calls on the other collections will already have run.
This code would actually be a little simpler if you avoid the server method altogether - you can modify the document in your collection's allow callback:
Template.post.events({
'click .new-sample-post' : function (e) {
var samplePost = {
title: "This is a title",
description: "This is a description"
};
var postId = Posts.insert(samplePost);
var sampleComment = {
body: "This is a comment",
postId: postId
};
var sampleCategory = {
tag: "Sample Category",
postId: postId
};
Comments.insert(sampleComment);
Categories.insert(sampleCategory);
}
});
Now you can add the userId and submitted fields in your Posts.allow() callback:
Posts.allow({
insert: function(userId, doc) {
doc.userId = userId;
doc.submitted = new Date().getTime();
return true;
}
});
If you wanted, you can still do the two secondary inserts within the callback for your first insert, in order to make the operation more atomic (in other words, to make sure the secondary inserts don't happen if the first insert fails).
You can use Session to store the results since Session is reactive and client side javascript is asynchronous so you cant assign the result to a variable directly using return.
So the reason you get undefined is because the result of Meteor.call is given in the callback. The callback will yield a result much later and by the time it returns a result the rest of your code will already have run. This is why its a good idea to use Session because you can use it in a template helper.
However for inserting a post its better to just insert the comments and category in the callback itself since you're not displaying the result in the html.
Meteor.call('insertSamplePost', samplePost, function(error, postId) {
if (error) {
console.log(error);
} else {
var sampleComment = {
body: "This is a comment",
postId: postId
};
var sampleCategory = {
tag: "Sample Category",
postId: postId
};
Comments.insert(sampleComment);
Categories.insert(sampleCategory);
}
});
This way if the result is an error the comment and category wont be inserted.