Skip to content

Commit

Permalink
resultのクエリを最適化
Browse files Browse the repository at this point in the history
  • Loading branch information
MurakawaTakuya committed Dec 29, 2024
1 parent e6b6b30 commit a7c9dc6
Show file tree
Hide file tree
Showing 6 changed files with 118 additions and 94 deletions.
4 changes: 3 additions & 1 deletion Documents/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -201,8 +201,10 @@ Use Create Post API to update post.
- URL: /result/:?userId
- Empty userId will return all results.
- Parameters
- limit?: number - The maximum number of results to return.(Default is 50)
- limit?: number - The maximum number of results to return. (Default is 50)
- offset?: number - The number of results to skip before starting to collect the result set.
- onlyPending?: boolean - If true, only pending goals will be returned. (Default is false)
- onlyCompleted?: boolean - If true, only completed or failed goals will be returned. (Default is false)
- Method: GET
- Response
```json
Expand Down
8 changes: 4 additions & 4 deletions functions/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,22 +61,22 @@ if (process.env.NODE_ENV === "production") {
});
}

// 10分間で最大300回に制限
// 10分間で最大100回に制限
app.use(
rateLimit({
windowMs: 10 * 60 * 1000,
max: 300,
max: 100,
keyGenerator: (req) => {
const key = req.headers["x-forwarded-for"] || req.ip || "unknown";
return Array.isArray(key) ? key[0] : key;
},
})
);
// 1時間で最大1000回に制限
// 1時間で最大300回に制限
app.use(
rateLimit({
windowMs: 60 * 60 * 1000,
max: 1000,
max: 300,
keyGenerator: (req) => {
const key = req.headers["x-forwarded-for"] || req.ip || "unknown";
return Array.isArray(key) ? key[0] : key;
Expand Down
158 changes: 81 additions & 77 deletions functions/src/routers/resultRouter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,72 +3,89 @@ import admin from "firebase-admin";
import { logger } from "firebase-functions";
import { countCompletedGoals, countFailedGoals, getStreak } from "../status";
import { GoalWithIdAndUserData, User } from "../types";
import { getUserFromId } from "./userRouter";

const router = express.Router();
const db = admin.firestore();

const getResults = async (
res: Response,
limit: number,
offset: number,
userId?: string,
includeSuccess = true,
includeFailed = true,
includePending = true
onlyPending = false,
onlyFinished = false
) => {
let goalQuery = db.collection("goal").limit(limit).offset(offset);
let baseQuery = db.collection("goal").limit(limit).offset(offset);

if (userId) {
goalQuery = goalQuery.where("userId", "==", userId);
const userDoc = await getUserFromId(userId);
if (!userDoc.exists) {
return res.status(404).json({ message: "User not found" });
}
baseQuery = baseQuery.where("userId", "==", userId);
}

if (!includeSuccess) {
goalQuery = goalQuery.where("post", "==", null);
if (onlyPending && onlyFinished) {
return res.status(400).json({
message:
"Cannot set both 'onlyPending' and 'onlyFinished'. Please set only one of 'onlyPending' or 'onlyFinished', or leave both false.",
});
}

if (!includeFailed) {
goalQuery = goalQuery
.where("post", "!=", null)
.where("deadline", ">", new Date());
}
const now = admin.firestore.Timestamp.now();

const pendingResults: GoalWithIdAndUserData[] = [];
const successResults: GoalWithIdAndUserData[] = [];
const failedResults: GoalWithIdAndUserData[] = [];

if (!includePending) {
goalQuery = goalQuery
const userList = new Map<string, User>(); // ユーザー情報のキャッシュ

if (onlyPending || (!onlyPending && !onlyFinished)) {
const pendingSnapshot = await baseQuery
.where("post", "==", null)
.where("deadline", "<=", new Date());
.where("deadline", ">", now)
.get();

const pendingGoals = await processGoals(pendingSnapshot.docs, userList);
pendingResults.push(...pendingGoals);
}

const goalSnapshot = await goalQuery.get();
if (onlyFinished || (!onlyPending && !onlyFinished)) {
const completedSnapshot = await baseQuery.where("post", "!=", null).get();

const goals = goalSnapshot.docs.map((doc) => {
const data = doc.data();
const post = data.post;
const failedSnapshot = await baseQuery
.where("post", "==", null)
.where("deadline", "<=", now)
.get();

return {
goalId: doc.id,
userId: data.userId,
deadline: data.deadline.toDate(),
text: data.text,
post: post && {
text: post.text,
storedId: post.storedId,
submittedAt: post.submittedAt.toDate(),
},
};
}) as GoalWithIdAndUserData[];
const completedResults = await processGoals(
completedSnapshot.docs,
userList
);
const failedResultsTemp = await processGoals(failedSnapshot.docs, userList);

if (!goals || goals.length === 0) {
return { successResults: [], failedResults: [], pendingResults: [] };
successResults.push(...completedResults);
failedResults.push(...failedResultsTemp);
}

const successResults: GoalWithIdAndUserData[] = [];
const failedResults: GoalWithIdAndUserData[] = [];
const pendingResults: GoalWithIdAndUserData[] = [];
return {
successResults,
failedResults,
pendingResults,
};
};

// mapで<userId, userName>のリストを作成し、userNameをキャッシュする
const userList = new Map<string, User>();
const processGoals = async (
docs: FirebaseFirestore.QueryDocumentSnapshot[],
userList: Map<string, User>
) => {
const results: GoalWithIdAndUserData[] = [];

for (const doc of docs) {
const data = doc.data();
const userId = data.userId;

for (const goal of goals) {
// userListにあるならば、userNameを取得し、無いならばfirestoreから取得してキャッシュする
const userId = goal.userId;
let userData = userList.get(userId);
if (!userData) {
const userDoc = await db.collection("user").doc(userId).get();
Expand All @@ -86,57 +103,44 @@ const getResults = async (
userList.set(userId, userData);
}

const post = goal.post;
if (post) {
if (post.submittedAt > goal.deadline) {
failedResults.push({
...goal,
userData,
});
} else {
successResults.push({
...goal,
userData,
});
}
} else if (goal.deadline < new Date()) {
failedResults.push({
...goal,
userData,
});
} else {
pendingResults.push({
...goal,
userData,
});
}
const post = data.post;
results.push({
goalId: doc.id,
userId: data.userId,
deadline: data.deadline.toDate(),
text: data.text,
post: post && {
text: post.text,
storedId: post.storedId,
submittedAt: post.submittedAt.toDate(),
},
userData,
});
}

return {
successResults,
failedResults,
pendingResults,
};
return results;
};

// GET: 全ての目標または特定のユーザーの目標に対する結果を取得
router.get("/:userId?", async (req: Request, res: Response) => {
const userId = req.params.userId;

const limit = parseInt(req.query.limit as string) || 100; // TODO: デフォルト値を適切に設定
let limit = parseInt(req.query.limit as string) || 100;
if (limit < 1 || limit > 100) {
limit = 100;
}
const offset = parseInt(req.query.offset as string) || 0;
const includeSuccess = req.query.success !== "false";
const includeFailed = req.query.failed !== "false";
const includePending = req.query.pending !== "false";
const onlyPending = req.query.onlyPending === "true"; // デフォルト: false
const onlyFinished = req.query.onlyFinished === "true"; // デフォルト: false

try {
const results = await getResults(
res,
limit,
offset,
userId,
includeSuccess,
includeFailed,
includePending
onlyPending,
onlyFinished
);
return res.json(results);
} catch (error) {
Expand Down
4 changes: 2 additions & 2 deletions functions/src/routers/userRouter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -204,10 +204,10 @@ router.delete("/:userId", async (req: Request, res: Response) => {
export default router;

// ユーザー名からユーザー情報を取得
const getUserFromName = async (userName: string) => {
export const getUserFromName = async (userName: string) => {
return await db.collection("user").where("name", "==", userName).get();
};

const getUserFromId = async (userId: string) => {
export const getUserFromId = async (userId: string) => {
return await db.collection("user").doc(userId).get();
};
4 changes: 2 additions & 2 deletions src/Components/DashBoard/DashBoard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export default function DashBoard({
const [isLoading, setIsLoading] = useState<boolean>(true);

useEffect(() => {
fetchResult({ userId })
fetchResult({ userId, success, failed, pending })
.then((data) => {
setSuccessResults(data.successResults);
setFailedResults(data.failedResults);
Expand All @@ -54,7 +54,7 @@ export default function DashBoard({
type: "warning",
});
});
}, [userId]);
}, [userId, success, failed, pending]);

useEffect(() => {
// 表示したい項目にデータがない場合はnoResultをtrueにする
Expand Down
34 changes: 26 additions & 8 deletions src/utils/API/Result/fetchResult.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,32 @@ import { appCheckToken, functionsEndpoint } from "@/app/firebase";
*/
export const fetchResult = async ({
userId = "",
}: { userId?: string } = {}) => {
const response = await fetch(`${functionsEndpoint}/result/${userId}`, {
method: "GET",
headers: {
"X-Firebase-AppCheck": appCheckToken,
"Content-Type": "application/json",
},
});
success = true,
failed = true,
pending = true,
}: {
userId?: string;
success?: boolean;
failed?: boolean;
pending?: boolean;
} = {}) => {
const queryParams = new URLSearchParams();
if (!success && !failed && pending) {
queryParams.append("onlyPending", "true");
} else if (success && failed && !pending) {
queryParams.append("onlyFinished", "true");
}

const response = await fetch(
`${functionsEndpoint}/result/${userId}?${queryParams.toString()}`,
{
method: "GET",
headers: {
"X-Firebase-AppCheck": appCheckToken,
"Content-Type": "application/json",
},
}
);

if (!response.ok) {
throw new Error("Network response was not ok");
Expand Down

0 comments on commit a7c9dc6

Please sign in to comment.