Browse Source

Implement git methods

Add endpoints for git create and delete repos.
master
TheoryOfNekomata 1 year ago
parent
commit
150a38ed5c
13 changed files with 303 additions and 8 deletions
  1. +3
    -1
      package.json
  2. +34
    -0
      src/modules/git/Git.controller.ts
  3. +28
    -0
      src/modules/git/Git.service.ts
  4. +3
    -0
      src/modules/git/index.ts
  5. +3
    -0
      src/modules/git/models.ts
  6. +68
    -0
      src/packages/fastify-compliant-http-errors/index.ts
  7. +23
    -0
      src/packages/fastify-integration-tests/index.ts
  8. +53
    -0
      src/packages/fastify-send-data/index.ts
  9. +26
    -0
      src/packages/fastify-service-session/index.ts
  10. +16
    -2
      src/routes.ts
  11. +16
    -5
      src/server.ts
  12. +7
    -0
      src/utils/types.ts
  13. +23
    -0
      yarn.lock

+ 3
- 1
package.json View File

@@ -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",


+ 34
- 0
src/modules/git/Git.controller.ts View File

@@ -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());
}
}

+ 28
- 0
src/modules/git/Git.service.ts View File

@@ -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');
}
}

+ 3
- 0
src/modules/git/index.ts View File

@@ -0,0 +1,3 @@
export * from './Git.service'
export * from './Git.controller'
export * from './models'

+ 3
- 0
src/modules/git/models.ts View File

@@ -0,0 +1,3 @@
import * as codeCore from '@modal/code-core'

export type CreateRepoOptions = codeCore.git.CreateOptions

+ 68
- 0
src/packages/fastify-compliant-http-errors/index.ts View File

@@ -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 }

+ 23
- 0
src/packages/fastify-integration-tests/index.ts View File

@@ -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)
}

+ 53
- 0
src/packages/fastify-send-data/index.ts View File

@@ -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)

+ 26
- 0
src/packages/fastify-service-session/index.ts View File

@@ -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)

+ 16
- 2
src/routes.ts View File

@@ -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,
})
}

+ 16
- 5
src/server.ts View File

@@ -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;
}

+ 7
- 0
src/utils/types.ts View File

@@ -0,0 +1,7 @@
import { RouteHandlerMethod } from 'fastify'

type Controller<T extends string> = {
[key in T]: RouteHandlerMethod;
};

export default Controller

+ 23
- 0
yarn.lock View File

@@ -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"


Loading…
Cancel
Save