序章

請求書は、企業が顧客やクライアントに提示できる商品やサービスの文書です。

デジタル請求ツールは、クライアントを追跡し、サービスと価格を記録し、支払い済みの請求書のステータスを更新し、請求書を表示するためのインターフェイスを提供する必要があります。 これには、CRUD(作成、読み取り、更新、削除)、データベース、およびルーティングが必要になります。

注:これは3部構成のシリーズのパート1です。 2番目のチュートリアルは、ノードを使用して軽量の請求書作成アプリを構築する方法:ユーザーインターフェイスです。 3番目のチュートリアルは、 Vueとノードを使用して軽量の請求書作成アプリを構築する方法:JWT認証と請求書の送信です。

このチュートリアルでは、VueNodeJSを使用して請求書発行アプリケーションを構築します。 このアプリケーションは、請求書の作成、送信、編集、削除などの機能を実行します。

前提条件

このチュートリアルを完了するには、次のものが必要です。

注: SQLiteは現在、デフォルトでmacOSおよびMacOSXにプリインストールされています。

このチュートリアルは、Node v16.1.0、npm v7.12.1、およびSQLitev3.32.3で検証されました。

ステップ1—プロジェクトの設定

要件がすべて設定されたので、次に行うことは、アプリケーションのバックエンドサーバーを作成することです。 バックエンドサーバーはデータベース接続を維持します。

新しいプロジェクトのディレクトリを作成することから始めます。

  1. mkdir invoicing-app

新しく作成されたプロジェクトディレクトリに移動します。

  1. cd invoicing-app

次に、ノードプロジェクトとして初期化します。

  1. npm init -y

サーバーが適切に機能するためには、インストールする必要のあるノードパッケージがいくつかあります。 次のコマンドを実行して、それらをインストールできます。

  1. npm install bcrypt@5.0.1 bluebird@3.7.2 cors@2.8.5 express@4.17.1 lodash@4.17.21 multer@1.4.2 sqlite3@5.0.2^> umzug@2.3.0<^>

このコマンドは、次のパッケージをインストールします。

  • bcryptユーザーパスワードをハッシュする
  • bluebird移行を作成するときにPromiseを使用する
  • corsクロスオリジンリソースシェアリング用
  • expressは、Webアプリケーションを強化します
  • ユーティリティメソッドの場合はlodash
  • multerは着信フォームリクエストを処理します
  • sqlite3は、データベースを作成および保守します
  • umzugデータベース移行を実行するタスクランナーとして

注:最初の発行以降、このチュートリアルはisEmpty()lodashを含むように更新されました。 multipart/form-dataを処理するミドルウェアライブラリがconnect-multipartyからmulterに変更されました。

アプリケーションロジックを格納するserver.jsファイルを作成します。 server.jsファイルで、必要なモジュールをインポートし、Expressアプリを作成します。

server.js
const express = require('express');
const cors = require('cors');
const sqlite3 = require('sqlite3').verbose();
const PORT = process.env.PORT || 3128;

const app = express();

app.use(express.urlencoded({extended: false}));
app.use(express.json());

app.use(cors());

// ...

/ルートを作成して、サーバーが機能することをテストします。

server.js
// ...

app.get('/', function(req, res) {
  res.send('Welcome to Invoicing App.');
});

app.listen()は、着信ルートをリッスンするポートをサーバーに指示します。

server.js
// ...

app.listen(PORT, function() {
  console.log(`App running on localhost:${PORT}.`);
});

サーバーを起動するには、プロジェクトディレクトリで次を実行します。

  1. node server

これで、アプリケーションは着信要求のリッスンを開始します。

ステップ2—SQLiteを使用したデータベースの作成と接続

請求書発行アプリケーションの場合、既存の請求書を保存するためのデータベースが必要です。 SQLiteは、このアプリケーションに最適なデータベースクライアントになります。

databaseフォルダーを作成することから始めます。

  1. mkdir database

sqlite3クライアントを実行し、次の新しいディレクトリにデータベース用のInvoicingApp.dbファイルを作成します。

  1. sqlite3 database/InvoicingApp.db

データベースが選択されたので、次は必要なテーブルを作成します。

このアプリケーションは、次の3つのテーブルを使用します。

  • 「ユーザー」-これにはユーザーデータが含まれます(idnameemailcompany_namepassword
  • 「請求書」-請求書のデータを保存します(idnamepaiduser_id
  • 「トランザクション」-請求書を作成するために一緒になる単一のトランザクション(namepriceinvoice_id

必要なテーブルが特定されたので、次のステップはテーブルを作成するためのクエリを実行することです。

移行は、アプリケーションの成長に合わせてデータベースの変更を追跡するために使用されます。 これを行うには、databaseディレクトリにmigrationsフォルダを作成します。

  1. mkdir database/migrations

これは、すべての移行ファイルの場所になります。

次に、migrationsフォルダーに1.0.jsファイルを作成します。 この命名規則は、最新の変更を追跡するためのものです。

1.0.jsファイルで、最初にノードモジュールをインポートします。

データベース/移行1.0.js
"use strict";
const path = require('path');
const Promise = require('bluebird');
const sqlite3 = require('sqlite3');

// ...

次に、移行ファイルの実行時に実行されるup関数と、データベースへの変更を元に戻すdown関数をエクスポートします。

database / migrations / 1.0.js
// ...

module.exports = {
  up: function() {
    return new Promise(function(resolve, reject) {
      let db = new sqlite3.Database('./database/InvoicingApp.db');

      db.run(`PRAGMA foreign_keys = ON`);

      // ...

up関数では、最初にデータベースへの接続が行われます。 次に、sqliteデータベースで外部キーが有効になります。 SQLiteでは、下位互換性を確保するために外部キーはデフォルトで無効になっているため、すべての接続で外部キーを有効にする必要があります。

次に、テーブルを作成するためのクエリを指定します。

database / migrations / 1.0.js
// ...

      db.serialize(function() {
        db.run(`CREATE TABLE users (
          id INTEGER PRIMARY KEY,
          name TEXT,
          email TEXT,
          company_name TEXT,
          password TEXT
        )`);

        db.run(`CREATE TABLE invoices (
          id INTEGER PRIMARY KEY,
          name TEXT,
          user_id INTEGER,
          paid NUMERIC,
          FOREIGN KEY(user_id) REFERENCES users(id)
        )`);

        db.run(`CREATE TABLE transactions (
          id INTEGER PRIMARY KEY,
          name TEXT,
          price INTEGER,
          invoice_id INTEGER,
          FOREIGN KEY(invoice_id) REFERENCES invoices(id)
        )`);
      });

      db.close();
    });
  }

}

serialize()関数は、クエリが同時にではなく順次実行されるように指定するために使用されます。

移行ファイルが作成されたら、次のステップはそれらを実行してデータベースに変更を加えることです。 これを行うには、アプリケーションのルートからscriptsフォルダーを作成します。

  1. mkdir scripts

次に、この新しいディレクトリにmigrate.jsというファイルを作成します。 そして、migrate.jsファイルに以下を追加します。

scripts / migrate.js
const path = require('path');
const Umzug = require('umzug');

let umzug = new Umzug({
  logging: function() {
    console.log.apply(null, arguments);
  },
  migrations: {
    path: './database/migrations',
    pattern: /\.js$/
  },
  upName: 'up'
});

// ...

まず、必要なノードモジュールがインポートされます。 次に、新しいumzugオブジェクトが構成とともに作成されます。 移行スクリプトのpathおよびpatternも指定されています。 構成の詳細については、 umzugREADMEを参照してください。

また、詳細なフィードバックを提供するには、以下に示すようにイベントをログに記録する関数を作成し、最後にup関数を実行して、移行フォルダーで指定されたデータベースクエリを実行します。

scripts / migrate.js
// ...

function logUmzugEvent(eventName) {
  return function(name, migration) {
    console.log(`${name} ${eventName}`);
  };
}

// using event listeners to log events
umzug.on('migrating', logUmzugEvent('migrating'));
umzug.on('migrated', logUmzugEvent('migrated'));
umzug.on('reverting', logUmzugEvent('reverting'));
umzug.on('reverted', logUmzugEvent('reverted'));

// this will run your migrations
umzug.up().then(console.log('all migrations done'));

ここで、スクリプトを実行するには、ターミナルに移動し、アプリケーションのルートディレクトリで次のコマンドを実行します。

  1. node scripts/migrate.js

次のような出力が表示されます。

Output
all migrations done == 1.0: migrating ======= 1.0 migrating

この時点で、migrate.jsスクリプトを実行すると、1.0.js構成がInvoicingApp.dbに適用されます。

ステップ3—アプリケーションルートの作成

データベースが適切に設定されたので、次はserver.jsファイルに戻ってアプリケーションルートを作成します。 このアプリケーションでは、次のルートが利用可能になります。

URL 方法 働き
/register POST 新規ユーザーを登録するには
/login POST 既存のユーザーにログインするには
/invoice POST 新しい請求書を作成するには
/invoice/user/{user_id} GET ユーザーのすべての請求書を取得するには
/invoice/user/{user_id}/{invoice_id} GET 特定の請求書を取得するには
/invoice/send POST クライアントに請求書を送信するには

POST /register

新しいユーザーを登録するために、サーバーの/registerルートに対してPOSTリクエストが行われます。

server.jsに再度アクセスし、次のコード行を追加します。

server.js
// ...

const _ = require('lodash');

const multer  = require('multer');
const upload = multer();

const bcrypt = require('bcrypt');
const saltRounds = 10;

// POST /register - begin

app.post('/register', upload.none(), function(req, res) {
  // check to make sure none of the fields are empty
  if (
    _.isEmpty(req.body.name)
    || _.isEmpty(req.body.email)
    || _.isEmpty(req.body.company_name)
    || _.isEmpty(req.body.password)
  ) {
    return res.json({
      "status": false,
      "message": "All fields are required."
    });
  }

  // any other intended checks

// ...

空のフィールドがあるかどうか、および送信されるデータがすべての仕様に一致するかどうかを確認するためのチェックが行われます。 エラーが発生した場合、エラーメッセージが応答としてユーザーに送信されます。 そうでない場合は、パスワードがハッシュされ、データがデータベースに保存され、ユーザーに登録されたことを通知する応答が送信されます。

server.js
// ...

    bcrypt.hash(req.body.password, saltRounds, function(err, hash) {

    let db = new sqlite3.Database('./database/InvoicingApp.db');

    let sql = `INSERT INTO
                users(
                  name,
                  email,
                  company_name,
                  password
                )
                VALUES(
                  '${req.body.name}',
                  '${req.body.email}',
                  '${req.body.company_name}',
                  '${hash}'
                )`;

    db.run(sql, function(err) {
      if (err) {
        throw err;
      } else {
        return res.json({
          "status": true,
          "message": "User Created."
        });
      }
    });

    db.close();
  });

});

// POST /register - end

ここで、Postmanなどのツールを使用してPOSTリクエストをnameemailcompany_namepassword/registerに送信するとします。 、新しいユーザーを作成します:

価値
name テストユーザー
email [email protected]
company_name テスト会社
password パスワード

クエリを使用してUsersテーブルを表示し、ユーザーの作成を確認できます。

  1. select * from users;

これで、データベースに新しく作成されたユーザーが含まれます。

Output
1|Test User|[email protected]|Test Company|[hashed password]

これで、/registerルートが確認されました。

POST /login

既存のユーザーが/loginルートを使用してシステムにログインしようとする場合、ユーザーは自分の電子メールアドレスとパスワードを提供する必要があります。 それを行うと、ルートは次のようにリクエストを処理します。

server.js
// ...

// POST /login - begin

app.post('/login', upload.none(), function(req, res) {
  let db = new sqlite3.Database('./database/InvoicingApp.db');

  let sql = `SELECT * from users where email='${req.body.email}'`;

  db.all(sql, [], (err, rows) => {
    if (err) {
      throw err;
    }

    db.close();

    if (rows.length == 0) {
      return res.json({
        "status": false,
        "message": "Sorry, wrong email."
      });
    }

// ...

特定の電子メールを持つユーザーのレコードをフェッチするために、データベースに対してクエリが実行されます。 結果が空の配列を返す場合は、ユーザーが存在しないことを意味し、エラーをユーザーに通知する応答が送信されます。

データベースクエリがユーザーデータを返す場合、入力されたパスワードがデータベース内のそのパスワードと一致するかどうかを確認するために、さらにチェックが行われます。 含まれている場合は、ユーザーデータとともに応答が送信されます。

server.js
// ...

    let user = rows[0];

    let authenticated = bcrypt.compareSync(req.body.password, user.password);

    delete user.password;

    if (authenticated) {
      return res.json({
        "status": true,
        "user": user
      });
    }

    return res.json({
      "status": false,
      "message": "Wrong password. Please retry."
    });
  });

});

// POST /login - end

// ...

ルートがテストされると、成功または失敗した結果が表示されます。

これで、Postmanなどのツールを使用してemailおよびpasswordを使用して/loginにPOSTリクエストを送信すると、応答が返されます。

価値
email [email protected]
password パスワード

このユーザーはデータベースに存在するため、次の応答が返されます。

Output
{ "status": true, "user": { "id": 1, "name": "Test User", "email": "[email protected]", "company_name": "Test Company" } }

これで、/loginルートが確認されました。

POST /invoice

/invoiceルートは、請求書の作成を処理します。 ルートに渡されるデータには、ユーザーID、請求書の名前、および請求書のステータスが含まれます。 また、請求書を構成するための単一のトランザクションも含まれます。

サーバーはリクエストを次のように処理します。

server.js
// ...

// POST /invoice - begin

app.post('/invoice', upload.none(), function(req, res) {
  // validate data
  if (_.isEmpty(req.body.name)) {
    return res.json({
      "status": false,
      "message": "Invoice needs a name."
    });
  }

  // perform other checks

// ...

最初に、サーバーに送信されたデータが検証されます。 次に、後続のクエリのためにデータベースへの接続が確立されます。

server.js
// ...

  // create invoice
  let db = new sqlite3.Database('./database/InvoicingApp.db');

  let sql = `INSERT INTO invoices(
                name,
                user_id,
                paid
              )
              VALUES(
                '${req.body.name}',
                '${req.body.user_id}',
                0
              )`;

// ...

請求書の作成に必要なINSERTクエリが書き込まれ、実行されます。 その後、単一のトランザクションがtransactionsテーブルに挿入され、それらを参照するための外部キーとしてinvoice_idが使用されます。

server.js
// ...

  db.serialize(function() {
    db.run(sql, function(err) {
      if (err) {
        throw err;
      }

      let invoice_id = this.lastID;

      for (let i = 0; i < req.body.txn_names.length; i++) {
        let query = `INSERT INTO
                      transactions(
                        name,
                        price,
                        invoice_id
                      ) VALUES(
                        '${req.body.txn_names[i]}',
                        '${req.body.txn_prices[i]}',
                        '${invoice_id}'
                      )`;

        db.run(query);
      }

      return res.json({
        "status": true,
        "message": "Invoice created."
      });
    });
  });

});
// POST /invoice - end

// ...

ここで、Postmanなどのツールを使用してPOSTリクエストをnameuser_idtxn_namestxn_prices/invoiceに送信するとします。 、新しい請求書を作成し、トランザクションを記録します。

価値
name テスト請求書
user_id 1
txn_names iPhone
txn_prices 600
txt_names マックブック
txn_prices 1700

次に、請求書テーブルを確認します。

  1. select * from invoices;

次の結果を確認します。

Output
1|Test Invoice|1|0

次のコマンドを実行します。

  1. select * from transactions;

次の結果を確認します。

Output
1|iPhone|600|1 2|Macbook|1700|1

これで、/invoiceルートが確認されました。

GET /invoice/user/{user_id}

これで、ユーザーが作成されたすべての請求書を表示したい場合、クライアントはGETリクエストを/invoice/user/:idルートに送信します。 user_idはルートパラメータとして渡されます。 リクエストは次のように処理されます。

index.js
// ...

// GET /invoice/user/:user_id - begin

app.get('/invoice/user/:user_id', upload.none(), function(req, res) {
  let db = new sqlite3.Database('./database/InvoicingApp.db');

  let sql = `SELECT * FROM invoices WHERE user_id='${req.params.user_id}' ORDER BY invoices.id`;

  db.all(sql, [], (err, rows) => {
    if (err) {
      throw err;
    }

    return res.json({
      "status": true,
      "invoices": rows
    });
  });
});

// GET /invoice/user/:user_id - end

// ...

クエリが実行され、特定のユーザーに属するすべての請求書と請求書に関連するトランザクションがフェッチされます。

ユーザーのすべての請求書のリクエストについて考えてみます。

localhost:3128/invoice/user/1

次のデータで応答します。

Output
{"status":true,"invoices":[{"id":1,"name":"Test Invoice","user_id":1,"paid":0}]}

これで、/invoice/user/:user_idルートが確認されました。

GET /invoice/user/{user_id}/{invoice_id}

特定の請求書を取得するには、user_idおよびinvoice_idを使用して/invoice/user/{user_id}/{invoice_id}ルートにGETリクエストを送信します。 リクエストは次のように処理されます。

index.js
// ...

// GET /invoice/user/:user_id/:invoice_id - begin

app.get('/invoice/user/:user_id/:invoice_id', upload.none(), function(req, res) {
  let db = new sqlite3.Database('./database/InvoicingApp.db');

  let sql = `SELECT * FROM invoices LEFT JOIN transactions ON invoices.id=transactions.invoice_id WHERE user_id='${req.params.user_id}' AND invoice_id='${req.params.invoice_id}' ORDER BY transactions.id`;

  db.all(sql, [], (err, rows) => {
    if (err) {
      throw err;
    }

    return res.json({
      "status": true,
      "transactions": rows
    });
  });
});

// GET /invoice/user/:user_id/:invoice_id - end

// set application port
// ...

クエリを実行して、単一の請求書と、ユーザーに属する請求書に関連するトランザクションをフェッチします。

ユーザーの特定の請求書のリクエストについて考えてみます。

localhost:3128/invoice/user/1/1

次のデータで応答します。

Output
{"status":true,"transactions":[{"id":1,"name":"iPhone","user_id":1,"paid":0,"price":600,"invoice_id":1},{"id":2,"name":"Macbook","user_id":1,"paid":0,"price":1700,"invoice_id":1}]}

これで、/invoice/user/:user_id/:invoice_idルートが確認されました。

結論

このチュートリアルでは、軽量の請求書発行アプリケーションに必要なすべてのルートを使用してサーバーをセットアップします。

ノードを使用して軽量の請求書作成アプリを構築する方法:ユーザーインターフェイスで学習を続けてください。