Pt 2: Building Your Own AdonisJS Package

in #utopian-io6 years ago (edited)

Repository

AdonisJS Repository

Last time on the series, we setup the structure for our first package. We also worked with the CLI (Command Line Interface) and we provided configurable options for our users. Today, we'll be delving much deeper. We'll setup package specific data management. We'll also add some features to allow us switch between multiple APIs for our quotes. Finally, we'll be publishing this to the NPM registry.

Difficulty

  • Intermediate

What Will I Learn?

At the end of this tutorial, we should have a fully functional Adonis package. We'll cover the following concepts.

  • Setting Up Package Specific Data Management.
  • Writing to The Filing System through NPM.
  • Writing Tests For Our Package with Japa.
  • Publishing to the NPM Registry.

Requirements

Introduction

On our previous tutorial in this series, we examined a lot of stuff within the course of which we built the initial structure of an Adonis package we'll be calling Sophos. Sophos allows us to get a wisdom quote through multiple entry points (web, api, cli).

In this tutorial, we'll be adding a couple of features to the package as well as preparing for a release to the NPM registry.

Briefing

Our package structure resembles the structure below at the moment:

  • config/
    • index.js
  • providers/
    • SophosProvider.js
  • src/
    • Sophos.js
  • instructions.js
  • instructions.md
  • package.json
  • README.md

Let's Get Straight To Work.

Previously, we added functionality that allowed us to fetch an inspirational random quote concerning startups from the wisdom quotes api. Since we're developers deeply in love with variety, we can do much better than just startups. We can allow our users also get great quotes on design from icons like Jony Ive. This is possible due to the good graces of QuotesOnDesign.com who've graciously provided an API we can work with.

If we head back into our src/Sophos.js, we begin our class with the following lines:

 class Sophos {
    sourceURL = 'https://wisdomapi.herokuapp.com/v1/';
}

Not good. Our resource endpoint is not flexible. We can do better by making this configurable by our end user. To do this, we'll add an array of objects to our configuration file which we'll be using to allow us switch resource endpoints effortlessly.

Heading back into config/index.js, let's make some changes; we'll change the sourceURL string property to a sourceURLs object to help us accommodate a wider range of options.

'use strict'

/*
|--------------------------------------------------------------------------
| Sophos
|--------------------------------------------------------------------------
|
| Sophos returns bits of inspiration for your next big design and startup ideas.
|
*/

module.exports = {
  /*
  |--------------------------------------------------------------------------
  | Source URLs
  |--------------------------------------------------------------------------
  |
  | The URLs of various resources queried for data.
  |
  */
  sourceURLs: {
      'startups': 'https://wisdomapi.herokuapp.com/v1/random',
      'design': 'http://quotesondesign.com/wp-json/posts?filter[orderby]=rand&filter[posts_per_page]=1'
  },
}

We added two key-value pairs to our sourceURLs map for resources mapping to various quote categories. Next, we simply need to modify our Sophos class to accommodate the changes.

 /**
  * The Sophos class makes a request to a url returning a promise
  * resolved with data or rejected with an error.
  *
  * @class Sophos
  *
  * @param {Object} Config
  */
 class Sophos {
    sourceURLs = {};

    constructor (Config) {
     this.config = Config.merge('sophos', {
       sourceURLs: this.sourceURLs
     })
    }

    getQuotes (category = 'startups') {
        return new Promise((resolve, reject) => {
            if (!this.config.sourceURLs.hasOwnProperty(category)) {
                reject({
                    error: `Sorry, Sophos does not support requests for "${category}"  at the moment.` 
                })
            }

            let endpoint = `${this.config.sourceURLs[category]}`
            
            request(endpoint, { json: true }, (err, res, body) => {
              if (err) return reject(err);
              return resolve(body)
            });
        });
    }
}

Great, we've just added some more custom options to our code. Our package is now capable of fetching quotes on design or startups from two different URLs. We also added a check to make sure our users request a supported category. If our check fails, we reject the promise and return a little error message to our user.

Writing to the Filing System Using the CLI Helper.

We'll be adding some functionality that requires data management practices. We'd like users to be able to be able to store a collection of favorite quotes simply by providing a user_id and the URL of the resource of interest. To do this, we'd need to publish migration files to the database/migrations folder of the Adonis application leveraging this package. Let's create our templates/SophosQuoteSchema.js migration template.

'use strict'

const Schema = use('Schema')

class SophosQuoteSchema extends Schema {
  up () {
    this.create('sophos_quotes', table => {
      table.increments()
      table.integer('user_id').notNullable();
      table.string('quote_url', 80).notNullable()
      table.timestamps()
    })
  }

  down () {
    this.drop('sophos_quotes')
  }
}

module.exports = SophosQuoteSchema

This migration template will be published to database/migration at installation. We are creating tables with the sophos namespace to prevent namespace collisions between our package migrations and other third party package migrations. This is a pretty good time to add an accompanying Lucid model for our SophosQuotes schema. Let's create templates/SophosQuote.js and add some code.

'use strict'

const Model = use('Model')

class SophosQuote extends Model {

  /**
   * A relationship on users is required as a means of identification.
   *
   * @method user
   *
   * @return {Object}
   */
  user () {
    return this.belongsTo('App/Models/User')
  }
}

module.exports = SophosQuote

We'll update instructions.js to copy our migrations and models at runtime. Open up instructions.js and modify as below.

'use strict'

const path = require('path')

async function copyQuoteMigration (cli) {
  try {
    const migrationsFile = cli.helpers.migrationsPath(`${new Date().getTime()}_sophos_quote.js`)
    await cli.copy(
      path.join(__dirname, 'templates', 'SophosQuoteSchema.js'),
      path.join(migrationsFile)
    )
    cli.command.completed('create', migrationsFile.replace(cli.helpers.appRoot(), '').replace(path.sep, ''))
  } catch (error) {
    // ignore error
  }
}

async function copyQuoteModel (cli) {
  try {
    await cli.copy(
      path.join(__dirname, 'templates', 'SophosQuote.js'),
      path.join(cli.appDir, 'Models/SophosQuote.js')
    )
    cli.command.completed('create', 'Models/SophosQuote.js')
  } catch (error) {
    // ignore error
  }
}

async function makeConfigFile (cli) {
    try {
      const inFile = path.join(__dirname, './config', 'index.js')
      const outFile = path.join(cli.helpers.configPath(), 'sophos.js')
      await cli.copy(inFile, outFile)
      cli.command.completed('create', 'config/sophos.js')
    } catch (error) {
      // ignore error
    }
}

module.exports = {
    await makeConfigFile(cli)
    await copyQuoteModel(cli)
    await copyQuoteMigration(cli)
}

Comparing our updated instructions.js module to our previous instructions.js module, you'll notice we abstracted our code into a series of async functions. We then export these asynchronous functions in module.exports.

Setting Up Package Specific Data Management

We'd love to provide a means for our users to be able to save their favorite quotes for ease of use at a later date. To do this, we must add a model string property to our config specifying the proper model we'll be using to save this information. Its important to note that, when you're developing packages, you should prioritize ease of customization above every other criteria. We'll add this setting to our config/index.js file.


module.exports = {
  /*
  |--------------------------------------------------------------------------
  | Model
  |--------------------------------------------------------------------------
  |
  | The model to be used for Sophos' quotes
  |
  */
  model: 'App/Models/SophosQuote',

}

Next, we'll write a bit of functionality to the src/Sophos class. We'll add the asynchronous saveQuote method.

    /*
     * Saves a quote for referencing later.
     */
    async saveQuote (attributes = null) {
        if (!attributes || typeof attributes !== 'object') throw('Required argument "attributes" was not provided.');
        const quote = await this._getModel().create(await)
    }

If the method fails to receive any attributes as an argument or if the attributes supplied is not an object, we throw an error and keep it movin'. Next, we call the this._getModel method that we define below.

    _getModel () {
        return use(this.config.model);
    }

Next, we need to provide a method that allows us to return the saved quotes (or quote, as the case may be). We'll call this method getSavedQuotes and define it below.

    /*
     * Returns all saved quotes.
     * If an id param is provided, returns a single quote.
     */
    async getSavedQuotes (id = nulll) {
        let self = this;
        return new Promise((resolve, reject) => {
            if (id) {
                return resolve(
                    await self._getModel()
                        .query()
                        .where('id', id)
                        .first()
                )
            }

            // return all saved quotes.
            return await self._getModel().query();
        });
    }

Here, we'll return a single Quote object if an id is provided and an array of Quote objects otherwise. We're doing great! Now all we need to do is write some tests for our code.

Writing Tests For Our Package with Japa.

A good package is properly tested. We'd like to write tests for our package and we can do this with the help of the Japa NPM module. Let's start by creating a file called japaFile.js. It will help us run our tests.

'use strict'

const cli = require('japa/cli')
cli.run('test/**/*.spec.js')

Next, we'll be writing a couple of tests. We'll be testing the save and retrieve quote functionality which we just wrote. Let's add some assertions and code.

'use strict'

/*
 * sophos
 *
 * (c) Caleb Mathew <[email protected]>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
*/

const test = require('japa')
const { Config } = require('@adonisjs/sink')
const Sophos = require('../src/Sophos')

test.group('Sophos', () => {
    test('should save a quote to the database', async (assert) => {
      const data = {
          user_id: 1,
          quote_url: 'https://quotesondesign.com/apple-commercial/'
      };

      let result = null;

      const Sophos = new Sophos()

      Sophos.saveQuote(data).then(quote => {
          result = quote
      }).catch(err => console.log(err))

      assert.equal(result.user_id, data.user_id)
      assert.equal(result.quote_url, data.quote_url)
    })

    test('should retrieve quote #1', async (assert) => {
      const id = 1;

      let result = null;

      const Sophos = new Sophos()

      Sophos.getQuotes(data).then(quote => {
          result = quote
      }).catch(err => console.log(err))

      assert.equal(result.id, id)
    })
})

Here, we are writing a couple of assertions that utilize our Sophos class. We are making sure the data passed to our class methods return expected values.

Publishing to the NPM Registry.

We are now ready to publish our package to the NPM registry. To get started, we must create an npmjs.org account. Alternatively, you can create an account from the terminal by running and following the prompts

npm adduser

Your screen should look something like the below

create-npm-user.png

If you were successful, running npm whoami should return your username.

Preparing Our Package For the World

We'll prepare our package for the world now. We need to test our package locally before taking it online. We'll test our package by including it in another running adonis project. We can let the global npm node_modules folder know about our package by creating a symbolic link or symlink. We can do this by running this command at the root of our package.

npm link

This will create a symlink in the global node_modules folder making our package accessible.

Next, we have to move over to an AdonisJS project directory and link our package to make it accessible. I'll be using the Homely project referenced in this series for this.

cd ../homely && npm link adonis-sophos

Great, we've linked our package locally. Running adonis serve --dev and if you don't get an error, you've successfully tested your package.

Finally, let's run the below command to publish our code to the NPM registry.

cd ../sophos
npm publish

Conclusion

We've covered some pretty important concepts in this tutorial. We examined a different approach to providing a configurable package. We also setup package specific data management and we wrote tests for our package. Finally, we published our package to the NPM registry available at https://www.npmjs.com/package/adonis-sophos

Resources

Curriculum

Sort:  

Tutorial very well structured, thank you for your contribution.
Keep up the good work.


Need help? Write a ticket on https://support.utopian.io/.
Chat with us on Discord.
[utopian-moderator]

Hey @creatrixity
Thanks for contributing on Utopian.
We're already looking forward to your next contribution!

Contributing on Utopian
Learn how to contribute on our website or by watching this tutorial on Youtube.

Want to chat? Join us on Discord https://discord.gg/h52nFrV.

Vote for Utopian Witness!

Coin Marketplace

STEEM 0.20
TRX 0.15
JST 0.029
BTC 63362.14
ETH 2592.64
USDT 1.00
SBD 2.80