timetable-app

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README | LICENSE

commit 5672d63f2a164a472bd0cb98bb38c65e1a7bd61f
parent a883703070f3d62b6c922ac9af509590a0254757
Author: Yuukin256 <Yuukin256@gmail.com>
Date:   Sat,  5 Nov 2022 01:20:59 +0900

APIを修正

Diffstat:
M.eslintrc.yml | 8+++++---
Msrc/api/classes/_classId@number/standard.ts | 11+++++++----
Msrc/pages/api/rest/v1/classes/[classId]/standard/api-types.ts | 2++
Msrc/pages/api/rest/v1/classes/[classId]/standard/api-types.validator.ts | 51+++++++++++++++++++++++++++++++++++++++++++++++++--
Msrc/pages/api/rest/v1/classes/[classId]/standard/index.ts | 98++++++++++++++++++++++++++++---------------------------------------------------
Asrc/pages/api/rest/v1/classes/api-types.ts | 3+++
Asrc/pages/api/rest/v1/classes/api-types.validator.ts | 60++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/pages/api/rest/v1/classes/index.ts | 27++++++++++++++++++---------
8 files changed, 178 insertions(+), 82 deletions(-)

diff --git a/.eslintrc.yml b/.eslintrc.yml @@ -39,9 +39,11 @@ rules: '@typescript-eslint/consistent-type-imports': error '@typescript-eslint/no-unused-vars': - - error - - varsIgnorePattern: _ - ignoreRestSiblings: true + - error + - varsIgnorePattern: _ + ignoreRestSiblings: true + + unused-imports/no-unused-imports: warn react/jsx-key: diff --git a/src/api/classes/_classId@number/standard.ts b/src/api/classes/_classId@number/standard.ts @@ -1,11 +1,14 @@ import type { DefineMethods } from 'aspida'; -import type { PostApiRequestBody } from 'pages/api/rest/v1/classes/[classId]/standard/api-types'; -import type { StandardEn } from 'types'; +import type { + GetApiResponseBody, + PostApiRequestBody, + PostApiResponseBody, +} from 'pages/api/rest/v1/classes/[classId]/standard/api-types'; export type Methods = DefineMethods<{ - get: { resBody: StandardEn }; + get: { resBody: GetApiResponseBody }; post: { reqBody: PostApiRequestBody; - resBody: StandardEn; + resBody: PostApiResponseBody; }; }>; diff --git a/src/pages/api/rest/v1/classes/[classId]/standard/api-types.ts b/src/pages/api/rest/v1/classes/[classId]/standard/api-types.ts @@ -1,3 +1,5 @@ import type { StandardEn } from 'types'; +export type GetApiResponseBody = StandardEn; export type PostApiRequestBody = StandardEn; +export type PostApiResponseBody = StandardEn; diff --git a/src/pages/api/rest/v1/classes/[classId]/standard/api-types.validator.ts b/src/pages/api/rest/v1/classes/[classId]/standard/api-types.validator.ts @@ -6,9 +6,8 @@ import * as apiTypes from './api-types'; export const SCHEMA = { "$schema": "http://json-schema.org/draft-07/schema#", - "$ref": "#/definitions/PostApiRequestBody", "definitions": { - "PostApiRequestBody": { + "GetApiResponseBody": { "$ref": "#/definitions/StandardEn" }, "StandardEn": { @@ -256,10 +255,37 @@ export const SCHEMA = { "Fri7" ], "additionalProperties": false + }, + "PostApiRequestBody": { + "$ref": "#/definitions/StandardEn" + }, + "PostApiResponseBody": { + "$ref": "#/definitions/StandardEn" } } }; const ajv = new Ajv({ removeAdditional: true }).addSchema(SCHEMA, "SCHEMA"); +export function validateGetApiResponseBody(payload: unknown): apiTypes.GetApiResponseBody { + /** Schema is defined in {@link SCHEMA.definitions.GetApiResponseBody } **/ + const validator = ajv.getSchema("SCHEMA#/definitions/GetApiResponseBody"); + const valid = validator(payload); + if (!valid) { + const error = new Error('Invalid GetApiResponseBody: ' + ajv.errorsText(validator.errors, {dataVar: "GetApiResponseBody"})); + error.name = "ValidationError"; + throw error; + } + return payload; +} + +export function isGetApiResponseBody(payload: unknown): payload is apiTypes.GetApiResponseBody { + try { + validateGetApiResponseBody(payload); + return true; + } catch (error) { + return false; + } +} + export function validatePostApiRequestBody(payload: unknown): apiTypes.PostApiRequestBody { /** Schema is defined in {@link SCHEMA.definitions.PostApiRequestBody } **/ const validator = ajv.getSchema("SCHEMA#/definitions/PostApiRequestBody"); @@ -280,3 +306,24 @@ export function isPostApiRequestBody(payload: unknown): payload is apiTypes.Post return false; } } + +export function validatePostApiResponseBody(payload: unknown): apiTypes.PostApiResponseBody { + /** Schema is defined in {@link SCHEMA.definitions.PostApiResponseBody } **/ + const validator = ajv.getSchema("SCHEMA#/definitions/PostApiResponseBody"); + const valid = validator(payload); + if (!valid) { + const error = new Error('Invalid PostApiResponseBody: ' + ajv.errorsText(validator.errors, {dataVar: "PostApiResponseBody"})); + error.name = "ValidationError"; + throw error; + } + return payload; +} + +export function isPostApiResponseBody(payload: unknown): payload is apiTypes.PostApiResponseBody { + try { + validatePostApiResponseBody(payload); + return true; + } catch (error) { + return false; + } +} diff --git a/src/pages/api/rest/v1/classes/[classId]/standard/index.ts b/src/pages/api/rest/v1/classes/[classId]/standard/index.ts @@ -1,82 +1,52 @@ -import { List } from 'immutable'; - -import { ABSTRACT_LESSON_NAMES } from 'constant'; import { prisma } from 'lib/prisma'; -import { isPostApiRequestBody } from './api-types.validator'; +import { isPostApiRequestBody, validateGetApiResponseBody, validatePostApiResponseBody } from './api-types.validator'; -import type { ClassUnit } from '@prisma/client'; import type { NextApiHandler } from 'next'; -import type { StandardEn } from 'types'; - -/** 完全 or 不完全 な ClassUnit 配列 から Standard データを返す */ -const classUnitsToStandard = (classUnits: ClassUnit[]): StandardEn => { - const find = (to: string) => classUnits.find((v) => v.abstractLessonName === to)?.subject ?? []; - return Object.fromEntries(ABSTRACT_LESSON_NAMES.map((v) => [v, find(v)])) as StandardEn; -}; const handler: NextApiHandler = async (req, res) => { - if (typeof req.query['classId'] !== 'string' || isNaN(parseInt(req.query['classId']))) { - return res.status(400).send('Bad Request'); - } - - const classId = parseInt(req.query['classId']); - - switch (req.method) { - case 'HEAD': { - return res.status(200).send('OK'); + try { + if (typeof req.query['classId'] !== 'string' || isNaN(parseInt(req.query['classId']))) { + return res.status(400).send('Bad Request'); } - case 'GET': { - const classUnits = await prisma.classUnit.findMany({ - where: { schoolClassId: { equals: classId } }, - }); + const classId = parseInt(req.query['classId']); - const standard = classUnitsToStandard(classUnits); - - return res.status(200).json(standard); - } - - case 'POST': { - if (isPostApiRequestBody(req.body)) { - const newStandard = req.body; - const oldClassUnits = await prisma.classUnit.findMany({ where: { schoolClassId: classId } }); - - // 各コマについてループ - // DBに変更を投げた Promise の配列が返る - const promises = ABSTRACT_LESSON_NAMES.flatMap((abstractLessonName) => { - const oldClassUnit = oldClassUnits.find((cu) => cu.abstractLessonName === abstractLessonName); - const newSubject = newStandard[abstractLessonName]; - - // 変化があった項目のみ更新(or作成) - if (oldClassUnit && !List(oldClassUnit.subject).equals(List(newSubject))) { - return prisma.classUnit.upsert({ - where: { schoolClassId_abstractLessonName: { schoolClassId: classId, abstractLessonName } }, - create: { abstractLessonName, subject: newSubject, SchoolClass: { connect: { id: classId } } }, - update: { subject: { set: newSubject } }, - }); - } + switch (req.method) { + case 'HEAD': { + return res.status(200).send('OK'); + } - return []; - }); + case 'GET': { + const { classId: _, ...standard } = await prisma.standard.findUniqueOrThrow({ where: { classId } }); - await Promise.all(promises); + return res.status(200).json(validateGetApiResponseBody(standard)); + } - // 更新後の Standard を取得して返す - const standard = classUnitsToStandard( - await prisma.classUnit.findMany({ - where: { schoolClassId: { equals: classId } }, - }) - ); - return res.status(200).json(standard); - } else { - return res.status(400).send('Bad Request'); + case 'POST': { + if (isPostApiRequestBody(req.body)) { + const newStandard = req.body; + + // DBを更新 + const { classId: _, ...updated } = await prisma.standard.upsert({ + where: { classId }, + create: { classId, ...newStandard }, + update: { ...newStandard }, + }); + + // 更新後の Standard を返す + return res.status(200).json(validatePostApiResponseBody(updated)); + } else { + return res.status(400).send('Bad Request'); + } } - } - default: { - return res.status(405).send('Method Not Allowed'); + default: { + return res.status(405).send('Method Not Allowed'); + } } + } catch { + return res.status(500).send('Internal Server Error'); } }; diff --git a/src/pages/api/rest/v1/classes/api-types.ts b/src/pages/api/rest/v1/classes/api-types.ts @@ -0,0 +1,3 @@ +import type { SchoolClass } from '@prisma/client'; + +export type GetApiResponseBody = SchoolClass[]; diff --git a/src/pages/api/rest/v1/classes/api-types.validator.ts b/src/pages/api/rest/v1/classes/api-types.validator.ts @@ -0,0 +1,60 @@ +// @ts-nocheck +// eslint-disable +// This file is generated by create-validator-ts +import Ajv from 'ajv'; +import * as apiTypes from './api-types'; + +export const SCHEMA = { + "$schema": "http://json-schema.org/draft-07/schema#", + "$ref": "#/definitions/GetApiResponseBody", + "definitions": { + "GetApiResponseBody": { + "type": "array", + "items": { + "$ref": "#/definitions/SchoolClass" + } + }, + "SchoolClass": { + "type": "object", + "properties": { + "id": { + "type": "number" + }, + "class": { + "type": "number" + }, + "grade": { + "type": "number" + } + }, + "required": [ + "id", + "class", + "grade" + ], + "additionalProperties": false, + "description": "Model SchoolClass" + } + } +}; +const ajv = new Ajv({ removeAdditional: true }).addSchema(SCHEMA, "SCHEMA"); +export function validateGetApiResponseBody(payload: unknown): apiTypes.GetApiResponseBody { + /** Schema is defined in {@link SCHEMA.definitions.GetApiResponseBody } **/ + const validator = ajv.getSchema("SCHEMA#/definitions/GetApiResponseBody"); + const valid = validator(payload); + if (!valid) { + const error = new Error('Invalid GetApiResponseBody: ' + ajv.errorsText(validator.errors, {dataVar: "GetApiResponseBody"})); + error.name = "ValidationError"; + throw error; + } + return payload; +} + +export function isGetApiResponseBody(payload: unknown): payload is apiTypes.GetApiResponseBody { + try { + validateGetApiResponseBody(payload); + return true; + } catch (error) { + return false; + } +} diff --git a/src/pages/api/rest/v1/classes/index.ts b/src/pages/api/rest/v1/classes/index.ts @@ -1,20 +1,29 @@ import { prisma } from 'lib/prisma'; +import { validateGetApiResponseBody } from './api-types.validator'; + import type { NextApiHandler } from 'next'; +// TODO: next-connect に置き換え const handler: NextApiHandler = async (req, res) => { - switch (req.method) { - case 'HEAD': { - return res.status(200).send('OK'); - } + try { + switch (req.method) { + case 'HEAD': { + return res.status(200).send('OK'); + } - case 'GET': { - return res.status(200).json(await prisma.schoolClass.findMany({ orderBy: [{ grade: 'asc' }, { class: 'asc' }] })); - } + case 'GET': { + const classes = await prisma.schoolClass.findMany({ orderBy: [{ grade: 'asc' }, { class: 'asc' }] }); + + return res.status(200).json(validateGetApiResponseBody(classes)); + } - default: { - return res.status(405).send('Method Not Allowed'); + default: { + return res.status(405).send('Method Not Allowed'); + } } + } catch { + return res.status(500).send('Internal Server Error'); } };