diff --git a/README.md b/README.md index 14bc863..3c6351c 100644 --- a/README.md +++ b/README.md @@ -181,6 +181,7 @@ Deployed documentation: | GET | `/api/auth/me` | Get current user profile, egg, and equipped items | Yes | | GET | `/api/auth/me/inventory` | View owned items | Yes | | PATCH | `/api/egg/equip` | Equip background, music, or cosmetic item | Yes | +| PATCH | `/api/egg/unequip` | Unequip background, music, or cosmetic item | Yes | | POST | `/api/posts` | Create notebook post | Yes | | GET | `/api/posts/all` | Get all posts created by current user | Yes | | GET | `/api/posts/:id` | Get one post | Yes | diff --git a/docs/api/openapi.yaml b/docs/api/openapi.yaml index 59d1d6b..08c9225 100644 --- a/docs/api/openapi.yaml +++ b/docs/api/openapi.yaml @@ -609,6 +609,7 @@ paths: # 2. EGG # - PATCH /api/egg/equip + # - PATCH /api/egg/unequip /api/egg/equip: patch: summary: Equip background, music, or cosmetic item to egg @@ -646,6 +647,43 @@ paths: application/json: schema: $ref: '#/components/schemas/Error' + /api/egg/unequip: + patch: + summary: Unquip background, music, or cosmetic item to egg + tags: [Egg] + security: + - bearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/EquipEggRequest' + responses: + "200": + description: Item unequipped successfully + content: + application/json: + schema: + $ref: '#/components/schemas/EquipEggResponse' + "400": + description: Invalid item for unequip action + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + "401": + description: Missing or invalid token + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + "404": + description: Item not found or not owned by user + content: + application/json: + schema: + $ref: '#/components/schemas/Error' # 3. POST / NOTEBOOK # - POST /api/posts diff --git a/src/services/eggService.js b/src/services/eggService.js index 77521d4..110a3ce 100644 --- a/src/services/eggService.js +++ b/src/services/eggService.js @@ -2,138 +2,187 @@ const eggModel = require("../models/eggModel"); const shopItemModel = require("../models/shopItemModel"); const userItemModel = require("../models/userItemModel"); +const { getDb } = require("../db"); const ALLOWED_ITEM_TYPES = new Set(["background", "music", "cosmetic"]); async function equip({ user_id, item_id }) { - let egg = await eggModel.findById(user_id); - if (!egg) { - const error = new Error("Egg not found"); - error.statusCode = 404; - throw error; - } - // TODO: 1) check whether the requested item is valid - const item = await shopItemModel.findById(item_id); - if (!item) { - const error = new Error("Item not found in the shop (This item is not on the list)"); - error.statusCode = 404; - throw error; - } - - // TODO: 2) check whether the requested item is owned by this user. - const itemInventory = await userItemModel.findByIds(user_id, item_id); - if (!itemInventory) { - const error = new Error("Item not found in the user's inventory"); - error.statusCode = 404; - throw error; - } - // TODO: 3) identify what kind of this item - let itemType = item.item_type; - - if (!ALLOWED_ITEM_TYPES.has(itemType)) { - const error = new Error("Invalid item type"); - error.statusCode = 400; - throw error; - } - let prior = egg[`active_${itemType}_id`]; + let db = getDb(); + // check whether transaction has started + let transactionStarted = false; + try { + // make this purchase process as the one atomic transaction + await db.run("BEGIN TRANSACTION"); + transactionStarted = true; + let egg = await eggModel.findById(user_id); + if (!egg) { + const error = new Error("Egg not found"); + error.statusCode = 404; + throw error; + } + // TODO: 1) check whether the requested item is valid + const item = await shopItemModel.findById(item_id); + if (!item) { + const error = new Error("Item not found in the shop (This item is not on the list)"); + error.statusCode = 404; + throw error; + } - // if there is an item which is already equipped, unequip - if (prior != null && prior != item_id) { - let priorInventory = await userItemModel.findByIds(user_id, prior); - let priorItem = await shopItemModel.findById(prior); - if (priorInventory) { - priorInventory.is_equipped = 0; - await userItemModel.update(priorInventory); + // TODO: 2) check whether the requested item is owned by this user. + const itemInventory = await userItemModel.findByIds(user_id, item_id); + if (!itemInventory) { + const error = new Error("Item not found in the user's inventory"); + error.statusCode = 404; + throw error; } - applyItemEffect(egg, priorItem, 0); - } - - if(prior != item_id){ - applyItemEffect(egg, item, 1); - } + // TODO: 3) identify what kind of this item + let itemType = item.item_type; - egg[`active_${itemType}_id`] = itemInventory.item_id; - itemInventory.is_equipped = 1; - // equip item - let flagEgg = await eggModel.update(egg); - let flagUserItem = await userItemModel.update(itemInventory); + if (!ALLOWED_ITEM_TYPES.has(itemType)) { + const error = new Error("Invalid item type"); + error.statusCode = 400; + throw error; + } + let prior = egg[`active_${itemType}_id`]; + + // if there is an item which is already equipped, unequip + if (prior != null && prior != item_id) { + let priorInventory = await userItemModel.findByIds(user_id, prior); + let priorItem = await shopItemModel.findById(prior); + if (priorInventory) { + priorInventory.is_equipped = 0; + await userItemModel.update(priorInventory); + } + if (priorItem) { + applyItemEffect(egg, priorItem, 0); + } + } + if (prior != item_id) { + applyItemEffect(egg, item, 1); + } - if (flagEgg && flagUserItem) { - return egg; - } else { - const error = new Error("Database error"); - error.statusCode = 500; + egg[`active_${itemType}_id`] = itemInventory.item_id; + itemInventory.is_equipped = 1; + // equip item + let flagEgg = await eggModel.update(egg); + let flagUserItem = await userItemModel.update(itemInventory); + + + if (flagEgg && flagUserItem) { + // finish transaction and apply changes into database + await db.run("COMMIT"); + transactionStarted = false; + return egg; + } else { + const error = new Error("Database error"); + error.statusCode = 500; + throw error; + } + } + catch (error) { + if (transactionStarted) { + try { + await db.run("ROLLBACK"); + } catch (rollbackError) { + console.error("Rollback failed:", rollbackError); + } + } throw error; } } async function unequip({ user_id, item_id }) { - let egg = await eggModel.findById(user_id); - if (!egg) { - const error = new Error("Egg not found"); - error.statusCode = 404; - throw error; - } - // TODO: 1) check whether the requested item is valid - const item = await shopItemModel.findById(item_id); - if (!item) { - const error = new Error("Item not found in the shop (This item is not on the list)"); - error.statusCode = 404; - throw error; - } + let db = getDb(); + // check whether transaction has started + let transactionStarted = false; + try { + // make this purchase process as the one atomic transaction + await db.run("BEGIN TRANSACTION"); + transactionStarted = true; + let egg = await eggModel.findById(user_id); + if (!egg) { + const error = new Error("Egg not found"); + error.statusCode = 404; + throw error; + } + // TODO: 1) check whether the requested item is valid + const item = await shopItemModel.findById(item_id); + if (!item) { + const error = new Error("Item not found in the shop (This item is not on the list)"); + error.statusCode = 404; + throw error; + } - // TODO: 2) check whether the requested item is owned by this user. - const itemInventory = await userItemModel.findByIds(user_id, item_id); - if (!itemInventory) { - const error = new Error("Item not found in the user's inventory"); - error.statusCode = 404; - throw error; - } - // TODO: 3) identify what kind of this item - let itemType = item.item_type; + // TODO: 2) check whether the requested item is owned by this user. + const itemInventory = await userItemModel.findByIds(user_id, item_id); + if (!itemInventory) { + const error = new Error("Item not found in the user's inventory"); + error.statusCode = 404; + throw error; + } + // TODO: 3) identify what kind of this item + let itemType = item.item_type; - if (!ALLOWED_ITEM_TYPES.has(itemType)) { - const error = new Error("Invalid item type"); - error.statusCode = 400; - throw error; - } - let prior = egg[`active_${itemType}_id`]; + if (!ALLOWED_ITEM_TYPES.has(itemType)) { + const error = new Error("Invalid item type"); + error.statusCode = 400; + throw error; + } + let prior = egg[`active_${itemType}_id`]; - // if there is an item which is already equipped, unequip - if (prior == null) { - const error = new Error("No Equipped Item is found"); - error.statusCode = 400; - throw error; - } - - if (prior !== item_id) { - const error = new Error("This item is not currently equipped"); - error.statusCode = 400; - throw error; - } + // if there is an item which is already equipped, unequip + if (prior == null) { + const error = new Error("No Equipped Item is found"); + error.statusCode = 400; + throw error; + } + + if (prior != item_id) { + const error = new Error("This item is not currently equipped"); + error.statusCode = 400; + throw error; + } - egg[`active_${itemType}_id`] = null; - itemInventory.is_equipped = 0; - // unequip item - applyItemEffect(egg, item, 1); - let flagEgg = await eggModel.update(egg); - let flagUserItem = await userItemModel.update(itemInventory); + egg[`active_${itemType}_id`] = null; + itemInventory.is_equipped = 0; + // unequip item + applyItemEffect(egg, item, 0); + let flagEgg = await eggModel.update(egg); + let flagUserItem = await userItemModel.update(itemInventory); - if (flagEgg && flagUserItem) { - return egg; - } else { - const error = new Error("Database error"); - error.statusCode = 500; + if (flagEgg && flagUserItem) { + // finish transaction and apply changes into database + await db.run("COMMIT"); + transactionStarted = false; + + return egg; + } else { + const error = new Error("Database error"); + error.statusCode = 500; + throw error; + } + } + catch (error) { + if (transactionStarted) { + try { + await db.run("ROLLBACK"); + } catch (rollbackError) { + console.error("Rollback failed:", rollbackError); + } + } throw error; } } -function applyItemEffect(egg, item, is_euip){ +function applyItemEffect(egg, item, isEquip) { let itemEffectType = item["effect_type"]; let itemEffectValue = Number(item["effect_value"]); - if(is_euip){ + if (!itemEffectType || !Number.isFinite(itemEffectValue)) { + return egg; + } + if (isEquip) { egg[itemEffectType] += itemEffectValue; } else { egg[itemEffectType] -= itemEffectValue; diff --git a/src/tests/services/eggService.test.js b/src/tests/services/eggService.test.js index 91287fe..907fcf6 100644 --- a/src/tests/services/eggService.test.js +++ b/src/tests/services/eggService.test.js @@ -3,6 +3,11 @@ jest.mock("../../models/eggModel"); jest.mock("../../models/shopItemModel"); jest.mock("../../models/userItemModel"); +jest.mock("../../db", () => ({ + getDb: () => ({ + run: jest.fn().mockResolvedValue({}) + }) +})); const eggService = require("../../services/eggService"); const eggModel = require("../../models/eggModel");