@@ -57,7 +57,9 @@ | |||
"vitest": "^0.9.4" | |||
}, | |||
"dependencies": { | |||
"fastify": "^3.27.0" | |||
"@theoryofnekomata/uuid-buffer": "^0.1.0", | |||
"fastify": "^3.27.0", | |||
"fastify-plugin": "^3.0.1" | |||
}, | |||
"scripts": { | |||
"prepublishOnly": "pridepack clean && pridepack build", | |||
@@ -0,0 +1,34 @@ | |||
import {GitService, GitServiceImpl} from './Git.service'; | |||
import {RouteHandlerMethod} from 'fastify'; | |||
import {constants} from 'http2'; | |||
import * as codeCore from '@modal/code-core'; | |||
import {HttpResponse} from '../../packages/fastify-send-data'; | |||
import Controller from '../../utils/types'; | |||
import {Uuid} from '@theoryofnekomata/uuid-buffer'; | |||
export interface GitController extends Controller< | |||
'createRepo' | |||
| 'deleteRepo' | |||
> {} | |||
class RepoCreatedResponse extends HttpResponse<codeCore.git.Repo>(constants.HTTP_STATUS_CREATED, 'Repo Created') {} | |||
class RepoDeletedResponse extends HttpResponse(constants.HTTP_STATUS_NO_CONTENT, 'Repo Deleted') {} | |||
export class GitControllerImpl implements GitController { | |||
private readonly gitService: GitService; | |||
constructor() { | |||
this.gitService = new GitServiceImpl() | |||
} | |||
readonly createRepo: RouteHandlerMethod = async (request, reply) => { | |||
const repo = await this.gitService.createRepo(request.body as codeCore.git.CreateRepoData, request.user); | |||
reply.sendData(new RepoCreatedResponse(repo)); | |||
} | |||
readonly deleteRepo: RouteHandlerMethod = async (request, reply) => { | |||
const params = request.params as { repoId: string } | |||
await this.gitService.deleteRepo(Uuid.from(params.repoId), request.user) | |||
reply.sendData(new RepoDeletedResponse()); | |||
} | |||
} |
@@ -0,0 +1,28 @@ | |||
import * as codeCore from '@modal/code-core'; | |||
export interface GitService { | |||
createRepo(options: codeCore.git.CreateRepoData, user?: codeCore.common.User): Promise<codeCore.git.Repo>; | |||
deleteRepo(repoId: codeCore.git.Repo['id'], user?: codeCore.common.User): Promise<void> | |||
} | |||
export class GitServiceImpl implements GitService { | |||
private readonly coreGitService: codeCore.git.GitService; | |||
constructor() { | |||
this.coreGitService = new codeCore.git.GitServiceImpl(); | |||
} | |||
async createRepo(options: codeCore.git.CreateRepoData, user?: codeCore.common.User): Promise<codeCore.git.Repo> { | |||
if (user) { | |||
return this.coreGitService.create(options, user); | |||
} | |||
throw new Error('Unauthorized'); | |||
} | |||
async deleteRepo(repoId: codeCore.git.Repo['id'], user?: codeCore.common.User): Promise<void> { | |||
if (user) { | |||
await this.coreGitService.delete(repoId, user); | |||
} | |||
throw new Error('Unauthorized'); | |||
} | |||
} |
@@ -0,0 +1,3 @@ | |||
export * from './Git.service' | |||
export * from './Git.controller' | |||
export * from './models' |
@@ -0,0 +1,3 @@ | |||
import * as codeCore from '@modal/code-core' | |||
export type CreateRepoOptions = codeCore.git.CreateOptions |
@@ -0,0 +1,68 @@ | |||
import { FastifyReply, FastifyRequest } from 'fastify' | |||
type AppErrorOptions = { | |||
cause?: Error, | |||
} | |||
export class AppError extends Error { | |||
public readonly cause?: Error | |||
constructor(message?: string, options?: AppErrorOptions) { | |||
super(message) | |||
if (typeof options === 'object') { | |||
this.cause = options.cause | |||
} | |||
} | |||
} | |||
const httpErrorClassFactory = (statusCode: number, statusMessage?: string) => class HttpError extends AppError { | |||
readonly statusCode = statusCode | |||
readonly statusMessage = statusMessage | |||
} | |||
interface CustomFastifyError extends Error { | |||
statusMessage?: string; | |||
validation: { | |||
keyword: string, | |||
instancePath: string, | |||
params: Record<string, unknown>, | |||
message: string, | |||
}[]; | |||
statusCode?: number; | |||
errors: Record<string, unknown>[]; | |||
detail?: string; | |||
} | |||
export const fastifyErrorHandler = (error: CustomFastifyError, _request: FastifyRequest, reply: FastifyReply) => { | |||
reply.headers({ | |||
'Content-Type': 'application/problem+json', | |||
}) | |||
if (error.validation) { | |||
reply.status(400) | |||
reply.raw.statusMessage = 'Invalid Request' | |||
reply.send({ | |||
title: 'There are errors in the request body.', | |||
'invalid-params': error.validation.map(v => ({ | |||
name: ( | |||
v.keyword === 'required' | |||
? `${v.instancePath}/${v.params.missingProperty}` | |||
: v.instancePath | |||
).replaceAll('/', '.'), | |||
reason: v.message, | |||
})), | |||
}) | |||
return | |||
} | |||
if (Number.isNaN(reply.raw.statusCode)) { | |||
reply.status(error?.statusCode ?? 500) | |||
} | |||
reply.raw.statusMessage = error.statusMessage ?? 'Unknown Error' | |||
reply.send({ | |||
title: reply.raw.statusMessage, | |||
detail: error.message, | |||
}) | |||
} | |||
export { httpErrorClassFactory as HttpError } |
@@ -0,0 +1,23 @@ | |||
import { FastifyInstance, InjectOptions, LightMyRequestResponse } from 'fastify' | |||
export type MockResponse = LightMyRequestResponse | |||
export type MockResponseThunk = () => Promise<MockResponse> | |||
export type MockClient = (opts: InjectOptions) => Promise<MockResponse> | |||
export const makeJsonRequestClient = (server: FastifyInstance): MockClient => (opts) => { | |||
const inject = { | |||
...opts, | |||
headers: { | |||
...(opts.headers ?? {}), | |||
'accept': 'applicaton/json', | |||
} as Record<string, string>, | |||
} | |||
if (opts.payload) { | |||
inject.headers['content-type'] = 'application/json' | |||
inject.payload = JSON.stringify(opts.payload) | |||
} | |||
return server.inject(inject) | |||
} |
@@ -0,0 +1,53 @@ | |||
import { FastifyInstance, FastifyReply } from 'fastify' | |||
import fp from 'fastify-plugin' | |||
import { constants } from 'http2' | |||
export interface ResponseDataInterface<T extends unknown = undefined> { | |||
statusCode: number | |||
statusMessage?: string | |||
body?: T extends undefined ? undefined : { data: T } | |||
} | |||
const fastifySendData = async (app: FastifyInstance) => { | |||
const sendDataKey = 'sendData' as const | |||
app.decorateReply(sendDataKey, function reply<T extends ResponseDataInterface>(this: FastifyReply, data: T) { | |||
this.status(data.statusCode) | |||
if (data.statusMessage) { | |||
this.raw.statusMessage = data.statusMessage | |||
} | |||
if (data.statusCode !== constants.HTTP_STATUS_NO_CONTENT) { | |||
this.send(data.body) | |||
return | |||
} | |||
this.removeHeader('content-type') | |||
this.removeHeader('content-length') | |||
this.send() | |||
}) | |||
} | |||
type SendData<O extends unknown = unknown, T extends ResponseDataInterface<O> = ResponseDataInterface<O>> = (data: T) => void | |||
declare module 'fastify' { | |||
interface FastifyReply { | |||
sendData: SendData | |||
} | |||
} | |||
export const HttpResponse = <T extends unknown = undefined>( | |||
statusCode = constants.HTTP_STATUS_OK, | |||
statusMessage?: string, | |||
) => class HttpResponseData { | |||
readonly statusCode = statusCode | |||
readonly statusMessage = statusMessage | |||
readonly body?: { data: T } | |||
constructor(data?: T) { | |||
if (data && statusCode !== constants.HTTP_STATUS_NO_CONTENT) { | |||
this.body = { | |||
data, | |||
} | |||
} | |||
} | |||
} | |||
export default fp(fastifySendData) |
@@ -0,0 +1,26 @@ | |||
import fp from 'fastify-plugin' | |||
import { FastifyInstance, FastifyRequest } from 'fastify' | |||
export interface FastifySessionOpts<SessionType = unknown, SessionId = string, RequestType extends FastifyRequest = FastifyRequest> { | |||
sessionRequestKey?: string, | |||
extractSessionId: (request: RequestType) => SessionId | null | undefined, | |||
isSessionValid: (sessionId: SessionId) => Promise<boolean>, | |||
getSession: (sessionId: SessionId) => Promise<SessionType> | |||
} | |||
const fastifySession = async (app: FastifyInstance, opts: FastifySessionOpts) => { | |||
const { sessionRequestKey = 'session' } = opts | |||
app.decorateRequest(sessionRequestKey, null) | |||
app.addHook('onRequest', async (request: FastifyRequest) => { | |||
const sessionId = opts.extractSessionId(request) | |||
if (typeof sessionId === 'string') { | |||
const isSessionValid = await opts.isSessionValid(sessionId) | |||
if (isSessionValid) { | |||
const mutableRequest = (request as unknown) as Record<string, unknown> | |||
mutableRequest[sessionRequestKey] = await opts.getSession(sessionId) | |||
} | |||
} | |||
}) | |||
} | |||
export default fp(fastifySession) |
@@ -1,7 +1,21 @@ | |||
import { FastifyInstance } from 'fastify'; | |||
import * as git from './modules/git' | |||
export const addRoutes = (SERVER: FastifyInstance) => { | |||
SERVER.get('/', async (_, reply) => { | |||
export const addRoutes = (server: FastifyInstance) => { | |||
server.get('/', async (_, reply) => { | |||
reply.send({hello: 'world'}) | |||
}); | |||
const gitController = new git.GitControllerImpl() | |||
server.route({ | |||
method: 'POST', | |||
url: '/repos', | |||
handler: gitController.createRepo, | |||
}) | |||
server.route({ | |||
method: 'DELETE', | |||
url: '/repos/:id', | |||
handler: gitController.deleteRepo, | |||
}) | |||
} |
@@ -1,9 +1,20 @@ | |||
import fastify from 'fastify'; | |||
import fastify, {FastifyServerOptions} from 'fastify'; | |||
import * as codeCore from '@modal/code-core'; | |||
import {fastifyErrorHandler} from './packages/fastify-compliant-http-errors'; | |||
import fastifySendData from './packages/fastify-send-data'; | |||
export const createServer = () => { | |||
const server = fastify({ | |||
logger: true, | |||
}) | |||
declare module 'fastify' { | |||
interface FastifyRequest { | |||
user?: codeCore.common.User, | |||
} | |||
} | |||
export const createServer = (opts?: FastifyServerOptions) => { | |||
const server = fastify(opts) | |||
server.setErrorHandler(fastifyErrorHandler) | |||
server.register(fastifySendData) | |||
return server; | |||
} |
@@ -0,0 +1,7 @@ | |||
import { RouteHandlerMethod } from 'fastify' | |||
type Controller<T extends string> = { | |||
[key in T]: RouteHandlerMethod; | |||
}; | |||
export default Controller |
@@ -359,6 +359,14 @@ | |||
resolved "https://registry.yarnpkg.com/@ovyerus/licenses/-/licenses-6.4.4.tgz#596e3ace46ab7c70bcf0e2b17f259796a4bedf9f" | |||
integrity sha512-IHjc31WXciQT3hfvdY+M59jBkQp70Fpr04tNDVO5rez2PNv4u8tE6w//CkU+GeBoO9k2ahneSqzjzvlgjyjkGw== | |||
"@theoryofnekomata/uuid-buffer@^0.1.0": | |||
version "0.1.0" | |||
resolved "https://js.pack.modal.sh/@theoryofnekomata%2fuuid-buffer/-/uuid-buffer-0.1.0.tgz#0917314e8230ce1a2047172b3277512bc0fd73a3" | |||
integrity sha512-DUKQE2UmS9vq+5kNp1f50U+XLdmgTEKWxRgeCgasXCipL7JNVQoYqwwcuDlCb+yNdqQ2/fNbAEWHKs3kRfa6+w== | |||
dependencies: | |||
"@types/uuid" "^8.3.4" | |||
uuid "^8.3.2" | |||
"@types/chai-subset@^1.3.3": | |||
version "1.3.3" | |||
resolved "https://registry.yarnpkg.com/@types/chai-subset/-/chai-subset-1.3.3.tgz#97893814e92abd2c534de422cb377e0e0bdaac94" | |||
@@ -386,6 +394,11 @@ | |||
resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.35.tgz#635b7586086d51fb40de0a2ec9d1014a5283ba4a" | |||
integrity sha512-vu1SrqBjbbZ3J6vwY17jBs8Sr/BKA+/a/WtjRG+whKg1iuLFOosq872EXS0eXWILdO36DHQQeku/ZcL6hz2fpg== | |||
"@types/uuid@^8.3.4": | |||
version "8.3.4" | |||
resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-8.3.4.tgz#bd86a43617df0594787d38b735f55c805becf1bc" | |||
integrity sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw== | |||
"@typescript-eslint/eslint-plugin@^5.9.0": | |||
version "5.25.0" | |||
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.25.0.tgz#e8ce050990e4d36cc200f2de71ca0d3eb5e77a31" | |||
@@ -1531,6 +1544,11 @@ fast-safe-stringify@^2.0.8: | |||
resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz#c406a83b6e70d9e35ce3b30a81141df30aeba884" | |||
integrity sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA== | |||
fastify-plugin@^3.0.1: | |||
version "3.0.1" | |||
resolved "https://registry.yarnpkg.com/fastify-plugin/-/fastify-plugin-3.0.1.tgz#79e84c29f401020f38b524f59f2402103fd21ed2" | |||
integrity sha512-qKcDXmuZadJqdTm6vlCqioEbyewF60b/0LOFCcYN1B6BIZGlYJumWWOYs70SFYLDAH4YqdE1cxH/RKMG7rFxgA== | |||
fastify@^3.27.0: | |||
version "3.29.0" | |||
resolved "https://registry.yarnpkg.com/fastify/-/fastify-3.29.0.tgz#b840107f4fd40cc999b886548bfcda8062e38168" | |||
@@ -3058,6 +3076,11 @@ util-deprecate@^1.0.1, util-deprecate@^1.0.2: | |||
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" | |||
integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= | |||
uuid@^8.3.2: | |||
version "8.3.2" | |||
resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" | |||
integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== | |||
v8-compile-cache@^2.0.3: | |||
version "2.3.0" | |||
resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee" | |||