{"id":20252775,"url":"https://github.com/pragmarb/pragma-migration","last_synced_at":"2025-07-22T21:34:36.081Z","repository":{"id":145588062,"uuid":"104784440","full_name":"pragmarb/pragma-migration","owner":"pragmarb","description":"Migrations make your API versions immortal.","archived":false,"fork":false,"pushed_at":"2018-07-22T16:42:18.000Z","size":139,"stargazers_count":0,"open_issues_count":5,"forks_count":0,"subscribers_count":0,"default_branch":"master","last_synced_at":"2025-07-01T12:52:55.814Z","etag":null,"topics":["api","migrations","pragma","ruby","trailblazer","versioning"],"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/pragmarb.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,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2017-09-25T18:05:49.000Z","updated_at":"2018-07-22T16:42:18.000Z","dependencies_parsed_at":null,"dependency_job_id":"a5c19e4c-139c-4dcf-b729-20564026f91c","html_url":"https://github.com/pragmarb/pragma-migration","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/pragmarb/pragma-migration","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pragmarb%2Fpragma-migration","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pragmarb%2Fpragma-migration/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pragmarb%2Fpragma-migration/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pragmarb%2Fpragma-migration/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/pragmarb","download_url":"https://codeload.github.com/pragmarb/pragma-migration/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pragmarb%2Fpragma-migration/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":266578680,"owners_count":23951150,"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","status":"online","status_checked_at":"2025-07-22T02:00:09.085Z","response_time":66,"last_error":null,"robots_txt_status":null,"robots_txt_updated_at":null,"robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"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":["api","migrations","pragma","ruby","trailblazer","versioning"],"created_at":"2024-11-14T10:19:22.706Z","updated_at":"2025-07-22T21:34:36.058Z","avatar_url":"https://github.com/pragmarb.png","language":"Ruby","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Pragma::Migration\n\n[![Build Status](https://travis-ci.org/pragmarb/pragma-migration.svg?branch=master)](https://travis-ci.org/pragmarb/pragma-migration)\n[![Coverage Status](https://coveralls.io/repos/github/pragmarb/pragma-migration/badge.svg?branch=master)](https://coveralls.io/github/pragmarb/pragma-migration?branch=master)\n[![Maintainability](https://api.codeclimate.com/v1/badges/e51e8d7489eb72ab97ba/maintainability)](https://codeclimate.com/github/pragmarb/pragma-migration/maintainability)\n\nPragma::Migration is an experiment at implementing [Stripe-style API versioning](https://stripe.com/blog/api-versioning).\n\n**This gem is highly experimental and still under active development. Usage in a production environment is strongly discouraged.**\n\n## Installation\n\nAdd this line to your application's Gemfile:\n\n```ruby\ngem 'pragma-migration'\n```\n\nAnd then execute:\n\n    $ bundle\n\nOr install it yourself as:\n\n    $ gem install pragma-migration\n\nNext, you're going to create a migration repository for your API:\n\n```ruby\nmodule API\n  module V1\n    class MigrationRepository \u003c Pragma::Migration::Repository\n      # The initial version isn't allowed to have migrations, because there is nothing\n      # to migrate from.\n      version '2017-12-17'\n    end\n  end\nend\n```\n\nAnd configure the gem:\n\n```ruby\n# config/initializers/pragma_migration.rb or equivalent in your framework\nPragma::Migration.configure do |config|\n  config.repository = API::V1::MigrationRepository\n  config.user_version_proc = lambda do |request|\n    # `request` here is a `Rack::Request` object.\n    request.get_header 'X-Api-Version'\n  end\nend\n```\n\nFinally, you need to mount the migration Rack middleware. In a Rails environment, this means adding\nthe following to `config/application.rb`:\n\n```ruby\nmodule YourApp\n  class Application \u003c Rails::Application\n    # ...\n\n    config.middleware.use Pragma::Migration::Middleware\n  end\nend\n```\n\n## Usage\n\nWhen you start working on a new API version, you should define a new version in the repository:\n\n```ruby\nmodule API\n  module V1\n    class MigrationRepository \u003c Pragma::Migration::Repository\n      version '2017-12-17'\n\n      # We will give this a date very far into the future for now, since we don't know the release\n      # date yet.\n      version '2100-01-01', [\n        # Add migrations here...\n      ]\n    end\n  end\nend\n```\n\nSuppose you are working on a new API version and you decide to remove the `_id` suffix from\nassociation properties. In order to support users who are on an older version of the API, you will\nneed to do the following:\n\n- remove the `_id` suffix from their requests;\n- add the `_id` suffix back to their responses.\n\nTo accomplish it, you might write a new migration like this:\n\n```ruby\nmodule API\n  module V1\n    module Migration\n      class RemoveIdSuffixFromAuthorInArticles \u003c Pragma::Migration::Base\n        # You can use any pattern supported by Mustermann here.\n        apply_to '/api/v1/articles/:id'\n\n        # Optionally, you can write a description for the migration, which you can use for\n        # documentation and changelogs.\n        describe 'The _id suffix has been removed from the author property in the Articles API.'\n\n        # The `up` method is called when a client on an old version makes a request, and should\n        # convert the request into a format that can be consumed by the operation.\n        def up\n          request.update_param 'author', request.delete_param('author_id')\n        end\n\n        # The `down` method is called when a response is sent to a client on an old version, and\n        # should convert the response into a format that can be consumed by the client.\n        def down\n          parsed_body = JSON.parse(response.body.join(''))\n          Rack::Response.new(\n            JSON.dump(parsed_body.merge('author' =\u003e parsed_body['author_id'])),\n            response.status,\n            response.headers\n          )\n        end\n      end\n    end\n  end\nend\n```\n\nNow, you will just add your migration to the repository:\n\n```ruby\nmodule API\n  module V1\n    class MigrationRepository \u003c Pragma::Migration::Repository\n      version '2017-12-17'\n\n      version '2100-01-01', [\n        API::V1::Migration::ChangeTimestampsToUnixEpochs,\n      ]\n    end\n  end\nend\n```\n\nAs you can see, the migration allows API requests generated by outdated clients to run on the new\nversion. You don't have to implement ugly conditionals everywhere in your API: all the changes are\nneatly contained in the API migrations.\n\nThere is no limit to how many migrations or versions you can have. There's also no limit on how old\nyour clients can be: even if they are 10 versions behind, the migrations for all versions will be\napplied in order, so that the clients are able to interact with the very latest version without even\nknowing it!\n\n### Using migrations to contain side effects\n\nIn some cases, migrations are more complex than a simple update of the request and response.\n\nLet's take this example scenario: you are building a blog API and you are working on a new version\nthat automatically sends an email to subscribers when a new article is sent, whereas the current\nversion requires a separate API call to accomplish this. Since you don't want to surprise existing\nusers with the new behavior, you only want to do this when the new API version is being used.\n\nYou can use a no-op migration like the following for this:\n\n```ruby\nmodule API\n  module V1\n    module Migration\n      class NotifySubscribersAutomatically \u003c Pragma::Migration::Base\n        describe 'Subscribers are now notified automatically when a new article is published.'\n      end\n    end\n  end\nend\n```\n\nThen, in your operation, you will only execute the new code if the migration has been executed (i.e.\nthe user's version is greater than the migration's version):\n\n```ruby\nrequire 'pragma/migration/hooks/operation'\n\nmodule API\n  module V1\n    module Article\n      module Operation\n        class Create \u003c Pragma::Operation::Create\n          step :notify_subscribers!\n\n          def notify_subscribers!(options)\n            return unless migration_rolled?(Migration::NotifySubscribersAutomatically)\n\n            # Notify subscribers here...\n          end\n        end\n      end\n    end\n  end\nend\n```\n\n### Implementing complex version tracking\n\nIt is possible to implement more complex tracking strategies for determining your user's API\nversion. For instance, you might want to store the API version on the user profile instead:\n\n```ruby\nPragma::Migration.configure do |config|\n  # ...\n\n  config.user_version_proc = lambda do |request|\n    current_user = UserFinder.(request)\n    current_user\u0026.api_version # nil or an invalid value will default to the latest version\n  end\nend\n```\n\nThe possibilities here are endless. Stripe adopts a hybrid strategy: they freeze a user's API\nversion when the user performs the first request. They allow the user to upgrade to newer versions\neither permanently (you are not allowed to go back after a grace period) or on a per-request basis,\nwhich is useful when doing partial upgrades.\n\nThis strategy can be accomplished quite easily with the following configuration:\n\n```ruby\nPragma::Migration.configure do |config|\n  # ...\n\n  config.user_version_proc = lambda do |request|\n    request.get_header('X-Api-Version') || UserFinder.(request)\u0026.api_version\n  end\nend\n```\n\n## FAQs\n\n### Why are the migrations so low-level?\n\nAdmittedly, the code for migrations is very low-level: you are interacting with requests and\nresponses directly, rather than using contracts and decorators. Unfortunately, so far we have been\nunable to come up with an abstraction that will not blow up at the first edge case. We are still\nexperimenting here - ideas are welcome!\n\n### What are the drawbacks of API migrations?\n\nIf you are used to ActiveRecord migrations, then you might be tempted to use this very freely.\nHowever, API migrations are very different from DB migrations: DB migrations are run once and then\nforgotten forever, API migrations are executed on _every request_ as long as clients are running on\nan outdated version of your API. This means that API migrations should be considered an active,\nevolving part of your codebase that you will have to maintain over time.\n\n### Why should I keep the `/v1` prefix?\n\nThe main reason for keeping the `/v1` prefix and the `API::V1` namespace in your API is that you\nmight want to introduce a change so disruptive that it warrants a separate major version, like\nmigrating from REST to GraphQL or introducing one alongside the other. In this case, you won't be\nable to use migrations to contain the change, so you will need to create a completely separate\ncodebase and URL scheme.\n\n### What is the impact on performance?\n\nWe have a simple benchmark that runs 2,000 migrations in both directions. You can check out\n`benchmark.rb` for the details. Improvements are welcome!\n\nHere are the results on my machine, a MacBook Pro 2017 i7 @ 3.1 GHz:\n\n```\n$ ruby -v benchmark.rb\n\nruby 2.4.2p198 (2017-09-14 revision 59899) [x86_64-darwin16]\n\nRunning 2k migrations, up and down:\n\n       user     system      total        real\n  0.090000   0.010000   0.100000 (  0.097414)\n```\n\n### Are you out of your mind?\n\nPossibly, [but we're not the only ones](https://stripe.com/blog/api-versioning).\n\n## Contributing\n\nBug reports and pull requests are welcome on GitHub at https://github.com/pragmarb/pragma-migration.\n\n## License\n\nThe gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpragmarb%2Fpragma-migration","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fpragmarb%2Fpragma-migration","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpragmarb%2Fpragma-migration/lists"}