Introducing RedwoodRecord

Several weeks ago I had a vision. A vision of easier access to data. Simpler syntax to access my database. Today, that vision becomes reality.

With the release of 0.39/1.0.0-rc.1 we’re including an experimental package called RedwoodRecord. It’s modeled off of ActiveRecord from Ruby on Rails. It’s an ORM that wraps Prisma and provides class-based access to your data. We’re calling it “experimental” because we don’t really know how the community is going to react to it, and the API isn’t fully baked. We’re trying some things out and need feedback to see where we should go with this library.

You can check out the official docs here, but here’s a quick guide to getting started.

First, add to your package.json:

yarn add -W api @redwoodjs/record

Next, generate a “model” class which will represent a single table in your database:

yarn rw g model User

(Take a look at api/src/models/index.js to see what was added.)

You now have a model at api/src/models/User.js. Before we can start using it, we need to run one more command which will turn your schema.prisma file into a cached JSON file, and create the index.js file in the models directory, adding a bit of config for the models:

yarn rw record init

For now you’ll need to run this command any time you change your schema, or after creating new models.

Now, you can update your users service to use your model instead of raw Prisma:

// api/src/services/users/users.js

import { User } from 'src/models'

export const users = () => {
  return User.all()
}

export const user = ({ id }) => {
  return User.find(id)
}

export const createUser = async ({ input }) => {
  return User.create(input, { throw: true })
}

export const updateUser = async ({ id, input }) => {
  const user = await User.find(id)
  return user.update(input, { throw: true })
}

export const deleteUser = async ({ id }) => {
  const user = await User.find(id)
  return user.destroy({ throw: true })
}

Since models are just classes, you can add your own functions/properties to the class and they’ll be available anywhere a user is. Let’s say you store firstName and lastName separately in your database, but you also want to display a fullName. Rather than write a function somewhere else and import it when needed, just add it to User:

// api/src/models/User.js

import { RedwoodRecord } from '@redwoodjs/record'

export default class User extends RedwoodRecord {
  get fullName() {
    return `${this.firstName} ${this.lastName}`
  }
}

And now whenever you want someone’s full name:

const user = User.find(123)
user.fullName 

You can also replace the object returned in getCurrentUser to be an instance of your model (dbAuth is used in this example, so session is just an object containing the id of the logged in user):

// api/src/lib/auth.js

export const getCurrentUser = async (session) => {
  return await User.find(session.id, { select: { id: true, email: true } })
}

Now, currentUser on the api side will be an instance of RedwoodRecord, so you can get to related models directly through your user. Here, we’ve also created a Post model and now we can get only those posts that the current user owns:

// api/src/services/posts/posts.js

export const posts = async () => {
  return context.currentUser.posts.all()
}

export const post = ({ id }) => {
  return context.currentUser.posts.find(id)
}

export const createPost = ({ input }) => {
  return context.currentUser.posts.create(input, { throw: true })
}

export const updatePost = async ({ id, input }) => {
  const post = await context.currentUser.posts.find(id)
  return post.update(input, { throw: true })
}

export const deletePost = async ({ id }) => {
  const post = await context.currentUser.posts.find(id)
  return post.destroy({ throw: true })
}

Start playing with it and let us know what you think! And check out the full docs for more syntax: Docs - RedwoodRecord : RedwoodJS Docs

Caveats

RedwoodRecord is not currently typed. The attributes on a model are created dynamically based on the object returned by Prisma, so I’m not even sure if it can be typed in its current iteration. If you have any insight into helping add Typescript to this library, get in touch!

8 Likes

First the framework, then dbAuth, now this :exploding_head: Thank you Rob, looking forward to getting to try this out!

(I should have bit the bullet and learned Rails, I had no idea it had so many neat things up its sleeves)

4 Likes

Looks neat, like prisma with training wheels. Is the goal to improve developer experience, by getting them working with data sooner in their journey?

  1. How quickly will developers out-grow RR, or is it intended to be a full-featured API?
  2. Will it feel like helpful “training wheels”, or a distraction from just learning prisma.
  3. Every time we diverge from a single path, like we did with the database, it creates a lot of confusion for newcomers. Is it possible to avoid multiple paths, by making RR the official Redwood Way ™?
1 Like

Yeah assuming this doesn’t turn out to be a dead end we’d probably make it the default interface to accessing the database, and then you can drop down to Prisma if there’s something you can’t accomplish through this interface. If it became as full-featured as ActiveRecord is in Rails then you could do 99% of anything you need to do through here. Time will tell.

It’s “experimental” so we’re not promoting it anywhere to avoid confusing newcomers. The tutorial and all other docs (besides the RR doc itself) only discuss Prisma.

How/why could this become a deadend? SQL itself is based on relational algebra, and I know absolutely nothing about the theoretical mathematics behind it. For example, turning these equations into Javscript:

:man_shrugging:

So I worry that I can only get so far in this implementation before not having a Ph.D in computer science will become a problem.

Not many people even dip down to this level, but the gem behind the database queries in ActiveRecord is arel, which is a layer between the familiar AR interface and whatever connector you’re using to actually talk to the database: File: README — Documentation for arel (9.0.0) THAT is where the relational algebra stuff is expressed into chainable functions and it twists my mind into a pretzel.

4 Likes

:eyes: .

3 Likes

I worked with Rails for a few years and I love Active Record. Should I incorporate RedwoodRecord into the app I’m working on now? Or, is this going to fade away due to lack of interest. I think you might see that Rails developers would find this as a very nice and familiar feature. Frankly, I’m still trying to see the benefit of graphql. I’m relatively new to React and Web3.

1 Like

We don’t have any plans to remove it! It’s pretty self-contained in the codebase, so even if no one other than me used it, we would keep it around. :wink: I’m just not sure how much active development it’s going to get if most people are happy wrangling the database through Prisma directly. Seems a little too low-level for productive app work if you ask me, but ignorance is bliss: these people don’t know what they’re missing!

We haven’t really made a concerted effort to promote it outside of the forums here. I’d like to do an introductory video or webcast to really show folks what it can do. But I don’t know that it’ll become as full-featured as Rails ActiveRecord unless someone that really knows the ins and outs of the theories behind relational algebra takes a crack at it.

Ok. I’ll give it a try and integrate it into my app. I’ll make 2 or 3 models and see how it works. I’ll let you know what I think.
J

2 Likes

This feature sounds neat. I tried using it because it liked the way it handles the validations insead of doing them directly on the service mainly because it groups the validation errors per fieldName so when I get those errors in the web I can easily show them on the corresponding field.

To contrast this, the current validate method used in the services validates only one field at a time and returns a promise that is rejected if the validation throws errors, this means that in order to return the validations for all fields the same way the RedwoodRecord does you need to wrap each validate in it’s own try catch and merge the errors at the end to throw a single error object.

Most errors should have been caught by the client side validations for a better user experience and de backend validators should be more of a safe-guard so maybe returning the errors for the first field that returns errors only might be fine but still the RedwoodRecord gives us a better dev experience.

The issue I am having right now is that the recods lib is in JS and I am using typescript for my project so even though my record extends RedwoodRecord, it says that for example build is not a function of my record because RedwoodRecord itself is not typed.

Would be really awesome if it was typed. Completely understand that it should probably not be a priority right now since v1.0 is getting ready to be released.

I think going forward this feature is sth that is going to be useful so :+1:t2: from me to continue the development of it. I might be initerested in lending a hand with the TS conversion if that is a thing we want to move forward with.

2 Likes

Thanks, and welcome! Sorry about the lack of TS…I hate it with the fiery passion of a thousand suns, but most people seem to like it! Going in I had no idea if they even could be typed (properties are created on the fly using defineProperty based on what comes back from the database). But @orta theorized that it could be typed doing something like this: typescript.org (I have no idea what any of that code means)

If you wanted to try implementing that we’d be forever grateful! We do have a persistent running type generation service now, so maybe something could be hooked into that which would generate types on the fly if/when Prisma models change? That thing is here: https://github.com/redwoodjs/redwood/blob/main/packages/internal/src/generate/watch.ts

My only requirement is that if I generate a JS project, I don’t want to see any remnants of changes that had to be made to satisfy some arbitrary TS requirement. I like the interface and generated code just the way it is, thank you very much! :slight_smile:

1 Like

Ok. I set it up in a new test project. And. It works! I like it and will use it in my next project. Thank you very much!
John