Introducing RedwoodRecord

So a different issue that might be from the vite upgrade.

I’m getting this error when trying to load a specific page of my app.

The requested module '/@fs/Users/chris/code/leanbod/api/src/models/datamodel.js' does not provide an export named 'default'

That comes up in the RW ErrorBoundary page and it’s originating from the models/index.js import of data model.

The bigger question though, is why is my frontend trying to import my redwood models? Shouldn’t they be API only?

I can navigate most pages, the one blowing up is a “show” page coming from an “index” page.

When I’m on the index page the Chrome “Sources” inspector tab shows all the code loaded, and there’s no models or anything as expected.

As soon as I click to navigate to the show page I see api/src show up with the full contents of api/src/lib and api/src/models

I’ll debug this and see if I can find something, it’s a blocker for me atm.

Nevermind, found my issue, was trying to cheat a bit and re-use a pure function I had on the API side… lol

1 Like

The basic version of RedwoodRecord is really slick but I think until it can handle validations and life cycles it’s still a bit far from ready. I was reading and thinking about how you all were thinking about implementing these features but I think the current path may be lacking a key advantage of ActiveRecord. Looking at this example:

export default class User extends RedwoodRecord {
  static afterCreate = async (user) => {
    await user.preferences.create({ email: 'weekly' })
  }
}

I think it’s missing the capability to skip or specify life cycle situations. From the AR docs:

class User < ApplicationRecord
  before_validation :normalize_name, on: :create

  # :on takes an array as well
  after_validation :set_location, on: [ :create, :update ]

  private
    def normalize_name
      self.name = name.downcase.titleize
    end

    def set_location
      self.location = LocationService.query(self)
    end
end

Attaching a validation hook to just create doesn’t seem like it would be possible unless there are going to be life cycle hooks for all the different situations that can occur. Another example is skipping a life cycle on a condition.

I’m sure some of the thinking is to put it into the function handler but what if instead, the definitions for life cycles leveraged HOCs. For example, using the example shared:

export default class User extends RedwoodRecord {
  static afterCreate = (config) => async (user) => {
    await user.preferences.create({ email: 'weekly' })
  }
}

where config could hold options like: { if: conditionalFunc(), on: [CREATE] } as an example. Maybe this syntax isn’t as friendly but something where we could pass these options which would allow handler functions to remain small and testable but enable life cycle hooks to be rich and powerful.

Update: Ah! I just realized my syntax on my idea is off but the idea holds. The point being make life cycle functions composables where you can pass the config and handler separately.

1 Like

Yeah I’d love it if RR had all of these features! I haven’t had a chance to extend it further, but duplicating the functionality of ActiveRecord in JS was always the ultimate goal.

But, I’ve had this worry in the back of my mind that RR can only get so far without Ruby’s metaprogramming abilities, and we’ll end up coding ourselves into a corner and then RR reaches some hard limit that we can’t get around. You end up having to write raw Prisma code again in your services, but now it’s mixed together with RR, and in the long run that might end up being worse from a maintainability standpoint than if you had just stuck with Prisma all along.

I don’t have any real evidence that this is what will happen, just a nebulous feeling in my gut. That, and the thought of trying to learn relational algebra so that all of SQL can be expressed in RR (to mimic the Arel syntax of ActiveRecord) is terrifying to me. Seeing this part of the wikipedia article made me feel like I was driving too fast over a hill in a car:

There’s a reason I didn’t pursue a computer science degree… :grimacing:

1 Like

I think your gut is spot on. I can’t tell you exactly what’s going to go wrong, but I agree that without Ruby’s full metaprogramming, this may be a pipe dream to get the full AR capabilities. I think where the implementation is though is pretty close to opening the gateway to some of this functionality. I feel like most of the relational algebra is just to settle the some of the more complex queries to resolve such as N-to-N relations.

I think some of the other complications are the fact that operations like create v. save could be differently implemented as create is technically static while save is technically an instance method. Maybe conceptually something like this could work but then you have the problem that someone is overriding it.

class RedwoodRecord {
  
  static validations = [];

  static create = async (recordInstance) => {
    this.validations.forEach((validation) => {
      this.call(validation, recordInstance);
    })
    // default create code
  }

  static update = async (recordInstance) => {
    this.validations.forEach((validation) => {
      this.call(validation, recordInstance);
    })
    // defaulte update code
  }

  async save() {
    await RedwoodRecord.update(this);
  }
}

class MyRecord extends RedwoodRecord {
  static validations = ['validator1'];

  static validator1 = async (instance) => {
    // do validation
  }
}

I think examples like this are why I’m thinking for the JS version of AR it needs to leverage composition instead of metaprogramming similar to:

class RedwoodRecord {
  
  static validations = [];

  static create = (customCreateFn) => async (recordInstance) => {
    this.validations.forEach((validation) => {
      this.call(validation, recordInstance);
    })
    if (customCreateFn) {
      customCreateFn(recordInstance)
    } else {
      // default create
    }
  }

  async save() {
    await RedwoodRecord.update(this);
  }
}

class MyRecord extends RedwoodRecord {
  static validations = ['validator1'];

  static validator1 = async (instance) => {
    // do validation
  }

  static create = () => {
    super(() => {
      // my custom implementation
    })
  }
}

but again - this leaves much to be desired and definitely highlights some of your key concerns around long term maintainability. I’ll keep pondering and see if I can come up with something better to recommend - just trying to help get this feature moving in that direction. Again - love the work - I’m just hungry for more and need to stop being so greedy :joy: