{"id":13878215,"url":"https://github.com/sirupsen/airrecord","last_synced_at":"2025-04-08T08:14:23.492Z","repository":{"id":10155402,"uuid":"64671002","full_name":"sirupsen/airrecord","owner":"sirupsen","description":"Ruby wrapper for Airtable, your personal database","archived":false,"fork":false,"pushed_at":"2023-09-18T18:53:57.000Z","size":173,"stargazers_count":305,"open_issues_count":17,"forks_count":61,"subscribers_count":14,"default_branch":"master","last_synced_at":"2025-04-01T05:34:32.660Z","etag":null,"topics":["airtable","database","ruby","spreadsheet"],"latest_commit_sha":null,"homepage":"","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/sirupsen.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","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":"2016-08-01T13:53:24.000Z","updated_at":"2025-03-21T09:30:20.000Z","dependencies_parsed_at":"2024-01-13T20:36:29.463Z","dependency_job_id":"b6947d79-1dd8-4b59-b0cd-b00146f00405","html_url":"https://github.com/sirupsen/airrecord","commit_stats":{"total_commits":146,"total_committers":24,"mean_commits":6.083333333333333,"dds":0.5410958904109588,"last_synced_commit":"f93bc9352093444da301e4609e622aed0a75fe95"},"previous_names":[],"tags_count":20,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sirupsen%2Fairrecord","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sirupsen%2Fairrecord/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sirupsen%2Fairrecord/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sirupsen%2Fairrecord/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/sirupsen","download_url":"https://codeload.github.com/sirupsen/airrecord/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247801169,"owners_count":20998339,"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":["airtable","database","ruby","spreadsheet"],"created_at":"2024-08-06T08:01:42.953Z","updated_at":"2025-04-08T08:14:23.458Z","avatar_url":"https://github.com/sirupsen.png","language":"Ruby","funding_links":[],"categories":["Ruby"],"sub_categories":[],"readme":"# Airrecord\n\nAirrecord is an alternative Airtable Ruby libary to\n[`airtable-ruby`](https://github.com/airtable/airtable-ruby). Airrecord attempts\nto enforce a more [database-like API to\nAirtable](http://sirupsen.com/minimum-viable-airtable/). However, there's also\nan [ad-hoc API available](https://github.com/sirupsen/airrecord#ad-hoc-api) that\nskips the class definitions!\n\nYou can add this line to your Gemfile to use Airrecord:\n\n```ruby\ngem 'airrecord'\n```\n\nA quick example to give an idea of the API that Airrecord provides:\n\n```ruby\nAirrecord.api_key = \"key1\"\n\nclass Tea \u003c Airrecord::Table\n  self.base_key = \"app1\"\n  self.table_name = \"Teas\"\n\n  has_many :brews, class: \"Brew\", column: \"Brews\"\n\n  def self.chinese\n    all(filter: '{Country} = \"China\"')\n  end\n\n  def self.cheapest_and_best\n    all(sort: { \"Rating\" =\u003e \"desc\", \"Price\" =\u003e \"asc\" })\n  end\n\n  def location\n    [self[\"Village\"], self[\"Country\"], self[\"Region\"]].compact.join(\", \")\n  end\n\n  def green?\n    self[\"Type\"] == \"Green\"\n  end\nend\n\nclass Brew \u003c Airrecord::Table\n  self.base_key = \"app1\"\n  self.table_name = \"Brews\"\n\n  belongs_to :tea, class: \"Tea\", column: \"Tea\"\n\n  def self.hot\n    all(filter: \"{Temperature} \u003e 90\")\n  end\n\n  def done_brewing?\n    Time.parse(self[\"Created At\"]) + self[\"Duration\"] \u003e Time.now\n  end\nend\n\nteas = Tea.all\ntea = teas.first\ntea[\"Country\"] # access atribute\ntea.location # instance methods\ntea.brews # associated brews\n```\n\nA short-hand API for definitions and more ad-hoc querying is also available:\n\n```ruby\nTea = Airrecord.table(\"api_key\", \"app_key\", \"Teas\")\n\nTea.all.each do |record|\n  puts \"#{record.id}: #{record[\"Name\"]}\"\nend\n\nTea.find(\"rec3838\")\n```\n\n## Documentation\n\n### Authentication\n\nBased on [Changelog](https://airtable.com/developers/web/api/changelog#anchor-2022-11-15) There are two ways to authenticate with Airtable API:\n1. API key\n2. Personal Access Token\n\n#### API key\nTo obtain your API client, navigate to the [Airtable's API\npage](https://airtable.com/api), select your base and obtain your API key and\napplication token.\n\n![](https://cloud.githubusercontent.com/assets/97400/23580721/a0815df4-00bb-11e7-9abf-140a01625678.png)\n\nYou can provide a global API key with:\n\n```ruby\nAirrecord.api_key = \"your api key\"\n```\n\nThe app token has to be set on the definitions of the tables (see API below).\nYou can override the API key per table.\n\n#### Personal Access Token (PAT)\n- To create a PAT, navigate to the [Airtable's Personal Access Tokens](https://airtable.com/create/tokens) page and create a new token.\n- Give your token a unique name. This name will be visible in record revision history.\n- Choose the scopes to grant to your token. This controls what API endpoints the token will be able to use. For more information, see [API scopes](https://airtable.com/api#authentication).\n- Click ‘add a base’ to grant the token access to a base or workspace.\n- Click ‘create token’ to create the token. You will be shown the token’s value. This is the only time you will be able to see the token’s value, so be sure to copy it to a secure location.\n\nYou can provide a global PAT with:\n\n```ruby\nAirrecord.api_key = \"your PAT\"\n```\n\n### Table\n\nThe Airrecord API is centered around definitions of `Airrecord::Table` from\nwhich the definitions of your tables inherit. This is analogous to\n`ActiveRecord::Base`. For example, we may have a Base to track teas we have\ntried.\n\n```ruby\nAirrecord.api_key = \"your api key\" # see authentication section\n\nclass Tea \u003c Airrecord::Table\n  self.base_key = \"app1\"\n  self.table_name = \"Teas\"\n\n  def location\n    [self[\"Village\"], self[\"Country\"], self[\"Region\"]].compact.join(\", \")\n  end\nend\n```\n\nThis gives us a class that maps to records in a table. Class methods are\navailable to fetch records on the table.\n\n### Reading a Single Record\n\nRetrieve a single record via `#find`:\n```ruby\ntea = Tea.find(\"someid\")\n```\n\n### Listing Records\n\nRetrieval of multiple records is usually done through `#all`. To get all records\nin a table:\n\n```ruby\nTea.all # array of Tea instances\n```\n\nYou can use all options supported by the API (they are documented on the [API page for your base](https://airtable.com/api)). By default `#all` will traverse all pages, see below on how to control pagination.\n\nTo use `filterbyFormula` to filter returned records:\n\n```ruby\n# Retrieve all teas from China\nTea.all(filter: '{Country} = \"China\"')\n\n# Retrieve all teas created in the past week\nTea.all(filter: \"DATETIME_DIFF(CREATED_TIME(), TODAY(), 'days') \u003c 7\")\n\n# Retrieve all teas that don't have a country defined\nTea.all(filter: \"{Country} = \\\"\\\"\")\n```\n\nThis filtering can, of course, also be done in Ruby directly after calling\n`#all` without `filter`, however, it may be more efficient to let Airtable\nfilter if you have a lot of records.\n\nYou can use `view` to only fetch records from a specific view. This is less\nad-hoc than `filterByFormula`:\n\n```ruby\n# Retrieve all teas in the green tea view\nTea.all(view: \"Green\")\n\n# Retrieve all Japanese teas\nTea.all(view: \"Japan\")\n```\n\nThe `sort` option can be used to sort results returned from the Airtable API.\n\n```ruby\n# Sort teas by the Name column in ascending order\nTea.all(sort: { \"Name\" =\u003e \"asc\" })\n\n# Sort teas by Type (green, black, oolong, ..) in descending order\nTea.all(sort: { \"Type\" =\u003e \"desc\" })\n\n# Sort teas by price in descending order\nTea.all(sort: { \"Price\" =\u003e \"desc\" })\n```\n\nNote again that the key _must_ be the full column name.\n\nAs mentioned above, by default Airrecord will return results from all pages.\nThis can be slow if you have 1000s of records. You may wish to use the `view`\nand/or `filter` option to sort in the results early, instead of doing 10s of\ncalls. Airrecord will _always_ fetch the maximum possible amount of records\n(100). This means that fetching 1,000 records will take 10 (at least) roundtrips. You can disable pagination (which fetches the first page) by passing `paginate: false`. This is especially useful if you're looking to fetch a set of recent records from a view or formula in tandem with a `sort`:\n\n```ruby\n# Only fetch the first page. Sorting is undefined.\nTea.all(paginate: false)\n\n# Give me only the most recent teas\nTea.all(sort: { \"Created At\" =\u003e \"desc\" }, paginate: false)\n```\n\nWhen you know the IDs of the records you want, and you want them in an ad-hoc\norder, use `#find_many` instead of `#all`:\n\n```ruby\nteas = Tea.find_many([\"someid\", \"anotherid\", \"yetanotherid\"])\n#=\u003e [\u003cTea @id=\"someid\"\u003e,\u003cTea @id=\"anotherid\"\u003e, \u003cTea @id=\"yetanotherid\"\u003e]\n```\n\n### Creating\n\nCreating a new record is done through `Table.create`.\n\n```ruby\ntea = Tea.create(\"Name\" =\u003e \"Feng Gang\", \"Type\" =\u003e \"Green\", \"Country\" =\u003e \"China\")\ntea.id # id of the new record\ntea[\"Name\"] # \"Feng Gang\"\n```\n\nIf you need to manipulate a record before saving it, you can use `Table.new`\ninstead of `create`, then call `#save` when you're ready.\n\n```ruby\ntea = Tea.new(\"Type\" =\u003e \"Green\", \"Country\" =\u003e \"China\")\ntea[\"Name\"] = \"Feng Gang\"\ntea.save\n```\n\nNote that column names need to match the exact column names in Airtable,\notherwise Airrecord will throw an error that no column matches it.\n\n\nIf you need to include optional request parameters, such as the `typecast` parameter, these can be passed to either `Table.create` or `#save`.\nThis is also supported when updating existing records with the `#save` method.\n\n```ruby\ntea = Tea.create(\n  {\"Name\" =\u003e \"Feng Gang\", \"Type\" =\u003e \"Green\", \"Country\" =\u003e \"China\"},\n  {\"typecast\" =\u003e true},\n)\n\n# Or with the #save method:\ntea = Tea.new({\"Name\" =\u003e \"Feng Gang\", \"Type\" =\u003e \"Green\"})\ntea[\"Name\"] = \"Feng Gang\"\ntea.save(\"typecast\" =\u003e true)\n```\n\n_Earlier versions of airrecord provided methods for snake-cased column names\nand symbols, however this proved error-prone without a proper schema API from\nAirtable which has still not been released._\n\n### Updating\n\nUpdating can be done in two ways:\n\n1. With a record instance, change the attributes and persist to Airtable with `#save`\n\n```ruby\ntea = Tea.find(\"someid\")\ntea[\"Name\"] = \"Feng Gang Organic\"\ntea[\"Village\"] = \"Feng Gang\"\n\ntea.save # persist to Airtable\n```\n\n2. With the `Table.update` class method (to save an API find call)\n\n```ruby\nTea.update(\"someid\", { \"Name\" =\u003e \"Feng Gang Organic\", \"Village\" =\u003e \"Feng Gang\" })\n```\n\n_Airtable's API doesn't allow you to change attachment's filename. As a workaround you can delete the original attachment and [upload a new one](https://github.com/sirupsen/airrecord#file-uploads) with the original URL and a new filename._\n\n### Deleting\n\nAn instantiated record can be deleted through `#destroy`:\n\n```ruby\ntea = Tea.find(\"rec839\")\ntea.destroy # deletes record\n```\n\n### File Uploads\n\nAirtable's API requires you to have uploaded your file to an intermediary and\nproviding the URL. Unfortunately, it does not allow uploading directly.\n\n```ruby\nword = World.find(\"cantankerous\")\nword[\"Pronounciation\"] = [{url: \"https://s3.ca-central-1.amazonaws.com/word-pronunciations/cantankerous.mp3\"}]\nword.save\n```\n\nS3 is a good place to upload files for Airtable. Airrecord does not support this\ndirectly, but the snippet below may be helpful:\n\n```ruby\n# Add this to your gemfile\n# Full docs at https://docs.aws.amazon.com/sdkforruby/api/Aws/S3/Client.html\nrequire 'aws-sdk-s3'\n\nAws.config.update(\n  credentials: Aws::Credentials.new(access_key, secret_key) # obtain from AWS\n  region: 'ca-central-1', # region\n)\n\ns3 = Aws::S3::Client.new\ns3.put_object({\n  body: File.open(\"cantankerous.mp3\"), # IO object\n  bucket: 'word-pronunciations',\n  key: 'cantankerous.mp3',\n  acl: \"public-read\",\n})\n```\n\n### Associations\n\nAirrecord supports managing associations between tables by linking\n`Airrecord::Table` classes. To continue with our tea example, we may have\nanother table in the base to track brews of a specific tea (temperature,\nsteeping time, rating, ..). A tea thus has many brews:\n\n```ruby\nclass Tea \u003c Airrecord::Table\n  self.base_key = \"app1\"\n  self.table_name = \"Teas\"\n\n  has_many :brews, class: \"Brew\", column: \"Brews\"\n  has_one :teapot, class: \"Teapot\", column: \"Teapot\"\nend\n\nclass Brew \u003c Airrecord::Table\n  self.base_key = \"app1\"\n  self.table_name = \"Brews\"\n\n  belongs_to :tea, class: \"Tea\", column: \"Tea\"\nend\n\nclass Teapot \u003c Airrecord::Table\n  self.base_key = \"app1\"\n  self.table_name = \"Teapot\"\n\n  belongs_to :tea, class: \"Tea\", column: \"Tea\"\nend\n```\n\nThis gives us access to a bunch of convenience methods to handle the assocation\nbetween the two tables. Note that the two tables need to be in the same base\n(i.e. have the same base key) otherwise this will _not_ work as Airtable does\n_not_ support associations across Bases.\n\n### Retrieving associated records\n\nTo retrieve records from associations to a record:\n\n```ruby\ntea = Tea.find(\"rec123\")\n\n# record.association returns Airrecord instances\ntea.brews #=\u003e [\u003cBrew @id=\"rec456\"\u003e, \u003cBrew @id=\"rec789\"\u003e]\ntea.teapot #=\u003e \u003cTeapot @id=\"rec012\"\u003e\n\n# record[\"Associated Column\"] returns the raw Airtable field, an array of IDs\ntea[\"Brews\"] #=\u003e [\"rec789\", \"rec456\"]\ntea[\"Teapot\"] #=\u003e [\"rec012\"]\n```\n\nThis in turn works the other way too:\n\n```ruby\nbrew = Brew.find(\"rec456\")\nbrew.tea #=\u003e \u003cTea @id=\"rec123\"\u003e the associated tea instance\nbrew[\"Tea\"] #=\u003e the raw Airtable field, a single-item array [\"rec123\"]\n```\n\n### Creating associated records\n\nYou can easily associate records with each other:\n\n```ruby\ntea = Tea.find(\"rec123\")\n# This will create a brew associated with the specific tea\nbrew = Brew.new(\"Temperature\" =\u003e \"80\", \"Time\" =\u003e \"4m\", \"Rating\" =\u003e \"5\")\nbrew.tea = tea\nbrew.create\n```\n\nAlternatively, you can specify association ids directly:\n\n```ruby\ntea = Tea.find(\"rec123\")\nbrew = Brew.new(\"Tea\" =\u003e [tea.id], \"Temperature\" =\u003e \"80\", \"Time\" =\u003e \"4m\", \"Rating\" =\u003e \"5\")\nbrew.create\n```\n\n### Ad-hoc API\n\nAirrtable provides a simple, ad-hoc API that will instantiate an anonymous\n`Airrecord::Table` for you on the fly with the configured key, app, and table.\nThis is useful if you require no custom definitions, or you're just playing\naround.\n\n```ruby\nTea = Airrecord.table(\"api_key\", \"app_key\", \"Teas\")\n\nTea.all.each do |record|\n  puts \"#{record.id}: #{record[\"Name\"]}\"\nend\n\nTea.find(\"rec3838\")\n```\n\n### Throttling\n\nAirtable's API enforces a 5 requests per second limit per client. In most cases,\nyou won't reach this limit in a single thread but it's possible for some fast\ncalls. Airrecord automatically enforces this limit. It doesn't use a naive\nthrottling algorithm that does a `sleep(0.2)` between each call, but rather\nkeeps a sliding window of when each call was made and will sleep at the end of\nthe window if requires. This means that bursting is supported.\n\n### Production Middlewares\n\nFor production use-cases, it's worth considering adding retries and circuit\nbreakers to Airrecord. This is _not_ enabled by default. Airrecord uses the\nFaraday gem for HTTP. Similar to Rack, you can add middlewares to provide\nreusable logic for making HTTP requests.\n\n#### Configuring Retries\n\nRefer to the documentation for [all configuration\noptions](http://www.rubydoc.info/gems/faraday/0.9.2/Faraday/Request/Retry).\n\n```ruby\nAirrecord::Table.client.connection.request :retry,\n  max: 5, interval: 1, interval_randomness: 2, backoff_factor: 2,\n  exceptions: [...] # It's suggested to be explicit here instead of relying on defaults\n```\n\nIf you are running background scripts or workers with the sole purpose of\ncommunicating with Airtable, it may be worth retrying on failures. Note that\nthis may cause the process to sleep for many seconds, so choose your values\ncarefully.\n\nThe `Net::HTTP` library that Faraday uses behind the scenes by default has\nopaque exceptions. If you choose to go beyond retrying on timeouts (as is\nprovided by default by the Retry middleware), I suggest referring to a complete\nlist of `Net::HTTP` exceptions, such as [this\none](https://github.com/Shopify/semian/blob/master/lib/semian/net_http.rb#L35-L44).\n\n### Circuit Breaker\n\nIf you're calling Airtable in an application and want to avoid hanging processes\nwhen Airtable is unavailable, we strongly recommend configuring [circuit\nbreakers](https://github.com/Shopify/semian#circuit-breaker). This is a\nmechanism that after `threshold` failures, it'll start failing immediately\ninstead of waiting until the timeout. This can avoid outages where all workers\nare hung trying to talk to a service that will never return, instead of serving\nuseful fallbacks or requests that don't rely on the service. Failing fast is\nparamount for building reliable systems.\n\nYou can configure a middleware such as\n[`faraday_middleware-circuit_breaker`](https://github.com/textmaster/faraday_middleware-circuit_breaker):\n\n```ruby\nAirrecord::Table.client.connection.request :circuit_breaker,\n  timeout: 20, threshold: 5\n```\n\n## Contributing\n\nContributions will be happily accepted in the form of Github Pull Requests!\n\n* Please ensure CI is passing before submitting your pull request for review.\n* Please provide tests that fail without your change.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsirupsen%2Fairrecord","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsirupsen%2Fairrecord","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsirupsen%2Fairrecord/lists"}