- ❌: 아직 개발되지 않았음.
- ⭐: 그 부분의 문서화가 덜 되었음.
- 💥: 그 API가 원자적이지 않아서 치명적인 쿼리 충돌(경쟁 상태)을 야기할 수 있음.
- 하나의 관리자는 오직 하나의 도서관만 관리할 수 있다.
- 관리자에게 주어진 도서관 ID는 바뀌지 않는다.
- 계정의 유형은 바뀌지 않는다.
- API들은 각기 요구되는 권한(인증)이 있어야 동작한다.
- 모든 API들은 원자적이어야 한다(즉 💥이 없어야 한다.).
- API 루틴이 실행되고 있을 때에 데이터베이스 오류가 나면,
{"success": false, "reason": "Something is wrong with the database."}가 반환된다.
- 어떤 도서관에 대한 정보를 다루는지로 들어온 API 요청을 나누어, 각 도서관에 해당하는 _작업 큐_에 넣어 각 도서관마다 순차적으로 처리한다.
- 이 문서를 확인하여 해결책을 찾아 본다.
- 좋은 문서를 찾은 것 같네요! 그런데 잠깐만요. 지금 머리가 조금 아프네요…….
-
로그인 하기
- 요청
- POST
/API/login
- 인자
IDpassword: 암호.
- 동작
- 입력된 인수가 유효한지 확인한다.
- 입력된 계정 정보가 맞는지 확인한다.
request.session.loggedInAs = ID
- 반환 값
{"success": false, "reason": "The ID is not valid."}{"success": false, "reason": "The password is not valid."}- 암호가 잘못되었거나 로그인 하려는 계정이 존재하지 않을 때에,
{"success": false, "reason": "Could not log-in."}. {"success": false, "reason": "An error occurred when comparing the password with the hash!"}{"success": false, "reason": "Something is wrong with the database."}{"success": true}
- 비고
- 로그인된 상태에서 로그인할 수 있다.
- 요청
-
로그아웃 하기
- 요청
- POST
/API/logout
- 인자
noGET: 반드시 참 값이어야 한다.
- 동작
noGET이 참 값인지 확인한다. 그렇지 않다면,{"success": false, "reason": "noGET is not truthy."}를 반환한다.request.session.loggedInAs = null.{"success": true}를 반환한다.
- 반환 값
{"success": true}{"success": false, "reason": "noGET is not truthy."}
- 요청
- 책장 안에 있는 책의 정보를 갱신하기 ❌
- 요청
- POST
/API/takeMyBooks
- 인자
libraryAPITokenbookcaseNumber: 책장 번호이다. 이것은 그 도서관에서 유일해야 한다.bookCodes: 이것은 그 책들의 RFID 태그에서 읽힌 책 코드들로 이루어진 배열이다.
- 동작
- 입력이 유효한지 검사한다. 이때에
bookcaseNumber는null이면 안 된다. - 도서관 ID를 얻는다:
db.libraries.findOne({libraryAPIToken: (그 도서관 API 토큰)}, {libraryID: 1}).libraryID. global.TaskManager.addTask((이후의 처리가 담긴 함수), "takeMyBooks", (그 도서관 ID));- 기존에 꽂혀(소유하고) 있던 책에 대한 소유권을 제거한다:
db.books.update({libraryID: (그 도서관 ID), bookcaseNumber: (그 책장 번호)}, {$set: {bookcaseNumber: null}}, {multi: true}). - 인수에 명시된 책을 소유한다:
db.books.update({libraryID: (그 도서관 ID), bookCode: {$in: (그 책 코드들로 이루어진 배열)}}, {$set: {bookcaseNumber: (그 책장 번호)}}, {multi: true}).
- 입력이 유효한지 검사한다. 이때에
- 반환 값
- 성공 시,
{"success": true}. - 실패 시,
{"success": false, "reason": (실패 까닭이 담긴 문자열)}.
- 성공 시,
- 요청
-
회원 가입하기
- 요청
- POST
/API/user/register
- 인자
- ID
- password
- 동작
- 입력된 인수가 유효한지 확인한다.
- 입력된 ID가 이미 등록된 계정의 ID인지 엄격하지 않게 확인한다:
db.accounts.findOne({ID: (그 계정 ID)}, {"_id": 1}). 만약 그렇다면,{"success": false, "reason": "The account already exists."}를 반환한다. - 입력된 암호에 대한 해시를 생성한다. 이는 연산 비용이 많이 드는 작업이다.
- 계정이 이미 있지 않으면 계정을 생성한다:
db.accounts.updateOne({ID: (그 계정 ID)}, {$setOnInsert: {ID: (그 계정 ID), passwordHash: (그 암호에 대한 해시), type: "user", information: {usingLibraries: []}}}, {upsert: true}). - 4번 단계에서 사용한 쿼리의 반환 값의
"upsertedId"프로퍼티가 존재하면{"success": true}를 반환하고, 아니면{"success": false, "reason": "The account already exists."}를 반환한다.
- 반환 값
{"success": true}{"success": false, "reason": "The ID is not valid."}{"success": false, "reason": "The password is not valid."}{"success": false, "reason": "The account already exists."}{"success": false, "reason": "Something is wrong with the database."}
- 요청
-
사용자 코드를 소유하기
- 요청
- POST
/API/user/ownUserCode
- 인자
libraryID: 그 사용자 코드가 유효한 도서관의 ID이다.userCode: 소유할 사용자 코드이다.
- 동작
- 입력된 인수가 유효한지 확인한다.
theAccount = db.accounts.findOne({ID: request.session.loggedInAs}, {type: 1, information: 1}).theAccount.type === "user"인지 확인한다. 그렇지 않다면,{"success": false, "reason": "You are not a user!"}를 반환한다.- 그 도서관에 대한 사용자 코드를 이미 가지고 있으면(
db.userCodes.findOne({libraryID: (the library ID), userID: request.session.loggedInAs})),{"success": false, "reason": "You already have a user code for the library."}를 반환한다. - 그 사용자 코드가 존재하고 소유되어 있지 않다면 소유한다:
queryResult = db.userCodes.updateOne({libraryID: (그 도서관 ID), userCode: (그 사용자 코드), userID: null}, {$set: {userID: request.session.loggedInAs}}). - 만약
queryResult.modifiedCount === 1이면,{"success": true}를 반환한다. - 그것이 아니고
queryResult.modifiedCount === 0이라면,{"success": false, "reason": "The user-code does not exist, or is already owned by another user."}를 반환한다.
- 반환 값
{"success": false, "reason": "The library ID is not valid."}{"success": false, "reason": "The user code is not valid."}{"success": false, "reason": "You have to log-in!"}{"success": false, "reason": "You are not a user!"}{"success": false, "reason": "You already have a user code for the library."}{"success": false, "reason": "Something is wrong with the database."}{"success": false, "reason": "The user-code does not exist, or is already owned by another user."}{"success": true}
- 요청
-
이용하고 있는 도서관 목록 얻기
- 요청
- POST
/API/user/getUsingLibraries
- 인자
noGET: 반드시 참 값이어야 한다.
- 동작
noGET이 참 값인지 확인한다. 그렇지 않다면,{"success": false, "reason": "noGET is not truthy."}를 반환한다.theAccount = db.accounts.findOne({ID: request.session.loggedInAs}, {type: 1, information: 1})theAccount.type === "user"인지 확인한다. 그렇지 않다면,{"success": false, "reason": "You are not a user!"}를 반환한다.JSON.stringify({"success": true, "usingLibraries": theAccount.information.usingLibraries})를 반환한다.
- 반환 값
{"success": false, "reason": "You have to log-in!"}{"success": false, "reason": "You are not a user!"}{"success": false, "reason": "Something is wrong with the database."}{"success": true, "usingLibraries": [{"libraryID": (그 도서관 ID), "userCode": (그 도서관에서의 그 사용자(요청자)의 코드)}, ...]}
- 요청
-
특정한 책이 있는 책장에 대한 점등 요청을 보내기 ❌ ⭐
- 요청
- POST
/API/user/light
- 인자
libraryIDISBN: 그 책의 ISBN.
- 동작
theAccount = db.accounts.findOne({ID: request.session.loggedInAs}, {type: 1, information: 1})theAccount.type === "user"인지 확인한다. 그렇지 않다면,{"success": false, "reason": "You are not a user!"}를 반환한다.- `
- 반환 값
{"success": false, "reason": "Something is wrong with the database."}{"success": false, "reason": "You are not a user!"}
- 요청
-
도서관에 책 추가하기
- 요청
- POST
/API/administrator/addBook또는/API/admin/addBook
- 인자
- ISBN: 추가할 책의 EAN-13 형식의 국제 표준 도서 번호를 담고 있는 문자열이다.
- bookCode: 이것은 그 책의 RFID 태그에 기록되어 있어야 한다.
- 동작
- 입력된 인수가 유효한지 확인한다.
- 그 관리자(요청자)의 도서관의 ID를 얻는다:
db.accounts.fineOne({ID: request.session.loggedInAs}).information.libraryID. - 그 책이 이미 있지 않으면 그 책을 추가한다:
db.books.updateOne({libraryID: (그 도서관 ID), bookCode: (그 책 코드)}, {$setOnInsert: {libraryID: (그 도서관 ID), bookCode: (그 책 코드), bookcaseNumber: null, ISBN: (그 국제 표준 도서 번호), bookcaseUpdatedAt: null}}, {upsert: true}). - 3번 단계에서 사용한 쿼리의 반환 값의
"upsertedId"프로퍼티가 존재하면{"success": true}를 반환하고, 아니면{"success": false, "reason": "The book already exists."}를 반환한다.
- 반환 값
{"success": false, "reason": "The ISBN is not valid."}{"success": false, "reason": "The book code is not valid."}{"success": false, "reason": "The book already exists."}{"success": false, "reason": "Something is wrong with the database."}{"success": true}
- 요청
-
그 관리자(요청자)의 도서관에 대한 정보 얻기
- 요청
- POST
/API/administrator/getLibraryInformation또는/API/admin/getLibraryInformation
- 인자
noGET: 반드시 참 값이어야 한다.
- 동작
noGET이 참 값인지 확인한다. 그렇지 않다면,{"success": false, "reason": "noGET is not truthy."}를 반환한다.theAccount = db.accounts.findOne({ID: request.session.loggedInAs}, {type: 1, information: 1})theAccount.type === "administrator"인지 확인한다. 그렇지 않다면,{"success": false, "reason": "You are not an administrator of a library!"}를 반환한다.theLibraryInformation = db.libraries.findOne({"libraryID": theAccount.information.libraryID}, {"_id": 0})JSON.stringify({"success": true, "libraryID": theLibraryInformation.libraryID, "libraryAPIToken": theLibraryInformation.libraryAPIToken, "userCodes": theLibraryInformation.userCodes})를 반환한다.
- 반환 값
{"success": false, "reason": "noGET is not truthy."}{"success": false, "reason": "You have to log-in!"}{"success": false, "reason": "You are not an administrator of a library!"}{"success": false, "reason": "Something is wrong with the database."}{"success": true, "libraryID": (그 도서관 ID), "libraryAPIToken": (그 도서관 API 토큰)}
- 요청
-
그 관리자(요청자)의 도서관의 사용자 코드에 대한 정보 얻기
- 요청
/API/administrator/getUserCodes또는/API/admin/getUserCodes
- 인자
noGET: 반드시 참 값이어야 한다.
- 동작
noGET이 참 값인지 확인한다. 그렇지 않다면,{"success": false, "reason": "noGET is not truthy."}를 반환한다.theAccount = db.accounts.findOne({ID: request.session.loggedInAs}, {type: 1, information: 1})theAccount.type === "administrator"인지 확인한다. 그렇지 않다면,{"success": false, "reason": "You are not an administrator of a library!"}를 반환한다.theUserCodes = db.userCodes.find({"libraryID": theAccount.information.libraryID}, {"libraryID": 1, "userCode": 1, "userID": 1, "permission": 1, "_id": 0})JSON.stringify({"success": true, "userCodes": theUserCodes})를 반환한다.
- 반환 값
{"success": false, "reason": "noGET is not truthy."}{"success": false, "reason": "You have to log-in!"}{"success": false, "reason": "You are not an administrator of a library!"}{"success": false, "reason": "Something is wrong with the database."}{"success": true, "userCodes": [{"libraryID": (그 도서관 ID), "userCode": (사용자 코드), "userID": (사용자 ID), "permission": {"borrowable": (true 또는 false), "lightable": (true 또는 false)}}, ...]}
- 요청
-
사용자 코드를 생성하고 관리하에 두기
- 요청
- POST
/API/administrator/newUserCode또는/API/admin/newUserCode
- 인자
noGET: 반드시 참 값이어야 한다.
- 동작
noGET이 참 값인지 확인한다. 그렇지 않다면,{"success": false, "reason": "noGET is not truthy."}를 반환한다.theAccount = db.accounts.findOne({ID: request.session.loggedInAs}, {type: 1, information: 1})theAccount.type === "administrator"인지 확인한다. 그렇지 않다면,{"success": false, "reason": "You are not an administrator of a library!"}를 반환한다.- 무작위의 사용자 코드를 생성한다:
(length => (Math.random().toString(36).substring(2, 2 + length) + '0'.repeat(length)).substring(0, length))(20).toUpperCase(). - 생성된 사용자 코드가 그 도서관에 존재하지 않으면 그 사용자 코드를 추가한다.:
queryResult = db.userCodes.updateOne({libraryID: theAccount.information.libraryID, "userCode": (새롭게 생성된 사용자 코드)}, {$setOnInsert: {libraryID: theAccount.information.libraryID, "userCode": (새롭게 생성된 사용자 코드), userID: null, permission: {"borrowable": false, "lightable": false}}}, {upsert: true}). - 그 사용자 코드가 추가되었으면(
if(queryResult && queryResult.upsertedCount === 1)),{"success": true, "theNewUserCode": (새롭게 생성된 사용자 코드)}를 반환한다. - 아니면,
{"success": false, "reason": "Could not generate a new user code. Please try again."}을 반환한다.
- 반환 값
{"success": false, "reason": "noGET is not truthy."}{"success": false, "reason": "You have to log-in!"}{"success": false, "reason": "You are not an administrator of a library!"}{"success": false, "reason": "Something is wrong with the database."}{"success": false, "reason": "Could not generate a new user code. Please try again."}{"success": true, "theNewUserCode": (새롭게 생성된 사용자 코드)}
- 요청
-
특정한 사용자 코드의 권한 설정하기
- 요청
- POST
/API/administrator/setPermissions또는/API/admin/setPermissions
- 인자
userCodeborrowablelightable
- 동작
- 입력된 인수가 유효한지 확인한다.
- 요청자가 관리자인지 확인한다.
- 그 관리자(요청자)의 도서관의 ID를 얻는다:
db.accounts.findOne({ID: request.session.loggedInAs}, {information: 1}).information.libraryID. db.userCodes.updateOne({libraryID: (그 도서관 ID), "userCode": (권한을 설정할 사용자 코드)}, {$set: {"permission": (설정할 권한들)}})후에, 만약 반환된 객체의modifiedCount가1이 아니면,{"success": false, "reason": "The user code does not exist."}를 반환한다.- 그렇지 않으면,
{"success": true}를 반환한다.
- 반환 값
{"success": false, "reason": "The user code is not valid."}{"success": false, "reason": "The ``borrowable`` is not valid."}{"success": false, "reason": "The ``lightable`` is not valid."}{"success": false, "reason": "You have to log-in!"}{"success": false, "reason": "You are not an administrator of a library!"}{"success": false, "reason": "Something is wrong with the database."}{"success": false, "reason": "The user code does not exist."}{"success": true}
- 요청
-
그 도서관의 특정한 사용자 코드 제거하기
- 요청
- POST
/API/administrator/deleteUserCode또는/API/admin/deleteUserCode
- 인자
userCode: 제거할, 그 도서관에 존재하는 사용자 코드이다.
- 동작
- 입력된 인수가 유효한지 확인한다.
- 요청자가 관리자인지 확인한다. 그렇지 않다면,
{"success": false, "reason": "You are not an administrator of a library!"}를 반환한다. - 그 관리자(요청자)의 도서관의 ID를 얻는다:
db.accounts.findOne({ID: request.session.loggedInAs}, {information: 1}).information.libraryID. - 그 사용자의 점등을 무효화한다:
db.lights.remove({libraryID: (그 도서관 ID), lighter: (제거할 사용자 코드)}). db.userCodes.remove({libraryID: (그 도서관 ID), userCode: (제거할 사용자 코드)}, {justOne: true})후에, 만약 그 반환 값에1의 값을 가진deletedCount속성이 없으면,{"success": false, "reason": "The user code does not exist."}를 반환한다.{"success": true}를 반환한다.
- 반환 값
{"success": false, "reason": "The user code is not valid."}{"success": false, "reason": "You have to log-in!"}{"success": false, "reason": "You are not an administrator of a library!"}{"success": false, "reason": "Something is wrong with the database."}{"success": false, "reason": "The user code does not exist."}{"success": true}
- 요청
-
새로운 도서관 API 토큰을 생성하여 그것을 도서관 API 토큰으로 하기 ❌
- 요청
- POST
/API/administrator/newLibraryAPIToken또는/API/admin/newLibraryAPIToken
- 인자
noGET: 반드시 참 값이어야 한다.
- 동작
noGET이 참 값인지 확인한다. 그렇지 않다면,{"success": false, "reason": "noGET is not truthy."}를 반환한다.theAccount.type === "administrator"인지 확인한다. 그렇지 않다면,{"success": false, "reason": "You are not an administrator of a library!"}를 반환한다.- 새로운 도서관 API 토큰이 될 무작위의 긴 문자열을 생성한다.
- 그것이 기존의 도서관 API 토큰과 같은지 확인한다. 만약 그렇다면, 3번 동작으로 간다.
- 그 관리자(요청자)의 도서관의 ID를 얻는다:
db.accounts.findOne({ID: request.session.loggedInAs}).information.libraryID. db.libraries.updateOne({libraryID: (그 도서관 ID)}, {$set: {libraryAPIToken: (그 새로운 도서관 API 토큰)}})
- 반환 값
- 성공 시,
{"success": true}. - 실패 시,
{"success": false, "reason": (실패 까닭이 담긴 문자열)}.
- 성공 시,
- 요청
이 글에 따르면, 특정한 문서가 있는지 확인할 때에, findOne은 find와 limit의 조합보다 매우 느리다고 한다. 그러나 나는 node-mongodb-native 모듈을 쓰고 이 모듈의 findOne은 내부적으로 find와 limit의 조합으로 구현되어 있기 때문에, 특정한 문서가 있는지 확인하기 위해 findOne을 쓰겠다.
// https://github.com/mongodb/node-mongodb-native/blob/c41966c1b1834c33390922650e582842dbad2934/lib/collection.js#L833
Collection.prototype.findOne = function() {
var self = this;
var args = Array.prototype.slice.call(arguments, 0);
var callback = args.pop();
var cursor = this.find.apply(this, args).limit(-1).batchSize(1);
// Return the item
cursor.next(function(err, item) {
if(err != null) return handleCallback(callback, toError(err), null);
handleCallback(callback, null, item);
});
}DB:
- LibraryLight
- accounts
- ID
- passwordHash
- type: "administrator" | "developer" | "user"
- information: {libraryID} | {} | {usingLibraries: [{libraryID, userCode}, ...]}
- libraries
- libraryID
- libraryAPIToken
- userCodes
- libraryID: (그 사용자 코드가 유효한 도서관의 ID)
- userCode: (20 자의, 반각 숫자 또는 라틴 알파벳으로 이루어진 문자열)
- userID: null | "이 사용자 코드에 해당하는 계정의 ID"
- permission: {"borrowable": true|false, "lightable": true|false}
- lights
- libraryID
- bookcaseNumber
- lightColor
- ISBN
- lighter
- expirationTime
- books
- ISBN
- libraryID
- bookcaseNumber:
- bookcaseUpdatedAt: $currentDate
- bookCode:
- bookInformation
- ISBN
- title: {main, sub1, sub2}
- description
- accounts
_사용자 코드_가 없다고 가정하면, 도서관 관리자에게 그 도서관을 이용할 권한을 받기 위해 계정 ID를 제출하여야 할 것이고, 이때에 타인 A의 ID를 제출하면 그 도서관의 관리자가 그 ID에 그 권한을 부여하여 A가 그 도서관의 이용자가 될 것이다. 그런데 이는 A의 요청에 의한 것이 아니므로, 이런 방식은 바람직하지 않다. 이에 ‘사용자 코드’라는 개념을 도입하여, 특정한 계정을 가진 자가 특정한 도서관의 이용자가 되려면 그 도서관에서 발급한 사용자 코드를 그 계정으로 로그인한 상태에서 도서관 ID와 함께 등록해야 하게 만듦으로써 상기된 방식의 문제점을 해결하였다.