Skip to content
This repository was archived by the owner on Oct 1, 2021. It is now read-only.

Initial work #1

Merged
merged 65 commits into from
Oct 17, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
65 commits
Select commit Hold shift + click to select a range
e214b8f
Basic setup
AuHau Mar 5, 2019
e1f8d93
Basic migrator functionality
AuHau Mar 7, 2019
04b8b4d
Strict mode
AuHau Mar 7, 2019
b386cb6
Linting
AuHau Mar 7, 2019
48525dc
Working CLI
AuHau Mar 14, 2019
1748537
Improving CLI usablity
AuHau Mar 22, 2019
fd07c3e
Dropping migration class interface
AuHau Mar 22, 2019
5091b72
Improving error handling and other tweaks of migrations
AuHau Mar 22, 2019
9eb9418
Refactoring to drop ipfs-repo in favor of datastore
AuHau Mar 28, 2019
3b18eba
Refactor migrator to pass only repo's path into migration and not dat…
AuHau Mar 31, 2019
d9311b0
'add' command for CLI
AuHau Apr 1, 2019
6a1a379
Linting
AuHau Apr 1, 2019
5a537ce
Writing proper README
AuHau Apr 4, 2019
5e6c4ba
Revert tests
AuHau Apr 8, 2019
b68f252
Migrate tests
AuHau Apr 8, 2019
7e5992a
More tests
AuHau Apr 8, 2019
6df2da1
Tests for lock.js and version.js
AuHau Apr 25, 2019
1a15eea
Description of tests
AuHau Apr 27, 2019
e7eb523
Update interface-datastore dependency
AuHau Apr 28, 2019
348c8c4
Fixing package-lock with correct interface-datastore version
AuHau Apr 28, 2019
fc7166a
Update wording of CLI reporter
AuHau Apr 28, 2019
1446453
Correct doctype for optional parameters
AuHau Apr 28, 2019
966080f
Exposing repo.getVersion
AuHau Apr 28, 2019
b64412b
Improving verification of reversibility
AuHau Apr 29, 2019
c9d4724
Adding check if repo is initialized
AuHau May 1, 2019
526d191
Renaming logging key
AuHau May 1, 2019
11111b7
Adding parameter ignoreLock
AuHau May 1, 2019
6dc649b
Adding parameter for options
AuHau May 1, 2019
de6527e
Adding documentation for ignoreLock and options parameters
AuHau May 15, 2019
dde678a
Instructions on integration with js-ipfs
AuHau May 22, 2019
1165993
Updating dependencies
AuHau May 30, 2019
64217df
Implementing Error's codes
AuHau May 31, 2019
e18e6d0
Refactoring into options object
AuHau May 31, 2019
1047d1a
Merging revert and migrate CLI commands
AuHau May 31, 2019
c836ed7
Extanding README with usage and other details
AuHau May 31, 2019
494e1c9
Integration test
AuHau Jun 3, 2019
3d29da1
Removing error message assertions
AuHau Jun 3, 2019
991fadf
Removing package.json for migrations
AuHau Jun 26, 2019
6649ea4
Adding basic sharness tests
AuHau Jun 26, 2019
7aa59d8
Removing 'reversible' flag from migration's API
AuHau Jul 1, 2019
54c7cbb
Renaming progressCb parameter to onProgress
AuHau Jul 1, 2019
f5f3ddf
Removing package.json mentions and discussing migration's dependencies
AuHau Jul 1, 2019
43d0066
Linting
AuHau Jul 1, 2019
6715d0d
Sharness tests
AuHau Jul 10, 2019
c665a1d
Non-throw error handling for getLatestMigrationVersion
AuHau Jul 18, 2019
e819342
Check if migration exist for migrate()
AuHau Jul 18, 2019
e7b6867
Fixing integration tests
AuHau Jul 18, 2019
f89b530
Grammar check
AuHau Jul 23, 2019
d19b486
Info about versioning
AuHau Jul 23, 2019
405d259
Grammar fixes
AuHau Jul 24, 2019
4ab9c4d
Messages tweak
AuHau Jul 24, 2019
77ca00e
Default paramaters
AuHau Jul 24, 2019
f6787a7
Tweaks
AuHau Jul 24, 2019
94cdc7a
Merge branch 'dev' of github.com:AuHau/js-ipfs-repo-migrations into dev
AuHau Jul 24, 2019
4fc8244
fix: update functions signature in templates
AuHau Jul 24, 2019
188268a
doc: CI/codecov/dependencies badges
AuHau Jul 24, 2019
1a6d6c6
style: lint fix
AuHau Jul 24, 2019
3e4415d
Message wordings
AuHau Jul 25, 2019
a0ce795
fix: tweaks
AuHau Jul 25, 2019
6996b6f
style: package json lint fix
AuHau Jul 25, 2019
edc7530
fix: update add command
AuHau Jul 26, 2019
724d02b
feat: utils for properly getting Datastore and its options
AuHau Sep 16, 2019
c93c732
feat: when error occures save last succesfull migration version
AuHau Sep 16, 2019
813f2aa
style: lint
AuHau Sep 16, 2019
a3d1712
fix: apply review suggestions
AuHau Oct 10, 2019
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[*]
indent_style = space
indent_size = 2
trim_trailing_whitespace = true
insert_final_newline = true
end_of_line = lf
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mind adding this file to .gitignore?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well I can, but I think it is a good thing to have around as it unities the code style. I know that for that there is aegir lint but this integrates directly with IDEs. You don't use it in other projects? I feel like I took it from some other JS package...
For example here: https://github.com/ipfs/js-ipfs/blob/master/.editorconfig

2 changes: 2 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
* text=auto
test/test-repo/** text eol=lf
31 changes: 31 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
docs

.eslintrc

# Logs
logs
*.log

# Runtime data
pids
*.pid
*.seed

# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov

# Coverage directory used by tools like istanbul
coverage

# node-waf configuration
.lock-wscript

build
dist

# Dependency directory
node_modules

# Tests
test/test-repo-for*
test/sharness/tmp
41 changes: 41 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
language: node_js
cache: npm
stages:
- check
- test
- cov

node_js:
- '10'

os:
- linux
- osx
- windows

script: npx nyc -s npm run test:node -- --bail
after_success: npx nyc report --reporter=text-lcov > coverage.lcov && npx codecov

jobs:
include:
- stage: check
script:
- npx aegir commitlint --travis
- npx aegir dep-check
- npm run lint

- stage: test
name: chrome
addons:
chrome: stable
script: npx aegir test -t browser

- stage: test
name: firefox
addons:
firefox: latest
script: npx aegir test -t browser -- --browsers FirefoxHeadless

notifications:
email: false

278 changes: 277 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,277 @@
# js-ipfs-migrator
# Migration tool for JS IPFS Repo

[![Travis CI](https://flat.badgen.net/travis/ipfs/js-ipfs-repo-migrations)](https://travis-ci.com/ipfs/js-ipfs-repo-migrations)
[![codecov](https://codecov.io/gh/ipfs/js-ipfs-repo/branch/master/graph/badge.svg)](https://codecov.io/gh/ipfs/js-ipfs-repo-migrations)
[![Dependency Status](https://david-dm.org/ipfs/js-ipfs-repo-migrations.svg?style=flat-square)](https://david-dm.org/ipfs/js-ipfs-repo-migrations)
[![](https://img.shields.io/badge/made%20by-Protocol%20Labs-blue.svg?style=flat-square)](http://ipn.io)
[![](https://img.shields.io/badge/project-IPFS-blue.svg?style=flat-square)](http://ipfs.io/)
[![](https://img.shields.io/badge/freenode-%23ipfs-blue.svg?style=flat-square)](http://webchat.freenode.net/?channels=%23ipfs)
[![standard-readme compliant](https://img.shields.io/badge/standard--readme-OK-green.svg?style=flat-square)](https://github.com/RichardLitt/standard-readme)
[![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat-square)](https://github.com/feross/standard)
![](https://img.shields.io/badge/npm-%3E%3D3.0.0-orange.svg?style=flat-square)
![](https://img.shields.io/badge/Node.js-%3E%3D10.0.0-orange.svg?style=flat-square)

> Migration framework for versioning of JS IPFS Repo

This package is inspired by the [go-ipfs repo migration tool](https://github.com/ipfs/fs-repo-migrations/)

## Lead Maintainer

[Adam Uhlíř](https://github.com/auhau/)

## Table of Contents

- [Background](#background)
- [Install](#install)
- [npm](#npm)
- [Use in Node.js](#use-in-nodejs)
- [Use in a browser with browserify, webpack or any other bundler](#use-in-a-browser-with-browserify-webpack-or-any-other-bundler)
- [Use in a browser Using a script tag](#use-in-a-browser-using-a-script-tag)
- [Usage](#usage)
- [Writing migration](#writing-migration)
- [Migrations matrix](#migrations-matrix)
- [API](#api)
- [CLI](#cli)
- [Versioning](#versioning)
- [Contribute](#contribute)
- [License](#license)

## Background


As js-ipfs evolves and new technologies, algorithms and data structures are incorporated it is necessary to
enable users to transition between versions. Different versions of js-ipfs may expect a different IPFS repo structure or content (see: [IPFS repo spec](https://github.com/ipfs/specs/tree/master/repo), [JS implementation](https://github.com/ipfs/js-ipfs-repo) ).
So the IPFS repo is versioned, and this package provides a framework to create migrations to transition
from one version of IPFS repo to the next/previous version.

This framework:
* Handles locking/unlocking of repository
* Defines migrations API
* Executes and reports migrations in both directions: forward and backward
* Simplifies creation of new migrations
* Works on the browser too!

## Install

### npm

```sh
> npm install ipfs-repo-migrations
```

### Use in Node.js

```js
const migrations = require('ipfs-repo-migrations')
```

### Use in a browser with browserify, webpack or any other bundler

```js
const migrations = require('ipfs-repo-migrations')
```

## Usage

Example:

```js
const migrations = require('ipfs-repo-migrations')
const getVersion = require('ipfs-repo-migrations/repo/version')

const repoPath = 'some/repo/path'
const repoVersion = await getVersion(repoPath)

if(repoVersion < migrations.getLatestMigrationVersion()){
// Old repo! Lets migrate to latest version!
await migrations.migrate(repoPath)
}
```

To migrate your repository using the CLI, see the [how to run migrations](./run.md) tutorial.

## API

### `.migrate(path, {toVersion, ignoreLock, repoOptions, onProgress, isDryRun}) -> Promise<void>`

Executes a forward migration to a specific version, or to the latest version if a specific version is not specified.

**Arguments:**

* `path` (string, mandatory) - path to the repo to be migrated
* `options` (object, optional) - options for the migration
* `options.toVersion` (int, optional) - version to which the repo should be migrated. Defaults to the latest migration version.
* `options.ignoreLock` (bool, optional) - if true will not lock the repo when applying migrations. Use with caution.
* `options.repoOptions` (object, optional) - options that are passed to migrations, that use them to construct the datastore. (options are the same as for IPFSRepo).
* `options.onProgress` (function, optional) - callback that is called after finishing execution of each migration to report progress.
* `options.isDryRun` (bool, optional) - flag that indicates if it is a dry run that should give the same output as running a migration but without making any actual changes.

#### `onProgress(migration, counter, totalMigrations)`

Signature of the progress callback.

**Arguments:**
* `migration` (object) - object of migration that just successfully finished running. See [Architecture of migrations](#architecture-of-migrations) for details.
* `counter` (int) - index of current migration.
* `totalMigrations` (int) - total count of migrations that will be run.

### `.revert(path, toVersion, {ignoreLock, options, onProgress, isDryRun}) -> Promise<void>`

Executes backward migration to a specific version.

**Arguments:**

* `path` (string, mandatory) - path to the repo to be reverted
* `toVersion` (int, mandatory) - version to which the repo should be reverted to.
* `options` (object, optional) - options for the reversion
* `options.ignoreLock` (bool, optional) - if true will not lock the repo when applying migrations. Use with caution.
* `options.options` (object, optional) - options that are passed to migrations, that use them to construct the datastore. (options are the same as for IPFSRepo).
* `options.onProgress` (function, optional) - callback that is called after finishing execution of each migration to report progress.
* `options.isDryRun` (bool, optional) - flag that indicates if it is a dry run that should give the same output as running a migration but without making any actual changes.

### `getLatestMigrationVersion() -> int`

Return the version of the latest migration.

## CLI

The CLI is a NodeJS binary named `jsipfs-repo-migrations`.
It has several commands:

* `migrate` - performs forward/backward migration to specific or latest version.
* `status` - check repo for migrations that should be run.
* `add` - bootstraps new migration.

For further details see the `--help` pages.

## Creating a new migration

Migrations are one of those things that can be extremely painful on users. At the end of the day, we want users never to have to think about it. The process should be:

- SAFE. No data lost. Ever.
- Revertible. Tools must implement forward and backward (if possible) migrations.
- Tests. Migrations have to be well tested.
- To Spec. The tools must conform to the spec.

If your migration has several parts, it should be fail-proof enough that if one part of migration fails the previous changes
are reverted before propagating the error. If possible then the outcome should be consistent repo so it migration could
be run again.

### Architecture of a migration

All migrations are placed in the `/migrations` folder. Each folder there represents one migration that follows the migration
API.

All migrations are collected in `/migrations/index.js`, which should not be edited manually. It is regenerated on
every run of `jsipfs-migrations add` (manual changes should follow the same style of modifications).
**The order of migrations is important and migrations must be sorted in ascending order**.

Each migration must follow this API. It must export an object in its `index.js` that has following properties:

* `version` (int) - Number that represents the version which the repo will migrate to (eg. `migration-8` will move the repo to version 8).
* `description` (string) - Brief description of what the migrations does.
* `migrate` (function) - Function that performs the migration (see signature of this function below)
* `revert` (function) - If defined then this function will revert the migration to the previous version. Otherwise it is assumed that it is not possible to revert this migration.

#### `.migrate(repoPath, isBrowser)`

_Do not confuse this function with the `require('ipfs-repo-migrations').migrate()` function that drives the whole migration process!_

Arguments:
* `repoPath` (string) - absolute path to the root of the repo
* `options` (object, optional) - object containing `IPFSRepo` options, that should be used to construct a datastore instance.
* `isBrowser` (bool) - indicates if the migration is run in a browser environment (as opposed to NodeJS)

#### `.revert(repoPath, isBrowser)`

_Do not confuse this function with the `require('ipfs-repo-migrations').revert()` function that drives the whole backward migration process!_

Arguments:
* `repoPath` (string) - path to the root of the repo
* `options` (object, optional) - object containing `IPFSRepo` options, that should be used to construct the datastore instance.
* `isBrowser` (bool) - indicates if the migration is run in a browser environment (as opposed to NodeJS)

### Browser vs. NodeJS environments

The migration might need to distinguish in which environment it runs (browser vs. NodeJS). For this reason there is an argument
`isBrowser` passed to migrations functions. But with simple migrations it should not be necessary to distinguish between
these environments as the datastore implementation will handle the main differences.

There are currently two main datastore implementations:
1. [`datastore-fs`](https://github.com/ipfs/js-datastore-fs) that is backed by file system and is used mainly in the NodeJS environment
2. [`datastore-level`](https://github.com/ipfs/js-datastore-level) that is backed by LevelDB and is used mainly in the browser environment

Both implementations share the same API and hence are interchangeable.

When the migration is run in a browser environment, `datastore-fs` is automatically replaced with `datastore-level` even
when it is directly imported (`require('datastore-fs')` will return `datastore-level` in a browser).
So with simple migrations you shouldn't worry about the difference between `datastore-fs` and `datastore-level`
and by default use the `datastore-fs` package (as the replace mechanism does not work vice versa).

### Guidelines

The recommended way to write a new migration is to first bootstrap a dummy migration using the CLI:

```sh
> npm run new-migration
```

A new folder is created with the bootstrapped migration. You can then simply fill in the required fields and
write the rest of the migration!

### Integration with js-ipfs

When a new migration is created, the repo version in [`js-ipfs-repo`](https://github.com/ipfs/js-ipfs-repo) should be updated with the new version,
together with updated version of this package. Then the updated version should be propagated to `js-ipfs`.

### Tests

If a migration affects any of the following functionality, it must provide tests for the following functions
to work under the version of the repo that it migrates to:

* `/src/repo/version.js`:`getVersion()` - retrieving repository's version
* `/src/repo/lock.js`:`lock()` - locking repository that uses file system
* `/src/repo/lock-memory.js`:`lock()` - locking repository that uses memory

Every migration must have test coverage. Tests for migrations should be placed in the `/test/migrations/` folder. Most probably
you will have to plug the tests into `browser.js`/`node.js` if they require specific bootstrapping on each platform.

### Empty migrations

For interop with go-ipfs it might be necessary just to bump a version of a repo without any actual
modification as there might not be any changes needed in the JS implementation. For that purpose you can create an "empty migration".

The easiest way to do so is with the CLI:

```sh
> npm run new-migration -- --empty
```

This will create an empty migration with the next version.

### Migrations matrix

| IPFS repo version | JS IPFS version |
| -----------------: |:----------------:|
| 7 | v0.0.0 - latest |

## Developer

### Module versioning notes

In order to have good overview of what version of package contains what kind of migrations, this package follows this versioning schema: `0.<versionOfLastMigration>.<patches>`.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tying the npm version to the repo version like this means we lose the ability to communicate breaking/feature change information for this module to dependants.

For example, how would we communicate a change like the async/await refactor in this module? There's no repo version increase and we can't release a breaking change like that in a patch release.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, you are right about this, but I don't believe there will be much of feature development/breaking changes in the future of this package. I believe that 80% of the changes will be adding new migrations, then 15% will be fixing bugs and 5% others...

In your example, we could communicate the breaking change using the major version. So for example from 0.8.0 we could bump to 1.8.0...
I kind of like the idea that you know what migrations the package contains just from the version number.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In your example, we could communicate the breaking change using the major version. So for example from 0.8.0 we could bump to 1.8.0...

We could...IMHO that would be a really strange versioning strategy though!

I kind of like the idea that you know what migrations the package contains just from the version number.

I kind of do too, but I'm not convinced it's worth it!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could...IMHO that would be a really strange versioning strategy though!

True, but following 0.<versionOfLastMigration>.<patches> is already "strange" anyway ;-)

What about having two parts in the version? E.g. <semver>-<last version of migration> so it could look like 0.2.0-7? When new migration is added it would be bumped to 0.3.0-8. This would allow us to have full-fledged semver capabilities and still have the information about migrations as part of the version?

What I am not sure is if this schema would not break some tooling? (aegir?)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there another reason other than convenience that would warrant this versioning strategy?

If you don't already know the module the versioning strategy isn't going to convey any additional info. Secondly, it's really easy for new developers to miss this important piece of info since it's a small section at the bottom of the README. Thirdly, if you do know the module and are upgrading to get the latest migrations you'll probably just need the latest version of this module anyway right? It's quite unlikely you'll ever need an older version of the module since the newer versions contain all the past migrations as well.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would not call it convenience, but being careful. Having a different version of this package installed may lead to irreversible actions that can happen without your knowledge (eq. when you have autoMigrations on - which is currently the default). Yes, it won't most probably lose the data per se, but it might migrate data that are not to be migrated at that moment (for example you have test repo and "production" repo and you want to do some tests, but you accidentally migrate wrong one etc...).

Of course, having this schema won't prevent all mix ups, but it can improve the situation. From version update 0.4.0 --> 0.5.0 you can't be sure if there was new migration added (yes, it will be most of the cases), or maybe some feature change? You will have to go study release notes, which should be always the case, but I am pretty sure a lot of devs don't do that.

In this light, I think having the -<latestVersionOfMigration> suffix is even better than the original schema as it stands out from Semver and should tingle dev's senses about what it most probably means.

I am not sure if my thoughts about this are not getting too tangled and strayed from reality, but I still feel like there are more pros over cons with this schema. Actually are there any cons? Except for the possible tooling side?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you feel strongly about this I'll not stand in your way any further.

I'm undecided if I agree that a pre-release suffix is a better strategy than using the regular major/minor/patch versions. My issue with this it is that according to semver:

A pre-release version indicates that the version is unstable and might not satisfy the intended compatibility requirements as denoted by its associated normal version

So this would convey to devs that it is not a stable version that should not be used without accepting some instability consequences.

I also do not know if AEgir will support you on this 😛

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmmm. Agree that this is a bit unfortunate. I checked finally SemVer spec and it directly supports "build metadata", which I feel could be exactly used for this. It stands aside in determining version precedence, which is a good thing.

We could have then schema <semver>+<latestMigrationVersion> (i.e 0.1.0+7) to meet both points... Only thing I am not sure is about the format of the metadata. Should it be just integer of the version or somethings like +migration.<int>? It would be more self-explanatory, but longer...


## Contribute

There are some ways you can make this module better:

- Consult our [open issues](https://github.com/ipfs/js-ipfs-repo/issues) and take on one of them
- Help our tests reach 100% coverage!

This repository falls under the IPFS [Code of Conduct](https://github.com/ipfs/community/blob/master/code-of-conduct.md).

[![](https://cdn.rawgit.com/jbenet/contribute-ipfs-gif/master/img/contribute.gif)](https://github.com/ipfs/community/blob/master/contributing.md)

## License

[MIT](LICENSE)
14 changes: 14 additions & 0 deletions migrations/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
'use strict'

// Do not modify this file manually as it will be overridden when running 'add' CLI command.
// Modify migration-templates.js file

const emptyMigration = {
description: 'Empty migration.',
migrate: () => {},
revert: () => {},
empty: true,
}

module.exports = [
]
Loading