Add methods for creation and deletion, covering metadata and file system.master
@@ -105,3 +105,4 @@ dist | |||||
.tern-port | .tern-port | ||||
.npmrc | .npmrc | ||||
.database/ |
@@ -80,5 +80,10 @@ | |||||
"author": "TheoryOfNekomata <allan.crisostomo@outlook.com>", | "author": "TheoryOfNekomata <allan.crisostomo@outlook.com>", | ||||
"publishConfig": { | "publishConfig": { | ||||
"access": "public" | "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))); | 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" | resolved "https://registry.yarnpkg.com/@ovyerus/licenses/-/licenses-6.4.4.tgz#596e3ace46ab7c70bcf0e2b17f259796a4bedf9f" | ||||
integrity sha512-IHjc31WXciQT3hfvdY+M59jBkQp70Fpr04tNDVO5rez2PNv4u8tE6w//CkU+GeBoO9k2ahneSqzjzvlgjyjkGw== | 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": | "@types/chai-subset@^1.3.3": | ||||
version "1.3.3" | version "1.3.3" | ||||
resolved "https://registry.yarnpkg.com/@types/chai-subset/-/chai-subset-1.3.3.tgz#97893814e92abd2c534de422cb377e0e0bdaac94" | 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" | resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.35.tgz#635b7586086d51fb40de0a2ec9d1014a5283ba4a" | ||||
integrity sha512-vu1SrqBjbbZ3J6vwY17jBs8Sr/BKA+/a/WtjRG+whKg1iuLFOosq872EXS0eXWILdO36DHQQeku/ZcL6hz2fpg== | 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": | "@typescript-eslint/eslint-plugin@^5.9.0": | ||||
version "5.25.0" | version "5.25.0" | ||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.25.0.tgz#e8ce050990e4d36cc200f2de71ca0d3eb5e77a31" | 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" | resolve.exports "^1.1.0" | ||||
yargs "^17.2.1" | 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: | prompts@^2.3.2, prompts@^2.4.2: | ||||
version "2.4.2" | version "2.4.2" | ||||
resolved "https://registry.yarnpkg.com/prompts/-/prompts-2.4.2.tgz#7b57e73b3a48029ad10ebd44f74b01722a4cb069" | 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" | resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" | ||||
integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= | 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: | v8-compile-cache@^2.0.3: | ||||
version "2.3.0" | version "2.3.0" | ||||
resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee" | resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee" | ||||