Make methods as thin wrappers to Git executable. Also pass the child process for easy stream operations to consumers.master
@@ -82,8 +82,6 @@ | |||
"access": "public" | |||
}, | |||
"dependencies": { | |||
"@prisma/client": "^3.14.0", | |||
"@theoryofnekomata/uuid-buffer": "^0.1.0", | |||
"prisma": "^3.14.0" | |||
"@theoryofnekomata/uuid-buffer": "^0.1.0" | |||
} | |||
} |
@@ -1,70 +0,0 @@ | |||
// 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,2 +1 @@ | |||
export * as common from './modules/common'; | |||
export * as git from './modules/git'; |
@@ -1 +0,0 @@ | |||
export * from './models'; |
@@ -1,12 +0,0 @@ | |||
import {Uuid} from '@theoryofnekomata/uuid-buffer'; | |||
export type User = { | |||
id: Uuid, | |||
username: string, | |||
} | |||
export type Org = { | |||
id: Uuid, | |||
name: string, | |||
description: string, | |||
} |
@@ -1,133 +1,58 @@ | |||
import * as path from 'path'; | |||
import {Uuid} from '@theoryofnekomata/uuid-buffer'; | |||
import {PrismaClient} from '@prisma/client'; | |||
import {ChildProcess} from 'child_process'; | |||
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> | |||
create(repoBasePath: string): Promise<ChildProcess> | |||
delete(repoBasePath: string): Promise<void> | |||
advertiseReceivePackRefs(repoBasePath: string): Promise<ChildProcess> | |||
advertiseUploadPackRefs(repoBasePath: string): Promise<ChildProcess> | |||
receivePack(repoBasePath: string): Promise<ChildProcess> | |||
uploadPack(repoBasePath: string): Promise<ChildProcess> | |||
} | |||
export class GitServiceImpl implements GitService { | |||
private readonly prismaClient: PrismaClient | |||
private readonly logService: LogService | |||
constructor() { | |||
this.prismaClient = new PrismaClient(); | |||
this.logService = new LogServiceImpl() | |||
private static isWindows() { | |||
return /^win/.test(process.platform); | |||
} | |||
private static getRepoBasePath(repoIdString: string) { | |||
return path.join('repos', repoIdString) | |||
async create(repoBasePath: string): Promise<ChildProcess> { | |||
await mkdirp(repoBasePath); | |||
return spawn( | |||
repoBasePath, | |||
'git', ['init', '--bare'], | |||
); | |||
} | |||
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.') | |||
async delete(repoBasePath: string): Promise<void> { | |||
await unlink(repoBasePath); | |||
} | |||
private static async createRepoFiles(repoIdString: string) { | |||
const repoBasePath = GitServiceImpl.getRepoBasePath(repoIdString); | |||
await mkdirp(repoBasePath); | |||
await spawn(repoBasePath, 'git', ['init', '--bare']); | |||
async advertiseReceivePackRefs(repoBasePath: string): Promise<ChildProcess> { | |||
const command = GitServiceImpl.isWindows() ? 'git' : 'git-receive-pack'; | |||
const commonArgs = ['--stateless-rpc', '--advertise-refs', '.']; | |||
const args = GitServiceImpl.isWindows() ? ['receive-pack', ...commonArgs] : commonArgs; | |||
return spawn(repoBasePath, command, args); | |||
} | |||
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 advertiseUploadPackRefs(repoBasePath: string): Promise<ChildProcess> { | |||
const command = GitServiceImpl.isWindows() ? 'git' : 'git-upload-pack'; | |||
const commonArgs = ['--stateless-rpc', '--advertise-refs', '.']; | |||
const args = GitServiceImpl.isWindows() ? ['upload-pack', ...commonArgs] : commonArgs; | |||
return spawn(repoBasePath, command, args); | |||
} | |||
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); | |||
async receivePack(repoBasePath: string): Promise<ChildProcess> { | |||
const command = GitServiceImpl.isWindows() ? 'git' : 'git-receive-pack'; | |||
const commonArgs = ['--stateless-rpc', '.']; | |||
const args = GitServiceImpl.isWindows() ? ['receive-pack', ...commonArgs] : commonArgs; | |||
return spawn(repoBasePath, command, args); | |||
} | |||
await this.logService.create( | |||
subject, | |||
models.GitAction.REPO_REMOVED, | |||
{ | |||
key: 'repoId', | |||
value: repoIdString, | |||
}, | |||
{ | |||
key: 'repoName', | |||
value: repo.name, | |||
}, | |||
); | |||
async uploadPack(repoBasePath: string): Promise<ChildProcess> { | |||
const command = GitServiceImpl.isWindows() ? 'git' : 'git-upload-pack'; | |||
const commonArgs = ['--stateless-rpc', '.']; | |||
const args = GitServiceImpl.isWindows() ? ['upload-pack', ...commonArgs] : commonArgs; | |||
return spawn(repoBasePath, command, args); | |||
} | |||
} |
@@ -1,2 +1 @@ | |||
export * from './Git.service' | |||
export * from './models' |
@@ -1,38 +0,0 @@ | |||
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 {} |
@@ -1,33 +0,0 @@ | |||
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, | |||
}) | |||
} | |||
} |
@@ -1,2 +0,0 @@ | |||
export * from './Log.service' | |||
export * from './models' |
@@ -1,18 +0,0 @@ | |||
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'>; |
@@ -1 +0,0 @@ | |||
export const notFoundFactory = <T extends Error>(ErrorClass: { new(): T }) => () => new ErrorClass() |
@@ -4,31 +4,4 @@ export const spawn = ( | |||
cwd: string, | |||
command: string, | |||
args: string[], | |||
parentProcess = process, | |||
) => new Promise<string>((resolve, reject) => { | |||
let stdout = ''; | |||
let stderr = ''; | |||
const theChildProcess = childProcess.spawn(command, args, { | |||
cwd, | |||
}); | |||
theChildProcess.stdout.on('data', (data) => { | |||
parentProcess.stdout.write(data); | |||
stdout += data; | |||
}); | |||
theChildProcess.stderr.on('data', (data) => { | |||
parentProcess.stderr.write(data); | |||
stderr += data; | |||
}); | |||
theChildProcess.on('close', (code) => { | |||
if (code !== 0) { | |||
reject(new Error(stderr)); | |||
return; | |||
} | |||
resolve(stdout); | |||
}) | |||
}) | |||
) => childProcess.spawn(command, args, { cwd }); |
@@ -347,23 +347,6 @@ | |||
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" | |||
@@ -2435,13 +2418,6 @@ 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" | |||