From 1b8b26b68ececf6b013a31b75c24eb736326315c Mon Sep 17 00:00:00 2001 From: TheoryOfNekomata Date: Fri, 20 May 2022 17:14:25 +0800 Subject: [PATCH] Implement methods for creation and deletion Add methods for creation and deletion, covering metadata and file system. --- .gitignore | 1 + package.json | 5 ++ prisma/schema.prisma | 70 +++++++++++++++++ src/git.ts | 35 --------- src/index.ts | 2 +- src/modules/common/index.ts | 1 + src/modules/common/models.ts | 12 +++ src/modules/git/Git.service.ts | 133 +++++++++++++++++++++++++++++++++ src/modules/git/index.ts | 2 + src/modules/git/models.ts | 38 ++++++++++ src/modules/log/Log.service.ts | 33 ++++++++ src/modules/log/index.ts | 2 + src/modules/log/models.ts | 18 +++++ src/utils/error.ts | 1 + src/utils/fs.ts | 4 + yarn.lock | 42 +++++++++++ 16 files changed, 363 insertions(+), 36 deletions(-) create mode 100644 prisma/schema.prisma delete mode 100644 src/git.ts create mode 100644 src/modules/common/index.ts create mode 100644 src/modules/common/models.ts create mode 100644 src/modules/git/Git.service.ts create mode 100644 src/modules/git/index.ts create mode 100644 src/modules/git/models.ts create mode 100644 src/modules/log/Log.service.ts create mode 100644 src/modules/log/index.ts create mode 100644 src/modules/log/models.ts create mode 100644 src/utils/error.ts diff --git a/.gitignore b/.gitignore index 53992de..bdc24bf 100644 --- a/.gitignore +++ b/.gitignore @@ -105,3 +105,4 @@ dist .tern-port .npmrc +.database/ \ No newline at end of file diff --git a/package.json b/package.json index 5280f00..efc0893 100644 --- a/package.json +++ b/package.json @@ -80,5 +80,10 @@ "author": "TheoryOfNekomata ", "publishConfig": { "access": "public" + }, + "dependencies": { + "@prisma/client": "^3.14.0", + "@theoryofnekomata/uuid-buffer": "^0.1.0", + "prisma": "^3.14.0" } } diff --git a/prisma/schema.prisma b/prisma/schema.prisma new file mode 100644 index 0000000..0d101be --- /dev/null +++ b/prisma/schema.prisma @@ -0,0 +1,70 @@ +// This is your Prisma schema file, +// learn more about it in the docs: https://pris.ly/d/prisma-schema + +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "sqlite" + url = env("DATABASE_URL") +} + +model User { + id Bytes @id + username String @unique + password String + createdAt DateTime @default(now()) + properties Property[] + userOrgs UserOrg[] +} + +model Property { + id Bytes @id + userId Bytes + user User @relation(fields: [userId], references: [id]) + name String + value String +} + +model Org { + id Bytes @id + name String @unique + description String + createdAt DateTime @default(now()) + orgUsers UserOrg[] +} + +model UserOrg { + id Int @id @default(autoincrement()) + userId Bytes + user User @relation(fields: [userId], references: [id]) + orgId Bytes + org Org @relation(fields: [orgId], references: [id]) +} + +model Repo { + id Bytes @id + name String + visibility Int + ownerId Bytes + ownerType Int + createdAt DateTime @default(now()) +} + +model Log { + id Int @id @default(autoincrement()) + subjectUserId Bytes + subjectUsername String + action String + createdAt DateTime @default(now()) + parameters LogParameter[] +} + +model LogParameter { + id Int @id @default(autoincrement()) + logId Int + log Log @relation(fields: [logId], references: [id]) + key String + value String +} diff --git a/src/git.ts b/src/git.ts deleted file mode 100644 index bcf707a..0000000 --- a/src/git.ts +++ /dev/null @@ -1,35 +0,0 @@ -import {spawn} from './utils/process'; -import {mkdirp} from './utils/fs'; - -export enum RepoVisibility { - PRIVATE, - INTERNAL, - PUBLIC, -} - -export enum OwnerType { - USER, - ORG, -} - -type Owner = { - name: string, - type: OwnerType, -} - -type CreateOptions = { - name: string, - owner: Owner, - visibility: RepoVisibility, -} - -const OWNER_TYPE_DIR_NAMES: Record = { - [OwnerType.USER]: 'users', - [OwnerType.ORG]: 'orgs', -} - -export const create = async (options: CreateOptions) => { - const repoBasePath = `${OWNER_TYPE_DIR_NAMES[options.owner.type]}/${options.owner.name}/${options.name}`; - await mkdirp(repoBasePath); - return spawn(repoBasePath, 'git', ['init', '--bare']); -} diff --git a/src/index.ts b/src/index.ts index b975775..fa912d4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1 +1 @@ -export * as git from './git'; +export * as git from './modules/git'; diff --git a/src/modules/common/index.ts b/src/modules/common/index.ts new file mode 100644 index 0000000..e9644da --- /dev/null +++ b/src/modules/common/index.ts @@ -0,0 +1 @@ +export * from './models'; diff --git a/src/modules/common/models.ts b/src/modules/common/models.ts new file mode 100644 index 0000000..9356cac --- /dev/null +++ b/src/modules/common/models.ts @@ -0,0 +1,12 @@ +import {Uuid} from '@theoryofnekomata/uuid-buffer'; + +export type User = { + id: Uuid, + username: string, +} + +export type Org = { + id: Uuid, + name: string, + description: string, +} diff --git a/src/modules/git/Git.service.ts b/src/modules/git/Git.service.ts new file mode 100644 index 0000000..9b9d3bf --- /dev/null +++ b/src/modules/git/Git.service.ts @@ -0,0 +1,133 @@ +import * as path from 'path'; +import {Uuid} from '@theoryofnekomata/uuid-buffer'; +import {PrismaClient} from '@prisma/client'; +import {spawn} from '../../utils/process'; +import {mkdirp, unlink} from '../../utils/fs'; +import {User} from '../common'; +import {LogService, LogServiceImpl} from '../log'; +import * as models from './models'; +import {notFoundFactory} from '../../utils/error'; + +export interface GitService { + create(data: models.CreateRepoData, subject: User): Promise + delete(repoId: models.Repo['id'], subject: User): Promise +} + +export class GitServiceImpl implements GitService { + private readonly prismaClient: PrismaClient + private readonly logService: LogService + + constructor() { + this.prismaClient = new PrismaClient(); + this.logService = new LogServiceImpl() + } + + private static getRepoBasePath(repoIdString: string) { + return path.join('repos', repoIdString) + } + + private async getOwnerName(ownerId: Uuid, ownerType: models.OwnerType) { + let owner: models.Owner + if (ownerType === models.OwnerType.USER) { + owner = await this.prismaClient.user.findUnique({ + where: { + id: ownerId, + }, + rejectOnNotFound: notFoundFactory(models.UserNotFoundError), + }) + return owner.username; + } + + if (ownerType === models.OwnerType.ORG) { + owner = await this.prismaClient.org.findUnique({ + where: { + id: ownerId, + }, + rejectOnNotFound: notFoundFactory(models.OrgNotFoundError), + }) + return owner.name; + } + + throw new models.UnknownOwnerTypeError('Unknown owner type.') + } + + private static async createRepoFiles(repoIdString: string) { + const repoBasePath = GitServiceImpl.getRepoBasePath(repoIdString); + await mkdirp(repoBasePath); + await spawn(repoBasePath, 'git', ['init', '--bare']); + } + + async create(data: models.CreateRepoData, subject: User): Promise { + const repoId = Uuid.v4(); + const repoIdString = repoId.toString() + + await GitServiceImpl.createRepoFiles(repoIdString); + const repoMetadata = await this.prismaClient.repo.create({ + data: { + id: repoId, + name: data.name, + visibility: data.visibility, + ownerId: data.ownerId, + ownerType: data.ownerType, + }, + }); + + const ownerName = await this.getOwnerName(data.ownerId, data.ownerType); + await this.logService.create( + subject, + models.GitAction.REPO_CREATED, + { + key: 'repoId', + value: repoIdString, + }, + { + key: 'repoName', + value: data.name, + }, + { + key: 'ownerId', + value: data.ownerId.toString(), + }, + { + key: 'ownerName', + value: ownerName, + }, + ); + + return { + ...repoMetadata, + id: Uuid.from(repoMetadata.id), + }; + } + + async delete(repoId: models.Repo['id'], subject: User): Promise { + const repoIdString = repoId.toString(); + const repo = await this.prismaClient.repo.findUnique({ + where: { + id: repoId, + }, + rejectOnNotFound: notFoundFactory(models.RepoNotFoundError), + }); + await this.prismaClient.repo.delete({ + where: { + id: repoId, + }, + }); + + const repoBasePath = GitServiceImpl.getRepoBasePath(repoIdString); + await unlink(repoBasePath); + + await this.logService.create( + subject, + models.GitAction.REPO_REMOVED, + { + key: 'repoId', + value: repoIdString, + }, + { + key: 'repoName', + value: repo.name, + }, + ); + } +} diff --git a/src/modules/git/index.ts b/src/modules/git/index.ts new file mode 100644 index 0000000..875e234 --- /dev/null +++ b/src/modules/git/index.ts @@ -0,0 +1,2 @@ +export * from './Git.service' +export * from './models' diff --git a/src/modules/git/models.ts b/src/modules/git/models.ts new file mode 100644 index 0000000..e6edd32 --- /dev/null +++ b/src/modules/git/models.ts @@ -0,0 +1,38 @@ +import {Uuid} from '@theoryofnekomata/uuid-buffer'; +import {Org, User} from '../common'; + +export enum RepoVisibility { + PRIVATE, + INTERNAL, + PUBLIC, +} + +export enum OwnerType { + USER, + ORG, +} + +export type Owner = User | Org; + +export enum GitAction { + REPO_CREATED = 'REPO_CREATED', + REPO_REMOVED = 'REPO_REMOVED', +} + +export type Repo = { + id: Uuid, + name: string, + ownerId: Uuid, + ownerType: OwnerType, + visibility: RepoVisibility, +} + +export type CreateRepoData = Omit; + +export class UnknownOwnerTypeError extends Error {} + +export class UserNotFoundError extends Error {} + +export class OrgNotFoundError extends Error {} + +export class RepoNotFoundError extends Error {} diff --git a/src/modules/log/Log.service.ts b/src/modules/log/Log.service.ts new file mode 100644 index 0000000..d09ee33 --- /dev/null +++ b/src/modules/log/Log.service.ts @@ -0,0 +1,33 @@ +import { Prisma, PrismaClient } from '@prisma/client'; +import {User} from '../common'; +import {Log, LogParameterData} from './models'; + +export interface LogService { + create(subject: User, action: string, ...parameters: LogParameterData[]): Promise +} + +export class LogServiceImpl implements LogService { + private readonly prismaClient: PrismaClient + + constructor() { + this.prismaClient = new PrismaClient() + } + + async create(subject: User, action: string, ...parameters: LogParameterData[]): Promise { + const createData: Prisma.LogCreateInput = { + subjectUsername: subject.username, + subjectUserId: subject.id, + action, + } + + if (parameters.length > 0) { + createData['parameters'] = { + create: parameters, + } + } + + return this.prismaClient.log.create({ + data: createData, + }) + } +} diff --git a/src/modules/log/index.ts b/src/modules/log/index.ts new file mode 100644 index 0000000..297cf31 --- /dev/null +++ b/src/modules/log/index.ts @@ -0,0 +1,2 @@ +export * from './Log.service' +export * from './models' diff --git a/src/modules/log/models.ts b/src/modules/log/models.ts new file mode 100644 index 0000000..3da0805 --- /dev/null +++ b/src/modules/log/models.ts @@ -0,0 +1,18 @@ +import {Uuid} from '@theoryofnekomata/uuid-buffer'; + +export type LogParameter = { + id: number, + logId: number, + key: string, + value: string, +} + +export type Log = { + id: number, + subjectUserId: Uuid, + subjectUsername: string, + action: string, + createdAt: Date, +} + +export type LogParameterData = Pick; diff --git a/src/utils/error.ts b/src/utils/error.ts new file mode 100644 index 0000000..35364ad --- /dev/null +++ b/src/utils/error.ts @@ -0,0 +1 @@ +export const notFoundFactory = (ErrorClass: { new(): T }) => () => new ErrorClass() diff --git a/src/utils/fs.ts b/src/utils/fs.ts index 7f4ea6b..9f7126f 100644 --- a/src/utils/fs.ts +++ b/src/utils/fs.ts @@ -13,3 +13,7 @@ export const mkdirp = (path: string) => { return Promise.allSettled(directoriesToCheck.map((d) => fs.mkdir(d))); } + +export const unlink = (path: string) => { + return fs.rm(path, { recursive: true, force: true, }) +} diff --git a/yarn.lock b/yarn.lock index c90b5f3..99439fb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -347,6 +347,31 @@ resolved "https://registry.yarnpkg.com/@ovyerus/licenses/-/licenses-6.4.4.tgz#596e3ace46ab7c70bcf0e2b17f259796a4bedf9f" integrity sha512-IHjc31WXciQT3hfvdY+M59jBkQp70Fpr04tNDVO5rez2PNv4u8tE6w//CkU+GeBoO9k2ahneSqzjzvlgjyjkGw== +"@prisma/client@^3.14.0": + version "3.14.0" + resolved "https://registry.yarnpkg.com/@prisma/client/-/client-3.14.0.tgz#bb90405c012fcca11f4647d91153ed4c58f3bd48" + integrity sha512-atb41UpgTR1MCst0VIbiHTMw8lmXnwUvE1KyUCAkq08+wJyjRE78Due+nSf+7uwqQn+fBFYVmoojtinhlLOSaA== + dependencies: + "@prisma/engines-version" "3.14.0-36.2b0c12756921c891fec4f68d9444e18c7d5d4a6a" + +"@prisma/engines-version@3.14.0-36.2b0c12756921c891fec4f68d9444e18c7d5d4a6a": + version "3.14.0-36.2b0c12756921c891fec4f68d9444e18c7d5d4a6a" + resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-3.14.0-36.2b0c12756921c891fec4f68d9444e18c7d5d4a6a.tgz#4edae57cf6527f35e22cebe75e49214fc0e99ac9" + integrity sha512-D+yHzq4a2r2Rrd0ZOW/mTZbgDIkUkD8ofKgusEI1xPiZz60Daks+UM7Me2ty5FzH3p/TgyhBpRrfIHx+ha20RQ== + +"@prisma/engines@3.14.0-36.2b0c12756921c891fec4f68d9444e18c7d5d4a6a": + version "3.14.0-36.2b0c12756921c891fec4f68d9444e18c7d5d4a6a" + resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-3.14.0-36.2b0c12756921c891fec4f68d9444e18c7d5d4a6a.tgz#7fa11bc26a51d450185c816cc0ab8cac673fb4bf" + integrity sha512-LwZvI3FY6f43xFjQNRuE10JM5R8vJzFTSmbV9X0Wuhv9kscLkjRlZt0BEoiHmO+2HA3B3xxbMfB5du7ZoSFXGg== + +"@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" @@ -374,6 +399,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" @@ -2405,6 +2435,13 @@ pridepack@1.1.1: resolve.exports "^1.1.0" yargs "^17.2.1" +prisma@^3.14.0: + version "3.14.0" + resolved "https://registry.yarnpkg.com/prisma/-/prisma-3.14.0.tgz#dd67ece37d7b5373e9fd9588971de0024b49be81" + integrity sha512-l9MOgNCn/paDE+i1K2fp9NZ+Du4trzPTJsGkaQHVBufTGqzoYHuNk8JfzXuIn0Gte6/ZjyKj652Jq/Lc1tp2yw== + dependencies: + "@prisma/engines" "3.14.0-36.2b0c12756921c891fec4f68d9444e18c7d5d4a6a" + prompts@^2.3.2, prompts@^2.4.2: version "2.4.2" resolved "https://registry.yarnpkg.com/prompts/-/prompts-2.4.2.tgz#7b57e73b3a48029ad10ebd44f74b01722a4cb069" @@ -2824,6 +2861,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"