{"id":17316496,"url":"https://github.com/pauljuliusmartinez/sequel-packer","last_synced_at":"2025-04-14T14:21:56.127Z","repository":{"id":56895027,"uuid":"262398881","full_name":"PaulJuliusMartinez/sequel-packer","owner":"PaulJuliusMartinez","description":"A Ruby serialization library for use with the Sequel ORM.","archived":false,"fork":false,"pushed_at":"2023-05-12T03:02:16.000Z","size":138,"stargazers_count":16,"open_issues_count":0,"forks_count":1,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-04-14T14:21:46.166Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/PaulJuliusMartinez.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE.txt","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null}},"created_at":"2020-05-08T18:27:52.000Z","updated_at":"2023-05-12T03:01:31.000Z","dependencies_parsed_at":"2024-01-13T20:56:24.503Z","dependency_job_id":"aa942b33-3a8f-4000-8efa-3862ee9942de","html_url":"https://github.com/PaulJuliusMartinez/sequel-packer","commit_stats":null,"previous_names":[],"tags_count":10,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/PaulJuliusMartinez%2Fsequel-packer","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/PaulJuliusMartinez%2Fsequel-packer/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/PaulJuliusMartinez%2Fsequel-packer/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/PaulJuliusMartinez%2Fsequel-packer/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/PaulJuliusMartinez","download_url":"https://codeload.github.com/PaulJuliusMartinez/sequel-packer/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248894949,"owners_count":21179154,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-10-15T13:13:12.547Z","updated_at":"2025-04-14T14:21:56.091Z","avatar_url":"https://github.com/PaulJuliusMartinez.png","language":"Ruby","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Sequel::Packer\n\n`Sequel::Packer` is a Ruby serialization library to be used with the [Sequel\nORM](https://github.com/jeremyevans/sequel) with the following qualities:\n\n* **Declarative:** Define the shape of your serialized data with a simple,\n  straightforward DSL.\n* **Flexible:** Certain contexts require different data. Packers provide an easy\n  way to opt-in to serializing certain data only when you need it. The library\n  also provides convenient escape hatches when you need to do something not\n  explicitly supported by the API.\n* **Reusable:** The Packer library naturally composes well with itself. Nested\n  data can be serialized in the same way no matter what endpoint it's fetched\n  from.\n* **Efficient:** When not using Sequel's\n  [`TacticalEagerLoading`](https://sequel.jeremyevans.net/rdoc-plugins/classes/Sequel/Plugins/TacticalEagerLoading.html)\n  plugin, the Packer library will intelligently determine which associations\n  and nested associations it needs to eager load in order to avoid any N+1 query\n  issues.\n\n## Example\n\n`Sequel::Packer` uses your existing `Sequel::Model` declarations and leverages\nthe use of associations to efficiently serialize data.\n\n```ruby\nclass User \u003c Sequel::Model(:users)\n  one_to_many :posts\nend\nclass Post \u003c Sequel::Model(:posts); end\n```\n\nPacker definitions use a simple domain-specific language (DSL) to declare which\nfields to serialize:\n\n```ruby\nclass PostPacker \u003c Sequel::Packer\n  model Post\n\n  field :id\n  field :title\n\n  trait :truncated_content do\n    field :truncated_content do |post|\n      post.content[0..Post::PREVIEW_LENGTH]\n    end\n  end\nend\n\nclass UserPacker \u003c Sequel::Packer\n  model User\n\n  field :id\n  field :name\n\n  trait :posts do\n    field :posts, PostPacker, :truncated_content\n  end\nend\n```\n\nOnce defined, Packers are easy to use; just call `.pack` and pass in a Sequel\ndataset, an array of models, or a single model, and get back Ruby hashes.\nFrom there you can simply call `to_json` on the result!\n\n```ruby\nUserPacker.pack(User.dataset)\n=\u003e [\n  {id: 1, name: 'Paul'},\n  {id: 2, name: 'Julius'},\n  ...\n]\n\nUserPacker.pack(User[1], :posts)\n=\u003e {\n  id: 1,\n  name: 'Paul',\n  posts: [\n    {\n      id: 15,\n      title: 'Announcing Sequel::Packer!',\n      truncated_content: 'Sequel::Packer is a new gem...',\n    },\n    {\n      id: 21,\n      title: 'Postgres Internals',\n      truncated_content: 'I never quite understood autovacuum...',\n    },\n    ...\n  ],\n}\n```\n\n## Contents\n\n- [Example](#example)\n- [Getting Started](#getting-started)\n  - [Installation](#installation)\n  - [Example Schema](#example-schema)\n  - [Basic Fields](#basic-fields)\n  - [Packing Associations by Nesting Packers](#packing-associations-by-nesting-packers)\n  - [Traits](#traits)\n- [API Reference](#api-reference)\n  - [Using a Packer](#using-a-packer)\n  - [Defining a Packer](#defining-a-packer)\n    - [`self.model(sequel_model_class)`](#selfmodelsequel_model_class)\n    - [`self.field(column_name)` (or `self.field(method_name)`)](#selffieldcolumn_name-or-selffieldmethod_name)\n    - [`self.field(key, \u0026block)`](#selffieldkey-block)\n    - [`self.field(association, subpacker, *traits)`](#selffieldassociation-subpacker-traits)\n    - [`self.field(\u0026block)`](#selffieldblock)\n    - [`self.trait(trait_name, \u0026block)`](#selftraittrait_name-block)\n    - [`self.eager(*associations)`](#selfeagerassociations)\n    - [`self.set_association_packer(association, subpacker, *traits)`](#selfset_association_packerassociation-subpacker-traits)\n    - [`self.pack_association(association, models)`](#selfpack_associationassociation-models)\n    - [`self.precompute(\u0026block)`](#selfprecomputeblock)\n  - [Context](#context)\n    - [`self.with_context(\u0026block)`](#selfwith_contextblock)\n- [Potential Future Functionality](#potential-future-functionality)\n  - [Automatically Generated Type Declarations](#automatically-generated-type-declarations)\n  - [Lifecycle Hooks](#lifecycle-hooks)\n  - [Less Data Fetching](#less-data-fetching)\n  - [Other Enhancements](#other-enhancements)\n- [Contributing](#contributing)\n  - [Development](#development)\n  - [Releases](#releases)\n- [Attribution](#attribution)\n- [License](#license)\n\n## Getting Started\n\nThis section will explain the basic use of `Sequel::Packer`. Check out the [API\nReference](#api-reference) for an exhaustive coverage of the API and more\ndetailed documentation.\n\n### Installation\n\nAdd this line to your application's Gemfile:\n\n```ruby\ngem 'sequel-packer'\n```\n\nAnd then execute:\n\n    $ bundle install\n\nOr install it yourself as:\n\n    $ gem install sequel-packer\n\n### Example Schema\n\nMost of the following examples will use the following database schema:\n\n```ruby\nDB.create_table(:users) do\n  primary_key :id\n  String :name\nend\n\nDB.create_table(:posts) do\n  primary_key :id\n  foreign_key :author_id, :users\n  String :title\n  String :content\nend\n\nDB.create_table(:comments) do\n  primary_key :id\n  foreign_key :author_id, :users\n  foreign_key :post_id, :posts\n  String :content\nend\n\nclass User \u003c Sequel::Model(:users)\n  one_to_many :posts, key: :author_id, class: :Post\nend\nclass Post \u003c Sequel::Model(:posts)\n  one_to_many :comments, key: :post_id, class: :Comment\nend\nclass Comment \u003c Sequel::Model(:comments)\n  many_to_one :author, key: :author_id, class: :User\nend\n```\n\n### Basic Fields\n\nSuppose an endpoint wants to fetch all the ten most recent comments by a user.\nAfter validating the user id, we end up with the Sequel dataset representing the\ndata we want to return:\n\n```ruby\nrecent_comments = Comment\n  .where(author_id: user_id)\n  .order(:id.desc)\n  .limit(10)\n```\n\nWe can define a Packer class to serialize just fields we want to, using a\ncustom DSL:\n\n```ruby\nclass CommentPacker \u003c Sequel::Packer\n  model Comment\n\n  field :id\n  field :content\nend\n```\n\nThis can then be used as follows:\n\n```ruby\nCommentPacker.pack(recent_comments)\n=\u003e [\n  {id: 536, content: \"Great post, man!\"},\n  {id: 436, content: \"lol\"},\n  {id: 413, content: \"What a story...\"},\n]\n```\n\n### Packing Associations by Nesting Packers\n\nNow, suppose that we want to fetch a post and all of its comments. We can do\nthis by defining another packer for `Post` that uses the `CommentPacker`:\n\n```ruby\nclass PostPacker \u003c Sequel::Packer\n  model Post\n\n  field :id\n  field :title\n  field :content\n  field :comments, CommentPacker\nend\n```\n\nSince `post.comments` is an array of `Sequel::Models` and not a primitive value,\nwe must tell the Packer how to serialize them using another packer. The second\nargument in `field :comments, CommentPacker` tells the `PostPacker` to use the\npack those comments using the `CommentPacker`.\n\nWe can then use this as follows:\n\n```ruby\nPostPacker.pack(Post[validated_id])\n=\u003e [\n  {\n    id: 682,\n    title: \"Announcing sequel-packer\",\n    content: \"I've written a new gem...\",\n    comments: [\n      {id: 536, content: \"Great post, man!\"},\n      {id: 541, content: \"Incredible, this solves my EXACT problem!\"},\n      ...\n    ],\n  }\n]\n```\n\n### Traits\n\nBut suppose we want to be able to show who authored each comment on the post. We\nfirst have to define a packer for users:\n\n```ruby\nclass UserPacker \u003c Sequel::Packer\n  model User\n\n  field :id\n  field :name\nend\n```\n\nWe could now define a new packer, `CommentWithAuthorPacker`, and use that in the\n`PostPacker` instead, but then we'd have to redeclare all the other fields we\nwant on a packed `Comment`:\n\n```ruby\nclass CommentWithAuthorPacker \u003c Sequel::Packer\n  model Comment\n\n  field :author, UserPacker\n\n  # Also defined in CommentPacker!\n  field :id\n  field :content\nend\n\nclass PostPacker \u003c Sequel::Packer\n  ...\n\n  # Eww!\n- field :comments, CommentPacker\n+ field :comments, CommentWithAuthorPacker\nend\n```\n\nDeclaring these fields in two places could cause them to get out of sync\nas more fields are added. Instead, we will use a _trait_. A _trait_ is a\nway to define a set of fields that we only want to pack sometimes. Instead\nof defining a totally new packer, we can extend the `CommentPacker` as follows:\n\n```ruby\nclass CommentPacker \u003c Sequel::Packer\n  model Comment\n\n  field :id\n  field :content\n\n+ trait :author do\n+   field :author, UserPacker\n+ end\nend\n```\n\nTo use a trait, simply pass it in when calling `pack`:\n\n```ruby\n# Without the trait\nCommentPacker.pack(Comment.dataset)\n=\u003e [\n  {id: 536, content: \"Great post, man!\"},\n  ...\n]\n\n# With the trait\nCommentPacker.pack(Comment.dataset, :author)\n=\u003e [\n  {\n    id: 536,\n    content: \"Great post, man!\",\n    author: {id: 1, name: \"Paul Martinez\"},\n  },\n  ...\n]\n```\n\nTo use a trait when packing an association in another packer, simply include\nthe name of the trait as additional argument to `field`. Thus, to modify our\nPostPacker to pack comments with their authors we make the following change:\n\n```ruby\nclass PostPacker \u003c Sequel::Packer\n  model Post\n\n  field :id\n  field :title\n  field :content\n\n- field :comments, CommentPacker\n+ field :comments, CommentPacker, :author\nend\n```\n\nWhile the basic Packer DSL is convenient, traits are the things that make\nPackers so powerful. Each packer should define a small set of fields that every\nendpoint needs, but then traits can be used to pack additional data only when\nit's needed.\n\n## API Reference\n\nCustom packers are written by creating subclasses of `Sequel::Packer`. This\nclass defines a DSL for declaring how a Sequel Model will be converted into a\nplain Ruby hash.\n\n### Using a Packer\n\nUsing a Packer is dead simple. There's a single class method:\n\n```ruby\nself.pack(data, *traits, **context)\n```\n\n`data` can be in the form of a Sequel dataset, an array of Sequel models, or\na single Sequel model. No matter which form the data is passed in, the Packer\nclass will ensure nested data is efficiently loaded.\n\nTo pack additional fields defined in a trait, pass the name of the trait as an\nadditional argument, e.g., `UserPacker.pack(users, :recent_posts)` to include\nrecent posts with each user.\n\nFinally, additional context can be provided to the Packer by passing additional\nkeyword arguments to `pack`. This context is handled opaquely by the Packer, but\nit can be accessed in the blocks passed to `field` declarations. Common uses of\n`context` include passing in the current user making a request, or passing in\nadditional precomputed data.\n\nThe implementation of `pack` is very simple. It creates an instance of a Packer,\nby passing in the traits and the context, then calls `pack` on that instance,\nand passes in the data:\n\n```ruby\ndef self.pack(data, *traits, **context)\n  return nil if !data # small easy optimization to avoid unnecessary work\n  new(*traits, **context).pack(data)\nend\n```\n\nIt simply combines a constructor and single exposed instance method:\n\n#### `initialize(*traits, **context)`\n\n#### `pack(data)`\n\nOne instantiated, the same Packer could be used to pack data multiple times.\nThis is unlikely to be needed, but the functionality is there.\n\n### Defining a Packer\n\n#### `self.model(sequel_model_class)`\n\nThe beginning of each Packer class must begin with `model MySequelModel`, which\nspecifies which Sequel Model this Packer class will serialize. This is mostly\nto catch certain errors at load time, rather than at run time:\n\n```ruby\nclass UserPacker \u003c Sequel::Packer\n  model User\n  ...\nend\n```\n\n#### `self.field(column_name)` (or `self.field(method_name)`)\n\nDefining the shape of the outputted data is done using the `field` method, which\nexists in four different variants. This first variant is the simplest. It simply\nfetches the value of the column from the model and stores it in the outputted\nhash under a key of the same name. Essentially `field :my_column` eventually\nresults in `hash[:my_column] = model.my_column`.\n\nSequel Models define accessor methods for each column in the underlying table,\nso technically underneath the hood Packer is actually calling the sending the\nmethod `column_name` to the model: `hash[:my_column] = model.send(:my_column)`.\n\nThis means that the result of any method can be serialized using\n`field :method_name`. For example, suppose a User model has a `first_name` and\n`last_name` column, and a helper method `full_name`:\n\n```ruby\nclass User \u003c Sequel::Model(:users)\n  def full_name\n    \"#{first_name} #{last_name}\"\n  end\nend\n```\n\nThen when `User.create(first_name: \"Paul\", last_name: \"Martinez\")` gets packed\nwith `field :full_name` specified, the outputted hash will contain\n`full_name: \"Paul Martinez\"`.\n\n#### `self.field(key, \u0026block)`\n\nA block can be passed to `field` to perform arbitrary computation and store the\nresult under the specified `key`. The block will be passed the model as a single\nargument. Use this to call methods on the model that may take additional\narguments, or to \"rename\" a column.\n\nExamples:\n\n```ruby\nclass MyPacker \u003c Sequel::Packer\n  model MyModel\n\n  field :friendly_public_name do |model|\n    model.unfriendly_internal_name\n  end\n\n  # Shorthand for above\n  field :friendly_public_name, \u0026:unfriendly_internal_name\n\n  field :foo do |model|\n    model.bar(baz, quux)\n  end\nend\n```\n\n#### `self.field(association, subpacker, *traits)`\n\nA Sequel association (defined in the model file using `one_to_many`, or\n`many_to_one`, etc.), can be packed using another Packer class, possibly with\nmultiple traits specified. A similar output could be generated by doing:\n\n```ruby\nfield :association do |model|\n  subpacker.pack(model.association_dataset, *traits)\nend\n```\n\nThis form is very inefficient though, because it would result in a new subpacker\ngetting instantiated for every packed model. Additionally, unless the subpacker\nis declared up-front, the Packer won't know to eager load that association,\npotentially resulting in many unnecessary database queries.\n\n#### `self.field(\u0026block)`\n\nPassing a block but no `key` to `field` allows for arbitrary manipulation of the\npacked hash. The block will be passed the model and the partially packed hash.\nOne potential usage is for dynamic keys that cannot be determined at load time,\nbut otherwise it's meant as a general escape hatch.\n\n```ruby\nfield do |model, hash|\n  hash[model.compute_dynamic_key] = model.dynamic_value\nend\n```\n\n#### `self.trait(trait_name, \u0026block)`\n\nDefine optional serialization behavior by defining additional fields within a\n`trait` block. Traits can be opted into when initializing a packer by passing\nthe name of the trait as an argument:\n\n```ruby\nclass MyPacker \u003c Sequel::Packer\n  model MyObj\n  field :id\n\n  trait :my_trait do\n    field :trait_field\n  end\nend\n\n# packed objects don't have trait_field\nMyPacker.pack(dataset)\n=\u003e [{id: 1}, {id: 2}, ...]\n# packed objects do have trait_field\nMyPacker.pack(dataset, :my_trait)\n=\u003e [{id: 1, trait_field: 'foo'}, {id: 2, trait_field: 'bar'}, ...]\n```\n\nTraits can also be used when packing associations by passing the name of the\ntraits after the packer class:\n\n```ruby\nclass MyOtherPacker \u003c Sequel::Packer\n  model MyObj\n  field :my_packers, MyPacker, :my_trait\nend\n```\n\n#### `self.eager(*associations)`\n\nWhen packing an association, a Packer will automatically ensure that association\nis eager loaded, but there may be cases when an association will be accessed\nthat the Packer doesn't know about. In these cases you can tell the Packer to\neager load that data by calling `eager(*associations)`, passing in arguments\nthe exact same way you would to [`Sequel::Dataset.eager`](\nhttps://sequel.jeremyevans.net/rdoc/classes/Sequel/Model/Associations/DatasetMethods.html#method-i-eager).\n\nOne case where this may be useful is for a \"count\" field, that just lists the\nnumber of associated objects, but doesn't actually return them:\n\n```ruby\nclass UserPacker \u003c Sequel::Packer\n  model User\n\n  field :id\n\n  eager(:posts)\n  field(:num_posts) do |user|\n    user.posts.count\n  end\nend\n\nUserPacker.pack(User.dataset)\n=\u003e [\n  {id: 123, num_posts: 7},\n  {id: 456, num_posts: 3},\n  ...\n]\n```\n\nUsing `eager` can help prevent N+1 query problems when not using Sequel's\n[`TacticalEagerLoading`](https://sequel.jeremyevans.net/rdoc-plugins/classes/Sequel/Plugins/TacticalEagerLoading.html)\nplugin.\n\nAnother use of `eager`, even when using `TacticalEagerLoading`, is to modify or\nlimit which records gets fetched from the database by using an eager proc. For\nexample, to only pack recent posts, published in the past month, we might do:\n\n```\nclass UserPacker \u003c Sequel::Packer\n  model User\n\n  field :id\n\n  trait :recent_posts do\n    eager posts: (proc {|ds| ds.where {created_at \u003e Time.now - 1.month}})\n    field :posts, PostIdPacker\n  end\nend\n```\n\n**IMPORTANT NOTE:** Eager procs are not guaranteed to be executed when passing\nin models, rather than a dataset, to `pack`. Specifically, if the models already\nhave fetched the association, the Packer won't refetch it. Because of this, it's\ngood practice to use `set_association_packer` and `pack_association` (see next\nsection) in a `field` block and duplicate the filtering action.\n\nAlso keep in mind that this limits the association that gets used by ALL fields,\nso if another field actually needs access to all the users posts, it might not\nmake sense to use `eager`.\n\nAdditionally, it's important to note that if `eager` is called multiple times,\nwith multiple procs, each proc will get applied to the dataset, likely resulting\nin overly restrictive filtering.\n\n#### `self.set_association_packer(association, subpacker, *traits)`\n\nSee `self.pack_association(association, models)` below.\n\n#### `self.pack_association(association, models)`\n\nThe simplest way to pack an association is to use\n`self.field(association, subpacker, *traits)`, but sometimes this doesn't do\nexactly what we want. We may want to pack the association under a different key\nthan the name of the association. Or we may only want to pack some of the\nassociated models (and it may be difficult or impossible to express which subset\nwe want to pack using `eager`). Or perhaps we have a `one_to_many` association\nand instead of packing an array, we want to pack a single associated object\nunder a key. The two methods, `set_association_packer` and `pack_association`\nare designed to handle these cases.\n\nFirst, we'll note that following are exactly equivalent:\n\n```ruby\nfield :my_assoc, MyAssocPacker, :trait1, :trait2\n```\n\nand\n\n```ruby\nset_association_packer :my_assoc, MyAssocPacker, :trait1, :trait2\nfield :my_assoc do |model|\n  pack_association(:my_assoc, model.my_assoc)\nend\n```\n\n`set_association_packer` tells the Packer class that we will want to pack models\nfrom a particular association using the designated Packer with the specified\ntraits. Declaring this ahead of time allows the Packer to ensure that the\nassociation is eager loaded, as well as any nested associations used when using\nthe designated Packer with the specified traits.\n\n`pack_association` can then be used in a `field` block to use that Packer after\nthe data has been fetched and we are actually packing the data. The key things\nhere are that we don't need to use the name of the association as the name of\nthe field, and that we can choose which models get serialized. If\n`pack_association` is passed an array, it will return an array of packed models,\nbut if it is passed a single model, it will return just that packed model.\n\nExamples:\n\n##### Use a different field name than the name of the association\n```ruby\nset_association_packer :ugly_internal_names, InternalPacker\nfield :nice_external_names do |model|\n  pack_association(:ugly_internal_names, model.ugly_internal_names)\nend\n```\n\n##### Pack a single instance of a `one_to_many` association\n```ruby\nclass PostPacker \u003c Sequel::Packer\n  set_association_packer :comments, CommentPacker\n  field :top_comment do |model|\n    pack_association(:comments, model.comments.max_by(\u0026:num_likes))\n  end\nend\n```\n\n#### `self.precompute(\u0026block)`\n\nOccasionally packing a model may require a computation that doesn't fit in with\nthe rest of the Packer paradigm. This may be a Sequel query that is particularly\ndifficult to express as an association, or even a call to an external service.\nIf such a computation can be performed in bulk, then the `precompute` method can\nbe used as an entry point for that operation.\n\nThe `precompute` method will execute a given block and pass it all of the models\nthat will be packed using that packer. This block will be executed a single\ntime, even when called by a deeply nested packer.\n\nThe `precompute` block is `instance_exec`ed in the context of the packer\ninstance, the result of any computation can be saved in a simple instance\nvariable (`@precomputed_result`) and later referenced inside the blocks that are\npassed to `field` methods.\n\nAs an example, suppose a video uploading platform performs additional video\nprocessing on every uploaded video and exposes the status of that processing as\na separate service over the network, rather than directly with the upload\nmetadata in the database. `precompute` could be used as follows:\n\n```ruby\nclass VideoUploadPacker \u003c Sequel::Packer\n  model VideoUpload\n\n  precompute do |video_uploads|\n    @processing_statuses = ResolutionService\n      .get_status_bulk(ids: video_uploads.map(\u0026:id))\n  end\n\n  field :id\n  field :filename\n  field :processing_status do |video_upload|\n    @processing_statuses[video_upload.id]\n  end\nend\n```\n\n#### Instance method versions\n\nIn addition to the class method versions of `field`, `eager`,\n`set_association_packer`, and `precompute`, there are also regular instance method\nversions which take the exact same arguments. When writing a `trait` block, the\nblock is evaulated in the context of a new Packer instance and actually calls the\ninstance method versions instead.\n\n### Context\n\nIn addition to the data to be packed, and a set of traits, the `pack` method\nalso accepts arbitrary keyword arguments. This is referred to as `context` is\nhandled opaquely by the Packer. The data passed in here is saved as the\n`@context` instance variable, which is then accessible from within the blocks\npassed to `field`, `trait`, and `precompute`, for whatever purpose. It is also\nautomatically passed to any nested subpackers.\n\nThe most common usage for context would be to pass in the current user making\na request. It could then be used to pack permission levels about records, for\nexample.\n\n```ruby\nclass PostPacker \u003c Sequel::Packer\n  model Post\n\n  eager :permissions\n  field :access_level do |post|\n    user_permission = post.permissions.find do |perm|\n      perm.user_id == @context[:user].id\n    end\n\n    user_permission.access_level\n  end\nend\n```\n\nYou might notice something inefficient about the above code. Even though we only\nwant to look at the user's permission record, we fetch ALL of the permission\nrecords for each Post. Ideally we would filter the `permissions` association\ndataset when we call `eager`, but we don't have access to `@context` at that\npoint. This leads to the final DSL method available when writing a Packer:\n\n#### `self.with_context(\u0026block)`\n\nYou can pass a block to `with_context` that will be executed as soon as a Packer\ninstance is constructed. The block can access `@context` and can also call the\nstandard Packer DSL methods, `field`, `eager`, etc.\n\nThe above example could then be made more efficient as follows:\n\n```ruby\nclass PostPacker \u003c Sequel::Packer\n  model Post\n\n- eager :permissions\n+ with_context do\n+   eager permissions: (proc {|ds| ds.where(user_id: @context[:user].id)})\n+ end\nend\n```\n\nA very tricky usage of `with_context` (and not recommended...) would be to\ncontrol the traits used on subpackers:\n\n```ruby\nclass UserPacker \u003c Sequel::Packer\n  model User\n\n  with_context do\n    field :comments, CommentPacker, *@context[:comment_traits]\n  end\nend\n\nUserPacker.pack(User.dataset, comment_traits: [])\n=\u003e [{comments: [{id: 7}, ...]}]\nUserPacker.pack(User.dataset, comment_traits: [:author])\n=\u003e [{comments: [{id: 7, author: {id: 1, ...}}, ...]}]\nUserPacker.pack(User.dataset, comment_traits: [:num_likes])\n=\u003e [{comments: [{id: 7, likes: 53}, ...]}]\n```\n\n## Potential Future Functionality\n\nThe 1.0.0 version of the Packer library is flexible to support many use cases.\nThat said, of course there are ways to improve it! There are three main\nimprovements I can imagine adding:\n\n### Automatically Generated Type Declarations\n\nIt would be fairly easy to add generate type definitions by adding arguments to\n`field`. Packers could produce [TypeScript\ninterface](https://www.typescriptlang.org/docs/handbook/interfaces.html)\ndeclarations, and adding a simple build step to a CI pipeline could enforce type\nsafety across the frontend and backend. Or they could produce\n[OpenAPI](http://spec.openapis.org/oas/v3.0.3) specifications, which could then\nbe used to automatically generate clients using something like\n[Swagger](https://swagger.io/).\n\n### Lifecycle Hooks\n\nIt should be fairly easy to extend the Packer library using standard Ruby\nfeatures like subclassing, mixins, via `include`, or even monkey-patching. It\nmay be beneficial to have explicit hooks for common operations however, like\n`before_fetch`, or `around_pack`. It's more likely that these hooks are needed\nfor logging and tracing capabilities, than for actual functionality, so I'd\nlike to see some real-world usage before committing to a specific style of\nintegration.\n\n### Less Data Fetching\n\nSequel by default fetches every column in a table, but a Packer knows (roughly)\nwhat data is going to be used so it could only select the columns neede for\nactual serialization, and limit how much data is actually fetched from the\ndatabase. I haven't done any benchmarking on this, so I'm not sure how much of\na benefit could be gained by this, but it would be interesting!\n\nThis could work roughly as follows:\n\n* Start by fetching all columns that appear in simple `field(:column_name)`\n  declarations\n* Add any columns need to fetch nested associations, or to re-asscociate fetched\n  records with their \"parent\" models, using the `left_key` and `right_key`\n  fields of the `AssociationReflections`\n* Add a `column(*columns)` DSL method to explicitly fetch additional columns\n\n### Other Enhancements\n\nHere are some other potential enhancements, though these are less fleshed out.\n\n* Support not including a key in a hash if the associated value is nil, to\n  reduce size of outputted data.\n* Support different casing of the outputted hashes, i.e., `snake_case` vs.\n  `camelCase`.\n* Explicitly support different output formats, rather than just plain Ruby\n  hashes, such as [Protocol\n  Buffers](https://developers.google.com/protocol-buffers) or [Cap'n\n  Proto](https://capnproto.org/).\n* When using nested `precompute` blocks, the Packer has to flatten the\n  associations of a model, which may be expensive, but has not been benchmarked.\n  These flattened arrays already exist internally in Sequel when the eager\n  loading occurs, but those aren't exposed. The code in Sequel could be\n  re-implemented as part of the library to avoid re-constructing those arrays.\n\n## Contributing\n\nBug reports and pull requests are welcome on GitHub at\nhttps://github.com/PaulJuliusMartinez/sequel-packer.\n\n### Development\n\nAfter checking out the repo, run `bin/setup` to install dependencies. Then, run\n`rake test` to run the tests. You can also run `bin/console` for an interactive\nprompt that will allow you to experiment.\n\n### Releases\n\nTo release a new version, update the version number in\n`lib/sequel/packer/version.rb`, update the `CHANGELOG.md` with new changes, then\nrun `rake release`, which which will create a git tag for the version, push git\ncommits and tags, and push the `.gem` file to\n[rubygems.org](https://rubygems.org).\n\n## Attribution\n\n[Karthik Viswanathan](https://github.com/karthikv) designed the original API\nof the Packer library while at [Affinity](https://www.affinity.co/). This\nlibrary is a ground up rewrite which defines a very similar API, but shares no\ncode with the original implementation.\n\n## License\n\nThe gem is available as open source under the terms of the\n[MIT License](https://opensource.org/licenses/MIT).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpauljuliusmartinez%2Fsequel-packer","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fpauljuliusmartinez%2Fsequel-packer","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpauljuliusmartinez%2Fsequel-packer/lists"}