ワイ 「この記事のカバーです」
https://qiita.com/kiwatchi1991/items/58b53c5b4ddf8d6a7053
バックナンバー
【第1回】「みんなのポートフォリオまとめサイト」を作ります~プロトタイプ作成編~
成果物
リポジトリ
フロントエンド
https://github.com/yuzuru2/minna_frontend
バックエンド
https://github.com/yuzuru2/minna_backend
コレクション定義(テーブル定義)
ワイ 「今回はNoSQLのMongoDBを使ってます」
ワイ 「コレクションとはRDBでいうテーブル的なやつです」
RDB | MongoDB |
---|---|
スキーマ | データベース |
テーブル | コレクション |
カラム | フィールド |
レコード | ドキュメント |
①Users(ユーザ)
uid: unique
物理名 | 論理名 | 型 |
---|---|---|
uid | ユーザID | string |
name | 名前 | string |
twitterUrl | TwitterのURL | string |
githubUrl | GitHubのURL | string |
createdAt | 作成時間 | Date |
updatedAt | 更新時間 | Date |
src/mongoose/collection/users.ts
import*asmongoosefrom'mongoose';importSchemafrom'src/mongoose';constmodel_name='users';interface_interface{uid:string;name:string;twitterUrl:string;githubUrl:string;createdAt:Date;updatedAt:Date;}interfacei_modelextendsmongoose.Document{}interfacei_modelextends_interface{}constmodel=mongoose.model(model_name,newSchema({uid:{type:String},name:{type:String,minlength:1,maxlength:15},twitterUrl:{type:String},githubUrl:{type:String},createdAt:{type:Date},updatedAt:{type:Date},}).index({uid:1},{unique:true}));// 作成exportconstcreate=async(params:Pick<i_model,'uid'>)=>{const_data:_interface={uid:params.uid,name:'名無し',twitterUrl:'',githubUrl:'',createdAt:newDate(),updatedAt:newDate(),};return(awaitmodel.insertMany([_data]))asi_model[];};// 抽出exportconstfind=async(params:Pick<i_model,'uid'>)=>{const_data:Pick<i_model,'uid'>={uid:params.uid};return(awaitmodel.find(_data))asi_model[];};// 更新exportconstupdate=async(uid:string,params:Pick<i_model,'name'|'twitterUrl'|'githubUrl'>)=>{returnawaitmodel.updateOne({uid:uid},{$set:{...params,updatedAt:newDate()}});};
②Products(ポートフォリオ)
_id: unique
物理名 | 論理名 | 型 |
---|---|---|
_id | ポートフォリオのID | string |
uid | ユーザID | string |
type | ポートフォリオのタイプ | number |
title | ポートフォリオのタイトル | string |
url | ポートフォリオのURL | string |
repo | リポジトリのURL | string |
createdAt | 作成時間 | Date |
updatedAt | 更新時間 | Date |
src/mongoose/collection/products.ts
import*asmongoosefrom'mongoose';importSchemafrom'src/mongoose';constmodel_name='products';constpagingNum=5;interface_interface{uid:string;type:number;title:string;url:string;repo:string;createdAt:Date;updatedAt:Date;}interfacei_modelextendsmongoose.Document{}interfacei_modelextends_interface{}constmodel=mongoose.model(model_name,newSchema({uid:{type:String},type:{type:Number,min:0,max:5},title:{type:String,minlength:1,maxlength:30},url:{type:String,minlength:1,maxlength:100},repo:{type:String,maxlength:100},createdAt:{type:Date},updatedAt:{type:Date},}));// 作成exportconstcreate=async(params:Pick<i_model,'uid'|'type'|'title'|'url'|'repo'>)=>{const_data:_interface={uid:params.uid,type:params.type,title:params.title,url:params.url,repo:params.repo,createdAt:newDate(),updatedAt:newDate(),};return(awaitmodel.insertMany([_data]))asi_model[];};// 更新exportconstupdate=async(id:string,uid:string,params:Pick<i_model,'type'|'title'|'url'|'repo'>)=>{returnawaitmodel.updateOne({_id:id,uid:uid},{$set:{...params,updatedAt:newDate()}});};// 削除exportconstdeleteProduct=async(id:string,uid:string)=>{returnawaitmodel.deleteOne({_id:id,uid:uid});};// 全投稿数exportconstcountAll=async()=>{returnmodel.find({}).countDocuments();};// ジャンル別投稿数exportconstcountType=async(type:number)=>{returnmodel.find({type:type}).countDocuments();};// タイトル別投稿数exportconstcountTitle=async(title:string)=>{returnmodel.find({title:{$regex:title}}).countDocuments();};// ユーザ別投稿数exportconstcountUser=async(uid:string)=>{returnmodel.find({uid:uid}).countDocuments();};// ページング全投稿exportconstpagingAll=async(num:number)=>{returnawaitmodel.aggregate([{$match:{},},{$lookup:{from:'users',localField:'uid',foreignField:'uid',as:'users_info',},},{$sort:{createdAt:-1},},{$skip:num*pagingNum,},{$limit:pagingNum},{$project:{_id:'$_id',type:'$type',title:'$title',url:'$url',repo:'$repo',name:'$users_info.name',uid:'$uid',createdAt:'$createdAt',updatedAt:'$updatedAt',},},]);};// ページングタイプ別exportconstpagingType=async(num:number,type:number)=>{returnawaitmodel.aggregate([{$match:{type:type,},},{$lookup:{from:'users',localField:'uid',foreignField:'uid',as:'users_info',},},{$sort:{createdAt:-1},},{$skip:num*pagingNum,},{$limit:pagingNum},{$project:{_id:'$_id',type:'$type',title:'$title',url:'$url',repo:'$repo',name:'$users_info.name',uid:'$uid',createdAt:'$createdAt',updatedAt:'$updatedAt',},},]);};// ページングタイトル別exportconstpagingTitle=async(num:number,title:string)=>{returnawaitmodel.aggregate([{$match:{title:{$regex:title},},},{$lookup:{from:'users',localField:'uid',foreignField:'uid',as:'users_info',},},{$sort:{createdAt:-1},},{$skip:num*pagingNum,},{$limit:pagingNum},{$project:{_id:'$_id',type:'$type',title:'$title',url:'$url',repo:'$repo',name:'$users_info.name',uid:'$uid',createdAt:'$createdAt',updatedAt:'$updatedAt',},},]);};// ページングユーザ別exportconstpagingUser=async(num:number,uid:string)=>{returnawaitmodel.aggregate([{$match:{uid:uid,},},{$lookup:{from:'users',localField:'uid',foreignField:'uid',as:'users_info',},},{$sort:{createdAt:-1},},{$skip:num*pagingNum,},{$limit:pagingNum},{$project:{_id:'$_id',type:'$type',title:'$title',url:'$url',repo:'$repo',name:'$users_info.name',uid:'$uid',createdAt:'$createdAt',updatedAt:'$updatedAt',},},]);};
REST API
ユーザ作成・ログイン
リクエストURL
Post
/v1/create/user
リクエストヘッダー
Authorization: Firebase Authorizationで発行されたjwttoken
Content-Type: application/json
リクエストパラメーター
{}
レスポンス
{}
ポートフォリオ投稿
リクエストURL
Post
/v1/create/product
リクエストヘッダー
Authorization: Firebase Authorizationで発行されたjwttoken
Content-Type: application/json
リクエストパラメーター
{
// ポートフォリオのタイトル
title: string;
// ポートフォリオのURL
url: string;
// ポートフォリオのリポジトリURL
repo: string;
// 0: Webアプリ
// 1: スマホアプリ
// 2: デスクトップアプリ
// 3: スクレイピング
// 4: ホムペ
// 5: その他
type: number;
}
レスポンス
{}
ユーザプロフィール更新
リクエストURL
Put
/v1/update/user
リクエストヘッダー
Authorization: Firebase Authorizationで発行されたjwttoken
Content-Type: application/json
リクエストパラメーター
{
// ユーザ名
name: string;
// GitHubのURL
githubUrl: string;
// TwitterのURL
twitterUrl: string;
}
レスポンス
{}
ポートフォリオ更新
リクエストURL
Put
/v1/update/product
リクエストヘッダー
Authorization: Firebase Authorizationで発行されたjwttoken
Content-Type: application/json
リクエストパラメーター
{
// ポートフォリオのID
id: string;
// ポートフォリオのタイトル
title: string;
// ポートフォリオのURL
url: string;
// ポートフォリオのリポジトリURL
repo: string;
// 0: Webアプリ
// 1: スマホアプリ
// 2: デスクトップアプリ
// 3: スクレイピング
// 4: ホムペ
// 5: その他
type: number;
}
レスポンス
{}
ポートフォリオ削除
リクエストURL
Delete
/v1/cancel/product
リクエストヘッダー
Authorization: Firebase Authorizationで発行されたjwttoken
Content-Type: application/json
リクエストパラメーター
{
// ポートフォリオのID
id: string;
}
レスポンス
{}
ユーザプロフィール参照
リクエストURL
Get
/v1/find/user/:uid
リクエストヘッダー
Authorization: null以外の値
リクエストパラメーター
{
// ユーザID
uid: string;
}
レスポンス
{
// ユーザ名
name: string;
// TwitterのURL
twitterUrl: string;
// GitHubのURL
githubUrl: string;
}
ページング全投稿(5件)
リクエストURL
Get
/v1/paging/all/:num
リクエストヘッダー
Authorization: null以外の値
リクエストパラメーター
{
// 1ページ目は1, 2ページ目は2....
num: string;
}
レスポンス
{
// 件数
count: number;
list: {
// ポートフォリオのID
_id: string;
// 0: Webアプリ
// 1: スマホアプリ
// 2: デスクトップアプリ
// 3: スクレイピング
// 4: ホムペ
// 5: その他
type: number;
// ポートフォリオのタイトル
title: string;
// ポートフォリオのURL
url: string;
// ポートフォリオのリポジトリURL
repo: string;
// 投稿者名
name: string[];
// ユーザID
uid: string;
// 作成日
createdAt: Date;
// 更新日
updatedAt: Date;
}[];
}
ページングポートフォリオのタイトル別(5件)
リクエストURL
Get
/v1/paging/title/:title/:num
リクエストヘッダー
Authorization: null以外の値
リクエストパラメーター
{
// 1ページ目は1, 2ページ目は2....
num: string;
title: string;
}
レスポンス
{
// 件数
count: number;
list: {
// ポートフォリオのID
_id: string;
// 0: Webアプリ
// 1: スマホアプリ
// 2: デスクトップアプリ
// 3: スクレイピング
// 4: ホムペ
// 5: その他
type: number;
// ポートフォリオのタイトル
title: string;
// ポートフォリオのURL
url: string;
// ポートフォリオのリポジトリURL
repo: string;
// 投稿者名
name: string[];
// ユーザID
uid: string;
// 作成日
createdAt: Date;
// 更新日
updatedAt: Date;
}[];
}
ページングタイプ別(5件)
リクエストURL
Get
/v1/paging/type/:type/:num
リクエストヘッダー
Authorization: null以外の値
リクエストパラメーター
{
// 1ページ目は1, 2ページ目は2....
num: string;
// 0: Webアプリ
// 1: スマホアプリ
// 2: デスクトップアプリ
// 3: スクレイピング
// 4: ホムペ
// 5: その他
type: string;
}
レスポンス
{
// 件数
count: number;
list: {
// ポートフォリオのID
_id: string;
// 0: Webアプリ
// 1: スマホアプリ
// 2: デスクトップアプリ
// 3: スクレイピング
// 4: ホムペ
// 5: その他
type: number;
// ポートフォリオのタイトル
title: string;
// ポートフォリオのURL
url: string;
// ポートフォリオのリポジトリURL
repo: string;
// 投稿者名
name: string[];
// ユーザID
uid: string;
// 作成日
createdAt: Date;
// 更新日
updatedAt: Date;
}[];
}
ページングユーザ投稿別
リクエストURL
Get
/v1/paging/user/:uid/:num
リクエストヘッダー
Authorization: null以外の値
リクエストパラメーター
{
// 1ページ目は1, 2ページ目は2....
num: string;
uid: string;
}
レスポンス
{
// 件数
count: number;
list: {
// ポートフォリオのID
_id: string;
// 0: Webアプリ
// 1: スマホアプリ
// 2: デスクトップアプリ
// 3: スクレイピング
// 4: ホムペ
// 5: その他
type: number;
// ポートフォリオのタイトル
title: string;
// ポートフォリオのURL
url: string;
// ポートフォリオのリポジトリURL
repo: string;
// 投稿者名
name: string[];
// ユーザID
uid: string;
// 作成日
createdAt: Date;
// 更新日
updatedAt: Date;
}[];
}
src/route/index.ts
import*asExpressfrom'express';import*asCorsfrom'cors';import*asDotEnvfrom'dotenv';importConstantfrom'src/constant';// route---importcreate_friendfrom'src/route/create/friend';importcreate_userfrom'src/route/create/user';importcreate_productfrom'src/route/create/product';importpaging_allfrom'src/route/paging/all';importpaging_titlefrom'src/route/paging/title';importpaging_typefrom'src/route/paging/type';importpaging_userfrom'src/route/paging/users';importupdate_productfrom'src/route/update/product';importupdate_userfrom'src/route/update/user';importcancel_friendfrom'src/route/cancel/friend';importcancel_productfrom'src/route/cancel/product';importfind_userfrom'src/route/find/user';// route---DotEnv.config();constapp=Express();constrouter=Express.Router();// middleware---app.use(Cors({origin:process.env.ORIGIN_URL}));app.use('/.netlify/functions/api',router);app.use(Express.urlencoded({extended:true}));app.use((req:Express.Request,res:Express.Response,next:Express.NextFunction)=>{req.headers.authorization!==undefined?next():res.sendStatus(403);});app.use((_,__,res:Express.Response,___)=>{res.sendStatus(500);});// middleware---// routing---// ユーザ作成router.post(Constant.API_VERSION+Constant.URL['/create/user'],create_user);// 投稿router.post(Constant.API_VERSION+Constant.URL['/create/product'],create_product);// フォローするrouter.post(Constant.API_VERSION+Constant.URL['/create/friend'],create_friend);// ページング全投稿router.get(Constant.API_VERSION+Constant.URL['/paging/all']+'/:num',paging_all);// ページングタイプ別router.get(Constant.API_VERSION+Constant.URL['/paging/type']+'/:type'+'/:num',paging_type);// ページングタイトル別router.get(Constant.API_VERSION+Constant.URL['/paging/title']+'/:title'+'/:num',paging_title);// ページングユーザ別router.get(Constant.API_VERSION+Constant.URL['/paging/user']+'/:uid'+'/:num',paging_user);// プロフィールrouter.get(Constant.API_VERSION+Constant.URL['/find/user']+'/:uid',find_user);// 更新 ユーザrouter.put(Constant.API_VERSION+Constant.URL['/update/user'],update_user);// 更新 記事router.put(Constant.API_VERSION+Constant.URL['/update/product'],update_product);// フォローはずすrouter.delete(Constant.API_VERSION+Constant.URL['/cancel/friend'],cancel_friend);// 投稿削除router.delete(Constant.API_VERSION+Constant.URL['/cancel/product'],cancel_product);// routing---exportdefaultapp;