Add methods for creation and deletion, covering metadata and file system.master
@@ -105,3 +105,4 @@ dist | |||
.tern-port | |||
.npmrc | |||
.database/ |
@@ -80,5 +80,10 @@ | |||
"author": "TheoryOfNekomata <allan.crisostomo@outlook.com>", | |||
"publishConfig": { | |||
"access": "public" | |||
}, | |||
"dependencies": { | |||
"@prisma/client": "^3.14.0", | |||
"@theoryofnekomata/uuid-buffer": "^0.1.0", | |||
"prisma": "^3.14.0" | |||
} | |||
} |
@@ -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 | |||
} |
@@ -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, string> = { | |||
[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']); | |||
} |
@@ -1 +1 @@ | |||
export * as git from './git'; | |||
export * as git from './modules/git'; |
@@ -0,0 +1 @@ | |||
export * from './models'; |
@@ -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, | |||
} |
@@ -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<models.Repo> | |||
delete(repoId: models.Repo['id'], subject: User): Promise<void> | |||
} | |||
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<models.Repo> { | |||
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<void> { | |||
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, | |||
}, | |||
); | |||
} | |||
} |
@@ -0,0 +1,2 @@ | |||
export * from './Git.service' | |||
export * from './models' |
@@ -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<Repo, 'id'>; | |||
export class UnknownOwnerTypeError extends Error {} | |||
export class UserNotFoundError extends Error {} | |||
export class OrgNotFoundError extends Error {} | |||
export class RepoNotFoundError extends Error {} |
@@ -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<Log> | |||
} | |||
export class LogServiceImpl implements LogService { | |||
private readonly prismaClient: PrismaClient | |||
constructor() { | |||
this.prismaClient = new PrismaClient() | |||
} | |||
async create(subject: User, action: string, ...parameters: LogParameterData[]): Promise<Log> { | |||
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, | |||
}) | |||
} | |||
} |
@@ -0,0 +1,2 @@ | |||
export * from './Log.service' | |||
export * from './models' |
@@ -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<LogParameter, 'key' | 'value'>; |
@@ -0,0 +1 @@ | |||
export const notFoundFactory = <T extends Error>(ErrorClass: { new(): T }) => () => new ErrorClass() |
@@ -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, }) | |||
} |
@@ -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" | |||