Enforce one field based on another - kebab-case example

I was just curious if anyone had any ideas on how to enforce one field based on another.

Let’s say I had:

title = “Make money on dgraph”
title-kebab = “make-money-on-dgraph”

Is there anyway to enforce, or generate on the backend, that the title-kebab field is a correct version of the title field?

Thanks,
J

This is tagged with Dgraph. Are you using GraphQL or DQL? With DQL, the answer is no, not on the database side. With GraphQL it is possible using a lambda field resolver

Slash Dgraph (GraphQL)…

So in that case as @verneleem said you can use lambda resolver to run javascript before the data is commited to the db and edit it so it will always contain - instead of space

1 Like

Could you guys show me how this could be done?

I am trying something like this:

Type Post {
  id: ID!
  name: string;
  nameKebab: String @lambda
}

Function:

function nameKebab({parent: {name} }) {
  console.log('working...');
  return kebabCase(name);
}

self.addGraphQLResolvers({
    "Post.nameKebab": nameKebab
});

function kebabCase(str) {
  const result = str.replace(
    /[A-Z\u00C0-\u00D6\u00D8-\u00DE]/g,
    match => '-' + match.toLowerCase()
  )
  return (str[0] === str[0].toUpperCase())
    ? result.substring(1)
    : result
}

The log is not printing anything, and the type is empty after I create a new record…

Thanks,

Jonathan

Hey, try setting the Dgraph lambda as follows :

function nameKebab({parent: {name} }) {
  console.log('nameKebab called');
  return name.replace(/\s/g,"-").toLowerCase()
}

self.addGraphQLResolvers({
    "Post.nameKebab": nameKebab
});

Following is the result after doing a call on the lambda :

{
          "id": "0x186a2",
          "name": "Please DON'T skewer me uwu",
          "nameKebab": "please-don't-skewer-me-uwu"
}

I am using subscriptions, and it seems lambdas do not support subscriptions. I sort of understand why (since one field basically equals a function), however, this is NOT my intended functionality.

I do feel lambdas should be added to subscriptions, but I digress…

I want to actually put data in the field, not run a function call every time the field is read.

Is there a way to set the field Post.nameKebab when a new Post is created? My goal here is to prevent a front-end user from setting this field when a new Post is created.

Thanks,
J

You can set a lambda mutation called newPostEntry and set the nameKebab field in that lambda mutation.That way you won’t have to call a function each time and save the data directly to your object instance. Check out the following link for understanding how to use lambdas in slashgql → https://dgraph.io/docs/graphql/lambda/overview/

I seemed to have gotten this working with one question:

Type:

type Mutation {
    newPost(
      name: String!
      description: String!
      published: Boolean!
    ): ID! @lambda
}

Lambda:

async function newPost({args, graphql}) {

  const newArgs = {
    name: args.name,
    description: args.description,
    nameKebab: args.name.replace(/\s/g,"-").toLowerCase(),
    published: args.published
  };

  const results = await graphql(`mutation addPost($post: AddPostInput!) {
  addPost(input: [$post]) {
    post {
      id
      name
      description
      published
    }
  }
}`, {"post": newArgs});
  return results.data.addPost.post[0].id
}

self.addGraphQLResolvers({
  "Mutation.newPost": newPost
});

Mutation:

private NEW_POST = gql`
mutation newPost($name: String!, $description: String!, $published: Boolean!) {
  newPost(name: $name, description: $description, published: $published)
}
`;

Security:

type Post @withSubscription @auth(
    add: { rule:  "{$DENIED: { eq: \"DENIED\" } }"}
){
  id: ID!
  name: String! @search(by: [fulltext])
  description: String! @search(by: [fulltext])
  nameKebab: String @search(by: [exact])
  published: Boolean!
}

Please let me know any way to simplify the mutation as there is only one real line of code that matters here.

This security part obviously does not work, as it locks any new adds… This is the question I do not understand. If I cannot prevent someone from running addPost directly, there is not point in creating any of this on the backend.

How could this be done security wise?

Thanks,
J

There are two ways I see around this.

  1. Inside the lambda perform the mutation using DQL which bypasses all auth rules.
  2. Inside the lambda override the authHeader with your own JWT that passes the add rule.

Option 1)

I tried changing my code to regular DQL, but I am not sure how. The previous query does not seem to work. What do I change this to, as I do not have an understanding of translating this to triples:

  const results = await dql.query(`mutation addPost($course: AddPostInput!) {
  addPost(input: [$post]) {
    course {
      id
      name
      description
      published
    }
  }
}`, {"post": newArgs});
  console.log(JSON.stringify(results.data));
  return results.data.addPost.post[0].id
}

Option 2)

How do you override the authHeader with your own JWT? I cannot use the variables in:

# Dgraph.Authorization in regular javascript…

Would I change the global.USER variable? This does not work…

I will try to free a block of time tomorrow to answer these.

2 Likes

@verneleem FYI, I am also looking for this solution.

I want to know how can issue graphql mutations as an ADMIN from lambdas

One way I can think of is to generate a new token within the lambda calling firebaseadmin (as I am using firebase) and pass along that token but to do it inside the lambda seems overly costly to me.
Another thing I can think of is to have my own server running somewhere, send a request to that and get the admin token from there and then pass the new token in graphql requests,

but even then, how exactly to pass the authToken for graphql calls within the lambdas ?

1 Like

@verneleem

Option 2)

My question here is specific to how to WRITE and PASS the new token, not just read it.

Option 1) - I am hoping to get option 1 figured out here:

docs PR: https://github.com/dgraph-io/dgraph-docs/pull/127

A resolver example using a graphql call and manually overriding the authHeader

provided by the client:

async function secretGraphQL({ parents, graphql }) {
 const ids = parents.map((p) => p.id);
 const secretResults = await graphql(
   `query myQueryName ($ids: [ID!]) { queryMyType(filter: { id: $ids }) { id controlledEdge { myField } }> }`,
   { ids },
   {
     key: 'X-My-App-Auth'
     value: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJodHRwczovL215LmFwcC5pby9qd3QvY2xhaW1zIjp7IlVTRVIiOiJmb28ifSwiZXhwIjoxODAwMDAwMDAwLCJzdWIiOiJ0ZXN0IiwibmFtZSI6IkpvaG4gRG9lIDIiLCJpYXQiOjE1MTYyMzkwMjJ9.wI3857KzwjtZAtOjng6MnzKVhFSqS1vt1SjxUMZF4jc'
   }
 );
 return parents.map((parent) => {
   const secretRes = secretResults.data.find(res => res.id === parent.id)
   parent.secretProperty = null
   if (secretRes) {
     if (secretRes.controlledEdge) {
       parent.secretProperty = secretRes.controlledEdge.myField
     }
   }
   return parent
 });
}
self.addMultiParentGraphQLResolvers({
 "MyType.secretProperty": secretGraphQL,
});

The above is an example of how to pass a JWT. How to make a JWT really depends on your auth setup. If you are using a 3rd party provider for auth such as Auth0, then you should do a login over http to the 3rd party. If you are using a more static configuration, then you could generate the jwt within the lambda script. But keep in mind that if you need to use any 3rd party packages, you will need to compile the script with webpack before pushing to Dgraph Cloud Lambda.

1 Like

Thanks @verneleem for the complete example.

This seems incredibly complicated. I also use firebase for my auth, which is already overly complicated until Field-level auth is implemented in the upcoming release.

I am going to stick with Option 2, which I am so happy it exists (despite the fact JSON is not accepted at this time in dql lambdas).

I have a feeling as slash-dgraph gets more mature, and I get more mature in my dgraph development, I will come back to this example needing more complicated security.

Thank you!

J

1 Like