Building a Dating site with Neo4j – Part Two

We came up with an idea for a dating site and an initial model in Part One. Next we are going to work on a back end HTTP API, because I’m old school and that’s the way I like it. We will build our HTTP API right into Neo4j using an extension which turns Neo4j from a Server into a Service. Unlike last time where we wrote a clone of Twitter, I don’t really know where I’m going with this, so let’s start with some of the obvious API endpoints and then we can design and build more as we go along. Is this Agile or am I just being an idiot? I can’t tell, so onward we go.

First obvious thing is, we need a schema. Luckily Neo4j is a “Schema Optional” database so we don’t have to worry about designing any tables or properties or figuring out what kind of properties each table will have. Because… well, we don’t have tables. The only real schema we need to worry about are Constraints and Indexes. For example we don’t want two users to have the same username or same email, so we will create a uniqueness constraint on those Label-Property combinations. We also want our users to pick Attributes they have and Attributes they want in a potential mate. To keep things keep clean and help the matching, we will seed the database with some Attributes and not let the users create them dynamically. However they need to be able to find and search for these Attributes, so we will index their names. Well, we will index a lowercase version of their names since the current Neo4j schema indexes are CaSe SeNsItIve. So our schema endpoint could start like this:

 
@Path("/schema")
public class Schema {

        private static final ObjectMapper objectMapper = new ObjectMapper();

        @POST
        @Path("/create")
        public Response create(@Context GraphDatabaseService db) throws IOException {
            ArrayList<String> results = new ArrayList<>();
            try (Transaction tx = db.beginTx()) {
                org.neo4j.graphdb.schema.Schema schema = db.schema();
                if (!schema.getConstraints(Labels.Attribute).iterator().hasNext()) {
                    schema.constraintFor(Labels.Attribute)
                            .assertPropertyIsUnique(LOWERCASE_NAME)
                            .create();

                    tx.success();
                    results.add("(:Attribute {lowercase_name}) constraint created");
                }
            }

            try (Transaction tx = db.beginTx()) {
                org.neo4j.graphdb.schema.Schema schema = db.schema();
                if (!schema.getConstraints(Labels.User).iterator().hasNext()) {
                    schema.constraintFor(Labels.User)
                            .assertPropertyIsUnique(USERNAME)
                            .create();
                    schema.constraintFor(Labels.User)
                            .assertPropertyIsUnique(EMAIL)
                            .create();
                    tx.success();
                    results.add("(:User {username}) constraint created");
                    results.add("(:User {email}) constraint created");
                }
            }
            results.add("Schema Created");
            return Response.ok().entity(objectMapper.writeValueAsString(results)).build();
        }
}

Next, we need to be able to create Users and then fetch those users. So how about we add that so our API looks like:

 
:POST   /v1/schema/create
:GET    /v1/users/{username}
:POST   /v1/users

We will create a Users class with an ObjectMapper to convert our results to JSON at the end of each method. Our first end point gets users, so we need to pass in a username and use the Graph Database Service in the context of our query. Inside of a transaction we will create a Node object by calling another method to find our user and then return all the properties of the user.

 
@Path("/users")
public class Users {

    private static final ObjectMapper objectMapper = new ObjectMapper();

    @GET
    @Path("/{username}")
    public Response getUser(@PathParam("username") final String username, @Context GraphDatabaseService db) throws IOException {
        Map<String, Object> results;
        try (Transaction tx = db.beginTx()) {
            Node user = findUser(username, db);
            results = user.getAllProperties();
            tx.success();
        }
        return Response.ok().entity(objectMapper.writeValueAsString(results)).build();
    }

Our findUser method is going to be used a ton I imagine so that’s why we are breaking it out. If it finds the user in he database it returns that user otherwise it errors out which means our end point will return an http 400 error. Notice we make sure to use the lowercase version of the username.

 
    public static Node findUser(String username, @Context GraphDatabaseService db) {
        if (username == null) { return null; }
        Node user = db.findNode(Labels.User, USERNAME, username.toLowerCase());
        if (user == null) { throw UserExceptions.userNotFound; }
        return user;
    }

We want to be good developers so we will write a little test for our method as well. I love the way Neo4j lets you create an in memory throw away instance for quick and easy testing. We will use the Neo4jRule class to create our temporary instance with a node that already exits as our Fixture and reference our extension Users class:

 
public class GetUserTest {
    @Rule
    public Neo4jRule neo4j = new Neo4jRule()
            .withFixture(FIXTURE)
            .withExtension("/v1", Users.class);

    private static final String FIXTURE =
            "CREATE (max:User {username:'maxdemarzi', " +
                    "email: 'maxdemarzi@hotmail.com', " +
                    "name: 'Max De Marzi'," +
                    "password: 'swordfish'})";

    private static final HashMap expected = new HashMap<String, Object>() {{
        put("username", "maxdemarzi");
        put("email", "maxdemarzi@hotmail.com");
        put("name", "Max De Marzi");
        put("password", "swordfish");
    }};

The test itself, creates the schema and requests the user, asserting that we get the result we were expecting.

 
    @Test
    public void shouldGetUser() {
        HTTP.POST(neo4j.httpURI().resolve("/v1/schema/create").toString());

        HTTP.Response response = HTTP.GET(neo4j.httpURI().resolve("/v1/users/maxdemarzi").toString());
        HashMap actual  = response.content();
        Assert.assertEquals(expected, actual);
    }

Now we need an endpoint to create a user. We’re going to make sure we don’t create a user with the same username or email as an existing user, so we’ll check those and if we’re good we’ll set the node properties. We’re getting a little ahead of ourselves here because we are expecting a bunch of properties we haven’t talked about yet. But these will be required for the dating site to work. For example we need to know what someone is looking for and well as what they are gender wise. We also want to know where they live and how far they are willing to look for love, so we can match them with people in their dating area. We may need to tweak this later, but it’s a good start.

 
    @POST
    public Response createUser(String body, @Context GraphDatabaseService db) throws IOException {
        HashMap parameters = UserValidator.validate(body);
        Map<String, Object> results;
        String username = ((String)parameters.get(USERNAME)).toLowerCase();
        String email = ((String)parameters.get(EMAIL)).toLowerCase();
        try (Transaction tx = db.beginTx()) {
            Node user = db.findNode(Labels.User, USERNAME, username);
            if (user == null) {
                user = db.findNode(Labels.User, EMAIL, email);
                if (user == null) {
                    user = db.createNode(Labels.User);
                    user.setProperty(EMAIL, email);
                    user.setProperty(NAME, parameters.get(NAME));
                    user.setProperty(BIO, parameters.get(BIO));
                    user.setProperty(USERNAME, username);
                    user.setProperty(PASSWORD, parameters.get(PASSWORD));
                    user.setProperty(IS, parameters.get(IS));
                    user.setProperty(IS_LOOKING_FOR, parameters.get(IS_LOOKING_FOR));
                    user.setProperty(HASH, new Md5Hash(email).toString());
                    user.setProperty(TIME, ZonedDateTime.now(utc));
                    user.setProperty(DISTANCE, parameters.get(DISTANCE));

                    Node city = db.findNode(Labels.City, FULL_NAME, parameters.get(CITY));
                    user.createRelationshipTo(city, RelationshipTypes.IN_LOCATION);

                    results = user.getAllProperties();
                } else {
                    throw UserExceptions.existingEmailParameter;
                }
            } else {
                throw UserExceptions.existingUsernameParameter;
            }
            tx.success();
        }
        return Response.ok().entity(objectMapper.writeValueAsString(results)).build();
    }

You may have noticed I used a UserValidator class to check the input. This class basically does this to all the input parameters and errors out if we run into invalid data:

 
        if (!input.containsKey(USERNAME)) {
            throw UserExceptions.missingUsernameParameter;
        } else {
            String username = (String)input.get(USERNAME);
            if (username.equals("")) {
                throw UserExceptions.emptyUsernameParameter;
            } else if (!username.matches(usernamePattern)) {
                throw UserExceptions.invalidUsernameParameter;
            }
        }

I’ll spare you the test code for createUser but it’s in the repository if you want to see it.

At this point we have a schema and we can create and retrieve users, our dating site is not super useful yet, but stay tuned and we will add more functionality in the next part.

Tagged , , , , , , , ,

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: