From 4b3c504715f0e14f8e18cc4677742c0c44df0752 Mon Sep 17 00:00:00 2001 From: Kengo Yamashita Date: Sun, 26 May 2024 23:19:54 +0900 Subject: [PATCH] feat: add method and test (#11) * feat: add method and test * chore: update * refactor: change method --- app/package-lock.json | 204 ++++++++++++++++++++++-------- app/package.json | 1 + app/src/service/firestore.test.ts | 43 +++++++ app/src/service/firestore.ts | 61 ++++----- firebase.json | 17 +++ firestore.indexes.json | 4 + firestore.rules | 9 ++ 7 files changed, 254 insertions(+), 85 deletions(-) create mode 100644 app/src/service/firestore.test.ts create mode 100644 firestore.indexes.json create mode 100644 firestore.rules diff --git a/app/package-lock.json b/app/package-lock.json index 4a65047..c967d1e 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -16,6 +16,7 @@ }, "devDependencies": { "@babel/core": "^7.23.7", + "@firebase/rules-unit-testing": "^3.0.2", "@solidjs/testing-library": "^0.8.7", "autoprefixer": "^10.4.19", "babel-preset-solid": "^1.8.9", @@ -1047,6 +1048,22 @@ "resolved": "https://registry.npmjs.org/@firebase/remote-config-types/-/remote-config-types-0.3.1.tgz", "integrity": "sha512-PgmfUugcJAinPLsJlYcBbNZe7KE2omdQw1WCT/z46nKkNVGkuHdVFSq54s3wiFa9BoHmLZ01u4hGXIhm6MdLOw==" }, + "node_modules/@firebase/rules-unit-testing": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@firebase/rules-unit-testing/-/rules-unit-testing-3.0.2.tgz", + "integrity": "sha512-3wysX8g3grwgm8WbaMoNnNP74WGQFYEm7QIa2MFCaUXyUxVAV/9rGHWPPSmqP7IWGoXFOG6cOLOsry6PeT+BoA==", + "dev": true, + "dependencies": { + "@types/node-fetch": "2.6.4", + "node-fetch": "2.6.7" + }, + "engines": { + "node": ">=10.10.0" + }, + "peerDependencies": { + "firebase": "^10.0.0" + } + }, "node_modules/@firebase/storage": { "version": "0.12.4", "resolved": "https://registry.npmjs.org/@firebase/storage/-/storage-0.12.4.tgz", @@ -1669,13 +1686,32 @@ } }, "node_modules/@types/babel__generator": { - "dev": true + "version": "7.6.8", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.8.tgz", + "integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.0.0" + } }, "node_modules/@types/babel__template": { - "dev": true + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } }, "node_modules/@types/babel__traverse": { - "dev": true + "version": "7.20.6", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.6.tgz", + "integrity": "sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.20.7" + } }, "node_modules/@types/node": { "version": "20.12.11", @@ -1685,6 +1721,30 @@ "undici-types": "~5.26.4" } }, + "node_modules/@types/node-fetch": { + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.4.tgz", + "integrity": "sha512-1ZX9fcN4Rvkvgv4E6PAY5WXUFWFcRWxZa3EW83UjycOB9ljJCedb2CupIP4RZMEwF/M3eTcCihbBRgwtGbg5Rg==", + "dev": true, + "dependencies": { + "@types/node": "*", + "form-data": "^3.0.0" + } + }, + "node_modules/@types/node-fetch/node_modules/form-data": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz", + "integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==", + "dev": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -1757,6 +1817,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true + }, "node_modules/autoprefixer": { "version": "10.4.19", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.19.tgz", @@ -2130,6 +2196,18 @@ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/commander": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", @@ -2291,6 +2369,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/dequal": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", @@ -2898,54 +2985,6 @@ "node": ">= 6" } }, - "node_modules/form-data/node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true - }, - "node_modules/form-data/node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/form-data/node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "dev": true, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/form-data/node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/form-data/node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/fraction.js": { "version": "4.3.7", "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", @@ -3715,6 +3754,27 @@ "node": ">=8.6" } }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/minimatch": { "version": "9.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", @@ -3750,6 +3810,42 @@ "thenify-all": "^1.0.0" } }, + "node_modules/node-fetch": { + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "dev": true, + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-fetch/node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "dev": true + }, + "node_modules/node-fetch/node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dev": true, + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/node-releases": { "version": "2.0.14", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", @@ -4884,6 +4980,12 @@ "requires-port": "^1.0.0" } }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "dev": true + }, "node_modules/ts-interface-checker": { "version": "0.1.13", "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", diff --git a/app/package.json b/app/package.json index 7052166..067ad8e 100644 --- a/app/package.json +++ b/app/package.json @@ -12,6 +12,7 @@ "license": "MIT", "devDependencies": { "@babel/core": "^7.23.7", + "@firebase/rules-unit-testing": "^3.0.2", "@solidjs/testing-library": "^0.8.7", "autoprefixer": "^10.4.19", "babel-preset-solid": "^1.8.9", diff --git a/app/src/service/firestore.test.ts b/app/src/service/firestore.test.ts new file mode 100644 index 0000000..9a3a625 --- /dev/null +++ b/app/src/service/firestore.test.ts @@ -0,0 +1,43 @@ +import { test} from "uvu"; +import * as assert from "uvu/assert"; +import fs from "node:fs"; +import { initializeTestEnvironment, RulesTestEnvironment } from '@firebase/rules-unit-testing'; + +import { getAllDocumentsWithCollectionName } from "./firestore"; +import type { Firestore } from "firebase/firestore"; + +// setup the test environment with the Firestore emulator +// before running the tests +let testEnv: RulesTestEnvironment | null = null; + +test.before.each(async() => { + testEnv = await initializeTestEnvironment({ + projectId: "test-project", + firestore: { + rules: fs.readFileSync("/home/yamashita/tegata/firestore.rules", "utf8"), + host: "localhost", + port: 8080 + } + }); + }); + +test.after.each(async() => { + await testEnv?.cleanup(); +}); + +test("getAllDocumentsWithCollectionName", async() => { + const user = testEnv?.authenticatedContext("test-user"); + const firestore = user?.firestore() as unknown as Firestore; + const collectionName = "users"; + if (!firestore) { + throw new Error("Firestore is not initialized"); + } + const result = await getAllDocumentsWithCollectionName(firestore, collectionName); + if (result instanceof Error) { + throw result; + } + assert.is(result.length, 0); +}); + + +test.run(); diff --git a/app/src/service/firestore.ts b/app/src/service/firestore.ts index 7813fac..fb04977 100644 --- a/app/src/service/firestore.ts +++ b/app/src/service/firestore.ts @@ -1,39 +1,34 @@ import { firebaseApp } from './firebase'; -import { CollectionReference, DocumentData, Firestore, query, Query, addDoc, collection, collectionGroup, connectFirestoreEmulator, deleteDoc, doc, getDoc, getFirestore, setDoc, updateDoc, getDocs, where } from 'firebase/firestore'; +import { CollectionReference, DocumentData, Firestore, query, Query, addDoc, collection, collectionGroup, deleteDoc, doc, getDoc, getFirestore, setDoc, updateDoc, getDocs, where, QuerySnapshot } from 'firebase/firestore'; -const db = getFirestore(firebaseApp); +export const firestore: Firestore = getFirestore(firebaseApp); /* firebaseはcollectionとdocumentの概念を持っている https://firebase.google.com/docs/reference/js/firestore_ */ -// useEmulatorを使うと、ローカルで動かすことができる -export function useEmulator(host: string, port: number, options?: {mockUserToken?: string}) { - try { - connectFirestoreEmulator(db, host, port, options); - } catch (e) { - // this method is used in the development environment, so it is not necessary to handle the error - console.error(e); - } -} - -function getCollection(collectionName: string): CollectionReference { +function getCollection(db: Firestore, collectionName: string): CollectionReference { return collection(db, collectionName); } -function getAllDocuments(collectionId: string):Query { - return collectionGroup(db, collectionId); - } - -export function getAllDocumentsWithCollectionName(collectionName: string):Query { - const collectionRef = getCollection(collectionName); - return getAllDocuments(collectionRef.id); +export async function getAllDocumentsWithCollectionName(db: Firestore, collectionName: string):Promise{ + const collectionRef = getCollection(db, collectionName); + try { + const documents: DocumentData[] = []; + const querySnapshot = await getDocs(collectionRef); + for (const doc of querySnapshot.docs) { + documents.push(doc.data()); + } + return documents; + } catch (error) { + return new Error('Error getting documents: ' + error); + } } // 新しいドキュメントを任意のIDで追加する -export async function addNewDocument(collectionName: string, data: DocumentData):Promise { - const collectionRef = getCollection(collectionName); +export async function addNewDocument(db: Firestore, collectionName: string, data: DocumentData):Promise { + const collectionRef = getCollection(db, collectionName); return setDoc(doc(collectionRef, data.id), data).then(() => { return null; }).catch((error) => { @@ -42,8 +37,8 @@ export async function addNewDocument(collectionName: string, data: DocumentData) } // autoIdを使って新しいドキュメントを追加する -export async function addNewDocumentWithAutoId(collectionName: string, data: DocumentData):Promise { - const collectionRef = getCollection(collectionName); +export async function addNewDocumentWithAutoId(db: Firestore, collectionName: string, data: DocumentData):Promise { + const collectionRef = getCollection(db, collectionName); return addDoc(collectionRef, data).then(() => { return null; }).catch((error) => { @@ -51,7 +46,7 @@ export async function addNewDocumentWithAutoId(collectionName: string, data: Doc }); } -export async function getDocumentById(collectionName: string, documentId: string):Promise { +export async function getDocumentById(db: Firestore, collectionName: string, documentId: string):Promise { const documentRef = doc(db, collectionName, documentId); const documentSnapshot = await getDoc(documentRef); if (documentSnapshot.exists()) { @@ -61,7 +56,7 @@ export async function getDocumentById(collectionName: string, documentId: string } } -export async function updateDocument(collectionName: string, documentId: string, data: DocumentData):Promise { +export async function updateDocument(db: Firestore, collectionName: string, documentId: string, data: DocumentData):Promise { const documentRef = doc(db, collectionName, documentId); return updateDoc(documentRef, data).then(() => { return null; @@ -70,7 +65,7 @@ export async function updateDocument(collectionName: string, documentId: string, }); } -export async function deleteDocument(collectionName: string, documentId: string):Promise { +export async function deleteDocument(db: Firestore, collectionName: string, documentId: string):Promise { const documentRef = doc(db, collectionName, documentId); return deleteDoc(documentRef).then(() => { return null; @@ -85,20 +80,18 @@ export type QueryObj = { value: unknown, } -export async function getDocumentsWithQuery(collectionName: string, queryObj: QueryObj):Promise { +export async function getDocumentsWithQuery(db: Firestore, collectionName: string, queryObj: QueryObj):Promise { const collectionRef = collection(db, collectionName); const q = query(collectionRef, where(queryObj.field, queryObj.operator, queryObj.value)); try { const querySnapshot = await getDocs(q); const documents: DocumentData[] = []; - querySnapshot.forEach((doc) => { - if (doc.data()) { - documents.push(doc.data()); - } - }); + for (const doc of querySnapshot.docs) { + documents.push(doc.data()); + } return documents; } catch (error) { - return new Error('Error getting documents: ' + error); + return new Error(`Error getting documents: ${error}`); } } diff --git a/firebase.json b/firebase.json index 93fb369..56fef47 100644 --- a/firebase.json +++ b/firebase.json @@ -12,5 +12,22 @@ "destination": "/index.html" } ] + }, + "firestore": { + "rules": "firestore.rules", + "indexes": "firestore.indexes.json" + }, + "emulators": { + "firestore": { + "port": 8080 + }, + "hosting": { + "port": 5000 + }, + "ui": { + "enabled": true, + "port": 9999 + }, + "singleProjectMode": true } } diff --git a/firestore.indexes.json b/firestore.indexes.json new file mode 100644 index 0000000..415027e --- /dev/null +++ b/firestore.indexes.json @@ -0,0 +1,4 @@ +{ + "indexes": [], + "fieldOverrides": [] +} diff --git a/firestore.rules b/firestore.rules new file mode 100644 index 0000000..2163074 --- /dev/null +++ b/firestore.rules @@ -0,0 +1,9 @@ +rules_version = '2'; + +service cloud.firestore { + match /databases/{database}/documents { + match /{document=**} { + allow read, write: if request.auth != null; + } + } +} \ No newline at end of file