How to implement `protectedFields` in Parse-Server? - parse-platform

I believe this is a new feature in Parse-Server.
By default, the User class's email field is considered a protected field, meaning that email is set to read: false, write: false to the public by default. But, every other field in the User class is set to read: true, write: false
In Github, I saw this example:
export type ClassLevelPermissions = {
find?: { [string]: boolean },
count?: { [string]: boolean },
get?: { [string]: boolean },
create?: { [string]: boolean },
update?: { [string]: boolean },
delete?: { [string]: boolean },
addField?: { [string]: boolean },
readUserFields?: string[],
writeUserFields?: string[],
// new feature
protectedFields?: { [string]: boolean }
};
For example, with the _User class, if the server was initialized with userSensitiveFields: ['email', 'sin', 'phone'], this would be the equivalent of:
{
// CLP for the class ... other
protectedFields: { "*": ["email", "sin"] }
};
Now if you wanted an moderator role to be able to see the user's email but not the sin and an admin which can read it all
{
protectedFields: {
"*": ["email", "sin"],
"role:moderator": ["sin"],
"role:admin": []
}
};
After seeing this example, I was still confused where exactly to implement protectedFields. Do I implement it in my app's index.js, or main.js, etc? Can somebody give me an example of how I can set a field: phoneNum to have a protectedField similiar to email's default?

It is an option in parse server initialization. See the protectedField option here:
http://parseplatform.org/parse-server/api/master/ParseServerOptions.html
I don't know exactly where/how you are running your Parse server, but it should be something like this:
var express = require('express');
var ParseServer = require('parse-server').ParseServer;
var app = express();
var api = new ParseServer({
databaseURI: 'mongodb://localhost:27017/dev',
cloud: '/home/myApp/cloud/main.js',
appId: 'myAppId',
masterKey: 'myMasterKey',
fileKey: 'optionalFileKey',
serverURL: 'http://localhost:1337/parse'
protectedFields: {
_User: {
"*": ["email", "sin"],
"role:moderator": ["sin"],
"role:admin": []
}
}
});
app.use('/parse', api);
app.listen(1337, function() {
console.log('parse-server-example running on port 1337.');
});

Related

Setup & configure NestJS 9 (TypeORM 0.3+) to use custom repositories

I am currently attempting to update my project to the latest version of NestJS and TypeORM and I am having a great deal of difficulty wrapping my head around how to setup the project so that I can use custom repositories in a similar way that it was before.
The biggest problem is that #EntityRepository and .getCustomRepository have been deprecated. I have been pouring over documentation and I can't seem to figure out the new way of doing things.
From the TypeORM docs, it says to do something like this:
export const UserRepository = dataSource.getRepository(User).extend({
findByName(firstName: string, lastName: string) {
return this.createQueryBuilder("user")
.where("user.firstName = :firstName", { firstName })
.andWhere("user.lastName = :lastName", { lastName })
.getMany()
},
})
ok... I get that... use the dataSource to create the repo and extend it with the additional functions. Seem's easy enough, right?
WRONG!
You can't create the repo until the dataSource object is created. In straight up TypeORM you can create your new DataSource but in nest, it's an async bootstrap... which means when you import your repo/service files the data source isn't yet there.
grrr.
So, here is my code (simplified).
Basically, it's a small app with users. The users have a role. So you can get the user by name, or update the user name so long as it has a role. If the role is null, then you can't do it. (the create method is not included, assume it's there)
app.config.ts
// database values and such are loaded from an env file. (not included)
export const applicationModuleConfig: ApplicationModuleConfig = {
typeOrmConfig: {
type: "postgres",
host,
username,
database,
port,
synchronize: false,
cache: true,
entities: [resolve(__dirname, "./**/*.entity.ts"), resolve(__dirname, "./**/*.entity.js")],
migrations: [resolve(__dirname, "./migrations/**/*{.ts,.js}")],
migrationsTableName: "migrations",
migrationsRun: true,
password,
}
}
users.entity.ts (trimmed, assume there are more fields/columns)
#Entity()
#ObjectType("Users")
export class Users {
#Field(() => Int)
#PrimaryGeneratedColumn()
id: number;
#Field()
#Column({ type: "varchar", nullable: false, default: "" })
name: string;
#Field()
#Column({ type: "varchar", nullable: false, default: "" })
role: string;
}
users.repository.ts
export const UsersRepository = dataSource.getRepository(Users).extend({
findByName(name: string) {
return this.createQueryBuilder("users")
.where("users.name = :name", { name })
.getOne()
},
async verifyRole(id: number) {
const user = await this.createQueryBuilder("users")
.where("users.id = :id", { id })
.getOne()
return user?.role !== null;
},
})
users.service.ts
#Injectable()
export class UsersService {
constructor(private readOnly usersRepo: UsersRepository) {}
findByName(name: string) {
return this.usersRepo.findByName(name);
}
verifyRole(id: number) {
return this.usersRepo.verifyRole(id);
}
}
users.resolver.ts
#Resolver(() => User)
#UseGuards(GraphqlAuthGuard, PermissionsGuard, ScopeGuard)
export class UserResolver {
constructor(private readonly usersService: UsersService) {}
#Query(() => User, { name: "user" })
async getUser(
#Args({
name: "name",
type: () => String,
})
name: string,
#CurrentUser() currentUser: AuthorizedUser,
): Promise<Users> {
return this.usersService.findByName(name);
}
// assume EditUsersInput is a object matching the entity fields.
#Mutation(() => Users)
#VerifyUser<{ input: EditUsersInput }>(({ input }) => {
// >>> this doesn't work <<<*
return UsersRepository.verifyRole(input.id);
})
async editUser(
#Args(
{
name: "input",
type: () => EditUsersInput,
},
new ValidationPipe(),
)
input: EditUsersInput,
): Promise<Users> {
await UsersRepository.editUser(input); // assume the editUser method exists in the service and repo.
return UsersRepository.findByUserId(input.id); // assume the findByUserId method exists in the service and repo.
}
app.module.ts (I will admit I am not sure what I am supposed to be doing in this file)
#Global()
#Module({
imports: [TypeOrmModule.forFeature([...entitiesWithoutAModuleHere])],
providers: [UsersService, UsersRepository],
exports: [UsersService, UsersRepository],
})
export class ApplicationModule {
static forRoot(config: ApplicationModuleConfig): DynamicModule {
const {
graphql,
typeOrmConfig,
numConcurrentReportJobs,
redis: { tls: redisTls, ...redisRest },
} = config;
////
// a bunch of redis code goes in here...
////
return {
module: ApplicationModule,
imports: [
// omitted unrelated imports, such as bullQueue and stuff like that.
TypeOrmModule.forRoot({
...typeOrmConfig,
}),
GraphQLModule.forRoot(graphql),
ConfigurationModule.forRoot(config), // I am not sure if this file is important or not, but I am omitting it for now.
],
};
}
constructor(public dataSource: DataSource) {
// AH HA! The dataSource has been initialized by nest!
// but, the repository errors have already occurred and it never got this far.
// I don't know what to do here.
}
}
main.ts
import blah from "blah";
import { applicationModuleConfig } from "./app.config";
import everythingElse from "otherStuff";
import { ApplicationModule } from "./app.module"; // <-- KaBOOM!
async function bootstrap() {
initializeTransactionalContext();
patchTypeORMRepositoryWithBaseRepository();
const ApplicationModule = require("./app.module");
const app = await NestFactory.create(ApplicationModule.forRoot(applicationModuleConfig), { bodyParser: false });
app.use(
session({
resave: false, //As per https://github.com/expressjs/session#resave
saveUninitialized: false,
store: new (require("connect-pg-simple")(session))({
createTableIfMissing: true,
}),
secret: process.env.APP_SESSION_SECRET, cookie: { secure: secureCookies },
}),
);
await app.listen(3000);
}
bootstrap().then(() => null);
I know I am doing it wrong, but I can't figure out how to do it right. Any thoughts, comments, suggestions, examples, tutorials, or explanations of the right way?
Thanks!

Make an ajax call inside of a .map()

I am upgrading jquery and saw that the "async: false" option has been deprecated. This makes sense and in 99.9% of cases I agree with the rationale, but I have a case where I think I really need it and I cannot for the life of me figure out how to make this work with a purely async ajax call no matter how I use promises or async/await.
My use case is in a Vue component and I have an array of contacts. What I need to do is map over the contacts and validate them. One such validation requires a quick check of email validity via a "check_email" ajax endpoint.
Once I validate (or not) the list, I then submit the list (if valid) or show error messages (if invalid).
My code is something like this
sendContacts: function() {
valid = this.validateContacts()
if (valid) {
// send the contacts
} else {
return // will display error messages on contacts objects
}
},
validateContacts: function() {
this.contacts = this.contacts.map((contact) => {
if (!contact.name) {
contact.validDetails.name = false
contact.valid = false
return contact
}
if (!contact.email) {
contact.validDetails.emailExists = false
contact.valid = false
return contact
}
if (!check_email(email)) { // THIS IS ASYNC NOW WHAT DO I DO
contact.valid = false
contact.validDetails.emailFormat = false
}
return contact
}
var validData = this.contacts.map(c => {
return c.valid
})
return !validData.includes(false)
}
function check_email(email) {
const url = `/api/v1/users/check-email?email=${email}`
let valid = false
$.ajax({
url: url,
type: 'POST',
async: false, // I can't do this anymore
headers: {
'X-CSRFToken': csrfToken
},
success: resp => {
valid = true
},
error: err => {
}
})
return valid
}
my data function:
data: function() {
return {
contacts: [this.initContact()],
showThanks: false,
emailError: false,
blankEmail: false,
blankName: false
}
},
methods: {
initContact: function() {
return {
name: null,
email: null,
title: null,
validDetails: this.initValidDetails(),
valid: true,
}
},
initValidDetails: function() {
return {
emailDomain: true,
emailExists: true,
emailFormat: true,
name: true
}
}
}
Again, I have tried async/await in every place I could think of and I cannot get this to validate properly and then perform correct logic regarding whether the send contacts function part of the function should fire. Please help!
Once any part of your validation is asynchronous, you must treat the entire thing as asynchronous. This includes when calling validateContacts in sendContacts.
First, you should change check_email to return Promise<bool>. It's usually a bad idea to include jQuery in a Vue project so let's use fetch instead (Axios being another popular alternative).
async function check_email(email) {
const params = new URLSearchParams({ email })
const res = await fetch(`/api/v1/users/check-email?${params}`, {
method: 'POST',
headers: {
'X-CSRFToken': csrfToken
}
})
return res.ok
}
As for your async validation logic, it's best to map your contacts to an array of promises and wait for them all with Promise.all.
async validateContacts () {
const validationPromises = this.contacts.map(async contact => {
if (!contact.name) {
return {
...contact,
valid: false,
validDetails: {
...contact.validDetails,
name: false
}
}
}
if (!contact.email) {
return {
...contact,
valid: false,
validDetails: {
...contact.validDetails,
emailExists: false
}
}
}
if (await check_email(contact.email)) { // await here
return {
...contact,
valid: false,
validDetails: {
...contact.validDetails,
emailFormat: false
}
}
}
return { ...contact, valid: true }
})
// now wait for all promises to resolve and check for any "false" values
this.contacts = await Promise.all(validationPromises)
return this.contacts.every(({ valid }) => valid)
}
As mentioned, now you need to treat this asynchronously in sendContacts
async sendContacts () {
if (await this.validateContacts()) {
// send the contacts
}
}

Parse Server ignores ACL

I have a simple Parse Cloud Code function for my NewsFeed Objects. For these NewsFeed Objects I set an ACL such that the fromUser can write to it and the toUser can read and write to it. I try to get these objects for every user with the following function:
Parse.Cloud.define("get_feed", async (request) => {
let user = request.user;
var query = new Parse.Query("NewsFeed");
query.equalTo("toUser", user);
query.include("fromUser");
query.descending("createdAt");
let result;
try {
result = await query.find();
} catch (error) {
throw error.message;
}
return result;
});
I would expect that I get all the objects which satisfy the query and have the following ACL:
"ACL" : {
"xXl3OIndCP": {
"read": true,
"write": true
},
"VPuRMZGhcv": {
"write": true
}
}
But unfortunately it works only if I add public read access to the ACL such that it looks like this
"ACL" : {
"*": {
"read": true
},
"xXl3OIndCP": {
"read": true,
"write": true
},
"VPuRMZGhcv": {
"write": true
}
}
I don't think that this is the expected behavior or am I wrong? Did anybody face this issue before?
Well, turned out that one has to pass the session token in the find() function so I had to use result = await query.find({sessionToken : user.getSessionToken()}); instead of result = await query.find();

Apollo Server / GraphQL - Properties of Nested Array Returning Null

Bear with me, I will explain this the best I can. Please let me know if more information is needed, I am trying to keep this as brief as possible.
I am using Apollo Server and the 'apollo-datasource-rest' plugin to access a REST API. When attempting to get the property values from a nested array of objects I get a null response for each field/property. In addition, the array being queried is only showing a single iteration when multiple are available.
The field in question is the 'cores' field within the Rocket type, i.e., launch.rocket.firstStage.cores
I have attempted various ways of mapping through 'cores' (thinking this was what it wanted) with no success.
To keep things short and simple I'm only including the code for the specific issue. All other parts of the query are operating as expected.
You can view the API response I am hitting here: https://api.spacexdata.com/v3/launches/77
schema.js
const { gql } = require('apollo-server');
const typeDefs = gql`
type Query {
singleLaunch(flightNumber: Int!): Launch
}
type Launch {
flightNumber: Int!
rocket: Rocket
}
type Rocket {
firstStage: Cores
}
type Cores {
cores: [CoreFields]
}
type CoreFields {
flight: Int
gridfins: Boolean
legs: Boolean
reused: Boolean
landingType: String
landingVehicle: String
landingSuccess: Boolean
}
`;
module.exports = typeDefs;
Data Source - launch.js
const { RESTDataSource } = require('apollo-datasource-rest');
class LaunchAPI extends RESTDataSource {
constructor() {
super();
this.baseURL = 'https://api.spacexdata.com/v3/';
}
async getLaunchById({ launchId }) {
const res = await this.get('launches', {
flight_number: launchId,
});
return this.launchReducer(res[0]);
}
launchReducer(launch) {
return {
flightNumber: launch.flight_number || 0,
rocket: {
firstStage: {
cores: [
{
flight: launch.rocket.first_stage.cores.flight,
gridfins: launch.rocket.first_stage.cores.gridfins,
legs: launch.rocket.first_stage.cores.legs,
landingType: launch.rocket.first_stage.cores.landing_type,
landingVehicle: launch.rocket.first_stage.cores.landing_vehicle,
landingSuccess: launch.rocket.first_stage.cores.landing_success,
},
],
},
};
}
}
module.exports = LaunchAPI;
resolvers.js
module.exports = {
Query: {
singleLaunch: (_, { flightNumber }, { dataSources }) =>
dataSources.launchAPI.getLaunchById({ launchId: flightNumber }),
},
};
Query
query GetLaunchById($flightNumber: Int!) {
singleLaunch(flightNumber: $flightNumber) {
flightNumber
rocket {
firstStage {
cores {
flight
gridfins
legs
reused
landingType
landingVehicle
landingSuccess
}
}
}
}
}
Expected Result
{
"data": {
"singleLaunch": {
"flightNumber": 77,
"rocket": {
"firstStage": {
"cores": [
{
"flight": 1,
"gridfins": true,
"legs": true,
"reused": true,
"landingType": "ASDS",
"landingVehicle": "OCISLY",
"landSuccess": true,
},
{
"flight": 1,
"gridfins": true,
"legs": true,
"reused": false,
"landingType": "RTLS",
"landingVehicle": "LZ-1",
"landSuccess": true
},
{
"flight": 1,
"gridfins": true,
"legs": true,
"reused": false,
"landingType": "RTLS",
"landingVehicle": "LZ-2",
"landSuccess": true
},
]
}
},
}
}
}
Actual Result (Through GraphQL Playground)
{
"data": {
"singleLaunch": {
"flightNumber": 77,
"rocket": {
"firstStage": {
"cores": [
{
"flight": null,
"gridfins": null,
"legs": null,
"reused": null,
"landingType": null,
"landingVehicle": null,
"landingSuccess": null
}
]
}
},
}
}
}
Any suggestions as to what I am doing wrong here would be greatly appreciated. Again, let me know if more information is needed.
Thank you!
Missing base url
There should be
await this.get( this.baseURL + 'launches'
IMHO there should be a map used within launchReducer to return an array, sth like:
launchReducer(launch) {
return {
flightNumber: launch.flight_number || 0,
rocket: {
firstStage: {
cores: launch.rocket.first_stage.cores.map(core => ({
flight: core.flight,
gridfins: core.gridfins,
legs: core.legs,
landingType: core.landing_type,
landingVehicle: core.landing_vehicle,
landSuccess: core.land_success,
})),
},
},
};
}
.map(core => ({ is for returning object [literal], the same as/shorter version of .map(core => { return {

graphql-subscriptions withFilter returns undefined; subscriptions without variables work ok

I'm trying to get my head around graphql-subscriptions and withFilter. Subscriptions without variables work as intended, but if I try to use withFilter, I only get 'Subscription field must return Async Iterable. Received: undefined' error when I try to run the subscription.
Am I doing something wrong with setting up withFilter, are the some incompatibilities with packages I'm using or am I completely missing something obvious here? All queries and mutations work properly, so the basic set up should be fine.
My set up is similar to this (all code snippets are in https://gist.github.com/aqmattil/41e10e7c9f30b8ea964cecdc61c58f20
Package.json
// package.json
"dependencies": {
"apollo-server-express": "^2.0.0-beta.2",
"body-parser": "^1.18.3",
"express": "^4.16.3",
"graphql": "^0.13.2",
"graphql-subscriptions": "^0.5.8",
"subscriptions-transport-ws": "^0.9.11"
}
Mutations
// mutations.js
const mutation = new GraphQLObjectType({
name: 'mutation',
fields: {
addSite: {
type: SiteType,
description: "Create a new Site",
args: {
name: { type: new GraphQLNonNull(GraphQLString) },
location: { type: GraphQLString },
company: { type: GraphQLString }
},
async resolve(parentValue, { name, location, company }) {
const site = await new Site({ name, location, company }).save()
const siteid = site._id;
console.log("addSite resolve", siteid, name, location, company );
pubsub.publish('siteAdded', { 'siteAdded': site } );
return site;
}
}
}
});
module.exports = mutation;
Subscriptions
// subscriptions.js
const graphql = require('graphql');
const {
GraphQLObjectType,
GraphQLString
} = graphql;
const { withFilter } = require('graphql-subscriptions');
const SiteType = require('./site_type');
const pubsub = require('./pubsub_helper');
const Subscriptions = new GraphQLObjectType({
name: 'subscription',
fields: () => ({
/*
// this code works, commented out to test withfilter
siteAdded: {
type: SiteType,
resolve(payload) {
return payload.siteAdded;
},
subscribe() {
return pubsub.asyncIterator('siteAdded');
}
},
*/
// test withFilter
siteAdded: {
type: SiteType,
args: {
name: { type: GraphQLString }
},
resolve(payload) {
return payload.siteAdded;
},
subscribe() {
// this returns undefined
withFilter(
() => {
console.log("in subscribe withfilter");
return pubsub.asyncIterator('siteAdded');
}
),
(payload, variables) => {
console.log("payload, variables", payload, variables);
return true;
}
}
}
})
});
module.exports = Subscriptions;
I'm using graphiql to run the queries,
// this is used to add a site
mutation {
addSite(name:"test name", location: "somewhere") {
id
}
}
// simple subscription - this works as inteded, and new sites are shown
subscription {
siteAdded {
name
location
company {
id
}
}
}
// using query variables --> returns "Subscription
// field must return Async Iterable. Received: undefined"
subscription {
siteAdded(name: "test name") {
name
location
company {
id
}
}
}

Resources