├── .circleci └── config.yml ├── .firebaserc ├── .gitignore ├── LICENSE ├── README.md ├── __tests__ ├── firestore │ ├── user_restaurant_test.ts │ └── user_review_test.ts └── functions │ ├── cron_restaurant_ranking.ts │ └── update_restaurant_rate.ts ├── firebase.json ├── firestore.rules ├── functions ├── .gitignore ├── package-lock.json ├── package.json ├── src │ └── index.ts └── tsconfig.json ├── jest.config.js ├── package-lock.json ├── package.json ├── scripts ├── insert_dummy_restaurant.ts ├── insert_dummy_review.ts └── test_trigger_pubsub.ts ├── src ├── ranking.ts ├── restaurant.ts └── review.ts └── tsconfig.json /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | 3 | jobs: 4 | test-with-emulators: 5 | docker: 6 | - image: circleci/openjdk:latest-node 7 | working_directory: ~/project 8 | steps: 9 | - checkout 10 | - run: 11 | name: Setup functions 12 | command: | 13 | npm --prefix functions ci 14 | npm --prefix functions run build 15 | - run: npm ci 16 | - run: 17 | name: Test 18 | command: npm run test:ci 19 | - store_test_results: 20 | path: ./junit 21 | 22 | workflows: 23 | version: 2 24 | firebase-test: 25 | jobs: 26 | - test-with-emulators 27 | -------------------------------------------------------------------------------- /.firebaserc: -------------------------------------------------------------------------------- 1 | { 2 | "projects": { 3 | "default": "testing-firebase-test", 4 | "test": "testing-firebase-test", 5 | "dev": "testing-firebase-9d0aa" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # next.js build output 61 | .next 62 | 63 | firebase_config.json 64 | firebase_config_test.json 65 | service_account.json 66 | service_account_test.json 67 | junit/ 68 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Kenta Kase 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TestingFirebase 2 | [![CircleCI](https://circleci.com/gh/Kesin11/TestingFirebase/tree/master.svg?style=svg)](https://circleci.com/gh/Kesin11/TestingFirebase/tree/master) 3 | 4 | FirebaseプロジェクトをEmulator Suite(Firestore, Cloud Functions, PubSub)によるエミュレータを使ってテストするサンプルリポジトリ 5 | 6 | # USAGE 7 | ```sh 8 | # 別ウィンドウでエミュレータを立ち上げておく 9 | npm run emulators:start 10 | # テスト実行 11 | npm run test 12 | 13 | # もしくはエミュレータの起動・終了とテストをセットで実行できる 14 | npm run test:ci 15 | ``` 16 | 17 | TDDで開発する場合は、jest --watchの状態でテストを裏で実行しておくのが便利。 18 | 19 | ```sh 20 | # 別ウィンドウでエミュレータを立ち上げておく 21 | npm run emulators:start 22 | # さらに別ウィンドウでfunctions/index.tsのためにtsc --watchを立ち上げる 23 | npm --prefix functions run build:watch 24 | # jest --watch相当 25 | npm run test:watch 26 | ``` 27 | 28 | 29 | # Firebaseプロジェクトの構成 30 | 開発用のdev、テスト用のtestとFirebaseプロジェクトが別々に分かれている想定。 31 | 開発はdevで行い、CIでのテストをtestで行う。 32 | 33 | プロジェクトの切り替えは `firebase use dev` と `firebase use test` 34 | 35 | # テスト 36 | ## Firestore 37 | [\_\_tests\_\_/firestore](./__tests__/firestore) 38 | 39 | ```sh 40 | npm run test:firestore 41 | ``` 42 | 43 | Firestoreエミュレータを単独で使用するテスト。 44 | 主にSecurity Rule(firestore.rules)のテストです。 45 | 46 | 47 | ## Cloud Functions 48 | [\_\_tests\_\_/functions](./__tests__/functions) 49 | 50 | ```sh 51 | npm run test:functions 52 | ``` 53 | 54 | Cloud Functions + Firestore, PubSub の複数のエミュレータを協調させたテスト。 55 | Firestoreトリガー、PubSubトリガーでFunctionsが実行されたあとの状態をテストする。 56 | 57 | # CI 58 | [CircleCIの設定](./.circleci/config.yml)参照 59 | 60 | # サンプルとしての題材 61 | このリポジトリの本題はテストコードですが、テストするための題材である実装側の説明です。 62 | 63 | ## 要件 64 | 架空のレストランのレビューサイト 65 | 66 | - レストランの情報は運営(admin)だけが編集可能 67 | - ユーザーはレストランにレビュー文と、1-5の評価を付けることができる 68 | - レストランには過去のレビュー評価の平均点が表示される 69 | - 平均点によるランキング機能がある 70 | - ランキングは1日1回更新 71 | 72 | ## Firestore設計 73 | - /restaurants/{restaurantId} 74 | - (レストラン情報はユーザーからはread only) 75 | - rateAvg: number(レビュー平均点数) 76 | - rateNum: number(レビュー数) 77 | - name: string(レストラン名) 78 | - /reviews/{userId} 79 | - (ログイン済みユーザーのみread/write可能、店舗ごとに一人のユーザーがレビュー投稿できるのは1回のみ) 80 | - rate: number(レビュー点数) 81 | - text: string(レビュー本文) 82 | - timestamp: timestamp(投稿時間) 83 | - userId: string(ユーザーid) 84 | - /rankings/{id} 85 | - (ランキングはユーザーからはread only) 86 | - rank: number(順位) 87 | - rateAvg: number(レビュー平均点数) 88 | - restaurantId: string(レストランid) 89 | - restaurantName: string(レストラン名) 90 | 91 | ### Functions設計 92 | 93 | 以下の要件は、ユーザーからのwriteで自由に書き換えられてしまうとダメなのでSecurity Ruleでガードしつつ、Cloud FunctionsからAdmin SDKを使ってFirestoreを更新します。 94 | 95 | - レストランには過去のレビュー評価の平均点が表示される 96 | - ユーザーがレビューを投稿したときのFirestoreトリガーで平均点を計算し直す 97 | - ランキングは1日1回更新 98 | - Cloud SchedulerからPubSubを発火させ、PubSubトリガーでランキングを再集計する 99 | 100 | # LICENSE 101 | MIT -------------------------------------------------------------------------------- /__tests__/firestore/user_restaurant_test.ts: -------------------------------------------------------------------------------- 1 | import { firestore as admin_firestore } from "firebase-admin" 2 | import { firestore } from 'firebase' 3 | import * as firebase from '@firebase/testing' 4 | import uuid from 'uuid/v4' 5 | import { readFileSync } from 'fs' 6 | import { RestaurantUserModel, RestaurantAdminModel } from '../../src/restaurant' 7 | 8 | // エミュレーターのメモリ空間はprojectId毎に分けられるので、テストスクリプト毎にユニークになるようにprojectIdをランダムにする 9 | const projectId = `test-${uuid()}` 10 | firebase.loadFirestoreRules({ 11 | projectId, 12 | rules: readFileSync('firestore.rules', 'utf8') 13 | }) 14 | 15 | const uid = 'test-user' 16 | 17 | const restaurantNames = [ 18 | 'Super burger', 19 | 'Ramen nihon ichi', 20 | 'Fire stake' 21 | ] 22 | 23 | describe('restaurant', () => { 24 | let userFirestore: firestore.Firestore 25 | let adminFirestore: admin_firestore.Firestore 26 | let restaurantUserModel: RestaurantUserModel 27 | 28 | beforeAll(async () => { 29 | userFirestore = firebase.initializeTestApp({ 30 | projectId, 31 | auth: { uid } 32 | }).firestore() 33 | restaurantUserModel = new RestaurantUserModel(userFirestore) 34 | 35 | // user SDKとadmin SDKのFirestoreは型レベルでは別物だが、initializeAdminAppはuser SDKのFirestoreを返すので無理やりキャスト 36 | adminFirestore = firebase.initializeAdminApp({ 37 | projectId 38 | }).firestore() as unknown as admin_firestore.Firestore 39 | 40 | }) 41 | 42 | let restaurantIds: string[] = [] 43 | let restaurantId: string 44 | beforeEach(async () => { 45 | // ダミーのレストランを追加 46 | const restaurantModel = new RestaurantAdminModel(adminFirestore) 47 | for (const name of restaurantNames) { 48 | await restaurantModel.add(name) 49 | } 50 | }) 51 | 52 | beforeEach(async () => { 53 | const snapshot = await restaurantUserModel.getAll() 54 | snapshot.forEach((doc) => restaurantIds.push(doc.id)) 55 | restaurantId = restaurantIds[0] 56 | }) 57 | 58 | afterAll(async () => { 59 | // firebaseとのlistnerを削除しないとテストが終了できない 60 | await Promise.all(firebase.apps().map(app => app.delete())) 61 | }) 62 | 63 | afterEach(async () => { 64 | // Firestoreのデータを初期化 65 | await firebase.clearFirestoreData({ 66 | projectId 67 | }) 68 | }) 69 | 70 | describe('未認証ユーザー', () => { 71 | test('readできない', async () => { 72 | const noAuthFirestore = firebase.initializeTestApp({ 73 | projectId, 74 | auth: undefined, 75 | }).firestore() 76 | const noAuthRestaurantUserModel = new RestaurantUserModel(noAuthFirestore) 77 | 78 | firebase.assertFails(noAuthRestaurantUserModel.getAll()) 79 | }) 80 | }) 81 | 82 | describe('CRUD', () => { 83 | test('取得できる', async () => { 84 | firebase.assertSucceeds(restaurantUserModel.getAll()) 85 | }) 86 | 87 | test('作成できない', async () => { 88 | const collectionRef = restaurantUserModel.collectionRef() 89 | firebase.assertFails(collectionRef.add({ 90 | name: 'new restaurant' 91 | })) 92 | }) 93 | 94 | test('編集できない', async () => { 95 | const docRef = restaurantUserModel.collectionRef().doc(restaurantId) 96 | firebase.assertFails(docRef.update({ 97 | name: 'new restaurant' 98 | })) 99 | }) 100 | 101 | test('削除できない', async () => { 102 | const docRef = restaurantUserModel.collectionRef().doc(restaurantId) 103 | firebase.assertFails(docRef.delete()) 104 | }) 105 | }) 106 | }) 107 | -------------------------------------------------------------------------------- /__tests__/firestore/user_review_test.ts: -------------------------------------------------------------------------------- 1 | import { firestore as admin_firestore } from "firebase-admin" 2 | import { firestore } from 'firebase' 3 | import * as firebase from '@firebase/testing' 4 | import uuid from 'uuid/v4' 5 | import { readFileSync } from 'fs' 6 | import { RestaurantUserModel, RestaurantAdminModel } from '../../src/restaurant' 7 | import { ReviewUserModel, ReviewAdminModel } from "../../src/review" 8 | 9 | // エミュレーターのメモリ空間はprojectId毎に分けられるので、テストスクリプト毎にユニークになるようにprojectIdをランダムにする 10 | const projectId = `test-${uuid()}` 11 | firebase.loadFirestoreRules({ 12 | projectId, 13 | rules: readFileSync('firestore.rules', 'utf8') 14 | }) 15 | 16 | const uid = 'test-user' 17 | 18 | const restaurantNames = [ 19 | 'Super burger', 20 | 'Ramen nihon ichi', 21 | 'Fire stake' 22 | ] 23 | 24 | describe('reviews', () => { 25 | let userFirestore: firestore.Firestore 26 | let adminFirestore: admin_firestore.Firestore 27 | let restaurantUserModel: RestaurantUserModel 28 | let reviewUserModel: ReviewUserModel 29 | 30 | beforeAll(async () => { 31 | // 前回の結果が残っていないように初期化 32 | await firebase.clearFirestoreData({ projectId }) 33 | 34 | userFirestore = firebase.initializeTestApp({ 35 | projectId, 36 | auth: { uid } 37 | }).firestore() 38 | restaurantUserModel = new RestaurantUserModel(userFirestore) 39 | reviewUserModel = new ReviewUserModel(userFirestore) 40 | 41 | // user SDKとadmin SDKのFirestoreは型レベルでは別物だが、initializeAdminAppはuser SDKのFirestoreを返すので無理やりキャスト 42 | adminFirestore = firebase.initializeAdminApp({ 43 | projectId 44 | }).firestore() as unknown as admin_firestore.Firestore 45 | }) 46 | 47 | let restaurantIds: string[] = [] 48 | let restaurantId: string 49 | beforeEach(async () => { 50 | // ダミーのレストランを追加 51 | const restaurantModel = new RestaurantAdminModel(adminFirestore) 52 | for (const name of restaurantNames) { 53 | await restaurantModel.add(name) 54 | } 55 | 56 | // 適当なレストランのidを保存 57 | const snapshot = await restaurantUserModel.getAll() 58 | snapshot.forEach((doc) => restaurantIds.push(doc.id)) 59 | restaurantId = restaurantIds[0] 60 | }) 61 | 62 | afterAll(async () => { 63 | // firebaseとのlistnerを削除しないとテストが終了できない 64 | await Promise.all(firebase.apps().map(app => app.delete())) 65 | }) 66 | 67 | afterEach(async () => { 68 | // Firestoreのデータを初期化 69 | await firebase.clearFirestoreData({ projectId }) 70 | }) 71 | 72 | describe('未認証ユーザー', () => { 73 | test('readできない', async () => { 74 | const noAuthFirestore = firebase.initializeTestApp({ 75 | projectId, 76 | auth: undefined, 77 | }).firestore() 78 | const noAuthReviewUserModel = new ReviewUserModel(noAuthFirestore) 79 | 80 | firebase.assertFails(noAuthReviewUserModel.getAll(restaurantId)) 81 | }) 82 | }) 83 | 84 | describe('CRUD', () => { 85 | const review = { 86 | rate: 3, 87 | text: 'とても美味しいお店です', 88 | userId: uid, 89 | } 90 | 91 | test('createできる', async () => { 92 | await reviewUserModel.create(restaurantId, review) 93 | const addedReview = await reviewUserModel.get(restaurantId, uid).then((doc) => doc.data()) 94 | 95 | expect(addedReview).toEqual({ 96 | ...review, 97 | updatedAt: expect.any(firestore.Timestamp) 98 | }) 99 | }) 100 | 101 | test('updateできない', async () => { 102 | await reviewUserModel.create(restaurantId, review) 103 | 104 | const docRef = reviewUserModel.collectionRef(restaurantId).doc(uid) 105 | firebase.assertFails(docRef.update({ 106 | updatedAt: firestore.FieldValue.serverTimestamp(), 107 | text: 'とても美味しいお店です 追記: 編集しました' 108 | })) 109 | }) 110 | 111 | test('deleteできない', async () => { 112 | await reviewUserModel.create(restaurantId, review) 113 | 114 | const docRef = reviewUserModel.collectionRef(restaurantId).doc(uid) 115 | firebase.assertFails(docRef.delete()) 116 | }) 117 | }) 118 | 119 | describe('バリデーション', () => { 120 | const base = { 121 | rate: 3, 122 | text: 'とても美味しいお店です', 123 | userId: uid, 124 | } 125 | 126 | test('自分以外のuser_idでは作成できない', async () => { 127 | const review = { 128 | ...base, 129 | userId: 'hogehoge', 130 | } 131 | firebase.assertFails(reviewUserModel.create(restaurantId, review)) 132 | }) 133 | 134 | describe('rateが', () => { 135 | test('負 NG', async () => { 136 | const review = { 137 | ...base, 138 | rate: -1, 139 | } 140 | firebase.assertFails(reviewUserModel.create(restaurantId, review)) 141 | }) 142 | 143 | test('0 NG', async () => { 144 | const review = { 145 | ...base, 146 | rate: 0, 147 | } 148 | firebase.assertFails(reviewUserModel.create(restaurantId, review)) 149 | }) 150 | 151 | test('1 OK', async () => { 152 | const review = { 153 | ...base, 154 | rate: 1, 155 | } 156 | firebase.assertSucceeds(reviewUserModel.create(restaurantId, review)) 157 | }) 158 | 159 | test('5 OK', async () => { 160 | const review = { 161 | ...base, 162 | rate: 1, 163 | } 164 | firebase.assertSucceeds(reviewUserModel.create(restaurantId, review)) 165 | }) 166 | 167 | test('5より大きい NG', async () => { 168 | const review = { 169 | ...base, 170 | rate: 6, 171 | } 172 | firebase.assertFails(reviewUserModel.create(restaurantId, review)) 173 | }) 174 | }) 175 | }) 176 | }) -------------------------------------------------------------------------------- /__tests__/functions/cron_restaurant_ranking.ts: -------------------------------------------------------------------------------- 1 | import { firestore as admin_firestore, firestore } from "firebase-admin" 2 | import * as firebase from '@firebase/testing' 3 | import { readFileSync } from 'fs' 4 | import { RestaurantAdminModel, Restaurant } from "../../src/restaurant" 5 | import { PubSub } from '@google-cloud/pubsub' 6 | import { RankingAdminModel } from "../../src/ranking" 7 | 8 | // FunctionsのエミュレータのprojectIdは.firebasercで定義されているものが使われる 9 | // 本物のFirebaseプロジェクトのprojectIdと一致させないとFirestoreトリガーのFunctionsがエミュレータで発火されない 10 | // TODO: 可能であればfirebase useした現在のprojectIdを自動的に取得したい 11 | const projectId = `testing-firebase-test` 12 | firebase.loadFirestoreRules({ 13 | projectId, 14 | rules: readFileSync('firestore.rules', 'utf8') 15 | }) 16 | 17 | const dummyRestaurants: Restaurant[] = [ 18 | { 19 | rateAvg: 3.5, 20 | rateNum: 3, 21 | name: 'Super buger', 22 | }, 23 | { 24 | rateAvg: 4.1, 25 | rateNum: 10, 26 | name: 'Ramen nihon ichi', 27 | }, 28 | { 29 | rateAvg: 2.8, 30 | rateNum: 7, 31 | name: 'Fire stake', 32 | }, 33 | ] 34 | 35 | const expectRankings = dummyRestaurants 36 | .sort((a, b) => b.rateAvg - a.rateAvg) 37 | .map((restaurant, index) => { 38 | return { 39 | rateAvg: restaurant.rateAvg, 40 | restaurantName: restaurant.name, 41 | rank: index + 1, 42 | } 43 | }) 44 | 45 | // user SDKとadmin SDKのFirestoreは型レベルでは別物だが、initializeAdminAppはuser SDKのFirestoreを返すので無理やりキャスト 46 | const adminFirestore = firebase.initializeAdminApp({ 47 | projectId 48 | }).firestore() as unknown as admin_firestore.Firestore 49 | const restaurantModel = new RestaurantAdminModel(adminFirestore) 50 | const rankingModel = new RankingAdminModel(adminFirestore) 51 | 52 | describe('cronRestaurantRanking', () => { 53 | const pubsub = new PubSub() 54 | let unsubscribe: () => void 55 | 56 | beforeAll(async () => { 57 | // エミュレータが起動され続けている場合に前のデータが残っている可能性あるため 58 | await firebase.clearFirestoreData({ 59 | projectId 60 | }) 61 | 62 | // ダミーのレストランを追加 63 | for (const restaurant of dummyRestaurants) { 64 | await restaurantModel.collectionRef().add(restaurant) 65 | } 66 | }) 67 | 68 | beforeEach(async () => { 69 | }) 70 | 71 | afterAll(async () => { 72 | // firebaseとのlistnerを削除しないとテストが終了できない 73 | await Promise.all(firebase.apps().map(app => app.delete())) 74 | }) 75 | 76 | afterEach(async () => { 77 | // Firestoreのデータを初期化 78 | await firebase.clearFirestoreData({ 79 | projectId 80 | }) 81 | 82 | // snapshotのsubscribeを解除 83 | if (unsubscribe) { 84 | unsubscribe() 85 | } 86 | }) 87 | 88 | test('trigger', async () => { 89 | await pubsub.topic('cron-restaurant-ranking').publishJSON({}) 90 | 91 | // functions側の処理が完了するまで待つため、 92 | // rankがmaxになったsnapshotを観測するまで待つ 93 | await new Promise((resolve) => { 94 | unsubscribe = rankingModel.collectionRef().where('rank', '==', dummyRestaurants.length).onSnapshot((snap) => { 95 | snap.docChanges().forEach((change) => { 96 | if (change.type === 'added') resolve() 97 | }) 98 | }) 99 | }) 100 | 101 | // /rankings のdocをrank順に取得し、比較用に配列に詰め直す 102 | const rankings: typeof expectRankings = [] 103 | const snapshot = await rankingModel.getAllOrderByRank() 104 | snapshot.forEach((doc) => { 105 | const data = doc.data() 106 | rankings.push({ 107 | rateAvg: data.rateAvg, 108 | restaurantName: data.restaurantName, 109 | rank: data.rank 110 | }) 111 | }) 112 | 113 | expect(rankings).toEqual(expectRankings) 114 | }) 115 | }) 116 | -------------------------------------------------------------------------------- /__tests__/functions/update_restaurant_rate.ts: -------------------------------------------------------------------------------- 1 | import { firestore as admin_firestore } from "firebase-admin" 2 | import * as firebase from '@firebase/testing' 3 | import { readFileSync } from 'fs' 4 | import { RestaurantAdminModel } from "../../src/restaurant" 5 | import { ReviewAdminModel } from "../../src/review" 6 | 7 | // FunctionsのエミュレータのprojectIdは.firebasercで定義されているものが使われる 8 | // 本物のFirebaseプロジェクトのprojectIdと一致させないとFirestoreトリガーのFunctionsがエミュレータで発火されない 9 | // TODO: 可能であればfirebase useした現在のprojectIdを自動的に取得したい 10 | const projectId = `testing-firebase-test` 11 | firebase.loadFirestoreRules({ 12 | projectId, 13 | rules: readFileSync('firestore.rules', 'utf8') 14 | }) 15 | 16 | const userIds = ['test-user1', 'test-user2'] 17 | 18 | const restaurantNames = [ 19 | 'Super burger', 20 | 'Ramen nihon ichi', 21 | 'Fire stake' 22 | ] 23 | 24 | // user SDKとadmin SDKのFirestoreは型レベルでは別物だが、initializeAdminAppはuser SDKのFirestoreを返すので無理やりキャスト 25 | const adminFirestore = firebase.initializeAdminApp({ 26 | projectId 27 | }).firestore() as unknown as admin_firestore.Firestore 28 | const restaurantModel = new RestaurantAdminModel(adminFirestore) 29 | const reviewModel = new ReviewAdminModel(adminFirestore) 30 | 31 | describe('reviews', () => { 32 | let restaurantIds: string[] = [] 33 | let restaurantId: string 34 | beforeAll(async () => { 35 | // エミュレータが起動され続けている場合に前のデータが残っている可能性あるため 36 | await firebase.clearFirestoreData({ 37 | projectId 38 | }) 39 | 40 | // ダミーのレストランを追加 41 | for (const name of restaurantNames) { 42 | const docRef = await restaurantModel.add(name) 43 | restaurantIds.push(docRef.id) 44 | } 45 | restaurantId = restaurantIds[0] 46 | }) 47 | 48 | beforeEach(async () => { 49 | }) 50 | 51 | afterAll(async () => { 52 | // firebaseとのlistnerを削除しないとテストが終了できない 53 | await Promise.all(firebase.apps().map(app => app.delete())) 54 | }) 55 | 56 | afterEach(async () => { 57 | // Firestoreのデータを初期化 58 | await firebase.clearFirestoreData({ 59 | projectId 60 | }) 61 | }) 62 | 63 | describe('when create reviews', () => { 64 | let unsubscribe: () => void 65 | // snapshotのsubscribeを解除 66 | afterEach(async () => { 67 | if (unsubscribe) { 68 | unsubscribe() 69 | } 70 | }) 71 | 72 | test('functions calculate rate avg', async () => { 73 | const rates = [3, 5] 74 | reviewModel.set(restaurantId, { 75 | rate: rates[0], 76 | text: 'rating three', 77 | userId: userIds[0] 78 | }) 79 | reviewModel.set(restaurantId, { 80 | rate: rates[1], 81 | text: 'rating five', 82 | userId: userIds[1] 83 | }) 84 | 85 | const expectRateAvg = (rates[0] + rates[1]) / rates.length 86 | const expectRateNum = rates.length 87 | await new Promise((resolve => { 88 | unsubscribe = restaurantModel.collectionRef().doc(restaurantId).onSnapshot((snap) => { 89 | const data = snap.data()! 90 | // functionsが正しく動作すればrateNumが2, rateAvgが4になるはず 91 | // functionsは2回実行されるはずなので、そのうち1回でも望んだ結果が来ればOK 92 | if (data.rateNum === expectRateNum) { 93 | expect(data.rateAvg).toEqual(expectRateAvg) 94 | resolve() 95 | } 96 | }) 97 | })) 98 | }) 99 | }) 100 | }) -------------------------------------------------------------------------------- /firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "firestore": { 3 | "rules": "firestore.rules", 4 | "indexes": "firestore.indexes.json" 5 | }, 6 | "emulators": { 7 | "firestore": { 8 | "port": "8080" 9 | }, 10 | "functions": { 11 | "port": 5001 12 | }, 13 | "pubsub": { 14 | "port": 8085 15 | } 16 | }, 17 | "functions": { 18 | "predeploy": "npm --prefix \"$RESOURCE_DIR\" run build", 19 | "source": "functions" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /firestore.rules: -------------------------------------------------------------------------------- 1 | rules_version = '2'; 2 | service cloud.firestore { 3 | // Anonymasユーザーではない 4 | function isAuthUser() { 5 | return request.auth != null; 6 | } 7 | // リクエストしているuidと同一か 8 | function isAuthor(uid) { 9 | return uid == request.auth.uid; 10 | } 11 | 12 | match /databases/{database}/documents { 13 | match /restaurants/{restaurantId} { 14 | allow read: if isAuthUser(); 15 | allow write: if false; 16 | 17 | match /reviews/{uid} { 18 | allow read: if isAuthUser(); 19 | allow create: if isAuthUser() && 20 | isAuthor(uid) && 21 | request.resource.data.rate >= 1 && 22 | request.resource.data.rate <= 5 && 23 | request.resource.data.updatedAt == request.time; 24 | } 25 | } 26 | match /rankings/{id} { 27 | allow read: if isAuthUser(); 28 | allow write: if false; 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /functions/.gitignore: -------------------------------------------------------------------------------- 1 | ## Compiled JavaScript files 2 | **/*.js 3 | **/*.js.map 4 | 5 | # Typescript v1 declaration files 6 | typings/ 7 | 8 | node_modules/ -------------------------------------------------------------------------------- /functions/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "functions", 3 | "requires": true, 4 | "lockfileVersion": 1, 5 | "dependencies": { 6 | "@firebase/app-types": { 7 | "version": "0.4.7", 8 | "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.4.7.tgz", 9 | "integrity": "sha512-4LnhDYsUhgxMBnCfQtWvrmMy9XxeZo059HiRbpt3ufdpUcZZOBDOouQdjkODwHLhcnNrB7LeyiqYpS2jrLT8Mw==" 10 | }, 11 | "@firebase/database": { 12 | "version": "0.5.12", 13 | "resolved": "https://registry.npmjs.org/@firebase/database/-/database-0.5.12.tgz", 14 | "integrity": "sha512-DIljaH+XspE6hRHErAkXAKLDiNIJkdmz0QaAWxSxqIumXgNDcxP8jEoJUXtuztWIqAhgD8z8q9/eNyYpM/p3nA==", 15 | "requires": { 16 | "@firebase/database-types": "0.4.7", 17 | "@firebase/logger": "0.1.30", 18 | "@firebase/util": "0.2.33", 19 | "faye-websocket": "0.11.3", 20 | "tslib": "1.10.0" 21 | } 22 | }, 23 | "@firebase/database-types": { 24 | "version": "0.4.7", 25 | "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-0.4.7.tgz", 26 | "integrity": "sha512-7UHZ0n6aj3sR5W4HsU18dysHMSIS6348xWTMypoA0G4mORaQSuleCSL6zJLaCosarDEojnncy06yW69fyFxZtA==", 27 | "requires": { 28 | "@firebase/app-types": "0.4.7" 29 | } 30 | }, 31 | "@firebase/logger": { 32 | "version": "0.1.30", 33 | "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.1.30.tgz", 34 | "integrity": "sha512-smlUDMiOLZD7kgBzcdSbICCvDblglq0X3NUcvV450kjZxOOPY1jPK54Qd/m0qbKrRlHWwr83reJGcPQDVBXd+A==" 35 | }, 36 | "@firebase/util": { 37 | "version": "0.2.33", 38 | "resolved": "https://registry.npmjs.org/@firebase/util/-/util-0.2.33.tgz", 39 | "integrity": "sha512-Z4gUew2wtPhLU9SKrHL8c3H66+MjY2JKtrlGLNdIVJGgR4i7AoNPnPdi13mpTXPKVuHe9ANDq/O04GfY89WXug==", 40 | "requires": { 41 | "tslib": "1.10.0" 42 | } 43 | }, 44 | "@google-cloud/common": { 45 | "version": "2.2.3", 46 | "resolved": "https://registry.npmjs.org/@google-cloud/common/-/common-2.2.3.tgz", 47 | "integrity": "sha512-lvw54mGKn8VqVIy2NzAk0l5fntBFX4UwQhHk6HaqkyCQ7WBl5oz4XhzKMtMilozF/3ObPcDogqwuyEWyZ6rnQQ==", 48 | "optional": true, 49 | "requires": { 50 | "@google-cloud/projectify": "^1.0.0", 51 | "@google-cloud/promisify": "^1.0.0", 52 | "arrify": "^2.0.0", 53 | "duplexify": "^3.6.0", 54 | "ent": "^2.2.0", 55 | "extend": "^3.0.2", 56 | "google-auth-library": "^5.5.0", 57 | "retry-request": "^4.0.0", 58 | "teeny-request": "^5.2.1" 59 | } 60 | }, 61 | "@google-cloud/firestore": { 62 | "version": "2.6.0", 63 | "resolved": "https://registry.npmjs.org/@google-cloud/firestore/-/firestore-2.6.0.tgz", 64 | "integrity": "sha512-5bpC7KZA+dCc+4Byp9yA7uvmM1kmVaXm6QiSQbf2Zz/rWftTr0N23f+5BKe9OXyY/nT44l2ygZjmP4Aw3ngLFg==", 65 | "optional": true, 66 | "requires": { 67 | "bun": "^0.0.12", 68 | "deep-equal": "^1.0.1", 69 | "functional-red-black-tree": "^1.0.1", 70 | "google-gax": "^1.7.5", 71 | "through2": "^3.0.0" 72 | } 73 | }, 74 | "@google-cloud/paginator": { 75 | "version": "2.0.2", 76 | "resolved": "https://registry.npmjs.org/@google-cloud/paginator/-/paginator-2.0.2.tgz", 77 | "integrity": "sha512-PCddVtZWvw0iZ3BLIsCXMBQvxUcS9O5CgfHBu8Zd8T3DCiML+oQED1odsbl3CQ9d3RrvBaj+eIh7Dv12D15PbA==", 78 | "optional": true, 79 | "requires": { 80 | "arrify": "^2.0.0", 81 | "extend": "^3.0.2" 82 | } 83 | }, 84 | "@google-cloud/projectify": { 85 | "version": "1.0.1", 86 | "resolved": "https://registry.npmjs.org/@google-cloud/projectify/-/projectify-1.0.1.tgz", 87 | "integrity": "sha512-xknDOmsMgOYHksKc1GPbwDLsdej8aRNIA17SlSZgQdyrcC0lx0OGo4VZgYfwoEU1YS8oUxF9Y+6EzDOb0eB7Xg==", 88 | "optional": true 89 | }, 90 | "@google-cloud/promisify": { 91 | "version": "1.0.3", 92 | "resolved": "https://registry.npmjs.org/@google-cloud/promisify/-/promisify-1.0.3.tgz", 93 | "integrity": "sha512-Rufgfl3TnkIil3CjsH33Q6093zeoVqyqCdvtvgHuCqRJxCZYfaVPIyr8JViMeLTD4Ja630pRKKZVSjKggoVbNg==", 94 | "optional": true 95 | }, 96 | "@google-cloud/storage": { 97 | "version": "3.5.0", 98 | "resolved": "https://registry.npmjs.org/@google-cloud/storage/-/storage-3.5.0.tgz", 99 | "integrity": "sha512-QxJ/zft4Kxbedpu7MQ5ZsNeS5WbonB7H28T32R4hQO2ply/j6n7bXmd5Vz0kzJu/iub20sK/ibgxYoxrgZD6CQ==", 100 | "optional": true, 101 | "requires": { 102 | "@google-cloud/common": "^2.1.1", 103 | "@google-cloud/paginator": "^2.0.0", 104 | "@google-cloud/promisify": "^1.0.0", 105 | "arrify": "^2.0.0", 106 | "compressible": "^2.0.12", 107 | "concat-stream": "^2.0.0", 108 | "date-and-time": "^0.10.0", 109 | "duplexify": "^3.5.0", 110 | "extend": "^3.0.2", 111 | "gaxios": "^2.0.1", 112 | "gcs-resumable-upload": "^2.2.4", 113 | "hash-stream-validation": "^0.2.1", 114 | "mime": "^2.2.0", 115 | "mime-types": "^2.0.8", 116 | "onetime": "^5.1.0", 117 | "p-limit": "^2.2.0", 118 | "pumpify": "^2.0.0", 119 | "readable-stream": "^3.4.0", 120 | "snakeize": "^0.1.0", 121 | "stream-events": "^1.0.1", 122 | "through2": "^3.0.0", 123 | "xdg-basedir": "^4.0.0" 124 | }, 125 | "dependencies": { 126 | "readable-stream": { 127 | "version": "3.4.0", 128 | "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.4.0.tgz", 129 | "integrity": "sha512-jItXPLmrSR8jmTRmRWJXCnGJsfy85mB3Wd/uINMXA65yrnFo0cPClFIUWzo2najVNSl+mx7/4W8ttlLWJe99pQ==", 130 | "optional": true, 131 | "requires": { 132 | "inherits": "^2.0.3", 133 | "string_decoder": "^1.1.1", 134 | "util-deprecate": "^1.0.1" 135 | } 136 | }, 137 | "string_decoder": { 138 | "version": "1.3.0", 139 | "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", 140 | "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", 141 | "optional": true, 142 | "requires": { 143 | "safe-buffer": "~5.2.0" 144 | } 145 | } 146 | } 147 | }, 148 | "@grpc/grpc-js": { 149 | "version": "0.6.9", 150 | "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-0.6.9.tgz", 151 | "integrity": "sha512-r1nDOEEiYmAsVYBaS4DPPqdwPOXPw7YhVOnnpPdWhlNtKbYzPash6DqWTTza9gBiYMA5d2Wiq6HzrPqsRaP4yA==", 152 | "optional": true, 153 | "requires": { 154 | "semver": "^6.2.0" 155 | } 156 | }, 157 | "@grpc/proto-loader": { 158 | "version": "0.5.3", 159 | "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.5.3.tgz", 160 | "integrity": "sha512-8qvUtGg77G2ZT2HqdqYoM/OY97gQd/0crSG34xNmZ4ZOsv3aQT/FQV9QfZPazTGna6MIoyUd+u6AxsoZjJ/VMQ==", 161 | "optional": true, 162 | "requires": { 163 | "lodash.camelcase": "^4.3.0", 164 | "protobufjs": "^6.8.6" 165 | } 166 | }, 167 | "@protobufjs/aspromise": { 168 | "version": "1.1.2", 169 | "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", 170 | "integrity": "sha1-m4sMxmPWaafY9vXQiToU00jzD78=", 171 | "optional": true 172 | }, 173 | "@protobufjs/base64": { 174 | "version": "1.1.2", 175 | "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", 176 | "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", 177 | "optional": true 178 | }, 179 | "@protobufjs/codegen": { 180 | "version": "2.0.4", 181 | "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", 182 | "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", 183 | "optional": true 184 | }, 185 | "@protobufjs/eventemitter": { 186 | "version": "1.1.0", 187 | "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", 188 | "integrity": "sha1-NVy8mLr61ZePntCV85diHx0Ga3A=", 189 | "optional": true 190 | }, 191 | "@protobufjs/fetch": { 192 | "version": "1.1.0", 193 | "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", 194 | "integrity": "sha1-upn7WYYUr2VwDBYZ/wbUVLDYTEU=", 195 | "optional": true, 196 | "requires": { 197 | "@protobufjs/aspromise": "^1.1.1", 198 | "@protobufjs/inquire": "^1.1.0" 199 | } 200 | }, 201 | "@protobufjs/float": { 202 | "version": "1.0.2", 203 | "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", 204 | "integrity": "sha1-Xp4avctz/Ap8uLKR33jIy9l7h9E=", 205 | "optional": true 206 | }, 207 | "@protobufjs/inquire": { 208 | "version": "1.1.0", 209 | "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", 210 | "integrity": "sha1-/yAOPnzyQp4tyvwRQIKOjMY48Ik=", 211 | "optional": true 212 | }, 213 | "@protobufjs/path": { 214 | "version": "1.1.2", 215 | "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", 216 | "integrity": "sha1-bMKyDFya1q0NzP0hynZz2Nf79o0=", 217 | "optional": true 218 | }, 219 | "@protobufjs/pool": { 220 | "version": "1.1.0", 221 | "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", 222 | "integrity": "sha1-Cf0V8tbTq/qbZbw2ZQbWrXhG/1Q=", 223 | "optional": true 224 | }, 225 | "@protobufjs/utf8": { 226 | "version": "1.1.0", 227 | "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", 228 | "integrity": "sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA=", 229 | "optional": true 230 | }, 231 | "@types/body-parser": { 232 | "version": "1.17.1", 233 | "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.17.1.tgz", 234 | "integrity": "sha512-RoX2EZjMiFMjZh9lmYrwgoP9RTpAjSHiJxdp4oidAQVO02T7HER3xj9UKue5534ULWeqVEkujhWcyvUce+d68w==", 235 | "requires": { 236 | "@types/connect": "*", 237 | "@types/node": "*" 238 | } 239 | }, 240 | "@types/connect": { 241 | "version": "3.4.32", 242 | "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.32.tgz", 243 | "integrity": "sha512-4r8qa0quOvh7lGD0pre62CAb1oni1OO6ecJLGCezTmhQ8Fz50Arx9RUszryR8KlgK6avuSXvviL6yWyViQABOg==", 244 | "requires": { 245 | "@types/node": "*" 246 | } 247 | }, 248 | "@types/express": { 249 | "version": "4.17.2", 250 | "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.2.tgz", 251 | "integrity": "sha512-5mHFNyavtLoJmnusB8OKJ5bshSzw+qkMIBAobLrIM48HJvunFva9mOa6aBwh64lBFyNwBbs0xiEFuj4eU/NjCA==", 252 | "requires": { 253 | "@types/body-parser": "*", 254 | "@types/express-serve-static-core": "*", 255 | "@types/serve-static": "*" 256 | } 257 | }, 258 | "@types/express-serve-static-core": { 259 | "version": "4.16.11", 260 | "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.16.11.tgz", 261 | "integrity": "sha512-K8d2M5t3tBQimkyaYTXxtHYyoJPUEhy2/omVRnTAKw5FEdT+Ft6lTaTOpoJdHeG+mIwQXXtqiTcYZ6IR8LTzjQ==", 262 | "requires": { 263 | "@types/node": "*", 264 | "@types/range-parser": "*" 265 | } 266 | }, 267 | "@types/lodash": { 268 | "version": "4.14.147", 269 | "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.147.tgz", 270 | "integrity": "sha512-+0zY1Axql6ru5T85Rh6aR6Zr0xT7c0USLqsHv01b3ID//dOrV3OvpXJGjm69+4QpxoZ0oMNEfmW1ltwXe2WVzQ==", 271 | "dev": true 272 | }, 273 | "@types/long": { 274 | "version": "4.0.0", 275 | "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.0.tgz", 276 | "integrity": "sha512-1w52Nyx4Gq47uuu0EVcsHBxZFJgurQ+rTKS3qMHxR1GY2T8c2AJYd6vZoZ9q1rupaDjU0yT+Jc2XTyXkjeMA+Q==", 277 | "optional": true 278 | }, 279 | "@types/mime": { 280 | "version": "2.0.1", 281 | "resolved": "https://registry.npmjs.org/@types/mime/-/mime-2.0.1.tgz", 282 | "integrity": "sha512-FwI9gX75FgVBJ7ywgnq/P7tw+/o1GUbtP0KzbtusLigAOgIgNISRK0ZPl4qertvXSIE8YbsVJueQ90cDt9YYyw==" 283 | }, 284 | "@types/node": { 285 | "version": "8.10.59", 286 | "resolved": "https://registry.npmjs.org/@types/node/-/node-8.10.59.tgz", 287 | "integrity": "sha512-8RkBivJrDCyPpBXhVZcjh7cQxVBSmRk9QM7hOketZzp6Tg79c0N8kkpAIito9bnJ3HCVCHVYz+KHTEbfQNfeVQ==" 288 | }, 289 | "@types/range-parser": { 290 | "version": "1.2.3", 291 | "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.3.tgz", 292 | "integrity": "sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA==" 293 | }, 294 | "@types/serve-static": { 295 | "version": "1.13.3", 296 | "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.3.tgz", 297 | "integrity": "sha512-oprSwp094zOglVrXdlo/4bAHtKTAxX6VT8FOZlBKrmyLbNvE1zxZyJ6yikMVtHIvwP45+ZQGJn+FdXGKTozq0g==", 298 | "requires": { 299 | "@types/express-serve-static-core": "*", 300 | "@types/mime": "*" 301 | } 302 | }, 303 | "abort-controller": { 304 | "version": "3.0.0", 305 | "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", 306 | "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", 307 | "optional": true, 308 | "requires": { 309 | "event-target-shim": "^5.0.0" 310 | } 311 | }, 312 | "accepts": { 313 | "version": "1.3.7", 314 | "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", 315 | "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==", 316 | "requires": { 317 | "mime-types": "~2.1.24", 318 | "negotiator": "0.6.2" 319 | } 320 | }, 321 | "agent-base": { 322 | "version": "4.3.0", 323 | "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.3.0.tgz", 324 | "integrity": "sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg==", 325 | "optional": true, 326 | "requires": { 327 | "es6-promisify": "^5.0.0" 328 | } 329 | }, 330 | "array-flatten": { 331 | "version": "1.1.1", 332 | "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", 333 | "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" 334 | }, 335 | "arrify": { 336 | "version": "2.0.1", 337 | "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz", 338 | "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==", 339 | "optional": true 340 | }, 341 | "base64-js": { 342 | "version": "1.3.1", 343 | "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz", 344 | "integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==", 345 | "optional": true 346 | }, 347 | "bignumber.js": { 348 | "version": "7.2.1", 349 | "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-7.2.1.tgz", 350 | "integrity": "sha512-S4XzBk5sMB+Rcb/LNcpzXr57VRTxgAvaAEDAl1AwRx27j00hT84O6OkteE7u8UB3NuaaygCRrEpqox4uDOrbdQ==", 351 | "optional": true 352 | }, 353 | "body-parser": { 354 | "version": "1.19.0", 355 | "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", 356 | "integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==", 357 | "requires": { 358 | "bytes": "3.1.0", 359 | "content-type": "~1.0.4", 360 | "debug": "2.6.9", 361 | "depd": "~1.1.2", 362 | "http-errors": "1.7.2", 363 | "iconv-lite": "0.4.24", 364 | "on-finished": "~2.3.0", 365 | "qs": "6.7.0", 366 | "raw-body": "2.4.0", 367 | "type-is": "~1.6.17" 368 | }, 369 | "dependencies": { 370 | "debug": { 371 | "version": "2.6.9", 372 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", 373 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", 374 | "requires": { 375 | "ms": "2.0.0" 376 | } 377 | }, 378 | "ms": { 379 | "version": "2.0.0", 380 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 381 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" 382 | } 383 | } 384 | }, 385 | "buffer-equal-constant-time": { 386 | "version": "1.0.1", 387 | "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", 388 | "integrity": "sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk=" 389 | }, 390 | "buffer-from": { 391 | "version": "1.1.1", 392 | "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", 393 | "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", 394 | "optional": true 395 | }, 396 | "bun": { 397 | "version": "0.0.12", 398 | "resolved": "https://registry.npmjs.org/bun/-/bun-0.0.12.tgz", 399 | "integrity": "sha512-Toms18J9DqnT+IfWkwxVTB2EaBprHvjlMWrTIsfX4xbu3ZBqVBwrERU0em1IgtRe04wT+wJxMlKHZok24hrcSQ==", 400 | "optional": true, 401 | "requires": { 402 | "readable-stream": "~1.0.32" 403 | } 404 | }, 405 | "bytes": { 406 | "version": "3.1.0", 407 | "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", 408 | "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==" 409 | }, 410 | "compressible": { 411 | "version": "2.0.17", 412 | "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.17.tgz", 413 | "integrity": "sha512-BGHeLCK1GV7j1bSmQQAi26X+GgWcTjLr/0tzSvMCl3LH1w1IJ4PFSPoV5316b30cneTziC+B1a+3OjoSUcQYmw==", 414 | "optional": true, 415 | "requires": { 416 | "mime-db": ">= 1.40.0 < 2" 417 | } 418 | }, 419 | "concat-stream": { 420 | "version": "2.0.0", 421 | "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", 422 | "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", 423 | "optional": true, 424 | "requires": { 425 | "buffer-from": "^1.0.0", 426 | "inherits": "^2.0.3", 427 | "readable-stream": "^3.0.2", 428 | "typedarray": "^0.0.6" 429 | }, 430 | "dependencies": { 431 | "readable-stream": { 432 | "version": "3.4.0", 433 | "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.4.0.tgz", 434 | "integrity": "sha512-jItXPLmrSR8jmTRmRWJXCnGJsfy85mB3Wd/uINMXA65yrnFo0cPClFIUWzo2najVNSl+mx7/4W8ttlLWJe99pQ==", 435 | "optional": true, 436 | "requires": { 437 | "inherits": "^2.0.3", 438 | "string_decoder": "^1.1.1", 439 | "util-deprecate": "^1.0.1" 440 | } 441 | }, 442 | "string_decoder": { 443 | "version": "1.3.0", 444 | "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", 445 | "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", 446 | "optional": true, 447 | "requires": { 448 | "safe-buffer": "~5.2.0" 449 | } 450 | } 451 | } 452 | }, 453 | "configstore": { 454 | "version": "5.0.0", 455 | "resolved": "https://registry.npmjs.org/configstore/-/configstore-5.0.0.tgz", 456 | "integrity": "sha512-eE/hvMs7qw7DlcB5JPRnthmrITuHMmACUJAp89v6PT6iOqzoLS7HRWhBtuHMlhNHo2AhUSA/3Dh1bKNJHcublQ==", 457 | "optional": true, 458 | "requires": { 459 | "dot-prop": "^5.1.0", 460 | "graceful-fs": "^4.1.2", 461 | "make-dir": "^3.0.0", 462 | "unique-string": "^2.0.0", 463 | "write-file-atomic": "^3.0.0", 464 | "xdg-basedir": "^4.0.0" 465 | } 466 | }, 467 | "content-disposition": { 468 | "version": "0.5.3", 469 | "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", 470 | "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==", 471 | "requires": { 472 | "safe-buffer": "5.1.2" 473 | }, 474 | "dependencies": { 475 | "safe-buffer": { 476 | "version": "5.1.2", 477 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", 478 | "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" 479 | } 480 | } 481 | }, 482 | "content-type": { 483 | "version": "1.0.4", 484 | "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", 485 | "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" 486 | }, 487 | "cookie": { 488 | "version": "0.4.0", 489 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", 490 | "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==" 491 | }, 492 | "cookie-signature": { 493 | "version": "1.0.6", 494 | "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", 495 | "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" 496 | }, 497 | "core-util-is": { 498 | "version": "1.0.2", 499 | "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", 500 | "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", 501 | "optional": true 502 | }, 503 | "cors": { 504 | "version": "2.8.5", 505 | "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", 506 | "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", 507 | "requires": { 508 | "object-assign": "^4", 509 | "vary": "^1" 510 | } 511 | }, 512 | "crypto-random-string": { 513 | "version": "2.0.0", 514 | "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", 515 | "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==", 516 | "optional": true 517 | }, 518 | "date-and-time": { 519 | "version": "0.10.0", 520 | "resolved": "https://registry.npmjs.org/date-and-time/-/date-and-time-0.10.0.tgz", 521 | "integrity": "sha512-IbIzxtvK80JZOVsWF6+NOjunTaoFVYxkAQoyzmflJyuRCJAJebehy48mPiCAedcGp4P7/UO3QYRWa0fe6INftg==", 522 | "optional": true 523 | }, 524 | "debug": { 525 | "version": "3.2.6", 526 | "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", 527 | "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", 528 | "optional": true, 529 | "requires": { 530 | "ms": "^2.1.1" 531 | } 532 | }, 533 | "deep-equal": { 534 | "version": "1.1.1", 535 | "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.1.1.tgz", 536 | "integrity": "sha512-yd9c5AdiqVcR+JjcwUQb9DkhJc8ngNr0MahEBGvDiJw8puWab2yZlh+nkasOnZP+EGTAP6rRp2JzJhJZzvNF8g==", 537 | "optional": true, 538 | "requires": { 539 | "is-arguments": "^1.0.4", 540 | "is-date-object": "^1.0.1", 541 | "is-regex": "^1.0.4", 542 | "object-is": "^1.0.1", 543 | "object-keys": "^1.1.1", 544 | "regexp.prototype.flags": "^1.2.0" 545 | } 546 | }, 547 | "define-properties": { 548 | "version": "1.1.3", 549 | "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", 550 | "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", 551 | "optional": true, 552 | "requires": { 553 | "object-keys": "^1.0.12" 554 | } 555 | }, 556 | "depd": { 557 | "version": "1.1.2", 558 | "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", 559 | "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" 560 | }, 561 | "destroy": { 562 | "version": "1.0.4", 563 | "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", 564 | "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" 565 | }, 566 | "dicer": { 567 | "version": "0.3.0", 568 | "resolved": "https://registry.npmjs.org/dicer/-/dicer-0.3.0.tgz", 569 | "integrity": "sha512-MdceRRWqltEG2dZqO769g27N/3PXfcKl04VhYnBlo2YhH7zPi88VebsjTKclaOyiuMaGU72hTfw3VkUitGcVCA==", 570 | "requires": { 571 | "streamsearch": "0.1.2" 572 | } 573 | }, 574 | "dot-prop": { 575 | "version": "5.2.0", 576 | "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.2.0.tgz", 577 | "integrity": "sha512-uEUyaDKoSQ1M4Oq8l45hSE26SnTxL6snNnqvK/VWx5wJhmff5z0FUVJDKDanor/6w3kzE3i7XZOk+7wC0EXr1A==", 578 | "optional": true, 579 | "requires": { 580 | "is-obj": "^2.0.0" 581 | } 582 | }, 583 | "duplexify": { 584 | "version": "3.7.1", 585 | "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz", 586 | "integrity": "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==", 587 | "optional": true, 588 | "requires": { 589 | "end-of-stream": "^1.0.0", 590 | "inherits": "^2.0.1", 591 | "readable-stream": "^2.0.0", 592 | "stream-shift": "^1.0.0" 593 | }, 594 | "dependencies": { 595 | "isarray": { 596 | "version": "1.0.0", 597 | "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", 598 | "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", 599 | "optional": true 600 | }, 601 | "readable-stream": { 602 | "version": "2.3.6", 603 | "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", 604 | "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", 605 | "optional": true, 606 | "requires": { 607 | "core-util-is": "~1.0.0", 608 | "inherits": "~2.0.3", 609 | "isarray": "~1.0.0", 610 | "process-nextick-args": "~2.0.0", 611 | "safe-buffer": "~5.1.1", 612 | "string_decoder": "~1.1.1", 613 | "util-deprecate": "~1.0.1" 614 | } 615 | }, 616 | "safe-buffer": { 617 | "version": "5.1.2", 618 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", 619 | "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", 620 | "optional": true 621 | }, 622 | "string_decoder": { 623 | "version": "1.1.1", 624 | "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", 625 | "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", 626 | "optional": true, 627 | "requires": { 628 | "safe-buffer": "~5.1.0" 629 | } 630 | } 631 | } 632 | }, 633 | "ecdsa-sig-formatter": { 634 | "version": "1.0.11", 635 | "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", 636 | "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", 637 | "requires": { 638 | "safe-buffer": "^5.0.1" 639 | } 640 | }, 641 | "ee-first": { 642 | "version": "1.1.1", 643 | "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", 644 | "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" 645 | }, 646 | "encodeurl": { 647 | "version": "1.0.2", 648 | "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", 649 | "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" 650 | }, 651 | "end-of-stream": { 652 | "version": "1.4.4", 653 | "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", 654 | "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", 655 | "optional": true, 656 | "requires": { 657 | "once": "^1.4.0" 658 | } 659 | }, 660 | "ent": { 661 | "version": "2.2.0", 662 | "resolved": "https://registry.npmjs.org/ent/-/ent-2.2.0.tgz", 663 | "integrity": "sha1-6WQhkyWiHQX0RGai9obtbOX13R0=", 664 | "optional": true 665 | }, 666 | "es6-promise": { 667 | "version": "4.2.8", 668 | "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", 669 | "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==", 670 | "optional": true 671 | }, 672 | "es6-promisify": { 673 | "version": "5.0.0", 674 | "resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz", 675 | "integrity": "sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM=", 676 | "optional": true, 677 | "requires": { 678 | "es6-promise": "^4.0.3" 679 | } 680 | }, 681 | "escape-html": { 682 | "version": "1.0.3", 683 | "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", 684 | "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" 685 | }, 686 | "etag": { 687 | "version": "1.8.1", 688 | "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", 689 | "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" 690 | }, 691 | "event-target-shim": { 692 | "version": "5.0.1", 693 | "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", 694 | "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", 695 | "optional": true 696 | }, 697 | "express": { 698 | "version": "4.17.1", 699 | "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz", 700 | "integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==", 701 | "requires": { 702 | "accepts": "~1.3.7", 703 | "array-flatten": "1.1.1", 704 | "body-parser": "1.19.0", 705 | "content-disposition": "0.5.3", 706 | "content-type": "~1.0.4", 707 | "cookie": "0.4.0", 708 | "cookie-signature": "1.0.6", 709 | "debug": "2.6.9", 710 | "depd": "~1.1.2", 711 | "encodeurl": "~1.0.2", 712 | "escape-html": "~1.0.3", 713 | "etag": "~1.8.1", 714 | "finalhandler": "~1.1.2", 715 | "fresh": "0.5.2", 716 | "merge-descriptors": "1.0.1", 717 | "methods": "~1.1.2", 718 | "on-finished": "~2.3.0", 719 | "parseurl": "~1.3.3", 720 | "path-to-regexp": "0.1.7", 721 | "proxy-addr": "~2.0.5", 722 | "qs": "6.7.0", 723 | "range-parser": "~1.2.1", 724 | "safe-buffer": "5.1.2", 725 | "send": "0.17.1", 726 | "serve-static": "1.14.1", 727 | "setprototypeof": "1.1.1", 728 | "statuses": "~1.5.0", 729 | "type-is": "~1.6.18", 730 | "utils-merge": "1.0.1", 731 | "vary": "~1.1.2" 732 | }, 733 | "dependencies": { 734 | "debug": { 735 | "version": "2.6.9", 736 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", 737 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", 738 | "requires": { 739 | "ms": "2.0.0" 740 | } 741 | }, 742 | "ms": { 743 | "version": "2.0.0", 744 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 745 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" 746 | }, 747 | "safe-buffer": { 748 | "version": "5.1.2", 749 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", 750 | "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" 751 | } 752 | } 753 | }, 754 | "extend": { 755 | "version": "3.0.2", 756 | "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", 757 | "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", 758 | "optional": true 759 | }, 760 | "fast-text-encoding": { 761 | "version": "1.0.0", 762 | "resolved": "https://registry.npmjs.org/fast-text-encoding/-/fast-text-encoding-1.0.0.tgz", 763 | "integrity": "sha512-R9bHCvweUxxwkDwhjav5vxpFvdPGlVngtqmx4pIZfSUhM/Q4NiIUHB456BAf+Q1Nwu3HEZYONtu+Rya+af4jiQ==", 764 | "optional": true 765 | }, 766 | "faye-websocket": { 767 | "version": "0.11.3", 768 | "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.3.tgz", 769 | "integrity": "sha512-D2y4bovYpzziGgbHYtGCMjlJM36vAl/y+xUyn1C+FVx8szd1E+86KwVw6XvYSzOP8iMpm1X0I4xJD+QtUb36OA==", 770 | "requires": { 771 | "websocket-driver": ">=0.5.1" 772 | } 773 | }, 774 | "finalhandler": { 775 | "version": "1.1.2", 776 | "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", 777 | "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", 778 | "requires": { 779 | "debug": "2.6.9", 780 | "encodeurl": "~1.0.2", 781 | "escape-html": "~1.0.3", 782 | "on-finished": "~2.3.0", 783 | "parseurl": "~1.3.3", 784 | "statuses": "~1.5.0", 785 | "unpipe": "~1.0.0" 786 | }, 787 | "dependencies": { 788 | "debug": { 789 | "version": "2.6.9", 790 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", 791 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", 792 | "requires": { 793 | "ms": "2.0.0" 794 | } 795 | }, 796 | "ms": { 797 | "version": "2.0.0", 798 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 799 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" 800 | } 801 | } 802 | }, 803 | "firebase-admin": { 804 | "version": "8.7.0", 805 | "resolved": "https://registry.npmjs.org/firebase-admin/-/firebase-admin-8.7.0.tgz", 806 | "integrity": "sha512-mAp/58ZHbZHGPlSe8JDyELOT6DAWGUv9N3pA6d+Sg5RS5H3I1xGnKkhXK9BMJsejUfA3mwOqFPMLD4yso7aFxw==", 807 | "requires": { 808 | "@firebase/database": "^0.5.1", 809 | "@google-cloud/firestore": "^2.0.0", 810 | "@google-cloud/storage": "^3.0.2", 811 | "@types/node": "^8.0.53", 812 | "dicer": "^0.3.0", 813 | "jsonwebtoken": "8.1.0", 814 | "node-forge": "0.7.4" 815 | } 816 | }, 817 | "firebase-functions": { 818 | "version": "3.3.0", 819 | "resolved": "https://registry.npmjs.org/firebase-functions/-/firebase-functions-3.3.0.tgz", 820 | "integrity": "sha512-dP6PCG+OwR6RtFpOqwPsLnfiCr3CwXAm/SVGMbO53vDAk0nhUQ1WGAyHDYmIyMAkaLJkIKGwDnX7XmZ5+yAg7g==", 821 | "requires": { 822 | "@types/express": "^4.17.0", 823 | "cors": "^2.8.5", 824 | "express": "^4.17.1", 825 | "jsonwebtoken": "^8.5.1", 826 | "lodash": "^4.17.14" 827 | }, 828 | "dependencies": { 829 | "jsonwebtoken": { 830 | "version": "8.5.1", 831 | "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz", 832 | "integrity": "sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w==", 833 | "requires": { 834 | "jws": "^3.2.2", 835 | "lodash.includes": "^4.3.0", 836 | "lodash.isboolean": "^3.0.3", 837 | "lodash.isinteger": "^4.0.4", 838 | "lodash.isnumber": "^3.0.3", 839 | "lodash.isplainobject": "^4.0.6", 840 | "lodash.isstring": "^4.0.1", 841 | "lodash.once": "^4.0.0", 842 | "ms": "^2.1.1", 843 | "semver": "^5.6.0" 844 | } 845 | }, 846 | "semver": { 847 | "version": "5.7.1", 848 | "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", 849 | "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" 850 | } 851 | } 852 | }, 853 | "firebase-functions-test": { 854 | "version": "0.1.7", 855 | "resolved": "https://registry.npmjs.org/firebase-functions-test/-/firebase-functions-test-0.1.7.tgz", 856 | "integrity": "sha512-/zVQhaUZ+M7z25aUaZSIah0MIDZIfnRfQxtHYTE8hgUgODmKdaMX20vh5Gv23hnCPauIHuYb7XFTUOZiWU1udA==", 857 | "dev": true, 858 | "requires": { 859 | "@types/lodash": "^4.14.104", 860 | "lodash": "^4.17.5" 861 | } 862 | }, 863 | "forwarded": { 864 | "version": "0.1.2", 865 | "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", 866 | "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=" 867 | }, 868 | "fresh": { 869 | "version": "0.5.2", 870 | "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", 871 | "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" 872 | }, 873 | "function-bind": { 874 | "version": "1.1.1", 875 | "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", 876 | "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", 877 | "optional": true 878 | }, 879 | "functional-red-black-tree": { 880 | "version": "1.0.1", 881 | "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", 882 | "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", 883 | "optional": true 884 | }, 885 | "gaxios": { 886 | "version": "2.1.0", 887 | "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-2.1.0.tgz", 888 | "integrity": "sha512-Gtpb5sdQmb82sgVkT2GnS2n+Kx4dlFwbeMYcDlD395aEvsLCSQXJJcHt7oJ2LrGxDEAeiOkK79Zv2A8Pzt6CFg==", 889 | "optional": true, 890 | "requires": { 891 | "abort-controller": "^3.0.0", 892 | "extend": "^3.0.2", 893 | "https-proxy-agent": "^3.0.0", 894 | "is-stream": "^2.0.0", 895 | "node-fetch": "^2.3.0" 896 | } 897 | }, 898 | "gcp-metadata": { 899 | "version": "3.2.2", 900 | "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-3.2.2.tgz", 901 | "integrity": "sha512-vR7kcJMCYJG/mYWp/a1OszdOqnLB/XW1GorWW1hc1lWVNL26L497zypWb9cG0CYDQ4Bl1Wk0+fSZFFjwJlTQgQ==", 902 | "optional": true, 903 | "requires": { 904 | "gaxios": "^2.1.0", 905 | "json-bigint": "^0.3.0" 906 | } 907 | }, 908 | "gcs-resumable-upload": { 909 | "version": "2.3.0", 910 | "resolved": "https://registry.npmjs.org/gcs-resumable-upload/-/gcs-resumable-upload-2.3.0.tgz", 911 | "integrity": "sha512-PclXJiEngrVx0c4K0LfE1XOxhmOkBEy39Rrhspdn6jAbbwe4OQMZfjo7Z1LHBrh57+bNZeIN4M+BooYppCoHSg==", 912 | "optional": true, 913 | "requires": { 914 | "abort-controller": "^3.0.0", 915 | "configstore": "^5.0.0", 916 | "gaxios": "^2.0.0", 917 | "google-auth-library": "^5.0.0", 918 | "pumpify": "^2.0.0", 919 | "stream-events": "^1.0.4" 920 | } 921 | }, 922 | "google-auth-library": { 923 | "version": "5.5.1", 924 | "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-5.5.1.tgz", 925 | "integrity": "sha512-zCtjQccWS/EHYyFdXRbfeSGM/gW+d7uMAcVnvXRnjBXON5ijo6s0nsObP0ifqileIDSbZjTlLtgo+UoN8IFJcg==", 926 | "optional": true, 927 | "requires": { 928 | "arrify": "^2.0.0", 929 | "base64-js": "^1.3.0", 930 | "fast-text-encoding": "^1.0.0", 931 | "gaxios": "^2.1.0", 932 | "gcp-metadata": "^3.2.0", 933 | "gtoken": "^4.1.0", 934 | "jws": "^3.1.5", 935 | "lru-cache": "^5.0.0" 936 | } 937 | }, 938 | "google-gax": { 939 | "version": "1.11.0", 940 | "resolved": "https://registry.npmjs.org/google-gax/-/google-gax-1.11.0.tgz", 941 | "integrity": "sha512-xdAdiMx0fvTg5f7pRLQbS7Srlu188KK6j1ur/dB7DEs3/Smpw5WBFIso0tkhkJRYhThPDnYgT3Ia+kjpWH4tRQ==", 942 | "optional": true, 943 | "requires": { 944 | "@grpc/grpc-js": "0.6.9", 945 | "@grpc/proto-loader": "^0.5.1", 946 | "@types/long": "^4.0.0", 947 | "abort-controller": "^3.0.0", 948 | "duplexify": "^3.6.0", 949 | "google-auth-library": "^5.0.0", 950 | "is-stream-ended": "^0.1.4", 951 | "lodash.at": "^4.6.0", 952 | "lodash.has": "^4.5.2", 953 | "node-fetch": "^2.6.0", 954 | "protobufjs": "^6.8.8", 955 | "retry-request": "^4.0.0", 956 | "semver": "^6.0.0", 957 | "walkdir": "^0.4.0" 958 | } 959 | }, 960 | "google-p12-pem": { 961 | "version": "2.0.3", 962 | "resolved": "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-2.0.3.tgz", 963 | "integrity": "sha512-Tq2kBCANxYYPxaBpTgCpRfdoPs9+/lNzc/Iaee4kuMVW5ascD+HwhpBsTLwH85C9Ev4qfB8KKHmpPQYyD2vg2w==", 964 | "optional": true, 965 | "requires": { 966 | "node-forge": "^0.9.0" 967 | }, 968 | "dependencies": { 969 | "node-forge": { 970 | "version": "0.9.1", 971 | "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.9.1.tgz", 972 | "integrity": "sha512-G6RlQt5Sb4GMBzXvhfkeFmbqR6MzhtnT7VTHuLadjkii3rdYHNdw0m8zA4BTxVIh68FicCQ2NSUANpsqkr9jvQ==", 973 | "optional": true 974 | } 975 | } 976 | }, 977 | "graceful-fs": { 978 | "version": "4.2.3", 979 | "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.3.tgz", 980 | "integrity": "sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ==", 981 | "optional": true 982 | }, 983 | "gtoken": { 984 | "version": "4.1.2", 985 | "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-4.1.2.tgz", 986 | "integrity": "sha512-/YvFlrrRjiSJf6a4Kmr4+E+HFbYdEntRPD/W+ZGLYDziHiWjX4S6ld8TNCyPMtUG3J3hPkZkpHbva40JA56spw==", 987 | "optional": true, 988 | "requires": { 989 | "gaxios": "^2.1.0", 990 | "google-p12-pem": "^2.0.0", 991 | "jws": "^3.1.5", 992 | "mime": "^2.2.0" 993 | } 994 | }, 995 | "has": { 996 | "version": "1.0.3", 997 | "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", 998 | "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", 999 | "optional": true, 1000 | "requires": { 1001 | "function-bind": "^1.1.1" 1002 | } 1003 | }, 1004 | "hash-stream-validation": { 1005 | "version": "0.2.2", 1006 | "resolved": "https://registry.npmjs.org/hash-stream-validation/-/hash-stream-validation-0.2.2.tgz", 1007 | "integrity": "sha512-cMlva5CxWZOrlS/cY0C+9qAzesn5srhFA8IT1VPiHc9bWWBLkJfEUIZr7MWoi89oOOGmpg8ymchaOjiArsGu5A==", 1008 | "optional": true, 1009 | "requires": { 1010 | "through2": "^2.0.0" 1011 | }, 1012 | "dependencies": { 1013 | "isarray": { 1014 | "version": "1.0.0", 1015 | "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", 1016 | "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", 1017 | "optional": true 1018 | }, 1019 | "readable-stream": { 1020 | "version": "2.3.6", 1021 | "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", 1022 | "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", 1023 | "optional": true, 1024 | "requires": { 1025 | "core-util-is": "~1.0.0", 1026 | "inherits": "~2.0.3", 1027 | "isarray": "~1.0.0", 1028 | "process-nextick-args": "~2.0.0", 1029 | "safe-buffer": "~5.1.1", 1030 | "string_decoder": "~1.1.1", 1031 | "util-deprecate": "~1.0.1" 1032 | } 1033 | }, 1034 | "safe-buffer": { 1035 | "version": "5.1.2", 1036 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", 1037 | "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", 1038 | "optional": true 1039 | }, 1040 | "string_decoder": { 1041 | "version": "1.1.1", 1042 | "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", 1043 | "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", 1044 | "optional": true, 1045 | "requires": { 1046 | "safe-buffer": "~5.1.0" 1047 | } 1048 | }, 1049 | "through2": { 1050 | "version": "2.0.5", 1051 | "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", 1052 | "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", 1053 | "optional": true, 1054 | "requires": { 1055 | "readable-stream": "~2.3.6", 1056 | "xtend": "~4.0.1" 1057 | } 1058 | } 1059 | } 1060 | }, 1061 | "http-errors": { 1062 | "version": "1.7.2", 1063 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", 1064 | "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", 1065 | "requires": { 1066 | "depd": "~1.1.2", 1067 | "inherits": "2.0.3", 1068 | "setprototypeof": "1.1.1", 1069 | "statuses": ">= 1.5.0 < 2", 1070 | "toidentifier": "1.0.0" 1071 | }, 1072 | "dependencies": { 1073 | "inherits": { 1074 | "version": "2.0.3", 1075 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", 1076 | "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" 1077 | } 1078 | } 1079 | }, 1080 | "http-parser-js": { 1081 | "version": "0.4.10", 1082 | "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.4.10.tgz", 1083 | "integrity": "sha1-ksnBN0w1CF912zWexWzCV8u5P6Q=" 1084 | }, 1085 | "http-proxy-agent": { 1086 | "version": "2.1.0", 1087 | "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-2.1.0.tgz", 1088 | "integrity": "sha512-qwHbBLV7WviBl0rQsOzH6o5lwyOIvwp/BdFnvVxXORldu5TmjFfjzBcWUWS5kWAZhmv+JtiDhSuQCp4sBfbIgg==", 1089 | "optional": true, 1090 | "requires": { 1091 | "agent-base": "4", 1092 | "debug": "3.1.0" 1093 | }, 1094 | "dependencies": { 1095 | "debug": { 1096 | "version": "3.1.0", 1097 | "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", 1098 | "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", 1099 | "optional": true, 1100 | "requires": { 1101 | "ms": "2.0.0" 1102 | } 1103 | }, 1104 | "ms": { 1105 | "version": "2.0.0", 1106 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 1107 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", 1108 | "optional": true 1109 | } 1110 | } 1111 | }, 1112 | "https-proxy-agent": { 1113 | "version": "3.0.1", 1114 | "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-3.0.1.tgz", 1115 | "integrity": "sha512-+ML2Rbh6DAuee7d07tYGEKOEi2voWPUGan+ExdPbPW6Z3svq+JCqr0v8WmKPOkz1vOVykPCBSuobe7G8GJUtVg==", 1116 | "optional": true, 1117 | "requires": { 1118 | "agent-base": "^4.3.0", 1119 | "debug": "^3.1.0" 1120 | } 1121 | }, 1122 | "iconv-lite": { 1123 | "version": "0.4.24", 1124 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", 1125 | "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", 1126 | "requires": { 1127 | "safer-buffer": ">= 2.1.2 < 3" 1128 | } 1129 | }, 1130 | "imurmurhash": { 1131 | "version": "0.1.4", 1132 | "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", 1133 | "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", 1134 | "optional": true 1135 | }, 1136 | "inherits": { 1137 | "version": "2.0.4", 1138 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 1139 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", 1140 | "optional": true 1141 | }, 1142 | "ipaddr.js": { 1143 | "version": "1.9.0", 1144 | "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.0.tgz", 1145 | "integrity": "sha512-M4Sjn6N/+O6/IXSJseKqHoFc+5FdGJ22sXqnjTpdZweHK64MzEPAyQZyEU3R/KRv2GLoa7nNtg/C2Ev6m7z+eA==" 1146 | }, 1147 | "is-arguments": { 1148 | "version": "1.0.4", 1149 | "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.0.4.tgz", 1150 | "integrity": "sha512-xPh0Rmt8NE65sNzvyUmWgI1tz3mKq74lGA0mL8LYZcoIzKOzDh6HmrYm3d18k60nHerC8A9Km8kYu87zfSFnLA==", 1151 | "optional": true 1152 | }, 1153 | "is-date-object": { 1154 | "version": "1.0.1", 1155 | "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.1.tgz", 1156 | "integrity": "sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY=", 1157 | "optional": true 1158 | }, 1159 | "is-obj": { 1160 | "version": "2.0.0", 1161 | "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", 1162 | "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", 1163 | "optional": true 1164 | }, 1165 | "is-regex": { 1166 | "version": "1.0.4", 1167 | "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.4.tgz", 1168 | "integrity": "sha1-VRdIm1RwkbCTDglWVM7SXul+lJE=", 1169 | "optional": true, 1170 | "requires": { 1171 | "has": "^1.0.1" 1172 | } 1173 | }, 1174 | "is-stream": { 1175 | "version": "2.0.0", 1176 | "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz", 1177 | "integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==", 1178 | "optional": true 1179 | }, 1180 | "is-stream-ended": { 1181 | "version": "0.1.4", 1182 | "resolved": "https://registry.npmjs.org/is-stream-ended/-/is-stream-ended-0.1.4.tgz", 1183 | "integrity": "sha512-xj0XPvmr7bQFTvirqnFr50o0hQIh6ZItDqloxt5aJrR4NQsYeSsyFQERYGCAzfindAcnKjINnwEEgLx4IqVzQw==", 1184 | "optional": true 1185 | }, 1186 | "is-typedarray": { 1187 | "version": "1.0.0", 1188 | "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", 1189 | "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", 1190 | "optional": true 1191 | }, 1192 | "isarray": { 1193 | "version": "0.0.1", 1194 | "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", 1195 | "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", 1196 | "optional": true 1197 | }, 1198 | "json-bigint": { 1199 | "version": "0.3.0", 1200 | "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-0.3.0.tgz", 1201 | "integrity": "sha1-DM2RLEuCcNBfBW+9E4FLU9OCWx4=", 1202 | "optional": true, 1203 | "requires": { 1204 | "bignumber.js": "^7.0.0" 1205 | } 1206 | }, 1207 | "jsonwebtoken": { 1208 | "version": "8.1.0", 1209 | "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-8.1.0.tgz", 1210 | "integrity": "sha1-xjl80uX9WD1lwAeoPce7eOaYK4M=", 1211 | "requires": { 1212 | "jws": "^3.1.4", 1213 | "lodash.includes": "^4.3.0", 1214 | "lodash.isboolean": "^3.0.3", 1215 | "lodash.isinteger": "^4.0.4", 1216 | "lodash.isnumber": "^3.0.3", 1217 | "lodash.isplainobject": "^4.0.6", 1218 | "lodash.isstring": "^4.0.1", 1219 | "lodash.once": "^4.0.0", 1220 | "ms": "^2.0.0", 1221 | "xtend": "^4.0.1" 1222 | } 1223 | }, 1224 | "jwa": { 1225 | "version": "1.4.1", 1226 | "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", 1227 | "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", 1228 | "requires": { 1229 | "buffer-equal-constant-time": "1.0.1", 1230 | "ecdsa-sig-formatter": "1.0.11", 1231 | "safe-buffer": "^5.0.1" 1232 | } 1233 | }, 1234 | "jws": { 1235 | "version": "3.2.2", 1236 | "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", 1237 | "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", 1238 | "requires": { 1239 | "jwa": "^1.4.1", 1240 | "safe-buffer": "^5.0.1" 1241 | } 1242 | }, 1243 | "lodash": { 1244 | "version": "4.17.15", 1245 | "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", 1246 | "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==" 1247 | }, 1248 | "lodash.at": { 1249 | "version": "4.6.0", 1250 | "resolved": "https://registry.npmjs.org/lodash.at/-/lodash.at-4.6.0.tgz", 1251 | "integrity": "sha1-k83OZk8KGZTqM9181A4jr9EbD/g=", 1252 | "optional": true 1253 | }, 1254 | "lodash.camelcase": { 1255 | "version": "4.3.0", 1256 | "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", 1257 | "integrity": "sha1-soqmKIorn8ZRA1x3EfZathkDMaY=", 1258 | "optional": true 1259 | }, 1260 | "lodash.has": { 1261 | "version": "4.5.2", 1262 | "resolved": "https://registry.npmjs.org/lodash.has/-/lodash.has-4.5.2.tgz", 1263 | "integrity": "sha1-0Z9NwQlQWMzL4rDN9O4P5Ko3yGI=", 1264 | "optional": true 1265 | }, 1266 | "lodash.includes": { 1267 | "version": "4.3.0", 1268 | "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", 1269 | "integrity": "sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8=" 1270 | }, 1271 | "lodash.isboolean": { 1272 | "version": "3.0.3", 1273 | "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", 1274 | "integrity": "sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY=" 1275 | }, 1276 | "lodash.isinteger": { 1277 | "version": "4.0.4", 1278 | "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", 1279 | "integrity": "sha1-YZwK89A/iwTDH1iChAt3sRzWg0M=" 1280 | }, 1281 | "lodash.isnumber": { 1282 | "version": "3.0.3", 1283 | "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", 1284 | "integrity": "sha1-POdoEMWSjQM1IwGsKHMX8RwLH/w=" 1285 | }, 1286 | "lodash.isplainobject": { 1287 | "version": "4.0.6", 1288 | "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", 1289 | "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=" 1290 | }, 1291 | "lodash.isstring": { 1292 | "version": "4.0.1", 1293 | "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", 1294 | "integrity": "sha1-1SfftUVuynzJu5XV2ur4i6VKVFE=" 1295 | }, 1296 | "lodash.once": { 1297 | "version": "4.1.1", 1298 | "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", 1299 | "integrity": "sha1-DdOXEhPHxW34gJd9UEyI+0cal6w=" 1300 | }, 1301 | "long": { 1302 | "version": "4.0.0", 1303 | "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", 1304 | "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==", 1305 | "optional": true 1306 | }, 1307 | "lru-cache": { 1308 | "version": "5.1.1", 1309 | "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", 1310 | "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", 1311 | "optional": true, 1312 | "requires": { 1313 | "yallist": "^3.0.2" 1314 | } 1315 | }, 1316 | "make-dir": { 1317 | "version": "3.0.0", 1318 | "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.0.0.tgz", 1319 | "integrity": "sha512-grNJDhb8b1Jm1qeqW5R/O63wUo4UXo2v2HMic6YT9i/HBlF93S8jkMgH7yugvY9ABDShH4VZMn8I+U8+fCNegw==", 1320 | "optional": true, 1321 | "requires": { 1322 | "semver": "^6.0.0" 1323 | } 1324 | }, 1325 | "media-typer": { 1326 | "version": "0.3.0", 1327 | "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", 1328 | "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" 1329 | }, 1330 | "merge-descriptors": { 1331 | "version": "1.0.1", 1332 | "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", 1333 | "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" 1334 | }, 1335 | "methods": { 1336 | "version": "1.1.2", 1337 | "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", 1338 | "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" 1339 | }, 1340 | "mime": { 1341 | "version": "2.4.4", 1342 | "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.4.tgz", 1343 | "integrity": "sha512-LRxmNwziLPT828z+4YkNzloCFC2YM4wrB99k+AV5ZbEyfGNWfG8SO1FUXLmLDBSo89NrJZ4DIWeLjy1CHGhMGA==", 1344 | "optional": true 1345 | }, 1346 | "mime-db": { 1347 | "version": "1.42.0", 1348 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.42.0.tgz", 1349 | "integrity": "sha512-UbfJCR4UAVRNgMpfImz05smAXK7+c+ZntjaA26ANtkXLlOe947Aag5zdIcKQULAiF9Cq4WxBi9jUs5zkA84bYQ==" 1350 | }, 1351 | "mime-types": { 1352 | "version": "2.1.25", 1353 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.25.tgz", 1354 | "integrity": "sha512-5KhStqB5xpTAeGqKBAMgwaYMnQik7teQN4IAzC7npDv6kzeU6prfkR67bc87J1kWMPGkoaZSq1npmexMgkmEVg==", 1355 | "requires": { 1356 | "mime-db": "1.42.0" 1357 | } 1358 | }, 1359 | "mimic-fn": { 1360 | "version": "2.1.0", 1361 | "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", 1362 | "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", 1363 | "optional": true 1364 | }, 1365 | "ms": { 1366 | "version": "2.1.2", 1367 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", 1368 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" 1369 | }, 1370 | "negotiator": { 1371 | "version": "0.6.2", 1372 | "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", 1373 | "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==" 1374 | }, 1375 | "node-fetch": { 1376 | "version": "2.6.0", 1377 | "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.0.tgz", 1378 | "integrity": "sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA==", 1379 | "optional": true 1380 | }, 1381 | "node-forge": { 1382 | "version": "0.7.4", 1383 | "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.7.4.tgz", 1384 | "integrity": "sha512-8Df0906+tq/omxuCZD6PqhPaQDYuyJ1d+VITgxoIA8zvQd1ru+nMJcDChHH324MWitIgbVkAkQoGEEVJNpn/PA==" 1385 | }, 1386 | "object-assign": { 1387 | "version": "4.1.1", 1388 | "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", 1389 | "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" 1390 | }, 1391 | "object-is": { 1392 | "version": "1.0.1", 1393 | "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.0.1.tgz", 1394 | "integrity": "sha1-CqYOyZiaCz7Xlc9NBvYs8a1lObY=", 1395 | "optional": true 1396 | }, 1397 | "object-keys": { 1398 | "version": "1.1.1", 1399 | "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", 1400 | "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", 1401 | "optional": true 1402 | }, 1403 | "on-finished": { 1404 | "version": "2.3.0", 1405 | "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", 1406 | "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", 1407 | "requires": { 1408 | "ee-first": "1.1.1" 1409 | } 1410 | }, 1411 | "once": { 1412 | "version": "1.4.0", 1413 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 1414 | "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", 1415 | "optional": true, 1416 | "requires": { 1417 | "wrappy": "1" 1418 | } 1419 | }, 1420 | "onetime": { 1421 | "version": "5.1.0", 1422 | "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.0.tgz", 1423 | "integrity": "sha512-5NcSkPHhwTVFIQN+TUqXoS5+dlElHXdpAWu9I0HP20YOtIi+aZ0Ct82jdlILDxjLEAWwvm+qj1m6aEtsDVmm6Q==", 1424 | "optional": true, 1425 | "requires": { 1426 | "mimic-fn": "^2.1.0" 1427 | } 1428 | }, 1429 | "p-limit": { 1430 | "version": "2.2.1", 1431 | "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.1.tgz", 1432 | "integrity": "sha512-85Tk+90UCVWvbDavCLKPOLC9vvY8OwEX/RtKF+/1OADJMVlFfEHOiMTPVyxg7mk/dKa+ipdHm0OUkTvCpMTuwg==", 1433 | "optional": true, 1434 | "requires": { 1435 | "p-try": "^2.0.0" 1436 | } 1437 | }, 1438 | "p-try": { 1439 | "version": "2.2.0", 1440 | "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", 1441 | "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", 1442 | "optional": true 1443 | }, 1444 | "parseurl": { 1445 | "version": "1.3.3", 1446 | "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", 1447 | "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" 1448 | }, 1449 | "path-to-regexp": { 1450 | "version": "0.1.7", 1451 | "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", 1452 | "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" 1453 | }, 1454 | "process-nextick-args": { 1455 | "version": "2.0.1", 1456 | "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", 1457 | "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", 1458 | "optional": true 1459 | }, 1460 | "protobufjs": { 1461 | "version": "6.8.8", 1462 | "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.8.8.tgz", 1463 | "integrity": "sha512-AAmHtD5pXgZfi7GMpllpO3q1Xw1OYldr+dMUlAnffGTAhqkg72WdmSY71uKBF/JuyiKs8psYbtKrhi0ASCD8qw==", 1464 | "optional": true, 1465 | "requires": { 1466 | "@protobufjs/aspromise": "^1.1.2", 1467 | "@protobufjs/base64": "^1.1.2", 1468 | "@protobufjs/codegen": "^2.0.4", 1469 | "@protobufjs/eventemitter": "^1.1.0", 1470 | "@protobufjs/fetch": "^1.1.0", 1471 | "@protobufjs/float": "^1.0.2", 1472 | "@protobufjs/inquire": "^1.1.0", 1473 | "@protobufjs/path": "^1.1.2", 1474 | "@protobufjs/pool": "^1.1.0", 1475 | "@protobufjs/utf8": "^1.1.0", 1476 | "@types/long": "^4.0.0", 1477 | "@types/node": "^10.1.0", 1478 | "long": "^4.0.0" 1479 | }, 1480 | "dependencies": { 1481 | "@types/node": { 1482 | "version": "10.17.5", 1483 | "resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.5.tgz", 1484 | "integrity": "sha512-RElZIr/7JreF1eY6oD5RF3kpmdcreuQPjg5ri4oQ5g9sq7YWU8HkfB3eH8GwAwxf5OaCh0VPi7r4N/yoTGelrA==", 1485 | "optional": true 1486 | } 1487 | } 1488 | }, 1489 | "proxy-addr": { 1490 | "version": "2.0.5", 1491 | "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.5.tgz", 1492 | "integrity": "sha512-t/7RxHXPH6cJtP0pRG6smSr9QJidhB+3kXu0KgXnbGYMgzEnUxRQ4/LDdfOwZEMyIh3/xHb8PX3t+lfL9z+YVQ==", 1493 | "requires": { 1494 | "forwarded": "~0.1.2", 1495 | "ipaddr.js": "1.9.0" 1496 | } 1497 | }, 1498 | "pump": { 1499 | "version": "3.0.0", 1500 | "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", 1501 | "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", 1502 | "optional": true, 1503 | "requires": { 1504 | "end-of-stream": "^1.1.0", 1505 | "once": "^1.3.1" 1506 | } 1507 | }, 1508 | "pumpify": { 1509 | "version": "2.0.1", 1510 | "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-2.0.1.tgz", 1511 | "integrity": "sha512-m7KOje7jZxrmutanlkS1daj1dS6z6BgslzOXmcSEpIlCxM3VJH7lG5QLeck/6hgF6F4crFf01UtQmNsJfweTAw==", 1512 | "optional": true, 1513 | "requires": { 1514 | "duplexify": "^4.1.1", 1515 | "inherits": "^2.0.3", 1516 | "pump": "^3.0.0" 1517 | }, 1518 | "dependencies": { 1519 | "duplexify": { 1520 | "version": "4.1.1", 1521 | "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.1.tgz", 1522 | "integrity": "sha512-DY3xVEmVHTv1wSzKNbwoU6nVjzI369Y6sPoqfYr0/xlx3IdX2n94xIszTcjPO8W8ZIv0Wb0PXNcjuZyT4wiICA==", 1523 | "optional": true, 1524 | "requires": { 1525 | "end-of-stream": "^1.4.1", 1526 | "inherits": "^2.0.3", 1527 | "readable-stream": "^3.1.1", 1528 | "stream-shift": "^1.0.0" 1529 | } 1530 | }, 1531 | "readable-stream": { 1532 | "version": "3.4.0", 1533 | "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.4.0.tgz", 1534 | "integrity": "sha512-jItXPLmrSR8jmTRmRWJXCnGJsfy85mB3Wd/uINMXA65yrnFo0cPClFIUWzo2najVNSl+mx7/4W8ttlLWJe99pQ==", 1535 | "optional": true, 1536 | "requires": { 1537 | "inherits": "^2.0.3", 1538 | "string_decoder": "^1.1.1", 1539 | "util-deprecate": "^1.0.1" 1540 | } 1541 | }, 1542 | "string_decoder": { 1543 | "version": "1.3.0", 1544 | "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", 1545 | "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", 1546 | "optional": true, 1547 | "requires": { 1548 | "safe-buffer": "~5.2.0" 1549 | } 1550 | } 1551 | } 1552 | }, 1553 | "qs": { 1554 | "version": "6.7.0", 1555 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", 1556 | "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==" 1557 | }, 1558 | "range-parser": { 1559 | "version": "1.2.1", 1560 | "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", 1561 | "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" 1562 | }, 1563 | "raw-body": { 1564 | "version": "2.4.0", 1565 | "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz", 1566 | "integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==", 1567 | "requires": { 1568 | "bytes": "3.1.0", 1569 | "http-errors": "1.7.2", 1570 | "iconv-lite": "0.4.24", 1571 | "unpipe": "1.0.0" 1572 | } 1573 | }, 1574 | "readable-stream": { 1575 | "version": "1.0.34", 1576 | "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", 1577 | "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", 1578 | "optional": true, 1579 | "requires": { 1580 | "core-util-is": "~1.0.0", 1581 | "inherits": "~2.0.1", 1582 | "isarray": "0.0.1", 1583 | "string_decoder": "~0.10.x" 1584 | } 1585 | }, 1586 | "regexp.prototype.flags": { 1587 | "version": "1.2.0", 1588 | "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.2.0.tgz", 1589 | "integrity": "sha512-ztaw4M1VqgMwl9HlPpOuiYgItcHlunW0He2fE6eNfT6E/CF2FtYi9ofOYe4mKntstYk0Fyh/rDRBdS3AnxjlrA==", 1590 | "optional": true, 1591 | "requires": { 1592 | "define-properties": "^1.1.2" 1593 | } 1594 | }, 1595 | "retry-request": { 1596 | "version": "4.1.1", 1597 | "resolved": "https://registry.npmjs.org/retry-request/-/retry-request-4.1.1.tgz", 1598 | "integrity": "sha512-BINDzVtLI2BDukjWmjAIRZ0oglnCAkpP2vQjM3jdLhmT62h0xnQgciPwBRDAvHqpkPT2Wo1XuUyLyn6nbGrZQQ==", 1599 | "optional": true, 1600 | "requires": { 1601 | "debug": "^4.1.1", 1602 | "through2": "^3.0.1" 1603 | }, 1604 | "dependencies": { 1605 | "debug": { 1606 | "version": "4.1.1", 1607 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", 1608 | "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", 1609 | "optional": true, 1610 | "requires": { 1611 | "ms": "^2.1.1" 1612 | } 1613 | } 1614 | } 1615 | }, 1616 | "safe-buffer": { 1617 | "version": "5.2.0", 1618 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.0.tgz", 1619 | "integrity": "sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg==" 1620 | }, 1621 | "safer-buffer": { 1622 | "version": "2.1.2", 1623 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", 1624 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" 1625 | }, 1626 | "semver": { 1627 | "version": "6.3.0", 1628 | "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", 1629 | "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", 1630 | "optional": true 1631 | }, 1632 | "send": { 1633 | "version": "0.17.1", 1634 | "resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz", 1635 | "integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==", 1636 | "requires": { 1637 | "debug": "2.6.9", 1638 | "depd": "~1.1.2", 1639 | "destroy": "~1.0.4", 1640 | "encodeurl": "~1.0.2", 1641 | "escape-html": "~1.0.3", 1642 | "etag": "~1.8.1", 1643 | "fresh": "0.5.2", 1644 | "http-errors": "~1.7.2", 1645 | "mime": "1.6.0", 1646 | "ms": "2.1.1", 1647 | "on-finished": "~2.3.0", 1648 | "range-parser": "~1.2.1", 1649 | "statuses": "~1.5.0" 1650 | }, 1651 | "dependencies": { 1652 | "debug": { 1653 | "version": "2.6.9", 1654 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", 1655 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", 1656 | "requires": { 1657 | "ms": "2.0.0" 1658 | }, 1659 | "dependencies": { 1660 | "ms": { 1661 | "version": "2.0.0", 1662 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 1663 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" 1664 | } 1665 | } 1666 | }, 1667 | "mime": { 1668 | "version": "1.6.0", 1669 | "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", 1670 | "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" 1671 | }, 1672 | "ms": { 1673 | "version": "2.1.1", 1674 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", 1675 | "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" 1676 | } 1677 | } 1678 | }, 1679 | "serve-static": { 1680 | "version": "1.14.1", 1681 | "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz", 1682 | "integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==", 1683 | "requires": { 1684 | "encodeurl": "~1.0.2", 1685 | "escape-html": "~1.0.3", 1686 | "parseurl": "~1.3.3", 1687 | "send": "0.17.1" 1688 | } 1689 | }, 1690 | "setprototypeof": { 1691 | "version": "1.1.1", 1692 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", 1693 | "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" 1694 | }, 1695 | "signal-exit": { 1696 | "version": "3.0.2", 1697 | "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", 1698 | "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", 1699 | "optional": true 1700 | }, 1701 | "snakeize": { 1702 | "version": "0.1.0", 1703 | "resolved": "https://registry.npmjs.org/snakeize/-/snakeize-0.1.0.tgz", 1704 | "integrity": "sha1-EMCI2LWOsHazIpu1oE4jLOEmQi0=", 1705 | "optional": true 1706 | }, 1707 | "statuses": { 1708 | "version": "1.5.0", 1709 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", 1710 | "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" 1711 | }, 1712 | "stream-events": { 1713 | "version": "1.0.5", 1714 | "resolved": "https://registry.npmjs.org/stream-events/-/stream-events-1.0.5.tgz", 1715 | "integrity": "sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg==", 1716 | "optional": true, 1717 | "requires": { 1718 | "stubs": "^3.0.0" 1719 | } 1720 | }, 1721 | "stream-shift": { 1722 | "version": "1.0.0", 1723 | "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.0.tgz", 1724 | "integrity": "sha1-1cdSgl5TZ+eG944Y5EXqIjoVWVI=", 1725 | "optional": true 1726 | }, 1727 | "streamsearch": { 1728 | "version": "0.1.2", 1729 | "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-0.1.2.tgz", 1730 | "integrity": "sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo=" 1731 | }, 1732 | "string_decoder": { 1733 | "version": "0.10.31", 1734 | "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", 1735 | "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", 1736 | "optional": true 1737 | }, 1738 | "stubs": { 1739 | "version": "3.0.0", 1740 | "resolved": "https://registry.npmjs.org/stubs/-/stubs-3.0.0.tgz", 1741 | "integrity": "sha1-6NK6H6nJBXAwPAMLaQD31fiavls=", 1742 | "optional": true 1743 | }, 1744 | "teeny-request": { 1745 | "version": "5.3.1", 1746 | "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-5.3.1.tgz", 1747 | "integrity": "sha512-hnUeun3xryzv92FbrnprltcdeDfSVaGFBlFPRvKJ2fO/ioQx9N0aSUbbXSfTO+ArRXine1gSWdWFWcgfrggWXw==", 1748 | "optional": true, 1749 | "requires": { 1750 | "http-proxy-agent": "^2.1.0", 1751 | "https-proxy-agent": "^3.0.0", 1752 | "node-fetch": "^2.2.0", 1753 | "stream-events": "^1.0.5", 1754 | "uuid": "^3.3.2" 1755 | } 1756 | }, 1757 | "through2": { 1758 | "version": "3.0.1", 1759 | "resolved": "https://registry.npmjs.org/through2/-/through2-3.0.1.tgz", 1760 | "integrity": "sha512-M96dvTalPT3YbYLaKaCuwu+j06D/8Jfib0o/PxbVt6Amhv3dUAtW6rTV1jPgJSBG83I/e04Y6xkVdVhSRhi0ww==", 1761 | "optional": true, 1762 | "requires": { 1763 | "readable-stream": "2 || 3" 1764 | }, 1765 | "dependencies": { 1766 | "readable-stream": { 1767 | "version": "3.4.0", 1768 | "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.4.0.tgz", 1769 | "integrity": "sha512-jItXPLmrSR8jmTRmRWJXCnGJsfy85mB3Wd/uINMXA65yrnFo0cPClFIUWzo2najVNSl+mx7/4W8ttlLWJe99pQ==", 1770 | "optional": true, 1771 | "requires": { 1772 | "inherits": "^2.0.3", 1773 | "string_decoder": "^1.1.1", 1774 | "util-deprecate": "^1.0.1" 1775 | } 1776 | }, 1777 | "string_decoder": { 1778 | "version": "1.3.0", 1779 | "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", 1780 | "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", 1781 | "optional": true, 1782 | "requires": { 1783 | "safe-buffer": "~5.2.0" 1784 | } 1785 | } 1786 | } 1787 | }, 1788 | "toidentifier": { 1789 | "version": "1.0.0", 1790 | "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", 1791 | "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==" 1792 | }, 1793 | "tslib": { 1794 | "version": "1.10.0", 1795 | "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.10.0.tgz", 1796 | "integrity": "sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==" 1797 | }, 1798 | "type-is": { 1799 | "version": "1.6.18", 1800 | "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", 1801 | "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", 1802 | "requires": { 1803 | "media-typer": "0.3.0", 1804 | "mime-types": "~2.1.24" 1805 | } 1806 | }, 1807 | "typedarray": { 1808 | "version": "0.0.6", 1809 | "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", 1810 | "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", 1811 | "optional": true 1812 | }, 1813 | "typedarray-to-buffer": { 1814 | "version": "3.1.5", 1815 | "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", 1816 | "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", 1817 | "optional": true, 1818 | "requires": { 1819 | "is-typedarray": "^1.0.0" 1820 | } 1821 | }, 1822 | "typescript": { 1823 | "version": "3.7.2", 1824 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.7.2.tgz", 1825 | "integrity": "sha512-ml7V7JfiN2Xwvcer+XAf2csGO1bPBdRbFCkYBczNZggrBZ9c7G3riSUeJmqEU5uOtXNPMhE3n+R4FA/3YOAWOQ==", 1826 | "dev": true 1827 | }, 1828 | "unique-string": { 1829 | "version": "2.0.0", 1830 | "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz", 1831 | "integrity": "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==", 1832 | "optional": true, 1833 | "requires": { 1834 | "crypto-random-string": "^2.0.0" 1835 | } 1836 | }, 1837 | "unpipe": { 1838 | "version": "1.0.0", 1839 | "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", 1840 | "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" 1841 | }, 1842 | "util-deprecate": { 1843 | "version": "1.0.2", 1844 | "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", 1845 | "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", 1846 | "optional": true 1847 | }, 1848 | "utils-merge": { 1849 | "version": "1.0.1", 1850 | "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", 1851 | "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" 1852 | }, 1853 | "uuid": { 1854 | "version": "3.3.3", 1855 | "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.3.tgz", 1856 | "integrity": "sha512-pW0No1RGHgzlpHJO1nsVrHKpOEIxkGg1xB+v0ZmdNH5OAeAwzAVrCnI2/6Mtx+Uys6iaylxa+D3g4j63IKKjSQ==", 1857 | "optional": true 1858 | }, 1859 | "vary": { 1860 | "version": "1.1.2", 1861 | "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", 1862 | "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" 1863 | }, 1864 | "walkdir": { 1865 | "version": "0.4.1", 1866 | "resolved": "https://registry.npmjs.org/walkdir/-/walkdir-0.4.1.tgz", 1867 | "integrity": "sha512-3eBwRyEln6E1MSzcxcVpQIhRG8Q1jLvEqRmCZqS3dsfXEDR/AhOF4d+jHg1qvDCpYaVRZjENPQyrVxAkQqxPgQ==", 1868 | "optional": true 1869 | }, 1870 | "websocket-driver": { 1871 | "version": "0.7.3", 1872 | "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.3.tgz", 1873 | "integrity": "sha512-bpxWlvbbB459Mlipc5GBzzZwhoZgGEZLuqPaR0INBGnPAY1vdBX6hPnoFXiw+3yWxDuHyQjO2oXTMyS8A5haFg==", 1874 | "requires": { 1875 | "http-parser-js": ">=0.4.0 <0.4.11", 1876 | "safe-buffer": ">=5.1.0", 1877 | "websocket-extensions": ">=0.1.1" 1878 | } 1879 | }, 1880 | "websocket-extensions": { 1881 | "version": "0.1.4", 1882 | "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", 1883 | "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==" 1884 | }, 1885 | "wrappy": { 1886 | "version": "1.0.2", 1887 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 1888 | "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", 1889 | "optional": true 1890 | }, 1891 | "write-file-atomic": { 1892 | "version": "3.0.1", 1893 | "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.1.tgz", 1894 | "integrity": "sha512-JPStrIyyVJ6oCSz/691fAjFtefZ6q+fP6tm+OS4Qw6o+TGQxNp1ziY2PgS+X/m0V8OWhZiO/m4xSj+Pr4RrZvw==", 1895 | "optional": true, 1896 | "requires": { 1897 | "imurmurhash": "^0.1.4", 1898 | "is-typedarray": "^1.0.0", 1899 | "signal-exit": "^3.0.2", 1900 | "typedarray-to-buffer": "^3.1.5" 1901 | } 1902 | }, 1903 | "xdg-basedir": { 1904 | "version": "4.0.0", 1905 | "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-4.0.0.tgz", 1906 | "integrity": "sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==", 1907 | "optional": true 1908 | }, 1909 | "xtend": { 1910 | "version": "4.0.2", 1911 | "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", 1912 | "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==" 1913 | }, 1914 | "yallist": { 1915 | "version": "3.1.1", 1916 | "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", 1917 | "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", 1918 | "optional": true 1919 | } 1920 | } 1921 | } 1922 | -------------------------------------------------------------------------------- /functions/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "functions", 3 | "scripts": { 4 | "build": "tsc", 5 | "build:watch": "tsc --watch", 6 | "serve": "npm run build && firebase serve --only functions", 7 | "shell": "npm run build && firebase functions:shell", 8 | "start": "npm run shell", 9 | "deploy": "firebase deploy --only functions", 10 | "logs": "firebase functions:log" 11 | }, 12 | "engines": { 13 | "node": "10" 14 | }, 15 | "main": "lib/index.js", 16 | "dependencies": { 17 | "firebase-admin": "8.7.0", 18 | "firebase-functions": "^3.3.0" 19 | }, 20 | "devDependencies": { 21 | "typescript": "3.7.2", 22 | "firebase-functions-test": "^0.1.7" 23 | }, 24 | "private": true 25 | } 26 | -------------------------------------------------------------------------------- /functions/src/index.ts: -------------------------------------------------------------------------------- 1 | import * as functions from 'firebase-functions' 2 | import * as admin from 'firebase-admin' 3 | 4 | const REVIEW_PATH = '/restaurants/{restaurantId}/reviews/{reviewId}' 5 | admin.initializeApp(functions.config().firebase) 6 | const firestore = admin.firestore() 7 | 8 | console.log('[functions] Start functions') 9 | 10 | export const updateRestaurantRate = functions.firestore 11 | .document(REVIEW_PATH) 12 | .onCreate(async (change, context) => { 13 | const review = change.data() 14 | const restaurantId = context.params.restaurantId 15 | if (!restaurantId) return 16 | 17 | // 現在のrateAvg, rateNumの取得->更新はアトミックである必要があるのでトランザクションで行う 18 | await firestore.runTransaction(async (tx) => { 19 | const restaurantRef = firestore.collection('/restaurants').doc(restaurantId) 20 | const restaurant = await tx.get(restaurantRef).then((doc) => doc.data()) 21 | console.dir(restaurant) 22 | if (!review || !restaurant) return 23 | 24 | const newRateAvg = (restaurant.rateAvg + review.rate) / (restaurant.rateNum + 1) 25 | console.dir(newRateAvg) 26 | tx.update(restaurantRef, { 27 | rateAvg: newRateAvg, 28 | rateNum: restaurant.rateNum + 1, 29 | }) 30 | }) 31 | }) 32 | 33 | export const pubsubFn = functions.pubsub.topic('test-topic').onPublish(async (msg, ctx) => { 34 | console.log('Received pubsub: test-topic'); 35 | console.log('json', JSON.stringify(msg.json), 'attrs', JSON.stringify(msg.attributes)); 36 | return true; 37 | }) 38 | 39 | type Ranking = { 40 | rank: number, 41 | rateAvg: number, 42 | restaurantId: string, 43 | restaurantName: string, 44 | } 45 | export const cronRestaurantRanking = functions.pubsub.topic('cron-restaurant-ranking').onPublish(async (msg, ctx) => { 46 | // ランキングの入れ替えは同時に行う必要があるのでbatchを使用 47 | const batch = firestore.batch() 48 | 49 | // 前日のランキングを削除 50 | const lastDaySnapshot = await firestore.collection('/rankings').get() 51 | lastDaySnapshot.forEach((doc) => { 52 | batch.delete(doc.ref) 53 | }) 54 | 55 | // 当日のランキングを作成 56 | let rank = 1 57 | const snapshot = await firestore.collection('/restaurants').orderBy('rateAvg', 'desc').get() 58 | snapshot.forEach((doc) => { 59 | const data = doc.data() 60 | const ranking: Ranking = { 61 | rank: rank, 62 | rateAvg: data.rateAvg, 63 | restaurantId: doc.id, 64 | restaurantName: data.name, 65 | } 66 | batch.set(firestore.collection('rankings').doc(), ranking) 67 | rank += 1 68 | }) 69 | 70 | // deleteとaddを同時に実行 71 | await batch.commit() 72 | }) 73 | 74 | // エミュレータではschedule用のtopicが認識されないらしく、発火はできない 75 | export const scheduledFunctionCrontab = functions.pubsub.schedule('5 11 * * *') 76 | .timeZone('Asia/Tokyo') 77 | .onRun((context) => { 78 | console.log('Received pubsub: firebase-schedule-scheduledFunctionCrontab-us-central1'); 79 | console.log('json', JSON.stringify(context)) 80 | return true 81 | }) 82 | -------------------------------------------------------------------------------- /functions/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "noImplicitReturns": true, 5 | "noUnusedLocals": true, 6 | "outDir": "lib", 7 | "sourceMap": true, 8 | "strict": true, 9 | "target": "es2017", 10 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 11 | }, 12 | "compileOnSave": true, 13 | "include": [ 14 | "src" 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | // For a detailed explanation regarding each configuration property, visit: 2 | // https://jestjs.io/docs/en/configuration.html 3 | 4 | module.exports = { 5 | preset: 'ts-jest', 6 | // All imported modules in your tests should be mocked automatically 7 | // automock: false, 8 | 9 | // Stop running tests after `n` failures 10 | // bail: 0, 11 | 12 | // Respect "browser" field in package.json when resolving modules 13 | // browser: false, 14 | 15 | // The directory where Jest should store its cached dependency information 16 | // cacheDirectory: "/private/var/folders/nr/nxc2q_7s2kx62_cxcr4_g5wm0000gn/T/jest_dx", 17 | 18 | // Automatically clear mock calls and instances between every test 19 | clearMocks: true, 20 | 21 | // Indicates whether the coverage information should be collected while executing the test 22 | // collectCoverage: false, 23 | 24 | // An array of glob patterns indicating a set of files for which coverage information should be collected 25 | // collectCoverageFrom: null, 26 | 27 | // The directory where Jest should output its coverage files 28 | // coverageDirectory: null, 29 | 30 | // An array of regexp pattern strings used to skip coverage collection 31 | // coveragePathIgnorePatterns: [ 32 | // "/node_modules/" 33 | // ], 34 | 35 | // A list of reporter names that Jest uses when writing coverage reports 36 | // coverageReporters: [ 37 | // "json", 38 | // "text", 39 | // "lcov", 40 | // "clover" 41 | // ], 42 | 43 | // An object that configures minimum threshold enforcement for coverage results 44 | // coverageThreshold: null, 45 | 46 | // A path to a custom dependency extractor 47 | // dependencyExtractor: null, 48 | 49 | // Make calling deprecated APIs throw helpful error messages 50 | // errorOnDeprecated: false, 51 | 52 | // Force coverage collection from ignored files usin a array of glob patterns 53 | // forceCoverageMatch: [], 54 | 55 | // A path to a module which exports an async function that is triggered once before all test suites 56 | // globalSetup: null, 57 | 58 | // A path to a module which exports an async function that is triggered once after all test suites 59 | // globalTeardown: null, 60 | 61 | // A set of global variables that need to be available in all test environments 62 | // globals: {}, 63 | 64 | // An array of directory names to be searched recursively up from the requiring module's location 65 | // moduleDirectories: [ 66 | // "node_modules" 67 | // ], 68 | 69 | // An array of file extensions your modules use 70 | // moduleFileExtensions: [ 71 | // "js", 72 | // "json", 73 | // "jsx", 74 | // "ts", 75 | // "tsx", 76 | // "node" 77 | // ], 78 | 79 | // A map from regular expressions to module names that allow to stub out resources with a single module 80 | // moduleNameMapper: {}, 81 | 82 | // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader 83 | // modulePathIgnorePatterns: [], 84 | 85 | // Activates notifications for test results 86 | // notify: false, 87 | 88 | // An enum that specifies notification mode. Requires { notify: true } 89 | // notifyMode: "failure-change", 90 | 91 | // A preset that is used as a base for Jest's configuration 92 | // preset: null, 93 | 94 | // Run tests from one or more projects 95 | // projects: null, 96 | 97 | // Use this configuration option to add custom reporters to Jest 98 | reporters: ["default", "jest-junit"], 99 | 100 | // Automatically reset mock state between every test 101 | // resetMocks: false, 102 | 103 | // Reset the module registry before running each individual test 104 | // resetModules: false, 105 | 106 | // A path to a custom resolver 107 | // resolver: null, 108 | 109 | // Automatically restore mock state between every test 110 | // restoreMocks: false, 111 | 112 | // The root directory that Jest should scan for tests and modules within 113 | // rootDir: null, 114 | 115 | // A list of paths to directories that Jest should use to search for files in 116 | // roots: [ 117 | // "" 118 | // ], 119 | 120 | // Allows you to use a custom runner instead of Jest's default test runner 121 | // runner: "jest-runner", 122 | 123 | // The paths to modules that run some code to configure or set up the testing environment before each test 124 | // setupFiles: [], 125 | 126 | // A list of paths to modules that run some code to configure or set up the testing framework before each test 127 | // setupFilesAfterEnv: [], 128 | 129 | // A list of paths to snapshot serializer modules Jest should use for snapshot testing 130 | // snapshotSerializers: [], 131 | 132 | // The test environment that will be used for testing 133 | testEnvironment: "node", 134 | 135 | // Options that will be passed to the testEnvironment 136 | // testEnvironmentOptions: {}, 137 | 138 | // Adds a location field to test results 139 | // testLocationInResults: false, 140 | 141 | // The glob patterns Jest uses to detect test files 142 | // testMatch: [ 143 | // "**/__tests__/**/*.[jt]s?(x)", 144 | // "**/?(*.)+(spec|test).[tj]s?(x)" 145 | // ], 146 | 147 | // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped 148 | // testPathIgnorePatterns: [ 149 | // "/node_modules/" 150 | // ], 151 | 152 | // The regexp pattern or array of patterns that Jest uses to detect test files 153 | // testRegex: [], 154 | 155 | // This option allows the use of a custom results processor 156 | // testResultsProcessor: null, 157 | 158 | // This option allows use of a custom test runner 159 | // testRunner: "jasmine2", 160 | 161 | // This option sets the URL for the jsdom environment. It is reflected in properties such as location.href 162 | // testURL: "http://localhost", 163 | 164 | // Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout" 165 | // timers: "real", 166 | 167 | // A map from regular expressions to paths to transformers 168 | // transform: null, 169 | 170 | // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation 171 | // transformIgnorePatterns: [ 172 | // "/node_modules/" 173 | // ], 174 | 175 | // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them 176 | // unmockedModulePathPatterns: undefined, 177 | 178 | // Indicates whether each individual test should be reported during the run 179 | // verbose: null, 180 | 181 | // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode 182 | // watchPathIgnorePatterns: [], 183 | 184 | // Whether to use watchman for file crawling 185 | // watchman: true, 186 | }; 187 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "testingfirebase", 3 | "version": "1.0.0", 4 | "description": "Testing firebase project", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "npm run test:firestore && npm run test:functions", 8 | "test:watch": "PUBSUB_EMULATOR_HOST=http://localhost:8085 PUBSUB_PROJECT_ID=testing-firebase-test jest --watch -i", 9 | "test:firestore": "JEST_JUNIT_OUTPUT_NAME=firestore.xml jest __tests__/firestore", 10 | "test:functions": "JEST_JUNIT_OUTPUT_NAME=functions.xml PUBSUB_EMULATOR_HOST=http://localhost:8085 PUBSUB_PROJECT_ID=testing-firebase-test jest -i __tests__/functions", 11 | "test:ci": "firebase --project test emulators:exec 'npm run test'", 12 | "firebase:use_dev": "firebase use dev", 13 | "firebase:use_test": "firebase use test", 14 | "emulators:start": "firebase emulators:start" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/Kesin11/TestingFirebase.git" 19 | }, 20 | "author": "kesin1202000@gmail.com", 21 | "license": "MIT", 22 | "bugs": { 23 | "url": "https://github.com/Kesin11/TestingFirebase/issues" 24 | }, 25 | "homepage": "https://github.com/Kesin11/TestingFirebase#readme", 26 | "private": true, 27 | "dependencies": { 28 | "@firebase/testing": "0.16.0", 29 | "@google-cloud/pubsub": "1.1.5", 30 | "@types/node": "12.12.12", 31 | "firebase": "7.5.0", 32 | "firebase-admin": "8.8.0", 33 | "uuid": "3.3.3" 34 | }, 35 | "devDependencies": { 36 | "@types/jest": "24.0.23", 37 | "@types/uuid": "3.4.6", 38 | "firebase-tools": "7.8.1", 39 | "jest": "24.9.0", 40 | "jest-junit": "9.0.0", 41 | "ts-jest": "24.2.0", 42 | "ts-node": "8.5.2", 43 | "typescript": "3.7.2" 44 | }, 45 | "jest-junit": { 46 | "outputDirectory": "./junit" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /scripts/insert_dummy_restaurant.ts: -------------------------------------------------------------------------------- 1 | import admin, { ServiceAccount } from 'firebase-admin' 2 | import serviceAccount from '../service_account.json' 3 | import { RestaurantAdminModel } from '../src/restaurant' 4 | 5 | const app = admin.initializeApp({ 6 | credential: admin.credential.cert(serviceAccount as ServiceAccount), 7 | }) 8 | 9 | const restaurantNames = [ 10 | 'Super burger', 11 | 'Ramen nihon ichi', 12 | 'Fire stake' 13 | ] 14 | 15 | const main = async () => { 16 | const firestore = admin.firestore() 17 | const restaurantModel = new RestaurantAdminModel(firestore) 18 | 19 | for (const name of restaurantNames) { 20 | await restaurantModel.add(name) 21 | } 22 | 23 | const snapshot = await restaurantModel.getAll() 24 | snapshot.forEach((doc) => console.log(doc.data())) 25 | } 26 | 27 | main().then(() => { 28 | app.delete() 29 | }) -------------------------------------------------------------------------------- /scripts/insert_dummy_review.ts: -------------------------------------------------------------------------------- 1 | import admin, { ServiceAccount } from 'firebase-admin' 2 | import serviceAccount from '../service_account.json' 3 | import { RestaurantAdminModel, Restaurant } from '../src/restaurant' 4 | import { FieldValue } from '@google-cloud/firestore' 5 | 6 | const app = admin.initializeApp({ 7 | credential: admin.credential.cert(serviceAccount as ServiceAccount), 8 | }) 9 | 10 | const restaurantNames = [ 11 | 'Super burger', 12 | 'Ramen nihon ichi', 13 | 'Fire stake' 14 | ] 15 | 16 | const randomRate = () => { 17 | const min = Math.ceil(1) 18 | const max = Math.floor(5) 19 | return Math.floor(Math.random() * (max - min + 1)) + min 20 | } 21 | 22 | const main = async () => { 23 | const firestore = admin.firestore() 24 | const restaurantModel = new RestaurantAdminModel(firestore) 25 | let snapshot = await restaurantModel.getAll() 26 | const restaurantIds: string[] = [] 27 | snapshot.forEach((doc) => restaurantIds.push(doc.id)) 28 | 29 | for (const id of restaurantIds) { 30 | const dummyUid = Math.random().toString(36).slice(-8) 31 | const ref = firestore.collection(`/restaurants/${id}/reviews`).doc(dummyUid) 32 | await ref.set({ 33 | rate: randomRate(), 34 | text: 'test', 35 | userId: dummyUid, 36 | updatedAt: FieldValue.serverTimestamp() 37 | }) 38 | 39 | const reviewSnapshot = await firestore.collection(`/restaurants/${id}/reviews`).get() 40 | console.log('restaurant:', id) 41 | reviewSnapshot.forEach((doc) => console.log(doc.data())) 42 | } 43 | 44 | snapshot = await restaurantModel.getAll() 45 | snapshot.forEach((doc) => console.log(doc.data())) 46 | } 47 | 48 | main().then(() => { 49 | app.delete() 50 | }) -------------------------------------------------------------------------------- /scripts/test_trigger_pubsub.ts: -------------------------------------------------------------------------------- 1 | import admin, { ServiceAccount } from 'firebase-admin' 2 | import serviceAccount from '../service_account_test.json' 3 | import { PubSub } from '@google-cloud/pubsub' 4 | 5 | const app = admin.initializeApp({ 6 | credential: admin.credential.cert(serviceAccount as ServiceAccount), 7 | }) 8 | 9 | const restaurantNames = [ 10 | 'Super burger', 11 | 'Ramen nihon ichi', 12 | 'Fire stake' 13 | ] 14 | 15 | const pubsub = new PubSub() 16 | 17 | const main = async () => { 18 | console.log("Pubsub Emulator:", process.env.PUBSUB_EMULATOR_HOST); 19 | if (!process.env.PUBSUB_EMULATOR_HOST) { 20 | throw "Need export env: PUBSUB_EMULATOR_HOST" 21 | } 22 | 23 | const msg = await pubsub.topic('test-topic').publishJSON({ 24 | foo: 'bar', 25 | date: new Date() 26 | }, { attr1: 'value' }); 27 | 28 | console.log(`published test-topic: ${JSON.stringify(msg)}`) 29 | 30 | const msg2 = await pubsub.topic('cron-restaurant-ranking').publishJSON({}) 31 | console.log(`published cron-restaurant-trigger: ${JSON.stringify(msg2)}`) 32 | 33 | // エミュレータではschedule用のtopicが認識されないらしく、エラーになる 34 | // const msgCron = await pubsub.topic('firebase-schedule-scheduledFunctionCrontab-us-central1').publishJSON({}) 35 | // console.log(`published firebase-schedule-scheduledFunctionCrontab-us-central1: ${JSON.stringify(msgCron)}`) 36 | } 37 | 38 | main().then(() => { 39 | app.delete() 40 | }) -------------------------------------------------------------------------------- /src/ranking.ts: -------------------------------------------------------------------------------- 1 | import { firestore as admin_firestore } from "firebase-admin" 2 | 3 | const COLLECTION_PATH = '/rankings' 4 | 5 | export class RankingAdminModel { 6 | constructor(private firestore: admin_firestore.Firestore) {} 7 | collectionRef() { return this.firestore.collection(COLLECTION_PATH) } 8 | 9 | async getAllOrderByRank() { 10 | return this.firestore.collection(COLLECTION_PATH).orderBy('rank', 'asc').get() 11 | } 12 | } -------------------------------------------------------------------------------- /src/restaurant.ts: -------------------------------------------------------------------------------- 1 | import { firestore as admin_firestore } from "firebase-admin" 2 | import { firestore } from 'firebase' 3 | 4 | const COLLECTION_PATH = '/restaurants' 5 | 6 | export type Restaurant = { 7 | rateAvg: number 8 | rateNum: number 9 | name: string 10 | } 11 | 12 | export class RestaurantAdminModel { 13 | constructor(private firestore: admin_firestore.Firestore) {} 14 | collectionRef() { return this.firestore.collection(COLLECTION_PATH) } 15 | 16 | async add(name: string) { 17 | return this.firestore.collection(COLLECTION_PATH).add({ 18 | name, 19 | rateAvg: 0, 20 | rateNum: 0 21 | }) 22 | } 23 | 24 | async getAll() { 25 | return this.firestore.collection(COLLECTION_PATH).get() 26 | } 27 | } 28 | 29 | export class RestaurantUserModel { 30 | constructor(private firestore: firestore.Firestore) {} 31 | collectionRef() { return this.firestore.collection(COLLECTION_PATH) } 32 | 33 | async getAll() { 34 | return this.firestore.collection(COLLECTION_PATH).get() 35 | } 36 | } -------------------------------------------------------------------------------- /src/review.ts: -------------------------------------------------------------------------------- 1 | import { firestore as admin_firestore } from "firebase-admin" 2 | import { firestore } from "firebase"; 3 | // (認証ユーザーのみ、自分のレビューは店毎に1つだけ、timestampはサーバー時間、rateは1-5のint) 4 | export type Review = { 5 | rate: number 6 | text: string 7 | updatedAt: firestore.Timestamp 8 | userId: string 9 | } 10 | export type ReviewInput = Omit 11 | 12 | export class ReviewAdminModel { 13 | constructor(private firestore: admin_firestore.Firestore) {} 14 | collectionRef (restaurantId: string) { 15 | return this.firestore.collection(`/restaurants/${restaurantId}/reviews`) 16 | } 17 | 18 | async set (restaurantId: string, review: ReviewInput) { 19 | const userId = review.userId 20 | return this.collectionRef(restaurantId).doc(userId).set({ 21 | ...review, 22 | updatedAt: firestore.FieldValue.serverTimestamp() 23 | }) 24 | } 25 | } 26 | 27 | export class ReviewUserModel { 28 | constructor(private firestore: firestore.Firestore) {} 29 | 30 | collectionRef (restaurantId: string) { 31 | return this.firestore.collection(`/restaurants/${restaurantId}/reviews`) 32 | } 33 | 34 | async get (restaurantId: string, userId: string) { 35 | return this.collectionRef(restaurantId).doc(userId).get() 36 | } 37 | 38 | async getAll (restaurantId: string) { 39 | return this.collectionRef(restaurantId).get() 40 | } 41 | 42 | async create (restaurantId: string, review: ReviewInput) { 43 | const userId = review.userId 44 | const docRef = this.collectionRef(restaurantId).doc(userId) 45 | const snapshot = await docRef.get() 46 | if (snapshot.exists) return 47 | 48 | return docRef.set({ 49 | ...review, 50 | updatedAt: firestore.FieldValue.serverTimestamp() 51 | }) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | "target": "ES2018", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */ 5 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ 6 | // "lib": [], /* Specify library files to be included in the compilation. */ 7 | // "allowJs": true, /* Allow javascript files to be compiled. */ 8 | // "checkJs": true, /* Report errors in .js files. */ 9 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 10 | // "declaration": true, /* Generates corresponding '.d.ts' file. */ 11 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 12 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 13 | // "outFile": "./", /* Concatenate and emit output to single file. */ 14 | // "outDir": "./", /* Redirect output structure to the directory. */ 15 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 16 | // "composite": true, /* Enable project compilation */ 17 | // "removeComments": true, /* Do not emit comments to output. */ 18 | // "noEmit": true, /* Do not emit outputs. */ 19 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 20 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 21 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 22 | 23 | /* Strict Type-Checking Options */ 24 | "strict": true, /* Enable all strict type-checking options. */ 25 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 26 | // "strictNullChecks": true, /* Enable strict null checks. */ 27 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 28 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 29 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 30 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 31 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 32 | 33 | /* Additional Checks */ 34 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 35 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 36 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 37 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 38 | 39 | /* Module Resolution Options */ 40 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 41 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 42 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 43 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 44 | // "typeRoots": [], /* List of folders to include type definitions from. */ 45 | // "types": [], /* Type declaration files to be included in compilation. */ 46 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 47 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 48 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 49 | 50 | /* Source Map Options */ 51 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 52 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 53 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 54 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 55 | 56 | /* Experimental Options */ 57 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 58 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 59 | "resolveJsonModule": true 60 | } 61 | } 62 | --------------------------------------------------------------------------------