AdonisJS

Adonis JS - 4. Seeds, Factories

Updated:

In this section, we will be learning about seeds, factories, and how to edit and delete our books.

Seeds &  Factories

Seeds & Factories allow us to populate our databases with "dummy" data which makes building and prototyping our application much easier. Let's create a seed and factory to populate our books table with more books.

To create a seed for our books, run the following command:

adonis make:seed Book

This will create a new file database/seeds/BookSeeder.js which looks like:

"use strict";

/*
    |--------------------------------------------------------------------------
    | BookSeeder
    |--------------------------------------------------------------------------
    |
    | Make use of the Factory instance to seed database with dummy data or
    | make use of Lucid models directly.
    |
    */

/** @type {import('@adonisjs/lucid/src/Factory')} */
const Factory = use("Factory");

class BookSeeder {
  async run() {}
}

module.exports = BookSeeder;

We now need to make a factory which will generate our dummy data for our books. Under the hood, Adonis uses Chance.js to generate this dummy data.

Update database/factory.js with the following:

"use strict";

/*
    |--------------------------------------------------------------------------
    | Factory
    |--------------------------------------------------------------------------
    |
    | Factories are used to define blueprints for database tables or Lucid
    | models. Later you can use these blueprints to seed your database
    | with dummy data.
    |
    */

/** @type {import('@adonisjs/lucid/src/Factory')} */
const Factory = use("Factory");

Factory.blueprint("App/Models/Book", (faker) => {
  return {
    title: faker.sentence({ words: 5 }),
    author: faker.name(),
    cover_image:
      "https://ismbook.com/wp-content/uploads/2018/12/best-philosophy-books.jpg",
    isbn: faker.string({ length: 10, numeric: true }),
  };
});

First, we are telling the factory to use our Book model Factory.blueprint("App/Models/Book", (faker) =>

Then we are populating the book properties with dummy data generated by Chance.js.

  1. title: faker.sentence({ words: 5 }) - title - is going to be a sentence of 5 random words:
  2. author: faker.name(), - author - is going to be a random fake name
  3. cover_image: "https://ismbook.com/wp-content/uploads/2018/12/best-philosophy-books.jpg", - is simply a url of an image of some books I found on google. You can replace this with whatever book image you would like.
  4. isbn: faker.string({ length: 10, numeric: true }), - isbn - random number which is 10 numbers long.

Now we need to tell our BookSeeder.js file to use this factory. Update it with the following:

"use strict";

/*
    |--------------------------------------------------------------------------
    | BookSeeder
    |--------------------------------------------------------------------------
    |
    | Make use of the Factory instance to seed database with dummy data or
    | make use of Lucid models directly.
    |
    */

/** @type {import('@adonisjs/lucid/src/Factory')} */
const Factory = use("Factory");

class BookSeeder {
  async run() {
    const booksArray = await Factory.model("App/Models/Book").createMany(7);
  }
}

module.exports = BookSeeder;

We are telling the book seeder to use the factory we just created and to create 7 books. Feel free to generate as many or as few books as you would like.

Now run the following command to execute our seeder and generate our books

adonis seed

If all goes well you should see a message like Seeded database in 200ms output into the console. Let's check our database to see our new books. seeded books in database Now that we have some books in our database, let's update our homepage to display these books. Currently, we are still showing hard coded data.

Index view

Let's update our book/index.edge view to display all of the properties of our books in a table.

@layout('layouts.default')

@section('content')
  <h1>Books Index</h1>

  @if(old('notification'))
    <div class="alert alert-success">
      {{ old('notification') }}
    </div>
  @endif

  <table class="table">
    <thead>
      <tr>
        <th scope="col">Cover Image</th>
        <th scope="col">Title</th>
        <th scope="col">Author</th>
        <th scope="col">ISBN</th>
      </tr>
    </thead>
    <tbody>
      @each(book in books)
        <tr>
          <td><img src="{{ book.cover_image }}" alt="{{ book.title }}" style="width: 100px;"></td>
          <td scope="row">{{ book.title }}</td>
          <td>{{ book.author }}</td>
          <td>{{ book.isbn }}</td>
        </tr>
      @endeach

    </tbody>
  </table>
@endsection

Next, we need to update our index method to query our books table in the database.

async index({ view }) {
  const books = await Book.all();

  return view.render("book.index", {
    books: books.toJSON(),
  });
}

We are grabbing all of the books and then passing them to our view. We need to use the .toJSON() method when accessing data from our database.

Our home page now looks like this:

Edit Books

Now that we have the ability to create books, we also need to be able to edit them. First, let's create a new route and view to display a single book.

adonis make:view book/show

Within our new show.edge file, add the following:

@layout('layouts.default')

@section('content')
  <table class="table">
    <thead>
      <tr>
        <th scope="col">Cover Image</th>
        <th scope="col">Title</th>
        <th scope="col">Author</th>
        <th scope="col">ISBN</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <td><img src="{{ book.cover_image }}" alt="{{ book.title }}" style="width: 100px;"></td>
        <th scope="row">{{ book.title }}</th>
        <td>{{ book.author }}</td>
        <td>{{ book.isbn }}</td>
      </tr>
    </tbody>
  </table>
@endsection

Next, add the following route to the bottom of routes.js

Route.get("books/:id", "BookController.show");

The /:id allows us to take data passed into the url and use it in our views. In this case when a user navigates to /books/1 the will be taken to our show.edge view and the book with the id of 1 will be displayed.

To make viewing each book easier, without having to know the book id, we will update our homepage so that clicking on a book will take you to the books show view.

Updated book/index.edge with the following:

@layout('layouts.default')

@section('content')
  <h1>Books Index</h1>

  @if(old('notification'))
    <div class="alert alert-success">
      {{ old('notification') }}
    </div>
  @endif

  <table class="table">
    <thead>
      <tr>
        <th scope="col">Cover Image</th>
        <th scope="col">Title</th>
        <th scope="col">Author</th>
        <th scope="col">ISBN</th>
      </tr>
    </thead>
    <tbody>
      @each(book in books)
        <tr>
          <td><a href="/books/{{ book.id }}"><img src="{{ book.cover_image }}" alt="{{ book.title }}" style="width: 100px;"></a></td>
          <td scope="row"><a href="/books/{{ book.id }}">{{ book.title }}</a></td>
          <td>{{ book.author }}</td>
          <td>{{ book.isbn }}</td>
        </tr>
      @endeach

    </tbody>
  </table>
@endsection

We have added links for both the cover image and book title, so that clicking on either of them will take the user to the show view.

Now let's update our controller with a new show() method.

async show({ params, view }) {
  const book = await Book.find(params.id);

  return view.render("book.show", {
    book,
  });
}

In this new method, we are looking up a particular Book by using Book.find(params.id). The params.id is the id that gets passed into the URL, which we are grabbing from our route with Route.get("books/:id", "BookController.show"). /:id becomes params.id

Now if we click on the cover image or title we should be taken to the show view, which looks like this:

Let's add an edit button to this view, which we will then create the appropriate view, route & controller methods for.

Update show.edge with the following:

@layout('layouts.default')

@section('content')
  <table class="table">
    <thead>
      <tr>
        <th scope="col">Cover Image</th>
        <th scope="col">Title</th>
        <th scope="col">Author</th>
        <th scope="col">ISBN</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <td><img src="{{ book.cover_image }}" alt="{{ book.title }}" style="width: 100px;"></td>
        <th scope="row">{{ book.title }}</th>
        <td>{{ book.author }}</td>
        <td>{{ book.isbn }}</td>
      </tr>
    </tbody>
  </table>

  <hr>
  <a href="/books/{{ book.id }}/edit" class="btn btn-dark d-block float-left">Edit Book</a>
@endsection

Our show view now looks like:

Next, we need to create a new route to match the buttons URL /books/{{ book.id }}/edit

Update routes.js with the following:

"use strict";

/*
    |--------------------------------------------------------------------------
    | Routes
    |--------------------------------------------------------------------------
    |
    | Http routes are entry points to your web application. You can create
    | routes for different URL's and bind Controller actions to them.
    |
    | A complete guide on routing is available here.
    | http://adonisjs.com/docs/4.1/routing
    |
    */

/** @type {typeof import('@adonisjs/framework/src/Route/Manager')} */
const Route = use("Route");

// Route.on("/").render("welcome");
Route.get("/", "BookController.index");
Route.get("books/create", "BookController.create");
Route.post("books", "BookController.store");
Route.get("books/:id", "BookController.show");
Route.get("/books/:id/edit", "BookController.edit");

Now we need to create an edit() method on our controller.

async edit({ params, view }) {
  const book = await Book.find(params.id);

  return view.render("book.edit", {
    book,
  });
}

It looks almost identical to our show() method. The only difference is it is rendering a new edit view. Which we need to create.

adonis make:view book/edit

Within edit.edge add the following:

@layout('layouts.default')

@section('content')
  <h1>Edit {{ book.title }}</h1>

  <form>
    {{ csrfField() }}

    <div class="form-group">
      <label for="">Title</label>
      <input type="text" name="title" class="form-control" value="{{ book.title }}">
    </div>

    <div class="form-group">
      <label for="">Author</label>
      <input type="text" name="author" class="form-control" value="{{ book.author }}">
    </div>

    <div class="form-group">
      <label for="">Cover Image</label>
      <input type="text" name="cover_image" class="form-control" placeholder="image URL" value="{{ book.cover_image }}">
    </div>

    <div class="form-group">
      <label for="">ISBN</label>
      <input type="text" name="isbn" class="form-control" value="{{ book.isbn }}">
    </div>

    <input type="submit" value="Submit" class="btn btn-primary">
  </form>
@endsection

Now if you click on the edit book for any book you should see the following:

The form should look just like our form to create a new book, however, each field will be pre-populated with the books data.

We can now makes edits to our book. However, if we click the submit button, nothing is going to happen. We need to create a new route and method to update() our edits.

Update routes.js with the following:

"use strict";

/*
    |--------------------------------------------------------------------------
    | Routes
    |--------------------------------------------------------------------------
    |
    | Http routes are entry points to your web application. You can create
    | routes for different URL's and bind Controller actions to them.
    |
    | A complete guide on routing is available here.
    | http://adonisjs.com/docs/4.1/routing
    |
    */

/** @type {typeof import('@adonisjs/framework/src/Route/Manager')} */
const Route = use("Route");

// Route.on("/").render("welcome");
Route.get("/", "BookController.index");
Route.get("books/create", "BookController.create");
Route.post("books", "BookController.store");
Route.get("books/:id", "BookController.show");
Route.get("/books/:id/edit", "BookController.edit");
Route.put("/books/:id", "BookController.update");

Now let's create our update() on our controller.

async update({ params, request, response, session }) {
  const book = await Book.find(params.id);

  book.title: request.input("title");
  book.author = request.input("author");
  book.cover_image = request.input("cover_image");
  book.isbn = request.input("isbn");

  await book.save();

  session.flash({ notification: "Book Updated" });

  return response.redirect("/");
}

Finally, we need to update our edit.edge form action to submit to our new route.

Update edit.edge with the following:

@layout('layouts.default')

@section('content')
  <h1>Edit {{ book.title }}</h1>

  <form action="/books/{{ book.id }}?_method=PUT" method="POST">
    {{ csrfField() }}

    <div class="form-group">
      <label for="">Title</label>
      <input type="text" name="title" class="form-control" value="{{ book.title }}">
    </div>

    <div class="form-group">
      <label for="">Author</label>
      <input type="text" name="author" class="form-control" value="{{ book.author }}">
    </div>

    <div class="form-group">
      <label for="">Cover Image</label>
      <input type="text" name="cover_image" class="form-control" placeholder="image URL" value="{{ book.cover_image }}">
    </div>

    <div class="form-group">
      <label for="">ISBN</label>
      <input type="text" name="isbn" class="form-control" value="{{ book.isbn }}">
    </div>

    <input type="submit" value="Submit" class="btn btn-primary">
  </form>
@endsection

You may be wondering what this odd syntax is in our form action:

<form action="/books/{{ book.id }}?_method=PUT" method="POST"></form>

Adonis calls this Method Spoofing which makes it very easy for our forms to make PUT requests, since HTML forms can only make GET & POST requests.

Let's alter some of the data in our form and see if our changes are being persisted in our database.

If you see the flash message with "Book updated" everything should be working and you should see your edited book on the home page with your new information.

Challenge: Add validation to the edit.edge view and the update() method. Take a look at how we are doing this for our create.edge and store() method.

Delete Books

Finally we need the ability to delete a book from our database. This will be much easier than our edit functionality.

First, let's add a delete button to our show.edge view.

@layout('layouts.default')

@section('content')
  <h1>Books Index</h1>

  <table class="table">
    <thead>
      <tr>
        <th scope="col">Title</th>
        <th scope="col">Author</th>
        <th scope="col">Cover Image</th>
        <th scope="col">ISBN</th>
        <th scope="col">Available</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <th scope="row">{{ book.title }}</th>
        <td>{{ book.author }}</td>
        <td><img src="{{ book.cover_image }}" alt="{{ book.title }}" style="width: 300px;"></td>
        <td>{{ book.isbn }}</td>
        <td>{{ book.available }}</td>
      </tr>
    </tbody>
  </table>

  <hr>
  <a href="/books/{{ book.id }}/edit" class="btn btn-dark d-block float-left">Edit Book</a>

  <form action="/books/{{ book.id }}?_method=DELETE" method="POST">
    {{ csrfField() }}

    <input type="submit" class="btn btn-danger d-block float-right" value="Delete Book">
  </form>
@endsection

Notice that for our Delete button we are nesting it inside of a form, that adds method spoofing for the DELETE request.

Next, let's create our delete route.

Within routes.js add the following:

"use strict";

/*
    |--------------------------------------------------------------------------
    | Routes
    |--------------------------------------------------------------------------
    |
    | Http routes are entry points to your web application. You can create
    | routes for different URL's and bind Controller actions to them.
    |
    | A complete guide on routing is available here.
    | http://adonisjs.com/docs/4.1/routing
    |
    */

/** @type {typeof import('@adonisjs/framework/src/Route/Manager')} */
const Route = use("Route");

// Route.on("/").render("welcome");
Route.get("/", "BookController.index");
Route.get("books/create", "BookController.create");
Route.post("books", "BookController.store");
Route.get("books/:id", "BookController.show");
Route.get("/books/:id/edit", "BookController.edit");
Route.put("/books/:id", "BookController.update");
Route.delete("/books/:id", "BookController.destroy");

Now let's create this new destroy() method on our controller.

async destroy({ params, session, response }) {
  const book = await Book.find(params.id);

  await book.delete();

  session.flash({ notification: "Book Deleted" });

  return response.redirect("/");
}

Our entire Book Controller should look like this:

"use strict";

const Book = use("App/Models/Book");
const { validate } = use("Validator");

class BookController {
  async index({ view }) {
    const books = await Book.all();

    return view.render("book.index", {
      books: books.toJSON(),
    });
  }

  async create({ view }) {
    return view.render("book.create");
  }

  async store({ request, response, session }) {
    const validation = await validate(request.all(), {
      title: "required",
      author: "required",
      cover_image: "required",
      isbn: "required|min:10|max:10",
    });

    if (validation.fails()) {
      session.withErrors(validation.messages()).flashAll();
      return response.redirect("back");
    }

    const book = new Book();

    book.title: request.input("title");
    book.author = request.input("author");
    book.cover_image = request.input("cover_image");
    book.isbn = request.input("isbn");

    await book.save();

    session.flash({ notification: "Book Created" });
    return response.redirect("/");
  }

  async show({ params, view }) {
    const book = await Book.find(params.id);

    return view.render("book.show", {
      book,
    });
  }

  async edit({ params, view }) {
    const book = await Book.find(params.id);

    return view.render("book.edit", {
      book,
    });
  }

  async update({ params, request, response, session }) {
    const book = await Book.find(params.id);

    book.title: request.input("title");
    book.author = request.input("author");
    book.cover_image = request.input("cover_image");
    book.isbn = request.input("isbn");

    await book.save();

    session.flash({ notification: "Book Updated" });

    return response.redirect("/");
  }

  async destroy({ params, session, response }) {
    const book = await Book.find(params.id);

    await book.delete();

    session.flash({ notification: "Book Deleted" });

    return response.redirect("/");
  }
}

module.exports = BookController;

Let's try to delete a book and make sure everything is working properly.

You should see the "Book Deleted" and the book should no longer be displayed on the homepage.

Repo

The code for this section can be found in the repo under the branch 4-seeds-factories-edit-delete.

Wrap Up

In this section we learned how to create seeds and factories to populate our database with dummy data and how to edit and delete books.

Previous
3. Migrations, Models, Forms