From fca7f3bc73dc9fa9bfadef198304b1ba88e6a44b Mon Sep 17 00:00:00 2001 From: Carrot7712 Date: Thu, 13 Jan 2022 16:48:07 +0800 Subject: [PATCH 01/61] feat: add R01.test.js and unit-test-helpers.js --- helpers/unit-test-helper.js | 143 ++++++++++++++++++++++++++ tests/R01.test.js | 195 ++++++++++++++++++++++++++++++++++++ 2 files changed, 338 insertions(+) create mode 100644 helpers/unit-test-helper.js create mode 100644 tests/R01.test.js diff --git a/helpers/unit-test-helper.js b/helpers/unit-test-helper.js new file mode 100644 index 000000000..ceed6030a --- /dev/null +++ b/helpers/unit-test-helper.js @@ -0,0 +1,143 @@ +const SequelizeMock = require('sequelize-mock') +const proxyquire = require('proxyquire') +const sinon = require('sinon') + +const dbMock = new SequelizeMock() + +const createModelMock = (name, data, joinedTableName, sourceData) => { + const defaultValue = data[0] + const mockModel = dbMock.define(name, defaultValue, { + instanceMethods: { + update: function (changes) { + const objIndex = data.findIndex(d => d.id === this.get('id')) + data[objIndex] = { + ...data[objIndex], + ...changes + } + return Promise.resolve() + }, + destroy: function () { + if (joinedTableName) { + const userId = this.get('userId') + const restaurantId = this.get('restaurantId') + const restaurant = sourceData.find(d => d.id === restaurantId) + restaurant[joinedTableName] = restaurant[joinedTableName].filter(d => !(d.userId === userId)) + } else { + data = data && data.filter(d => d.userId !== this.get('userId') && d.restaurantId !== this.get('restaurantId')) // remove + } + } + } + }) + + // 模擬 Sequelize 行為 + // 將 mock user db 中的 findByPK 用 findOne 取代 (sequelize mock not support findByPK) + mockModel.findByPk = id => mockModel.findOne({ where: { id: id } }) + // 將 count 的 function 預設回傳假資料數目 1 + mockModel.count = () => 1 + // 因為 mock 中的 create 有問題,因此指向 upsert function, 這樣可以在 useHandler 中取得 create 呼叫 + mockModel.create = mockModel.upsert + + // modify middleware + if (joinedTableName) { + mockModel.$queryInterface.$useHandler((query, queryOptions) => { + if (query === 'upsert') { + // 新增 joinTable 資料到模擬資料 + const { userId, restaurantId } = queryOptions[0] + const restaurant = sourceData.find(d => d.id === restaurantId) + restaurant && restaurant[joinedTableName].push({ userId: userId }) + data.push(queryOptions[0]) + return Promise.resolve(data && data.map(d => mockModel.build(d))) + } else if (query === 'findAll') { + // 回傳模擬資料 + if (!data) { + return mockModel.build([defaultValue]) + } + return Promise.resolve(data ? data.map(d => mockModel.build(d)) : []) + } else if (query === 'findOne') { + const item = data.find(d => d.userId === queryOptions[0].where.userId && d.restaurantId === queryOptions[0].where.restaurantId) + if (!item) return null + + const result = mockModel.build(item) + return Promise.resolve(result) + } else if (query === 'destroy') { + // destroy 可以從 where 取得要刪除的資料 + // 因此就可以模擬將模擬資料中的資料刪除 + // 刪除模擬資料中的某一筆 joinTable 資料 + const { userId, restaurantId } = queryOptions[0].where + const restaurant = data.find(d => d.id === restaurantId) + restaurant[joinedTableName] = restaurant[joinedTableName].filter(d => !(d.userId === userId)) + return Promise.resolve(data.map(d => mockModel.build(d))) + } + }) + } else { + mockModel.$queryInterface.$useHandler((query, queryOptions, done) => { + if (query === 'upsert') { + // create 時會帶 userId 跟 restaurantId (ex: Like.create({ userId: 1, restaurantId: 2})) + const { userId, restaurantId } = queryOptions[0] + + // 新增這個 Like 的資訊到模擬資料裡 + data.push({ userId, restaurantId }) + + // 回傳模擬資料 + return Promise.resolve(mockModel.build(data)) + } else if (query === 'findAll') { + // 回傳模擬資料 + if (!data) { + return mockModel.build([defaultValue]) + } + return Promise.resolve(data ? data.map(d => mockModel.build(d)) : []) + } else if (query === 'findOne') { + let item + if (queryOptions[0].id) { + item = data.find(d => d.id === queryOptions[0].id) + } else { + item = data.find(d => d.userId === queryOptions[0].where.userId && d.restaurantId === queryOptions[0].where.restaurantId) + } + if (!item) return null + + const result = mockModel.build(item) + return Promise.resolve(result) + } else if (query === 'destroy') { + // destroy 可以從 where 取得要刪除的資料 + // 因此就可以模擬將模擬資料中的資料刪除 + const { userId, restaurantId } = queryOptions[0].where + data = data.filter(d => !(d.userId === userId && d.restaurantId === restaurantId)) + + return Promise.resolve(mockModel.build(data)) + } + }) + } + + return mockModel +} + +const createControllerProxy = (path, model) => { + const controller = proxyquire(path, { + '../models': model + }) + + return controller +} + +const mockRequest = query => { + return { + ...query, + flash: sinon.spy() + } +} +const mockResponse = () => { + return { + redirect: sinon.spy(), + render: sinon.spy() + } +} + +const mockNext = err => console.log('[ERROR]:', err) + +module.exports = { + createModelMock, + createControllerProxy, + mockRequest, + mockResponse, + mockNext +} diff --git a/tests/R01.test.js b/tests/R01.test.js new file mode 100644 index 000000000..574086d64 --- /dev/null +++ b/tests/R01.test.js @@ -0,0 +1,195 @@ +const request = require('supertest') +const chai = require('chai') +const sinon = require('sinon') +const should = chai.should() + +const app = require('../app') +const { createModelMock, createControllerProxy, mockRequest, mockResponse, mockNext } = require('../helpers/unit-test-helper'); + +describe('# R01', () => { + describe('登入測試: POST /signin', function(){ + // 以下測試會發出請求,測試資料庫內是否有作業指定的使用者資料 + // 測試資料的來源是真實的資料庫 + it('#1 密碼錯誤', function(done){ + request(app) + // 對 POST /signin 發出請求,參數是錯誤的密碼 + .post('/signin') + .type('urlencoded') + .send('email=root@example.com&password=123') + // 期待登入驗證回應失敗,重新導向 /signin + .expect('Location', '/signin') + .expect(302, done) + }) + + it('#2 帳號錯誤', function(done){ + request(app) + // 對 POST /signin 發出請求,參數是錯誤的帳號 + .post('/signin') + .type('urlencoded') + .send('email=tu&password=12345678') + // 期待登入驗證回應失敗,重新導向 /signin + .expect('Location', '/signin') + .expect(302, done) + }) + + it('#3 成功登入', function(done){ + request(app) + // 對 POST /signin 發出請求,參數是作業指定的使用者帳號密碼 + .post('/signin') + .type('urlencoded') + .send('email=root@example.com&password=12345678') + // 期待登入驗證成功,重新導向 /restaurants + .expect('Location', '/restaurants') + .expect(302, done) + }) + }); + + describe('# 使用者權限管理', function () { + // 前置準備 + before(() => { + // 製作假資料 + // 本 context 會用這筆資料進行測試 + this.UserMock = createModelMock('User', [{ + id: 1, + email: 'root@example.com', + name: 'admin', + isAdmin: false, + }]) + + // 修改 adminController 中的資料庫連線設定,由連向真實的資料庫 -> 改為連向模擬的 User table + this.adminController = createControllerProxy('../controllers/admin-controller', { User: this.UserMock }) + }) + + // 開始測試 + context('# [顯示使用者清單]', () => { + it(' GET /admin/users ', async () => { + // 模擬 request & response & next + const req = mockRequest() // 對 GET /admin/users 發出請求 + const res = mockResponse() + const next = mockNext + + // 測試作業指定的 adminController.getUsers 函式 + await this.adminController.getUsers(req, res, next) + + // getUser 正確執行的話,應呼叫 res.render + // res.render 的第 2 個參數應是 users + // 根據測試資料,users 中的第 1 筆資料,name 屬性值應該要是 'admin' + res.render.getCall(0).args[1].users[0].name.should.equal('admin') + }) + }) + + context('# [修改使用者權限] for root', () => { + before(() => { + // 製作假資料 + // 本 context 會用這筆資料進行測試 + this.UserMock = createModelMock( + 'User', + [{ + id: 1, + email: 'root@example.com', + name: 'admin', + isAdmin: true, // 是管理者 + }] + ) + + // 將 adminController 中的 User db 取代成 User mock db + this.adminController = createControllerProxy('../controllers/admin-controller', { User: this.UserMock }) + }) + + it(' PATCH /admin/users/:id ', async () => { + // 模擬 request & response & next + const req = mockRequest({ params: { id: 1 } }) // 帶入 params.id = 1,對 PATCH /admin/users/1 發出請求 + const res = mockResponse() + const next = mockNext + + // 測試作業指定的 adminController.patchUser 函式 + await this.adminController.patchUser(req, res, next) + + // patchUser 正確執行的話,應呼叫 req.flash + // req.flash 的參數應該要與下列字串一致 + req.flash.calledWith('error_messages','禁止變更 root 權限').should.be.true + + // patchUser 執行完畢,應呼叫 res.redirect 並重新導向上一頁 + res.redirect.calledWith('back').should.be.true + }) + }) + + context('# [修改使用者權限] for user (user -> admin)', () => { + before(() => { + // 製作假資料 + // 本 context 會用這筆資料進行測試 + this.UserMock = createModelMock( + 'User', + [{ + id: 1, + email: 'user@example.com', + name: 'user', + isAdmin: false, // 非管理者 + }] + ) + // 將 adminController 中的 User db 取代成 User mock db + this.adminController = createControllerProxy('../controllers/admin-controller', { User: this.UserMock }) + }) + + it(' PATCH /admin/users/:id ', async () => { + // 模擬 request & response & next + const req = mockRequest({ params: { id: 1 } }) // 帶入 params.id = 1,對 PATCH /admin/users/1 發出請求 + const res = mockResponse() + const next = mockNext + + // 測試作業指定的 adminController.patchUser 函式 + await this.adminController.patchUser(req, res, next) + + // patchUser 正確執行的話,應呼叫 req.flash + // req.flash 的參數應與下列字串一致 + req.flash.calledWith('success_messages','使用者權限變更成功').should.be.true + // patchUser 執行完畢,應呼叫 res.redirect 並重新導向 /admin/users + res.redirect.calledWith('/admin/users').should.be.true + + // patchUser 執行完畢後,假資料中 id:1 使用者的應該要是 isAdmin:true + // 將假資料撈出,比對確認有成功修改到 + const user = await this.UserMock.findOne({ where: { id: 1 } }) + user.isAdmin.should.equal(true) + }) + }) + + context('# [修改使用者權限] for user (admin -> user)', () => { + before(() => { + // 製作假資料 + // 本 context 會用這筆資料進行測試 + this.UserMock = createModelMock( + 'User', + [{ + id: 2, + email: 'user2@example.com', + name: 'user2', + isAdmin: true // 是管理者 + }] + ) + // 將 adminController 中的 User db 取代成 User mock db + this.adminController = createControllerProxy('../controllers/admin-controller', { User: this.UserMock }) + }) + + it(' PATCH /admin/users/:id ', async () => { + // 模擬 request & response & next + const req = mockRequest({ params: { id: 2 } }) // 帶入 params.id = 2,對 PATCH /admin/users/2 發出請求 + const res = mockResponse() + const next = mockNext + + // 測試作業指定的 adminController.patchUser 函式 + await this.adminController.patchUser(req, res, next) + + // patchUser 正確執行的話,應呼叫 req.flash + // req.flash 的參數應與下列字串一致 + req.flash.calledWith('success_messages','使用者權限變更成功').should.be.true + // patchUser 執行完畢,應呼叫 res.redirect 並重新導向 /admin/users + res.redirect.calledWith('/admin/users').should.be.true + + // patchUser 執行完畢後,假資料中 id:2 使用者的應該要是 isAdmin:true + // 將假資料撈出,比對確認有成功修改到 + const user = await this.UserMock.findOne({ where: { id: 2 } }) + user.isAdmin.should.equal(false) + }) + }) + }) +}) \ No newline at end of file From 96da4bee5176c5e643633f5bc462405ff38c6660 Mon Sep 17 00:00:00 2001 From: Carrot7712 Date: Thu, 13 Jan 2022 18:01:55 +0800 Subject: [PATCH 02/61] feat:add R02.test.js --- tests/R02.test.js | 60 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 tests/R02.test.js diff --git a/tests/R02.test.js b/tests/R02.test.js new file mode 100644 index 000000000..728cce4dc --- /dev/null +++ b/tests/R02.test.js @@ -0,0 +1,60 @@ +const chai = require('chai') +const request = require('supertest') +const sinon = require('sinon') +const should = chai.should() + +const { createModelMock, createControllerProxy, mockRequest, mockResponse, mockNext } = require('../helpers/unit-test-helper'); + +describe('# R02: 餐廳資訊整理:Dashboard', function () { + context('# [Q1: Dashboard - 1 - controller / view / route]', () => { + before(async () => { + // 製作假資料 + // 本 context 會用這筆資料進行測試 + this.UserMock = createModelMock('User', [{ + id: 1, + email: 'root@example.com', + name: 'admin', + isAdmin: false, + }]) + this.RestaurantMock = createModelMock('Restaurant', [{ + id: 1, + name: '銷魂麵', + viewCounts: 3 + }]) + this.CategoryMock = createModelMock('Category', [{ + id: 1, + name: '食物' + }]) + this.CommentMock = createModelMock('Comment', [{ + id: 1, + text: "gogogo" + }]) + + // 連向模擬的 tables + this.restController = createControllerProxy('../controllers/restaurant-controller', { + User: this.UserMock, + Category: this.CategoryMock, + Restaurant: this.RestaurantMock, + Comment: this.CommentMock, + }) + }) + + it(' GET /restaurants/:id/dashboard ', async () => { + // 模擬 request & response & next + const req = mockRequest({ params: { id: 1 } }) // 帶入 params.id = 1,對 GET /restaurants/1/dashboard 發出請求 + const res = mockResponse() + const next = mockNext + + // 測試 restController.getDashBoard 函式 + await this.restController.getDashboard(req, res, next) + + // getDashBoard 正確執行的話,應呼叫 res.render + // res.render 的第 1 個參數要是 'dashboard' + // res.render 的第 2 個參數要包含 restaurant,其 name 屬性的值應是 '銷魂麵' + // res.render 的第 2 個參數要包含 restaurant,其 viewCounts 值應該是 3 + res.render.getCall(0).args[0].should.equal('dashboard') + res.render.getCall(0).args[1].restaurant.name.should.equal('銷魂麵') + res.render.getCall(0).args[1].restaurant.viewCounts.should.equal(3) + }) + }) +}) \ No newline at end of file From 37322b4d2a72d4b3c495ce00294561093b055cb7 Mon Sep 17 00:00:00 2001 From: Carrot7712 Date: Thu, 13 Jan 2022 18:03:44 +0800 Subject: [PATCH 03/61] feat:add R03.test.js --- tests/R03.test.js | 155 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 155 insertions(+) create mode 100644 tests/R03.test.js diff --git a/tests/R03.test.js b/tests/R03.test.js new file mode 100644 index 000000000..9f39816ab --- /dev/null +++ b/tests/R03.test.js @@ -0,0 +1,155 @@ +const chai = require('chai') +const sinon = require('sinon') +const should = chai.should() + +const helpers = require('../helpers/auth-helpers') +const { createModelMock, createControllerProxy, mockRequest, mockResponse, mockNext } = require('../helpers/unit-test-helper'); + +describe('# R03', () => { + describe('# R03: 建立 User Profile', function () { + context('# [瀏覽 Profile]', () => { + // 前置準備 + before(() => { + // 模擬登入驗證 + this.ensureAuthenticated = sinon + .stub(helpers, 'ensureAuthenticated') + .returns(true) + this.getUser = sinon.stub(helpers, 'getUser').returns({ id: 1 }) + // 製作假資料 + // 本 context 會用這筆資料進行測試 + this.UserMock = createModelMock('User', [{ + id: 1, + email: 'root@example.com', + name: 'admin', + isAdmin: false, + }]) + + // 修改 userController 中的資料庫連線設定,由連向真實的資料庫 -> 改為連向模擬的 User table + this.userController = createControllerProxy('../controllers/user-controller', { User: this.UserMock }) + }) + + // 開始測試 + it(' GET /users/:id ', async () => { + // 模擬 request & response & next + const req = mockRequest({ params: { id: 1 } }) // 帶入 params.id = 1,對 GET /users/1 發出請求 + const res = mockResponse() + const next = mockNext + + // 測試作業指定的 userController.getUser 函式 + await this.userController.getUser(req, res, next); + + // getUser 正確執行的話,應呼叫 res.render + // res.render 的第 1 個參數要是 'users/profile' + // res.render 的第 2 個參數要是 user,其 id 屬性的值應是 1 + res.render.getCall(0).args[0].should.equal('users/profile') + res.render.getCall(0).args[1].user.id.should.equal(1) + }) + + // 測試完畢,清除資料 + after(async () => { + // 清除模擬驗證資料 + this.ensureAuthenticated.restore() + this.getUser.restore() + }) + }) + + context('# [瀏覽編輯 Profile 頁面]', () => { + before(() => { + // 模擬登入驗證 + this.ensureAuthenticated = sinon + .stub(helpers, 'ensureAuthenticated') + .returns(true) + this.getUser = sinon.stub(helpers, 'getUser').returns({ id: 1 }) + + // 製作假資料 + // 本 context 會用這筆資料進行測試 + this.UserMock = createModelMock('User', [{ + id: 1, + email: 'root@example.com', + name: 'admin', + isAdmin: false, + }]) + + // 連向模擬的 User table + this.userController = createControllerProxy('../controllers/user-controller', { User: this.UserMock }) + }) + + it(' GET /users/:id/edit ', async () => { + // 模擬 request & response & next + const req = mockRequest({ params: { id: 1 } }) // 帶入 params.id = 1,對 GET /users/1/edit 發出請求 + const res = mockResponse() + const next = mockNext + + // 測試作業指定的 adminController.editUser 函式 + await this.userController.editUser(req, res, next) + + // editUser 執行完畢後,應呼叫 res.render + // res.render 的第 1 個參數要是 'users/edit' + // res.render 的第 2 個參數要是 user,其 name 屬性的值應是 'admin' + res.render.getCall(0).args[0].should.equal('users/edit') + res.render.getCall(0).args[1].user.name.should.equal('admin') + }) + + after(async () => { + // 清除模擬驗證資料 + this.ensureAuthenticated.restore() + this.getUser.restore() + }) + }) + + context('# [編輯 Profile]', () => { + before(async () => { + // 模擬登入驗證 + this.ensureAuthenticated = sinon + .stub(helpers, 'ensureAuthenticated') + .returns(true) + this.getUser = sinon.stub(helpers, 'getUser').returns({ id: 1 }) + // 製作假資料 + // 本 context 會用這筆資料進行測試 + this.UserMock = createModelMock( + 'User', + [{ + id: 1, + email: 'root@example.com', + name: 'admin', + isAdmin: false, + }] + ) + + // 連向模擬的 User table + this.userController = createControllerProxy('../controllers/user-controller', { User: this.UserMock }) + }) + + it(' PUT /users/:id ', async () => { + // 模擬 request & response & next + // 對 PUT /users/1 發出 request,並夾帶 body.name = amdin2, user.id = 1 + const req = mockRequest({ + user: {id: 1}, + params: { id: 1 }, + body: { name: 'admin2' }, + }) + const res = mockResponse() + const next = mockNext + + // 測試作業指定的 userController.putUser 函式 + await this.userController.putUser(req, res, next) + + // putUser 正確執行的話,應呼叫 req.flash + // req.flash 的參數應與下列字串一致 + req.flash.calledWith('success_messages','使用者資料編輯成功').should.be.true + // putUser 執行完畢,應呼叫 res.redirect 並重新導向 /users/1 + res.redirect.calledWith('/users/1').should.be.true + // putUser 執行完畢後,id:1 使用者的 name 應該已被修改 + // 將假資料撈出,比對確認有成功修改到 + const user = await this.UserMock.findOne({ where: { id: 1 } }) + user.name.should.equal('admin2') + }) + + after(async () => { + // 清除模擬驗證資料 + this.ensureAuthenticated.restore() + this.getUser.restore() + }) + }) + }) +}) \ No newline at end of file From 7ece79ef4c63264ebac70974f8c2bbce3c80e14c Mon Sep 17 00:00:00 2001 From: Amber Yan Date: Wed, 2 Mar 2022 15:23:34 +0800 Subject: [PATCH 04/61] add R01.test.js && unit-test-helper.js --- helpers/unit-test-helper.js | 143 ++++++++++++++++++++++++++ tests/R01.test.js | 195 ++++++++++++++++++++++++++++++++++++ 2 files changed, 338 insertions(+) create mode 100644 helpers/unit-test-helper.js create mode 100644 tests/R01.test.js diff --git a/helpers/unit-test-helper.js b/helpers/unit-test-helper.js new file mode 100644 index 000000000..ceed6030a --- /dev/null +++ b/helpers/unit-test-helper.js @@ -0,0 +1,143 @@ +const SequelizeMock = require('sequelize-mock') +const proxyquire = require('proxyquire') +const sinon = require('sinon') + +const dbMock = new SequelizeMock() + +const createModelMock = (name, data, joinedTableName, sourceData) => { + const defaultValue = data[0] + const mockModel = dbMock.define(name, defaultValue, { + instanceMethods: { + update: function (changes) { + const objIndex = data.findIndex(d => d.id === this.get('id')) + data[objIndex] = { + ...data[objIndex], + ...changes + } + return Promise.resolve() + }, + destroy: function () { + if (joinedTableName) { + const userId = this.get('userId') + const restaurantId = this.get('restaurantId') + const restaurant = sourceData.find(d => d.id === restaurantId) + restaurant[joinedTableName] = restaurant[joinedTableName].filter(d => !(d.userId === userId)) + } else { + data = data && data.filter(d => d.userId !== this.get('userId') && d.restaurantId !== this.get('restaurantId')) // remove + } + } + } + }) + + // 模擬 Sequelize 行為 + // 將 mock user db 中的 findByPK 用 findOne 取代 (sequelize mock not support findByPK) + mockModel.findByPk = id => mockModel.findOne({ where: { id: id } }) + // 將 count 的 function 預設回傳假資料數目 1 + mockModel.count = () => 1 + // 因為 mock 中的 create 有問題,因此指向 upsert function, 這樣可以在 useHandler 中取得 create 呼叫 + mockModel.create = mockModel.upsert + + // modify middleware + if (joinedTableName) { + mockModel.$queryInterface.$useHandler((query, queryOptions) => { + if (query === 'upsert') { + // 新增 joinTable 資料到模擬資料 + const { userId, restaurantId } = queryOptions[0] + const restaurant = sourceData.find(d => d.id === restaurantId) + restaurant && restaurant[joinedTableName].push({ userId: userId }) + data.push(queryOptions[0]) + return Promise.resolve(data && data.map(d => mockModel.build(d))) + } else if (query === 'findAll') { + // 回傳模擬資料 + if (!data) { + return mockModel.build([defaultValue]) + } + return Promise.resolve(data ? data.map(d => mockModel.build(d)) : []) + } else if (query === 'findOne') { + const item = data.find(d => d.userId === queryOptions[0].where.userId && d.restaurantId === queryOptions[0].where.restaurantId) + if (!item) return null + + const result = mockModel.build(item) + return Promise.resolve(result) + } else if (query === 'destroy') { + // destroy 可以從 where 取得要刪除的資料 + // 因此就可以模擬將模擬資料中的資料刪除 + // 刪除模擬資料中的某一筆 joinTable 資料 + const { userId, restaurantId } = queryOptions[0].where + const restaurant = data.find(d => d.id === restaurantId) + restaurant[joinedTableName] = restaurant[joinedTableName].filter(d => !(d.userId === userId)) + return Promise.resolve(data.map(d => mockModel.build(d))) + } + }) + } else { + mockModel.$queryInterface.$useHandler((query, queryOptions, done) => { + if (query === 'upsert') { + // create 時會帶 userId 跟 restaurantId (ex: Like.create({ userId: 1, restaurantId: 2})) + const { userId, restaurantId } = queryOptions[0] + + // 新增這個 Like 的資訊到模擬資料裡 + data.push({ userId, restaurantId }) + + // 回傳模擬資料 + return Promise.resolve(mockModel.build(data)) + } else if (query === 'findAll') { + // 回傳模擬資料 + if (!data) { + return mockModel.build([defaultValue]) + } + return Promise.resolve(data ? data.map(d => mockModel.build(d)) : []) + } else if (query === 'findOne') { + let item + if (queryOptions[0].id) { + item = data.find(d => d.id === queryOptions[0].id) + } else { + item = data.find(d => d.userId === queryOptions[0].where.userId && d.restaurantId === queryOptions[0].where.restaurantId) + } + if (!item) return null + + const result = mockModel.build(item) + return Promise.resolve(result) + } else if (query === 'destroy') { + // destroy 可以從 where 取得要刪除的資料 + // 因此就可以模擬將模擬資料中的資料刪除 + const { userId, restaurantId } = queryOptions[0].where + data = data.filter(d => !(d.userId === userId && d.restaurantId === restaurantId)) + + return Promise.resolve(mockModel.build(data)) + } + }) + } + + return mockModel +} + +const createControllerProxy = (path, model) => { + const controller = proxyquire(path, { + '../models': model + }) + + return controller +} + +const mockRequest = query => { + return { + ...query, + flash: sinon.spy() + } +} +const mockResponse = () => { + return { + redirect: sinon.spy(), + render: sinon.spy() + } +} + +const mockNext = err => console.log('[ERROR]:', err) + +module.exports = { + createModelMock, + createControllerProxy, + mockRequest, + mockResponse, + mockNext +} diff --git a/tests/R01.test.js b/tests/R01.test.js new file mode 100644 index 000000000..574086d64 --- /dev/null +++ b/tests/R01.test.js @@ -0,0 +1,195 @@ +const request = require('supertest') +const chai = require('chai') +const sinon = require('sinon') +const should = chai.should() + +const app = require('../app') +const { createModelMock, createControllerProxy, mockRequest, mockResponse, mockNext } = require('../helpers/unit-test-helper'); + +describe('# R01', () => { + describe('登入測試: POST /signin', function(){ + // 以下測試會發出請求,測試資料庫內是否有作業指定的使用者資料 + // 測試資料的來源是真實的資料庫 + it('#1 密碼錯誤', function(done){ + request(app) + // 對 POST /signin 發出請求,參數是錯誤的密碼 + .post('/signin') + .type('urlencoded') + .send('email=root@example.com&password=123') + // 期待登入驗證回應失敗,重新導向 /signin + .expect('Location', '/signin') + .expect(302, done) + }) + + it('#2 帳號錯誤', function(done){ + request(app) + // 對 POST /signin 發出請求,參數是錯誤的帳號 + .post('/signin') + .type('urlencoded') + .send('email=tu&password=12345678') + // 期待登入驗證回應失敗,重新導向 /signin + .expect('Location', '/signin') + .expect(302, done) + }) + + it('#3 成功登入', function(done){ + request(app) + // 對 POST /signin 發出請求,參數是作業指定的使用者帳號密碼 + .post('/signin') + .type('urlencoded') + .send('email=root@example.com&password=12345678') + // 期待登入驗證成功,重新導向 /restaurants + .expect('Location', '/restaurants') + .expect(302, done) + }) + }); + + describe('# 使用者權限管理', function () { + // 前置準備 + before(() => { + // 製作假資料 + // 本 context 會用這筆資料進行測試 + this.UserMock = createModelMock('User', [{ + id: 1, + email: 'root@example.com', + name: 'admin', + isAdmin: false, + }]) + + // 修改 adminController 中的資料庫連線設定,由連向真實的資料庫 -> 改為連向模擬的 User table + this.adminController = createControllerProxy('../controllers/admin-controller', { User: this.UserMock }) + }) + + // 開始測試 + context('# [顯示使用者清單]', () => { + it(' GET /admin/users ', async () => { + // 模擬 request & response & next + const req = mockRequest() // 對 GET /admin/users 發出請求 + const res = mockResponse() + const next = mockNext + + // 測試作業指定的 adminController.getUsers 函式 + await this.adminController.getUsers(req, res, next) + + // getUser 正確執行的話,應呼叫 res.render + // res.render 的第 2 個參數應是 users + // 根據測試資料,users 中的第 1 筆資料,name 屬性值應該要是 'admin' + res.render.getCall(0).args[1].users[0].name.should.equal('admin') + }) + }) + + context('# [修改使用者權限] for root', () => { + before(() => { + // 製作假資料 + // 本 context 會用這筆資料進行測試 + this.UserMock = createModelMock( + 'User', + [{ + id: 1, + email: 'root@example.com', + name: 'admin', + isAdmin: true, // 是管理者 + }] + ) + + // 將 adminController 中的 User db 取代成 User mock db + this.adminController = createControllerProxy('../controllers/admin-controller', { User: this.UserMock }) + }) + + it(' PATCH /admin/users/:id ', async () => { + // 模擬 request & response & next + const req = mockRequest({ params: { id: 1 } }) // 帶入 params.id = 1,對 PATCH /admin/users/1 發出請求 + const res = mockResponse() + const next = mockNext + + // 測試作業指定的 adminController.patchUser 函式 + await this.adminController.patchUser(req, res, next) + + // patchUser 正確執行的話,應呼叫 req.flash + // req.flash 的參數應該要與下列字串一致 + req.flash.calledWith('error_messages','禁止變更 root 權限').should.be.true + + // patchUser 執行完畢,應呼叫 res.redirect 並重新導向上一頁 + res.redirect.calledWith('back').should.be.true + }) + }) + + context('# [修改使用者權限] for user (user -> admin)', () => { + before(() => { + // 製作假資料 + // 本 context 會用這筆資料進行測試 + this.UserMock = createModelMock( + 'User', + [{ + id: 1, + email: 'user@example.com', + name: 'user', + isAdmin: false, // 非管理者 + }] + ) + // 將 adminController 中的 User db 取代成 User mock db + this.adminController = createControllerProxy('../controllers/admin-controller', { User: this.UserMock }) + }) + + it(' PATCH /admin/users/:id ', async () => { + // 模擬 request & response & next + const req = mockRequest({ params: { id: 1 } }) // 帶入 params.id = 1,對 PATCH /admin/users/1 發出請求 + const res = mockResponse() + const next = mockNext + + // 測試作業指定的 adminController.patchUser 函式 + await this.adminController.patchUser(req, res, next) + + // patchUser 正確執行的話,應呼叫 req.flash + // req.flash 的參數應與下列字串一致 + req.flash.calledWith('success_messages','使用者權限變更成功').should.be.true + // patchUser 執行完畢,應呼叫 res.redirect 並重新導向 /admin/users + res.redirect.calledWith('/admin/users').should.be.true + + // patchUser 執行完畢後,假資料中 id:1 使用者的應該要是 isAdmin:true + // 將假資料撈出,比對確認有成功修改到 + const user = await this.UserMock.findOne({ where: { id: 1 } }) + user.isAdmin.should.equal(true) + }) + }) + + context('# [修改使用者權限] for user (admin -> user)', () => { + before(() => { + // 製作假資料 + // 本 context 會用這筆資料進行測試 + this.UserMock = createModelMock( + 'User', + [{ + id: 2, + email: 'user2@example.com', + name: 'user2', + isAdmin: true // 是管理者 + }] + ) + // 將 adminController 中的 User db 取代成 User mock db + this.adminController = createControllerProxy('../controllers/admin-controller', { User: this.UserMock }) + }) + + it(' PATCH /admin/users/:id ', async () => { + // 模擬 request & response & next + const req = mockRequest({ params: { id: 2 } }) // 帶入 params.id = 2,對 PATCH /admin/users/2 發出請求 + const res = mockResponse() + const next = mockNext + + // 測試作業指定的 adminController.patchUser 函式 + await this.adminController.patchUser(req, res, next) + + // patchUser 正確執行的話,應呼叫 req.flash + // req.flash 的參數應與下列字串一致 + req.flash.calledWith('success_messages','使用者權限變更成功').should.be.true + // patchUser 執行完畢,應呼叫 res.redirect 並重新導向 /admin/users + res.redirect.calledWith('/admin/users').should.be.true + + // patchUser 執行完畢後,假資料中 id:2 使用者的應該要是 isAdmin:true + // 將假資料撈出,比對確認有成功修改到 + const user = await this.UserMock.findOne({ where: { id: 2 } }) + user.isAdmin.should.equal(false) + }) + }) + }) +}) \ No newline at end of file From b54a2e99ab2d9f4d1a6d4fe0393213518626296c Mon Sep 17 00:00:00 2001 From: Amber Yan Date: Wed, 2 Mar 2022 15:27:03 +0800 Subject: [PATCH 05/61] add R02.test.js file --- tests/R02.test.js | 60 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 tests/R02.test.js diff --git a/tests/R02.test.js b/tests/R02.test.js new file mode 100644 index 000000000..728cce4dc --- /dev/null +++ b/tests/R02.test.js @@ -0,0 +1,60 @@ +const chai = require('chai') +const request = require('supertest') +const sinon = require('sinon') +const should = chai.should() + +const { createModelMock, createControllerProxy, mockRequest, mockResponse, mockNext } = require('../helpers/unit-test-helper'); + +describe('# R02: 餐廳資訊整理:Dashboard', function () { + context('# [Q1: Dashboard - 1 - controller / view / route]', () => { + before(async () => { + // 製作假資料 + // 本 context 會用這筆資料進行測試 + this.UserMock = createModelMock('User', [{ + id: 1, + email: 'root@example.com', + name: 'admin', + isAdmin: false, + }]) + this.RestaurantMock = createModelMock('Restaurant', [{ + id: 1, + name: '銷魂麵', + viewCounts: 3 + }]) + this.CategoryMock = createModelMock('Category', [{ + id: 1, + name: '食物' + }]) + this.CommentMock = createModelMock('Comment', [{ + id: 1, + text: "gogogo" + }]) + + // 連向模擬的 tables + this.restController = createControllerProxy('../controllers/restaurant-controller', { + User: this.UserMock, + Category: this.CategoryMock, + Restaurant: this.RestaurantMock, + Comment: this.CommentMock, + }) + }) + + it(' GET /restaurants/:id/dashboard ', async () => { + // 模擬 request & response & next + const req = mockRequest({ params: { id: 1 } }) // 帶入 params.id = 1,對 GET /restaurants/1/dashboard 發出請求 + const res = mockResponse() + const next = mockNext + + // 測試 restController.getDashBoard 函式 + await this.restController.getDashboard(req, res, next) + + // getDashBoard 正確執行的話,應呼叫 res.render + // res.render 的第 1 個參數要是 'dashboard' + // res.render 的第 2 個參數要包含 restaurant,其 name 屬性的值應是 '銷魂麵' + // res.render 的第 2 個參數要包含 restaurant,其 viewCounts 值應該是 3 + res.render.getCall(0).args[0].should.equal('dashboard') + res.render.getCall(0).args[1].restaurant.name.should.equal('銷魂麵') + res.render.getCall(0).args[1].restaurant.viewCounts.should.equal(3) + }) + }) +}) \ No newline at end of file From ef44c3efaf42025c138378e9f42035f25c330526 Mon Sep 17 00:00:00 2001 From: Amber Yan Date: Wed, 2 Mar 2022 15:27:56 +0800 Subject: [PATCH 06/61] add R03.test.js file --- tests/R03.test.js | 155 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 155 insertions(+) create mode 100644 tests/R03.test.js diff --git a/tests/R03.test.js b/tests/R03.test.js new file mode 100644 index 000000000..9f39816ab --- /dev/null +++ b/tests/R03.test.js @@ -0,0 +1,155 @@ +const chai = require('chai') +const sinon = require('sinon') +const should = chai.should() + +const helpers = require('../helpers/auth-helpers') +const { createModelMock, createControllerProxy, mockRequest, mockResponse, mockNext } = require('../helpers/unit-test-helper'); + +describe('# R03', () => { + describe('# R03: 建立 User Profile', function () { + context('# [瀏覽 Profile]', () => { + // 前置準備 + before(() => { + // 模擬登入驗證 + this.ensureAuthenticated = sinon + .stub(helpers, 'ensureAuthenticated') + .returns(true) + this.getUser = sinon.stub(helpers, 'getUser').returns({ id: 1 }) + // 製作假資料 + // 本 context 會用這筆資料進行測試 + this.UserMock = createModelMock('User', [{ + id: 1, + email: 'root@example.com', + name: 'admin', + isAdmin: false, + }]) + + // 修改 userController 中的資料庫連線設定,由連向真實的資料庫 -> 改為連向模擬的 User table + this.userController = createControllerProxy('../controllers/user-controller', { User: this.UserMock }) + }) + + // 開始測試 + it(' GET /users/:id ', async () => { + // 模擬 request & response & next + const req = mockRequest({ params: { id: 1 } }) // 帶入 params.id = 1,對 GET /users/1 發出請求 + const res = mockResponse() + const next = mockNext + + // 測試作業指定的 userController.getUser 函式 + await this.userController.getUser(req, res, next); + + // getUser 正確執行的話,應呼叫 res.render + // res.render 的第 1 個參數要是 'users/profile' + // res.render 的第 2 個參數要是 user,其 id 屬性的值應是 1 + res.render.getCall(0).args[0].should.equal('users/profile') + res.render.getCall(0).args[1].user.id.should.equal(1) + }) + + // 測試完畢,清除資料 + after(async () => { + // 清除模擬驗證資料 + this.ensureAuthenticated.restore() + this.getUser.restore() + }) + }) + + context('# [瀏覽編輯 Profile 頁面]', () => { + before(() => { + // 模擬登入驗證 + this.ensureAuthenticated = sinon + .stub(helpers, 'ensureAuthenticated') + .returns(true) + this.getUser = sinon.stub(helpers, 'getUser').returns({ id: 1 }) + + // 製作假資料 + // 本 context 會用這筆資料進行測試 + this.UserMock = createModelMock('User', [{ + id: 1, + email: 'root@example.com', + name: 'admin', + isAdmin: false, + }]) + + // 連向模擬的 User table + this.userController = createControllerProxy('../controllers/user-controller', { User: this.UserMock }) + }) + + it(' GET /users/:id/edit ', async () => { + // 模擬 request & response & next + const req = mockRequest({ params: { id: 1 } }) // 帶入 params.id = 1,對 GET /users/1/edit 發出請求 + const res = mockResponse() + const next = mockNext + + // 測試作業指定的 adminController.editUser 函式 + await this.userController.editUser(req, res, next) + + // editUser 執行完畢後,應呼叫 res.render + // res.render 的第 1 個參數要是 'users/edit' + // res.render 的第 2 個參數要是 user,其 name 屬性的值應是 'admin' + res.render.getCall(0).args[0].should.equal('users/edit') + res.render.getCall(0).args[1].user.name.should.equal('admin') + }) + + after(async () => { + // 清除模擬驗證資料 + this.ensureAuthenticated.restore() + this.getUser.restore() + }) + }) + + context('# [編輯 Profile]', () => { + before(async () => { + // 模擬登入驗證 + this.ensureAuthenticated = sinon + .stub(helpers, 'ensureAuthenticated') + .returns(true) + this.getUser = sinon.stub(helpers, 'getUser').returns({ id: 1 }) + // 製作假資料 + // 本 context 會用這筆資料進行測試 + this.UserMock = createModelMock( + 'User', + [{ + id: 1, + email: 'root@example.com', + name: 'admin', + isAdmin: false, + }] + ) + + // 連向模擬的 User table + this.userController = createControllerProxy('../controllers/user-controller', { User: this.UserMock }) + }) + + it(' PUT /users/:id ', async () => { + // 模擬 request & response & next + // 對 PUT /users/1 發出 request,並夾帶 body.name = amdin2, user.id = 1 + const req = mockRequest({ + user: {id: 1}, + params: { id: 1 }, + body: { name: 'admin2' }, + }) + const res = mockResponse() + const next = mockNext + + // 測試作業指定的 userController.putUser 函式 + await this.userController.putUser(req, res, next) + + // putUser 正確執行的話,應呼叫 req.flash + // req.flash 的參數應與下列字串一致 + req.flash.calledWith('success_messages','使用者資料編輯成功').should.be.true + // putUser 執行完畢,應呼叫 res.redirect 並重新導向 /users/1 + res.redirect.calledWith('/users/1').should.be.true + // putUser 執行完畢後,id:1 使用者的 name 應該已被修改 + // 將假資料撈出,比對確認有成功修改到 + const user = await this.UserMock.findOne({ where: { id: 1 } }) + user.name.should.equal('admin2') + }) + + after(async () => { + // 清除模擬驗證資料 + this.ensureAuthenticated.restore() + this.getUser.restore() + }) + }) + }) +}) \ No newline at end of file From 1ead393948309b86423652cac6712a1e19485384 Mon Sep 17 00:00:00 2001 From: Amber Yan Date: Wed, 2 Mar 2022 15:28:28 +0800 Subject: [PATCH 07/61] add R04.test.js --- tests/R04.test.js | 98 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 tests/R04.test.js diff --git a/tests/R04.test.js b/tests/R04.test.js new file mode 100644 index 000000000..f06ddab32 --- /dev/null +++ b/tests/R04.test.js @@ -0,0 +1,98 @@ +const chai = require('chai') +const sinon = require('sinon') +const should = chai.should() + +const helpers = require('../helpers/auth-helpers') +const { createModelMock, createControllerProxy, mockRequest, mockResponse, mockNext } = require('../helpers/unit-test-helper') + +// 模擬 2 間餐廳資料 +let mockRestaurantData = [ + { + id: 1, + name: 'Restaurant1', + tel: 'tel', + address: 'address', + opening_hours: 'opening_hours', + description: 'test description', + FavoritedUsers: [ + { + userId: 1, + }, + ], + }, + { + id: 2, + name: 'Restaurant2', + tel: 'tel', + address: 'address', + opening_hours: 'opening_hours', + description: 'description', + categoryId: 1, + FavoritedUsers: [], + }, +] + +describe('# R04: Like / Unlike', function () { + context('# Q1: 使用者可以 Like 餐廳', () => { + before(() => { + // 模擬登入驗證 + this.ensureAuthenticated = sinon + .stub(helpers, 'ensureAuthenticated') + .returns(true) + this.getUser = sinon.stub(helpers, 'getUser').returns({ id: 1 }) + + // 建立了一個模擬的 Restaurant table,裡面放入 2 間餐廳資料 + this.restaurantMock = createModelMock('Restaurant', mockRestaurantData) + + // 建立了一個模擬的 Like table,裡面目前是空的 + this.mockLikeData = [] + this.likeMock = createModelMock('Like', this.mockLikeData) + + // 連向模擬的 Like table + this.userController = createControllerProxy('../controllers/user-controller', { + Like: this.likeMock, + Restaurant: this.restaurantMock + }) + }) + + it(' POST /like/:restaurantId ', async () => { + // 模擬 request & response & next + const req = mockRequest({ params: { restaurantId: 2 }, user: {id: 1} }) // 帶入 params.restaurantId = 2, user.id = 1,對 POST /like/2 發出請求 + const res = mockResponse() + const next = mockNext + + // 測試 userController.addLike 函式 + await this.userController.addLike(req, res, next) + // 將模擬的 Like table 內的資料全數撈出 + const likes = await this.likeMock.findAll() + // addLike 執行完畢後,Like table 應會從空的 -> 變成有 1 筆資料 + likes.should.have.lengthOf(1) + // 資料裡的 userId 應該會跟我們傳入的 user id 一樣 + likes[0].userId.should.equal(1) + // 資料裡的 RestaurantId 會跟我們傳入的 params.restaurantId 一樣 + likes[0].restaurantId.should.equal(2) + }) + + it(' DELETE /like/:restaurantId ', async () => { + // 模擬 request & response & next + // 模擬發出 request, 帶入 params.id = 1, params.restaurantId = 2, user.id = 1 + const req = mockRequest({ params: { id: 1, restaurantId: 2 }, user: {id: 1} }) // 對 DELETE /like/2 發出請求 + const res = mockResponse() + const next = mockNext + + // 測試作業指定的 userController.removeLike 函式 + await this.userController.removeLike(req, res, next) + + // 將模擬的 Like table 內的資料全數撈出 + const likes = await this.likeMock.findAll() + // addLike 執行完畢後,Like table 應會從有 1 筆資料 -> 變成空的 + likes.should.have.lengthOf(0) + }) + + after(async () => { + // 清除驗證資料 + this.ensureAuthenticated.restore() + this.getUser.restore() + }) + }) +}) \ No newline at end of file From 79ff15885ef6a386efcfd22160781a580049ada1 Mon Sep 17 00:00:00 2001 From: zjzheng17 <90173539+zjzheng17@users.noreply.github.com> Date: Mon, 18 Jul 2022 12:36:56 +0800 Subject: [PATCH 08/61] =?UTF-8?q?=E8=A8=BB=E8=A7=A3=20typo=20=E4=BF=AE?= =?UTF-8?q?=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/R01.test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/R01.test.js b/tests/R01.test.js index 574086d64..92a989e23 100644 --- a/tests/R01.test.js +++ b/tests/R01.test.js @@ -185,11 +185,11 @@ describe('# R01', () => { // patchUser 執行完畢,應呼叫 res.redirect 並重新導向 /admin/users res.redirect.calledWith('/admin/users').should.be.true - // patchUser 執行完畢後,假資料中 id:2 使用者的應該要是 isAdmin:true + // patchUser 執行完畢後,假資料中 id:2 使用者的應該要是 isAdmin: // 將假資料撈出,比對確認有成功修改到 const user = await this.UserMock.findOne({ where: { id: 2 } }) user.isAdmin.should.equal(false) }) }) }) -}) \ No newline at end of file +}) From 9d566b6e0de4de3ba4dc33137230dc8cce77a37b Mon Sep 17 00:00:00 2001 From: zjzheng17 <90173539+zjzheng17@users.noreply.github.com> Date: Mon, 18 Jul 2022 12:43:45 +0800 Subject: [PATCH 09/61] correcting typo in comment --- tests/R01.test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/R01.test.js b/tests/R01.test.js index 574086d64..e635e363b 100644 --- a/tests/R01.test.js +++ b/tests/R01.test.js @@ -185,11 +185,11 @@ describe('# R01', () => { // patchUser 執行完畢,應呼叫 res.redirect 並重新導向 /admin/users res.redirect.calledWith('/admin/users').should.be.true - // patchUser 執行完畢後,假資料中 id:2 使用者的應該要是 isAdmin:true + // patchUser 執行完畢後,假資料中 id:2 使用者的應該要是 isAdmin:false // 將假資料撈出,比對確認有成功修改到 const user = await this.UserMock.findOne({ where: { id: 2 } }) user.isAdmin.should.equal(false) }) }) }) -}) \ No newline at end of file +}) From 4fe60b0a0771173e7a6d4a57404c645237bb7d7e Mon Sep 17 00:00:00 2001 From: zjzheng17 <90173539+zjzheng17@users.noreply.github.com> Date: Mon, 18 Jul 2022 12:48:56 +0800 Subject: [PATCH 10/61] correcting typo in comment --- tests/R01.test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/R01.test.js b/tests/R01.test.js index 574086d64..e635e363b 100644 --- a/tests/R01.test.js +++ b/tests/R01.test.js @@ -185,11 +185,11 @@ describe('# R01', () => { // patchUser 執行完畢,應呼叫 res.redirect 並重新導向 /admin/users res.redirect.calledWith('/admin/users').should.be.true - // patchUser 執行完畢後,假資料中 id:2 使用者的應該要是 isAdmin:true + // patchUser 執行完畢後,假資料中 id:2 使用者的應該要是 isAdmin:false // 將假資料撈出,比對確認有成功修改到 const user = await this.UserMock.findOne({ where: { id: 2 } }) user.isAdmin.should.equal(false) }) }) }) -}) \ No newline at end of file +}) From 03c14c01ab2502f04a830604c0e49974d3e3f654 Mon Sep 17 00:00:00 2001 From: zjzheng17 <90173539+zjzheng17@users.noreply.github.com> Date: Mon, 18 Jul 2022 12:56:31 +0800 Subject: [PATCH 11/61] correcting typo in comment --- tests/R01.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/R01.test.js b/tests/R01.test.js index 92a989e23..e635e363b 100644 --- a/tests/R01.test.js +++ b/tests/R01.test.js @@ -185,7 +185,7 @@ describe('# R01', () => { // patchUser 執行完畢,應呼叫 res.redirect 並重新導向 /admin/users res.redirect.calledWith('/admin/users').should.be.true - // patchUser 執行完畢後,假資料中 id:2 使用者的應該要是 isAdmin: + // patchUser 執行完畢後,假資料中 id:2 使用者的應該要是 isAdmin:false // 將假資料撈出,比對確認有成功修改到 const user = await this.UserMock.findOne({ where: { id: 2 } }) user.isAdmin.should.equal(false) From 326bed88961214ac4b79fdc5be6a6b8da642301f Mon Sep 17 00:00:00 2001 From: tuterwell <46310069+tuterwell@users.noreply.github.com> Date: Thu, 10 Aug 2023 16:51:13 +0800 Subject: [PATCH 12/61] fix R01.test.js comment typo --- tests/R01.test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/R01.test.js b/tests/R01.test.js index 574086d64..e635e363b 100644 --- a/tests/R01.test.js +++ b/tests/R01.test.js @@ -185,11 +185,11 @@ describe('# R01', () => { // patchUser 執行完畢,應呼叫 res.redirect 並重新導向 /admin/users res.redirect.calledWith('/admin/users').should.be.true - // patchUser 執行完畢後,假資料中 id:2 使用者的應該要是 isAdmin:true + // patchUser 執行完畢後,假資料中 id:2 使用者的應該要是 isAdmin:false // 將假資料撈出,比對確認有成功修改到 const user = await this.UserMock.findOne({ where: { id: 2 } }) user.isAdmin.should.equal(false) }) }) }) -}) \ No newline at end of file +}) From d5b1553f1b1972bcc403f3153e227721c2d29bb2 Mon Sep 17 00:00:00 2001 From: EugeneChen Date: Sat, 9 Sep 2023 16:59:04 +0800 Subject: [PATCH 13/61] chore: init github workflow --- .github/workflows/ci.yml | 47 ++++++++++++++++++++++++++++++++++++++++ config/config.json | 8 +++++++ 2 files changed, 55 insertions(+) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 000000000..fce6d4d04 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,47 @@ +name: Node.js test + +on: + push: + branches: +# - main + - '*-test' + pull_request: + branches: +# - main + +env: + NODE_ENV: github + +jobs: + test: + + runs-on: ubuntu-latest + + strategy: + matrix: + node-version: [18.x] # ex: [16.x, 18.x], See supported Node.js release schedule at https://nodejs.org/en/about/releases/ + + steps: + - uses: mirromutth/mysql-action@v1.1 + with: + host port: 3306 # Optional, default value is 3306. The port of host + container port: 3307 # Optional, default value is 3306. The port of container + character set server: 'utf8mb4' # Optional, default value is 'utf8mb4'. The '--character-set-server' option for mysqld + collation server: 'utf8mb4_unicode_ci' # Optional, default value is 'utf8mb4_general_ci'. The '--collation-server' option for mysqld + mysql version: '8.0' # Optional, default value is "latest". The version of the MySQL + mysql database: 'forum' # Optional, default value is "test". The specified database which will be create + mysql root password: 'password' # Required if "mysql user" is empty, default is empty. The root superuser password + # mysql user: 'github' # Required if "mysql root password" is empty, default is empty. The superuser for the specified database. Can use secrets, too + # mysql password: 'password' # Required if "mysql user" exists. The password for the "mysql user" + - run: mysql --version + - uses: actions/checkout@v3 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v3 + with: + node-version: ${{ matrix.node-version }} + cache: 'npm' + - run: npm install + - run: npm install sequelize-cli -g + - run: sequelize db:migrate + - run: sequelize db:seed:all + - run: npm test diff --git a/config/config.json b/config/config.json index 93cd6cd89..9507314d4 100644 --- a/config/config.json +++ b/config/config.json @@ -19,5 +19,13 @@ "host": "127.0.0.1", "dialect": "mysql", "logging": false + }, + "github": { + "username": "root", + "password": "password", + "database": "forum", + "host": "127.0.0.1", + "dialect": "mysql", + "logging": false } } From 6f29ed35cff47135b1d0ba27d4d5e8df129d1faa Mon Sep 17 00:00:00 2001 From: EugeneChen Date: Sat, 9 Sep 2023 17:02:14 +0800 Subject: [PATCH 14/61] chore: init github workflow --- .github/workflows/ci.yml | 47 ++++++++++++++++++++++++++++++++++++++++ config/config.json | 8 +++++++ 2 files changed, 55 insertions(+) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 000000000..fce6d4d04 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,47 @@ +name: Node.js test + +on: + push: + branches: +# - main + - '*-test' + pull_request: + branches: +# - main + +env: + NODE_ENV: github + +jobs: + test: + + runs-on: ubuntu-latest + + strategy: + matrix: + node-version: [18.x] # ex: [16.x, 18.x], See supported Node.js release schedule at https://nodejs.org/en/about/releases/ + + steps: + - uses: mirromutth/mysql-action@v1.1 + with: + host port: 3306 # Optional, default value is 3306. The port of host + container port: 3307 # Optional, default value is 3306. The port of container + character set server: 'utf8mb4' # Optional, default value is 'utf8mb4'. The '--character-set-server' option for mysqld + collation server: 'utf8mb4_unicode_ci' # Optional, default value is 'utf8mb4_general_ci'. The '--collation-server' option for mysqld + mysql version: '8.0' # Optional, default value is "latest". The version of the MySQL + mysql database: 'forum' # Optional, default value is "test". The specified database which will be create + mysql root password: 'password' # Required if "mysql user" is empty, default is empty. The root superuser password + # mysql user: 'github' # Required if "mysql root password" is empty, default is empty. The superuser for the specified database. Can use secrets, too + # mysql password: 'password' # Required if "mysql user" exists. The password for the "mysql user" + - run: mysql --version + - uses: actions/checkout@v3 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v3 + with: + node-version: ${{ matrix.node-version }} + cache: 'npm' + - run: npm install + - run: npm install sequelize-cli -g + - run: sequelize db:migrate + - run: sequelize db:seed:all + - run: npm test diff --git a/config/config.json b/config/config.json index 93cd6cd89..9507314d4 100644 --- a/config/config.json +++ b/config/config.json @@ -19,5 +19,13 @@ "host": "127.0.0.1", "dialect": "mysql", "logging": false + }, + "github": { + "username": "root", + "password": "password", + "database": "forum", + "host": "127.0.0.1", + "dialect": "mysql", + "logging": false } } From 02a746f22d082b47bbf804ceb8851e6b15577ea3 Mon Sep 17 00:00:00 2001 From: EugeneChen Date: Sat, 9 Sep 2023 17:02:50 +0800 Subject: [PATCH 15/61] chore: init github workflow --- .github/workflows/ci.yml | 47 ++++++++++++++++++++++++++++++++++++++++ config/config.json | 8 +++++++ 2 files changed, 55 insertions(+) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 000000000..fce6d4d04 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,47 @@ +name: Node.js test + +on: + push: + branches: +# - main + - '*-test' + pull_request: + branches: +# - main + +env: + NODE_ENV: github + +jobs: + test: + + runs-on: ubuntu-latest + + strategy: + matrix: + node-version: [18.x] # ex: [16.x, 18.x], See supported Node.js release schedule at https://nodejs.org/en/about/releases/ + + steps: + - uses: mirromutth/mysql-action@v1.1 + with: + host port: 3306 # Optional, default value is 3306. The port of host + container port: 3307 # Optional, default value is 3306. The port of container + character set server: 'utf8mb4' # Optional, default value is 'utf8mb4'. The '--character-set-server' option for mysqld + collation server: 'utf8mb4_unicode_ci' # Optional, default value is 'utf8mb4_general_ci'. The '--collation-server' option for mysqld + mysql version: '8.0' # Optional, default value is "latest". The version of the MySQL + mysql database: 'forum' # Optional, default value is "test". The specified database which will be create + mysql root password: 'password' # Required if "mysql user" is empty, default is empty. The root superuser password + # mysql user: 'github' # Required if "mysql root password" is empty, default is empty. The superuser for the specified database. Can use secrets, too + # mysql password: 'password' # Required if "mysql user" exists. The password for the "mysql user" + - run: mysql --version + - uses: actions/checkout@v3 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v3 + with: + node-version: ${{ matrix.node-version }} + cache: 'npm' + - run: npm install + - run: npm install sequelize-cli -g + - run: sequelize db:migrate + - run: sequelize db:seed:all + - run: npm test diff --git a/config/config.json b/config/config.json index 93cd6cd89..9507314d4 100644 --- a/config/config.json +++ b/config/config.json @@ -19,5 +19,13 @@ "host": "127.0.0.1", "dialect": "mysql", "logging": false + }, + "github": { + "username": "root", + "password": "password", + "database": "forum", + "host": "127.0.0.1", + "dialect": "mysql", + "logging": false } } From 16bd3687c4212fa8a86642ac01c80ffda083bad4 Mon Sep 17 00:00:00 2001 From: EugeneChen Date: Sat, 9 Sep 2023 17:04:13 +0800 Subject: [PATCH 16/61] chore: init github workflow --- .github/workflows/ci.yml | 47 ++++++++++++++++++++++++++++++++++++++++ config/config.json | 8 +++++++ 2 files changed, 55 insertions(+) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 000000000..fce6d4d04 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,47 @@ +name: Node.js test + +on: + push: + branches: +# - main + - '*-test' + pull_request: + branches: +# - main + +env: + NODE_ENV: github + +jobs: + test: + + runs-on: ubuntu-latest + + strategy: + matrix: + node-version: [18.x] # ex: [16.x, 18.x], See supported Node.js release schedule at https://nodejs.org/en/about/releases/ + + steps: + - uses: mirromutth/mysql-action@v1.1 + with: + host port: 3306 # Optional, default value is 3306. The port of host + container port: 3307 # Optional, default value is 3306. The port of container + character set server: 'utf8mb4' # Optional, default value is 'utf8mb4'. The '--character-set-server' option for mysqld + collation server: 'utf8mb4_unicode_ci' # Optional, default value is 'utf8mb4_general_ci'. The '--collation-server' option for mysqld + mysql version: '8.0' # Optional, default value is "latest". The version of the MySQL + mysql database: 'forum' # Optional, default value is "test". The specified database which will be create + mysql root password: 'password' # Required if "mysql user" is empty, default is empty. The root superuser password + # mysql user: 'github' # Required if "mysql root password" is empty, default is empty. The superuser for the specified database. Can use secrets, too + # mysql password: 'password' # Required if "mysql user" exists. The password for the "mysql user" + - run: mysql --version + - uses: actions/checkout@v3 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v3 + with: + node-version: ${{ matrix.node-version }} + cache: 'npm' + - run: npm install + - run: npm install sequelize-cli -g + - run: sequelize db:migrate + - run: sequelize db:seed:all + - run: npm test diff --git a/config/config.json b/config/config.json index 93cd6cd89..9507314d4 100644 --- a/config/config.json +++ b/config/config.json @@ -19,5 +19,13 @@ "host": "127.0.0.1", "dialect": "mysql", "logging": false + }, + "github": { + "username": "root", + "password": "password", + "database": "forum", + "host": "127.0.0.1", + "dialect": "mysql", + "logging": false } } From fe01b7fc741892ac666e6a719c80636b12114f21 Mon Sep 17 00:00:00 2001 From: tuterwell <46310069+tuterwell@users.noreply.github.com> Date: Tue, 12 Sep 2023 16:16:52 +0800 Subject: [PATCH 17/61] Update unit-test-helper.js --- helpers/unit-test-helper.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helpers/unit-test-helper.js b/helpers/unit-test-helper.js index ceed6030a..2f80195ed 100644 --- a/helpers/unit-test-helper.js +++ b/helpers/unit-test-helper.js @@ -14,7 +14,7 @@ const createModelMock = (name, data, joinedTableName, sourceData) => { ...data[objIndex], ...changes } - return Promise.resolve() + return Promise.resolve([1, [data[objIndex]]]) }, destroy: function () { if (joinedTableName) { From 13c29eb7bfb446180a7a8adc1e707a991897ff0e Mon Sep 17 00:00:00 2001 From: tuterwell <46310069+tuterwell@users.noreply.github.com> Date: Tue, 12 Sep 2023 16:17:34 +0800 Subject: [PATCH 18/61] Update unit-test-helper.js update() --- helpers/unit-test-helper.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helpers/unit-test-helper.js b/helpers/unit-test-helper.js index ceed6030a..2f80195ed 100644 --- a/helpers/unit-test-helper.js +++ b/helpers/unit-test-helper.js @@ -14,7 +14,7 @@ const createModelMock = (name, data, joinedTableName, sourceData) => { ...data[objIndex], ...changes } - return Promise.resolve() + return Promise.resolve([1, [data[objIndex]]]) }, destroy: function () { if (joinedTableName) { From e2665dad55839d8f96322f8727310181a0fba4a8 Mon Sep 17 00:00:00 2001 From: EugeneChen Date: Sun, 17 Sep 2023 23:08:23 +0800 Subject: [PATCH 19/61] chore: format code --- helpers/unit-test-helper.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/helpers/unit-test-helper.js b/helpers/unit-test-helper.js index ceed6030a..e79b8e74b 100644 --- a/helpers/unit-test-helper.js +++ b/helpers/unit-test-helper.js @@ -8,13 +8,14 @@ const createModelMock = (name, data, joinedTableName, sourceData) => { const defaultValue = data[0] const mockModel = dbMock.define(name, defaultValue, { instanceMethods: { - update: function (changes) { + update: async function (changes) { const objIndex = data.findIndex(d => d.id === this.get('id')) data[objIndex] = { ...data[objIndex], ...changes } - return Promise.resolve() + const ThisModel = dbMock.model(name); + return ThisModel.build(data[objIndex]) }, destroy: function () { if (joinedTableName) { From 1be932b9d1998168679d2cdf5a7a9e2f1d2bca91 Mon Sep 17 00:00:00 2001 From: EugeneChen Date: Sun, 17 Sep 2023 23:11:38 +0800 Subject: [PATCH 20/61] test: fix update method --- tests/R01.test.js | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/tests/R01.test.js b/tests/R01.test.js index e635e363b..e419d79cb 100644 --- a/tests/R01.test.js +++ b/tests/R01.test.js @@ -7,10 +7,10 @@ const app = require('../app') const { createModelMock, createControllerProxy, mockRequest, mockResponse, mockNext } = require('../helpers/unit-test-helper'); describe('# R01', () => { - describe('登入測試: POST /signin', function(){ + describe('登入測試: POST /signin', function () { // 以下測試會發出請求,測試資料庫內是否有作業指定的使用者資料 // 測試資料的來源是真實的資料庫 - it('#1 密碼錯誤', function(done){ + it('#1 密碼錯誤', function (done) { request(app) // 對 POST /signin 發出請求,參數是錯誤的密碼 .post('/signin') @@ -21,7 +21,7 @@ describe('# R01', () => { .expect(302, done) }) - it('#2 帳號錯誤', function(done){ + it('#2 帳號錯誤', function (done) { request(app) // 對 POST /signin 發出請求,參數是錯誤的帳號 .post('/signin') @@ -32,13 +32,13 @@ describe('# R01', () => { .expect(302, done) }) - it('#3 成功登入', function(done){ + it('#3 成功登入', function (done) { request(app) // 對 POST /signin 發出請求,參數是作業指定的使用者帳號密碼 .post('/signin') .type('urlencoded') .send('email=root@example.com&password=12345678') - // 期待登入驗證成功,重新導向 /restaurants + // 期待登入驗證成功,重新導向 /restaurants .expect('Location', '/restaurants') .expect(302, done) }) @@ -59,7 +59,7 @@ describe('# R01', () => { // 修改 adminController 中的資料庫連線設定,由連向真實的資料庫 -> 改為連向模擬的 User table this.adminController = createControllerProxy('../controllers/admin-controller', { User: this.UserMock }) }) - + // 開始測試 context('# [顯示使用者清單]', () => { it(' GET /admin/users ', async () => { @@ -80,10 +80,10 @@ describe('# R01', () => { context('# [修改使用者權限] for root', () => { before(() => { - // 製作假資料 - // 本 context 會用這筆資料進行測試 + // 製作假資料 + // 本 context 會用這筆資料進行測試 this.UserMock = createModelMock( - 'User', + 'User', [{ id: 1, email: 'root@example.com', @@ -91,7 +91,7 @@ describe('# R01', () => { isAdmin: true, // 是管理者 }] ) - + // 將 adminController 中的 User db 取代成 User mock db this.adminController = createControllerProxy('../controllers/admin-controller', { User: this.UserMock }) }) @@ -102,14 +102,14 @@ describe('# R01', () => { const res = mockResponse() const next = mockNext - // 測試作業指定的 adminController.patchUser 函式 + // 測試作業指定的 adminController.patchUser 函式 await this.adminController.patchUser(req, res, next) // patchUser 正確執行的話,應呼叫 req.flash // req.flash 的參數應該要與下列字串一致 - req.flash.calledWith('error_messages','禁止變更 root 權限').should.be.true + req.flash.calledWith('error_messages', '禁止變更 root 權限').should.be.true - // patchUser 執行完畢,應呼叫 res.redirect 並重新導向上一頁 + // patchUser 執行完畢,應呼叫 res.redirect 並重新導向上一頁 res.redirect.calledWith('back').should.be.true }) }) @@ -140,9 +140,9 @@ describe('# R01', () => { // 測試作業指定的 adminController.patchUser 函式 await this.adminController.patchUser(req, res, next) - // patchUser 正確執行的話,應呼叫 req.flash + // patchUser 正確執行的話,應呼叫 req.flash // req.flash 的參數應與下列字串一致 - req.flash.calledWith('success_messages','使用者權限變更成功').should.be.true + req.flash.calledWith('success_messages', '使用者權限變更成功').should.be.true // patchUser 執行完畢,應呼叫 res.redirect 並重新導向 /admin/users res.redirect.calledWith('/admin/users').should.be.true @@ -179,9 +179,9 @@ describe('# R01', () => { // 測試作業指定的 adminController.patchUser 函式 await this.adminController.patchUser(req, res, next) - // patchUser 正確執行的話,應呼叫 req.flash + // patchUser 正確執行的話,應呼叫 req.flash // req.flash 的參數應與下列字串一致 - req.flash.calledWith('success_messages','使用者權限變更成功').should.be.true + req.flash.calledWith('success_messages', '使用者權限變更成功').should.be.true // patchUser 執行完畢,應呼叫 res.redirect 並重新導向 /admin/users res.redirect.calledWith('/admin/users').should.be.true From fa173a70d6dd2ed5da3ca72f1c26527d34fdb337 Mon Sep 17 00:00:00 2001 From: EugeneChen Date: Sun, 17 Sep 2023 23:32:47 +0800 Subject: [PATCH 21/61] chore: fix code format --- helpers/unit-test-helper.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helpers/unit-test-helper.js b/helpers/unit-test-helper.js index e79b8e74b..17ccb5f67 100644 --- a/helpers/unit-test-helper.js +++ b/helpers/unit-test-helper.js @@ -14,7 +14,7 @@ const createModelMock = (name, data, joinedTableName, sourceData) => { ...data[objIndex], ...changes } - const ThisModel = dbMock.model(name); + const ThisModel = dbMock.model(name) return ThisModel.build(data[objIndex]) }, destroy: function () { From 1b8cd24141905460223684ad5dff33d4d2f863ff Mon Sep 17 00:00:00 2001 From: tuterwell <46310069+tuterwell@users.noreply.github.com> Date: Mon, 18 Sep 2023 13:21:50 +0800 Subject: [PATCH 22/61] Delete .travis.yml --- .travis.yml | 20 -------------------- 1 file changed, 20 deletions(-) delete mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index a61512188..000000000 --- a/.travis.yml +++ /dev/null @@ -1,20 +0,0 @@ -language: node_js -node_js: - - "14" - -services: - - mysql - -# 設定參數(Travis CI 會按照參數分別執行) -env: - - NODE_ENV=travis JWT_SECRET=JWT_SECRET - -# 在 install 前執行的指令 -before_install: - - mysql -e 'CREATE DATABASE IF NOT EXISTS forum default character set utf8mb4 collate utf8mb4_unicode_ci;' - - npm install sequelize-cli -g - -# 在執行測試之前的指令 -before_script: - - sequelize db:migrate - - sequelize db:seed:all From c26e8ee7e762a258826717c09138927ade57cdb4 Mon Sep 17 00:00:00 2001 From: tuterwell <46310069+tuterwell@users.noreply.github.com> Date: Mon, 18 Sep 2023 13:22:49 +0800 Subject: [PATCH 23/61] Delete .travis.yml --- .travis.yml | 20 -------------------- 1 file changed, 20 deletions(-) delete mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index a61512188..000000000 --- a/.travis.yml +++ /dev/null @@ -1,20 +0,0 @@ -language: node_js -node_js: - - "14" - -services: - - mysql - -# 設定參數(Travis CI 會按照參數分別執行) -env: - - NODE_ENV=travis JWT_SECRET=JWT_SECRET - -# 在 install 前執行的指令 -before_install: - - mysql -e 'CREATE DATABASE IF NOT EXISTS forum default character set utf8mb4 collate utf8mb4_unicode_ci;' - - npm install sequelize-cli -g - -# 在執行測試之前的指令 -before_script: - - sequelize db:migrate - - sequelize db:seed:all From 10fd9435db38af216ee1c5f5219426a189146587 Mon Sep 17 00:00:00 2001 From: sdfghjka Date: Sun, 2 Feb 2025 14:10:51 +0800 Subject: [PATCH 24/61] add index page --- app.js | 20 +- controllers/restaurant-controller.js | 6 + package-lock.json | 5733 +------------------------- package.json | 1 + routes/index.js | 13 +- views/layouts/main.hbs | 23 + views/restaurants.hbs | 1 + 7 files changed, 104 insertions(+), 5693 deletions(-) create mode 100644 controllers/restaurant-controller.js create mode 100644 views/layouts/main.hbs create mode 100644 views/restaurants.hbs diff --git a/app.js b/app.js index 0a97155de..41092c536 100644 --- a/app.js +++ b/app.js @@ -1,13 +1,15 @@ -const express = require('express') -const routes = require('./routes') +const express = require("express"); +const routes = require("./routes"); +const handlebars = require("express-handlebars"); +const app = express(); +const port = process.env.PORT || 3000; -const app = express() -const port = process.env.PORT || 3000 - -app.use(routes) +app.use(routes); +app.engine("hbs", handlebars({ extname: ".hbs" })); +app.set("view engine", "hbs"); app.listen(port, () => { - console.info(`Example app listening on port ${port}!`) -}) + console.info(`Example app listening on port http://localhost:${port}/`); +}); -module.exports = app +module.exports = app; diff --git a/controllers/restaurant-controller.js b/controllers/restaurant-controller.js new file mode 100644 index 000000000..69753cab9 --- /dev/null +++ b/controllers/restaurant-controller.js @@ -0,0 +1,6 @@ +const restaurantController = { + getRestaurants: (req, res) => { + return res.render('restaurants') + } +} +module.exports = restaurantController diff --git a/package-lock.json b/package-lock.json index 7fdc1f3c0..6ec7f8a87 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,5677 +1,8 @@ { "name": "forum-express-grading", "version": "3.0.1", - "lockfileVersion": 2, + "lockfileVersion": 1, "requires": true, - "packages": { - "": { - "name": "forum-express-grading", - "version": "3.0.1", - "dependencies": { - "express": "^4.17.1", - "mysql2": "^2.3.0", - "sequelize": "^6.6.5", - "sequelize-cli": "^6.2.0" - }, - "devDependencies": { - "chai": "^4.3.4", - "eslint": "^7.32.0", - "eslint-config-standard": "^16.0.3", - "eslint-plugin-import": "^2.23.4", - "eslint-plugin-node": "^11.1.0", - "eslint-plugin-promise": "^5.1.0", - "mocha": "^9.1.1", - "nodemon": "^2.0.12", - "proxyquire": "^2.1.3", - "sequelize-mock": "^0.10.2", - "sinon": "^11.1.2", - "supertest": "^6.1.6" - } - }, - "node_modules/@babel/code-frame": { - "version": "7.12.11", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.11.tgz", - "integrity": "sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw==", - "dev": true, - "dependencies": { - "@babel/highlight": "^7.10.4" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.15.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.15.7.tgz", - "integrity": "sha512-K4JvCtQqad9OY2+yTU8w+E82ywk/fe+ELNlt1G8z3bVGlZfn/hOcQQsUhGhW/N+tb3fxK800wLtKOE/aM0m72w==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/highlight": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.16.0.tgz", - "integrity": "sha512-t8MH41kUQylBtu2+4IQA3atqevA2lRgqA2wyVB/YiWmsDSuylZZuXOUy9ric30hfzauEFfdsuk/eXTRrGrfd0g==", - "dev": true, - "dependencies": { - "@babel/helper-validator-identifier": "^7.15.7", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/highlight/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", - "dev": true, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/@babel/highlight/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@eslint/eslintrc": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.4.3.tgz", - "integrity": "sha512-J6KFFz5QCYUJq3pf0mjEcCJVERbzv71PUIDczuh9JkwGEzced6CO5ADLHB1rbf/+oPBtoPfMYNOpGDzCANlbXw==", - "dev": true, - "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.1.1", - "espree": "^7.3.0", - "globals": "^13.9.0", - "ignore": "^4.0.6", - "import-fresh": "^3.2.1", - "js-yaml": "^3.13.1", - "minimatch": "^3.0.4", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^10.12.0 || >=12.0.0" - } - }, - "node_modules/@eslint/eslintrc/node_modules/debug": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", - "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", - "dev": true, - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/@eslint/eslintrc/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "node_modules/@humanwhocodes/config-array": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.5.0.tgz", - "integrity": "sha512-FagtKFz74XrTl7y6HCzQpwDfXP0yhxe9lHLD1UZxjvZIcbyRz8zTFF/yYNfSfzU414eDwZ1SrO0Qvtyf+wFMQg==", - "dev": true, - "dependencies": { - "@humanwhocodes/object-schema": "^1.2.0", - "debug": "^4.1.1", - "minimatch": "^3.0.4" - }, - "engines": { - "node": ">=10.10.0" - } - }, - "node_modules/@humanwhocodes/config-array/node_modules/debug": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", - "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", - "dev": true, - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/@humanwhocodes/config-array/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "node_modules/@humanwhocodes/object-schema": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", - "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", - "dev": true - }, - "node_modules/@sindresorhus/is": { - "version": "0.14.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz", - "integrity": "sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/@sinonjs/commons": { - "version": "1.8.3", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.3.tgz", - "integrity": "sha512-xkNcLAn/wZaX14RPlwizcKicDk9G3F8m2nU3L7Ukm5zBgTwiT0wsoFAHx9Jq56fJA1z/7uKGtCRu16sOUCLIHQ==", - "dev": true, - "dependencies": { - "type-detect": "4.0.8" - } - }, - "node_modules/@sinonjs/fake-timers": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-7.1.2.tgz", - "integrity": "sha512-iQADsW4LBMISqZ6Ci1dupJL9pprqwcVFTcOsEmQOEhW+KLCVn/Y4Jrvg2k19fIHCp+iFprriYPTdRcQR8NbUPg==", - "dev": true, - "dependencies": { - "@sinonjs/commons": "^1.7.0" - } - }, - "node_modules/@sinonjs/samsam": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-6.1.1.tgz", - "integrity": "sha512-cZ7rKJTLiE7u7Wi/v9Hc2fs3Ucc3jrWeMgPHbbTCeVAB2S0wOBbYlkJVeNSL04i7fdhT8wIbDq1zhC/PXTD2SA==", - "dev": true, - "dependencies": { - "@sinonjs/commons": "^1.6.0", - "lodash.get": "^4.4.2", - "type-detect": "^4.0.8" - } - }, - "node_modules/@sinonjs/text-encoding": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.2.tgz", - "integrity": "sha512-sXXKG+uL9IrKqViTtao2Ws6dy0znu9sOaP1di/jKGW1M6VssO8vlpXCQcpZ+jisQ1tTFAC5Jo/EOzFbggBagFQ==", - "dev": true - }, - "node_modules/@szmarczak/http-timer": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-1.1.2.tgz", - "integrity": "sha512-XIB2XbzHTN6ieIjfIMV9hlVcfPU26s2vafYWQcZHWXHOxiaRZYEDKEwdl129Zyg50+foYV2jCgtrqSA6qNuNSA==", - "dev": true, - "dependencies": { - "defer-to-connect": "^1.0.1" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/@types/debug": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.7.tgz", - "integrity": "sha512-9AonUzyTjXXhEOa0DnqpzZi6VHlqKMswga9EXjpXnnqxwLtdvPPtlO8evrI5D9S6asFRCQ6v+wpiUKbw+vKqyg==", - "dependencies": { - "@types/ms": "*" - } - }, - "node_modules/@types/json5": { - "version": "0.0.29", - "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", - "integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=", - "dev": true - }, - "node_modules/@types/ms": { - "version": "0.7.31", - "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.31.tgz", - "integrity": "sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==" - }, - "node_modules/@types/node": { - "version": "14.14.5", - "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.5.tgz", - "integrity": "sha512-H5Wn24s/ZOukBmDn03nnGTp18A60ny9AmCwnEcgJiTgSGsCO7k+NWP7zjCCbhlcnVCoI+co52dUAt9GMhOSULw==" - }, - "node_modules/@types/validator": { - "version": "13.7.9", - "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.7.9.tgz", - "integrity": "sha512-y5KJ1PjGXPpU4CZ7lThDu31s+FqvzhqwMOR6Go/x6xaQMFjgzwfzfOvCwABsylr/5n8sB1qFQm1Vi7TaCB8P+A==" - }, - "node_modules/@ungap/promise-all-settled": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz", - "integrity": "sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q==", - "dev": true - }, - "node_modules/abbrev": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" - }, - "node_modules/accepts": { - "version": "1.3.7", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", - "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==", - "dependencies": { - "mime-types": "~2.1.24", - "negotiator": "0.6.2" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/acorn": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", - "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", - "dev": true, - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ansi-align": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz", - "integrity": "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==", - "dev": true, - "dependencies": { - "string-width": "^4.1.0" - } - }, - "node_modules/ansi-align/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/ansi-align/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true - }, - "node_modules/ansi-align/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/ansi-align/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/ansi-align/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/ansi-colors": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", - "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/anymatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", - "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", - "dev": true, - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dev": true, - "dependencies": { - "sprintf-js": "~1.0.2" - } - }, - "node_modules/array-flatten": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" - }, - "node_modules/array-includes": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.4.tgz", - "integrity": "sha512-ZTNSQkmWumEbiHO2GF4GmWxYVTiQyJy2XOTa15sdQSrvKn7l+180egQMqlrMOUMCyLMD7pmyQe4mMDUT6Behrw==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.19.1", - "get-intrinsic": "^1.1.1", - "is-string": "^1.0.7" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.flat": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.2.5.tgz", - "integrity": "sha512-KaYU+S+ndVqyUnignHftkwc58o3uVU1jzczILJ1tN2YaIZpFIKBiP/x/j97E5MVPsaCloPbqWLB/8qCTVvT2qg==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.19.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/asap": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", - "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", - "dev": true - }, - "node_modules/assertion-error": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", - "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", - "dev": true, - "engines": { - "node": "*" - } - }, - "node_modules/astral-regex": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", - "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true - }, - "node_modules/balanced-match": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" - }, - "node_modules/binary-extensions": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.1.0.tgz", - "integrity": "sha512-1Yj8h9Q+QDF5FzhMs/c9+6UntbD5MkRfRwac8DoEm9ZfUBZ7tZ55YcGVAzEe4bXsdQHEk+s9S5wsOKVdZrw0tQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/bluebird": { - "version": "3.7.2", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", - "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==" - }, - "node_modules/body-parser": { - "version": "1.19.0", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", - "integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==", - "dependencies": { - "bytes": "3.1.0", - "content-type": "~1.0.4", - "debug": "2.6.9", - "depd": "~1.1.2", - "http-errors": "1.7.2", - "iconv-lite": "0.4.24", - "on-finished": "~2.3.0", - "qs": "6.7.0", - "raw-body": "2.4.0", - "type-is": "~1.6.17" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/boxen": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/boxen/-/boxen-5.1.2.tgz", - "integrity": "sha512-9gYgQKXx+1nP8mP7CzFyaUARhg7D3n1dF/FnErWmu9l6JvGpNUN278h0aSb+QjoiKSWG+iZ3uHrcqk0qrY9RQQ==", - "dev": true, - "dependencies": { - "ansi-align": "^3.0.0", - "camelcase": "^6.2.0", - "chalk": "^4.1.0", - "cli-boxes": "^2.2.1", - "string-width": "^4.2.2", - "type-fest": "^0.20.2", - "widest-line": "^3.1.0", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/boxen/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/boxen/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/boxen/node_modules/camelcase": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.2.0.tgz", - "integrity": "sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/boxen/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/boxen/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/boxen/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true - }, - "node_modules/boxen/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/boxen/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/boxen/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/boxen/node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, - "dependencies": { - "fill-range": "^7.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/browser-stdout": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", - "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", - "dev": true - }, - "node_modules/bytes": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", - "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/cacheable-request": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-6.1.0.tgz", - "integrity": "sha512-Oj3cAGPCqOZX7Rz64Uny2GYAZNliQSqfbePrgAQ1wKAihYmCUnraBtJtKcGR4xz7wF+LoJC+ssFZvv5BgF9Igg==", - "dev": true, - "dependencies": { - "clone-response": "^1.0.2", - "get-stream": "^5.1.0", - "http-cache-semantics": "^4.0.0", - "keyv": "^3.0.0", - "lowercase-keys": "^2.0.0", - "normalize-url": "^4.1.0", - "responselike": "^1.0.2" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/cacheable-request/node_modules/get-stream": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", - "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", - "dev": true, - "dependencies": { - "pump": "^3.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cacheable-request/node_modules/lowercase-keys": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", - "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/call-bind": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", - "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", - "dev": true, - "dependencies": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "engines": { - "node": ">=6" - } - }, - "node_modules/chai": { - "version": "4.3.6", - "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.6.tgz", - "integrity": "sha512-bbcp3YfHCUzMOvKqsztczerVgBKSsEijCySNlHHbX3VG1nskvqjz5Rfso1gGwD6w6oOV3eI60pKuMOV5MV7p3Q==", - "dev": true, - "dependencies": { - "assertion-error": "^1.1.0", - "check-error": "^1.0.2", - "deep-eql": "^3.0.1", - "get-func-name": "^2.0.0", - "loupe": "^2.3.1", - "pathval": "^1.1.1", - "type-detect": "^4.0.5" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/chalk": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", - "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/chalk/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/chalk/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/chalk/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/check-error": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", - "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=", - "dev": true, - "engines": { - "node": "*" - } - }, - "node_modules/chokidar": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", - "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", - "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - ], - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/ci-info": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", - "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", - "dev": true - }, - "node_modules/cli-boxes": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-2.2.1.tgz", - "integrity": "sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw==", - "dev": true, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cli-color": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/cli-color/-/cli-color-1.4.0.tgz", - "integrity": "sha512-xu6RvQqqrWEo6MPR1eixqGPywhYBHRs653F9jfXB2Hx4jdM/3WxiNE1vppRmxtMIfl16SFYTpYlrnqH/HsK/2w==", - "dependencies": { - "ansi-regex": "^2.1.1", - "d": "1", - "es5-ext": "^0.10.46", - "es6-iterator": "^2.0.3", - "memoizee": "^0.4.14", - "timers-ext": "^0.1.5" - } - }, - "node_modules/cliui": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", - "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", - "dependencies": { - "string-width": "^3.1.0", - "strip-ansi": "^5.2.0", - "wrap-ansi": "^5.1.0" - } - }, - "node_modules/clone-response": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz", - "integrity": "sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws=", - "dev": true, - "dependencies": { - "mimic-response": "^1.0.0" - } - }, - "node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" - }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" - }, - "node_modules/component-emitter": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", - "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==", - "dev": true - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" - }, - "node_modules/config-chain": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.12.tgz", - "integrity": "sha512-a1eOIcu8+7lUInge4Rpf/n4Krkf3Dd9lqhljRzII1/Zno/kRtUWnznPO3jOKBmTEktkt3fkxisUcivoj0ebzoA==", - "dependencies": { - "ini": "^1.3.4", - "proto-list": "~1.2.1" - } - }, - "node_modules/configstore": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/configstore/-/configstore-5.0.1.tgz", - "integrity": "sha512-aMKprgk5YhBNyH25hj8wGt2+D52Sw1DRRIzqBwLp2Ya9mFmY8KPvvtvmna8SxVR9JMZ4kzMD68N22vlaRpkeFA==", - "dev": true, - "dependencies": { - "dot-prop": "^5.2.0", - "graceful-fs": "^4.1.2", - "make-dir": "^3.0.0", - "unique-string": "^2.0.0", - "write-file-atomic": "^3.0.0", - "xdg-basedir": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/content-disposition": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", - "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==", - "dependencies": { - "safe-buffer": "5.1.2" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/content-type": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", - "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", - "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie-signature": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" - }, - "node_modules/cookiejar": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.3.tgz", - "integrity": "sha512-JxbCBUdrfr6AQjOXrxoTvAMJO4HBTUIlBzslcJPAz+/KT8yk53fXun51u+RenNYvad/+Vc2DIz5o9UxlCDymFQ==", - "dev": true - }, - "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", - "dev": true, - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/crypto-random-string": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", - "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/d": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/d/-/d-1.0.1.tgz", - "integrity": "sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA==", - "dependencies": { - "es5-ext": "^0.10.50", - "type": "^1.0.1" - } - }, - "node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/decompress-response": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-3.3.0.tgz", - "integrity": "sha1-gKTdMjdIOEv6JICDYirt7Jgq3/M=", - "dev": true, - "dependencies": { - "mimic-response": "^1.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/deep-eql": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", - "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", - "dev": true, - "dependencies": { - "type-detect": "^4.0.0" - }, - "engines": { - "node": ">=0.12" - } - }, - "node_modules/deep-extend": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", - "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", - "dev": true, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true - }, - "node_modules/defer-to-connect": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-1.1.3.tgz", - "integrity": "sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ==", - "dev": true - }, - "node_modules/define-properties": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", - "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", - "dev": true, - "dependencies": { - "object-keys": "^1.0.12" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "dev": true, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/denque": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", - "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", - "engines": { - "node": ">=0.10" - } - }, - "node_modules/depd": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", - "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/destroy": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", - "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" - }, - "node_modules/dezalgo": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.3.tgz", - "integrity": "sha512-K7i4zNfT2kgQz3GylDw40ot9GAE47sFZ9EXHFSPP6zONLgH6kWXE0KWJchkbQJLBkRazq4APwZ4OwiFFlT95OQ==", - "dev": true, - "dependencies": { - "asap": "^2.0.0", - "wrappy": "1" - } - }, - "node_modules/diff": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", - "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", - "dev": true, - "engines": { - "node": ">=0.3.1" - } - }, - "node_modules/doctrine": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", - "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", - "dev": true, - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/dot-prop": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz", - "integrity": "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==", - "dev": true, - "dependencies": { - "is-obj": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/dottie": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/dottie/-/dottie-2.0.2.tgz", - "integrity": "sha512-fmrwR04lsniq/uSr8yikThDTrM7epXHBAAjH9TbeH3rEA8tdCO7mRzB9hdmdGyJCxF8KERo9CITcm3kGuoyMhg==" - }, - "node_modules/duplexer3": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz", - "integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=", - "dev": true - }, - "node_modules/editorconfig": { - "version": "0.15.3", - "resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-0.15.3.tgz", - "integrity": "sha512-M9wIMFx96vq0R4F+gRpY3o2exzb8hEj/n9S8unZtHSvYjibBp/iMufSzvmOcV/laG0ZtuTVGtiJggPOSW2r93g==", - "dependencies": { - "commander": "^2.19.0", - "lru-cache": "^4.1.5", - "semver": "^5.6.0", - "sigmund": "^1.0.1" - }, - "bin": { - "editorconfig": "bin/editorconfig" - } - }, - "node_modules/editorconfig/node_modules/lru-cache": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", - "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", - "dependencies": { - "pseudomap": "^1.0.2", - "yallist": "^2.1.2" - } - }, - "node_modules/editorconfig/node_modules/semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "bin": { - "semver": "bin/semver" - } - }, - "node_modules/editorconfig/node_modules/yallist": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", - "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=" - }, - "node_modules/ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" - }, - "node_modules/emoji-regex": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", - "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==" - }, - "node_modules/encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/end-of-stream": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", - "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", - "dev": true, - "dependencies": { - "once": "^1.4.0" - } - }, - "node_modules/enquirer": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz", - "integrity": "sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==", - "dev": true, - "dependencies": { - "ansi-colors": "^4.1.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/es-abstract": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.19.1.tgz", - "integrity": "sha512-2vJ6tjA/UfqLm2MPs7jxVybLoB8i1t1Jd9R3kISld20sIxPcTbLuggQOUxeWeAvIUkduv/CfMjuh4WmiXr2v9w==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", - "get-intrinsic": "^1.1.1", - "get-symbol-description": "^1.0.0", - "has": "^1.0.3", - "has-symbols": "^1.0.2", - "internal-slot": "^1.0.3", - "is-callable": "^1.2.4", - "is-negative-zero": "^2.0.1", - "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.1", - "is-string": "^1.0.7", - "is-weakref": "^1.0.1", - "object-inspect": "^1.11.0", - "object-keys": "^1.1.1", - "object.assign": "^4.1.2", - "string.prototype.trimend": "^1.0.4", - "string.prototype.trimstart": "^1.0.4", - "unbox-primitive": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/es-to-primitive": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", - "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", - "dev": true, - "dependencies": { - "is-callable": "^1.1.4", - "is-date-object": "^1.0.1", - "is-symbol": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/es5-ext": { - "version": "0.10.53", - "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.53.tgz", - "integrity": "sha512-Xs2Stw6NiNHWypzRTY1MtaG/uJlwCk8kH81920ma8mvN8Xq1gsfhZvpkImLQArw8AHnv8MT2I45J3c0R8slE+Q==", - "dependencies": { - "es6-iterator": "~2.0.3", - "es6-symbol": "~3.1.3", - "next-tick": "~1.0.0" - } - }, - "node_modules/es6-iterator": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", - "integrity": "sha1-p96IkUGgWpSwhUQDstCg+/qY87c=", - "dependencies": { - "d": "1", - "es5-ext": "^0.10.35", - "es6-symbol": "^3.1.1" - } - }, - "node_modules/es6-symbol": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.3.tgz", - "integrity": "sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA==", - "dependencies": { - "d": "^1.0.1", - "ext": "^1.1.2" - } - }, - "node_modules/es6-weak-map": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/es6-weak-map/-/es6-weak-map-2.0.3.tgz", - "integrity": "sha512-p5um32HOTO1kP+w7PRnB+5lQ43Z6muuMuIMffvDN8ZB4GcnjLBV6zGStpbASIMk4DCAvEaamhe2zhyCb/QXXsA==", - "dependencies": { - "d": "1", - "es5-ext": "^0.10.46", - "es6-iterator": "^2.0.3", - "es6-symbol": "^3.1.1" - } - }, - "node_modules/escalade": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/escape-goat": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/escape-goat/-/escape-goat-2.1.1.tgz", - "integrity": "sha512-8/uIhbG12Csjy2JEW7D9pHbreaVaS/OpN3ycnyvElTdwM5n6GY6W6e2IPemfvGZeUMqZ9A/3GqIZMgKnBhAw/Q==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" - }, - "node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint": { - "version": "7.32.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.32.0.tgz", - "integrity": "sha512-VHZ8gX+EDfz+97jGcgyGCyRia/dPOd6Xh9yPv8Bl1+SoaIwD+a/vlrOmGRUyOYu7MwUhc7CxqeaDZU13S4+EpA==", - "dev": true, - "dependencies": { - "@babel/code-frame": "7.12.11", - "@eslint/eslintrc": "^0.4.3", - "@humanwhocodes/config-array": "^0.5.0", - "ajv": "^6.10.0", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", - "debug": "^4.0.1", - "doctrine": "^3.0.0", - "enquirer": "^2.3.5", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^5.1.1", - "eslint-utils": "^2.1.0", - "eslint-visitor-keys": "^2.0.0", - "espree": "^7.3.1", - "esquery": "^1.4.0", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", - "functional-red-black-tree": "^1.0.1", - "glob-parent": "^5.1.2", - "globals": "^13.6.0", - "ignore": "^4.0.6", - "import-fresh": "^3.0.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "js-yaml": "^3.13.1", - "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.4.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.0.4", - "natural-compare": "^1.4.0", - "optionator": "^0.9.1", - "progress": "^2.0.0", - "regexpp": "^3.1.0", - "semver": "^7.2.1", - "strip-ansi": "^6.0.0", - "strip-json-comments": "^3.1.0", - "table": "^6.0.9", - "text-table": "^0.2.0", - "v8-compile-cache": "^2.0.3" - }, - "bin": { - "eslint": "bin/eslint.js" - }, - "engines": { - "node": "^10.12.0 || >=12.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-config-standard": { - "version": "16.0.3", - "resolved": "https://registry.npmjs.org/eslint-config-standard/-/eslint-config-standard-16.0.3.tgz", - "integrity": "sha512-x4fmJL5hGqNJKGHSjnLdgA6U6h1YW/G2dW9fA+cyVur4SK6lyue8+UgNKWlZtUDTXvgKDD/Oa3GQjmB5kjtVvg==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "peerDependencies": { - "eslint": "^7.12.1", - "eslint-plugin-import": "^2.22.1", - "eslint-plugin-node": "^11.1.0", - "eslint-plugin-promise": "^4.2.1 || ^5.0.0" - } - }, - "node_modules/eslint-import-resolver-node": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.6.tgz", - "integrity": "sha512-0En0w03NRVMn9Uiyn8YRPDKvWjxCWkslUEhGNTdGx15RvPJYQ+lbOlqrlNI2vEAs4pDYK4f/HN2TbDmk5TP0iw==", - "dev": true, - "dependencies": { - "debug": "^3.2.7", - "resolve": "^1.20.0" - } - }, - "node_modules/eslint-import-resolver-node/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "dependencies": { - "ms": "^2.1.1" - } - }, - "node_modules/eslint-import-resolver-node/node_modules/is-core-module": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.8.0.tgz", - "integrity": "sha512-vd15qHsaqrRL7dtH6QNuy0ndJmRDrS9HAM1CAiSifNUFv4x1a0CCVsj18hJ1mShxIG6T2i1sO78MkP56r0nYRw==", - "dev": true, - "dependencies": { - "has": "^1.0.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/eslint-import-resolver-node/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true - }, - "node_modules/eslint-import-resolver-node/node_modules/resolve": { - "version": "1.20.0", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz", - "integrity": "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==", - "dev": true, - "dependencies": { - "is-core-module": "^2.2.0", - "path-parse": "^1.0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/eslint-module-utils": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.7.1.tgz", - "integrity": "sha512-fjoetBXQZq2tSTWZ9yWVl2KuFrTZZH3V+9iD1V1RfpDgxzJR+mPd/KZmMiA8gbPqdBzpNiEHOuT7IYEWxrH0zQ==", - "dev": true, - "dependencies": { - "debug": "^3.2.7", - "find-up": "^2.1.0", - "pkg-dir": "^2.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/eslint-module-utils/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "dependencies": { - "ms": "^2.1.1" - } - }, - "node_modules/eslint-module-utils/node_modules/find-up": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", - "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", - "dev": true, - "dependencies": { - "locate-path": "^2.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/eslint-module-utils/node_modules/locate-path": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", - "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", - "dev": true, - "dependencies": { - "p-locate": "^2.0.0", - "path-exists": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/eslint-module-utils/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true - }, - "node_modules/eslint-module-utils/node_modules/p-limit": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", - "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", - "dev": true, - "dependencies": { - "p-try": "^1.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/eslint-module-utils/node_modules/p-locate": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", - "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", - "dev": true, - "dependencies": { - "p-limit": "^1.1.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/eslint-module-utils/node_modules/p-try": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", - "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/eslint-plugin-es": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-es/-/eslint-plugin-es-3.0.1.tgz", - "integrity": "sha512-GUmAsJaN4Fc7Gbtl8uOBlayo2DqhwWvEzykMHSCZHU3XdJ+NSzzZcVhXh3VxX5icqQ+oQdIEawXX8xkR3mIFmQ==", - "dev": true, - "dependencies": { - "eslint-utils": "^2.0.0", - "regexpp": "^3.0.0" - }, - "engines": { - "node": ">=8.10.0" - }, - "funding": { - "url": "https://github.com/sponsors/mysticatea" - }, - "peerDependencies": { - "eslint": ">=4.19.1" - } - }, - "node_modules/eslint-plugin-import": { - "version": "2.25.3", - "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.25.3.tgz", - "integrity": "sha512-RzAVbby+72IB3iOEL8clzPLzL3wpDrlwjsTBAQXgyp5SeTqqY+0bFubwuo+y/HLhNZcXV4XqTBO4LGsfyHIDXg==", - "dev": true, - "dependencies": { - "array-includes": "^3.1.4", - "array.prototype.flat": "^1.2.5", - "debug": "^2.6.9", - "doctrine": "^2.1.0", - "eslint-import-resolver-node": "^0.3.6", - "eslint-module-utils": "^2.7.1", - "has": "^1.0.3", - "is-core-module": "^2.8.0", - "is-glob": "^4.0.3", - "minimatch": "^3.0.4", - "object.values": "^1.1.5", - "resolve": "^1.20.0", - "tsconfig-paths": "^3.11.0" - }, - "engines": { - "node": ">=4" - }, - "peerDependencies": { - "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8" - } - }, - "node_modules/eslint-plugin-import/node_modules/doctrine": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", - "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", - "dev": true, - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/eslint-plugin-import/node_modules/is-core-module": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.8.0.tgz", - "integrity": "sha512-vd15qHsaqrRL7dtH6QNuy0ndJmRDrS9HAM1CAiSifNUFv4x1a0CCVsj18hJ1mShxIG6T2i1sO78MkP56r0nYRw==", - "dev": true, - "dependencies": { - "has": "^1.0.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/eslint-plugin-import/node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/eslint-plugin-import/node_modules/resolve": { - "version": "1.20.0", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz", - "integrity": "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==", - "dev": true, - "dependencies": { - "is-core-module": "^2.2.0", - "path-parse": "^1.0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/eslint-plugin-node": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-node/-/eslint-plugin-node-11.1.0.tgz", - "integrity": "sha512-oUwtPJ1W0SKD0Tr+wqu92c5xuCeQqB3hSCHasn/ZgjFdA9iDGNkNf2Zi9ztY7X+hNuMib23LNGRm6+uN+KLE3g==", - "dev": true, - "dependencies": { - "eslint-plugin-es": "^3.0.0", - "eslint-utils": "^2.0.0", - "ignore": "^5.1.1", - "minimatch": "^3.0.4", - "resolve": "^1.10.1", - "semver": "^6.1.0" - }, - "engines": { - "node": ">=8.10.0" - }, - "peerDependencies": { - "eslint": ">=5.16.0" - } - }, - "node_modules/eslint-plugin-node/node_modules/ignore": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz", - "integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==", - "dev": true, - "engines": { - "node": ">= 4" - } - }, - "node_modules/eslint-plugin-node/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/eslint-plugin-promise": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-promise/-/eslint-plugin-promise-5.2.0.tgz", - "integrity": "sha512-SftLb1pUG01QYq2A/hGAWfDRXqYD82zE7j7TopDOyNdU+7SvvoXREls/+PRTY17vUXzXnZA/zfnyKgRH6x4JJw==", - "dev": true, - "engines": { - "node": "^10.12.0 || >=12.0.0" - }, - "peerDependencies": { - "eslint": "^7.0.0" - } - }, - "node_modules/eslint-scope": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", - "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", - "dev": true, - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/eslint-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", - "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", - "dev": true, - "dependencies": { - "eslint-visitor-keys": "^1.1.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/mysticatea" - } - }, - "node_modules/eslint-utils/node_modules/eslint-visitor-keys": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", - "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/eslint-visitor-keys": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", - "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", - "dev": true, - "engines": { - "node": ">=10" - } - }, - "node_modules/eslint/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/eslint/node_modules/debug": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", - "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", - "dev": true, - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/eslint/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "node_modules/eslint/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/espree": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-7.3.1.tgz", - "integrity": "sha512-v3JCNCE64umkFpmkFGqzVKsOT0tN1Zr+ueqLZfpV1Ob8e+CEgPWa+OxCoGH3tnhimMKIaBm4m/vaRpJ/krRz2g==", - "dev": true, - "dependencies": { - "acorn": "^7.4.0", - "acorn-jsx": "^5.3.1", - "eslint-visitor-keys": "^1.3.0" - }, - "engines": { - "node": "^10.12.0 || >=12.0.0" - } - }, - "node_modules/espree/node_modules/eslint-visitor-keys": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", - "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true, - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/esquery": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz", - "integrity": "sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==", - "dev": true, - "dependencies": { - "estraverse": "^5.1.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/esquery/node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, - "dependencies": { - "estraverse": "^5.2.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/esrecurse/node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "dev": true, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/event-emitter": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz", - "integrity": "sha1-34xp7vFkeSPHFXuc6DhAYQsCzDk=", - "dependencies": { - "d": "1", - "es5-ext": "~0.10.14" - } - }, - "node_modules/express": { - "version": "4.17.1", - "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz", - "integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==", - "dependencies": { - "accepts": "~1.3.7", - "array-flatten": "1.1.1", - "body-parser": "1.19.0", - "content-disposition": "0.5.3", - "content-type": "~1.0.4", - "cookie": "0.4.0", - "cookie-signature": "1.0.6", - "debug": "2.6.9", - "depd": "~1.1.2", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "~1.1.2", - "fresh": "0.5.2", - "merge-descriptors": "1.0.1", - "methods": "~1.1.2", - "on-finished": "~2.3.0", - "parseurl": "~1.3.3", - "path-to-regexp": "0.1.7", - "proxy-addr": "~2.0.5", - "qs": "6.7.0", - "range-parser": "~1.2.1", - "safe-buffer": "5.1.2", - "send": "0.17.1", - "serve-static": "1.14.1", - "setprototypeof": "1.1.1", - "statuses": "~1.5.0", - "type-is": "~1.6.18", - "utils-merge": "1.0.1", - "vary": "~1.1.2" - }, - "engines": { - "node": ">= 0.10.0" - } - }, - "node_modules/ext": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/ext/-/ext-1.4.0.tgz", - "integrity": "sha512-Key5NIsUxdqKg3vIsdw9dSuXpPCQ297y6wBjL30edxwPgt2E44WcWBZey/ZvUc6sERLTxKdyCu4gZFmUbk1Q7A==", - "dependencies": { - "type": "^2.0.0" - } - }, - "node_modules/ext/node_modules/type": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/type/-/type-2.1.0.tgz", - "integrity": "sha512-G9absDWvhAWCV2gmF1zKud3OyC61nZDwWvBL2DApaVFogI07CprggiQAOOjvp2NRjYWFzPyu7vwtDrQFq8jeSA==" - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true - }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", - "dev": true - }, - "node_modules/fast-safe-stringify": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", - "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", - "dev": true - }, - "node_modules/file-entry-cache": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", - "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", - "dev": true, - "dependencies": { - "flat-cache": "^3.0.4" - }, - "engines": { - "node": "^10.12.0 || >=12.0.0" - } - }, - "node_modules/fill-keys": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/fill-keys/-/fill-keys-1.0.2.tgz", - "integrity": "sha1-mo+jb06K1jTjv2tPPIiCVRRS6yA=", - "dev": true, - "dependencies": { - "is-object": "~1.0.1", - "merge-descriptors": "~1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/finalhandler": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", - "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", - "dependencies": { - "debug": "2.6.9", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "on-finished": "~2.3.0", - "parseurl": "~1.3.3", - "statuses": "~1.5.0", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/find-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", - "dependencies": { - "locate-path": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/flat": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", - "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", - "dev": true, - "bin": { - "flat": "cli.js" - } - }, - "node_modules/flat-cache": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", - "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", - "dev": true, - "dependencies": { - "flatted": "^3.1.0", - "rimraf": "^3.0.2" - }, - "engines": { - "node": "^10.12.0 || >=12.0.0" - } - }, - "node_modules/flatted": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.4.tgz", - "integrity": "sha512-8/sOawo8tJ4QOBX8YlQBMxL8+RLZfxMQOif9o0KUKTNTjMYElWPE0r/m5VNFxTRd0NSw8qSy8dajrwX4RYI1Hw==", - "dev": true - }, - "node_modules/form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "dev": true, - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/formidable": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/formidable/-/formidable-2.0.1.tgz", - "integrity": "sha512-rjTMNbp2BpfQShhFbR3Ruk3qk2y9jKpvMW78nJgx8QKtxjDVrwbZG+wvDOmVbifHyOUOQJXxqEy6r0faRrPzTQ==", - "dev": true, - "dependencies": { - "dezalgo": "1.0.3", - "hexoid": "1.0.0", - "once": "1.4.0", - "qs": "6.9.3" - }, - "funding": { - "url": "https://ko-fi.com/tunnckoCore/commissions" - } - }, - "node_modules/formidable/node_modules/qs": { - "version": "6.9.3", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.9.3.tgz", - "integrity": "sha512-EbZYNarm6138UKKq46tdx08Yo/q9ZhFoAXAI1meAFd2GtbRDhbZY2WQSICskT0c5q99aFzLG1D4nvTk9tqfXIw==", - "dev": true, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/forwarded": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", - "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fs-extra": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", - "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", - "dependencies": { - "graceful-fs": "^4.1.2", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" - }, - "engines": { - "node": ">=6 <7 || >=8" - } - }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" - }, - "node_modules/fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "dev": true, - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" - }, - "node_modules/functional-red-black-tree": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", - "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", - "dev": true - }, - "node_modules/generate-function": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", - "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", - "dependencies": { - "is-property": "^1.0.2" - } - }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "engines": { - "node": "6.* || 8.* || >= 10.*" - } - }, - "node_modules/get-func-name": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", - "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=", - "dev": true, - "engines": { - "node": "*" - } - }, - "node_modules/get-intrinsic": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz", - "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==", - "dev": true, - "dependencies": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-stream": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", - "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", - "dev": true, - "dependencies": { - "pump": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/get-symbol-description": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz", - "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/glob": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", - "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/global-dirs": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-3.0.0.tgz", - "integrity": "sha512-v8ho2DS5RiCjftj1nD9NmnfaOzTdud7RRnVd9kFNOjqZbISlx5DQ+OrTkywgd0dIt7oFCvKetZSHoHcP3sDdiA==", - "dev": true, - "dependencies": { - "ini": "2.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/global-dirs/node_modules/ini": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ini/-/ini-2.0.0.tgz", - "integrity": "sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==", - "dev": true, - "engines": { - "node": ">=10" - } - }, - "node_modules/globals": { - "version": "13.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.12.0.tgz", - "integrity": "sha512-uS8X6lSKN2JumVoXrbUz+uG4BYG+eiawqm3qFcT7ammfbUHeCBoJMlHcec/S3krSk73/AE/f0szYFmgAA3kYZg==", - "dev": true, - "dependencies": { - "type-fest": "^0.20.2" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/got": { - "version": "9.6.0", - "resolved": "https://registry.npmjs.org/got/-/got-9.6.0.tgz", - "integrity": "sha512-R7eWptXuGYxwijs0eV+v3o6+XH1IqVK8dJOEecQfTmkncw9AV4dcw/Dhxi8MdlqPthxxpZyizMzyg8RTmEsG+Q==", - "dev": true, - "dependencies": { - "@sindresorhus/is": "^0.14.0", - "@szmarczak/http-timer": "^1.1.2", - "cacheable-request": "^6.0.0", - "decompress-response": "^3.3.0", - "duplexer3": "^0.1.4", - "get-stream": "^4.1.0", - "lowercase-keys": "^1.0.1", - "mimic-response": "^1.0.1", - "p-cancelable": "^1.0.0", - "to-readable-stream": "^1.0.0", - "url-parse-lax": "^3.0.0" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", - "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==" - }, - "node_modules/growl": { - "version": "1.10.5", - "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", - "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", - "dev": true, - "engines": { - "node": ">=4.x" - } - }, - "node_modules/has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dependencies": { - "function-bind": "^1.1.1" - }, - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/has-bigints": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.1.tgz", - "integrity": "sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/has-symbols": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz", - "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-tostringtag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", - "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", - "dev": true, - "dependencies": { - "has-symbols": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-yarn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/has-yarn/-/has-yarn-2.1.0.tgz", - "integrity": "sha512-UqBRqi4ju7T+TqGNdqAO0PaSVGsDGJUBQvk9eUWNGRY1CFGDzYhLWoM7JQEemnlvVcv/YEmc2wNW8BC24EnUsw==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/he": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", - "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", - "dev": true, - "bin": { - "he": "bin/he" - } - }, - "node_modules/hexoid": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/hexoid/-/hexoid-1.0.0.tgz", - "integrity": "sha512-QFLV0taWQOZtvIRIAdBChesmogZrtuXvVWsFHZTk2SU+anspqZ2vMnoLg7IE1+Uk16N19APic1BuF8bC8c2m5g==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/http-cache-semantics": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz", - "integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==", - "dev": true - }, - "node_modules/http-errors": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", - "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", - "dependencies": { - "depd": "~1.1.2", - "inherits": "2.0.3", - "setprototypeof": "1.1.1", - "statuses": ">= 1.5.0 < 2", - "toidentifier": "1.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ignore": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", - "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", - "dev": true, - "engines": { - "node": ">= 4" - } - }, - "node_modules/ignore-by-default": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", - "integrity": "sha1-SMptcvbGo68Aqa1K5odr44ieKwk=", - "dev": true - }, - "node_modules/import-fresh": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", - "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", - "dev": true, - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/import-lazy": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-2.1.0.tgz", - "integrity": "sha1-BWmOPUXIjo1+nZLLBYTnfwlvPkM=", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", - "dev": true, - "engines": { - "node": ">=0.8.19" - } - }, - "node_modules/inflection": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/inflection/-/inflection-1.13.4.tgz", - "integrity": "sha512-6I/HUDeYFfuNCVS3td055BaXBwKYuzw7K3ExVMStBowKo9oOAMJIXIHvdyR3iboTCp1b+1i5DSkIZTcwIktuDw==", - "engines": [ - "node >= 0.4.0" - ] - }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" - }, - "node_modules/ini": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", - "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==", - "deprecated": "Please update to ini >=1.3.6 to avoid a prototype pollution issue", - "engines": { - "node": "*" - } - }, - "node_modules/internal-slot": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.3.tgz", - "integrity": "sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA==", - "dev": true, - "dependencies": { - "get-intrinsic": "^1.1.0", - "has": "^1.0.3", - "side-channel": "^1.0.4" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/is-bigint": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", - "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", - "dev": true, - "dependencies": { - "has-bigints": "^1.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, - "dependencies": { - "binary-extensions": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-boolean-object": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", - "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-callable": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.4.tgz", - "integrity": "sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-ci": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-2.0.0.tgz", - "integrity": "sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==", - "dev": true, - "dependencies": { - "ci-info": "^2.0.0" - }, - "bin": { - "is-ci": "bin.js" - } - }, - "node_modules/is-core-module": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.0.0.tgz", - "integrity": "sha512-jq1AH6C8MuteOoBPwkxHafmByhL9j5q4OaPGdbuD+ZtQJVzH+i6E3BJDQcBA09k57i2Hh2yQbEG8yObZ0jdlWw==", - "dependencies": { - "has": "^1.0.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-date-object": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", - "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", - "dev": true, - "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", - "engines": { - "node": ">=4" - } - }, - "node_modules/is-glob": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", - "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", - "dev": true, - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-installed-globally": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.4.0.tgz", - "integrity": "sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ==", - "dev": true, - "dependencies": { - "global-dirs": "^3.0.0", - "is-path-inside": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-negative-zero": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", - "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-npm": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-5.0.0.tgz", - "integrity": "sha512-WW/rQLOazUq+ST/bCAVBp/2oMERWLsR7OrKyt052dNDk4DHcDE0/7QSXITlmi+VBcV13DfIbysG3tZJm5RfdBA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/is-number-object": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.6.tgz", - "integrity": "sha512-bEVOqiRcvo3zO1+G2lVMy+gkkEm9Yh7cDMRusKKu5ZJKPUYSJwICTKZrNKHA2EbSP0Tu0+6B/emsYNHZyn6K8g==", - "dev": true, - "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-obj": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", - "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-object": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-object/-/is-object-1.0.2.tgz", - "integrity": "sha512-2rRIahhZr2UWb45fIOuvZGpFtz0TyOZLf32KxBbSoUCeZR495zCKlWUKKUByk3geS2eAs7ZAABt0Y/Rx0GiQGA==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-path-inside": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", - "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-plain-obj": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", - "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-promise": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz", - "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==" - }, - "node_modules/is-property": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", - "integrity": "sha1-V/4cTkhHTt1lsJkR8msc1Ald2oQ=" - }, - "node_modules/is-regex": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", - "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-shared-array-buffer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.1.tgz", - "integrity": "sha512-IU0NmyknYZN0rChcKhRO1X8LYz5Isj/Fsqh8NJOSf+N/hCOTwy29F32Ik7a+QszE63IdvmwdTPDd6cZ5pg4cwA==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-string": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", - "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", - "dev": true, - "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-symbol": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", - "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", - "dev": true, - "dependencies": { - "has-symbols": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-typedarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", - "dev": true - }, - "node_modules/is-unicode-supported": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", - "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-weakref": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", - "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-yarn-global": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/is-yarn-global/-/is-yarn-global-0.3.0.tgz", - "integrity": "sha512-VjSeb/lHmkoyd8ryPVIKvOCn4D1koMqY+vqyjjUfc3xyKtP4dYOxM44sZrnqQSzSds3xyOrUTLTC9LVCVgLngw==", - "dev": true - }, - "node_modules/isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==", - "dev": true - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", - "dev": true - }, - "node_modules/js-beautify": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.13.0.tgz", - "integrity": "sha512-/Tbp1OVzZjbwzwJQFIlYLm9eWQ+3aYbBXLSaqb1mEJzhcQAfrqMMQYtjb6io+U6KpD0ID4F+Id3/xcjH3l/sqA==", - "dependencies": { - "config-chain": "^1.1.12", - "editorconfig": "^0.15.3", - "glob": "^7.1.3", - "mkdirp": "^1.0.4", - "nopt": "^5.0.0" - }, - "bin": { - "css-beautify": "js/bin/css-beautify.js", - "html-beautify": "js/bin/html-beautify.js", - "js-beautify": "js/bin/js-beautify.js" - } - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true - }, - "node_modules/js-yaml": { - "version": "3.14.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.0.tgz", - "integrity": "sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A==", - "dev": true, - "dependencies": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/json-buffer": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.0.tgz", - "integrity": "sha1-Wx85evx11ne96Lz8Dkfh+aPZqJg=", - "dev": true - }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true - }, - "node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", - "dev": true - }, - "node_modules/json5": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", - "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", - "dev": true, - "dependencies": { - "minimist": "^1.2.0" - }, - "bin": { - "json5": "lib/cli.js" - } - }, - "node_modules/jsonfile": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", - "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/just-extend": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.2.1.tgz", - "integrity": "sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg==", - "dev": true - }, - "node_modules/keyv": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-3.1.0.tgz", - "integrity": "sha512-9ykJ/46SN/9KPM/sichzQ7OvXyGDYKGTaDlKMGCAlg2UK8KRy4jb0d8sFc+0Tt0YYnThq8X2RZgCg74RPxgcVA==", - "dev": true, - "dependencies": { - "json-buffer": "3.0.0" - } - }, - "node_modules/latest-version": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-5.1.0.tgz", - "integrity": "sha512-weT+r0kTkRQdCdYCNtkMwWXQTMEswKrFBkm4ckQOMVhhqhIMI1UT2hMj+1iigIhgSZm5gTmrRXBNoGUgaTY1xA==", - "dev": true, - "dependencies": { - "package-json": "^6.3.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/locate-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", - "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", - "dependencies": { - "p-locate": "^3.0.0", - "path-exists": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" - }, - "node_modules/lodash.get": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", - "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", - "dev": true - }, - "node_modules/lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true - }, - "node_modules/lodash.truncate": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz", - "integrity": "sha1-WjUNoLERO4N+z//VgSy+WNbq4ZM=", - "dev": true - }, - "node_modules/log-symbols": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", - "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", - "dev": true, - "dependencies": { - "chalk": "^4.1.0", - "is-unicode-supported": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/long": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", - "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==" - }, - "node_modules/loupe": { - "version": "2.3.4", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.4.tgz", - "integrity": "sha512-OvKfgCC2Ndby6aSTREl5aCCPTNIzlDfQZvZxNUrBrihDhL3xcrYegTblhmEiCrg2kKQz4XsFIaemE5BF4ybSaQ==", - "dev": true, - "dependencies": { - "get-func-name": "^2.0.0" - } - }, - "node_modules/lowercase-keys": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz", - "integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/lru-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/lru-queue/-/lru-queue-0.1.0.tgz", - "integrity": "sha1-Jzi9nw089PhEkMVzbEhpmsYyzaM=", - "dependencies": { - "es5-ext": "~0.10.2" - } - }, - "node_modules/make-dir": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", - "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", - "dev": true, - "dependencies": { - "semver": "^6.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/make-dir/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/memoizee": { - "version": "0.4.14", - "resolved": "https://registry.npmjs.org/memoizee/-/memoizee-0.4.14.tgz", - "integrity": "sha512-/SWFvWegAIYAO4NQMpcX+gcra0yEZu4OntmUdrBaWrJncxOqAziGFlHxc7yjKVK2uu3lpPW27P27wkR82wA8mg==", - "dependencies": { - "d": "1", - "es5-ext": "^0.10.45", - "es6-weak-map": "^2.0.2", - "event-emitter": "^0.3.5", - "is-promise": "^2.1", - "lru-queue": "0.1", - "next-tick": "1", - "timers-ext": "^0.1.5" - } - }, - "node_modules/merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" - }, - "node_modules/methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/mime-db": { - "version": "1.44.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.44.0.tgz", - "integrity": "sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.27", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.27.tgz", - "integrity": "sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w==", - "dependencies": { - "mime-db": "1.44.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mimic-response": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", - "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", - "dev": true - }, - "node_modules/mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", - "bin": { - "mkdirp": "bin/cmd.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/mocha": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-9.2.2.tgz", - "integrity": "sha512-L6XC3EdwT6YrIk0yXpavvLkn8h+EU+Y5UcCHKECyMbdUIxyMuZj4bX4U9e1nvnvUUvQVsV2VHQr5zLdcUkhW/g==", - "dev": true, - "dependencies": { - "@ungap/promise-all-settled": "1.1.2", - "ansi-colors": "4.1.1", - "browser-stdout": "1.3.1", - "chokidar": "3.5.3", - "debug": "4.3.3", - "diff": "5.0.0", - "escape-string-regexp": "4.0.0", - "find-up": "5.0.0", - "glob": "7.2.0", - "growl": "1.10.5", - "he": "1.2.0", - "js-yaml": "4.1.0", - "log-symbols": "4.1.0", - "minimatch": "4.2.1", - "ms": "2.1.3", - "nanoid": "3.3.1", - "serialize-javascript": "6.0.0", - "strip-json-comments": "3.1.1", - "supports-color": "8.1.1", - "which": "2.0.2", - "workerpool": "6.2.0", - "yargs": "16.2.0", - "yargs-parser": "20.2.4", - "yargs-unparser": "2.0.0" - }, - "bin": { - "_mocha": "bin/_mocha", - "mocha": "bin/mocha" - }, - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mochajs" - } - }, - "node_modules/mocha/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/mocha/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/mocha/node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true - }, - "node_modules/mocha/node_modules/cliui": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", - "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", - "dev": true, - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^7.0.0" - } - }, - "node_modules/mocha/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/mocha/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/mocha/node_modules/debug": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", - "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", - "dev": true, - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/mocha/node_modules/debug/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "node_modules/mocha/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true - }, - "node_modules/mocha/node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/mocha/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/mocha/node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/mocha/node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/mocha/node_modules/minimatch": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-4.2.1.tgz", - "integrity": "sha512-9Uq1ChtSZO+Mxa/CL1eGizn2vRn3MlLgzhT0Iz8zaY8NdvxvB0d5QdPFmCKf7JKA9Lerx5vRrnwO03jsSfGG9g==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/mocha/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true - }, - "node_modules/mocha/node_modules/p-limit": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.0.2.tgz", - "integrity": "sha512-iwqZSOoWIW+Ew4kAGUlN16J4M7OB3ysMLSZtnhmqx7njIHFPlxWBX8xo3lVTyFVq6mI/lL9qt2IsN1sHwaxJkg==", - "dev": true, - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/mocha/node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/mocha/node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/mocha/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/mocha/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/mocha/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, - "node_modules/mocha/node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/mocha/node_modules/y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true, - "engines": { - "node": ">=10" - } - }, - "node_modules/mocha/node_modules/yargs": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", - "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", - "dev": true, - "dependencies": { - "cliui": "^7.0.2", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.0", - "y18n": "^5.0.5", - "yargs-parser": "^20.2.2" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/mocha/node_modules/yargs-parser": { - "version": "20.2.4", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", - "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==", - "dev": true, - "engines": { - "node": ">=10" - } - }, - "node_modules/module-not-found-error": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/module-not-found-error/-/module-not-found-error-1.0.1.tgz", - "integrity": "sha1-z4tP9PKWQGdNbN0CsOO8UjwrvcA=", - "dev": true - }, - "node_modules/moment": { - "version": "2.29.4", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz", - "integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==", - "engines": { - "node": "*" - } - }, - "node_modules/moment-timezone": { - "version": "0.5.38", - "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.38.tgz", - "integrity": "sha512-nMIrzGah4+oYZPflDvLZUgoVUO4fvAqHstvG3xAUnMolWncuAiLDWNnJZj6EwJGMGfb1ZcuTFE6GI3hNOVWI/Q==", - "dependencies": { - "moment": ">= 2.9.0" - }, - "engines": { - "node": "*" - } - }, - "node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" - }, - "node_modules/mysql2": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-2.3.3.tgz", - "integrity": "sha512-wxJUev6LgMSgACDkb/InIFxDprRa6T95+VEoR+xPvtngtccNH2dGjEB/fVZ8yg1gWv1510c9CvXuJHi5zUm0ZA==", - "dependencies": { - "denque": "^2.0.1", - "generate-function": "^2.3.1", - "iconv-lite": "^0.6.3", - "long": "^4.0.0", - "lru-cache": "^6.0.0", - "named-placeholders": "^1.1.2", - "seq-queue": "^0.0.5", - "sqlstring": "^2.3.2" - }, - "engines": { - "node": ">= 8.0" - } - }, - "node_modules/mysql2/node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/named-placeholders": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.2.tgz", - "integrity": "sha512-wiFWqxoLL3PGVReSZpjLVxyJ1bRqe+KKJVbr4hGs1KWfTZTQyezHFBbuKj9hsizHyGV2ne7EMjHdxEGAybD5SA==", - "dependencies": { - "lru-cache": "^4.1.3" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/named-placeholders/node_modules/lru-cache": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", - "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", - "dependencies": { - "pseudomap": "^1.0.2", - "yallist": "^2.1.2" - } - }, - "node_modules/named-placeholders/node_modules/yallist": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", - "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=" - }, - "node_modules/nanoid": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.1.tgz", - "integrity": "sha512-n6Vs/3KGyxPQd6uO0eH4Bv0ojGSUvuLlIHtC3Y0kEO23YRge8H9x1GCzLn28YX0H66pMkxuaeESFq4tKISKwdw==", - "dev": true, - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", - "dev": true - }, - "node_modules/negotiator": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", - "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/next-tick": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz", - "integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw=" - }, - "node_modules/nise": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/nise/-/nise-5.1.1.tgz", - "integrity": "sha512-yr5kW2THW1AkxVmCnKEh4nbYkJdB3I7LUkiUgOvEkOp414mc2UMaHMA7pjq1nYowhdoJZGwEKGaQVbxfpWj10A==", - "dev": true, - "dependencies": { - "@sinonjs/commons": "^1.8.3", - "@sinonjs/fake-timers": ">=5", - "@sinonjs/text-encoding": "^0.7.1", - "just-extend": "^4.0.2", - "path-to-regexp": "^1.7.0" - } - }, - "node_modules/nise/node_modules/path-to-regexp": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", - "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==", - "dev": true, - "dependencies": { - "isarray": "0.0.1" - } - }, - "node_modules/nodemon": { - "version": "2.0.14", - "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.14.tgz", - "integrity": "sha512-frcpDx+PviKEQRSYzwhckuO2zoHcBYLHI754RE9z5h1RGtrngerc04mLpQQCPWBkH/2ObrX7We9YiwVSYZpFJQ==", - "dev": true, - "hasInstallScript": true, - "dependencies": { - "chokidar": "^3.2.2", - "debug": "^3.2.6", - "ignore-by-default": "^1.0.1", - "minimatch": "^3.0.4", - "pstree.remy": "^1.1.7", - "semver": "^5.7.1", - "supports-color": "^5.5.0", - "touch": "^3.1.0", - "undefsafe": "^2.0.3", - "update-notifier": "^5.1.0" - }, - "bin": { - "nodemon": "bin/nodemon.js" - }, - "engines": { - "node": ">=8.10.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/nodemon" - } - }, - "node_modules/nodemon/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "dependencies": { - "ms": "^2.1.1" - } - }, - "node_modules/nodemon/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/nodemon/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true - }, - "node_modules/nodemon/node_modules/semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true, - "bin": { - "semver": "bin/semver" - } - }, - "node_modules/nodemon/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/nopt": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", - "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", - "dependencies": { - "abbrev": "1" - }, - "bin": { - "nopt": "bin/nopt.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/normalize-url": { - "version": "4.5.1", - "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-4.5.1.tgz", - "integrity": "sha512-9UZCFRHQdNrfTpGg8+1INIg93B6zE0aXMVFkw1WFwvO4SlZywU6aLg5Of0Ap/PgcbSw4LNxvMWXMeugwMCX0AA==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/object-inspect": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.0.tgz", - "integrity": "sha512-Ho2z80bVIvJloH+YzRmpZVQe87+qASmBUKZDWgx9cu+KDrX2ZDH/3tMy+gXbZETVGs2M8YdxObOh7XAtim9Y0g==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "dev": true, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/object.assign": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz", - "integrity": "sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.0", - "define-properties": "^1.1.3", - "has-symbols": "^1.0.1", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.values": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.5.tgz", - "integrity": "sha512-QUZRW0ilQ3PnPpbNtgdNV1PDbEqLIiSFB3l+EnGtBQ/8SUTLj1PZwtQHABZtLgwpJZTSZhuGLOGk57Drx2IvYg==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.19.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/on-finished": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", - "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/optionator": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", - "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", - "dev": true, - "dependencies": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.3" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/p-cancelable": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-1.1.0.tgz", - "integrity": "sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", - "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", - "dependencies": { - "p-limit": "^2.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "engines": { - "node": ">=6" - } - }, - "node_modules/package-json": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/package-json/-/package-json-6.5.0.tgz", - "integrity": "sha512-k3bdm2n25tkyxcjSKzB5x8kfVxlMdgsbPr0GkZcwHsLpba6cBjqCt1KlcChKEvxHIcTB1FVMuwoijZ26xex5MQ==", - "dev": true, - "dependencies": { - "got": "^9.6.0", - "registry-auth-token": "^4.0.0", - "registry-url": "^5.0.0", - "semver": "^6.2.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/package-json/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/path-exists": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", - "engines": { - "node": ">=4" - } - }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/path-parse": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", - "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==" - }, - "node_modules/path-to-regexp": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" - }, - "node_modules/pathval": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", - "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", - "dev": true, - "engines": { - "node": "*" - } - }, - "node_modules/pg-connection-string": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.5.0.tgz", - "integrity": "sha512-r5o/V/ORTA6TmUnyWZR9nCj1klXCO2CEKNRlVuJptZe85QuhFayC7WeMic7ndayT5IRIR0S0xFxFi2ousartlQ==" - }, - "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/pkg-dir": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-2.0.0.tgz", - "integrity": "sha1-9tXREJ4Z1j7fQo4L1X4Sd3YVM0s=", - "dev": true, - "dependencies": { - "find-up": "^2.1.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/pkg-dir/node_modules/find-up": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", - "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", - "dev": true, - "dependencies": { - "locate-path": "^2.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/pkg-dir/node_modules/locate-path": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", - "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", - "dev": true, - "dependencies": { - "p-locate": "^2.0.0", - "path-exists": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/pkg-dir/node_modules/p-limit": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", - "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", - "dev": true, - "dependencies": { - "p-try": "^1.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/pkg-dir/node_modules/p-locate": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", - "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", - "dev": true, - "dependencies": { - "p-limit": "^1.1.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/pkg-dir/node_modules/p-try": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", - "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/prepend-http": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz", - "integrity": "sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc=", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/progress": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", - "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", - "dev": true, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/proto-list": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", - "integrity": "sha1-IS1b/hMYMGpCD2QCuOJv85ZHqEk=" - }, - "node_modules/proxy-addr": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.6.tgz", - "integrity": "sha512-dh/frvCBVmSsDYzw6n926jv974gddhkFPfiN8hPOi30Wax25QZyZEGveluCgliBnqmuM+UJmBErbAUFIoDbjOw==", - "dependencies": { - "forwarded": "~0.1.2", - "ipaddr.js": "1.9.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/proxyquire": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/proxyquire/-/proxyquire-2.1.3.tgz", - "integrity": "sha512-BQWfCqYM+QINd+yawJz23tbBM40VIGXOdDw3X344KcclI/gtBbdWF6SlQ4nK/bYhF9d27KYug9WzljHC6B9Ysg==", - "dev": true, - "dependencies": { - "fill-keys": "^1.0.2", - "module-not-found-error": "^1.0.1", - "resolve": "^1.11.1" - } - }, - "node_modules/pseudomap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", - "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=" - }, - "node_modules/pstree.remy": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", - "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", - "dev": true - }, - "node_modules/pump": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", - "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", - "dev": true, - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "node_modules/punycode": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/pupa": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/pupa/-/pupa-2.1.1.tgz", - "integrity": "sha512-l1jNAspIBSFqbT+y+5FosojNpVpF94nlI+wDUpqP9enwOTfHx9f0gh5nB96vl+6yTpsJsypeNrwfzPrKuHB41A==", - "dev": true, - "dependencies": { - "escape-goat": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/qs": { - "version": "6.7.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", - "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==", - "engines": { - "node": ">=0.6" - } - }, - "node_modules/randombytes": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", - "dev": true, - "dependencies": { - "safe-buffer": "^5.1.0" - } - }, - "node_modules/range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/raw-body": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz", - "integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==", - "dependencies": { - "bytes": "3.1.0", - "http-errors": "1.7.2", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/rc": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", - "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", - "dev": true, - "dependencies": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - }, - "bin": { - "rc": "cli.js" - } - }, - "node_modules/rc/node_modules/strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, - "node_modules/regexpp": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", - "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", - "dev": true, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/mysticatea" - } - }, - "node_modules/registry-auth-token": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-4.2.1.tgz", - "integrity": "sha512-6gkSb4U6aWJB4SF2ZvLb76yCBjcvufXBqvvEx1HbmKPkutswjW1xNVRY0+daljIYRbogN7O0etYSlbiaEQyMyw==", - "dev": true, - "dependencies": { - "rc": "^1.2.8" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/registry-url": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-5.1.0.tgz", - "integrity": "sha512-8acYXXTI0AkQv6RAOjE3vOaIXZkT9wo4LOFbBKYQEEnnMNBpKqdUrI6S4NT0KPIo/WVvJ5tE/X5LF/TQUf0ekw==", - "dev": true, - "dependencies": { - "rc": "^1.2.8" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/require-from-string": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/require-main-filename": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", - "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==" - }, - "node_modules/resolve": { - "version": "1.18.1", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.18.1.tgz", - "integrity": "sha512-lDfCPaMKfOJXjy0dPayzPdF1phampNWr3qFCjAu+rw/qbQmr5jWH5xN2hwh9QKfw9E5v4hwV7A+jrCmL8yjjqA==", - "dependencies": { - "is-core-module": "^2.0.0", - "path-parse": "^1.0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/responselike": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/responselike/-/responselike-1.0.2.tgz", - "integrity": "sha1-kYcg7ztjHFZCvgaPFa3lpG9Loec=", - "dev": true, - "dependencies": { - "lowercase-keys": "^1.0.0" - } - }, - "node_modules/retry-as-promised": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/retry-as-promised/-/retry-as-promised-6.1.0.tgz", - "integrity": "sha512-Hj/jY+wFC+SB9SDlIIFWiGOHnNG0swYbGYsOj2BJ8u2HKUaobNKab0OIC0zOLYzDy0mb7A4xA5BMo4LMz5YtEA==" - }, - "node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dev": true, - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" - }, - "node_modules/semver": { - "version": "7.3.8", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", - "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/semver-diff": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/semver-diff/-/semver-diff-3.1.1.tgz", - "integrity": "sha512-GX0Ix/CJcHyB8c4ykpHGIAvLyOwOobtM/8d+TQkAd81/bEjgPHrfba41Vpesr7jX/t8Uh+R3EX9eAS5be+jQYg==", - "dev": true, - "dependencies": { - "semver": "^6.3.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/semver-diff/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/send": { - "version": "0.17.1", - "resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz", - "integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==", - "dependencies": { - "debug": "2.6.9", - "depd": "~1.1.2", - "destroy": "~1.0.4", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "~1.7.2", - "mime": "1.6.0", - "ms": "2.1.1", - "on-finished": "~2.3.0", - "range-parser": "~1.2.1", - "statuses": "~1.5.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/send/node_modules/ms": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", - "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" - }, - "node_modules/seq-queue": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/seq-queue/-/seq-queue-0.0.5.tgz", - "integrity": "sha1-1WgS4cAXpuTnw+Ojeh2m143TyT4=" - }, - "node_modules/sequelize": { - "version": "6.25.3", - "resolved": "https://registry.npmjs.org/sequelize/-/sequelize-6.25.3.tgz", - "integrity": "sha512-sbbvDGft6UfSRdIC0dcZvMxxJYi6g0+IgWvIpTuk6ccEoIyLcr7Sk6nXWwGfkdlDabGeunhhhI4yH8PleVMvtw==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/sequelize" - } - ], - "dependencies": { - "@types/debug": "^4.1.7", - "@types/validator": "^13.7.1", - "debug": "^4.3.3", - "dottie": "^2.0.2", - "inflection": "^1.13.2", - "lodash": "^4.17.21", - "moment": "^2.29.1", - "moment-timezone": "^0.5.34", - "pg-connection-string": "^2.5.0", - "retry-as-promised": "^6.1.0", - "semver": "^7.3.5", - "sequelize-pool": "^7.1.0", - "toposort-class": "^1.0.1", - "uuid": "^8.3.2", - "validator": "^13.7.0", - "wkx": "^0.5.0" - }, - "engines": { - "node": ">=10.0.0" - }, - "peerDependenciesMeta": { - "ibm_db": { - "optional": true - }, - "mariadb": { - "optional": true - }, - "mysql2": { - "optional": true - }, - "oracledb": { - "optional": true - }, - "pg": { - "optional": true - }, - "pg-hstore": { - "optional": true - }, - "snowflake-sdk": { - "optional": true - }, - "sqlite3": { - "optional": true - }, - "tedious": { - "optional": true - } - } - }, - "node_modules/sequelize-cli": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/sequelize-cli/-/sequelize-cli-6.2.0.tgz", - "integrity": "sha512-6WQ2x91hg30dUn66mXHnzvHATZ4pyI1GHSNbS/TNN/vRR4BLRSLijadeMgC8zqmKDsL0VqzVVopJWfJakuP++Q==", - "dependencies": { - "cli-color": "^1.4.0", - "fs-extra": "^7.0.1", - "js-beautify": "^1.8.8", - "lodash": "^4.17.5", - "resolve": "^1.5.0", - "umzug": "^2.3.0", - "yargs": "^13.1.0" - }, - "bin": { - "sequelize": "lib/sequelize", - "sequelize-cli": "lib/sequelize" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/sequelize-mock": { - "version": "0.10.2", - "resolved": "https://registry.npmjs.org/sequelize-mock/-/sequelize-mock-0.10.2.tgz", - "integrity": "sha1-GdOXHM2utbhkFwwkznkqinHxRL0=", - "dev": true, - "dependencies": { - "bluebird": "^3.4.6", - "inflection": "^1.10.0", - "lodash": "^4.16.4" - } - }, - "node_modules/sequelize-pool": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/sequelize-pool/-/sequelize-pool-7.1.0.tgz", - "integrity": "sha512-G9c0qlIWQSK29pR/5U2JF5dDQeqqHRragoyahj/Nx4KOOQ3CPPfzxnfqFPCSB7x5UgjOgnZ61nSxz+fjDpRlJg==", - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/sequelize/node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/sequelize/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - }, - "node_modules/serialize-javascript": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", - "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", - "dev": true, - "dependencies": { - "randombytes": "^2.1.0" - } - }, - "node_modules/serve-static": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz", - "integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==", - "dependencies": { - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "parseurl": "~1.3.3", - "send": "0.17.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/set-blocking": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" - }, - "node_modules/setprototypeof": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", - "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/side-channel": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", - "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/sigmund": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz", - "integrity": "sha1-P/IfGYytIXX587eBhT/ZTQ0ZtZA=" - }, - "node_modules/signal-exit": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.5.tgz", - "integrity": "sha512-KWcOiKeQj6ZyXx7zq4YxSMgHRlod4czeBQZrPb8OKcohcqAXShm7E20kEMle9WBt26hFcAf0qLOcp5zmY7kOqQ==", - "dev": true - }, - "node_modules/sinon": { - "version": "11.1.2", - "resolved": "https://registry.npmjs.org/sinon/-/sinon-11.1.2.tgz", - "integrity": "sha512-59237HChms4kg7/sXhiRcUzdSkKuydDeTiamT/jesUVHshBgL8XAmhgFo0GfK6RruMDM/iRSij1EybmMog9cJw==", - "dev": true, - "dependencies": { - "@sinonjs/commons": "^1.8.3", - "@sinonjs/fake-timers": "^7.1.2", - "@sinonjs/samsam": "^6.0.2", - "diff": "^5.0.0", - "nise": "^5.1.0", - "supports-color": "^7.2.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/sinon" - } - }, - "node_modules/slice-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", - "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.0.0", - "astral-regex": "^2.0.0", - "is-fullwidth-code-point": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/slice-ansi?sponsor=1" - } - }, - "node_modules/slice-ansi/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/slice-ansi/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/slice-ansi/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/slice-ansi/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", - "dev": true - }, - "node_modules/sqlstring": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.2.tgz", - "integrity": "sha512-vF4ZbYdKS8OnoJAWBmMxCQDkiEBkGQYU7UZPtL8flbDRSNkhaXvRJ279ZtI6M+zDaQovVU4tuRgzK5fVhvFAhg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/statuses": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", - "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/string-width": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", - "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", - "dependencies": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/string.prototype.trimend": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz", - "integrity": "sha512-y9xCjw1P23Awk8EvTpcyL2NIr1j7wJ39f+k6lvRnSMz+mz9CGz9NYPelDk42kOz6+ql8xjfK8oYzy3jAP5QU5A==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.trimstart": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.4.tgz", - "integrity": "sha512-jh6e984OBfvxS50tdY2nRZnoC5/mLFKOREQfw8t5yytkoUsJRNxvI/E39qu1sD0OtWI3OC0XgKSmcWwziwYuZw==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", - "dependencies": { - "ansi-regex": "^4.1.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/strip-ansi/node_modules/ansi-regex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", - "engines": { - "node": ">=6" - } - }, - "node_modules/strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/superagent": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/superagent/-/superagent-8.0.2.tgz", - "integrity": "sha512-QtYZ9uaNAMexI7XWl2vAXAh0j4q9H7T0WVEI/y5qaUB3QLwxo+voUgCQ217AokJzUTIVOp0RTo7fhZrwhD7A2Q==", - "deprecated": "Please use v8.0.0 until https://github.com/visionmedia/superagent/issues/1743 is resolved", - "dev": true, - "dependencies": { - "component-emitter": "^1.3.0", - "cookiejar": "^2.1.3", - "debug": "^4.3.4", - "fast-safe-stringify": "^2.1.1", - "form-data": "^4.0.0", - "formidable": "^2.0.1", - "methods": "^1.1.2", - "mime": "2.6.0", - "qs": "^6.11.0", - "semver": "^7.3.7" - }, - "engines": { - "node": ">=6.4.0 <13 || >=14" - } - }, - "node_modules/superagent/node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/superagent/node_modules/mime": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", - "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", - "dev": true, - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/superagent/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "node_modules/superagent/node_modules/qs": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", - "dev": true, - "dependencies": { - "side-channel": "^1.0.4" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/supertest": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/supertest/-/supertest-6.3.0.tgz", - "integrity": "sha512-QgWju1cNoacP81Rv88NKkQ4oXTzGg0eNZtOoxp1ROpbS4OHY/eK5b8meShuFtdni161o5X0VQvgo7ErVyKK+Ow==", - "dev": true, - "dependencies": { - "methods": "^1.1.2", - "superagent": "^8.0.0" - }, - "engines": { - "node": ">=6.4.0" - } - }, - "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/table": { - "version": "6.7.5", - "resolved": "https://registry.npmjs.org/table/-/table-6.7.5.tgz", - "integrity": "sha512-LFNeryOqiQHqCVKzhkymKwt6ozeRhlm8IL1mE8rNUurkir4heF6PzMyRgaTa4tlyPTGGgXuvVOF/OLWiH09Lqw==", - "dev": true, - "dependencies": { - "ajv": "^8.0.1", - "lodash.truncate": "^4.4.2", - "slice-ansi": "^4.0.0", - "string-width": "^4.2.3", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/table/node_modules/ajv": { - "version": "8.8.2", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.8.2.tgz", - "integrity": "sha512-x9VuX+R/jcFj1DHo/fCp99esgGDWiHENrKxaCENuCxpoMCmAt/COCGVDwA7kleEpEzJjDnvh3yGoOuLu0Dtllw==", - "dev": true, - "dependencies": { - "fast-deep-equal": "^3.1.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/table/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/table/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true - }, - "node_modules/table/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/table/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true - }, - "node_modules/table/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/table/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/text-table": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", - "dev": true - }, - "node_modules/timers-ext": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/timers-ext/-/timers-ext-0.1.7.tgz", - "integrity": "sha512-b85NUNzTSdodShTIbky6ZF02e8STtVVfD+fu4aXXShEELpozH+bCpJLYMPZbsABN2wDH7fJpqIoXxJpzbf0NqQ==", - "dependencies": { - "es5-ext": "~0.10.46", - "next-tick": "1" - } - }, - "node_modules/to-readable-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/to-readable-stream/-/to-readable-stream-1.0.0.tgz", - "integrity": "sha512-Iq25XBt6zD5npPhlLVXGFN3/gyR2/qODcKNNyTMd4vbm39HUaOiAM4PMq0eMVC/Tkxz+Zjdsc55g9yyz+Yq00Q==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/toidentifier": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", - "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==", - "engines": { - "node": ">=0.6" - } - }, - "node_modules/toposort-class": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toposort-class/-/toposort-class-1.0.1.tgz", - "integrity": "sha1-f/0feMi+KMO6Rc1OGj9e4ZO9mYg=" - }, - "node_modules/touch": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz", - "integrity": "sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA==", - "dev": true, - "dependencies": { - "nopt": "~1.0.10" - }, - "bin": { - "nodetouch": "bin/nodetouch.js" - } - }, - "node_modules/touch/node_modules/nopt": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz", - "integrity": "sha1-bd0hvSoxQXuScn3Vhfim83YI6+4=", - "dev": true, - "dependencies": { - "abbrev": "1" - }, - "bin": { - "nopt": "bin/nopt.js" - }, - "engines": { - "node": "*" - } - }, - "node_modules/tsconfig-paths": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.12.0.tgz", - "integrity": "sha512-e5adrnOYT6zqVnWqZu7i/BQ3BnhzvGbjEjejFXO20lKIKpwTaupkCPgEfv4GZK1IBciJUEhYs3J3p75FdaTFVg==", - "dev": true, - "dependencies": { - "@types/json5": "^0.0.29", - "json5": "^1.0.1", - "minimist": "^1.2.0", - "strip-bom": "^3.0.0" - } - }, - "node_modules/type": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/type/-/type-1.2.0.tgz", - "integrity": "sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg==" - }, - "node_modules/type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, - "dependencies": { - "prelude-ls": "^1.2.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/type-detect": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", - "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/type-is": { - "version": "1.6.18", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", - "dependencies": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/typedarray-to-buffer": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", - "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", - "dev": true, - "dependencies": { - "is-typedarray": "^1.0.0" - } - }, - "node_modules/umzug": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/umzug/-/umzug-2.3.0.tgz", - "integrity": "sha512-Z274K+e8goZK8QJxmbRPhl89HPO1K+ORFtm6rySPhFKfKc5GHhqdzD0SGhSWHkzoXasqJuItdhorSvY7/Cgflw==", - "dependencies": { - "bluebird": "^3.7.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/unbox-primitive": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.1.tgz", - "integrity": "sha512-tZU/3NqK3dA5gpE1KtyiJUrEB0lxnGkMFHptJ7q6ewdZ8s12QrODwNbhIJStmJkd1QDXa1NRA8aF2A1zk/Ypyw==", - "dev": true, - "dependencies": { - "function-bind": "^1.1.1", - "has-bigints": "^1.0.1", - "has-symbols": "^1.0.2", - "which-boxed-primitive": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/undefsafe": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", - "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", - "dev": true - }, - "node_modules/unique-string": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz", - "integrity": "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==", - "dev": true, - "dependencies": { - "crypto-random-string": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/universalify": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", - "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", - "engines": { - "node": ">= 4.0.0" - } - }, - "node_modules/unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/update-notifier": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-5.1.0.tgz", - "integrity": "sha512-ItnICHbeMh9GqUy31hFPrD1kcuZ3rpxDZbf4KUDavXwS0bW5m7SLbDQpGX3UYr072cbrF5hFUs3r5tUsPwjfHw==", - "dev": true, - "dependencies": { - "boxen": "^5.0.0", - "chalk": "^4.1.0", - "configstore": "^5.0.1", - "has-yarn": "^2.1.0", - "import-lazy": "^2.1.0", - "is-ci": "^2.0.0", - "is-installed-globally": "^0.4.0", - "is-npm": "^5.0.0", - "is-yarn-global": "^0.3.0", - "latest-version": "^5.1.0", - "pupa": "^2.1.1", - "semver": "^7.3.4", - "semver-diff": "^3.1.1", - "xdg-basedir": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/yeoman/update-notifier?sponsor=1" - } - }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/url-parse-lax": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-3.0.0.tgz", - "integrity": "sha1-FrXK/Afb42dsGxmZF3gj1lA6yww=", - "dev": true, - "dependencies": { - "prepend-http": "^2.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/utils-merge": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=", - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "bin": { - "uuid": "dist/bin/uuid" - } - }, - "node_modules/v8-compile-cache": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz", - "integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==", - "dev": true - }, - "node_modules/validator": { - "version": "13.7.0", - "resolved": "https://registry.npmjs.org/validator/-/validator-13.7.0.tgz", - "integrity": "sha512-nYXQLCBkpJ8X6ltALua9dRrZDHVYxjJ1wgskNt1lH9fzGjs3tgojGSCBjmEPwkWS1y29+DrizMTW19Pr9uB2nw==", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/which-boxed-primitive": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", - "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", - "dev": true, - "dependencies": { - "is-bigint": "^1.0.1", - "is-boolean-object": "^1.1.0", - "is-number-object": "^1.0.4", - "is-string": "^1.0.5", - "is-symbol": "^1.0.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-module": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", - "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=" - }, - "node_modules/widest-line": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-3.1.0.tgz", - "integrity": "sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg==", - "dev": true, - "dependencies": { - "string-width": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/widest-line/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/widest-line/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true - }, - "node_modules/widest-line/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/widest-line/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/widest-line/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wkx": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/wkx/-/wkx-0.5.0.tgz", - "integrity": "sha512-Xng/d4Ichh8uN4l0FToV/258EjMGU9MGcA0HV2d9B/ZpZB3lqQm7nkOdZdm5GhKtLLhAE7PiVQwN4eN+2YJJUg==", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/word-wrap": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", - "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/workerpool": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.0.tgz", - "integrity": "sha512-Rsk5qQHJ9eowMH28Jwhe8HEbmdYDX4lwoMWshiCXugjtHqMD9ZbiqSDLxcsfdqsETPzVUtX5s1Z5kStiIM6l4A==", - "dev": true - }, - "node_modules/wrap-ansi": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", - "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", - "dependencies": { - "ansi-styles": "^3.2.0", - "string-width": "^3.0.0", - "strip-ansi": "^5.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" - }, - "node_modules/write-file-atomic": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", - "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", - "dev": true, - "dependencies": { - "imurmurhash": "^0.1.4", - "is-typedarray": "^1.0.0", - "signal-exit": "^3.0.2", - "typedarray-to-buffer": "^3.1.5" - } - }, - "node_modules/xdg-basedir": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-4.0.0.tgz", - "integrity": "sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/y18n": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", - "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==" - }, - "node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" - }, - "node_modules/yargs": { - "version": "13.3.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz", - "integrity": "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==", - "dependencies": { - "cliui": "^5.0.0", - "find-up": "^3.0.0", - "get-caller-file": "^2.0.1", - "require-directory": "^2.1.1", - "require-main-filename": "^2.0.0", - "set-blocking": "^2.0.0", - "string-width": "^3.0.0", - "which-module": "^2.0.0", - "y18n": "^4.0.0", - "yargs-parser": "^13.1.2" - } - }, - "node_modules/yargs-parser": { - "version": "13.1.2", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz", - "integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==", - "dependencies": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - } - }, - "node_modules/yargs-unparser": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", - "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", - "dev": true, - "dependencies": { - "camelcase": "^6.0.0", - "decamelize": "^4.0.0", - "flat": "^5.0.2", - "is-plain-obj": "^2.1.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/yargs-unparser/node_modules/camelcase": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.2.0.tgz", - "integrity": "sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/yargs-unparser/node_modules/decamelize": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", - "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - } - }, "dependencies": { "@babel/code-frame": { "version": "7.12.11", @@ -5910,8 +241,7 @@ "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, - "requires": {} + "dev": true }, "ajv": { "version": "6.12.6", @@ -6885,8 +1215,7 @@ "version": "16.0.3", "resolved": "https://registry.npmjs.org/eslint-config-standard/-/eslint-config-standard-16.0.3.tgz", "integrity": "sha512-x4fmJL5hGqNJKGHSjnLdgA6U6h1YW/G2dW9fA+cyVur4SK6lyue8+UgNKWlZtUDTXvgKDD/Oa3GQjmB5kjtVvg==", - "dev": true, - "requires": {} + "dev": true }, "eslint-import-resolver-node": { "version": "0.3.6", @@ -7107,8 +1436,7 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/eslint-plugin-promise/-/eslint-plugin-promise-5.2.0.tgz", "integrity": "sha512-SftLb1pUG01QYq2A/hGAWfDRXqYD82zE7j7TopDOyNdU+7SvvoXREls/+PRTY17vUXzXnZA/zfnyKgRH6x4JJw==", - "dev": true, - "requires": {} + "dev": true }, "eslint-scope": { "version": "5.1.1", @@ -7265,6 +1593,23 @@ "vary": "~1.1.2" } }, + "express-handlebars": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/express-handlebars/-/express-handlebars-5.3.3.tgz", + "integrity": "sha512-/tWy/VmfdaZ5RUZUprgDzcCTj4QEnaXHbtLSXr4ROTpC/3aynrg4nIkn1+eLlcEp4Cs7HkxDlu2jJaz3aBG//w==", + "requires": { + "glob": "^7.1.7", + "graceful-fs": "^4.2.7", + "handlebars": "^4.7.7" + }, + "dependencies": { + "graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" + } + } + }, "ext": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/ext/-/ext-1.4.0.tgz", @@ -7577,6 +1922,18 @@ "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", "dev": true }, + "handlebars": { + "version": "4.7.8", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", + "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", + "requires": { + "minimist": "^1.2.5", + "neo-async": "^2.6.2", + "source-map": "^0.6.1", + "uglify-js": "^3.1.4", + "wordwrap": "^1.0.0" + } + }, "has": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", @@ -8211,8 +2568,7 @@ "minimist": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", - "dev": true + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" }, "mkdirp": { "version": "1.0.4", @@ -8553,6 +2909,11 @@ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==" }, + "neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==" + }, "next-tick": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz", @@ -9356,6 +3717,11 @@ } } }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + }, "sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", @@ -9685,6 +4051,12 @@ "is-typedarray": "^1.0.0" } }, + "uglify-js": { + "version": "3.19.3", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", + "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", + "optional": true + }, "umzug": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/umzug/-/umzug-2.3.0.tgz", @@ -9886,6 +4258,11 @@ "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", "dev": true }, + "wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==" + }, "workerpool": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.0.tgz", diff --git a/package.json b/package.json index b6c7f1b9c..5be3eca9b 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ }, "dependencies": { "express": "^4.17.1", + "express-handlebars": "^5.3.3", "mysql2": "^2.3.0", "sequelize": "^6.6.5", "sequelize-cli": "^6.2.0" diff --git a/routes/index.js b/routes/index.js index b4b2743f7..2ceeef90b 100644 --- a/routes/index.js +++ b/routes/index.js @@ -1,8 +1,9 @@ -const express = require('express') -const router = express.Router() +const express = require("express"); +const router = express.Router(); +const restController = require("../controllers/restaurant-controller"); -router.get('/', (req, res) => { - res.send('Hello World!') -}) -module.exports = router +router.get("/restaurants", restController.getRestaurants); +router.use("/", (req, res) => res.redirect("/restaurants")); + +module.exports = router; diff --git a/views/layouts/main.hbs b/views/layouts/main.hbs new file mode 100644 index 000000000..b00d09b88 --- /dev/null +++ b/views/layouts/main.hbs @@ -0,0 +1,23 @@ + + + + + + + 餐廳評論網 + + +
+
+
+ {{{body}}} +
+
+
+ + + + \ No newline at end of file diff --git a/views/restaurants.hbs b/views/restaurants.hbs new file mode 100644 index 000000000..7b5bc24b5 --- /dev/null +++ b/views/restaurants.hbs @@ -0,0 +1 @@ +
restaurants
\ No newline at end of file From 16b2a67e6e124d4dcb654a5bd0b14ae0169a348f Mon Sep 17 00:00:00 2001 From: sdfghjka Date: Sun, 2 Feb 2025 14:49:32 +0800 Subject: [PATCH 25/61] add admin index page --- controllers/admin-controller.js | 6 ++++++ routes/index.js | 3 ++- routes/modules/admin.js | 7 +++++++ views/admin/restaurants.hbs | 1 + 4 files changed, 16 insertions(+), 1 deletion(-) create mode 100644 controllers/admin-controller.js create mode 100644 routes/modules/admin.js create mode 100644 views/admin/restaurants.hbs diff --git a/controllers/admin-controller.js b/controllers/admin-controller.js new file mode 100644 index 000000000..b72c550ce --- /dev/null +++ b/controllers/admin-controller.js @@ -0,0 +1,6 @@ +const adminController = { + getRestaurants: (req, res) => { + return res.render("admin/restaurants"); + }, +}; +module.exports = adminController; diff --git a/routes/index.js b/routes/index.js index 2ceeef90b..218aff1bc 100644 --- a/routes/index.js +++ b/routes/index.js @@ -1,9 +1,10 @@ const express = require("express"); const router = express.Router(); const restController = require("../controllers/restaurant-controller"); - +const adminController = require("./modules/admin"); router.get("/restaurants", restController.getRestaurants); +router.use("/admin",adminController); router.use("/", (req, res) => res.redirect("/restaurants")); module.exports = router; diff --git a/routes/modules/admin.js b/routes/modules/admin.js new file mode 100644 index 000000000..578718c06 --- /dev/null +++ b/routes/modules/admin.js @@ -0,0 +1,7 @@ +const express = require("express"); +const router = express.Router(); +const adminController = require("../../controllers/admin-controller"); + +router.get("/restaurants", adminController.getRestaurants); +router.use("/", (req, res) => res.redirect("/admin/restaurants")); +module.exports = router; diff --git a/views/admin/restaurants.hbs b/views/admin/restaurants.hbs new file mode 100644 index 000000000..fadb4711e --- /dev/null +++ b/views/admin/restaurants.hbs @@ -0,0 +1 @@ +
admin restaurants
\ No newline at end of file From 0fe53cc2c9a31bf8809003fc343a44cb87cd3d0f Mon Sep 17 00:00:00 2001 From: sdfghjka Date: Sun, 2 Feb 2025 16:10:33 +0800 Subject: [PATCH 26/61] add user model --- migrations/20250202080350-create-user.js | 33 ++++++++++++++++++++++++ models/user.js | 27 +++++++++++++++++++ 2 files changed, 60 insertions(+) create mode 100644 migrations/20250202080350-create-user.js create mode 100644 models/user.js diff --git a/migrations/20250202080350-create-user.js b/migrations/20250202080350-create-user.js new file mode 100644 index 000000000..f7b6013c3 --- /dev/null +++ b/migrations/20250202080350-create-user.js @@ -0,0 +1,33 @@ +'use strict'; +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.createTable('Users', { + id: { + allowNull: false, + autoIncrement: true, + primaryKey: true, + type: Sequelize.INTEGER + }, + name: { + type: Sequelize.STRING + }, + email: { + type: Sequelize.STRING + }, + password: { + type: Sequelize.STRING + }, + created_at: { + allowNull: false, + type: Sequelize.DATE + }, + updated_at: { + allowNull: false, + type: Sequelize.DATE + } + }); + }, + down: async (queryInterface, Sequelize) => { + await queryInterface.dropTable('Users'); + } +}; \ No newline at end of file diff --git a/models/user.js b/models/user.js new file mode 100644 index 000000000..03c3ca8be --- /dev/null +++ b/models/user.js @@ -0,0 +1,27 @@ +'use strict'; +const { + Model +} = require('sequelize'); +module.exports = (sequelize, DataTypes) => { + class User extends Model { + /** + * Helper method for defining associations. + * This method is not a part of Sequelize lifecycle. + * The `models/index` file will call this method automatically. + */ + static associate(models) { + // define association here + } + }; + User.init({ + name: DataTypes.STRING, + email: DataTypes.STRING, + password: DataTypes.STRING + }, { + sequelize, + modelName: 'User', + tableName: 'Users', + underscored: true, + }); + return User; +}; \ No newline at end of file From a0bd58d9f6e554044549bde007e07ad19994c785 Mon Sep 17 00:00:00 2001 From: sdfghjka Date: Mon, 3 Feb 2025 13:36:49 +0800 Subject: [PATCH 27/61] user signup --- app.js | 1 + controllers/user-controller.js | 27 +++++++++++++++++++++++++++ package-lock.json | 5 +++++ package.json | 1 + routes/index.js | 5 ++++- views/signup.hbs | 26 ++++++++++++++++++++++++++ 6 files changed, 64 insertions(+), 1 deletion(-) create mode 100644 controllers/user-controller.js create mode 100644 views/signup.hbs diff --git a/app.js b/app.js index 41092c536..80331d78a 100644 --- a/app.js +++ b/app.js @@ -4,6 +4,7 @@ const handlebars = require("express-handlebars"); const app = express(); const port = process.env.PORT || 3000; +app.use(express.urlencoded({ extended: true })) app.use(routes); app.engine("hbs", handlebars({ extname: ".hbs" })); app.set("view engine", "hbs"); diff --git a/controllers/user-controller.js b/controllers/user-controller.js new file mode 100644 index 000000000..1000295ff --- /dev/null +++ b/controllers/user-controller.js @@ -0,0 +1,27 @@ +const bcrypt = require('bcryptjs'); +const db = require("../models"); +const User = db.User; + +const userController = { + signUpPage: (req, res) => { + res.render("signup"); + }, + signUp: (req, res) => { + bcrypt + .hash(req.body.password, 10) + .then((hash) => + User.create({ + name: req.body.name, + email: req.body.email, + password: hash, + }) + ) + .then(() => { + res.redirect("/signin"); + }) + .catch((err)=>{ + console.log(err); + }) + }, +}; +module.exports = userController; diff --git a/package-lock.json b/package-lock.json index 6ec7f8a87..d764ee666 100644 --- a/package-lock.json +++ b/package-lock.json @@ -400,6 +400,11 @@ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" }, + "bcryptjs": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz", + "integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==" + }, "binary-extensions": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.1.0.tgz", diff --git a/package.json b/package.json index 5be3eca9b..bb636e865 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "url": "git+https://github.com/ALPHACamp/forum-express-grading.git" }, "dependencies": { + "bcryptjs": "^2.4.3", "express": "^4.17.1", "express-handlebars": "^5.3.3", "mysql2": "^2.3.0", diff --git a/routes/index.js b/routes/index.js index 218aff1bc..62e0497cb 100644 --- a/routes/index.js +++ b/routes/index.js @@ -2,9 +2,12 @@ const express = require("express"); const router = express.Router(); const restController = require("../controllers/restaurant-controller"); const adminController = require("./modules/admin"); +const userController = require("../controllers/user-controller"); router.get("/restaurants", restController.getRestaurants); -router.use("/admin",adminController); +router.use("/admin", adminController); +router.get("/signup", userController.signUpPage); +router.post("/signup", userController.signUp); router.use("/", (req, res) => res.redirect("/restaurants")); module.exports = router; diff --git a/views/signup.hbs b/views/signup.hbs new file mode 100644 index 000000000..c7a13851a --- /dev/null +++ b/views/signup.hbs @@ -0,0 +1,26 @@ +
+

Sign Up

+
+ +
+

Sign In

+
\ No newline at end of file From 0a3e70d672b46dd8ebd3a79ff5b589c7717d5377 Mon Sep 17 00:00:00 2001 From: sdfghjka Date: Mon, 3 Feb 2025 14:43:23 +0800 Subject: [PATCH 28/61] flash msg & signup verification --- app.js | 13 ++++++++ controllers/user-controller.js | 26 +++++++++------- middleware/error-handler.js | 11 +++++++ package-lock.json | 55 ++++++++++++++++++++++++++++++++++ package.json | 2 ++ routes/index.js | 3 +- views/layouts/main.hbs | 1 + views/partials/messages.hbs | 18 +++++++++++ 8 files changed, 118 insertions(+), 11 deletions(-) create mode 100644 middleware/error-handler.js create mode 100644 views/partials/messages.hbs diff --git a/app.js b/app.js index 80331d78a..d7678caca 100644 --- a/app.js +++ b/app.js @@ -3,8 +3,21 @@ const routes = require("./routes"); const handlebars = require("express-handlebars"); const app = express(); const port = process.env.PORT || 3000; +const flash = require('connect-flash'); +const session = require('express-session'); +const SESSION_SECRET = 'secret' + +app.use(session({ secret: SESSION_SECRET, resave: false, saveUninitialized: false })) +app.use(flash()) app.use(express.urlencoded({ extended: true })) + +app.use((req, res, next) => { + res.locals.success_messages = req.flash('success_messages') // 設定 success_msg 訊息 + res.locals.error_messages = req.flash('error_messages') // 設定 warning_msg 訊息 + next() +}) + app.use(routes); app.engine("hbs", handlebars({ extname: ".hbs" })); app.set("view engine", "hbs"); diff --git a/controllers/user-controller.js b/controllers/user-controller.js index 1000295ff..3af85f7b5 100644 --- a/controllers/user-controller.js +++ b/controllers/user-controller.js @@ -1,27 +1,33 @@ -const bcrypt = require('bcryptjs'); +const bcrypt = require("bcryptjs"); const db = require("../models"); +const { where } = require("sequelize"); const User = db.User; const userController = { signUpPage: (req, res) => { res.render("signup"); }, - signUp: (req, res) => { - bcrypt - .hash(req.body.password, 10) - .then((hash) => + signUp: (req, res, next) => { + if (req.body.password !== req.body.passwordCheck) + throw new Error("Passwords do not match!"); + + User.findOne({ where: { email: req.body.email } }) + .then((user) => { + if (user) throw new Error("Email already exists!"); + return bcrypt.hash(req.body.password, 10); + }) + .then((hash) => { User.create({ name: req.body.name, email: req.body.email, password: hash, - }) - ) + }); + }) .then(() => { + req.flash("success_messages", "成功註冊帳號!"); res.redirect("/signin"); }) - .catch((err)=>{ - console.log(err); - }) + .catch((err) => next(err)); }, }; module.exports = userController; diff --git a/middleware/error-handler.js b/middleware/error-handler.js new file mode 100644 index 000000000..991aa2476 --- /dev/null +++ b/middleware/error-handler.js @@ -0,0 +1,11 @@ +module.exports = { + generalErrorHandler(err, req, res, next) { + if (err instanceof Error) { + req.flash("error_messages", `${err.name}: ${err.message}`); + } else { + req.flash("error_messages", `${err}`); + } + res.redirect("back"); + next(err); + }, +}; diff --git a/package-lock.json b/package-lock.json index d764ee666..39ef77ca3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -790,6 +790,11 @@ "xdg-basedir": "^4.0.0" } }, + "connect-flash": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/connect-flash/-/connect-flash-0.1.1.tgz", + "integrity": "sha512-2rcfELQt/ZMP+SM/pG8PyhJRaLKp+6Hk2IUBNkEit09X+vwn3QsAL3ZbYtxUn7NVPzbMTSLRDhqe0B/eh30RYA==" + }, "content-disposition": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", @@ -1615,6 +1620,38 @@ } } }, + "express-session": { + "version": "1.17.2", + "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.17.2.tgz", + "integrity": "sha512-mPcYcLA0lvh7D4Oqr5aNJFMtBMKPLl++OKKxkHzZ0U0oDq1rpKBnkR5f5vCHR26VeArlTOEF9td4x5IjICksRQ==", + "requires": { + "cookie": "0.4.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-headers": "~1.0.2", + "parseurl": "~1.3.3", + "safe-buffer": "5.2.1", + "uid-safe": "~2.1.5" + }, + "dependencies": { + "cookie": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==" + }, + "depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + } + } + }, "ext": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/ext/-/ext-1.4.0.tgz", @@ -3067,6 +3104,11 @@ "ee-first": "1.1.1" } }, + "on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==" + }, "once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -3331,6 +3373,11 @@ "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==" }, + "random-bytes": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", + "integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==" + }, "randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -4062,6 +4109,14 @@ "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", "optional": true }, + "uid-safe": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz", + "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==", + "requires": { + "random-bytes": "~1.0.0" + } + }, "umzug": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/umzug/-/umzug-2.3.0.tgz", diff --git a/package.json b/package.json index bb636e865..8d78ebb6e 100644 --- a/package.json +++ b/package.json @@ -14,8 +14,10 @@ }, "dependencies": { "bcryptjs": "^2.4.3", + "connect-flash": "^0.1.1", "express": "^4.17.1", "express-handlebars": "^5.3.3", + "express-session": "^1.17.2", "mysql2": "^2.3.0", "sequelize": "^6.6.5", "sequelize-cli": "^6.2.0" diff --git a/routes/index.js b/routes/index.js index 62e0497cb..609209fe7 100644 --- a/routes/index.js +++ b/routes/index.js @@ -3,11 +3,12 @@ const router = express.Router(); const restController = require("../controllers/restaurant-controller"); const adminController = require("./modules/admin"); const userController = require("../controllers/user-controller"); +const { generalErrorHandler } = require("../middleware/error-handler"); router.get("/restaurants", restController.getRestaurants); router.use("/admin", adminController); router.get("/signup", userController.signUpPage); router.post("/signup", userController.signUp); router.use("/", (req, res) => res.redirect("/restaurants")); - +router.use("/", generalErrorHandler); module.exports = router; diff --git a/views/layouts/main.hbs b/views/layouts/main.hbs index b00d09b88..13f994646 100644 --- a/views/layouts/main.hbs +++ b/views/layouts/main.hbs @@ -11,6 +11,7 @@
+ {{> messages}} {{{body}}}
diff --git a/views/partials/messages.hbs b/views/partials/messages.hbs new file mode 100644 index 000000000..fd311f3e2 --- /dev/null +++ b/views/partials/messages.hbs @@ -0,0 +1,18 @@ +{{#if error_messages}} + +{{/if}} +{{#if success_messages}} + +{{/if}} +{{#if warning_msg}} + +{{/if}} \ No newline at end of file From 7089131da5ae064a22c399f12348a1c7664afa41 Mon Sep 17 00:00:00 2001 From: sdfghjka Date: Mon, 3 Feb 2025 15:30:29 +0800 Subject: [PATCH 29/61] passport init & signin --- app.js | 26 +++++++++++-------- config/passport.js | 47 ++++++++++++++++++++++++++++++++++ controllers/user-controller.js | 12 +++++++++ package-lock.json | 27 +++++++++++++++++++ package.json | 2 ++ routes/index.js | 12 +++++++++ views/signin.hbs | 19 ++++++++++++++ 7 files changed, 135 insertions(+), 10 deletions(-) create mode 100644 config/passport.js create mode 100644 views/signin.hbs diff --git a/app.js b/app.js index d7678caca..715080bf5 100644 --- a/app.js +++ b/app.js @@ -3,20 +3,26 @@ const routes = require("./routes"); const handlebars = require("express-handlebars"); const app = express(); const port = process.env.PORT || 3000; -const flash = require('connect-flash'); -const session = require('express-session'); -const SESSION_SECRET = 'secret' +const flash = require("connect-flash"); +const session = require("express-session"); +const SESSION_SECRET = "secret"; +const passport = require('passport'); +app.use( + session({ secret: SESSION_SECRET, resave: false, saveUninitialized: false }) +); -app.use(session({ secret: SESSION_SECRET, resave: false, saveUninitialized: false })) -app.use(flash()) -app.use(express.urlencoded({ extended: true })) +app.use(passport.initialize()); +app.use(passport.session()); + +app.use(flash()); +app.use(express.urlencoded({ extended: true })); app.use((req, res, next) => { - res.locals.success_messages = req.flash('success_messages') // 設定 success_msg 訊息 - res.locals.error_messages = req.flash('error_messages') // 設定 warning_msg 訊息 - next() -}) + res.locals.success_messages = req.flash("success_messages"); // 設定 success_msg 訊息 + res.locals.error_messages = req.flash("error_messages"); // 設定 warning_msg 訊息 + next(); +}); app.use(routes); app.engine("hbs", handlebars({ extname: ".hbs" })); diff --git a/config/passport.js b/config/passport.js new file mode 100644 index 000000000..0d073959f --- /dev/null +++ b/config/passport.js @@ -0,0 +1,47 @@ +const passport = require("passport"); +const LocalStrategy = require("passport-local"); +const bcrypt = require("bcryptjs"); +const db = require("../models"); +const User = db.User; +// set up Passport strategy +passport.use( + new LocalStrategy( + // customize user field + { + usernameField: "email", + passwordField: "password", + passReqToCallback: true, + }, + // authenticate user + (req, email, password, cb) => { + User.findOne({ where: { email } }).then((user) => { + if (!user) + return cb( + null, + false, + req.flash("error_messages", "帳號或密碼輸入錯誤!") + ); + bcrypt.compare(password, user.password).then((res) => { + if (!res) + return cb( + null, + false, + req.flash("error_messages", "帳號或密碼輸入錯誤!") + ); + return cb(null, user); + }); + }); + } + ) +); +// serialize and deserialize user +passport.serializeUser((user, cb) => { + cb(null, user.id); +}); +passport.deserializeUser((id, cb) => { + User.findByPk(id).then((user) => { + user = user.toJSON(); + return cb(null, user); + }); +}); +module.exports = passport; diff --git a/controllers/user-controller.js b/controllers/user-controller.js index 3af85f7b5..a3c0c5046 100644 --- a/controllers/user-controller.js +++ b/controllers/user-controller.js @@ -29,5 +29,17 @@ const userController = { }) .catch((err) => next(err)); }, + signInPage: (req, res) => { + res.render('signin') + }, + signIn: (req, res) => { + req.flash('success_messages', '成功登入!') + res.redirect('/restaurants') + }, + logout: (req, res) => { + req.flash('success_messages', '登出成功!') + req.logout() + res.redirect('/signin') + } }; module.exports = userController; diff --git a/package-lock.json b/package-lock.json index 39ef77ca3..6122bd4bf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3192,6 +3192,28 @@ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" }, + "passport": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/passport/-/passport-0.4.1.tgz", + "integrity": "sha512-IxXgZZs8d7uFSt3eqNjM9NQ3g3uQCW5avD8mRNoXV99Yig50vjuaez6dQK2qC0kVWPRTujxY0dWgGfT09adjYg==", + "requires": { + "passport-strategy": "1.x.x", + "pause": "0.0.1" + } + }, + "passport-local": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/passport-local/-/passport-local-1.0.0.tgz", + "integrity": "sha512-9wCE6qKznvf9mQYYbgJ3sVOHmCWoUNMVFoZzNoznmISbhnNNPhN9xfY3sLmScHMetEJeoY7CXwfhCe7argfQow==", + "requires": { + "passport-strategy": "1.x.x" + } + }, + "passport-strategy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz", + "integrity": "sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==" + }, "path-exists": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", @@ -3224,6 +3246,11 @@ "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", "dev": true }, + "pause": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", + "integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==" + }, "pg-connection-string": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.5.0.tgz", diff --git a/package.json b/package.json index 8d78ebb6e..7cd09eb57 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,8 @@ "express-handlebars": "^5.3.3", "express-session": "^1.17.2", "mysql2": "^2.3.0", + "passport": "^0.4.1", + "passport-local": "^1.0.0", "sequelize": "^6.6.5", "sequelize-cli": "^6.2.0" }, diff --git a/routes/index.js b/routes/index.js index 609209fe7..3bcb928c8 100644 --- a/routes/index.js +++ b/routes/index.js @@ -4,11 +4,23 @@ const restController = require("../controllers/restaurant-controller"); const adminController = require("./modules/admin"); const userController = require("../controllers/user-controller"); const { generalErrorHandler } = require("../middleware/error-handler"); +const passport = require('../config/passport'); router.get("/restaurants", restController.getRestaurants); router.use("/admin", adminController); + router.get("/signup", userController.signUpPage); router.post("/signup", userController.signUp); + +router.get("/signin", userController.signInPage); +router.post( + "/signin", + passport.authenticate("local", { + failureRedirect: "/signin", + failureFlash: true, + }), + userController.signIn +); router.use("/", (req, res) => res.redirect("/restaurants")); router.use("/", generalErrorHandler); module.exports = router; diff --git a/views/signin.hbs b/views/signin.hbs new file mode 100644 index 000000000..68c557e1d --- /dev/null +++ b/views/signin.hbs @@ -0,0 +1,19 @@ +
+

Sign In

+
+ +
+

Sign Up

+
\ No newline at end of file From d86618d605bd601d8820df4e766d977644e37e22 Mon Sep 17 00:00:00 2001 From: sdfghjka Date: Mon, 3 Feb 2025 16:03:50 +0800 Subject: [PATCH 30/61] add header & footer --- app.js | 7 +++++-- helpers/auth-helper.js | 6 ++++++ helpers/handlebars-helpers.js | 4 ++++ package-lock.json | 5 +++++ package.json | 1 + views/layouts/main.hbs | 14 ++++++++------ views/partials/footer.hbs | 5 +++++ views/partials/header.hbs | 28 ++++++++++++++++++++++++++++ 8 files changed, 62 insertions(+), 8 deletions(-) create mode 100644 helpers/auth-helper.js create mode 100644 helpers/handlebars-helpers.js create mode 100644 views/partials/footer.hbs create mode 100644 views/partials/header.hbs diff --git a/app.js b/app.js index 715080bf5..a016a0687 100644 --- a/app.js +++ b/app.js @@ -6,7 +6,9 @@ const port = process.env.PORT || 3000; const flash = require("connect-flash"); const session = require("express-session"); const SESSION_SECRET = "secret"; -const passport = require('passport'); +const passport = require("passport"); +const { getUser } = require("./helpers/auth-helper"); +const handlebarsHelpers = require("./helpers/handlebars-helpers"); app.use( session({ secret: SESSION_SECRET, resave: false, saveUninitialized: false }) @@ -21,11 +23,12 @@ app.use(express.urlencoded({ extended: true })); app.use((req, res, next) => { res.locals.success_messages = req.flash("success_messages"); // 設定 success_msg 訊息 res.locals.error_messages = req.flash("error_messages"); // 設定 warning_msg 訊息 + res.locals.user = getUser(req); next(); }); app.use(routes); -app.engine("hbs", handlebars({ extname: ".hbs" })); +app.engine("hbs", handlebars({ extname: ".hbs", helpers: handlebarsHelpers })); app.set("view engine", "hbs"); app.listen(port, () => { diff --git a/helpers/auth-helper.js b/helpers/auth-helper.js new file mode 100644 index 000000000..a74485d42 --- /dev/null +++ b/helpers/auth-helper.js @@ -0,0 +1,6 @@ +const getUser = (req) => { + return req.user || null; +}; +module.exports = { + getUser, +}; diff --git a/helpers/handlebars-helpers.js b/helpers/handlebars-helpers.js new file mode 100644 index 000000000..e32c26049 --- /dev/null +++ b/helpers/handlebars-helpers.js @@ -0,0 +1,4 @@ +const dayjs = require('dayjs') //載入 dayjs 套件 +module.exports = { + currentYear: () => dayjs().year() //取得當年年份作為 currentYear 的屬性值,並導出 +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 6122bd4bf..66ed6e4f5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -850,6 +850,11 @@ "type": "^1.0.1" } }, + "dayjs": { + "version": "1.10.6", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.10.6.tgz", + "integrity": "sha512-AztC/IOW4L1Q41A86phW5Thhcrco3xuAA+YX/BLpLWWjRcTj5TOt/QImBLmCKlrF7u7k47arTnOyL6GnbG8Hvw==" + }, "debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", diff --git a/package.json b/package.json index 7cd09eb57..7603c02d9 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "dependencies": { "bcryptjs": "^2.4.3", "connect-flash": "^0.1.1", + "dayjs": "^1.10.6", "express": "^4.17.1", "express-handlebars": "^5.3.3", "express-session": "^1.17.2", diff --git a/views/layouts/main.hbs b/views/layouts/main.hbs index 13f994646..9571b74ec 100644 --- a/views/layouts/main.hbs +++ b/views/layouts/main.hbs @@ -1,5 +1,5 @@ - + @@ -7,18 +7,20 @@ 餐廳評論網 - -
-
-
+ + {{> header}} +
+
+
{{> messages}} {{{body}}}
+ {{> footer}} + - \ No newline at end of file diff --git a/views/partials/footer.hbs b/views/partials/footer.hbs new file mode 100644 index 000000000..e028efea4 --- /dev/null +++ b/views/partials/footer.hbs @@ -0,0 +1,5 @@ +
+
+ © 2017-{{ currentYear }} +
+
\ No newline at end of file diff --git a/views/partials/header.hbs b/views/partials/header.hbs new file mode 100644 index 000000000..edf70ca4c --- /dev/null +++ b/views/partials/header.hbs @@ -0,0 +1,28 @@ +
+ +
\ No newline at end of file From 668b47429f6fa711d69cb4746b01ae3c4600e7dd Mon Sep 17 00:00:00 2001 From: sdfghjka Date: Mon, 3 Feb 2025 23:13:51 +0800 Subject: [PATCH 31/61] user model add is_admin and add restaurants model --- app.js | 2 +- helpers/auth-helper.js | 6 --- helpers/auth-helpers.js | 10 +++++ middleware/auth.js | 21 ++++++++++ .../20250203140245-add-is-admin-to-users.js | 13 +++++++ .../20250203150937-create-restaurant.js | 39 +++++++++++++++++++ models/restaurant.js | 29 ++++++++++++++ models/user.js | 3 +- routes/index.js | 6 ++- routes/modules/admin.js | 3 +- 10 files changed, 121 insertions(+), 11 deletions(-) delete mode 100644 helpers/auth-helper.js create mode 100644 helpers/auth-helpers.js create mode 100644 middleware/auth.js create mode 100644 migrations/20250203140245-add-is-admin-to-users.js create mode 100644 migrations/20250203150937-create-restaurant.js create mode 100644 models/restaurant.js diff --git a/app.js b/app.js index a016a0687..50b0db46c 100644 --- a/app.js +++ b/app.js @@ -7,7 +7,7 @@ const flash = require("connect-flash"); const session = require("express-session"); const SESSION_SECRET = "secret"; const passport = require("passport"); -const { getUser } = require("./helpers/auth-helper"); +const { getUser } = require("./helpers/auth-helpers"); const handlebarsHelpers = require("./helpers/handlebars-helpers"); app.use( diff --git a/helpers/auth-helper.js b/helpers/auth-helper.js deleted file mode 100644 index a74485d42..000000000 --- a/helpers/auth-helper.js +++ /dev/null @@ -1,6 +0,0 @@ -const getUser = (req) => { - return req.user || null; -}; -module.exports = { - getUser, -}; diff --git a/helpers/auth-helpers.js b/helpers/auth-helpers.js new file mode 100644 index 000000000..3ca682bc5 --- /dev/null +++ b/helpers/auth-helpers.js @@ -0,0 +1,10 @@ +const getUser = (req) => { + return req.user || null; +}; +const ensureAuthenticated = req => { + return req.isAuthenticated() +} +module.exports = { + getUser, + ensureAuthenticated +}; diff --git a/middleware/auth.js b/middleware/auth.js new file mode 100644 index 000000000..5c89a7fb0 --- /dev/null +++ b/middleware/auth.js @@ -0,0 +1,21 @@ +const { ensureAuthenticated, getUser } = require('../helpers/auth-helpers') +const authenticated = (req, res, next) => { + // if (req.isAuthenticated) + if (ensureAuthenticated(req)) { + return next() + } + res.redirect('/signin') +} +const authenticatedAdmin = (req, res, next) => { + // if (req.isAuthenticated) + if (ensureAuthenticated(req)) { + if (getUser(req).isAdmin) return next() + res.redirect('/') + } else { + res.redirect('/signin') + } +} +module.exports = { + authenticated, + authenticatedAdmin +} \ No newline at end of file diff --git a/migrations/20250203140245-add-is-admin-to-users.js b/migrations/20250203140245-add-is-admin-to-users.js new file mode 100644 index 000000000..bb0d2e4e5 --- /dev/null +++ b/migrations/20250203140245-add-is-admin-to-users.js @@ -0,0 +1,13 @@ +'use strict'; + +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.addColumn('Users', 'is_admin', { + type: Sequelize.BOOLEAN, + defaultValue: false + }) + }, + down: async (queryInterface, Sequelize) => { + await queryInterface.removeColumn('Users', 'is_admin') + } +} diff --git a/migrations/20250203150937-create-restaurant.js b/migrations/20250203150937-create-restaurant.js new file mode 100644 index 000000000..c4b16f51e --- /dev/null +++ b/migrations/20250203150937-create-restaurant.js @@ -0,0 +1,39 @@ +"use strict"; +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.createTable("Restaurants", { + id: { + allowNull: false, + autoIncrement: true, + primaryKey: true, + type: Sequelize.INTEGER, + }, + name: { + type: Sequelize.STRING, + }, + tel: { + type: Sequelize.STRING, + }, + address: { + type: Sequelize.STRING, + }, + opening_hours: { + type: Sequelize.STRING, + }, + description: { + type: Sequelize.TEXT, + }, + created_at: { + allowNull: false, + type: Sequelize.DATE, + }, + updated_at: { + allowNull: false, + type: Sequelize.DATE, + }, + }); + }, + down: async (queryInterface, Sequelize) => { + await queryInterface.dropTable("Restaurants"); + }, +}; diff --git a/models/restaurant.js b/models/restaurant.js new file mode 100644 index 000000000..bc83476f0 --- /dev/null +++ b/models/restaurant.js @@ -0,0 +1,29 @@ +'use strict'; +const { + Model +} = require('sequelize'); +module.exports = (sequelize, DataTypes) => { + class Restaurant extends Model { + /** + * Helper method for defining associations. + * This method is not a part of Sequelize lifecycle. + * The `models/index` file will call this method automatically. + */ + static associate(models) { + // define association here + } + }; + Restaurant.init({ + name: DataTypes.STRING, + tel: DataTypes.STRING, + address: DataTypes.STRING, + openingHours: DataTypes.STRING, + description: DataTypes.TEXT + }, { + sequelize, + modelName: 'Restaurant', + tableName: 'Restaurants', + underscored: true, + }); + return Restaurant; +}; \ No newline at end of file diff --git a/models/user.js b/models/user.js index 03c3ca8be..7a0e6e56e 100644 --- a/models/user.js +++ b/models/user.js @@ -16,7 +16,8 @@ module.exports = (sequelize, DataTypes) => { User.init({ name: DataTypes.STRING, email: DataTypes.STRING, - password: DataTypes.STRING + password: DataTypes.STRING, + isAdmin: DataTypes.BOOLEAN }, { sequelize, modelName: 'User', diff --git a/routes/index.js b/routes/index.js index 3bcb928c8..dbc076f37 100644 --- a/routes/index.js +++ b/routes/index.js @@ -4,9 +4,10 @@ const restController = require("../controllers/restaurant-controller"); const adminController = require("./modules/admin"); const userController = require("../controllers/user-controller"); const { generalErrorHandler } = require("../middleware/error-handler"); -const passport = require('../config/passport'); +const { authenticated } = require("../middleware/auth"); +const passport = require("../config/passport"); -router.get("/restaurants", restController.getRestaurants); +router.get("/restaurants", authenticated, restController.getRestaurants); router.use("/admin", adminController); router.get("/signup", userController.signUpPage); @@ -21,6 +22,7 @@ router.post( }), userController.signIn ); +router.get("/logout", userController.logout); router.use("/", (req, res) => res.redirect("/restaurants")); router.use("/", generalErrorHandler); module.exports = router; diff --git a/routes/modules/admin.js b/routes/modules/admin.js index 578718c06..23726d283 100644 --- a/routes/modules/admin.js +++ b/routes/modules/admin.js @@ -1,7 +1,8 @@ const express = require("express"); const router = express.Router(); const adminController = require("../../controllers/admin-controller"); +const { authenticatedAdmin } = require("../../middleware/auth"); -router.get("/restaurants", adminController.getRestaurants); +router.get("/restaurants", authenticatedAdmin, adminController.getRestaurants); router.use("/", (req, res) => res.redirect("/admin/restaurants")); module.exports = router; From 92c80dfe06864aa929d0af3abaa999c602a3d202 Mon Sep 17 00:00:00 2001 From: sdfghjka Date: Mon, 3 Feb 2025 23:33:03 +0800 Subject: [PATCH 32/61] modify admin restaurants page --- controllers/admin-controller.js | 13 +++++++++-- views/admin/restaurants.hbs | 39 ++++++++++++++++++++++++++++++++- 2 files changed, 49 insertions(+), 3 deletions(-) diff --git a/controllers/admin-controller.js b/controllers/admin-controller.js index b72c550ce..e5e628f67 100644 --- a/controllers/admin-controller.js +++ b/controllers/admin-controller.js @@ -1,6 +1,15 @@ +const { Restaurant } = require("../models"); + const adminController = { - getRestaurants: (req, res) => { - return res.render("admin/restaurants"); + getRestaurants: (req, res, next) => { + Restaurant.findAll({ + raw: true, + }) + + .then((restaurants) => res.render("admin/restaurants", { restaurants })) + + .catch((err) => next(err)); }, }; + module.exports = adminController; diff --git a/views/admin/restaurants.hbs b/views/admin/restaurants.hbs index fadb4711e..e509584e8 100644 --- a/views/admin/restaurants.hbs +++ b/views/admin/restaurants.hbs @@ -1 +1,38 @@ -
admin restaurants
\ No newline at end of file +

餐廳後台

+ + + +
+
+ + + + + + + + + + {{#each restaurants}} + + + + + + {{/each}} + +
#Name
{{this.id}}{{this.name}} + + +
+ +
+
\ No newline at end of file From dff2f5a9de5f499fe96f8aeba40ff6dd83c71113 Mon Sep 17 00:00:00 2001 From: sdfghjka Date: Tue, 4 Feb 2025 15:10:28 +0800 Subject: [PATCH 33/61] create restaurant --- controllers/admin-controller.js | 19 +++++++++++++++++++ routes/index.js | 5 +++-- routes/modules/admin.js | 5 +++-- views/admin/create-restaurant.hbs | 26 ++++++++++++++++++++++++++ 4 files changed, 51 insertions(+), 4 deletions(-) create mode 100644 views/admin/create-restaurant.hbs diff --git a/controllers/admin-controller.js b/controllers/admin-controller.js index e5e628f67..c16a9cbf6 100644 --- a/controllers/admin-controller.js +++ b/controllers/admin-controller.js @@ -10,6 +10,25 @@ const adminController = { .catch((err) => next(err)); }, + createRestaurant: (req, res) => { + return res.render("admin/create-restaurant"); + }, + postRestaurant: (req, res, next) => { + const { name, tel, address, openingHours, description } = req.body; + if (!name) throw new Error("Restaurant name is required!"); + Restaurant.create({ + name, + tel, + address, + openingHours, + description, + }) + .then(() => { + req.flash("success_messages", "restaurant was successfully created"); + res.redirect("/admin/restaurants"); + }) + .catch((err) => next(err)); + }, }; module.exports = adminController; diff --git a/routes/index.js b/routes/index.js index dbc076f37..1bddfefec 100644 --- a/routes/index.js +++ b/routes/index.js @@ -1,14 +1,15 @@ const express = require("express"); const router = express.Router(); const restController = require("../controllers/restaurant-controller"); +const admin = require("../routes/modules/admin"); const adminController = require("./modules/admin"); const userController = require("../controllers/user-controller"); const { generalErrorHandler } = require("../middleware/error-handler"); -const { authenticated } = require("../middleware/auth"); +const { authenticated, authenticatedAdmin } = require("../middleware/auth"); const passport = require("../config/passport"); router.get("/restaurants", authenticated, restController.getRestaurants); -router.use("/admin", adminController); +router.use("/admin", authenticatedAdmin, admin); router.get("/signup", userController.signUpPage); router.post("/signup", userController.signUp); diff --git a/routes/modules/admin.js b/routes/modules/admin.js index 23726d283..f5f913c21 100644 --- a/routes/modules/admin.js +++ b/routes/modules/admin.js @@ -1,8 +1,9 @@ const express = require("express"); const router = express.Router(); const adminController = require("../../controllers/admin-controller"); -const { authenticatedAdmin } = require("../../middleware/auth"); -router.get("/restaurants", authenticatedAdmin, adminController.getRestaurants); +router.get("/restaurants", adminController.getRestaurants); +router.get("/restaurants/create", adminController.createRestaurant); +router.post("/restaurants", adminController.postRestaurant); router.use("/", (req, res) => res.redirect("/admin/restaurants")); module.exports = router; diff --git a/views/admin/create-restaurant.hbs b/views/admin/create-restaurant.hbs new file mode 100644 index 000000000..c2f41fd39 --- /dev/null +++ b/views/admin/create-restaurant.hbs @@ -0,0 +1,26 @@ +
+
+

Create Restaurant

+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
\ No newline at end of file From 858ab44a313f0758990fd0d1c6586d083daf4fc0 Mon Sep 17 00:00:00 2001 From: sdfghjka Date: Tue, 4 Feb 2025 15:32:38 +0800 Subject: [PATCH 34/61] admin restaurant page --- controllers/admin-controller.js | 10 ++++++++++ routes/index.js | 1 - routes/modules/admin.js | 1 + views/admin/restaurant.hbs | 19 +++++++++++++++++++ 4 files changed, 30 insertions(+), 1 deletion(-) create mode 100644 views/admin/restaurant.hbs diff --git a/controllers/admin-controller.js b/controllers/admin-controller.js index c16a9cbf6..03e44ec89 100644 --- a/controllers/admin-controller.js +++ b/controllers/admin-controller.js @@ -29,6 +29,16 @@ const adminController = { }) .catch((err) => next(err)); }, + getRestaurant: (req, res, next) => { + Restaurant.findByPk(req.params.id, { + raw: true, + }) + .then((restaurant) => { + if (!restaurant) throw new Error("Restaurant didn't exist!"); + res.render("admin/restaurant", { restaurant }); + }) + .catch((err) => next(err)); + }, }; module.exports = adminController; diff --git a/routes/index.js b/routes/index.js index 1bddfefec..bf35ee694 100644 --- a/routes/index.js +++ b/routes/index.js @@ -2,7 +2,6 @@ const express = require("express"); const router = express.Router(); const restController = require("../controllers/restaurant-controller"); const admin = require("../routes/modules/admin"); -const adminController = require("./modules/admin"); const userController = require("../controllers/user-controller"); const { generalErrorHandler } = require("../middleware/error-handler"); const { authenticated, authenticatedAdmin } = require("../middleware/auth"); diff --git a/routes/modules/admin.js b/routes/modules/admin.js index f5f913c21..fdb4146a8 100644 --- a/routes/modules/admin.js +++ b/routes/modules/admin.js @@ -4,6 +4,7 @@ const adminController = require("../../controllers/admin-controller"); router.get("/restaurants", adminController.getRestaurants); router.get("/restaurants/create", adminController.createRestaurant); +router.get('/restaurants/:id', adminController.getRestaurant); router.post("/restaurants", adminController.postRestaurant); router.use("/", (req, res) => res.redirect("/admin/restaurants")); module.exports = router; diff --git a/views/admin/restaurant.hbs b/views/admin/restaurant.hbs new file mode 100644 index 000000000..2745da835 --- /dev/null +++ b/views/admin/restaurant.hbs @@ -0,0 +1,19 @@ +
+
+

{{restaurant.name}}

+
+
+
+
    +
  • Opening Hour: {{restaurant.openingHours}}
  • +
  • Tel: {{restaurant.tel}}
  • +
  • Address: {{restaurant.address}}
  • +
+
+
+
+

{{restaurant.description}}

+
+
+
+Back \ No newline at end of file From 35fa19963220c8fc2f12a509976202db0217d407 Mon Sep 17 00:00:00 2001 From: sdfghjka Date: Tue, 4 Feb 2025 16:06:44 +0800 Subject: [PATCH 35/61] admin update restaurant --- app.js | 3 ++- controllers/admin-controller.js | 30 ++++++++++++++++++++++++ package-lock.json | 21 +++++++++++++++++ package.json | 1 + routes/modules/admin.js | 4 +++- views/admin/create-restaurant.hbs | 22 +---------------- views/admin/edit-restaurant.hbs | 6 +++++ views/partials/admin-restaurant-form.hbs | 24 +++++++++++++++++++ 8 files changed, 88 insertions(+), 23 deletions(-) create mode 100644 views/admin/edit-restaurant.hbs create mode 100644 views/partials/admin-restaurant-form.hbs diff --git a/app.js b/app.js index 50b0db46c..162fa5c40 100644 --- a/app.js +++ b/app.js @@ -9,6 +9,7 @@ const SESSION_SECRET = "secret"; const passport = require("passport"); const { getUser } = require("./helpers/auth-helpers"); const handlebarsHelpers = require("./helpers/handlebars-helpers"); +const methodOverride = require("method-override"); app.use( session({ secret: SESSION_SECRET, resave: false, saveUninitialized: false }) @@ -16,9 +17,9 @@ app.use( app.use(passport.initialize()); app.use(passport.session()); - app.use(flash()); app.use(express.urlencoded({ extended: true })); +app.use(methodOverride("_method")); app.use((req, res, next) => { res.locals.success_messages = req.flash("success_messages"); // 設定 success_msg 訊息 diff --git a/controllers/admin-controller.js b/controllers/admin-controller.js index 03e44ec89..6bbedb6b5 100644 --- a/controllers/admin-controller.js +++ b/controllers/admin-controller.js @@ -39,6 +39,36 @@ const adminController = { }) .catch((err) => next(err)); }, + editRestaurant: (req, res, next) => { + Restaurant.findByPk(req.params.id, { + raw: true, + }) + .then((restaurant) => { + if (!restaurant) throw new Error("Restaurant didn't exist!"); + res.render("admin/edit-restaurant", { restaurant }); + }) + .catch((err) => next(err)); + }, + putRestaurant: (req, res, next) => { + const { name, tel, address, openingHours, description } = req.body; + if (!name) throw new Error("Restaurant name is required!"); + Restaurant.findByPk(req.params.id) + .then((restaurant) => { + if (!restaurant) throw new Error("Restaurant didn't exist!"); + return restaurant.update({ + name, + tel, + address, + openingHours, + description, + }); + }) + .then(() => { + req.flash("success_messages", "restaurant was successfully to update"); + res.redirect("/admin/restaurants"); + }) + .catch((err) => next(err)); + }, }; module.exports = adminController; diff --git a/package-lock.json b/package-lock.json index 66ed6e4f5..fd8cdc9de 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2575,6 +2575,27 @@ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" }, + "method-override": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/method-override/-/method-override-3.0.0.tgz", + "integrity": "sha512-IJ2NNN/mSl9w3kzWB92rcdHpz+HjkxhDJWNDBqSlas+zQdP8wBiJzITPg08M/k2uVvMow7Sk41atndNtt/PHSA==", + "requires": { + "debug": "3.1.0", + "methods": "~1.1.2", + "parseurl": "~1.3.2", + "vary": "~1.1.2" + }, + "dependencies": { + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "requires": { + "ms": "2.0.0" + } + } + } + }, "methods": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", diff --git a/package.json b/package.json index 7603c02d9..a89387f83 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "express": "^4.17.1", "express-handlebars": "^5.3.3", "express-session": "^1.17.2", + "method-override": "^3.0.0", "mysql2": "^2.3.0", "passport": "^0.4.1", "passport-local": "^1.0.0", diff --git a/routes/modules/admin.js b/routes/modules/admin.js index fdb4146a8..9e26d11a8 100644 --- a/routes/modules/admin.js +++ b/routes/modules/admin.js @@ -4,7 +4,9 @@ const adminController = require("../../controllers/admin-controller"); router.get("/restaurants", adminController.getRestaurants); router.get("/restaurants/create", adminController.createRestaurant); -router.get('/restaurants/:id', adminController.getRestaurant); +router.get("/restaurants/:id/edit", adminController.editRestaurant); +router.get("/restaurants/:id", adminController.getRestaurant); +router.put("/restaurants/:id", adminController.putRestaurant); router.post("/restaurants", adminController.postRestaurant); router.use("/", (req, res) => res.redirect("/admin/restaurants")); module.exports = router; diff --git a/views/admin/create-restaurant.hbs b/views/admin/create-restaurant.hbs index c2f41fd39..2afd79b4a 100644 --- a/views/admin/create-restaurant.hbs +++ b/views/admin/create-restaurant.hbs @@ -2,25 +2,5 @@

Create Restaurant

-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
- + {{> admin-restaurant-form}} \ No newline at end of file diff --git a/views/admin/edit-restaurant.hbs b/views/admin/edit-restaurant.hbs new file mode 100644 index 000000000..a36b77283 --- /dev/null +++ b/views/admin/edit-restaurant.hbs @@ -0,0 +1,6 @@ +
+
+

Edit Restaurant

+
+ {{> admin-restaurant-form}} +
\ No newline at end of file diff --git a/views/partials/admin-restaurant-form.hbs b/views/partials/admin-restaurant-form.hbs new file mode 100644 index 000000000..13cc60b75 --- /dev/null +++ b/views/partials/admin-restaurant-form.hbs @@ -0,0 +1,24 @@ +
+ + +
+
+ + // 新增 value +
+
+ + // 新增 value +
+
+ + // 新增 value +
+
+ + +
+ \ No newline at end of file From eb12036cc1e01190c015cae9f3c69374f08d7e27 Mon Sep 17 00:00:00 2001 From: sdfghjka Date: Tue, 4 Feb 2025 21:37:39 +0800 Subject: [PATCH 36/61] delete restaurant --- controllers/admin-controller.js | 9 +++++++++ routes/modules/admin.js | 1 + views/admin/restaurants.hbs | 2 +- 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/controllers/admin-controller.js b/controllers/admin-controller.js index 6bbedb6b5..5db88655e 100644 --- a/controllers/admin-controller.js +++ b/controllers/admin-controller.js @@ -69,6 +69,15 @@ const adminController = { }) .catch((err) => next(err)); }, + deleteRestaurant: (req, res, next) => { + return Restaurant.findByPk(req.params.id) + .then(restaurant => { + if (!restaurant) throw new Error("Restaurant didn't exist!") + return restaurant.destroy() + }) + .then(() => res.redirect('/admin/restaurants')) + .catch(err => next(err)) + } }; module.exports = adminController; diff --git a/routes/modules/admin.js b/routes/modules/admin.js index 9e26d11a8..b70256c4e 100644 --- a/routes/modules/admin.js +++ b/routes/modules/admin.js @@ -7,6 +7,7 @@ router.get("/restaurants/create", adminController.createRestaurant); router.get("/restaurants/:id/edit", adminController.editRestaurant); router.get("/restaurants/:id", adminController.getRestaurant); router.put("/restaurants/:id", adminController.putRestaurant); +router.delete("/restaurants/:id", adminController.deleteRestaurant); router.post("/restaurants", adminController.postRestaurant); router.use("/", (req, res) => res.redirect("/admin/restaurants")); module.exports = router; diff --git a/views/admin/restaurants.hbs b/views/admin/restaurants.hbs index e509584e8..2069f9a31 100644 --- a/views/admin/restaurants.hbs +++ b/views/admin/restaurants.hbs @@ -29,7 +29,7 @@
- +
From cc897d9035ea3906415da2e7d4e91017f04569ab Mon Sep 17 00:00:00 2001 From: sdfghjka Date: Wed, 5 Feb 2025 14:17:19 +0800 Subject: [PATCH 37/61] add restaurant image --- app.js | 3 + controllers/admin-controller.js | 46 ++++-- helpers/file-helpers.js | 15 ++ middleware/multer.js | 3 + ...20250204135727-add-image-to-restaurants.js | 11 ++ models/restaurant.js | 38 ++--- package-lock.json | 152 +++++++++++++++++- package.json | 1 + routes/modules/admin.js | 13 +- views/admin/create-restaurant.hbs | 2 +- views/admin/edit-restaurant.hbs | 2 +- views/admin/restaurant.hbs | 15 +- views/partials/admin-restaurant-form.hbs | 7 +- 13 files changed, 267 insertions(+), 41 deletions(-) create mode 100644 helpers/file-helpers.js create mode 100644 middleware/multer.js create mode 100644 migrations/20250204135727-add-image-to-restaurants.js diff --git a/app.js b/app.js index 162fa5c40..28e6dd14f 100644 --- a/app.js +++ b/app.js @@ -10,6 +10,8 @@ const passport = require("passport"); const { getUser } = require("./helpers/auth-helpers"); const handlebarsHelpers = require("./helpers/handlebars-helpers"); const methodOverride = require("method-override"); +const path = require("path"); + app.use( session({ secret: SESSION_SECRET, resave: false, saveUninitialized: false }) @@ -20,6 +22,7 @@ app.use(passport.session()); app.use(flash()); app.use(express.urlencoded({ extended: true })); app.use(methodOverride("_method")); +app.use("/upload", express.static(path.join(__dirname, "upload"))); app.use((req, res, next) => { res.locals.success_messages = req.flash("success_messages"); // 設定 success_msg 訊息 diff --git a/controllers/admin-controller.js b/controllers/admin-controller.js index 5db88655e..0cc06728c 100644 --- a/controllers/admin-controller.js +++ b/controllers/admin-controller.js @@ -1,4 +1,5 @@ const { Restaurant } = require("../models"); +const { localFileHandler } = require("../helpers/file-helpers"); const adminController = { getRestaurants: (req, res, next) => { @@ -16,13 +17,20 @@ const adminController = { postRestaurant: (req, res, next) => { const { name, tel, address, openingHours, description } = req.body; if (!name) throw new Error("Restaurant name is required!"); - Restaurant.create({ - name, - tel, - address, - openingHours, - description, - }) + //修改以下 + const { file } = req; // 把檔案取出來,也可以寫成 const file = req.file + localFileHandler(file) // 把取出的檔案傳給 file-helper 處理後 + .then((filePath) => + Restaurant.create({ + // 再 create 這筆餐廳資料 + name, + tel, + address, + openingHours, + description, + image: filePath || null, + }) + ) .then(() => { req.flash("success_messages", "restaurant was successfully created"); res.redirect("/admin/restaurants"); @@ -52,15 +60,23 @@ const adminController = { putRestaurant: (req, res, next) => { const { name, tel, address, openingHours, description } = req.body; if (!name) throw new Error("Restaurant name is required!"); - Restaurant.findByPk(req.params.id) - .then((restaurant) => { + const { file } = req; // 把檔案取出來 + Promise.all([ + // 非同步處理 + Restaurant.findByPk(req.params.id), // 去資料庫查有沒有這間餐廳 + localFileHandler(file), // 把檔案傳到 file-helper 處理 + ]) + .then(([restaurant, filePath]) => { + // 以上兩樣事都做完以後 if (!restaurant) throw new Error("Restaurant didn't exist!"); return restaurant.update({ + // 修改這筆資料 name, tel, address, openingHours, description, + image: filePath || restaurant.image, // 如果 filePath 是 Truthy (使用者有上傳新照片) 就用 filePath,是 Falsy (使用者沒有上傳新照片) 就沿用原本資料庫內的值 }); }) .then(() => { @@ -71,13 +87,13 @@ const adminController = { }, deleteRestaurant: (req, res, next) => { return Restaurant.findByPk(req.params.id) - .then(restaurant => { - if (!restaurant) throw new Error("Restaurant didn't exist!") - return restaurant.destroy() + .then((restaurant) => { + if (!restaurant) throw new Error("Restaurant didn't exist!"); + return restaurant.destroy(); }) - .then(() => res.redirect('/admin/restaurants')) - .catch(err => next(err)) - } + .then(() => res.redirect("/admin/restaurants")) + .catch((err) => next(err)); + }, }; module.exports = adminController; diff --git a/helpers/file-helpers.js b/helpers/file-helpers.js new file mode 100644 index 000000000..0ce240612 --- /dev/null +++ b/helpers/file-helpers.js @@ -0,0 +1,15 @@ +const fs = require("fs"); +const localFileHandler = (file) => { + return new Promise((resolve, reject) => { + if (!file) return resolve(null); + const fileName = `upload/${file.originalname}`; + return fs.promises + .readFile(file.path) + .then((data) => fs.promises.writeFile(fileName, data)) + .then(() => resolve(`/${fileName}`)) + .catch((err) => reject(err)); + }); +}; +module.exports = { + localFileHandler, +}; diff --git a/middleware/multer.js b/middleware/multer.js new file mode 100644 index 000000000..540133617 --- /dev/null +++ b/middleware/multer.js @@ -0,0 +1,3 @@ +const multer = require("multer"); +const upload = multer({ dest: "temp/" }); +module.exports = upload; diff --git a/migrations/20250204135727-add-image-to-restaurants.js b/migrations/20250204135727-add-image-to-restaurants.js new file mode 100644 index 000000000..4ad9d1977 --- /dev/null +++ b/migrations/20250204135727-add-image-to-restaurants.js @@ -0,0 +1,11 @@ +"use strict"; +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.addColumn("Restaurants", "image", { + type: Sequelize.STRING, + }); + }, + down: async (queryInterface, Sequelize) => { + await queryInterface.removeColumn("Restaurants", "image"); + }, +}; diff --git a/models/restaurant.js b/models/restaurant.js index bc83476f0..213da8694 100644 --- a/models/restaurant.js +++ b/models/restaurant.js @@ -1,7 +1,5 @@ -'use strict'; -const { - Model -} = require('sequelize'); +"use strict"; +const { Model } = require("sequelize"); module.exports = (sequelize, DataTypes) => { class Restaurant extends Model { /** @@ -12,18 +10,22 @@ module.exports = (sequelize, DataTypes) => { static associate(models) { // define association here } - }; - Restaurant.init({ - name: DataTypes.STRING, - tel: DataTypes.STRING, - address: DataTypes.STRING, - openingHours: DataTypes.STRING, - description: DataTypes.TEXT - }, { - sequelize, - modelName: 'Restaurant', - tableName: 'Restaurants', - underscored: true, - }); + } + Restaurant.init( + { + name: DataTypes.STRING, + tel: DataTypes.STRING, + address: DataTypes.STRING, + openingHours: DataTypes.STRING, + description: DataTypes.TEXT, + image: DataTypes.STRING, + }, + { + sequelize, + modelName: "Restaurant", + tableName: "Restaurants", + underscored: true, + } + ); return Restaurant; -}; \ No newline at end of file +}; diff --git a/package-lock.json b/package-lock.json index fd8cdc9de..5e58f9c71 100644 --- a/package-lock.json +++ b/package-lock.json @@ -333,6 +333,11 @@ "picomatch": "^2.0.4" } }, + "append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==" + }, "argparse": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", @@ -554,6 +559,20 @@ "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", "dev": true }, + "buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" + }, + "busboy": { + "version": "0.2.14", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-0.2.14.tgz", + "integrity": "sha512-InWFDomvlkEj+xWLBfU3AvnbVYqeTWmQopiW0tWWEy5yehYm2YkGEc59sUmw/4ty5Zj/b0WHGs1LgecuBSBGrg==", + "requires": { + "dicer": "0.2.5", + "readable-stream": "1.1.x" + } + }, "bytes": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", @@ -767,6 +786,46 @@ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" }, + "concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "requires": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" + }, + "readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, "config-chain": { "version": "1.1.12", "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.12.tgz", @@ -824,6 +883,11 @@ "integrity": "sha512-JxbCBUdrfr6AQjOXrxoTvAMJO4HBTUIlBzslcJPAz+/KT8yk53fXun51u+RenNYvad/+Vc2DIz5o9UxlCDymFQ==", "dev": true }, + "core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" + }, "cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -944,6 +1008,15 @@ "wrappy": "1" } }, + "dicer": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/dicer/-/dicer-0.2.5.tgz", + "integrity": "sha512-FDvbtnq7dzlPz0wyYlOExifDEZcu8h+rErEXgfxqmLfRfC/kJidEFh4+effJRO3P0xmfqyPbSMG0LveNRfTKVg==", + "requires": { + "readable-stream": "1.1.x", + "streamsearch": "0.1.2" + } + }, "diff": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", @@ -2349,8 +2422,7 @@ "isarray": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==", - "dev": true + "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==" }, "isexe": { "version": "2.0.0", @@ -2911,6 +2983,36 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" }, + "multer": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.3.tgz", + "integrity": "sha512-np0YLKncuZoTzufbkM6wEKp68EhWJXcU6fq6QqrSwkckd2LlMgd1UqhUJLj6NS/5sZ8dE8LYDWslsltJznnXlg==", + "requires": { + "append-field": "^1.0.0", + "busboy": "^0.2.11", + "concat-stream": "^1.5.2", + "mkdirp": "^0.5.4", + "object-assign": "^4.1.1", + "on-finished": "^2.3.0", + "type-is": "^1.6.4", + "xtend": "^4.0.0" + }, + "dependencies": { + "minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==" + }, + "mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "requires": { + "minimist": "^1.2.6" + } + } + } + }, "mysql2": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-2.3.3.tgz", @@ -3087,6 +3189,11 @@ "integrity": "sha512-9UZCFRHQdNrfTpGg8+1INIg93B6zE0aXMVFkw1WFwvO4SlZywU6aLg5Of0Ap/PgcbSw4LNxvMWXMeugwMCX0AA==", "dev": true }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==" + }, "object-inspect": { "version": "1.12.0", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.0.tgz", @@ -3354,6 +3461,11 @@ "integrity": "sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc=", "dev": true }, + "process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + }, "progress": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", @@ -3476,6 +3588,17 @@ } } }, + "readable-stream": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "integrity": "sha512-+MeVjFf4L44XUkhM1eYbD8fyEsxcV81pqMSR5gblfcLCHfZvbrqy4/qYHE+/R5HoBUT11WV5O08Cr1n3YXkWVQ==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, "readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -3843,6 +3966,11 @@ "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" }, + "streamsearch": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-0.1.2.tgz", + "integrity": "sha512-jos8u++JKm0ARcSUTAZXOVC0mSox7Bhn6sBgty73P1f3JGf7yG2clTbBNHUdde/kdvP2FESam+vM6l8jBrNxHA==" + }, "string-width": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", @@ -3873,6 +4001,11 @@ "define-properties": "^1.1.3" } }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==" + }, "strip-ansi": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", @@ -4147,6 +4280,11 @@ "mime-types": "~2.1.24" } }, + "typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==" + }, "typedarray-to-buffer": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", @@ -4255,6 +4393,11 @@ "prepend-http": "^2.0.0" } }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, "utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", @@ -4415,6 +4558,11 @@ "integrity": "sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==", "dev": true }, + "xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==" + }, "y18n": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", diff --git a/package.json b/package.json index a89387f83..06c6c2a6b 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "express-handlebars": "^5.3.3", "express-session": "^1.17.2", "method-override": "^3.0.0", + "multer": "^1.4.3", "mysql2": "^2.3.0", "passport": "^0.4.1", "passport-local": "^1.0.0", diff --git a/routes/modules/admin.js b/routes/modules/admin.js index b70256c4e..45095a1dd 100644 --- a/routes/modules/admin.js +++ b/routes/modules/admin.js @@ -1,13 +1,22 @@ const express = require("express"); const router = express.Router(); const adminController = require("../../controllers/admin-controller"); +const upload = require('../../middleware/multer'); router.get("/restaurants", adminController.getRestaurants); router.get("/restaurants/create", adminController.createRestaurant); router.get("/restaurants/:id/edit", adminController.editRestaurant); router.get("/restaurants/:id", adminController.getRestaurant); -router.put("/restaurants/:id", adminController.putRestaurant); +router.put( + "/restaurants/:id", + upload.single("image"), + adminController.putRestaurant +); router.delete("/restaurants/:id", adminController.deleteRestaurant); -router.post("/restaurants", adminController.postRestaurant); +router.post( + "/restaurants", + upload.single("image"), + adminController.postRestaurant +); router.use("/", (req, res) => res.redirect("/admin/restaurants")); module.exports = router; diff --git a/views/admin/create-restaurant.hbs b/views/admin/create-restaurant.hbs index 2afd79b4a..0ade7e4a0 100644 --- a/views/admin/create-restaurant.hbs +++ b/views/admin/create-restaurant.hbs @@ -1,4 +1,4 @@ -
+

Create Restaurant

diff --git a/views/admin/edit-restaurant.hbs b/views/admin/edit-restaurant.hbs index a36b77283..4e9d40e45 100644 --- a/views/admin/edit-restaurant.hbs +++ b/views/admin/edit-restaurant.hbs @@ -1,4 +1,4 @@ - +

Edit Restaurant

diff --git a/views/admin/restaurant.hbs b/views/admin/restaurant.hbs index 2745da835..08717a355 100644 --- a/views/admin/restaurant.hbs +++ b/views/admin/restaurant.hbs @@ -3,6 +3,19 @@

{{restaurant.name}}

+ +
+

影片播放

+ +
  • Opening Hour: {{restaurant.openingHours}}
  • @@ -14,6 +27,6 @@

    {{restaurant.description}}

    -
    +
Back \ No newline at end of file diff --git a/views/partials/admin-restaurant-form.hbs b/views/partials/admin-restaurant-form.hbs index 13cc60b75..056f3136c 100644 --- a/views/partials/admin-restaurant-form.hbs +++ b/views/partials/admin-restaurant-form.hbs @@ -21,4 +21,9 @@
- \ No newline at end of file +
+ + +
+ +Back \ No newline at end of file From a78a47d12c9ef6584ae4d864fb6892ed5bba2719 Mon Sep 17 00:00:00 2001 From: sdfghjka Date: Wed, 5 Feb 2025 14:44:12 +0800 Subject: [PATCH 38/61] add seed data --- package-lock.json | 5 +++ package.json | 1 + seeders/20250205062840-users-seed-file.js | 31 +++++++++++++++++++ .../20250205063852-restaurants-seed-file.js | 24 ++++++++++++++ 4 files changed, 61 insertions(+) create mode 100644 seeders/20250205062840-users-seed-file.js create mode 100644 seeders/20250205063852-restaurants-seed-file.js diff --git a/package-lock.json b/package-lock.json index 5e58f9c71..b956edb4d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1745,6 +1745,11 @@ } } }, + "faker": { + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/faker/-/faker-5.5.3.tgz", + "integrity": "sha512-wLTv2a28wjUyWkbnX7u/ABZBkUkIF2fCd73V6P2oFqEGEktDfzWx4UxrSqtPRw0xPRAcjeAOIiJWqZm3pP4u3g==" + }, "fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", diff --git a/package.json b/package.json index 06c6c2a6b..7f6a7a830 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "express": "^4.17.1", "express-handlebars": "^5.3.3", "express-session": "^1.17.2", + "faker": "^5.5.3", "method-override": "^3.0.0", "multer": "^1.4.3", "mysql2": "^2.3.0", diff --git a/seeders/20250205062840-users-seed-file.js b/seeders/20250205062840-users-seed-file.js new file mode 100644 index 000000000..5bfad1d0d --- /dev/null +++ b/seeders/20250205062840-users-seed-file.js @@ -0,0 +1,31 @@ +'use strict' +const bcrypt = require('bcryptjs') +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.bulkInsert('Users', [{ // 一次新增三筆資料 + email: 'root@example.com', + password: await bcrypt.hash('12345678', 10), + is_admin: true, + name: 'root', + created_at: new Date(), + updated_at: new Date() + }, { + email: 'user1@example.com', + password: await bcrypt.hash('12345678', 10), + is_admin: false, + name: 'user1', + created_at: new Date(), + updated_at: new Date() + }, { + email: 'user2@example.com', + password: await bcrypt.hash('12345678', 10), + is_admin: false, + name: 'user2', + created_at: new Date(), + updated_at: new Date() + }], {}) + }, + down: async (queryInterface, Sequelize) => { // 清空資料表中所有資料 + await queryInterface.bulkDelete('Users', {}) + } +} diff --git a/seeders/20250205063852-restaurants-seed-file.js b/seeders/20250205063852-restaurants-seed-file.js new file mode 100644 index 000000000..4d58d026a --- /dev/null +++ b/seeders/20250205063852-restaurants-seed-file.js @@ -0,0 +1,24 @@ +"use strict"; +const faker = require("faker"); +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.bulkInsert( + "Restaurants", + Array.from({ length: 50 }, () => ({ + name: faker.name.findName(), + tel: faker.phone.phoneNumber(), + address: faker.address.streetAddress(), + opening_hours: "08:00", + image: `https://loremflickr.com/320/240/nude/?random=${ + Math.random() * 100 + }`, + description: faker.lorem.text(), + created_at: new Date(), + updated_at: new Date(), + })) + ); + }, + down: async (queryInterface, Sequelize) => { + await queryInterface.bulkDelete("Restaurants", {}); + }, +}; From d67dffa5309fc3fe10bb75428e96058da3a6dc6c Mon Sep 17 00:00:00 2001 From: sdfghjka Date: Wed, 5 Feb 2025 15:50:10 +0800 Subject: [PATCH 39/61] adjust middleware/auth.js --- middleware/auth.js | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/middleware/auth.js b/middleware/auth.js index 5c89a7fb0..8f0c4848d 100644 --- a/middleware/auth.js +++ b/middleware/auth.js @@ -1,21 +1,21 @@ -const { ensureAuthenticated, getUser } = require('../helpers/auth-helpers') +const helpers = require("../helpers/auth-helpers"); const authenticated = (req, res, next) => { // if (req.isAuthenticated) if (ensureAuthenticated(req)) { - return next() + return next(); } - res.redirect('/signin') -} + res.redirect("/signin"); +}; const authenticatedAdmin = (req, res, next) => { // if (req.isAuthenticated) - if (ensureAuthenticated(req)) { - if (getUser(req).isAdmin) return next() - res.redirect('/') + if (helpers.ensureAuthenticated(req)) { + if (helpers.getUser(req).isAdmin) return next(); + res.redirect("/"); } else { - res.redirect('/signin') + res.redirect("/signin"); } -} +}; module.exports = { authenticated, - authenticatedAdmin -} \ No newline at end of file + authenticatedAdmin, +}; From 47da6dbe177055707782ddfb0aa2f94805731929 Mon Sep 17 00:00:00 2001 From: sdfghjka Date: Thu, 6 Feb 2025 21:49:19 +0800 Subject: [PATCH 40/61] add user manage page --- app.js | 1 + controllers/admin-controller.js | 33 +++++++++++++++++++++++- middleware/auth.js | 2 +- routes/modules/admin.js | 5 +++- views/admin/users.hbs | 37 +++++++++++++++++++++++++++ views/partials/admin-table-switch.hbs | 9 +++++++ 6 files changed, 84 insertions(+), 3 deletions(-) create mode 100644 views/admin/users.hbs create mode 100644 views/partials/admin-table-switch.hbs diff --git a/app.js b/app.js index 28e6dd14f..a05425a00 100644 --- a/app.js +++ b/app.js @@ -21,6 +21,7 @@ app.use(passport.initialize()); app.use(passport.session()); app.use(flash()); app.use(express.urlencoded({ extended: true })); +app.use(express.json()); app.use(methodOverride("_method")); app.use("/upload", express.static(path.join(__dirname, "upload"))); diff --git a/controllers/admin-controller.js b/controllers/admin-controller.js index 0cc06728c..f70b8a115 100644 --- a/controllers/admin-controller.js +++ b/controllers/admin-controller.js @@ -1,5 +1,7 @@ -const { Restaurant } = require("../models"); +const { Restaurant, User } = require("../models"); const { localFileHandler } = require("../helpers/file-helpers"); +const { raw } = require("express"); +const { where } = require("sequelize"); const adminController = { getRestaurants: (req, res, next) => { @@ -94,6 +96,35 @@ const adminController = { .then(() => res.redirect("/admin/restaurants")) .catch((err) => next(err)); }, + getUsers: (req, res, next) => { + return User.findAll({ + raw: true, + }) + .then((users) => { + console.log(users) + res.render("admin/users", { users }); + }) + .catch((err) => next(err)); + }, + patchUser: (req, res, next) => { + const id = req.params.id; + return User.findByPk(id) + .then((user) => { + if (!user) throw new Error("User didn't exist!"); + if (user.email === "root@example.com") { + req.flash("error_messages", "禁止變更 root 權限"); + return res.redirect("back"); + } + return user.update({ + isAdmin: !user.isAdmin, + }); + }) + .then(() => { + req.flash("success_messages", "使用者權限變更成功"); + return res.redirect("/admin/users"); + }) + .catch((err) => next(err)); + }, }; module.exports = adminController; diff --git a/middleware/auth.js b/middleware/auth.js index 8f0c4848d..5530fe8a5 100644 --- a/middleware/auth.js +++ b/middleware/auth.js @@ -1,7 +1,7 @@ const helpers = require("../helpers/auth-helpers"); const authenticated = (req, res, next) => { // if (req.isAuthenticated) - if (ensureAuthenticated(req)) { + if (helpers.ensureAuthenticated(req)) { return next(); } res.redirect("/signin"); diff --git a/routes/modules/admin.js b/routes/modules/admin.js index 45095a1dd..47d656b87 100644 --- a/routes/modules/admin.js +++ b/routes/modules/admin.js @@ -1,7 +1,8 @@ const express = require("express"); const router = express.Router(); const adminController = require("../../controllers/admin-controller"); -const upload = require('../../middleware/multer'); +const upload = require("../../middleware/multer"); +const { ro } = require("faker/lib/locales"); router.get("/restaurants", adminController.getRestaurants); router.get("/restaurants/create", adminController.createRestaurant); @@ -18,5 +19,7 @@ router.post( upload.single("image"), adminController.postRestaurant ); +router.get("/users", adminController.getUsers); +router.patch("/users/:id", adminController.patchUser); router.use("/", (req, res) => res.redirect("/admin/restaurants")); module.exports = router; diff --git a/views/admin/users.hbs b/views/admin/users.hbs new file mode 100644 index 000000000..c666f7854 --- /dev/null +++ b/views/admin/users.hbs @@ -0,0 +1,37 @@ +{{> admin-table-switch}} + + + + + + + + + + + + {{#each users}} + + + + + + + + {{/each}} + +
#NameEmailRole#
{{this.id}}{{this.name}}{{this.email}}{{#if this.isAdmin}}admin{{else}}user{{/if}} + + + +
\ No newline at end of file diff --git a/views/partials/admin-table-switch.hbs b/views/partials/admin-table-switch.hbs new file mode 100644 index 000000000..e48be2a95 --- /dev/null +++ b/views/partials/admin-table-switch.hbs @@ -0,0 +1,9 @@ +

餐廳後台

+ \ No newline at end of file From db6676f3900e396c163f3be66bb96e70af3ced79 Mon Sep 17 00:00:00 2001 From: sdfghjka Date: Thu, 6 Feb 2025 22:49:56 +0800 Subject: [PATCH 41/61] add category model --- migrations/20250206141946-create-category.js | 27 +++++++++++++++++++ ...206143452-add-category-id-to-restaurant.js | 16 +++++++++++ models/category.js | 26 ++++++++++++++++++ models/restaurant.js | 2 +- 4 files changed, 70 insertions(+), 1 deletion(-) create mode 100644 migrations/20250206141946-create-category.js create mode 100644 migrations/20250206143452-add-category-id-to-restaurant.js create mode 100644 models/category.js diff --git a/migrations/20250206141946-create-category.js b/migrations/20250206141946-create-category.js new file mode 100644 index 000000000..ddf7e4bfc --- /dev/null +++ b/migrations/20250206141946-create-category.js @@ -0,0 +1,27 @@ +'use strict'; +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.createTable('Categories', { + id: { + allowNull: false, + autoIncrement: true, + primaryKey: true, + type: Sequelize.INTEGER + }, + name: { + type: Sequelize.STRING + }, + created_at: { + allowNull: false, + type: Sequelize.DATE + }, + updated_at: { + allowNull: false, + type: Sequelize.DATE + } + }); + }, + down: async (queryInterface, Sequelize) => { + await queryInterface.dropTable('Categories'); + } +}; \ No newline at end of file diff --git a/migrations/20250206143452-add-category-id-to-restaurant.js b/migrations/20250206143452-add-category-id-to-restaurant.js new file mode 100644 index 000000000..7c30ec2f5 --- /dev/null +++ b/migrations/20250206143452-add-category-id-to-restaurant.js @@ -0,0 +1,16 @@ +"use strict"; +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.addColumn("Restaurants", "category_id", { + type: Sequelize.INTEGER, + allowNull: false, + references: { + model: "Categories", + key: "id", + }, + }); + }, + down: async (queryInterface, Sequelize) => { + await queryInterface.removeColumn("Restaurants", "category_id"); + }, +}; diff --git a/models/category.js b/models/category.js new file mode 100644 index 000000000..5a3c9d49a --- /dev/null +++ b/models/category.js @@ -0,0 +1,26 @@ +"use strict"; +const { Model } = require("sequelize"); +module.exports = (sequelize, DataTypes) => { + class Category extends Model { + /** + * Helper method for defining associations. + * This method is not a part of Sequelize lifecycle. + * The `models/index` file will call this method automatically. + */ + static associate(models) { + Category.hasMany(models.Restaurant, { foreignKey: "categoryId" }); + } + } + Category.init( + { + name: DataTypes.STRING, + }, + { + sequelize, + modelName: "Category", + tableName: 'Categories', + underscored: true, + } + ); + return Category; +}; diff --git a/models/restaurant.js b/models/restaurant.js index 213da8694..bee72a563 100644 --- a/models/restaurant.js +++ b/models/restaurant.js @@ -8,7 +8,7 @@ module.exports = (sequelize, DataTypes) => { * The `models/index` file will call this method automatically. */ static associate(models) { - // define association here + Restaurant.belongsTo(models.Category, { foreignKey: "categoryId" }); } } Restaurant.init( From 07c783c554fd08c1f9015fee201f90eb594378db Mon Sep 17 00:00:00 2001 From: sdfghjka Date: Thu, 6 Feb 2025 23:24:13 +0800 Subject: [PATCH 42/61] update seed files --- .../20250206151601-categories-seed-file.js | 27 +++++++++++++++++++ ... 20250206152038-restaurants-seed-file2.js} | 6 +++++ 2 files changed, 33 insertions(+) create mode 100644 seeders/20250206151601-categories-seed-file.js rename seeders/{20250205063852-restaurants-seed-file.js => 20250206152038-restaurants-seed-file2.js} (73%) diff --git a/seeders/20250206151601-categories-seed-file.js b/seeders/20250206151601-categories-seed-file.js new file mode 100644 index 000000000..7560fb896 --- /dev/null +++ b/seeders/20250206151601-categories-seed-file.js @@ -0,0 +1,27 @@ +"use strict"; +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.bulkInsert( + "Categories", + [ + "中式料理", + "日本料理", + "義大利料理", + "墨西哥料理", + "素食料理", + "美式料理", + "複合式料理", + ].map((item) => { + return { + name: item, + created_at: new Date(), + updated_at: new Date(), + }; + }), + {} + ); + }, + down: async (queryInterface, Sequelize) => { + await queryInterface.bulkDelete("Categories", {}); + }, +}; diff --git a/seeders/20250205063852-restaurants-seed-file.js b/seeders/20250206152038-restaurants-seed-file2.js similarity index 73% rename from seeders/20250205063852-restaurants-seed-file.js rename to seeders/20250206152038-restaurants-seed-file2.js index 4d58d026a..e1d898f14 100644 --- a/seeders/20250205063852-restaurants-seed-file.js +++ b/seeders/20250206152038-restaurants-seed-file2.js @@ -2,6 +2,10 @@ const faker = require("faker"); module.exports = { up: async (queryInterface, Sequelize) => { + const categories = await queryInterface.sequelize.query( + "SELECT id FROM Categories;", + { type: queryInterface.sequelize.QueryTypes.SELECT } + ); await queryInterface.bulkInsert( "Restaurants", Array.from({ length: 50 }, () => ({ @@ -15,6 +19,8 @@ module.exports = { description: faker.lorem.text(), created_at: new Date(), updated_at: new Date(), + category_id: + categories[Math.floor(Math.random() * categories.length)].id, })) ); }, From 8f485dff3bf24abfaedc9dc98e391df196425e0c Mon Sep 17 00:00:00 2001 From: sdfghjka Date: Fri, 7 Feb 2025 16:12:52 +0800 Subject: [PATCH 43/61] add categories selector on admin create and edit page --- controllers/admin-controller.js | 37 +++++++++++++++++------- views/admin/restaurant.hbs | 1 + views/admin/restaurants.hbs | 2 ++ views/partials/admin-restaurant-form.hbs | 8 +++++ 4 files changed, 37 insertions(+), 11 deletions(-) diff --git a/controllers/admin-controller.js b/controllers/admin-controller.js index 0cc06728c..103da1eff 100644 --- a/controllers/admin-controller.js +++ b/controllers/admin-controller.js @@ -1,21 +1,30 @@ -const { Restaurant } = require("../models"); +const { Restaurant, Category } = require("../models"); const { localFileHandler } = require("../helpers/file-helpers"); const adminController = { getRestaurants: (req, res, next) => { Restaurant.findAll({ raw: true, + nest: true, + include: [Category] }) .then((restaurants) => res.render("admin/restaurants", { restaurants })) .catch((err) => next(err)); }, - createRestaurant: (req, res) => { - return res.render("admin/create-restaurant"); + createRestaurant: (req, res, next) => { + return Category.findAll({ + raw: true, + }) + .then((categories) => + res.render("admin/create-restaurant", { categories }) + ) + .catch((err) => next(err)); }, postRestaurant: (req, res, next) => { - const { name, tel, address, openingHours, description } = req.body; + const { name, tel, address, openingHours, description, categoryId } = + req.body; if (!name) throw new Error("Restaurant name is required!"); //修改以下 const { file } = req; // 把檔案取出來,也可以寫成 const file = req.file @@ -29,6 +38,7 @@ const adminController = { openingHours, description, image: filePath || null, + categoryId, }) ) .then(() => { @@ -40,6 +50,8 @@ const adminController = { getRestaurant: (req, res, next) => { Restaurant.findByPk(req.params.id, { raw: true, + nest: true, + include: [Category] }) .then((restaurant) => { if (!restaurant) throw new Error("Restaurant didn't exist!"); @@ -48,17 +60,19 @@ const adminController = { .catch((err) => next(err)); }, editRestaurant: (req, res, next) => { - Restaurant.findByPk(req.params.id, { - raw: true, - }) - .then((restaurant) => { - if (!restaurant) throw new Error("Restaurant didn't exist!"); - res.render("admin/edit-restaurant", { restaurant }); + return Promise.all([ + Restaurant.findByPk(req.params.id, { raw: true }), + Category.findAll({ raw: true }), + ]) + .then(([restaurant, categories]) => { + if (!restaurant) throw new Error("Restaurant doesn't exist!"); + res.render("admin/edit-restaurant", { restaurant, categories }); }) .catch((err) => next(err)); }, putRestaurant: (req, res, next) => { - const { name, tel, address, openingHours, description } = req.body; + const { name, tel, address, openingHours, description, categoryId } = + req.body; if (!name) throw new Error("Restaurant name is required!"); const { file } = req; // 把檔案取出來 Promise.all([ @@ -77,6 +91,7 @@ const adminController = { openingHours, description, image: filePath || restaurant.image, // 如果 filePath 是 Truthy (使用者有上傳新照片) 就用 filePath,是 Falsy (使用者沒有上傳新照片) 就沿用原本資料庫內的值 + categoryId, }); }) .then(() => { diff --git a/views/admin/restaurant.hbs b/views/admin/restaurant.hbs index 08717a355..685ad5be9 100644 --- a/views/admin/restaurant.hbs +++ b/views/admin/restaurant.hbs @@ -1,6 +1,7 @@

{{restaurant.name}}

+

{{restaurant.Category.name}}

+
+ + +
// 新增 value From fde8e0ff13c4012db97c7e49cf6680e24db9e4f4 Mon Sep 17 00:00:00 2001 From: sdfghjka Date: Fri, 7 Feb 2025 21:54:08 +0800 Subject: [PATCH 44/61] add ifCond hbs helper & update category selector --- helpers/handlebars-helpers.js | 9 ++++++--- views/partials/admin-restaurant-form.hbs | 12 ++++++------ 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/helpers/handlebars-helpers.js b/helpers/handlebars-helpers.js index e32c26049..6488122e2 100644 --- a/helpers/handlebars-helpers.js +++ b/helpers/handlebars-helpers.js @@ -1,4 +1,7 @@ -const dayjs = require('dayjs') //載入 dayjs 套件 +const dayjs = require("dayjs"); //載入 dayjs 套件 module.exports = { - currentYear: () => dayjs().year() //取得當年年份作為 currentYear 的屬性值,並導出 -} \ No newline at end of file + currentYear: () => dayjs().year(), //取得當年年份作為 currentYear 的屬性值,並導出 + ifCond: function (a, b, options) { + return a === b ? options.fn(this) : options.inverse(this); + }, +}; diff --git a/views/partials/admin-restaurant-form.hbs b/views/partials/admin-restaurant-form.hbs index 2729e9ca4..12f82ecc3 100644 --- a/views/partials/admin-restaurant-form.hbs +++ b/views/partials/admin-restaurant-form.hbs @@ -1,29 +1,29 @@
-
- {{#each categories}} - + {{/each}}
- // 新增 value +
// 新增 value + value="{{restaurant.address}}">
// 新增 value + value="{{restaurant.openingHours}}">
From d72fb1a7542b6d32d774e7af8bd334935420eaaf Mon Sep 17 00:00:00 2001 From: sdfghjka Date: Sat, 8 Feb 2025 12:51:04 +0800 Subject: [PATCH 45/61] category create --- controllers/category-controller.js | 26 +++++++++++++++++++------- routes/modules/admin.js | 5 +++-- views/admin/categories.hbs | 10 ++++++++++ 3 files changed, 32 insertions(+), 9 deletions(-) diff --git a/controllers/category-controller.js b/controllers/category-controller.js index 3759fb6a8..471772c17 100644 --- a/controllers/category-controller.js +++ b/controllers/category-controller.js @@ -1,11 +1,23 @@ -const { Category } = require('../models') +const { Category } = require("../models"); const categoryController = { getCategories: (req, res, next) => { return Category.findAll({ - raw: true + raw: true, }) - .then(categories => res.render('admin/categories', { categories })) - .catch(err => next(err)) - } -} -module.exports = categoryController \ No newline at end of file + .then((categories) => res.render("admin/categories", { categories })) + .catch((err) => next(err)); + }, + postCategory: (req, res) => { + const { name } = req.body; + if(!name) throw new Error("Category name is required!") + Category.create({ + name, + }).then(()=>{ + req.flash('success_messages','新增類別成功!') + res.redirect("/admin/categories"); + }).catch((error)=>{ + next(error) + }) + }, +}; +module.exports = categoryController; diff --git a/routes/modules/admin.js b/routes/modules/admin.js index 8b9a11d31..c018d7b1a 100644 --- a/routes/modules/admin.js +++ b/routes/modules/admin.js @@ -2,7 +2,7 @@ const express = require("express"); const router = express.Router(); const adminController = require("../../controllers/admin-controller"); const upload = require("../../middleware/multer"); -const categoryController = require('../../controllers/category-controller') +const categoryController = require("../../controllers/category-controller"); router.get("/restaurants", adminController.getRestaurants); router.get("/restaurants/create", adminController.createRestaurant); @@ -21,6 +21,7 @@ router.post( ); router.get("/users", adminController.getUsers); router.patch("/users/:id", adminController.patchUser); -router.get('/categories', categoryController.getCategories) +router.get("/categories", categoryController.getCategories); +router.post("/categories", categoryController.postCategory); router.use("/", (req, res) => res.redirect("/admin/restaurants")); module.exports = router; diff --git a/views/admin/categories.hbs b/views/admin/categories.hbs index e7d8c233b..77329e017 100644 --- a/views/admin/categories.hbs +++ b/views/admin/categories.hbs @@ -1,4 +1,14 @@ {{>admin-tabs route='categories'}} +
+
+
+ +
+
+ +
+
+
From 0b80b2273473fbe2fc99ed1aa7d1ed890f9f74e2 Mon Sep 17 00:00:00 2001 From: sdfghjka Date: Sat, 8 Feb 2025 13:08:23 +0800 Subject: [PATCH 46/61] category update --- controllers/category-controller.js | 39 ++++++++++++++++++++++-------- routes/modules/admin.js | 2 ++ views/admin/categories.hbs | 28 +++++++++++++++------ 3 files changed, 51 insertions(+), 18 deletions(-) diff --git a/controllers/category-controller.js b/controllers/category-controller.js index 471772c17..275d3388a 100644 --- a/controllers/category-controller.js +++ b/controllers/category-controller.js @@ -1,23 +1,42 @@ const { Category } = require("../models"); const categoryController = { getCategories: (req, res, next) => { - return Category.findAll({ - raw: true, - }) - .then((categories) => res.render("admin/categories", { categories })) + return Promise.all([ + Category.findAll({ raw: true }), + req.params.id ? Category.findByPk(req.params.id, { raw: true }) : null, + ]) + .then(([categories, category]) => { + res.render("admin/categories", { + categories, + category, + }); + }) .catch((err) => next(err)); }, postCategory: (req, res) => { const { name } = req.body; - if(!name) throw new Error("Category name is required!") + if (!name) throw new Error("Category name is required!"); Category.create({ name, - }).then(()=>{ - req.flash('success_messages','新增類別成功!') - res.redirect("/admin/categories"); - }).catch((error)=>{ - next(error) }) + .then(() => { + req.flash("success_messages", "新增類別成功!"); + res.redirect("/admin/categories"); + }) + .catch((error) => { + next(error); + }); + }, + putCategory: (req, res, next) => { + const { name } = req.body; + if (!name) throw new Error("Category name is required!"); + return Category.findByPk(req.params.id) + .then((category) => { + if (!category) throw new Error("Category doesn't exist!"); + return category.update({ name }); + }) + .then(() => res.redirect("/admin/categories")) + .catch((err) => next(err)); }, }; module.exports = categoryController; diff --git a/routes/modules/admin.js b/routes/modules/admin.js index c018d7b1a..05b135079 100644 --- a/routes/modules/admin.js +++ b/routes/modules/admin.js @@ -21,6 +21,8 @@ router.post( ); router.get("/users", adminController.getUsers); router.patch("/users/:id", adminController.patchUser); +router.get("/categories/:id", categoryController.getCategories); +router.put("/categories/:id", categoryController.putCategory); router.get("/categories", categoryController.getCategories); router.post("/categories", categoryController.postCategory); router.use("/", (req, res) => res.redirect("/admin/restaurants")); diff --git a/views/admin/categories.hbs b/views/admin/categories.hbs index 77329e017..8169f6995 100644 --- a/views/admin/categories.hbs +++ b/views/admin/categories.hbs @@ -1,13 +1,25 @@ {{>admin-tabs route='categories'}}
-
-
- -
-
- -
- +{{#if category}} +
+
+ +
+
+ + or Create New +
+ + {{else}} +
+
+ +
+
+ +
+ + {{/if}}
From 2387b5b0052244bbb90bef630540ef737a77d613 Mon Sep 17 00:00:00 2001 From: sdfghjka Date: Sat, 8 Feb 2025 13:25:45 +0800 Subject: [PATCH 47/61] category delete --- controllers/category-controller.js | 17 ++++++++++++++++- routes/modules/admin.js | 1 + 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/controllers/category-controller.js b/controllers/category-controller.js index 275d3388a..cccb1760e 100644 --- a/controllers/category-controller.js +++ b/controllers/category-controller.js @@ -13,7 +13,7 @@ const categoryController = { }) .catch((err) => next(err)); }, - postCategory: (req, res) => { + postCategory: (req, res, next) => { const { name } = req.body; if (!name) throw new Error("Category name is required!"); Category.create({ @@ -38,5 +38,20 @@ const categoryController = { .then(() => res.redirect("/admin/categories")) .catch((err) => next(err)); }, + deleteCategory: (req, res, next) => { + const { id } = req.params; + Category.findByPk(id) + .then((Category)=>{ + if (!Category) throw new Error("Category didn't exist!"); + return Category.destroy(); + }) + .then(()=>{ + req.flash("success_messages", "刪除類別成功!") + return res.redirect("/admin/categories"); + }) + .catch((error)=>{ + next(error) + }) + }, }; module.exports = categoryController; diff --git a/routes/modules/admin.js b/routes/modules/admin.js index 05b135079..e8bbad11a 100644 --- a/routes/modules/admin.js +++ b/routes/modules/admin.js @@ -23,6 +23,7 @@ router.get("/users", adminController.getUsers); router.patch("/users/:id", adminController.patchUser); router.get("/categories/:id", categoryController.getCategories); router.put("/categories/:id", categoryController.putCategory); +router.delete("/categories/:id", categoryController.deleteCategory); router.get("/categories", categoryController.getCategories); router.post("/categories", categoryController.postCategory); router.use("/", (req, res) => res.redirect("/admin/restaurants")); From 1ededb5e6bc916a5d596e3a29f5a0638f885f424 Mon Sep 17 00:00:00 2001 From: sdfghjka Date: Sat, 8 Feb 2025 14:18:44 +0800 Subject: [PATCH 48/61] add restaurants index page --- controllers/restaurant-controller.js | 17 +++++++++++++++-- views/restaurants.hbs | 22 +++++++++++++++++++++- 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/controllers/restaurant-controller.js b/controllers/restaurant-controller.js index 69753cab9..d6dae2264 100644 --- a/controllers/restaurant-controller.js +++ b/controllers/restaurant-controller.js @@ -1,6 +1,19 @@ +const { Restaurant, Category } = require('../models') const restaurantController = { getRestaurants: (req, res) => { - return res.render('restaurants') + return Restaurant.findAll({ + include: Category, + nest: true, + raw: true + }).then(restaurants => { + const data = restaurants.map(r => ({ + ...r, + description: r.description.substring(0, 50) + })) + return res.render('restaurants', { + restaurants: data + }) + }) } } -module.exports = restaurantController +module.exports = restaurantController \ No newline at end of file diff --git a/views/restaurants.hbs b/views/restaurants.hbs index 7b5bc24b5..245e7a2a6 100644 --- a/views/restaurants.hbs +++ b/views/restaurants.hbs @@ -1 +1,21 @@ -
restaurants
\ No newline at end of file +
+ {{#each restaurants}} +
+
+ + Card image cap + +
+

+ + {{this.name}} + +

+ {{this.Category.name}} +

{{this.description}}

+
+
+
+ {{/each}} +
\ No newline at end of file From c229d40ff65a02822578408c0afea837c4bfb4e2 Mon Sep 17 00:00:00 2001 From: sdfghjka Date: Sat, 8 Feb 2025 14:27:03 +0800 Subject: [PATCH 49/61] add restaurant page --- controllers/restaurant-controller.js | 38 +++++++++++++++++++--------- routes/index.js | 4 +++ views/restaurant.hbs | 21 +++++++++++++++ 3 files changed, 51 insertions(+), 12 deletions(-) create mode 100644 views/restaurant.hbs diff --git a/controllers/restaurant-controller.js b/controllers/restaurant-controller.js index d6dae2264..7f7342fa4 100644 --- a/controllers/restaurant-controller.js +++ b/controllers/restaurant-controller.js @@ -1,19 +1,33 @@ -const { Restaurant, Category } = require('../models') +const { Restaurant, Category } = require("../models"); const restaurantController = { getRestaurants: (req, res) => { return Restaurant.findAll({ include: Category, nest: true, - raw: true - }).then(restaurants => { - const data = restaurants.map(r => ({ + raw: true, + }).then((restaurants) => { + const data = restaurants.map((r) => ({ ...r, - description: r.description.substring(0, 50) - })) - return res.render('restaurants', { - restaurants: data - }) + description: r.description.substring(0, 50), + })); + return res.render("restaurants", { + restaurants: data, + }); + }); + }, + getRestaurant: (req, res, next) => { + return Restaurant.findByPk(req.params.id, { + include: Category, + nest: true, + raw: true, }) - } -} -module.exports = restaurantController \ No newline at end of file + .then((restaurant) => { + if (!restaurant) throw new Error("Restaurant didn't exist!"); + res.render("restaurant", { + restaurant, + }); + }) + .catch((err) => next(err)); + }, +}; +module.exports = restaurantController; diff --git a/routes/index.js b/routes/index.js index bf35ee694..ec70e2b8c 100644 --- a/routes/index.js +++ b/routes/index.js @@ -7,6 +7,9 @@ const { generalErrorHandler } = require("../middleware/error-handler"); const { authenticated, authenticatedAdmin } = require("../middleware/auth"); const passport = require("../config/passport"); + + +router.get("/restaurants/:id", authenticated, restController.getRestaurant); router.get("/restaurants", authenticated, restController.getRestaurants); router.use("/admin", authenticatedAdmin, admin); @@ -23,6 +26,7 @@ router.post( userController.signIn ); router.get("/logout", userController.logout); + router.use("/", (req, res) => res.redirect("/restaurants")); router.use("/", generalErrorHandler); module.exports = router; diff --git a/views/restaurant.hbs b/views/restaurant.hbs new file mode 100644 index 000000000..051fd35fe --- /dev/null +++ b/views/restaurant.hbs @@ -0,0 +1,21 @@ +
+
+

{{restaurant.name}}

+

[{{restaurant.Category.name}}]

+
+
+ +
+
    +
  • Opening Hour: {{restaurant.openingHours}}
  • +
  • Tel: {{restaurant.tel}}
  • +
  • Address: {{restaurant.address}}
  • +
+
+
+
+

{{restaurant.description}}

+
+
+
+Back \ No newline at end of file From d7703306cc1233f56d76fe9785863afe8fd908ed Mon Sep 17 00:00:00 2001 From: sdfghjka Date: Sat, 8 Feb 2025 16:14:53 +0800 Subject: [PATCH 50/61] R02 test --- controllers/restaurant-controller.js | 42 +++++++++---- ...208065919-add-viewCounts-to-restaurants.js | 14 +++++ models/restaurant.js | 4 ++ routes/index.js | 1 + tests/R02.test.js | 60 +++++++++++++++++++ views/dashboard.hbs | 14 +++++ views/restaurant.hbs | 1 + 7 files changed, 123 insertions(+), 13 deletions(-) create mode 100644 migrations/20250208065919-add-viewCounts-to-restaurants.js create mode 100644 tests/R02.test.js create mode 100644 views/dashboard.hbs diff --git a/controllers/restaurant-controller.js b/controllers/restaurant-controller.js index 7f7342fa4..5e2c1bb92 100644 --- a/controllers/restaurant-controller.js +++ b/controllers/restaurant-controller.js @@ -1,33 +1,49 @@ const { Restaurant, Category } = require("../models"); -const restaurantController = { - getRestaurants: (req, res) => { +const restController = { + getRestaurants: (req, res, next) => { return Restaurant.findAll({ include: Category, nest: true, raw: true, - }).then((restaurants) => { - const data = restaurants.map((r) => ({ - ...r, - description: r.description.substring(0, 50), - })); - return res.render("restaurants", { - restaurants: data, + }) + .then((restaurants) => { + const data = restaurants.map((r) => ({ + ...r, + description: r.description.substring(0, 50), + })); + return res.render("restaurants", { + restaurants: data, + }); + }) + .catch((error) => { + next(error); }); - }); }, getRestaurant: (req, res, next) => { return Restaurant.findByPk(req.params.id, { include: Category, nest: true, - raw: true, }) .then((restaurant) => { if (!restaurant) throw new Error("Restaurant didn't exist!"); + restaurant.increment({ viewCounts: 1 }); res.render("restaurant", { - restaurant, + restaurant: restaurant.toJSON(), }); }) .catch((err) => next(err)); }, + getDashboard: (req, res, next) => { + return Restaurant.findByPk(req.params.id, { + include: Category, + raw: true, + nest: true, + }) + .then((restaurant) => { + if (!restaurant) throw new Error("Restaurant didn't exist!"); + res.render("dashboard", { restaurant }); + }) + .catch((err) => next(err)); + }, }; -module.exports = restaurantController; +module.exports = restController; diff --git a/migrations/20250208065919-add-viewCounts-to-restaurants.js b/migrations/20250208065919-add-viewCounts-to-restaurants.js new file mode 100644 index 000000000..222cb522b --- /dev/null +++ b/migrations/20250208065919-add-viewCounts-to-restaurants.js @@ -0,0 +1,14 @@ +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.addColumn('Restaurants', "view_Counts", { + type: Sequelize.INTEGER, + allowNull: false, + defaultValue: 0, + }); + }, + + down: async (queryInterface, Sequelize) => { + await queryInterface.removeColumn('Restaurants', "view_Counts"); + } +}; + diff --git a/models/restaurant.js b/models/restaurant.js index bee72a563..5ae3bab13 100644 --- a/models/restaurant.js +++ b/models/restaurant.js @@ -19,6 +19,10 @@ module.exports = (sequelize, DataTypes) => { openingHours: DataTypes.STRING, description: DataTypes.TEXT, image: DataTypes.STRING, + viewCounts: { + type: DataTypes.INTEGER, + defaultValue: 0, + }, }, { sequelize, diff --git a/routes/index.js b/routes/index.js index ec70e2b8c..9f5a820e8 100644 --- a/routes/index.js +++ b/routes/index.js @@ -11,6 +11,7 @@ const passport = require("../config/passport"); router.get("/restaurants/:id", authenticated, restController.getRestaurant); router.get("/restaurants", authenticated, restController.getRestaurants); +router.get("/restaurants/:id/dashboard", authenticated, restController.getDashboard); router.use("/admin", authenticatedAdmin, admin); router.get("/signup", userController.signUpPage); diff --git a/tests/R02.test.js b/tests/R02.test.js new file mode 100644 index 000000000..728cce4dc --- /dev/null +++ b/tests/R02.test.js @@ -0,0 +1,60 @@ +const chai = require('chai') +const request = require('supertest') +const sinon = require('sinon') +const should = chai.should() + +const { createModelMock, createControllerProxy, mockRequest, mockResponse, mockNext } = require('../helpers/unit-test-helper'); + +describe('# R02: 餐廳資訊整理:Dashboard', function () { + context('# [Q1: Dashboard - 1 - controller / view / route]', () => { + before(async () => { + // 製作假資料 + // 本 context 會用這筆資料進行測試 + this.UserMock = createModelMock('User', [{ + id: 1, + email: 'root@example.com', + name: 'admin', + isAdmin: false, + }]) + this.RestaurantMock = createModelMock('Restaurant', [{ + id: 1, + name: '銷魂麵', + viewCounts: 3 + }]) + this.CategoryMock = createModelMock('Category', [{ + id: 1, + name: '食物' + }]) + this.CommentMock = createModelMock('Comment', [{ + id: 1, + text: "gogogo" + }]) + + // 連向模擬的 tables + this.restController = createControllerProxy('../controllers/restaurant-controller', { + User: this.UserMock, + Category: this.CategoryMock, + Restaurant: this.RestaurantMock, + Comment: this.CommentMock, + }) + }) + + it(' GET /restaurants/:id/dashboard ', async () => { + // 模擬 request & response & next + const req = mockRequest({ params: { id: 1 } }) // 帶入 params.id = 1,對 GET /restaurants/1/dashboard 發出請求 + const res = mockResponse() + const next = mockNext + + // 測試 restController.getDashBoard 函式 + await this.restController.getDashboard(req, res, next) + + // getDashBoard 正確執行的話,應呼叫 res.render + // res.render 的第 1 個參數要是 'dashboard' + // res.render 的第 2 個參數要包含 restaurant,其 name 屬性的值應是 '銷魂麵' + // res.render 的第 2 個參數要包含 restaurant,其 viewCounts 值應該是 3 + res.render.getCall(0).args[0].should.equal('dashboard') + res.render.getCall(0).args[1].restaurant.name.should.equal('銷魂麵') + res.render.getCall(0).args[1].restaurant.viewCounts.should.equal(3) + }) + }) +}) \ No newline at end of file diff --git a/views/dashboard.hbs b/views/dashboard.hbs new file mode 100644 index 000000000..d8a1e5d00 --- /dev/null +++ b/views/dashboard.hbs @@ -0,0 +1,14 @@ +
+
+

{{restaurant.name}}

+

{{restaurant.Category.name}}

+
+
+
    +
  • 瀏覽次數 : {{restaurant.viewCounts}}
  • +
  • 評論數 : -
  • +
  • 收藏數 : -
  • +
+
+
+Back \ No newline at end of file diff --git a/views/restaurant.hbs b/views/restaurant.hbs index 051fd35fe..216e5af93 100644 --- a/views/restaurant.hbs +++ b/views/restaurant.hbs @@ -16,6 +16,7 @@

{{restaurant.description}}

+ Dashboard
Back \ No newline at end of file From 2fbcddb36ef38aab9a84a9542e7d4b392938b439 Mon Sep 17 00:00:00 2001 From: sdfghjka Date: Sat, 8 Feb 2025 16:39:27 +0800 Subject: [PATCH 51/61] add categories navbar on restaurants index page --- controllers/restaurant-controller.js | 39 ++++++++++++++++------------ views/restaurants.hbs | 11 ++++++++ 2 files changed, 33 insertions(+), 17 deletions(-) diff --git a/controllers/restaurant-controller.js b/controllers/restaurant-controller.js index 5e2c1bb92..962fbf363 100644 --- a/controllers/restaurant-controller.js +++ b/controllers/restaurant-controller.js @@ -1,23 +1,28 @@ const { Restaurant, Category } = require("../models"); const restController = { - getRestaurants: (req, res, next) => { - return Restaurant.findAll({ - include: Category, - nest: true, - raw: true, - }) - .then((restaurants) => { - const data = restaurants.map((r) => ({ - ...r, - description: r.description.substring(0, 50), - })); - return res.render("restaurants", { - restaurants: data, - }); - }) - .catch((error) => { - next(error); + getRestaurants: (req, res) => { + const categoryId = Number(req.query.categoryId) || ""; + return Promise.all([ + Restaurant.findAll({ + include: Category, + where: { + ...(categoryId ? { categoryId } : {}), + }, + nest: true, + raw: true, + }), + Category.findAll({ raw: true }), + ]).then(([restaurants, categories]) => { + const data = restaurants.map((r) => ({ + ...r, + description: r.description.substring(0, 50), + })); + return res.render("restaurants", { + restaurants: data, + categories, + categoryId, }); + }); }, getRestaurant: (req, res, next) => { return Restaurant.findByPk(req.params.id, { diff --git a/views/restaurants.hbs b/views/restaurants.hbs index 245e7a2a6..0526b52f0 100644 --- a/views/restaurants.hbs +++ b/views/restaurants.hbs @@ -1,3 +1,14 @@ +
{{#each restaurants}}
From e8d995d70590c6541f73bac54c4c924a9c7bf9c7 Mon Sep 17 00:00:00 2001 From: sdfghjka Date: Sat, 8 Feb 2025 21:52:58 +0800 Subject: [PATCH 52/61] add pagination on restaurants index page --- controllers/restaurant-controller.js | 14 ++++++++++++-- helpers/pagination-helper.js | 19 +++++++++++++++++++ views/restaurants.hbs | 25 ++++++++++++++++++++++++- 3 files changed, 55 insertions(+), 3 deletions(-) create mode 100644 helpers/pagination-helper.js diff --git a/controllers/restaurant-controller.js b/controllers/restaurant-controller.js index 962fbf363..2b02ec534 100644 --- a/controllers/restaurant-controller.js +++ b/controllers/restaurant-controller.js @@ -1,19 +1,28 @@ const { Restaurant, Category } = require("../models"); +const { getOffset, getPagination } = require("../helpers/pagination-helper"); + const restController = { getRestaurants: (req, res) => { const categoryId = Number(req.query.categoryId) || ""; + const DEFAULT_LIMIT = 9; + const page = Number(req.query.page) || 1; + const limit = Number(req.query.limit) || DEFAULT_LIMIT; + const offset = getOffset(limit, page); + return Promise.all([ - Restaurant.findAll({ + Restaurant.findAndCountAll({ include: Category, where: { ...(categoryId ? { categoryId } : {}), }, + offset, + limit, nest: true, raw: true, }), Category.findAll({ raw: true }), ]).then(([restaurants, categories]) => { - const data = restaurants.map((r) => ({ + const data = restaurants.rows.map((r) => ({ ...r, description: r.description.substring(0, 50), })); @@ -21,6 +30,7 @@ const restController = { restaurants: data, categories, categoryId, + pagination: getPagination(limit, page, restaurants.count), }); }); }, diff --git a/helpers/pagination-helper.js b/helpers/pagination-helper.js new file mode 100644 index 000000000..536534d44 --- /dev/null +++ b/helpers/pagination-helper.js @@ -0,0 +1,19 @@ +const getOffset = (limit, page) => (page - 1) * limit; +const getPagination = (limit, page, total) => { + const totalPage = Math.ceil(total / limit); + const pages = Array.from({ length: totalPage }, (_, index) => index + 1); + const currentPage = page < 1 ? 1 : page > totalPage ? totalPage : page; + const prev = currentPage - 1 < 1 ? 1 : currentPage - 1; + const next = currentPage + 1 > totalPage ? totalPage : currentPage + 1; + return { + pages, + totalPage, + currentPage, + prev, + next, + }; +}; +module.exports = { + getOffset, + getPagination, +}; diff --git a/views/restaurants.hbs b/views/restaurants.hbs index 0526b52f0..41cd436ca 100644 --- a/views/restaurants.hbs +++ b/views/restaurants.hbs @@ -29,4 +29,27 @@
{{/each}} - \ No newline at end of file + + \ No newline at end of file From 993a809e7fd29eb3d37b4eb605a0aaff79b99a6f Mon Sep 17 00:00:00 2001 From: sdfghjka Date: Sat, 8 Feb 2025 22:16:15 +0800 Subject: [PATCH 53/61] add comment model --- migrations/20250208141204-create-comment.js | 33 ++++++++++++++++++ models/comment.js | 28 ++++++++++++++++ models/restaurant.js | 1 + models/user.js | 37 +++++++++++---------- 4 files changed, 81 insertions(+), 18 deletions(-) create mode 100644 migrations/20250208141204-create-comment.js create mode 100644 models/comment.js diff --git a/migrations/20250208141204-create-comment.js b/migrations/20250208141204-create-comment.js new file mode 100644 index 000000000..51f5143be --- /dev/null +++ b/migrations/20250208141204-create-comment.js @@ -0,0 +1,33 @@ +'use strict'; +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.createTable('Comments', { + id: { + allowNull: false, + autoIncrement: true, + primaryKey: true, + type: Sequelize.INTEGER + }, + text: { + type: Sequelize.STRING + }, + user_id: { + type: Sequelize.INTEGER + }, + restaurant_id: { + type: Sequelize.INTEGER + }, + created_at: { + allowNull: false, + type: Sequelize.DATE + }, + updated_at: { + allowNull: false, + type: Sequelize.DATE + } + }); + }, + down: async (queryInterface, Sequelize) => { + await queryInterface.dropTable('Comments'); + } +}; \ No newline at end of file diff --git a/models/comment.js b/models/comment.js new file mode 100644 index 000000000..afb116c6c --- /dev/null +++ b/models/comment.js @@ -0,0 +1,28 @@ +"use strict"; +const { Model } = require("sequelize"); +module.exports = (sequelize, DataTypes) => { + class Comment extends Model { + /** + * Helper method for defining associations. + * This method is not a part of Sequelize lifecycle. + * The `models/index` file will call this method automatically. + */ + static associate(models) { + Comment.belongsTo(models.Restaurant, { foreignKey: "restaurantId" }); + Comment.belongsTo(models.User, { foreignKey: "userId" }); + } + } + Comment.init( + { + text: DataTypes.STRING, + userId: DataTypes.INTEGER, + restaurantId: DataTypes.INTEGER, + }, + { + sequelize, + modelName: "Comment", + underscored: true, + } + ); + return Comment; +}; diff --git a/models/restaurant.js b/models/restaurant.js index 5ae3bab13..9f362f19d 100644 --- a/models/restaurant.js +++ b/models/restaurant.js @@ -9,6 +9,7 @@ module.exports = (sequelize, DataTypes) => { */ static associate(models) { Restaurant.belongsTo(models.Category, { foreignKey: "categoryId" }); + Restaurant.hasMany(models.Comment, { foreignKey: "restaurantId" }); } } Restaurant.init( diff --git a/models/user.js b/models/user.js index 7a0e6e56e..41807eaf2 100644 --- a/models/user.js +++ b/models/user.js @@ -1,7 +1,5 @@ -'use strict'; -const { - Model -} = require('sequelize'); +"use strict"; +const { Model } = require("sequelize"); module.exports = (sequelize, DataTypes) => { class User extends Model { /** @@ -10,19 +8,22 @@ module.exports = (sequelize, DataTypes) => { * The `models/index` file will call this method automatically. */ static associate(models) { - // define association here + User.hasMany(models.Comment, { foreignKey: "userId" }); } - }; - User.init({ - name: DataTypes.STRING, - email: DataTypes.STRING, - password: DataTypes.STRING, - isAdmin: DataTypes.BOOLEAN - }, { - sequelize, - modelName: 'User', - tableName: 'Users', - underscored: true, - }); + } + User.init( + { + name: DataTypes.STRING, + email: DataTypes.STRING, + password: DataTypes.STRING, + isAdmin: DataTypes.BOOLEAN, + }, + { + sequelize, + modelName: "User", + tableName: "Users", + underscored: true, + } + ); return User; -}; \ No newline at end of file +}; From 8ebd6fa93d93aac2a698d2e56dd8336c24ec67e5 Mon Sep 17 00:00:00 2001 From: sdfghjka Date: Sat, 8 Feb 2025 22:29:37 +0800 Subject: [PATCH 54/61] add post comment on restaurant page --- controllers/comment-controller.js | 27 +++++++++++++++++++++++++++ routes/index.js | 12 ++++++++---- views/restaurant.hbs | 24 ++++++++++++++++++++---- 3 files changed, 55 insertions(+), 8 deletions(-) create mode 100644 controllers/comment-controller.js diff --git a/controllers/comment-controller.js b/controllers/comment-controller.js new file mode 100644 index 000000000..1fb5482af --- /dev/null +++ b/controllers/comment-controller.js @@ -0,0 +1,27 @@ +const { Comment, User, Restaurant } = require("../models"); +const commentController = { + postComment: (req, res, next) => { + const { restaurantId, text } = req.body; + const userId = req.user.id; + if (!text) throw new Error("Comment text is required!"); + return Promise.all([ + User.findByPk(userId), + Restaurant.findByPk(restaurantId), + ]) + .then(([user, restaurant]) => { + if (!user) throw new Error("User didn't exist!"); + if (!restaurant) throw new Error("Restaurant didn't exist!"); + return Comment.create({ + text, + restaurantId, + userId, + }); + }) + .then(() => { + req.flash("success_messages", "comment was successfully created"); + res.redirect(`/restaurants/${restaurantId}`); + }) + .catch((err) => next(err)); + }, +}; +module.exports = commentController; diff --git a/routes/index.js b/routes/index.js index 9f5a820e8..4bafcd6e8 100644 --- a/routes/index.js +++ b/routes/index.js @@ -1,17 +1,21 @@ const express = require("express"); const router = express.Router(); const restController = require("../controllers/restaurant-controller"); +const commentController = require("../controllers/comment-controller"); const admin = require("../routes/modules/admin"); const userController = require("../controllers/user-controller"); const { generalErrorHandler } = require("../middleware/error-handler"); const { authenticated, authenticatedAdmin } = require("../middleware/auth"); const passport = require("../config/passport"); - - router.get("/restaurants/:id", authenticated, restController.getRestaurant); router.get("/restaurants", authenticated, restController.getRestaurants); -router.get("/restaurants/:id/dashboard", authenticated, restController.getDashboard); +router.get( + "/restaurants/:id/dashboard", + authenticated, + restController.getDashboard +); + router.use("/admin", authenticatedAdmin, admin); router.get("/signup", userController.signUpPage); @@ -27,7 +31,7 @@ router.post( userController.signIn ); router.get("/logout", userController.logout); - +router.post("/comments", authenticated, commentController.postComment); router.use("/", (req, res) => res.redirect("/restaurants")); router.use("/", generalErrorHandler); module.exports = router; diff --git a/views/restaurant.hbs b/views/restaurant.hbs index 216e5af93..c535af357 100644 --- a/views/restaurant.hbs +++ b/views/restaurant.hbs @@ -4,7 +4,12 @@

[{{restaurant.Category.name}}]

- +
  • Opening Hour: {{restaurant.openingHours}}
  • @@ -16,7 +21,18 @@

    {{restaurant.description}}

    - Dashboard -
    + Dashboard +
-Back \ No newline at end of file +
+
+ + +
+ + + Back + \ No newline at end of file From 7ec04613b15590fd7e526987d83cd2b30a2c2af6 Mon Sep 17 00:00:00 2001 From: sdfghjka Date: Sat, 8 Feb 2025 22:53:30 +0800 Subject: [PATCH 55/61] show comments on restaurant page --- controllers/restaurant-controller.js | 8 +++++--- helpers/handlebars-helpers.js | 5 ++++- views/restaurant.hbs | 10 ++++++++++ 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/controllers/restaurant-controller.js b/controllers/restaurant-controller.js index 2b02ec534..ed4ab18b7 100644 --- a/controllers/restaurant-controller.js +++ b/controllers/restaurant-controller.js @@ -1,4 +1,4 @@ -const { Restaurant, Category } = require("../models"); +const { Restaurant, Category, Comment, User } = require("../models"); const { getOffset, getPagination } = require("../helpers/pagination-helper"); const restController = { @@ -36,8 +36,10 @@ const restController = { }, getRestaurant: (req, res, next) => { return Restaurant.findByPk(req.params.id, { - include: Category, - nest: true, + include: [ + Category, + { model: Comment, include: User } + ] }) .then((restaurant) => { if (!restaurant) throw new Error("Restaurant didn't exist!"); diff --git a/helpers/handlebars-helpers.js b/helpers/handlebars-helpers.js index 6488122e2..10cb4c996 100644 --- a/helpers/handlebars-helpers.js +++ b/helpers/handlebars-helpers.js @@ -1,6 +1,9 @@ const dayjs = require("dayjs"); //載入 dayjs 套件 +const relativeTime = require("dayjs/plugin/relativeTime"); +dayjs.extend(relativeTime); module.exports = { - currentYear: () => dayjs().year(), //取得當年年份作為 currentYear 的屬性值,並導出 + currentYear: () => dayjs().year(), + relativeTimeFromNow: (a) => dayjs(a).fromNow(), ifCond: function (a, b, options) { return a === b ? options.fn(this) : options.inverse(this); }, diff --git a/views/restaurant.hbs b/views/restaurant.hbs index c535af357..8480b195e 100644 --- a/views/restaurant.hbs +++ b/views/restaurant.hbs @@ -27,6 +27,16 @@ >Dashboard
+{{!-- Comment --}} +

所有評論:

+{{#each restaurant.Comments}} +
+

{{this.User.name}}

+

{{this.text}}

+
{{ relativeTimeFromNow this.createdAt}}
+
+
+{{/each}}
From 5f39c60a4d8bb6c2d9ff27c3c59b412d41b9a0cb Mon Sep 17 00:00:00 2001 From: sdfghjka Date: Sat, 8 Feb 2025 23:13:46 +0800 Subject: [PATCH 56/61] add delete comment for admin --- controllers/comment-controller.js | 11 +++++++++++ routes/index.js | 1 + views/restaurant.hbs | 5 +++++ 3 files changed, 17 insertions(+) diff --git a/controllers/comment-controller.js b/controllers/comment-controller.js index 1fb5482af..bbabbc835 100644 --- a/controllers/comment-controller.js +++ b/controllers/comment-controller.js @@ -23,5 +23,16 @@ const commentController = { }) .catch((err) => next(err)); }, + deleteComment: (req, res, next) => { + return Comment.findByPk(req.params.id) + .then((comment) => { + if (!comment) throw new Error("Comment didn't exist!"); + return comment.destroy(); + }) + .then((deletedComment) => + res.redirect(`/restaurants/${deletedComment.restaurantId}`) + ) + .catch((err) => next(err)); + }, }; module.exports = commentController; diff --git a/routes/index.js b/routes/index.js index 4bafcd6e8..6d8ae5888 100644 --- a/routes/index.js +++ b/routes/index.js @@ -31,6 +31,7 @@ router.post( userController.signIn ); router.get("/logout", userController.logout); +router.delete('/comments/:id', authenticatedAdmin, commentController.deleteComment) router.post("/comments", authenticated, commentController.postComment); router.use("/", (req, res) => res.redirect("/restaurants")); router.use("/", generalErrorHandler); diff --git a/views/restaurant.hbs b/views/restaurant.hbs index 8480b195e..7e233f78a 100644 --- a/views/restaurant.hbs +++ b/views/restaurant.hbs @@ -30,6 +30,11 @@ {{!-- Comment --}}

所有評論:

{{#each restaurant.Comments}} +{{#if ../user.isAdmin}} + + + + {{/if}}

{{this.User.name}}

{{this.text}}

From a84bf68c527d2053af5df7269aace691c6d8b410 Mon Sep 17 00:00:00 2001 From: sdfghjka Date: Sun, 9 Feb 2025 17:08:42 +0800 Subject: [PATCH 57/61] R03 test --- controllers/user-controller.js | 73 +++++++-- .../20250209044157-add-image-to-user.js | 15 ++ models/user.js | 1 + routes/index.js | 9 +- routes/modules/users.js | 10 ++ tests/R03.test.js | 155 ++++++++++++++++++ views/partials/header.hbs | 3 + views/users/edit.hbs | 22 +++ views/users/profile.hbs | 65 ++++++++ 9 files changed, 342 insertions(+), 11 deletions(-) create mode 100644 migrations/20250209044157-add-image-to-user.js create mode 100644 routes/modules/users.js create mode 100644 tests/R03.test.js create mode 100644 views/users/edit.hbs create mode 100644 views/users/profile.hbs diff --git a/controllers/user-controller.js b/controllers/user-controller.js index a3c0c5046..47fb7b4af 100644 --- a/controllers/user-controller.js +++ b/controllers/user-controller.js @@ -2,6 +2,10 @@ const bcrypt = require("bcryptjs"); const db = require("../models"); const { where } = require("sequelize"); const User = db.User; +const Comment = db.Comment; +const Restaurant = db.Restaurant; +const { localFileHandler } = require("../helpers/file-helpers"); +const { raw } = require("express"); const userController = { signUpPage: (req, res) => { @@ -14,7 +18,7 @@ const userController = { User.findOne({ where: { email: req.body.email } }) .then((user) => { if (user) throw new Error("Email already exists!"); - return bcrypt.hash(req.body.password, 10); + return bcrypt.hash(req.body.password, 10); }) .then((hash) => { User.create({ @@ -24,22 +28,71 @@ const userController = { }); }) .then(() => { - req.flash("success_messages", "成功註冊帳號!"); + req.flash("success_messages", "成功註冊帳號!"); res.redirect("/signin"); }) - .catch((err) => next(err)); + .catch((err) => next(err)); }, signInPage: (req, res) => { - res.render('signin') + res.render("signin"); }, signIn: (req, res) => { - req.flash('success_messages', '成功登入!') - res.redirect('/restaurants') + req.flash("success_messages", "成功登入!"); + res.redirect("/restaurants"); }, logout: (req, res) => { - req.flash('success_messages', '登出成功!') - req.logout() - res.redirect('/signin') - } + req.flash("success_messages", "登出成功!"); + req.logout(); + res.redirect("/signin"); + }, + getUser: (req, res, next) => { + return Promise.all([ + Comment.findAndCountAll({ + where: { userId: 1 }, + include: [Restaurant], + nest: true, + raw: true, + }), + User.findByPk(req.params.id, { + raw: true, + }), + ]) + .then(([comment, user]) => { + return res.render("users/profile", { user, comment }); + }) + .catch((error) => { + next(error); + }); + }, + editUser: (req, res, next) => { + return User.findByPk(req.params.id) + .then((user) => { + return res.render("users/edit", { user: user.toJSON() }); + }) + .catch((error) => { + next(error); + }); + }, + putUser: (req, res, next) => { + const { name } = req.body; + const { file } = req; + if (req.user.id !== Number(req.params.id)) + throw new Error("You don't have permission"); + if (!name.trim()) throw new Error("User name is required!"); + return Promise.all([User.findByPk(req.params.id), localFileHandler(file)]) + .then(([user, filePath]) => { + if (!user) throw new Error("User didn't exist!"); + + return user.update({ + name, + image: filePath || user.image, + }); + }) + .then((user) => { + req.flash("success_messages", "使用者資料編輯成功"); + res.redirect(`/users/${user.id}`); + }) + .catch((err) => next(err)); + }, }; module.exports = userController; diff --git a/migrations/20250209044157-add-image-to-user.js b/migrations/20250209044157-add-image-to-user.js new file mode 100644 index 000000000..b40e88ef7 --- /dev/null +++ b/migrations/20250209044157-add-image-to-user.js @@ -0,0 +1,15 @@ +'use strict'; + +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.addColumn('Users','image',{ + type: Sequelize.STRING + }) + }, + + down: async (queryInterface, Sequelize) => { + await queryInterface.removeColumn('Users','image',{ + type: Sequelize.STRING + }) + } +}; diff --git a/models/user.js b/models/user.js index 41807eaf2..23cc15bb7 100644 --- a/models/user.js +++ b/models/user.js @@ -17,6 +17,7 @@ module.exports = (sequelize, DataTypes) => { email: DataTypes.STRING, password: DataTypes.STRING, isAdmin: DataTypes.BOOLEAN, + image: DataTypes.STRING }, { sequelize, diff --git a/routes/index.js b/routes/index.js index 6d8ae5888..4e08120dd 100644 --- a/routes/index.js +++ b/routes/index.js @@ -3,6 +3,7 @@ const router = express.Router(); const restController = require("../controllers/restaurant-controller"); const commentController = require("../controllers/comment-controller"); const admin = require("../routes/modules/admin"); +const users = require("../routes/modules/users"); const userController = require("../controllers/user-controller"); const { generalErrorHandler } = require("../middleware/error-handler"); const { authenticated, authenticatedAdmin } = require("../middleware/auth"); @@ -17,6 +18,7 @@ router.get( ); router.use("/admin", authenticatedAdmin, admin); +router.use("/users", users); router.get("/signup", userController.signUpPage); router.post("/signup", userController.signUp); @@ -31,7 +33,12 @@ router.post( userController.signIn ); router.get("/logout", userController.logout); -router.delete('/comments/:id', authenticatedAdmin, commentController.deleteComment) + +router.delete( + "/comments/:id", + authenticatedAdmin, + commentController.deleteComment +); router.post("/comments", authenticated, commentController.postComment); router.use("/", (req, res) => res.redirect("/restaurants")); router.use("/", generalErrorHandler); diff --git a/routes/modules/users.js b/routes/modules/users.js new file mode 100644 index 000000000..8dc143515 --- /dev/null +++ b/routes/modules/users.js @@ -0,0 +1,10 @@ +const express = require("express"); +const router = express.Router(); +const userController = require("../../controllers/user-controller"); +const upload = require("../../middleware/multer"); + +router.get("/:id/edit", userController.editUser); +router.get("/:id", userController.getUser); +router.put("/:id", upload.single("image"), userController.putUser); + +module.exports = router; diff --git a/tests/R03.test.js b/tests/R03.test.js new file mode 100644 index 000000000..9f39816ab --- /dev/null +++ b/tests/R03.test.js @@ -0,0 +1,155 @@ +const chai = require('chai') +const sinon = require('sinon') +const should = chai.should() + +const helpers = require('../helpers/auth-helpers') +const { createModelMock, createControllerProxy, mockRequest, mockResponse, mockNext } = require('../helpers/unit-test-helper'); + +describe('# R03', () => { + describe('# R03: 建立 User Profile', function () { + context('# [瀏覽 Profile]', () => { + // 前置準備 + before(() => { + // 模擬登入驗證 + this.ensureAuthenticated = sinon + .stub(helpers, 'ensureAuthenticated') + .returns(true) + this.getUser = sinon.stub(helpers, 'getUser').returns({ id: 1 }) + // 製作假資料 + // 本 context 會用這筆資料進行測試 + this.UserMock = createModelMock('User', [{ + id: 1, + email: 'root@example.com', + name: 'admin', + isAdmin: false, + }]) + + // 修改 userController 中的資料庫連線設定,由連向真實的資料庫 -> 改為連向模擬的 User table + this.userController = createControllerProxy('../controllers/user-controller', { User: this.UserMock }) + }) + + // 開始測試 + it(' GET /users/:id ', async () => { + // 模擬 request & response & next + const req = mockRequest({ params: { id: 1 } }) // 帶入 params.id = 1,對 GET /users/1 發出請求 + const res = mockResponse() + const next = mockNext + + // 測試作業指定的 userController.getUser 函式 + await this.userController.getUser(req, res, next); + + // getUser 正確執行的話,應呼叫 res.render + // res.render 的第 1 個參數要是 'users/profile' + // res.render 的第 2 個參數要是 user,其 id 屬性的值應是 1 + res.render.getCall(0).args[0].should.equal('users/profile') + res.render.getCall(0).args[1].user.id.should.equal(1) + }) + + // 測試完畢,清除資料 + after(async () => { + // 清除模擬驗證資料 + this.ensureAuthenticated.restore() + this.getUser.restore() + }) + }) + + context('# [瀏覽編輯 Profile 頁面]', () => { + before(() => { + // 模擬登入驗證 + this.ensureAuthenticated = sinon + .stub(helpers, 'ensureAuthenticated') + .returns(true) + this.getUser = sinon.stub(helpers, 'getUser').returns({ id: 1 }) + + // 製作假資料 + // 本 context 會用這筆資料進行測試 + this.UserMock = createModelMock('User', [{ + id: 1, + email: 'root@example.com', + name: 'admin', + isAdmin: false, + }]) + + // 連向模擬的 User table + this.userController = createControllerProxy('../controllers/user-controller', { User: this.UserMock }) + }) + + it(' GET /users/:id/edit ', async () => { + // 模擬 request & response & next + const req = mockRequest({ params: { id: 1 } }) // 帶入 params.id = 1,對 GET /users/1/edit 發出請求 + const res = mockResponse() + const next = mockNext + + // 測試作業指定的 adminController.editUser 函式 + await this.userController.editUser(req, res, next) + + // editUser 執行完畢後,應呼叫 res.render + // res.render 的第 1 個參數要是 'users/edit' + // res.render 的第 2 個參數要是 user,其 name 屬性的值應是 'admin' + res.render.getCall(0).args[0].should.equal('users/edit') + res.render.getCall(0).args[1].user.name.should.equal('admin') + }) + + after(async () => { + // 清除模擬驗證資料 + this.ensureAuthenticated.restore() + this.getUser.restore() + }) + }) + + context('# [編輯 Profile]', () => { + before(async () => { + // 模擬登入驗證 + this.ensureAuthenticated = sinon + .stub(helpers, 'ensureAuthenticated') + .returns(true) + this.getUser = sinon.stub(helpers, 'getUser').returns({ id: 1 }) + // 製作假資料 + // 本 context 會用這筆資料進行測試 + this.UserMock = createModelMock( + 'User', + [{ + id: 1, + email: 'root@example.com', + name: 'admin', + isAdmin: false, + }] + ) + + // 連向模擬的 User table + this.userController = createControllerProxy('../controllers/user-controller', { User: this.UserMock }) + }) + + it(' PUT /users/:id ', async () => { + // 模擬 request & response & next + // 對 PUT /users/1 發出 request,並夾帶 body.name = amdin2, user.id = 1 + const req = mockRequest({ + user: {id: 1}, + params: { id: 1 }, + body: { name: 'admin2' }, + }) + const res = mockResponse() + const next = mockNext + + // 測試作業指定的 userController.putUser 函式 + await this.userController.putUser(req, res, next) + + // putUser 正確執行的話,應呼叫 req.flash + // req.flash 的參數應與下列字串一致 + req.flash.calledWith('success_messages','使用者資料編輯成功').should.be.true + // putUser 執行完畢,應呼叫 res.redirect 並重新導向 /users/1 + res.redirect.calledWith('/users/1').should.be.true + // putUser 執行完畢後,id:1 使用者的 name 應該已被修改 + // 將假資料撈出,比對確認有成功修改到 + const user = await this.UserMock.findOne({ where: { id: 1 } }) + user.name.should.equal('admin2') + }) + + after(async () => { + // 清除模擬驗證資料 + this.ensureAuthenticated.restore() + this.getUser.restore() + }) + }) + }) +}) \ No newline at end of file diff --git a/views/partials/header.hbs b/views/partials/header.hbs index edf70ca4c..198a21297 100644 --- a/views/partials/header.hbs +++ b/views/partials/header.hbs @@ -17,6 +17,9 @@ + diff --git a/views/users/edit.hbs b/views/users/edit.hbs new file mode 100644 index 000000000..d734bb969 --- /dev/null +++ b/views/users/edit.hbs @@ -0,0 +1,22 @@ +
+ +
+ User Image +
+ + +
+ + +
+ + +
+ + +
+ + + + + diff --git a/views/users/profile.hbs b/views/users/profile.hbs new file mode 100644 index 000000000..9711082c5 --- /dev/null +++ b/views/users/profile.hbs @@ -0,0 +1,65 @@ + + + + + 使用者個人頁面 + + + + +
+ +
+
+
+ 使用者大頭照 +
+
+

{{user.name}}

+

{{user.email}}

+

評論數: {{comment.count}}

+
+
+ 編輯 +
+ +
+
+ + +
+
評論過的餐廳
+ +
+ +
+ + + + \ No newline at end of file From fc6d2030b26c8759f22d2a2371194fd7922dd057 Mon Sep 17 00:00:00 2001 From: sdfghjka Date: Sun, 9 Feb 2025 22:00:24 +0800 Subject: [PATCH 58/61] add feeds page --- controllers/restaurant-controller.js | 30 ++++++++++++++++--- routes/index.js | 1 + views/feeds.hbs | 45 ++++++++++++++++++++++++++++ views/partials/restaurant-tabs.hbs | 8 +++++ views/restaurants.hbs | 4 +++ 5 files changed, 84 insertions(+), 4 deletions(-) create mode 100644 views/feeds.hbs create mode 100644 views/partials/restaurant-tabs.hbs diff --git a/controllers/restaurant-controller.js b/controllers/restaurant-controller.js index ed4ab18b7..d7925e071 100644 --- a/controllers/restaurant-controller.js +++ b/controllers/restaurant-controller.js @@ -36,10 +36,7 @@ const restController = { }, getRestaurant: (req, res, next) => { return Restaurant.findByPk(req.params.id, { - include: [ - Category, - { model: Comment, include: User } - ] + include: [Category, { model: Comment, include: User }], }) .then((restaurant) => { if (!restaurant) throw new Error("Restaurant didn't exist!"); @@ -62,5 +59,30 @@ const restController = { }) .catch((err) => next(err)); }, + getFeeds: (req, res, next) => { + return Promise.all([ + Restaurant.findAll({ + limit: 10, + order: [["createdAt", "DESC"]], + include: [Category], + raw: true, + nest: true, + }), + Comment.findAll({ + limit: 10, + order: [["createdAt", "DESC"]], + include: [User, Restaurant], + raw: true, + nest: true, + }), + ]) + .then(([restaurants, comments]) => { + res.render("feeds", { + restaurants, + comments, + }); + }) + .catch((err) => next(err)); + }, }; module.exports = restController; diff --git a/routes/index.js b/routes/index.js index 4e08120dd..f163631c6 100644 --- a/routes/index.js +++ b/routes/index.js @@ -9,6 +9,7 @@ const { generalErrorHandler } = require("../middleware/error-handler"); const { authenticated, authenticatedAdmin } = require("../middleware/auth"); const passport = require("../config/passport"); +router.get("/restaurants/feeds", authenticated, restController.getFeeds); router.get("/restaurants/:id", authenticated, restController.getRestaurant); router.get("/restaurants", authenticated, restController.getRestaurants); router.get( diff --git a/views/feeds.hbs b/views/feeds.hbs new file mode 100644 index 000000000..6dedb8093 --- /dev/null +++ b/views/feeds.hbs @@ -0,0 +1,45 @@ +{{> restaurant-tabs route='feeds'}} +

最新動態

+
+
+
+
+
+ 最新餐廳 +
+
+ {{#each restaurants}} +
+

+ {{this.name}} + [{{this.Category.name}}] +

+

{{this.description}}

+ {{relativeTimeFromNow this.createdAt}} +
+
+ {{/each}} +
+
+
+
+
+
+ 最新評論 +
+
+ {{#each comments}} +
+

+ {{this.Restaurant.name}} +

+

{{this.text}}

+ - {{this.User.name}} + at {{relativeTimeFromNow this.createdAt}} +
+
+ {{/each}} +
+
+
+
\ No newline at end of file diff --git a/views/partials/restaurant-tabs.hbs b/views/partials/restaurant-tabs.hbs new file mode 100644 index 000000000..3b5969b9a --- /dev/null +++ b/views/partials/restaurant-tabs.hbs @@ -0,0 +1,8 @@ + \ No newline at end of file diff --git a/views/restaurants.hbs b/views/restaurants.hbs index 41cd436ca..55e2e4301 100644 --- a/views/restaurants.hbs +++ b/views/restaurants.hbs @@ -1,3 +1,7 @@ +{{> restaurant-tabs route='index'}} +
From c86ca7782d7f6e097528f403856fa0de7ff2c2da Mon Sep 17 00:00:00 2001 From: sdfghjka Date: Mon, 10 Feb 2025 12:44:07 +0800 Subject: [PATCH 61/61] get user's favorited restaurants & switch button --- config/passport.js | 7 ++++--- controllers/restaurant-controller.js | 13 ++++++++++++- views/restaurant.hbs | 9 +++++++++ views/restaurants.hbs | 15 +++++++++------ 4 files changed, 34 insertions(+), 10 deletions(-) diff --git a/config/passport.js b/config/passport.js index 0d073959f..9a4f156df 100644 --- a/config/passport.js +++ b/config/passport.js @@ -1,8 +1,7 @@ const passport = require("passport"); const LocalStrategy = require("passport-local"); const bcrypt = require("bcryptjs"); -const db = require("../models"); -const User = db.User; +const { User, Restaurant } = require("../models"); // set up Passport strategy passport.use( new LocalStrategy( @@ -39,7 +38,9 @@ passport.serializeUser((user, cb) => { cb(null, user.id); }); passport.deserializeUser((id, cb) => { - User.findByPk(id).then((user) => { + return User.findByPk(id, { + include: [{ model: Restaurant, as: "FavoritedRestaurants" }], + }).then((user) => { user = user.toJSON(); return cb(null, user); }); diff --git a/controllers/restaurant-controller.js b/controllers/restaurant-controller.js index d7925e071..9d90663ff 100644 --- a/controllers/restaurant-controller.js +++ b/controllers/restaurant-controller.js @@ -22,9 +22,12 @@ const restController = { }), Category.findAll({ raw: true }), ]).then(([restaurants, categories]) => { + const favoritedRestaurantsId = + req.user && req.user.FavoritedRestaurants.map((fr) => fr.id); const data = restaurants.rows.map((r) => ({ ...r, description: r.description.substring(0, 50), + isFavorited: favoritedRestaurantsId.includes(r.id), })); return res.render("restaurants", { restaurants: data, @@ -36,13 +39,21 @@ const restController = { }, getRestaurant: (req, res, next) => { return Restaurant.findByPk(req.params.id, { - include: [Category, { model: Comment, include: User }], + include: [ + Category, + { model: Comment, include: User }, + { model: User, as: "FavoritedUsers" }, + ], }) .then((restaurant) => { if (!restaurant) throw new Error("Restaurant didn't exist!"); + const isFavorited = restaurant.FavoritedUsers.some( + (f) => f.id === req.user.id + ); restaurant.increment({ viewCounts: 1 }); res.render("restaurant", { restaurant: restaurant.toJSON(), + isFavorited, }); }) .catch((err) => next(err)); diff --git a/views/restaurant.hbs b/views/restaurant.hbs index 7e233f78a..9e67f555c 100644 --- a/views/restaurant.hbs +++ b/views/restaurant.hbs @@ -25,6 +25,15 @@ class="btn btn-primary" href="/restaurants/{{restaurant.id}}/dashboard" >Dashboard + {{#if isFavorited }} +
+ + + {{else}} +
+ + + {{/if}}
{{!-- Comment --}} diff --git a/views/restaurants.hbs b/views/restaurants.hbs index 4fe5e7ff8..fe813a23d 100644 --- a/views/restaurants.hbs +++ b/views/restaurants.hbs @@ -30,12 +30,15 @@ {{this.Category.name}}

{{this.description}}

-
- - -
- - + {{#if isFavorited}} +
+ + + {{else}} +
+ + + {{/if}}