@@ -57,7 +57,9 @@ | |||||
"vitest": "^0.9.4" | "vitest": "^0.9.4" | ||||
}, | }, | ||||
"dependencies": { | "dependencies": { | ||||
"fastify": "^3.27.0" | |||||
"@theoryofnekomata/uuid-buffer": "^0.1.0", | |||||
"fastify": "^3.27.0", | |||||
"fastify-plugin": "^3.0.1" | |||||
}, | }, | ||||
"scripts": { | "scripts": { | ||||
"prepublishOnly": "pridepack clean && pridepack build", | "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 { 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'}) | 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; | 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" | resolved "https://registry.yarnpkg.com/@ovyerus/licenses/-/licenses-6.4.4.tgz#596e3ace46ab7c70bcf0e2b17f259796a4bedf9f" | ||||
integrity sha512-IHjc31WXciQT3hfvdY+M59jBkQp70Fpr04tNDVO5rez2PNv4u8tE6w//CkU+GeBoO9k2ahneSqzjzvlgjyjkGw== | 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": | "@types/chai-subset@^1.3.3": | ||||
version "1.3.3" | version "1.3.3" | ||||
resolved "https://registry.yarnpkg.com/@types/chai-subset/-/chai-subset-1.3.3.tgz#97893814e92abd2c534de422cb377e0e0bdaac94" | 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" | resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.35.tgz#635b7586086d51fb40de0a2ec9d1014a5283ba4a" | ||||
integrity sha512-vu1SrqBjbbZ3J6vwY17jBs8Sr/BKA+/a/WtjRG+whKg1iuLFOosq872EXS0eXWILdO36DHQQeku/ZcL6hz2fpg== | 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": | "@typescript-eslint/eslint-plugin@^5.9.0": | ||||
version "5.25.0" | version "5.25.0" | ||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.25.0.tgz#e8ce050990e4d36cc200f2de71ca0d3eb5e77a31" | 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" | resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz#c406a83b6e70d9e35ce3b30a81141df30aeba884" | ||||
integrity sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA== | 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: | fastify@^3.27.0: | ||||
version "3.29.0" | version "3.29.0" | ||||
resolved "https://registry.yarnpkg.com/fastify/-/fastify-3.29.0.tgz#b840107f4fd40cc999b886548bfcda8062e38168" | 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" | resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" | ||||
integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= | 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: | v8-compile-cache@^2.0.3: | ||||
version "2.3.0" | version "2.3.0" | ||||
resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee" | resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee" | ||||