Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
38 changes: 38 additions & 0 deletions docs/api/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
261 changes: 155 additions & 106 deletions src/services/eggService.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
5 changes: 5 additions & 0 deletions src/tests/services/eggService.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
Loading