Browse Source

Turn methods to thin wrappers

Make methods as thin wrappers to Git executable. Also pass the child
process for easy stream operations to consumers.
master
TheoryOfNekomata 1 year ago
parent
commit
60a65b078e
14 changed files with 40 additions and 345 deletions
  1. +1
    -3
      package.json
  2. +0
    -70
      prisma/schema.prisma
  3. +0
    -1
      src/index.ts
  4. +0
    -1
      src/modules/common/index.ts
  5. +0
    -12
      src/modules/common/models.ts
  6. +38
    -113
      src/modules/git/Git.service.ts
  7. +0
    -1
      src/modules/git/index.ts
  8. +0
    -38
      src/modules/git/models.ts
  9. +0
    -33
      src/modules/log/Log.service.ts
  10. +0
    -2
      src/modules/log/index.ts
  11. +0
    -18
      src/modules/log/models.ts
  12. +0
    -1
      src/utils/error.ts
  13. +1
    -28
      src/utils/process.ts
  14. +0
    -24
      yarn.lock

+ 1
- 3
package.json View File

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

+ 0
- 70
prisma/schema.prisma View File

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

+ 0
- 1
src/index.ts View File

@@ -1,2 +1 @@
export * as common from './modules/common';
export * as git from './modules/git';

+ 0
- 1
src/modules/common/index.ts View File

@@ -1 +0,0 @@
export * from './models';

+ 0
- 12
src/modules/common/models.ts View File

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

+ 38
- 113
src/modules/git/Git.service.ts View File

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

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

@@ -1,2 +1 @@
export * from './Git.service'
export * from './models'

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

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

+ 0
- 33
src/modules/log/Log.service.ts View File

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

+ 0
- 2
src/modules/log/index.ts View File

@@ -1,2 +0,0 @@
export * from './Log.service'
export * from './models'

+ 0
- 18
src/modules/log/models.ts View File

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

+ 0
- 1
src/utils/error.ts View File

@@ -1 +0,0 @@
export const notFoundFactory = <T extends Error>(ErrorClass: { new(): T }) => () => new ErrorClass()

+ 1
- 28
src/utils/process.ts View File

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

+ 0
- 24
yarn.lock View File

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


Loading…
Cancel
Save