cerebris / jsonapi-resources

A resource-focused Rails library for developing JSON:API compliant servers.
http://jsonapi-resources.com
MIT License
2.32k stars 528 forks source link

Public key #434

Open lgebhardt opened 8 years ago

lgebhardt commented 8 years ago

Have the ability to have a public key, which is used by the resources with the underlying model using a different primary key. For example internally the models use integer primary keys and externally the resources use uuids.

Initially this project was trying to make the primary_key option perform both roles, which was problematic. I think it might be possible however to make this feature work with a public_key option on a Resource.

This has been requested several times, and recently #427 brought this up again.

beechnut commented 8 years ago

I have a use case that might warrant a slightly different approach to the public id.

My records are stored in a Microsoft SQL Server database. The ids are in UUID format, and the integer primary key you see in most other databases is a field called new_count. For various reasons, new_count cannot be the primary key. Before using JR, in order to create a friendly URL that I could also parse in a controller, I had:

class Project < ActiveRecord::Base
  def to_param
    "#{count}--#{name.to_s.parameterize}"
  end
end

Project.find_by(new_count: 430).to_param
# => "430--improving-the-youth-summer-jobs-program"

In the controller, I would then parse the friendly URL and look it up using the count.

class ProjectsController < ActionController::Base
  def show
    count, _sep, name = params[:id].partition('--')
    # using above example, count: 430, _sep: '--', name: 'improving-the-youth-summer-jobs-program'
    @project = Project.find_by(new_count: count)
  end
end

I might suggest a slightly different approach to this issue, which is, to implicitly use #to_param as the primary key instead of #id, and then to allow overriding the controller's find operation for a given action, in case the developer has to do anything to parse the URL or call a specific method.

In the end, I would like to write code that overrides the way records are looked up in the #show action. I imagine it looking something like this.

class ProjectResource < JSONAPI::Resource
  # This could also be done implicitly in every resource,
  # as most resources would simply return the record's primary key.
  def id
    to_param
  end
end

class ProjectsController < JSONAPI::ResourceController
  find_operation :show do
    count, _, name = params[:id].partition('--')
    # Then either returning the ActiveRecord Relation directly
    Project.find_by(count: new_count)
    # OR returning a hash that will be used in `#find_by`
    { count: new_count }
  end
  # or
  def show
    find_operation do
      # parse & find logic from above
    end
    super
  end
end

Thoughts? If we come to an agreement on an approach, I'm happy to try to implement this.

lgebhardt commented 8 years ago

Have you looked at overriding the resource's find_by_key method instead of doing the logic in the controller?

jsmestad commented 8 years ago

@lgebhardt I'm interested in this and willing to do the changes necessary if you have some guidance on how you'd like to approach solving this

dvogel commented 8 years ago

@lgebhardt could you elaborate on the problems caused by making the resource's #_primary_key method name a field other than the actual primary key? I'm interested in helping support the public key feature but it would be nice to know the problem you ran into.

yourivdlans commented 8 years ago

I'm also interested in the public_key approach.

Right now I have done to following to make this work:

class NewsResource < JSONAPI::Resource
  immutable

  key_type :uuid

  attributes :uuid, :title, :content

  def id
    uuid
  end

  def self.find_by_key(key, options = {})
    context = options[:context]
    records = records(options)
    records = apply_includes(records, options)
    model = records.find_by(uuid: key)
    fail JSONAPI::Exceptions::RecordNotFound, key if model.nil?
    resource_for_model(model).new(model, context)
  end
end
asadakbarml commented 2 years ago

@lgebhardt Is there an official way currently to handle the resource using a different attribute that the models primary key as the public key? I saw that https://github.com/cerebris/jsonapi-resources/pull/427 was closed but don't understand the point in the final comment https://github.com/cerebris/jsonapi-resources/pull/427#issuecomment-468480847

Based on the fact that support for other types of models has progressed

SingleShot commented 11 months ago

Resurrecting this thread. I too would like to be able define an attribute to be the key used for looking up a model. I don't like leaking the internal database id out into the API, and many of our resources have naturally unique identifiers that are not the database primary key.

An example might be a country model where the database primary key is an integer, but the natural key is a country code string. I would like to be able to do something like this:

class CountryResource < JSONAPI::Resource
  public_key :code
  key_type :string
  attributes :code, :name
end

Maybe the primary key is 17 and the code is uk, I would be able to do this:

GET /countries/uk

{
  "id": "uk",
  "type": "countries",
  "attributes": {
    "code": "uk",
    "name": "United Kingdom"
  }
}
SingleShot commented 11 months ago

Update - it does seem I can get what I want with the following:

class CountryResource < JSONAPI::Resource
  key_type :string
  attributes :code, :name

  key_type :string

  def id
    @model.code
  end

  def self._primary_key
    :code
  end
end

I didn't test a ton but it seems to work with the include param as well.

So really, the idea of a public_key setting would be to DRY up the above.