init studio admin backend
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
node_modules
|
||||
dist
|
||||
.env
|
||||
npm-debug.log
|
||||
@@ -0,0 +1,29 @@
|
||||
NODE_ENV=development
|
||||
PORT=4500
|
||||
CORS_ORIGIN=http://localhost:5173,http://localhost:5174,http://localhost:5175,http://localhost:5176,http://127.0.0.1:5173,http://127.0.0.1:5174,http://127.0.0.1:5175,http://127.0.0.1:5176
|
||||
DATABASE_URL=postgresql://studio_admin:future1234!%40@localhost:5436/studio_admin?schema=public
|
||||
AUTH_DATABASE_URL=postgres://atnmy_auth:<password>@localhost:5434/atnmy_auth
|
||||
LAUNCHER_DATABASE_URL=postgres://launcher:<password>@localhost:5435/launcher
|
||||
DATABASE_OPTIONAL=false
|
||||
AUTH_SERVER_URL=http://localhost:4100
|
||||
GITEA_BASE_URL=http://localhost:3000
|
||||
GITEA_APP_ORG=platform
|
||||
GITEA_ADMIN_TOKEN=
|
||||
BUILD_REGISTRY=localhost:5000
|
||||
BUILD_ARTIFACT_INSTALL_COMMAND=npm ci
|
||||
LAUNCHER_BUILD_COMMAND="npm ci && npm run build:linux"
|
||||
BUILD_ARTIFACT_OUTPUT_DIR=dist
|
||||
BUILD_ARTIFACT_DIR=/tmp/studio-admin-artifacts
|
||||
BUILD_ARTIFACT_BASE_URL=
|
||||
R2_ACCOUNT_ID=
|
||||
R2_ENDPOINT=
|
||||
R2_ACCESS_KEY_ID=
|
||||
R2_SECRET_ACCESS_KEY=
|
||||
R2_BUCKET=
|
||||
R2_PUBLIC_BASE_URL=
|
||||
R2_ARTIFACT_PREFIX=studio-artifacts
|
||||
R2_REGION=auto
|
||||
R2_FORCE_PATH_STYLE=true
|
||||
KUBECTL_BIN=kubectl
|
||||
KUBECONFIG=
|
||||
K8S_DEFAULT_NAMESPACE=default
|
||||
@@ -0,0 +1,5 @@
|
||||
node_modules
|
||||
dist
|
||||
.env
|
||||
coverage
|
||||
uploads
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"singleQuote": true,
|
||||
"trailingComma": "all"
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
FROM node:22-bookworm-slim AS builder
|
||||
WORKDIR /app
|
||||
COPY package*.json ./
|
||||
RUN npm ci
|
||||
COPY . .
|
||||
RUN npm run build
|
||||
|
||||
FROM node:22-bookworm-slim AS runner
|
||||
WORKDIR /app
|
||||
ENV NODE_ENV=production
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends ca-certificates curl git docker.io \
|
||||
&& curl -fsSL -o /usr/local/bin/kubectl "https://dl.k8s.io/release/v1.30.0/bin/linux/amd64/kubectl" \
|
||||
&& chmod 0755 /usr/local/bin/kubectl \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
COPY package*.json ./
|
||||
RUN npm ci --omit=dev
|
||||
COPY --from=builder /app/dist ./dist
|
||||
COPY --from=builder /app/prisma ./prisma
|
||||
COPY --from=builder /app/node_modules/.prisma ./node_modules/.prisma
|
||||
COPY --from=builder /app/node_modules/@prisma/launcher-client ./node_modules/@prisma/launcher-client
|
||||
COPY --from=builder /app/node_modules/@prisma/auth-client ./node_modules/@prisma/auth-client
|
||||
CMD ["node", "dist/main.js"]
|
||||
@@ -0,0 +1,38 @@
|
||||
# Autonomy Studio Admin Backend
|
||||
|
||||
NestJS + Prisma backend for Studio Admin.
|
||||
|
||||
## Stack
|
||||
|
||||
- NestJS
|
||||
- Prisma
|
||||
- PostgreSQL
|
||||
- local k3s integration through `kubectl`
|
||||
|
||||
## Local setup
|
||||
|
||||
```bash
|
||||
npm install
|
||||
cp .env.example .env
|
||||
docker compose up -d
|
||||
npm run prisma:generate
|
||||
npm run start:dev
|
||||
```
|
||||
|
||||
## k3s connection
|
||||
|
||||
For local k3s, keep `KUBECONFIG` empty if `kubectl` already points to the target cluster.
|
||||
Otherwise set it explicitly:
|
||||
|
||||
```bash
|
||||
KUBECONFIG=/etc/rancher/k3s/k3s.yaml
|
||||
```
|
||||
|
||||
Runtime APIs use `kubectl` for the first implementation:
|
||||
|
||||
- `GET /api/runtime/namespaces`
|
||||
- `GET /api/runtime/workloads`
|
||||
- `POST /api/runtime/workloads/:namespace/:kind/:name/restart`
|
||||
- `POST /api/runtime/workloads/:namespace/:kind/:name/scale`
|
||||
- `GET /api/runtime/workloads/:namespace/:kind/:name/logs`
|
||||
- `GET /api/runtime/events`
|
||||
@@ -0,0 +1,21 @@
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:17-alpine
|
||||
container_name: atnmy-stdo-adm-v2-postgres
|
||||
environment:
|
||||
POSTGRES_DB: studio_admin
|
||||
POSTGRES_USER: studio_admin
|
||||
POSTGRES_PASSWORD: future1234!@
|
||||
ports:
|
||||
- "5436:5432"
|
||||
volumes:
|
||||
- atnmy-stdo-adm-v2-postgres:/var/lib/postgresql/data
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U studio_admin -d studio_admin"]
|
||||
interval: 5s
|
||||
timeout: 3s
|
||||
retries: 20
|
||||
|
||||
volumes:
|
||||
atnmy-stdo-adm-v2-postgres:
|
||||
@@ -0,0 +1,35 @@
|
||||
import js from '@eslint/js';
|
||||
import globals from 'globals';
|
||||
import tseslint from 'typescript-eslint';
|
||||
import prettier from 'eslint-plugin-prettier';
|
||||
|
||||
export default tseslint.config(
|
||||
{
|
||||
ignores: ['eslint.config.mjs', 'dist/**', 'node_modules/**'],
|
||||
},
|
||||
js.configs.recommended,
|
||||
...tseslint.configs.recommendedTypeChecked,
|
||||
{
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.node,
|
||||
...globals.jest,
|
||||
},
|
||||
sourceType: 'commonjs',
|
||||
parserOptions: {
|
||||
projectService: true,
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
prettier,
|
||||
},
|
||||
rules: {
|
||||
'prettier/prettier': 'error',
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'@typescript-eslint/no-unsafe-assignment': 'off',
|
||||
'@typescript-eslint/no-unsafe-member-access': 'off',
|
||||
'@typescript-eslint/no-unsafe-call': 'off',
|
||||
},
|
||||
},
|
||||
);
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/nest-cli",
|
||||
"collection": "@nestjs/schematics",
|
||||
"sourceRoot": "src",
|
||||
"compilerOptions": {
|
||||
"deleteOutDir": true
|
||||
}
|
||||
}
|
||||
Generated
+11010
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,79 @@
|
||||
{
|
||||
"name": "atnmy-stdo-adm-v2-be",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"description": "Backend API for Autonomy Studio Admin",
|
||||
"license": "UNLICENSED",
|
||||
"scripts": {
|
||||
"clean": "node -e \"const fs=require('fs'); fs.rmSync('dist', { recursive: true, force: true }); fs.rmSync('tsconfig.build.tsbuildinfo', { force: true })\"",
|
||||
"prebuild": "npm run clean",
|
||||
"build": "prisma generate && prisma generate --schema prisma/launcher.schema.prisma && prisma generate --schema prisma/auth.schema.prisma && nest build",
|
||||
"start": "nest start",
|
||||
"prestart:dev": "npm run clean",
|
||||
"start:dev": "nest start --watch",
|
||||
"start:prod": "node dist/main.js",
|
||||
"lint": "eslint \"{src,test}/**/*.ts\" --fix",
|
||||
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
||||
"prisma:generate": "prisma generate && prisma generate --schema prisma/launcher.schema.prisma && prisma generate --schema prisma/auth.schema.prisma",
|
||||
"prisma:migrate": "prisma migrate dev",
|
||||
"prisma:deploy": "prisma migrate deploy",
|
||||
"prisma:studio": "prisma studio",
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch",
|
||||
"test:cov": "jest --coverage"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "^3.1075.0",
|
||||
"@nestjs/common": "^11.1.24",
|
||||
"@nestjs/config": "^4.0.4",
|
||||
"@nestjs/core": "^11.1.24",
|
||||
"@nestjs/platform-express": "^11.1.24",
|
||||
"@prisma/client": "^6.19.3",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.15.1",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"rxjs": "^7.8.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^10.0.1",
|
||||
"@nestjs/cli": "^11.0.21",
|
||||
"@nestjs/schematics": "^11.1.0",
|
||||
"@nestjs/testing": "^11.1.24",
|
||||
"@types/express": "^5.0.6",
|
||||
"@types/jest": "^30.0.0",
|
||||
"@types/node": "^25.9.1",
|
||||
"@types/supertest": "^7.2.0",
|
||||
"eslint": "^10.4.1",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-prettier": "^5.5.6",
|
||||
"globals": "^17.6.0",
|
||||
"jest": "^30.4.2",
|
||||
"prettier": "^3.8.3",
|
||||
"prisma": "^6.19.3",
|
||||
"source-map-support": "^0.5.21",
|
||||
"supertest": "^7.2.2",
|
||||
"ts-jest": "^29.4.11",
|
||||
"ts-loader": "^9.6.0",
|
||||
"ts-node": "^10.9.2",
|
||||
"tsconfig-paths": "^4.2.0",
|
||||
"typescript": "^6.0.3",
|
||||
"typescript-eslint": "^8.60.0"
|
||||
},
|
||||
"jest": {
|
||||
"moduleFileExtensions": [
|
||||
"js",
|
||||
"json",
|
||||
"ts"
|
||||
],
|
||||
"rootDir": ".",
|
||||
"testRegex": ".*\\.spec\\.ts$",
|
||||
"transform": {
|
||||
"^.+\\.(t|j)s$": "ts-jest"
|
||||
},
|
||||
"collectCoverageFrom": [
|
||||
"src/**/*.(t|j)s"
|
||||
],
|
||||
"coverageDirectory": "coverage",
|
||||
"testEnvironment": "node"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
generator authClient {
|
||||
provider = "prisma-client-js"
|
||||
output = "../node_modules/@prisma/auth-client"
|
||||
binaryTargets = ["native", "debian-openssl-3.0.x", "debian-openssl-1.1.x"]
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
url = env("AUTH_DATABASE_URL")
|
||||
}
|
||||
|
||||
enum UserRole {
|
||||
ADMIN
|
||||
USER
|
||||
}
|
||||
|
||||
enum LicenseCode {
|
||||
FREE
|
||||
EDU
|
||||
PRO
|
||||
}
|
||||
|
||||
model User {
|
||||
id String @id @default(uuid()) @db.Uuid
|
||||
email String @unique
|
||||
passwordHash String @map("password_hash")
|
||||
name String?
|
||||
role UserRole @default(USER)
|
||||
isActive Boolean @default(true) @map("is_active")
|
||||
lastLoginAt DateTime? @map("last_login_at") @db.Timestamptz(3)
|
||||
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)
|
||||
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(3)
|
||||
userLicenses UserLicense[]
|
||||
refreshTokens RefreshToken[]
|
||||
|
||||
@@map("users")
|
||||
}
|
||||
|
||||
model RefreshToken {
|
||||
id String @id @default(uuid()) @db.Uuid
|
||||
userId String @map("user_id") @db.Uuid
|
||||
tokenHash String @unique @map("token_hash")
|
||||
expiresAt DateTime @map("expires_at") @db.Timestamptz(3)
|
||||
revokedAt DateTime? @map("revoked_at") @db.Timestamptz(3)
|
||||
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([userId])
|
||||
@@map("refresh_tokens")
|
||||
}
|
||||
|
||||
model License {
|
||||
id String @id @default(uuid()) @db.Uuid
|
||||
code LicenseCode @unique
|
||||
name String
|
||||
description String?
|
||||
defaultDurationDays Int? @default(365) @map("default_duration_days")
|
||||
isActive Boolean @default(true) @map("is_active")
|
||||
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)
|
||||
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(3)
|
||||
userLicenses UserLicense[]
|
||||
|
||||
@@index([isActive])
|
||||
@@map("licenses")
|
||||
}
|
||||
|
||||
model UserLicense {
|
||||
id String @id @default(uuid()) @db.Uuid
|
||||
userId String @map("user_id") @db.Uuid
|
||||
licenseId String @map("license_id") @db.Uuid
|
||||
startsAt DateTime @default(now()) @map("starts_at") @db.Timestamptz(3)
|
||||
expiresAt DateTime? @map("expires_at") @db.Timestamptz(3)
|
||||
assignedById String? @map("assigned_by") @db.Uuid
|
||||
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
license License @relation(fields: [licenseId], references: [id], onDelete: Restrict)
|
||||
|
||||
@@index([userId, startsAt])
|
||||
@@index([licenseId])
|
||||
@@map("user_licenses")
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
generator launcherClient {
|
||||
provider = "prisma-client-js"
|
||||
output = "../node_modules/@prisma/launcher-client"
|
||||
binaryTargets = ["native", "debian-openssl-3.0.x", "debian-openssl-1.1.x"]
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
url = env("LAUNCHER_DATABASE_URL")
|
||||
}
|
||||
|
||||
model LauncherNotice {
|
||||
id String @id @default(uuid()) @db.Uuid
|
||||
appKey String @default("Launcher") @map("app_key")
|
||||
title String
|
||||
body String
|
||||
linkUrl String? @map("link_url")
|
||||
isActive Boolean @default(true) @map("is_active")
|
||||
startsAt DateTime? @map("starts_at") @db.Timestamptz(3)
|
||||
endsAt DateTime? @map("ends_at") @db.Timestamptz(3)
|
||||
priority Int @default(0)
|
||||
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)
|
||||
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(3)
|
||||
|
||||
@@index([appKey, isActive, priority])
|
||||
@@map("launcher_notices")
|
||||
}
|
||||
|
||||
model ProvisionedDevice {
|
||||
id String @id @default(uuid()) @db.Uuid
|
||||
accountEmail String @map("account_email")
|
||||
authUserId String? @map("auth_user_id") @db.Uuid
|
||||
hostname String
|
||||
tailscaleHostname String @map("tailscale_hostname")
|
||||
sshUser String? @map("ssh_user")
|
||||
workspaceRoot String? @map("workspace_root")
|
||||
usageType String @default("personal") @map("usage_type")
|
||||
os String?
|
||||
arch String?
|
||||
kernel String?
|
||||
distro String?
|
||||
device Json?
|
||||
installedAt DateTime @default(now()) @map("installed_at") @db.Timestamptz(3)
|
||||
lastProvisionedAt DateTime @default(now()) @map("last_provisioned_at") @db.Timestamptz(3)
|
||||
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)
|
||||
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(3)
|
||||
|
||||
@@unique([accountEmail, tailscaleHostname])
|
||||
@@index([accountEmail])
|
||||
@@index([tailscaleHostname])
|
||||
@@map("provisioned_devices")
|
||||
}
|
||||
|
||||
model LauncherSshKey {
|
||||
id String @id @default(uuid()) @db.Uuid
|
||||
accountEmail String @map("account_email")
|
||||
authUserId String? @map("auth_user_id") @db.Uuid
|
||||
publicKey String @map("public_key")
|
||||
publicKeyHash String @map("public_key_hash")
|
||||
keyType String? @map("key_type")
|
||||
label String?
|
||||
lastProvisionedAt DateTime @default(now()) @map("last_provisioned_at") @db.Timestamptz(3)
|
||||
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)
|
||||
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(3)
|
||||
|
||||
@@unique([accountEmail, publicKeyHash])
|
||||
@@index([accountEmail])
|
||||
@@map("launcher_ssh_keys")
|
||||
}
|
||||
|
||||
|
||||
model LauncherCommonSetting {
|
||||
id String @id @default(uuid()) @db.Uuid
|
||||
key String @unique
|
||||
name String
|
||||
type String
|
||||
value String
|
||||
description String?
|
||||
isActive Boolean @default(true) @map("is_active")
|
||||
isSecret Boolean @default(false) @map("is_secret")
|
||||
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)
|
||||
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(3)
|
||||
|
||||
@@index([type, isActive])
|
||||
@@map("launcher_common_settings")
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
-- CreateEnum
|
||||
CREATE TYPE "AppStatus" AS ENUM ('ACTIVE', 'READY', 'DISABLED');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "ComponentType" AS ENUM ('ELECTRON', 'CONTAINER', 'STATIC_WEB', 'DATABASE', 'MIGRATION');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "JobStatus" AS ENUM ('PENDING', 'RUNNING', 'SUCCEEDED', 'FAILED', 'CANCELED');
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "apps" (
|
||||
"id" UUID NOT NULL,
|
||||
"key" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"description" TEXT,
|
||||
"namespace" TEXT,
|
||||
"status" "AppStatus" NOT NULL DEFAULT 'READY',
|
||||
"created_at" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMPTZ(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "apps_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "app_components" (
|
||||
"id" UUID NOT NULL,
|
||||
"app_id" UUID NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"type" "ComponentType" NOT NULL,
|
||||
"repository" TEXT,
|
||||
"branch" TEXT,
|
||||
"build_target" TEXT,
|
||||
"created_at" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMPTZ(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "app_components_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "build_jobs" (
|
||||
"id" UUID NOT NULL,
|
||||
"component_id" UUID NOT NULL,
|
||||
"branch" TEXT NOT NULL,
|
||||
"commit_sha" TEXT,
|
||||
"artifact" TEXT,
|
||||
"image" TEXT,
|
||||
"status" "JobStatus" NOT NULL DEFAULT 'PENDING',
|
||||
"logs" TEXT,
|
||||
"started_at" TIMESTAMPTZ(3),
|
||||
"finished_at" TIMESTAMPTZ(3),
|
||||
"created_at" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "build_jobs_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "app_releases" (
|
||||
"id" UUID NOT NULL,
|
||||
"app_id" UUID NOT NULL,
|
||||
"version" TEXT NOT NULL,
|
||||
"branch" TEXT NOT NULL,
|
||||
"commit_sha" TEXT,
|
||||
"artifact" TEXT,
|
||||
"image_tags" JSONB,
|
||||
"status" "JobStatus" NOT NULL DEFAULT 'PENDING',
|
||||
"created_at" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMPTZ(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "app_releases_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "deployment_jobs" (
|
||||
"id" UUID NOT NULL,
|
||||
"app_id" UUID NOT NULL,
|
||||
"release_id" UUID,
|
||||
"environment" TEXT NOT NULL,
|
||||
"namespace" TEXT NOT NULL,
|
||||
"status" "JobStatus" NOT NULL DEFAULT 'PENDING',
|
||||
"requested_by" TEXT,
|
||||
"logs" TEXT,
|
||||
"started_at" TIMESTAMPTZ(3),
|
||||
"finished_at" TIMESTAMPTZ(3),
|
||||
"created_at" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "deployment_jobs_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "notices" (
|
||||
"id" UUID NOT NULL,
|
||||
"scope" TEXT NOT NULL,
|
||||
"title" TEXT NOT NULL,
|
||||
"body" TEXT NOT NULL,
|
||||
"audience" TEXT NOT NULL,
|
||||
"is_active" BOOLEAN NOT NULL DEFAULT true,
|
||||
"starts_at" TIMESTAMPTZ(3),
|
||||
"ends_at" TIMESTAMPTZ(3),
|
||||
"created_at" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMPTZ(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "notices_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "apps_key_key" ON "apps"("key");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "app_components_app_id_name_key" ON "app_components"("app_id", "name");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "build_jobs_component_id_created_at_idx" ON "build_jobs"("component_id", "created_at");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "app_releases_app_id_version_key" ON "app_releases"("app_id", "version");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "deployment_jobs_app_id_created_at_idx" ON "deployment_jobs"("app_id", "created_at");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "notices_scope_is_active_idx" ON "notices"("scope", "is_active");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "app_components" ADD CONSTRAINT "app_components_app_id_fkey" FOREIGN KEY ("app_id") REFERENCES "apps"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "build_jobs" ADD CONSTRAINT "build_jobs_component_id_fkey" FOREIGN KEY ("component_id") REFERENCES "app_components"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "app_releases" ADD CONSTRAINT "app_releases_app_id_fkey" FOREIGN KEY ("app_id") REFERENCES "apps"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "deployment_jobs" ADD CONSTRAINT "deployment_jobs_app_id_fkey" FOREIGN KEY ("app_id") REFERENCES "apps"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "deployment_jobs" ADD CONSTRAINT "deployment_jobs_release_id_fkey" FOREIGN KEY ("release_id") REFERENCES "app_releases"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
@@ -0,0 +1,320 @@
|
||||
-- CreateEnum
|
||||
CREATE TYPE "AdminRole" AS ENUM ('OWNER', 'ADMIN', 'OPERATOR', 'VIEWER');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "NoticeStatus" AS ENUM ('DRAFT', 'SCHEDULED', 'PUBLISHED', 'ARCHIVED');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "LicenseCode" AS ENUM ('FREE', 'EDUCATION', 'PROFESSIONAL');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "RuntimeAction" AS ENUM ('RESTART', 'SCALE', 'LOGS', 'EVENTS', 'ROLLBACK', 'STOP');
|
||||
|
||||
-- DropIndex
|
||||
DROP INDEX "notices_scope_is_active_idx";
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "notices" ADD COLUMN "priority" INTEGER NOT NULL DEFAULT 0,
|
||||
ADD COLUMN "status" "NoticeStatus" NOT NULL DEFAULT 'DRAFT';
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "admins" (
|
||||
"id" UUID NOT NULL,
|
||||
"email" TEXT NOT NULL,
|
||||
"password_hash" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"role" "AdminRole" NOT NULL DEFAULT 'ADMIN',
|
||||
"is_active" BOOLEAN NOT NULL DEFAULT true,
|
||||
"last_login_at" TIMESTAMPTZ(3),
|
||||
"created_at" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMPTZ(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "admins_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "admin_sessions" (
|
||||
"id" UUID NOT NULL,
|
||||
"admin_id" UUID NOT NULL,
|
||||
"token_hash" TEXT NOT NULL,
|
||||
"ip_address" TEXT,
|
||||
"user_agent" TEXT,
|
||||
"expires_at" TIMESTAMPTZ(3) NOT NULL,
|
||||
"revoked_at" TIMESTAMPTZ(3),
|
||||
"created_at" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "admin_sessions_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "admin_audit_logs" (
|
||||
"id" UUID NOT NULL,
|
||||
"admin_id" UUID,
|
||||
"action" TEXT NOT NULL,
|
||||
"resource" TEXT NOT NULL,
|
||||
"resource_id" TEXT,
|
||||
"metadata" JSONB,
|
||||
"ip_address" TEXT,
|
||||
"created_at" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "admin_audit_logs_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "launcher_versions" (
|
||||
"id" UUID NOT NULL,
|
||||
"version" TEXT NOT NULL,
|
||||
"minimum_version" TEXT,
|
||||
"artifact" TEXT NOT NULL,
|
||||
"download_url" TEXT NOT NULL,
|
||||
"rollout_percent" INTEGER NOT NULL DEFAULT 0,
|
||||
"release_notes" TEXT,
|
||||
"is_active" BOOLEAN NOT NULL DEFAULT false,
|
||||
"created_at" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMPTZ(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "launcher_versions_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "launcher_common_settings" (
|
||||
"id" UUID NOT NULL,
|
||||
"key" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"type" TEXT NOT NULL,
|
||||
"value" TEXT NOT NULL,
|
||||
"description" TEXT,
|
||||
"is_active" BOOLEAN NOT NULL DEFAULT true,
|
||||
"is_secret" BOOLEAN NOT NULL DEFAULT false,
|
||||
"created_at" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMPTZ(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "launcher_common_settings_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "launcher_app_links" (
|
||||
"id" UUID NOT NULL,
|
||||
"app_id" UUID NOT NULL,
|
||||
"release_id" UUID,
|
||||
"display_order" INTEGER NOT NULL DEFAULT 0,
|
||||
"is_enabled" BOOLEAN NOT NULL DEFAULT true,
|
||||
"launch_url" TEXT,
|
||||
"artifact" TEXT,
|
||||
"created_at" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMPTZ(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "launcher_app_links_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "license_plan_snapshots" (
|
||||
"id" UUID NOT NULL,
|
||||
"code" "LicenseCode" NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"description" TEXT,
|
||||
"default_duration_days" INTEGER,
|
||||
"is_active" BOOLEAN NOT NULL DEFAULT true,
|
||||
"assigned_count" INTEGER NOT NULL DEFAULT 0,
|
||||
"active_user_count" INTEGER NOT NULL DEFAULT 0,
|
||||
"expiring_soon_count" INTEGER NOT NULL DEFAULT 0,
|
||||
"synced_at" TIMESTAMPTZ(3),
|
||||
"created_at" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMPTZ(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "license_plan_snapshots_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "user_license_snapshots" (
|
||||
"id" UUID NOT NULL,
|
||||
"license_id" UUID NOT NULL,
|
||||
"auth_user_id" TEXT,
|
||||
"email" TEXT NOT NULL,
|
||||
"name" TEXT,
|
||||
"starts_at" TIMESTAMPTZ(3),
|
||||
"expires_at" TIMESTAMPTZ(3),
|
||||
"assigned_by" TEXT,
|
||||
"is_active" BOOLEAN NOT NULL DEFAULT true,
|
||||
"synced_at" TIMESTAMPTZ(3),
|
||||
"created_at" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "user_license_snapshots_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "runtime_control_jobs" (
|
||||
"id" UUID NOT NULL,
|
||||
"namespace" TEXT NOT NULL,
|
||||
"kind" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"action" "RuntimeAction" NOT NULL,
|
||||
"status" "JobStatus" NOT NULL DEFAULT 'PENDING',
|
||||
"requested_by" TEXT,
|
||||
"request" JSONB,
|
||||
"result" JSONB,
|
||||
"logs" TEXT,
|
||||
"started_at" TIMESTAMPTZ(3),
|
||||
"finished_at" TIMESTAMPTZ(3),
|
||||
"created_at" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "runtime_control_jobs_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "platform_connections" (
|
||||
"id" UUID NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"endpoint" TEXT NOT NULL,
|
||||
"role" TEXT NOT NULL,
|
||||
"status" TEXT NOT NULL,
|
||||
"last_sync" TIMESTAMPTZ(3),
|
||||
"metadata" JSONB,
|
||||
"created_at" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMPTZ(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "platform_connections_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "agent_devices" (
|
||||
"id" UUID NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"account" TEXT,
|
||||
"status" TEXT NOT NULL,
|
||||
"workspace" TEXT,
|
||||
"ssh_key" TEXT,
|
||||
"last_seen" TIMESTAMPTZ(3),
|
||||
"metadata" JSONB,
|
||||
"created_at" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMPTZ(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "agent_devices_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "tailscale_nodes" (
|
||||
"id" UUID NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"ip" TEXT NOT NULL,
|
||||
"role" TEXT,
|
||||
"user" TEXT,
|
||||
"routes" TEXT,
|
||||
"status" TEXT NOT NULL,
|
||||
"last_seen" TIMESTAMPTZ(3),
|
||||
"metadata" JSONB,
|
||||
"created_at" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMPTZ(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "tailscale_nodes_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "tailscale_settings" (
|
||||
"id" UUID NOT NULL,
|
||||
"key" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"value" TEXT NOT NULL,
|
||||
"description" TEXT,
|
||||
"status" TEXT NOT NULL,
|
||||
"created_at" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMPTZ(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "tailscale_settings_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "admins_email_key" ON "admins"("email");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "admins_is_active_role_idx" ON "admins"("is_active", "role");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "admin_sessions_token_hash_key" ON "admin_sessions"("token_hash");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "admin_sessions_admin_id_expires_at_idx" ON "admin_sessions"("admin_id", "expires_at");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "admin_audit_logs_resource_created_at_idx" ON "admin_audit_logs"("resource", "created_at");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "admin_audit_logs_admin_id_created_at_idx" ON "admin_audit_logs"("admin_id", "created_at");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "launcher_versions_version_key" ON "launcher_versions"("version");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "launcher_versions_is_active_created_at_idx" ON "launcher_versions"("is_active", "created_at");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "launcher_common_settings_key_key" ON "launcher_common_settings"("key");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "launcher_common_settings_type_is_active_idx" ON "launcher_common_settings"("type", "is_active");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "launcher_app_links_app_id_key" ON "launcher_app_links"("app_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "launcher_app_links_display_order_is_enabled_idx" ON "launcher_app_links"("display_order", "is_enabled");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "license_plan_snapshots_code_key" ON "license_plan_snapshots"("code");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "user_license_snapshots_email_idx" ON "user_license_snapshots"("email");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "user_license_snapshots_license_id_is_active_idx" ON "user_license_snapshots"("license_id", "is_active");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "runtime_control_jobs_namespace_kind_name_idx" ON "runtime_control_jobs"("namespace", "kind", "name");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "runtime_control_jobs_created_at_idx" ON "runtime_control_jobs"("created_at");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "platform_connections_name_key" ON "platform_connections"("name");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "platform_connections_status_idx" ON "platform_connections"("status");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "agent_devices_name_key" ON "agent_devices"("name");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "agent_devices_status_last_seen_idx" ON "agent_devices"("status", "last_seen");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "tailscale_nodes_name_key" ON "tailscale_nodes"("name");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "tailscale_nodes_status_last_seen_idx" ON "tailscale_nodes"("status", "last_seen");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "tailscale_settings_key_key" ON "tailscale_settings"("key");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "tailscale_settings_status_idx" ON "tailscale_settings"("status");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "notices_scope_status_idx" ON "notices"("scope", "status");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "notices_is_active_priority_idx" ON "notices"("is_active", "priority");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "admin_sessions" ADD CONSTRAINT "admin_sessions_admin_id_fkey" FOREIGN KEY ("admin_id") REFERENCES "admins"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "admin_audit_logs" ADD CONSTRAINT "admin_audit_logs_admin_id_fkey" FOREIGN KEY ("admin_id") REFERENCES "admins"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "launcher_app_links" ADD CONSTRAINT "launcher_app_links_app_id_fkey" FOREIGN KEY ("app_id") REFERENCES "apps"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "launcher_app_links" ADD CONSTRAINT "launcher_app_links_release_id_fkey" FOREIGN KEY ("release_id") REFERENCES "app_releases"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "user_license_snapshots" ADD CONSTRAINT "user_license_snapshots_license_id_fkey" FOREIGN KEY ("license_id") REFERENCES "license_plan_snapshots"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@@ -0,0 +1 @@
|
||||
DROP TABLE IF EXISTS "agent_devices";
|
||||
@@ -0,0 +1 @@
|
||||
DROP TABLE IF EXISTS "notices";
|
||||
@@ -0,0 +1,200 @@
|
||||
COMMENT ON TABLE "admins" IS '스튜디오 어드민 콘솔 관리자 계정';
|
||||
COMMENT ON COLUMN "admins"."id" IS '관리자 고유 ID';
|
||||
COMMENT ON COLUMN "admins"."email" IS '관리자 로그인 이메일';
|
||||
COMMENT ON COLUMN "admins"."password_hash" IS '관리자 비밀번호 해시';
|
||||
COMMENT ON COLUMN "admins"."name" IS '관리자 표시 이름';
|
||||
COMMENT ON COLUMN "admins"."role" IS '관리자 권한 역할';
|
||||
COMMENT ON COLUMN "admins"."is_active" IS '관리자 계정 활성 여부';
|
||||
COMMENT ON COLUMN "admins"."last_login_at" IS '마지막 로그인 시각';
|
||||
COMMENT ON COLUMN "admins"."created_at" IS '생성 시각';
|
||||
COMMENT ON COLUMN "admins"."updated_at" IS '수정 시각';
|
||||
|
||||
COMMENT ON TABLE "admin_sessions" IS '스튜디오 어드민 콘솔 관리자 로그인 세션';
|
||||
COMMENT ON COLUMN "admin_sessions"."id" IS '관리자 세션 고유 ID';
|
||||
COMMENT ON COLUMN "admin_sessions"."admin_id" IS '관리자 ID';
|
||||
COMMENT ON COLUMN "admin_sessions"."token_hash" IS '세션 토큰 해시';
|
||||
COMMENT ON COLUMN "admin_sessions"."ip_address" IS '접속 IP 주소';
|
||||
COMMENT ON COLUMN "admin_sessions"."user_agent" IS '접속 브라우저 또는 클라이언트 정보';
|
||||
COMMENT ON COLUMN "admin_sessions"."expires_at" IS '세션 만료 시각';
|
||||
COMMENT ON COLUMN "admin_sessions"."revoked_at" IS '세션 폐기 시각';
|
||||
COMMENT ON COLUMN "admin_sessions"."created_at" IS '생성 시각';
|
||||
|
||||
COMMENT ON TABLE "admin_audit_logs" IS '스튜디오 어드민 콘솔 관리자 작업 감사 로그';
|
||||
COMMENT ON COLUMN "admin_audit_logs"."id" IS '감사 로그 고유 ID';
|
||||
COMMENT ON COLUMN "admin_audit_logs"."admin_id" IS '작업을 수행한 관리자 ID';
|
||||
COMMENT ON COLUMN "admin_audit_logs"."action" IS '수행 작업 코드';
|
||||
COMMENT ON COLUMN "admin_audit_logs"."resource" IS '작업 대상 리소스 유형';
|
||||
COMMENT ON COLUMN "admin_audit_logs"."resource_id" IS '작업 대상 리소스 ID';
|
||||
COMMENT ON COLUMN "admin_audit_logs"."metadata" IS '작업 상세 메타데이터';
|
||||
COMMENT ON COLUMN "admin_audit_logs"."ip_address" IS '작업 요청 IP 주소';
|
||||
COMMENT ON COLUMN "admin_audit_logs"."created_at" IS '생성 시각';
|
||||
|
||||
COMMENT ON TABLE "apps" IS '스튜디오에서 관리하는 애플리케이션 그룹';
|
||||
COMMENT ON COLUMN "apps"."id" IS '애플리케이션 고유 ID';
|
||||
COMMENT ON COLUMN "apps"."key" IS '애플리케이션 식별 키';
|
||||
COMMENT ON COLUMN "apps"."name" IS '애플리케이션 표시 이름';
|
||||
COMMENT ON COLUMN "apps"."description" IS '애플리케이션 설명';
|
||||
COMMENT ON COLUMN "apps"."namespace" IS '배포 대상 네임스페이스';
|
||||
COMMENT ON COLUMN "apps"."status" IS '애플리케이션 관리 상태';
|
||||
COMMENT ON COLUMN "apps"."created_at" IS '생성 시각';
|
||||
COMMENT ON COLUMN "apps"."updated_at" IS '수정 시각';
|
||||
|
||||
COMMENT ON TABLE "app_components" IS '애플리케이션을 구성하는 프론트엔드, 백엔드, DB, 마이그레이션 등 구성요소';
|
||||
COMMENT ON COLUMN "app_components"."id" IS '애플리케이션 구성요소 고유 ID';
|
||||
COMMENT ON COLUMN "app_components"."app_id" IS '소속 애플리케이션 ID';
|
||||
COMMENT ON COLUMN "app_components"."name" IS '구성요소 이름';
|
||||
COMMENT ON COLUMN "app_components"."type" IS '구성요소 유형';
|
||||
COMMENT ON COLUMN "app_components"."repository" IS '소스 저장소 주소 또는 식별자';
|
||||
COMMENT ON COLUMN "app_components"."branch" IS '기본 브랜치';
|
||||
COMMENT ON COLUMN "app_components"."build_target" IS '빌드 대상 또는 산출물 구분';
|
||||
COMMENT ON COLUMN "app_components"."created_at" IS '생성 시각';
|
||||
COMMENT ON COLUMN "app_components"."updated_at" IS '수정 시각';
|
||||
|
||||
COMMENT ON TABLE "build_jobs" IS '애플리케이션 구성요소 빌드 작업 이력';
|
||||
COMMENT ON COLUMN "build_jobs"."id" IS '빌드 작업 고유 ID';
|
||||
COMMENT ON COLUMN "build_jobs"."component_id" IS '빌드 대상 구성요소 ID';
|
||||
COMMENT ON COLUMN "build_jobs"."branch" IS '빌드 대상 브랜치';
|
||||
COMMENT ON COLUMN "build_jobs"."commit_sha" IS '빌드 대상 커밋 해시';
|
||||
COMMENT ON COLUMN "build_jobs"."artifact" IS '빌드 산출물 위치 또는 식별자';
|
||||
COMMENT ON COLUMN "build_jobs"."image" IS '빌드된 컨테이너 이미지 태그';
|
||||
COMMENT ON COLUMN "build_jobs"."status" IS '빌드 작업 상태';
|
||||
COMMENT ON COLUMN "build_jobs"."logs" IS '빌드 로그';
|
||||
COMMENT ON COLUMN "build_jobs"."started_at" IS '빌드 시작 시각';
|
||||
COMMENT ON COLUMN "build_jobs"."finished_at" IS '빌드 종료 시각';
|
||||
COMMENT ON COLUMN "build_jobs"."created_at" IS '생성 시각';
|
||||
|
||||
COMMENT ON TABLE "app_releases" IS '애플리케이션 릴리즈 버전과 산출물 정보';
|
||||
COMMENT ON COLUMN "app_releases"."id" IS '애플리케이션 릴리즈 고유 ID';
|
||||
COMMENT ON COLUMN "app_releases"."app_id" IS '릴리즈 대상 애플리케이션 ID';
|
||||
COMMENT ON COLUMN "app_releases"."version" IS '릴리즈 버전';
|
||||
COMMENT ON COLUMN "app_releases"."branch" IS '릴리즈 기준 브랜치';
|
||||
COMMENT ON COLUMN "app_releases"."commit_sha" IS '릴리즈 기준 커밋 해시';
|
||||
COMMENT ON COLUMN "app_releases"."artifact" IS '릴리즈 산출물 위치 또는 식별자';
|
||||
COMMENT ON COLUMN "app_releases"."image_tags" IS '릴리즈에 포함된 이미지 태그 목록';
|
||||
COMMENT ON COLUMN "app_releases"."status" IS '릴리즈 생성 상태';
|
||||
COMMENT ON COLUMN "app_releases"."created_at" IS '생성 시각';
|
||||
COMMENT ON COLUMN "app_releases"."updated_at" IS '수정 시각';
|
||||
|
||||
COMMENT ON TABLE "deployment_jobs" IS '애플리케이션 릴리즈 배포 작업 이력';
|
||||
COMMENT ON COLUMN "deployment_jobs"."id" IS '배포 작업 고유 ID';
|
||||
COMMENT ON COLUMN "deployment_jobs"."app_id" IS '배포 대상 애플리케이션 ID';
|
||||
COMMENT ON COLUMN "deployment_jobs"."release_id" IS '배포 대상 릴리즈 ID';
|
||||
COMMENT ON COLUMN "deployment_jobs"."environment" IS '배포 환경';
|
||||
COMMENT ON COLUMN "deployment_jobs"."namespace" IS '배포 대상 네임스페이스';
|
||||
COMMENT ON COLUMN "deployment_jobs"."status" IS '배포 작업 상태';
|
||||
COMMENT ON COLUMN "deployment_jobs"."requested_by" IS '배포 요청자';
|
||||
COMMENT ON COLUMN "deployment_jobs"."logs" IS '배포 로그';
|
||||
COMMENT ON COLUMN "deployment_jobs"."started_at" IS '배포 시작 시각';
|
||||
COMMENT ON COLUMN "deployment_jobs"."finished_at" IS '배포 종료 시각';
|
||||
COMMENT ON COLUMN "deployment_jobs"."created_at" IS '생성 시각';
|
||||
|
||||
COMMENT ON TABLE "launcher_versions" IS '스튜디오 메인 런처 버전 관리 정보';
|
||||
COMMENT ON COLUMN "launcher_versions"."id" IS '런처 버전 고유 ID';
|
||||
COMMENT ON COLUMN "launcher_versions"."version" IS '런처 버전';
|
||||
COMMENT ON COLUMN "launcher_versions"."minimum_version" IS '최소 지원 런처 버전';
|
||||
COMMENT ON COLUMN "launcher_versions"."artifact" IS '런처 빌드 산출물 위치 또는 식별자';
|
||||
COMMENT ON COLUMN "launcher_versions"."download_url" IS '런처 다운로드 URL';
|
||||
COMMENT ON COLUMN "launcher_versions"."rollout_percent" IS '점진 배포 비율';
|
||||
COMMENT ON COLUMN "launcher_versions"."release_notes" IS '런처 릴리즈 노트';
|
||||
COMMENT ON COLUMN "launcher_versions"."is_active" IS '활성 런처 버전 여부';
|
||||
COMMENT ON COLUMN "launcher_versions"."created_at" IS '생성 시각';
|
||||
COMMENT ON COLUMN "launcher_versions"."updated_at" IS '수정 시각';
|
||||
|
||||
COMMENT ON TABLE "launcher_common_settings" IS '런처 공통 연결 설정과 플랫폼 연동 설정';
|
||||
COMMENT ON COLUMN "launcher_common_settings"."id" IS '공통 설정 고유 ID';
|
||||
COMMENT ON COLUMN "launcher_common_settings"."key" IS '공통 설정 식별 키';
|
||||
COMMENT ON COLUMN "launcher_common_settings"."name" IS '공통 설정 표시 이름';
|
||||
COMMENT ON COLUMN "launcher_common_settings"."type" IS '공통 설정 유형';
|
||||
COMMENT ON COLUMN "launcher_common_settings"."value" IS '공통 설정 값';
|
||||
COMMENT ON COLUMN "launcher_common_settings"."description" IS '공통 설정 설명';
|
||||
COMMENT ON COLUMN "launcher_common_settings"."is_active" IS '공통 설정 활성 여부';
|
||||
COMMENT ON COLUMN "launcher_common_settings"."is_secret" IS '민감 정보 여부';
|
||||
COMMENT ON COLUMN "launcher_common_settings"."created_at" IS '생성 시각';
|
||||
COMMENT ON COLUMN "launcher_common_settings"."updated_at" IS '수정 시각';
|
||||
|
||||
COMMENT ON TABLE "launcher_app_links" IS '런처에 노출할 애플리케이션 연결 및 실행 설정';
|
||||
COMMENT ON COLUMN "launcher_app_links"."id" IS '런처 앱 연결 고유 ID';
|
||||
COMMENT ON COLUMN "launcher_app_links"."app_id" IS '연결된 애플리케이션 ID';
|
||||
COMMENT ON COLUMN "launcher_app_links"."release_id" IS '런처 실행에 사용할 릴리즈 ID';
|
||||
COMMENT ON COLUMN "launcher_app_links"."display_order" IS '런처 노출 순서';
|
||||
COMMENT ON COLUMN "launcher_app_links"."is_enabled" IS '런처 노출 및 실행 활성 여부';
|
||||
COMMENT ON COLUMN "launcher_app_links"."launch_url" IS '런처 실행 URL';
|
||||
COMMENT ON COLUMN "launcher_app_links"."artifact" IS '런처가 내려받을 실행 산출물 위치 또는 식별자';
|
||||
COMMENT ON COLUMN "launcher_app_links"."created_at" IS '생성 시각';
|
||||
COMMENT ON COLUMN "launcher_app_links"."updated_at" IS '수정 시각';
|
||||
|
||||
COMMENT ON TABLE "license_plan_snapshots" IS '공통 인증 DB에서 동기화한 라이센스 플랜 스냅샷';
|
||||
COMMENT ON COLUMN "license_plan_snapshots"."id" IS '라이센스 플랜 스냅샷 고유 ID';
|
||||
COMMENT ON COLUMN "license_plan_snapshots"."code" IS '라이센스 플랜 코드';
|
||||
COMMENT ON COLUMN "license_plan_snapshots"."name" IS '라이센스 플랜 표시 이름';
|
||||
COMMENT ON COLUMN "license_plan_snapshots"."description" IS '라이센스 플랜 설명';
|
||||
COMMENT ON COLUMN "license_plan_snapshots"."default_duration_days" IS '기본 라이센스 기간 일수';
|
||||
COMMENT ON COLUMN "license_plan_snapshots"."is_active" IS '라이센스 플랜 활성 여부';
|
||||
COMMENT ON COLUMN "license_plan_snapshots"."assigned_count" IS '전체 할당 수';
|
||||
COMMENT ON COLUMN "license_plan_snapshots"."active_user_count" IS '활성 사용자 수';
|
||||
COMMENT ON COLUMN "license_plan_snapshots"."expiring_soon_count" IS '만료 예정 사용자 수';
|
||||
COMMENT ON COLUMN "license_plan_snapshots"."synced_at" IS '원본 DB 동기화 시각';
|
||||
COMMENT ON COLUMN "license_plan_snapshots"."created_at" IS '생성 시각';
|
||||
COMMENT ON COLUMN "license_plan_snapshots"."updated_at" IS '수정 시각';
|
||||
|
||||
COMMENT ON TABLE "user_license_snapshots" IS '공통 인증 DB에서 동기화한 사용자 라이센스 할당 스냅샷';
|
||||
COMMENT ON COLUMN "user_license_snapshots"."id" IS '사용자 라이센스 스냅샷 고유 ID';
|
||||
COMMENT ON COLUMN "user_license_snapshots"."license_id" IS '라이센스 플랜 스냅샷 ID';
|
||||
COMMENT ON COLUMN "user_license_snapshots"."auth_user_id" IS '공통 인증 사용자 ID';
|
||||
COMMENT ON COLUMN "user_license_snapshots"."email" IS '사용자 이메일';
|
||||
COMMENT ON COLUMN "user_license_snapshots"."name" IS '사용자 이름';
|
||||
COMMENT ON COLUMN "user_license_snapshots"."starts_at" IS '라이센스 시작 시각';
|
||||
COMMENT ON COLUMN "user_license_snapshots"."expires_at" IS '라이센스 만료 시각';
|
||||
COMMENT ON COLUMN "user_license_snapshots"."assigned_by" IS '라이센스 할당자';
|
||||
COMMENT ON COLUMN "user_license_snapshots"."is_active" IS '사용자 라이센스 활성 여부';
|
||||
COMMENT ON COLUMN "user_license_snapshots"."synced_at" IS '원본 DB 동기화 시각';
|
||||
COMMENT ON COLUMN "user_license_snapshots"."created_at" IS '생성 시각';
|
||||
|
||||
COMMENT ON TABLE "runtime_control_jobs" IS '플랫폼 런타임 리소스 제어 작업 이력';
|
||||
COMMENT ON COLUMN "runtime_control_jobs"."id" IS '런타임 제어 작업 고유 ID';
|
||||
COMMENT ON COLUMN "runtime_control_jobs"."namespace" IS '제어 대상 네임스페이스';
|
||||
COMMENT ON COLUMN "runtime_control_jobs"."kind" IS '제어 대상 리소스 종류';
|
||||
COMMENT ON COLUMN "runtime_control_jobs"."name" IS '제어 대상 리소스 이름';
|
||||
COMMENT ON COLUMN "runtime_control_jobs"."action" IS '제어 작업 유형';
|
||||
COMMENT ON COLUMN "runtime_control_jobs"."status" IS '제어 작업 상태';
|
||||
COMMENT ON COLUMN "runtime_control_jobs"."requested_by" IS '제어 요청자';
|
||||
COMMENT ON COLUMN "runtime_control_jobs"."request" IS '제어 요청 페이로드';
|
||||
COMMENT ON COLUMN "runtime_control_jobs"."result" IS '제어 결과 페이로드';
|
||||
COMMENT ON COLUMN "runtime_control_jobs"."logs" IS '제어 작업 로그';
|
||||
COMMENT ON COLUMN "runtime_control_jobs"."started_at" IS '제어 시작 시각';
|
||||
COMMENT ON COLUMN "runtime_control_jobs"."finished_at" IS '제어 종료 시각';
|
||||
COMMENT ON COLUMN "runtime_control_jobs"."created_at" IS '생성 시각';
|
||||
|
||||
COMMENT ON TABLE "platform_connections" IS '플랫폼 게이트웨이 및 외부 플랫폼 연결 상태';
|
||||
COMMENT ON COLUMN "platform_connections"."id" IS '플랫폼 연결 고유 ID';
|
||||
COMMENT ON COLUMN "platform_connections"."name" IS '플랫폼 연결 이름';
|
||||
COMMENT ON COLUMN "platform_connections"."endpoint" IS '플랫폼 연결 엔드포인트';
|
||||
COMMENT ON COLUMN "platform_connections"."role" IS '플랫폼 연결 역할';
|
||||
COMMENT ON COLUMN "platform_connections"."status" IS '플랫폼 연결 상태';
|
||||
COMMENT ON COLUMN "platform_connections"."last_sync" IS '마지막 동기화 시각';
|
||||
COMMENT ON COLUMN "platform_connections"."metadata" IS '플랫폼 연결 메타데이터';
|
||||
COMMENT ON COLUMN "platform_connections"."created_at" IS '생성 시각';
|
||||
COMMENT ON COLUMN "platform_connections"."updated_at" IS '수정 시각';
|
||||
|
||||
COMMENT ON TABLE "tailscale_nodes" IS '테일스케일 노드 상태 스냅샷';
|
||||
COMMENT ON COLUMN "tailscale_nodes"."id" IS '테일스케일 노드 고유 ID';
|
||||
COMMENT ON COLUMN "tailscale_nodes"."name" IS '테일스케일 노드 이름';
|
||||
COMMENT ON COLUMN "tailscale_nodes"."ip" IS '테일스케일 노드 IP';
|
||||
COMMENT ON COLUMN "tailscale_nodes"."role" IS '테일스케일 노드 역할';
|
||||
COMMENT ON COLUMN "tailscale_nodes"."user" IS '테일스케일 사용자';
|
||||
COMMENT ON COLUMN "tailscale_nodes"."routes" IS '광고 또는 허용된 라우트';
|
||||
COMMENT ON COLUMN "tailscale_nodes"."status" IS '테일스케일 노드 상태';
|
||||
COMMENT ON COLUMN "tailscale_nodes"."last_seen" IS '마지막 확인 시각';
|
||||
COMMENT ON COLUMN "tailscale_nodes"."metadata" IS '테일스케일 노드 메타데이터';
|
||||
COMMENT ON COLUMN "tailscale_nodes"."created_at" IS '생성 시각';
|
||||
COMMENT ON COLUMN "tailscale_nodes"."updated_at" IS '수정 시각';
|
||||
|
||||
COMMENT ON TABLE "tailscale_settings" IS '테일스케일 연동 설정';
|
||||
COMMENT ON COLUMN "tailscale_settings"."id" IS '테일스케일 설정 고유 ID';
|
||||
COMMENT ON COLUMN "tailscale_settings"."key" IS '테일스케일 설정 식별 키';
|
||||
COMMENT ON COLUMN "tailscale_settings"."name" IS '테일스케일 설정 표시 이름';
|
||||
COMMENT ON COLUMN "tailscale_settings"."value" IS '테일스케일 설정 값';
|
||||
COMMENT ON COLUMN "tailscale_settings"."description" IS '테일스케일 설정 설명';
|
||||
COMMENT ON COLUMN "tailscale_settings"."status" IS '테일스케일 설정 상태';
|
||||
COMMENT ON COLUMN "tailscale_settings"."created_at" IS '생성 시각';
|
||||
COMMENT ON COLUMN "tailscale_settings"."updated_at" IS '수정 시각';
|
||||
@@ -0,0 +1,2 @@
|
||||
DROP TABLE IF EXISTS "user_license_snapshots";
|
||||
DROP TABLE IF EXISTS "license_plan_snapshots";
|
||||
@@ -0,0 +1,8 @@
|
||||
ALTER TABLE "launcher_versions"
|
||||
ADD COLUMN "repository" TEXT,
|
||||
ADD COLUMN "branch" TEXT,
|
||||
ADD COLUMN "commit_sha" TEXT;
|
||||
|
||||
COMMENT ON COLUMN "launcher_versions"."repository" IS '런처 버전을 생성한 Gitea 저장소';
|
||||
COMMENT ON COLUMN "launcher_versions"."branch" IS '런처 버전을 생성한 Gitea 브랜치';
|
||||
COMMENT ON COLUMN "launcher_versions"."commit_sha" IS '런처 버전을 생성한 Gitea 커밋 해시';
|
||||
@@ -0,0 +1,36 @@
|
||||
CREATE TABLE "admin_ssh_keys" (
|
||||
"id" UUID NOT NULL,
|
||||
"admin_id" UUID NOT NULL,
|
||||
"label" TEXT,
|
||||
"key_type" TEXT NOT NULL,
|
||||
"public_key" TEXT NOT NULL,
|
||||
"public_key_hash" TEXT NOT NULL,
|
||||
"fingerprint" TEXT NOT NULL,
|
||||
"gitea_username" TEXT,
|
||||
"gitea_key_id" INTEGER,
|
||||
"created_at" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMPTZ(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "admin_ssh_keys_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX "admin_ssh_keys_public_key_hash_key" ON "admin_ssh_keys"("public_key_hash");
|
||||
CREATE INDEX "admin_ssh_keys_admin_id_created_at_idx" ON "admin_ssh_keys"("admin_id", "created_at");
|
||||
CREATE INDEX "admin_ssh_keys_gitea_username_idx" ON "admin_ssh_keys"("gitea_username");
|
||||
|
||||
ALTER TABLE "admin_ssh_keys"
|
||||
ADD CONSTRAINT "admin_ssh_keys_admin_id_fkey"
|
||||
FOREIGN KEY ("admin_id") REFERENCES "admins"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
COMMENT ON TABLE "admin_ssh_keys" IS '관리자 계정별 Gitea SSH 공개키 등록 정보';
|
||||
COMMENT ON COLUMN "admin_ssh_keys"."id" IS '관리자 SSH 키 고유 ID';
|
||||
COMMENT ON COLUMN "admin_ssh_keys"."admin_id" IS 'SSH 키를 소유한 관리자 ID';
|
||||
COMMENT ON COLUMN "admin_ssh_keys"."label" IS 'SSH 키 표시명';
|
||||
COMMENT ON COLUMN "admin_ssh_keys"."key_type" IS 'SSH 공개키 타입';
|
||||
COMMENT ON COLUMN "admin_ssh_keys"."public_key" IS 'SSH 공개키 원문';
|
||||
COMMENT ON COLUMN "admin_ssh_keys"."public_key_hash" IS 'SSH 공개키 중복 방지용 해시';
|
||||
COMMENT ON COLUMN "admin_ssh_keys"."fingerprint" IS 'SSH 공개키 SHA256 fingerprint';
|
||||
COMMENT ON COLUMN "admin_ssh_keys"."gitea_username" IS '키를 등록할 Gitea 사용자명';
|
||||
COMMENT ON COLUMN "admin_ssh_keys"."gitea_key_id" IS 'Gitea에 등록된 SSH key ID';
|
||||
COMMENT ON COLUMN "admin_ssh_keys"."created_at" IS '생성 시각';
|
||||
COMMENT ON COLUMN "admin_ssh_keys"."updated_at" IS '수정 시각';
|
||||
@@ -0,0 +1,65 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "runtime_env_profiles" (
|
||||
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
|
||||
"app_id" UUID NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"environment" TEXT NOT NULL,
|
||||
"namespace" TEXT NOT NULL,
|
||||
"is_active" BOOLEAN NOT NULL DEFAULT true,
|
||||
"created_at" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMPTZ(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "runtime_env_profiles_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "runtime_env_variables" (
|
||||
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
|
||||
"profile_id" UUID NOT NULL,
|
||||
"component_id" UUID,
|
||||
"key" TEXT NOT NULL,
|
||||
"value" TEXT NOT NULL,
|
||||
"is_secret" BOOLEAN NOT NULL DEFAULT false,
|
||||
"created_at" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMPTZ(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "runtime_env_variables_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "runtime_env_profiles_app_id_environment_name_key" ON "runtime_env_profiles"("app_id", "environment", "name");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "runtime_env_profiles_app_id_environment_idx" ON "runtime_env_profiles"("app_id", "environment");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "runtime_env_variables_profile_id_component_id_key_key" ON "runtime_env_variables"("profile_id", "component_id", "key");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "runtime_env_variables_profile_id_is_secret_idx" ON "runtime_env_variables"("profile_id", "is_secret");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "runtime_env_variables_component_id_idx" ON "runtime_env_variables"("component_id");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "runtime_env_profiles" ADD CONSTRAINT "runtime_env_profiles_app_id_fkey" FOREIGN KEY ("app_id") REFERENCES "apps"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "runtime_env_variables" ADD CONSTRAINT "runtime_env_variables_profile_id_fkey" FOREIGN KEY ("profile_id") REFERENCES "runtime_env_profiles"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "runtime_env_variables" ADD CONSTRAINT "runtime_env_variables_component_id_fkey" FOREIGN KEY ("component_id") REFERENCES "app_components"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
COMMENT ON TABLE "runtime_env_profiles" IS '앱별 런타임 환경 프로파일';
|
||||
COMMENT ON COLUMN "runtime_env_profiles"."app_id" IS '앱 ID';
|
||||
COMMENT ON COLUMN "runtime_env_profiles"."name" IS '프로파일명';
|
||||
COMMENT ON COLUMN "runtime_env_profiles"."environment" IS '배포 환경';
|
||||
COMMENT ON COLUMN "runtime_env_profiles"."namespace" IS '적용 namespace';
|
||||
COMMENT ON COLUMN "runtime_env_profiles"."is_active" IS '활성 여부';
|
||||
|
||||
COMMENT ON TABLE "runtime_env_variables" IS '런타임 환경변수 및 시크릿';
|
||||
COMMENT ON COLUMN "runtime_env_variables"."profile_id" IS '런타임 환경 프로파일 ID';
|
||||
COMMENT ON COLUMN "runtime_env_variables"."component_id" IS '적용 구성요소 ID';
|
||||
COMMENT ON COLUMN "runtime_env_variables"."key" IS '환경변수 키';
|
||||
COMMENT ON COLUMN "runtime_env_variables"."value" IS '환경변수 값';
|
||||
COMMENT ON COLUMN "runtime_env_variables"."is_secret" IS 'Secret 적용 여부';
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TYPE "RuntimeAction" ADD VALUE IF NOT EXISTS 'MIGRATION';
|
||||
@@ -0,0 +1,38 @@
|
||||
CREATE TABLE "launcher_build_jobs" (
|
||||
"id" UUID NOT NULL,
|
||||
"target" TEXT NOT NULL DEFAULT 'launcher-client',
|
||||
"repository" TEXT,
|
||||
"branch" TEXT NOT NULL DEFAULT 'release',
|
||||
"commit_sha" TEXT,
|
||||
"version" TEXT NOT NULL,
|
||||
"artifact" TEXT NOT NULL,
|
||||
"upload_path" TEXT NOT NULL,
|
||||
"download_url" TEXT NOT NULL,
|
||||
"status" "JobStatus" NOT NULL DEFAULT 'PENDING',
|
||||
"logs" TEXT,
|
||||
"started_at" TIMESTAMPTZ(3),
|
||||
"finished_at" TIMESTAMPTZ(3),
|
||||
"created_at" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMPTZ(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "launcher_build_jobs_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE INDEX "launcher_build_jobs_status_created_at_idx" ON "launcher_build_jobs"("status", "created_at");
|
||||
|
||||
COMMENT ON TABLE "launcher_build_jobs" IS '런처 설치 파일 빌드 작업 이력';
|
||||
COMMENT ON COLUMN "launcher_build_jobs"."id" IS '런처 빌드 작업 고유 ID';
|
||||
COMMENT ON COLUMN "launcher_build_jobs"."target" IS '빌드 대상';
|
||||
COMMENT ON COLUMN "launcher_build_jobs"."repository" IS '빌드할 소스 저장소';
|
||||
COMMENT ON COLUMN "launcher_build_jobs"."branch" IS '빌드 기준 브랜치';
|
||||
COMMENT ON COLUMN "launcher_build_jobs"."commit_sha" IS '빌드 기준 커밋 해시';
|
||||
COMMENT ON COLUMN "launcher_build_jobs"."version" IS '런처 버전';
|
||||
COMMENT ON COLUMN "launcher_build_jobs"."artifact" IS '생성할 산출물 파일명';
|
||||
COMMENT ON COLUMN "launcher_build_jobs"."upload_path" IS '산출물 업로드 디렉터리';
|
||||
COMMENT ON COLUMN "launcher_build_jobs"."download_url" IS '런처 다운로드 URL';
|
||||
COMMENT ON COLUMN "launcher_build_jobs"."status" IS '빌드 작업 상태';
|
||||
COMMENT ON COLUMN "launcher_build_jobs"."logs" IS '빌드 작업 로그';
|
||||
COMMENT ON COLUMN "launcher_build_jobs"."started_at" IS '빌드 시작 시각';
|
||||
COMMENT ON COLUMN "launcher_build_jobs"."finished_at" IS '빌드 종료 시각';
|
||||
COMMENT ON COLUMN "launcher_build_jobs"."created_at" IS '생성 시각';
|
||||
COMMENT ON COLUMN "launcher_build_jobs"."updated_at" IS '수정 시각';
|
||||
@@ -0,0 +1,23 @@
|
||||
ALTER TABLE "app_components" ADD COLUMN "port" INTEGER;
|
||||
ALTER TABLE "app_components" ADD COLUMN "target_port" INTEGER;
|
||||
|
||||
CREATE TABLE "runtime_ingress_rules" (
|
||||
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
|
||||
"profile_id" UUID NOT NULL,
|
||||
"component_id" UUID NOT NULL,
|
||||
"host" TEXT NOT NULL,
|
||||
"path" TEXT NOT NULL DEFAULT '/',
|
||||
"service_port" INTEGER NOT NULL,
|
||||
"tls_enabled" BOOLEAN NOT NULL DEFAULT false,
|
||||
"tls_secret_name" TEXT,
|
||||
"created_at" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
CONSTRAINT "runtime_ingress_rules_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX "runtime_ingress_rules_profile_id_host_path_key" ON "runtime_ingress_rules"("profile_id", "host", "path");
|
||||
CREATE INDEX "runtime_ingress_rules_profile_id_idx" ON "runtime_ingress_rules"("profile_id");
|
||||
CREATE INDEX "runtime_ingress_rules_component_id_idx" ON "runtime_ingress_rules"("component_id");
|
||||
|
||||
ALTER TABLE "runtime_ingress_rules" ADD CONSTRAINT "runtime_ingress_rules_profile_id_fkey" FOREIGN KEY ("profile_id") REFERENCES "runtime_env_profiles"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
ALTER TABLE "runtime_ingress_rules" ADD CONSTRAINT "runtime_ingress_rules_component_id_fkey" FOREIGN KEY ("component_id") REFERENCES "app_components"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@@ -0,0 +1,20 @@
|
||||
ALTER TABLE "app_components"
|
||||
ADD COLUMN "image_source" TEXT DEFAULT 'SOURCE',
|
||||
ADD COLUMN "external_image" TEXT,
|
||||
ADD COLUMN "workload_type" TEXT DEFAULT 'Deployment',
|
||||
ADD COLUMN "replicas" INTEGER DEFAULT 1,
|
||||
ADD COLUMN "service_type" TEXT DEFAULT 'ClusterIP',
|
||||
ADD COLUMN "storage_enabled" BOOLEAN NOT NULL DEFAULT false,
|
||||
ADD COLUMN "storage_size" TEXT,
|
||||
ADD COLUMN "storage_mount_path" TEXT,
|
||||
ADD COLUMN "storage_retain" BOOLEAN NOT NULL DEFAULT true;
|
||||
|
||||
COMMENT ON COLUMN "app_components"."image_source" IS '구성요소 이미지 공급 방식. SOURCE, EXTERNAL, DATABASE 중 하나';
|
||||
COMMENT ON COLUMN "app_components"."external_image" IS '외부 이미지 사용 시 이미지 주소';
|
||||
COMMENT ON COLUMN "app_components"."workload_type" IS '실행 워크로드 유형. Deployment, StatefulSet, Job';
|
||||
COMMENT ON COLUMN "app_components"."replicas" IS '기본 복제 수';
|
||||
COMMENT ON COLUMN "app_components"."service_type" IS '서비스 유형. ClusterIP, NodePort 등';
|
||||
COMMENT ON COLUMN "app_components"."storage_enabled" IS '스토리지 사용 여부';
|
||||
COMMENT ON COLUMN "app_components"."storage_size" IS 'PVC 요청 용량';
|
||||
COMMENT ON COLUMN "app_components"."storage_mount_path" IS '컨테이너 마운트 경로';
|
||||
COMMENT ON COLUMN "app_components"."storage_retain" IS '워크로드 삭제 시 PVC 보존 여부';
|
||||
@@ -0,0 +1,4 @@
|
||||
ALTER TABLE "app_components"
|
||||
ADD COLUMN "ports" JSONB;
|
||||
|
||||
COMMENT ON COLUMN "app_components"."ports" IS '구성요소 서비스/컨테이너 포트 목록. [{name, port, targetPort, protocol}]';
|
||||
@@ -0,0 +1,3 @@
|
||||
# Please do not edit this file manually
|
||||
# It should be added in your version-control system (e.g., Git)
|
||||
provider = "postgresql"
|
||||
@@ -0,0 +1,379 @@
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
binaryTargets = ["native", "debian-openssl-3.0.x", "debian-openssl-1.1.x"]
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
enum AppStatus {
|
||||
ACTIVE
|
||||
READY
|
||||
DISABLED
|
||||
}
|
||||
|
||||
enum ComponentType {
|
||||
ELECTRON
|
||||
CONTAINER
|
||||
STATIC_WEB
|
||||
DATABASE
|
||||
MIGRATION
|
||||
}
|
||||
|
||||
enum JobStatus {
|
||||
PENDING
|
||||
RUNNING
|
||||
SUCCEEDED
|
||||
FAILED
|
||||
CANCELED
|
||||
}
|
||||
|
||||
enum AdminRole {
|
||||
OWNER
|
||||
ADMIN
|
||||
OPERATOR
|
||||
VIEWER
|
||||
}
|
||||
|
||||
enum RuntimeAction {
|
||||
MIGRATION
|
||||
RESTART
|
||||
SCALE
|
||||
LOGS
|
||||
EVENTS
|
||||
ROLLBACK
|
||||
STOP
|
||||
}
|
||||
|
||||
model AdminRoleOption {
|
||||
id String @id @default(uuid()) @db.Uuid
|
||||
code AdminRole @unique
|
||||
name String
|
||||
description String?
|
||||
sortOrder Int @default(0) @map("sort_order")
|
||||
isActive Boolean @default(true) @map("is_active")
|
||||
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)
|
||||
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(3)
|
||||
|
||||
@@index([isActive, sortOrder])
|
||||
@@map("admin_roles")
|
||||
}
|
||||
|
||||
model Admin {
|
||||
id String @id @default(uuid()) @db.Uuid
|
||||
email String @unique
|
||||
passwordHash String @map("password_hash")
|
||||
name String
|
||||
role AdminRole @default(ADMIN)
|
||||
isActive Boolean @default(true) @map("is_active")
|
||||
lastLoginAt DateTime? @map("last_login_at") @db.Timestamptz(3)
|
||||
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)
|
||||
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(3)
|
||||
sessions AdminSession[]
|
||||
auditLogs AdminAuditLog[]
|
||||
sshKeys AdminSshKey[]
|
||||
|
||||
@@index([isActive, role])
|
||||
@@map("admins")
|
||||
}
|
||||
|
||||
model AdminSshKey {
|
||||
id String @id @default(uuid()) @db.Uuid
|
||||
adminId String @map("admin_id") @db.Uuid
|
||||
label String?
|
||||
keyType String @map("key_type")
|
||||
publicKey String @map("public_key")
|
||||
publicKeyHash String @unique @map("public_key_hash")
|
||||
fingerprint String
|
||||
giteaUsername String? @map("gitea_username")
|
||||
giteaKeyId Int? @map("gitea_key_id")
|
||||
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)
|
||||
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(3)
|
||||
admin Admin @relation(fields: [adminId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([adminId, createdAt])
|
||||
@@index([giteaUsername])
|
||||
@@map("admin_ssh_keys")
|
||||
}
|
||||
|
||||
model AdminSession {
|
||||
id String @id @default(uuid()) @db.Uuid
|
||||
adminId String @map("admin_id") @db.Uuid
|
||||
tokenHash String @unique @map("token_hash")
|
||||
ipAddress String? @map("ip_address")
|
||||
userAgent String? @map("user_agent")
|
||||
expiresAt DateTime @map("expires_at") @db.Timestamptz(3)
|
||||
revokedAt DateTime? @map("revoked_at") @db.Timestamptz(3)
|
||||
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)
|
||||
admin Admin @relation(fields: [adminId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([adminId, expiresAt])
|
||||
@@map("admin_sessions")
|
||||
}
|
||||
|
||||
model AdminAuditLog {
|
||||
id String @id @default(uuid()) @db.Uuid
|
||||
adminId String? @map("admin_id") @db.Uuid
|
||||
action String
|
||||
resource String
|
||||
resourceId String? @map("resource_id")
|
||||
metadata Json?
|
||||
ipAddress String? @map("ip_address")
|
||||
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)
|
||||
admin Admin? @relation(fields: [adminId], references: [id], onDelete: SetNull)
|
||||
|
||||
@@index([resource, createdAt])
|
||||
@@index([adminId, createdAt])
|
||||
@@map("admin_audit_logs")
|
||||
}
|
||||
|
||||
model App {
|
||||
id String @id @default(uuid()) @db.Uuid
|
||||
key String @unique
|
||||
name String
|
||||
description String?
|
||||
namespace String?
|
||||
status AppStatus @default(READY)
|
||||
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)
|
||||
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(3)
|
||||
components AppComponent[]
|
||||
releases AppRelease[]
|
||||
deployments DeploymentJob[]
|
||||
runtimeProfiles RuntimeEnvProfile[]
|
||||
|
||||
@@map("apps")
|
||||
}
|
||||
|
||||
model AppComponent {
|
||||
id String @id @default(uuid()) @db.Uuid
|
||||
appId String @map("app_id") @db.Uuid
|
||||
name String
|
||||
type ComponentType
|
||||
imageSource String? @default("SOURCE") @map("image_source")
|
||||
externalImage String? @map("external_image")
|
||||
workloadType String? @default("Deployment") @map("workload_type")
|
||||
replicas Int? @default(1)
|
||||
serviceType String? @default("ClusterIP") @map("service_type")
|
||||
storageEnabled Boolean @default(false) @map("storage_enabled")
|
||||
storageSize String? @map("storage_size")
|
||||
storageMountPath String? @map("storage_mount_path")
|
||||
storageRetain Boolean @default(true) @map("storage_retain")
|
||||
repository String?
|
||||
branch String?
|
||||
buildTarget String? @map("build_target")
|
||||
port Int?
|
||||
targetPort Int? @map("target_port")
|
||||
ports Json?
|
||||
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)
|
||||
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(3)
|
||||
app App @relation(fields: [appId], references: [id], onDelete: Cascade)
|
||||
buildJobs BuildJob[]
|
||||
runtimeVariables RuntimeEnvVariable[]
|
||||
ingressRules RuntimeIngressRule[]
|
||||
|
||||
@@unique([appId, name])
|
||||
@@map("app_components")
|
||||
}
|
||||
|
||||
model BuildJob {
|
||||
id String @id @default(uuid()) @db.Uuid
|
||||
componentId String @map("component_id") @db.Uuid
|
||||
branch String
|
||||
commitSha String? @map("commit_sha")
|
||||
artifact String?
|
||||
image String?
|
||||
status JobStatus @default(PENDING)
|
||||
logs String?
|
||||
startedAt DateTime? @map("started_at") @db.Timestamptz(3)
|
||||
finishedAt DateTime? @map("finished_at") @db.Timestamptz(3)
|
||||
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)
|
||||
component AppComponent @relation(fields: [componentId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([componentId, createdAt])
|
||||
@@map("build_jobs")
|
||||
}
|
||||
|
||||
model AppRelease {
|
||||
id String @id @default(uuid()) @db.Uuid
|
||||
appId String @map("app_id") @db.Uuid
|
||||
version String
|
||||
branch String
|
||||
commitSha String? @map("commit_sha")
|
||||
artifact String?
|
||||
imageTags Json? @map("image_tags")
|
||||
status JobStatus @default(PENDING)
|
||||
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)
|
||||
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(3)
|
||||
app App @relation(fields: [appId], references: [id], onDelete: Cascade)
|
||||
deployments DeploymentJob[]
|
||||
|
||||
@@unique([appId, version])
|
||||
@@map("app_releases")
|
||||
}
|
||||
|
||||
model DeploymentJob {
|
||||
id String @id @default(uuid()) @db.Uuid
|
||||
appId String @map("app_id") @db.Uuid
|
||||
releaseId String? @map("release_id") @db.Uuid
|
||||
environment String
|
||||
namespace String
|
||||
status JobStatus @default(PENDING)
|
||||
requestedBy String? @map("requested_by")
|
||||
logs String?
|
||||
startedAt DateTime? @map("started_at") @db.Timestamptz(3)
|
||||
finishedAt DateTime? @map("finished_at") @db.Timestamptz(3)
|
||||
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)
|
||||
app App @relation(fields: [appId], references: [id], onDelete: Cascade)
|
||||
release AppRelease? @relation(fields: [releaseId], references: [id], onDelete: SetNull)
|
||||
|
||||
@@index([appId, createdAt])
|
||||
@@map("deployment_jobs")
|
||||
}
|
||||
|
||||
model LauncherBuildJob {
|
||||
id String @id @default(uuid()) @db.Uuid
|
||||
target String @default("launcher-client")
|
||||
repository String?
|
||||
branch String @default("release")
|
||||
commitSha String? @map("commit_sha")
|
||||
version String
|
||||
artifact String
|
||||
uploadPath String @map("upload_path")
|
||||
downloadUrl String @map("download_url")
|
||||
status JobStatus @default(PENDING)
|
||||
logs String?
|
||||
startedAt DateTime? @map("started_at") @db.Timestamptz(3)
|
||||
finishedAt DateTime? @map("finished_at") @db.Timestamptz(3)
|
||||
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)
|
||||
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(3)
|
||||
|
||||
@@index([status, createdAt])
|
||||
@@map("launcher_build_jobs")
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
model RuntimeControlJob {
|
||||
id String @id @default(uuid()) @db.Uuid
|
||||
namespace String
|
||||
kind String
|
||||
name String
|
||||
action RuntimeAction
|
||||
status JobStatus @default(PENDING)
|
||||
requestedBy String? @map("requested_by")
|
||||
request Json?
|
||||
result Json?
|
||||
logs String?
|
||||
startedAt DateTime? @map("started_at") @db.Timestamptz(3)
|
||||
finishedAt DateTime? @map("finished_at") @db.Timestamptz(3)
|
||||
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)
|
||||
|
||||
@@index([namespace, kind, name])
|
||||
@@index([createdAt])
|
||||
@@map("runtime_control_jobs")
|
||||
}
|
||||
|
||||
model RuntimeEnvProfile {
|
||||
id String @id @default(uuid()) @db.Uuid
|
||||
appId String @map("app_id") @db.Uuid
|
||||
name String
|
||||
environment String
|
||||
namespace String
|
||||
isActive Boolean @default(true) @map("is_active")
|
||||
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)
|
||||
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(3)
|
||||
app App @relation(fields: [appId], references: [id], onDelete: Cascade)
|
||||
variables RuntimeEnvVariable[]
|
||||
ingressRules RuntimeIngressRule[]
|
||||
|
||||
@@unique([appId, environment, name])
|
||||
@@index([appId, environment])
|
||||
@@map("runtime_env_profiles")
|
||||
}
|
||||
|
||||
model RuntimeEnvVariable {
|
||||
id String @id @default(uuid()) @db.Uuid
|
||||
profileId String @map("profile_id") @db.Uuid
|
||||
componentId String? @map("component_id") @db.Uuid
|
||||
key String
|
||||
value String
|
||||
isSecret Boolean @default(false) @map("is_secret")
|
||||
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)
|
||||
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(3)
|
||||
profile RuntimeEnvProfile @relation(fields: [profileId], references: [id], onDelete: Cascade)
|
||||
component AppComponent? @relation(fields: [componentId], references: [id], onDelete: SetNull)
|
||||
|
||||
@@unique([profileId, componentId, key])
|
||||
@@index([profileId, isSecret])
|
||||
@@index([componentId])
|
||||
@@map("runtime_env_variables")
|
||||
}
|
||||
|
||||
model RuntimeIngressRule {
|
||||
id String @id @default(uuid()) @db.Uuid
|
||||
profileId String @map("profile_id") @db.Uuid
|
||||
componentId String @map("component_id") @db.Uuid
|
||||
host String
|
||||
path String @default("/")
|
||||
servicePort Int @map("service_port")
|
||||
tlsEnabled Boolean @default(false) @map("tls_enabled")
|
||||
tlsSecretName String? @map("tls_secret_name")
|
||||
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)
|
||||
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(3)
|
||||
profile RuntimeEnvProfile @relation(fields: [profileId], references: [id], onDelete: Cascade)
|
||||
component AppComponent @relation(fields: [componentId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@unique([profileId, host, path])
|
||||
@@index([profileId])
|
||||
@@index([componentId])
|
||||
@@map("runtime_ingress_rules")
|
||||
}
|
||||
|
||||
model PlatformConnection {
|
||||
id String @id @default(uuid()) @db.Uuid
|
||||
name String @unique
|
||||
endpoint String
|
||||
role String
|
||||
status String
|
||||
lastSync DateTime? @map("last_sync") @db.Timestamptz(3)
|
||||
metadata Json?
|
||||
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)
|
||||
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(3)
|
||||
|
||||
@@index([status])
|
||||
@@map("platform_connections")
|
||||
}
|
||||
|
||||
model TailscaleNode {
|
||||
id String @id @default(uuid()) @db.Uuid
|
||||
name String @unique
|
||||
ip String
|
||||
role String?
|
||||
user String?
|
||||
routes String?
|
||||
status String
|
||||
lastSeen DateTime? @map("last_seen") @db.Timestamptz(3)
|
||||
metadata Json?
|
||||
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)
|
||||
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(3)
|
||||
|
||||
@@index([status, lastSeen])
|
||||
@@map("tailscale_nodes")
|
||||
}
|
||||
|
||||
model TailscaleSetting {
|
||||
id String @id @default(uuid()) @db.Uuid
|
||||
key String @unique
|
||||
name String
|
||||
value String
|
||||
description String?
|
||||
status String
|
||||
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)
|
||||
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(3)
|
||||
|
||||
@@index([status])
|
||||
@@map("tailscale_settings")
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { Body, Controller, Post, Req } from '@nestjs/common';
|
||||
|
||||
import { AdminAuthService } from './admin-auth.service';
|
||||
import { Public } from './decorators/admin-auth.decorators';
|
||||
import { AdminLoginDto } from './dto';
|
||||
|
||||
@Controller('auth/admin')
|
||||
export class AdminAuthController {
|
||||
constructor(private readonly adminAuthService: AdminAuthService) {}
|
||||
|
||||
@Public()
|
||||
@Post('login')
|
||||
login(
|
||||
@Body() body: AdminLoginDto,
|
||||
@Req() request: { ip?: string; headers: Record<string, string | string[] | undefined> },
|
||||
) {
|
||||
const userAgent = request.headers['user-agent'];
|
||||
|
||||
return this.adminAuthService.login(body, {
|
||||
ipAddress: request.ip,
|
||||
userAgent: Array.isArray(userAgent) ? userAgent.join(', ') : userAgent,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
import { CanActivate, ExecutionContext, ForbiddenException, Injectable, UnauthorizedException } from '@nestjs/common';
|
||||
import { Reflector } from '@nestjs/core';
|
||||
import { createHash } from 'node:crypto';
|
||||
|
||||
import { AdminRole } from '../admins/admin-role';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import { ADMIN_PUBLIC_KEY, ADMIN_ROLES_KEY } from './decorators/admin-auth.decorators';
|
||||
|
||||
type RequestWithAdmin = {
|
||||
method?: string;
|
||||
headers?: Record<string, string | string[] | undefined>;
|
||||
admin?: {
|
||||
id: string;
|
||||
email: string;
|
||||
name: string;
|
||||
role: AdminRole;
|
||||
};
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class AdminAuthGuard implements CanActivate {
|
||||
constructor(
|
||||
private readonly reflector: Reflector,
|
||||
private readonly prisma: PrismaService,
|
||||
) {}
|
||||
|
||||
async canActivate(context: ExecutionContext) {
|
||||
const isPublic = this.reflector.getAllAndOverride<boolean>(ADMIN_PUBLIC_KEY, [
|
||||
context.getHandler(),
|
||||
context.getClass(),
|
||||
]);
|
||||
|
||||
if (isPublic) return true;
|
||||
|
||||
const request = context.switchToHttp().getRequest<RequestWithAdmin>();
|
||||
if (request.method?.toUpperCase() === 'OPTIONS') return true;
|
||||
|
||||
const token = getBearerToken(request.headers?.authorization);
|
||||
if (!token) {
|
||||
throw new UnauthorizedException('관리자 로그인이 필요합니다.');
|
||||
}
|
||||
|
||||
const tokenHash = createHash('sha256').update(token).digest('hex');
|
||||
const session = await this.prisma.adminSession.findUnique({
|
||||
where: { tokenHash },
|
||||
include: {
|
||||
admin: {
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
name: true,
|
||||
role: true,
|
||||
isActive: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const now = new Date();
|
||||
if (!session || session.revokedAt || session.expiresAt <= now || !session.admin.isActive) {
|
||||
throw new UnauthorizedException('관리자 세션이 유효하지 않습니다.');
|
||||
}
|
||||
|
||||
request.admin = {
|
||||
id: session.admin.id,
|
||||
email: session.admin.email,
|
||||
name: session.admin.name,
|
||||
role: session.admin.role,
|
||||
};
|
||||
|
||||
const roles = this.reflector.getAllAndOverride<AdminRole[]>(ADMIN_ROLES_KEY, [
|
||||
context.getHandler(),
|
||||
context.getClass(),
|
||||
]);
|
||||
|
||||
if (roles?.length && !roles.includes(session.admin.role)) {
|
||||
throw new ForbiddenException('권한이 없습니다.');
|
||||
}
|
||||
|
||||
const method = request.method?.toUpperCase();
|
||||
if (session.admin.role === AdminRole.VIEWER && method && !['GET', 'HEAD', 'OPTIONS'].includes(method)) {
|
||||
throw new ForbiddenException('조회 권한만 허용됩니다.');
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
function getBearerToken(value: string | string[] | undefined) {
|
||||
const header = Array.isArray(value) ? value[0] : value;
|
||||
const match = header?.match(/^Bearer\s+(.+)$/i);
|
||||
return match?.[1]?.trim() || '';
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { PrismaModule } from '../prisma/prisma.module';
|
||||
import { AdminAuthController } from './admin-auth.controller';
|
||||
import { AdminAuthService } from './admin-auth.service';
|
||||
|
||||
@Module({
|
||||
imports: [PrismaModule],
|
||||
controllers: [AdminAuthController],
|
||||
providers: [AdminAuthService],
|
||||
})
|
||||
export class AdminAuthModule {}
|
||||
@@ -0,0 +1,83 @@
|
||||
import { randomBytes, scryptSync, timingSafeEqual, createHash } from 'node:crypto';
|
||||
|
||||
import { Injectable, UnauthorizedException } from '@nestjs/common';
|
||||
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import { AdminLoginDto } from './dto';
|
||||
|
||||
@Injectable()
|
||||
export class AdminAuthService {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
|
||||
async login(
|
||||
body: AdminLoginDto,
|
||||
context: { ipAddress?: string; userAgent?: string },
|
||||
) {
|
||||
const admin = await this.prisma.admin.findUnique({
|
||||
where: { email: body.email.trim().toLowerCase() },
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
name: true,
|
||||
role: true,
|
||||
isActive: true,
|
||||
passwordHash: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!admin || !admin.isActive || !verifyPassword(body.password, admin.passwordHash)) {
|
||||
throw new UnauthorizedException('이메일 또는 비밀번호가 올바르지 않습니다.');
|
||||
}
|
||||
|
||||
const token = randomBytes(32).toString('hex');
|
||||
const tokenHash = hashToken(token);
|
||||
const expiresAt = new Date(Date.now() + 1000 * 60 * 60 * 12);
|
||||
|
||||
await this.prisma.$transaction([
|
||||
this.prisma.admin.update({
|
||||
where: { id: admin.id },
|
||||
data: { lastLoginAt: new Date() },
|
||||
}),
|
||||
this.prisma.adminSession.create({
|
||||
data: {
|
||||
adminId: admin.id,
|
||||
tokenHash,
|
||||
ipAddress: context.ipAddress,
|
||||
userAgent: context.userAgent,
|
||||
expiresAt,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
return {
|
||||
token,
|
||||
expiresAt,
|
||||
admin: {
|
||||
id: admin.id,
|
||||
email: admin.email,
|
||||
name: admin.name,
|
||||
role: admin.role,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function verifyPassword(password: string, passwordHash: string) {
|
||||
const [scheme, salt, storedHash] = passwordHash.split(':');
|
||||
if (scheme !== 'scrypt' || !salt || !storedHash) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const currentHash = scryptSync(password, salt, 64);
|
||||
const storedBuffer = Buffer.from(storedHash, 'hex');
|
||||
|
||||
if (storedBuffer.length !== currentHash.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return timingSafeEqual(storedBuffer, currentHash);
|
||||
}
|
||||
|
||||
function hashToken(token: string) {
|
||||
return createHash('sha256').update(token).digest('hex');
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import { SetMetadata } from '@nestjs/common';
|
||||
import { AdminRole } from '../../admins/admin-role';
|
||||
|
||||
export const ADMIN_PUBLIC_KEY = 'adminPublic';
|
||||
export const ADMIN_ROLES_KEY = 'adminRoles';
|
||||
|
||||
export const Public = () => SetMetadata(ADMIN_PUBLIC_KEY, true);
|
||||
export const AdminRoles = (...roles: AdminRole[]) => SetMetadata(ADMIN_ROLES_KEY, roles);
|
||||
@@ -0,0 +1,9 @@
|
||||
import { IsEmail, IsString } from 'class-validator';
|
||||
|
||||
export class AdminLoginDto {
|
||||
@IsEmail()
|
||||
email!: string;
|
||||
|
||||
@IsString()
|
||||
password!: string;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
export const AdminRole = {
|
||||
OWNER: 'OWNER',
|
||||
ADMIN: 'ADMIN',
|
||||
OPERATOR: 'OPERATOR',
|
||||
VIEWER: 'VIEWER',
|
||||
} as const;
|
||||
|
||||
export type AdminRole = (typeof AdminRole)[keyof typeof AdminRole];
|
||||
@@ -0,0 +1,58 @@
|
||||
import { Body, Controller, Delete, Get, Param, Patch, Post } from '@nestjs/common';
|
||||
|
||||
import {
|
||||
CreateAdminSshKeyDto,
|
||||
CreateAdminDto,
|
||||
ResetAdminPasswordDto,
|
||||
UpdateAdminRoleDto,
|
||||
UpdateAdminStatusDto,
|
||||
} from './dto';
|
||||
import { AdminRoles } from '../admin-auth/decorators/admin-auth.decorators';
|
||||
import { AdminRole } from './admin-role';
|
||||
import { AdminsService } from './admins.service';
|
||||
|
||||
@AdminRoles(AdminRole.OWNER)
|
||||
@Controller('admins')
|
||||
export class AdminsController {
|
||||
constructor(private readonly adminsService: AdminsService) {}
|
||||
|
||||
@Get('roles')
|
||||
listRoles() {
|
||||
return this.adminsService.listRoles();
|
||||
}
|
||||
|
||||
@Get()
|
||||
list() {
|
||||
return this.adminsService.list();
|
||||
}
|
||||
|
||||
@Post()
|
||||
create(@Body() body: CreateAdminDto) {
|
||||
return this.adminsService.create(body);
|
||||
}
|
||||
|
||||
@Patch(':id/status')
|
||||
updateStatus(@Param('id') id: string, @Body() body: UpdateAdminStatusDto) {
|
||||
return this.adminsService.updateStatus(id, body);
|
||||
}
|
||||
|
||||
@Patch(':id/role')
|
||||
updateRole(@Param('id') id: string, @Body() body: UpdateAdminRoleDto) {
|
||||
return this.adminsService.updateRole(id, body);
|
||||
}
|
||||
|
||||
@Patch(':id/password')
|
||||
resetPassword(@Param('id') id: string, @Body() body: ResetAdminPasswordDto) {
|
||||
return this.adminsService.resetPassword(id, body);
|
||||
}
|
||||
|
||||
@Post(':id/ssh-keys')
|
||||
createSshKey(@Param('id') id: string, @Body() body: CreateAdminSshKeyDto) {
|
||||
return this.adminsService.createSshKey(id, body);
|
||||
}
|
||||
|
||||
@Delete(':id/ssh-keys/:keyId')
|
||||
deleteSshKey(@Param('id') id: string, @Param('keyId') keyId: string) {
|
||||
return this.adminsService.deleteSshKey(id, keyId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { GiteaModule } from '../gitea/gitea.module';
|
||||
import { PrismaModule } from '../prisma/prisma.module';
|
||||
import { AdminsController } from './admins.controller';
|
||||
import { AdminsService } from './admins.service';
|
||||
|
||||
@Module({
|
||||
imports: [GiteaModule, PrismaModule],
|
||||
controllers: [AdminsController],
|
||||
providers: [AdminsService],
|
||||
})
|
||||
export class AdminsModule {}
|
||||
@@ -0,0 +1,295 @@
|
||||
import { createHash, randomBytes, scryptSync } from 'node:crypto';
|
||||
|
||||
import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
|
||||
|
||||
import { GiteaService } from '../gitea/gitea.service';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import { AdminRole } from './admin-role';
|
||||
import {
|
||||
CreateAdminSshKeyDto,
|
||||
CreateAdminDto,
|
||||
ResetAdminPasswordDto,
|
||||
UpdateAdminRoleDto,
|
||||
UpdateAdminStatusDto,
|
||||
} from './dto';
|
||||
|
||||
@Injectable()
|
||||
export class AdminsService {
|
||||
constructor(
|
||||
private readonly giteaService: GiteaService,
|
||||
private readonly prisma: PrismaService,
|
||||
) {}
|
||||
|
||||
async listRoles() {
|
||||
await this.ensureDefaultRoles();
|
||||
|
||||
return this.prisma.adminRoleOption.findMany({
|
||||
where: { isActive: true },
|
||||
orderBy: [{ sortOrder: 'asc' }, { code: 'asc' }],
|
||||
select: {
|
||||
id: true,
|
||||
code: true,
|
||||
name: true,
|
||||
description: true,
|
||||
sortOrder: true,
|
||||
isActive: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
list() {
|
||||
return this.prisma.admin.findMany({
|
||||
orderBy: [{ role: 'asc' }, { createdAt: 'desc' }],
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
name: true,
|
||||
role: true,
|
||||
isActive: true,
|
||||
lastLoginAt: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
sessions: {
|
||||
orderBy: { createdAt: 'desc' },
|
||||
take: 1,
|
||||
select: {
|
||||
id: true,
|
||||
expiresAt: true,
|
||||
revokedAt: true,
|
||||
createdAt: true,
|
||||
},
|
||||
},
|
||||
sshKeys: {
|
||||
orderBy: { createdAt: 'desc' },
|
||||
select: adminSshKeySelect,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async create(body: CreateAdminDto) {
|
||||
await this.ensureRoleEnabled(body.role);
|
||||
|
||||
const admin = await this.prisma.admin.create({
|
||||
data: {
|
||||
email: body.email.trim().toLowerCase(),
|
||||
name: body.name.trim(),
|
||||
role: body.role,
|
||||
passwordHash: hashPassword(body.temporaryPassword),
|
||||
},
|
||||
select: adminSelect,
|
||||
});
|
||||
|
||||
return { admin };
|
||||
}
|
||||
|
||||
async updateStatus(id: string, body: UpdateAdminStatusDto) {
|
||||
await this.ensureExists(id);
|
||||
|
||||
const admin = await this.prisma.admin.update({
|
||||
where: { id },
|
||||
data: { isActive: body.isActive },
|
||||
select: adminSelect,
|
||||
});
|
||||
|
||||
if (!body.isActive) {
|
||||
await this.prisma.adminSession.updateMany({
|
||||
where: { adminId: id, revokedAt: null },
|
||||
data: { revokedAt: new Date() },
|
||||
});
|
||||
}
|
||||
|
||||
return { admin };
|
||||
}
|
||||
|
||||
async updateRole(id: string, body: UpdateAdminRoleDto) {
|
||||
await this.ensureExists(id);
|
||||
await this.ensureRoleEnabled(body.role);
|
||||
|
||||
const admin = await this.prisma.admin.update({
|
||||
where: { id },
|
||||
data: { role: body.role },
|
||||
select: adminSelect,
|
||||
});
|
||||
|
||||
return { admin };
|
||||
}
|
||||
|
||||
async resetPassword(id: string, body: ResetAdminPasswordDto) {
|
||||
await this.ensureExists(id);
|
||||
|
||||
const admin = await this.prisma.admin.update({
|
||||
where: { id },
|
||||
data: { passwordHash: hashPassword(body.temporaryPassword) },
|
||||
select: adminSelect,
|
||||
});
|
||||
|
||||
await this.prisma.adminSession.updateMany({
|
||||
where: { adminId: id, revokedAt: null },
|
||||
data: { revokedAt: new Date() },
|
||||
});
|
||||
|
||||
return { admin };
|
||||
}
|
||||
|
||||
async createSshKey(adminId: string, body: CreateAdminSshKeyDto) {
|
||||
await this.ensureExists(adminId);
|
||||
|
||||
const parsedKey = parseSshPublicKey(body.publicKey);
|
||||
const label = body.label?.trim() || `${parsedKey.keyType} ${parsedKey.fingerprint}`;
|
||||
const giteaUsername = body.giteaUsername?.trim() || null;
|
||||
let giteaKeyId: number | null = null;
|
||||
|
||||
if (giteaUsername) {
|
||||
const giteaKey = await this.giteaService.createUserSshKey(giteaUsername, {
|
||||
title: label,
|
||||
key: parsedKey.normalizedKey,
|
||||
});
|
||||
giteaKeyId = giteaKey.id;
|
||||
}
|
||||
|
||||
const sshKey = await this.prisma.adminSshKey.create({
|
||||
data: {
|
||||
adminId,
|
||||
label,
|
||||
keyType: parsedKey.keyType,
|
||||
publicKey: parsedKey.normalizedKey,
|
||||
publicKeyHash: parsedKey.publicKeyHash,
|
||||
fingerprint: parsedKey.fingerprint,
|
||||
giteaUsername,
|
||||
giteaKeyId,
|
||||
},
|
||||
select: adminSshKeySelect,
|
||||
});
|
||||
|
||||
return { sshKey };
|
||||
}
|
||||
|
||||
async deleteSshKey(adminId: string, keyId: string) {
|
||||
await this.ensureExists(adminId);
|
||||
|
||||
const sshKey = await this.prisma.adminSshKey.findFirst({
|
||||
where: { id: keyId, adminId },
|
||||
select: {
|
||||
id: true,
|
||||
giteaUsername: true,
|
||||
giteaKeyId: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!sshKey) {
|
||||
throw new NotFoundException('SSH 키를 찾을 수 없습니다.');
|
||||
}
|
||||
|
||||
await this.prisma.adminSshKey.delete({ where: { id: keyId } });
|
||||
|
||||
if (sshKey.giteaUsername && sshKey.giteaKeyId) {
|
||||
await this.giteaService.deleteUserSshKey(
|
||||
sshKey.giteaUsername,
|
||||
sshKey.giteaKeyId,
|
||||
);
|
||||
}
|
||||
|
||||
return { deleted: true };
|
||||
}
|
||||
|
||||
private async ensureRoleEnabled(role: AdminRole) {
|
||||
await this.ensureDefaultRoles();
|
||||
|
||||
const roleOption = await this.prisma.adminRoleOption.findUnique({
|
||||
where: { code: role },
|
||||
select: { isActive: true },
|
||||
});
|
||||
|
||||
if (!roleOption?.isActive) {
|
||||
throw new BadRequestException('사용할 수 없는 관리자 권한입니다.');
|
||||
}
|
||||
}
|
||||
|
||||
private async ensureDefaultRoles() {
|
||||
await this.prisma.adminRoleOption.createMany({
|
||||
data: [
|
||||
{ code: AdminRole.OWNER, name: 'OWNER', description: '전체 관리자 권한', sortOrder: 1 },
|
||||
{ code: AdminRole.ADMIN, name: 'ADMIN', description: '관리자 권한', sortOrder: 2 },
|
||||
{ code: AdminRole.VIEWER, name: 'VIEWER', description: '조회 권한', sortOrder: 3 },
|
||||
],
|
||||
skipDuplicates: true,
|
||||
});
|
||||
}
|
||||
|
||||
private async ensureExists(id: string) {
|
||||
const admin = await this.prisma.admin.findUnique({
|
||||
where: { id },
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
if (!admin) {
|
||||
throw new NotFoundException('관리자를 찾을 수 없습니다.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const adminSelect = {
|
||||
id: true,
|
||||
email: true,
|
||||
name: true,
|
||||
role: true,
|
||||
isActive: true,
|
||||
lastLoginAt: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
} as const;
|
||||
|
||||
const adminSshKeySelect = {
|
||||
id: true,
|
||||
label: true,
|
||||
keyType: true,
|
||||
fingerprint: true,
|
||||
giteaUsername: true,
|
||||
giteaKeyId: true,
|
||||
createdAt: true,
|
||||
} as const;
|
||||
|
||||
function hashPassword(password: string) {
|
||||
const salt = randomBytes(16).toString('hex');
|
||||
const hash = scryptSync(password, salt, 64).toString('hex');
|
||||
return `scrypt:${salt}:${hash}`;
|
||||
}
|
||||
|
||||
function parseSshPublicKey(publicKey: string) {
|
||||
const normalizedKey = publicKey.trim().replace(/\s+/g, ' ');
|
||||
const [keyType, keyBody] = normalizedKey.split(' ');
|
||||
|
||||
if (!keyType || !keyBody || !isSupportedSshKeyType(keyType)) {
|
||||
throw new BadRequestException('지원하지 않는 SSH 공개키 형식입니다.');
|
||||
}
|
||||
|
||||
let keyBuffer: Buffer;
|
||||
try {
|
||||
keyBuffer = Buffer.from(keyBody, 'base64');
|
||||
} catch {
|
||||
throw new BadRequestException('SSH 공개키 base64 값을 확인하세요.');
|
||||
}
|
||||
|
||||
if (!keyBuffer.length) {
|
||||
throw new BadRequestException('SSH 공개키 값이 비어 있습니다.');
|
||||
}
|
||||
|
||||
return {
|
||||
keyType,
|
||||
normalizedKey,
|
||||
publicKeyHash: createHash('sha256').update(normalizedKey).digest('hex'),
|
||||
fingerprint: `SHA256:${createHash('sha256')
|
||||
.update(keyBuffer)
|
||||
.digest('base64')
|
||||
.replace(/=+$/, '')}`,
|
||||
};
|
||||
}
|
||||
|
||||
function isSupportedSshKeyType(keyType: string) {
|
||||
return (
|
||||
keyType === 'ssh-ed25519' ||
|
||||
keyType === 'ssh-rsa' ||
|
||||
keyType.startsWith('ecdsa-sha2-')
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import { IsBoolean, IsEmail, IsEnum, IsOptional, IsString, MinLength } from 'class-validator';
|
||||
import { AdminRole } from './admin-role';
|
||||
|
||||
export class CreateAdminDto {
|
||||
@IsEmail()
|
||||
email!: string;
|
||||
|
||||
@IsString()
|
||||
name!: string;
|
||||
|
||||
@IsEnum(AdminRole)
|
||||
role!: AdminRole;
|
||||
|
||||
@IsString()
|
||||
@MinLength(8)
|
||||
temporaryPassword!: string;
|
||||
}
|
||||
|
||||
export class UpdateAdminStatusDto {
|
||||
@IsBoolean()
|
||||
isActive!: boolean;
|
||||
}
|
||||
|
||||
export class UpdateAdminRoleDto {
|
||||
@IsEnum(AdminRole)
|
||||
role!: AdminRole;
|
||||
}
|
||||
|
||||
export class ResetAdminPasswordDto {
|
||||
@IsString()
|
||||
@MinLength(8)
|
||||
temporaryPassword!: string;
|
||||
}
|
||||
|
||||
export class CreateAdminSshKeyDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
label?: string;
|
||||
|
||||
@IsString()
|
||||
publicKey!: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
giteaUsername?: string;
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { Controller, Delete, Get, Param, Patch, Query, Body } from '@nestjs/common';
|
||||
|
||||
import {
|
||||
ListLauncherSshKeysQuery,
|
||||
ListProvisionedDevicesQuery,
|
||||
UpdateProvisionedDeviceDto,
|
||||
} from './dto';
|
||||
import { AgentsService } from './agents.service';
|
||||
|
||||
@Controller('agents')
|
||||
export class AgentsController {
|
||||
constructor(private readonly agentsService: AgentsService) {}
|
||||
|
||||
@Get('devices')
|
||||
listDevices(@Query() query: ListProvisionedDevicesQuery) {
|
||||
return this.agentsService.listDevices(query);
|
||||
}
|
||||
|
||||
@Get('devices/:id')
|
||||
getDevice(@Param('id') id: string) {
|
||||
return this.agentsService.getDevice(id);
|
||||
}
|
||||
|
||||
@Patch('devices/:id')
|
||||
updateDevice(
|
||||
@Param('id') id: string,
|
||||
@Body() body: UpdateProvisionedDeviceDto,
|
||||
) {
|
||||
return this.agentsService.updateDevice(id, body);
|
||||
}
|
||||
|
||||
@Get('ssh-keys')
|
||||
listSshKeys(@Query() query: ListLauncherSshKeysQuery) {
|
||||
return this.agentsService.listSshKeys(query);
|
||||
}
|
||||
|
||||
@Delete('ssh-keys/:id')
|
||||
removeSshKey(@Param('id') id: string) {
|
||||
return this.agentsService.removeSshKey(id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { LauncherPrismaModule } from '../launcher-prisma/launcher-prisma.module';
|
||||
import { AgentsController } from './agents.controller';
|
||||
import { AgentsService } from './agents.service';
|
||||
|
||||
@Module({
|
||||
imports: [LauncherPrismaModule],
|
||||
controllers: [AgentsController],
|
||||
providers: [AgentsService],
|
||||
})
|
||||
export class AgentsModule {}
|
||||
@@ -0,0 +1,136 @@
|
||||
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||
|
||||
import { LauncherPrismaService } from '../launcher-prisma/launcher-prisma.service';
|
||||
import {
|
||||
ListLauncherSshKeysQuery,
|
||||
ListProvisionedDevicesQuery,
|
||||
UpdateProvisionedDeviceDto,
|
||||
} from './dto';
|
||||
|
||||
@Injectable()
|
||||
export class AgentsService {
|
||||
constructor(private readonly launcherPrisma: LauncherPrismaService) {}
|
||||
|
||||
async listDevices(query: ListProvisionedDevicesQuery) {
|
||||
const keyword = query.keyword?.trim();
|
||||
|
||||
const devices = await this.launcherPrisma.provisionedDevice.findMany({
|
||||
where: {
|
||||
accountEmail: query.accountEmail || undefined,
|
||||
usageType: query.usageType || undefined,
|
||||
OR: keyword
|
||||
? [
|
||||
{ accountEmail: { contains: keyword, mode: 'insensitive' } },
|
||||
{ hostname: { contains: keyword, mode: 'insensitive' } },
|
||||
{
|
||||
tailscaleHostname: {
|
||||
contains: keyword,
|
||||
mode: 'insensitive',
|
||||
},
|
||||
},
|
||||
{ sshUser: { contains: keyword, mode: 'insensitive' } },
|
||||
{ workspaceRoot: { contains: keyword, mode: 'insensitive' } },
|
||||
]
|
||||
: undefined,
|
||||
},
|
||||
orderBy: [{ lastProvisionedAt: 'desc' }, { createdAt: 'desc' }],
|
||||
});
|
||||
|
||||
return {
|
||||
devices: devices.map((device) => ({
|
||||
...device,
|
||||
status: getDeviceStatus(device.lastProvisionedAt),
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
async getDevice(id: string) {
|
||||
const device = await this.launcherPrisma.provisionedDevice.findUnique({
|
||||
where: { id },
|
||||
});
|
||||
|
||||
if (!device) {
|
||||
throw new NotFoundException('장비를 찾을 수 없습니다.');
|
||||
}
|
||||
|
||||
return { device: { ...device, status: getDeviceStatus(device.lastProvisionedAt) } };
|
||||
}
|
||||
|
||||
async updateDevice(id: string, body: UpdateProvisionedDeviceDto) {
|
||||
await this.ensureDeviceExists(id);
|
||||
|
||||
const device = await this.launcherPrisma.provisionedDevice.update({
|
||||
where: { id },
|
||||
data: {
|
||||
sshUser: body.sshUser,
|
||||
workspaceRoot: body.workspaceRoot,
|
||||
usageType: body.usageType,
|
||||
},
|
||||
});
|
||||
|
||||
return { device };
|
||||
}
|
||||
|
||||
async listSshKeys(query: ListLauncherSshKeysQuery) {
|
||||
const keyword = query.keyword?.trim();
|
||||
|
||||
const sshKeys = await this.launcherPrisma.launcherSshKey.findMany({
|
||||
where: {
|
||||
accountEmail: query.accountEmail || undefined,
|
||||
OR: keyword
|
||||
? [
|
||||
{ accountEmail: { contains: keyword, mode: 'insensitive' } },
|
||||
{ label: { contains: keyword, mode: 'insensitive' } },
|
||||
{ publicKeyHash: { contains: keyword, mode: 'insensitive' } },
|
||||
{ keyType: { contains: keyword, mode: 'insensitive' } },
|
||||
]
|
||||
: undefined,
|
||||
},
|
||||
orderBy: [{ lastProvisionedAt: 'desc' }, { createdAt: 'desc' }],
|
||||
});
|
||||
|
||||
return { sshKeys };
|
||||
}
|
||||
|
||||
async removeSshKey(id: string) {
|
||||
const sshKey = await this.launcherPrisma.launcherSshKey.findUnique({
|
||||
where: { id },
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
if (!sshKey) {
|
||||
throw new NotFoundException('SSH 키를 찾을 수 없습니다.');
|
||||
}
|
||||
|
||||
await this.launcherPrisma.launcherSshKey.delete({
|
||||
where: { id },
|
||||
});
|
||||
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
private async ensureDeviceExists(id: string) {
|
||||
const device = await this.launcherPrisma.provisionedDevice.findUnique({
|
||||
where: { id },
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
if (!device) {
|
||||
throw new NotFoundException('장비를 찾을 수 없습니다.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getDeviceStatus(lastProvisionedAt: Date) {
|
||||
const minutes = (Date.now() - lastProvisionedAt.getTime()) / 1000 / 60;
|
||||
|
||||
if (minutes <= 15) {
|
||||
return '정상';
|
||||
}
|
||||
|
||||
if (minutes <= 1440) {
|
||||
return '확인 필요';
|
||||
}
|
||||
|
||||
return '장기 미접속';
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import { IsOptional, IsString } from 'class-validator';
|
||||
|
||||
export class ListProvisionedDevicesQuery {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
keyword?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
accountEmail?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
usageType?: string;
|
||||
}
|
||||
|
||||
export class UpdateProvisionedDeviceDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
sshUser?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
workspaceRoot?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
usageType?: string;
|
||||
}
|
||||
|
||||
export class ListLauncherSshKeysQuery {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
accountEmail?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
keyword?: string;
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { APP_GUARD } from '@nestjs/core';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
|
||||
import { AdminAuthGuard } from './admin-auth/admin-auth.guard';
|
||||
import { AdminAuthModule } from './admin-auth/admin-auth.module';
|
||||
import { AdminsModule } from './admins/admins.module';
|
||||
import { AgentsModule } from './agents/agents.module';
|
||||
import { AppsModule } from './apps/apps.module';
|
||||
import { GiteaModule } from './gitea/gitea.module';
|
||||
import { HealthModule } from './health/health.module';
|
||||
import { LauncherModule } from './launcher/launcher.module';
|
||||
import { LicensesModule } from './licenses/licenses.module';
|
||||
import { NoticesModule } from './notices/notices.module';
|
||||
import { PrismaModule } from './prisma/prisma.module';
|
||||
import { RuntimeModule } from './runtime/runtime.module';
|
||||
import { UsersModule } from './users/users.module';
|
||||
|
||||
@Module({
|
||||
providers: [
|
||||
{
|
||||
provide: APP_GUARD,
|
||||
useClass: AdminAuthGuard,
|
||||
},
|
||||
],
|
||||
imports: [
|
||||
ConfigModule.forRoot({
|
||||
isGlobal: true,
|
||||
}),
|
||||
AdminAuthModule,
|
||||
AdminsModule,
|
||||
AgentsModule,
|
||||
AppsModule,
|
||||
GiteaModule,
|
||||
HealthModule,
|
||||
LauncherModule,
|
||||
LicensesModule,
|
||||
NoticesModule,
|
||||
PrismaModule,
|
||||
RuntimeModule,
|
||||
UsersModule,
|
||||
],
|
||||
})
|
||||
export class AppModule {}
|
||||
@@ -0,0 +1,17 @@
|
||||
export const AppStatus = {
|
||||
ACTIVE: 'ACTIVE',
|
||||
READY: 'READY',
|
||||
DISABLED: 'DISABLED',
|
||||
} as const;
|
||||
|
||||
export type AppStatus = (typeof AppStatus)[keyof typeof AppStatus];
|
||||
|
||||
export const ComponentType = {
|
||||
ELECTRON: 'ELECTRON',
|
||||
CONTAINER: 'CONTAINER',
|
||||
STATIC_WEB: 'STATIC_WEB',
|
||||
DATABASE: 'DATABASE',
|
||||
MIGRATION: 'MIGRATION',
|
||||
} as const;
|
||||
|
||||
export type ComponentType = (typeof ComponentType)[keyof typeof ComponentType];
|
||||
@@ -0,0 +1,190 @@
|
||||
import { Body, Controller, Delete, Get, Param, Patch, Post, Query } from '@nestjs/common';
|
||||
|
||||
import { LinkComponentSourceDto } from '../gitea/dto';
|
||||
import { AppsService } from './apps.service';
|
||||
import {
|
||||
CreateAppComponentDto,
|
||||
CreateAppDto,
|
||||
CreateAppReleaseDto,
|
||||
CreateBuildJobDto,
|
||||
CreateDeploymentJobDto,
|
||||
CreateMigrationJobDto,
|
||||
CreateRuntimeEnvProfileDto,
|
||||
CreateRuntimeEnvVariableDto,
|
||||
CreateRuntimeIngressRuleDto,
|
||||
EnsureRuntimeNamespaceDto,
|
||||
} from './dto';
|
||||
|
||||
@Controller('apps')
|
||||
export class AppsController {
|
||||
constructor(private readonly appsService: AppsService) {}
|
||||
|
||||
@Get()
|
||||
listApps() {
|
||||
return this.appsService.listApps();
|
||||
}
|
||||
|
||||
@Post()
|
||||
createApp(@Body() body: CreateAppDto) {
|
||||
return this.appsService.createApp(body);
|
||||
}
|
||||
|
||||
@Delete(':appId')
|
||||
deleteApp(@Param('appId') appId: string) {
|
||||
return this.appsService.deleteApp(appId);
|
||||
}
|
||||
|
||||
@Post(':appId/components')
|
||||
createComponent(
|
||||
@Param('appId') appId: string,
|
||||
@Body() body: CreateAppComponentDto,
|
||||
) {
|
||||
return this.appsService.createComponent(appId, body);
|
||||
}
|
||||
|
||||
@Patch('components/:componentId')
|
||||
updateComponent(
|
||||
@Param('componentId') componentId: string,
|
||||
@Body() body: CreateAppComponentDto,
|
||||
) {
|
||||
return this.appsService.updateComponent(componentId, body);
|
||||
}
|
||||
|
||||
@Delete('components/:componentId')
|
||||
deleteComponent(@Param('componentId') componentId: string) {
|
||||
return this.appsService.deleteComponent(componentId);
|
||||
}
|
||||
|
||||
@Get('sources')
|
||||
listSources() {
|
||||
return this.appsService.listSources();
|
||||
}
|
||||
|
||||
@Get('builds')
|
||||
listBuildJobs(@Query('appKey') appKey?: string) {
|
||||
return this.appsService.listBuildJobs(appKey);
|
||||
}
|
||||
|
||||
@Post('builds')
|
||||
createBuildJob(@Body() body: CreateBuildJobDto) {
|
||||
return this.appsService.createBuildJob(body);
|
||||
}
|
||||
|
||||
@Patch('builds/:buildJobId/cancel')
|
||||
cancelBuildJob(@Param('buildJobId') buildJobId: string) {
|
||||
return this.appsService.cancelBuildJob(buildJobId);
|
||||
}
|
||||
|
||||
@Delete('builds/:buildJobId')
|
||||
deleteBuildJob(@Param('buildJobId') buildJobId: string) {
|
||||
return this.appsService.deleteBuildJob(buildJobId);
|
||||
}
|
||||
|
||||
@Patch('components/:componentId/source')
|
||||
linkComponentSource(
|
||||
@Param('componentId') componentId: string,
|
||||
@Body() body: LinkComponentSourceDto,
|
||||
) {
|
||||
return this.appsService.linkComponentSource(componentId, body);
|
||||
}
|
||||
|
||||
@Get('releases')
|
||||
listReleases(@Query('appKey') appKey?: string) {
|
||||
return this.appsService.listReleases(appKey);
|
||||
}
|
||||
|
||||
@Post('releases')
|
||||
createRelease(@Body() body: CreateAppReleaseDto) {
|
||||
return this.appsService.createRelease(body);
|
||||
}
|
||||
|
||||
@Get('deployments')
|
||||
listDeployments(@Query('appKey') appKey?: string) {
|
||||
return this.appsService.listDeployments(appKey);
|
||||
}
|
||||
|
||||
@Post('deployments')
|
||||
createDeploymentJob(@Body() body: CreateDeploymentJobDto) {
|
||||
return this.appsService.createDeploymentJob(body);
|
||||
}
|
||||
|
||||
@Get('migrations')
|
||||
listMigrationJobs() {
|
||||
return this.appsService.listMigrationJobs();
|
||||
}
|
||||
|
||||
@Post('migrations')
|
||||
createMigrationJob(@Body() body: CreateMigrationJobDto) {
|
||||
return this.appsService.createMigrationJob(body);
|
||||
}
|
||||
|
||||
@Get('runtime-namespaces/:namespace')
|
||||
checkRuntimeNamespace(@Param('namespace') namespace: string) {
|
||||
return this.appsService.checkRuntimeNamespace(namespace);
|
||||
}
|
||||
|
||||
@Get('runtime-namespaces/:namespace/resources')
|
||||
listRuntimeResources(@Param('namespace') namespace: string) {
|
||||
return this.appsService.listRuntimeResources(namespace);
|
||||
}
|
||||
|
||||
@Post('runtime-namespaces')
|
||||
ensureRuntimeNamespace(@Body() body: EnsureRuntimeNamespaceDto) {
|
||||
return this.appsService.ensureRuntimeNamespace(body);
|
||||
}
|
||||
|
||||
@Patch('runtime-deployments/:namespace/:deploymentName/restart')
|
||||
restartRuntimeDeployment(
|
||||
@Param('namespace') namespace: string,
|
||||
@Param('deploymentName') deploymentName: string,
|
||||
) {
|
||||
return this.appsService.restartRuntimeDeployment(namespace, deploymentName);
|
||||
}
|
||||
|
||||
@Get('runtime-profiles')
|
||||
listRuntimeProfiles(@Query('appId') appId?: string) {
|
||||
return this.appsService.listRuntimeProfiles(appId);
|
||||
}
|
||||
|
||||
@Post('runtime-profiles')
|
||||
createRuntimeProfile(@Body() body: CreateRuntimeEnvProfileDto) {
|
||||
return this.appsService.createRuntimeProfile(body);
|
||||
}
|
||||
|
||||
@Post('runtime-profiles/:profileId/variables')
|
||||
createRuntimeVariable(
|
||||
@Param('profileId') profileId: string,
|
||||
@Body() body: CreateRuntimeEnvVariableDto,
|
||||
) {
|
||||
return this.appsService.createRuntimeVariable(profileId, body);
|
||||
}
|
||||
|
||||
@Post('runtime-profiles/:profileId/ingress-rules')
|
||||
createRuntimeIngressRule(
|
||||
@Param('profileId') profileId: string,
|
||||
@Body() body: CreateRuntimeIngressRuleDto,
|
||||
) {
|
||||
return this.appsService.createRuntimeIngressRule(profileId, body);
|
||||
}
|
||||
|
||||
@Delete('runtime-profiles/:profileId/ingress-rules/:ruleId')
|
||||
deleteRuntimeIngressRule(
|
||||
@Param('profileId') profileId: string,
|
||||
@Param('ruleId') ruleId: string,
|
||||
) {
|
||||
return this.appsService.deleteRuntimeIngressRule(profileId, ruleId);
|
||||
}
|
||||
|
||||
@Delete('runtime-profiles/:profileId/variables/:variableId')
|
||||
deleteRuntimeVariable(
|
||||
@Param('profileId') profileId: string,
|
||||
@Param('variableId') variableId: string,
|
||||
) {
|
||||
return this.appsService.deleteRuntimeVariable(profileId, variableId);
|
||||
}
|
||||
|
||||
@Patch('runtime-profiles/:profileId/apply')
|
||||
applyRuntimeProfile(@Param('profileId') profileId: string) {
|
||||
return this.appsService.applyRuntimeProfile(profileId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { LauncherModule } from '../launcher/launcher.module';
|
||||
import { PrismaModule } from '../prisma/prisma.module';
|
||||
import { AppsController } from './apps.controller';
|
||||
import { AppsService } from './apps.service';
|
||||
import { BuildRunnerService } from './build-runner.service';
|
||||
|
||||
@Module({
|
||||
imports: [PrismaModule, LauncherModule],
|
||||
controllers: [AppsController],
|
||||
providers: [AppsService, BuildRunnerService],
|
||||
})
|
||||
export class AppsModule {}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,782 @@
|
||||
import { spawn, type ChildProcess } from 'node:child_process';
|
||||
import { createHash } from 'node:crypto';
|
||||
import { createReadStream } from 'node:fs';
|
||||
import { promises as fs } from 'node:fs';
|
||||
import * as os from 'node:os';
|
||||
import * as path from 'node:path';
|
||||
|
||||
import {
|
||||
Injectable,
|
||||
Logger,
|
||||
OnModuleDestroy,
|
||||
OnModuleInit,
|
||||
} from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { App, AppComponent, BuildJob } from '@prisma/client';
|
||||
import { PutObjectCommand, S3Client } from '@aws-sdk/client-s3';
|
||||
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
|
||||
type BuildJobWithComponent = BuildJob & {
|
||||
component: AppComponent & {
|
||||
app: App;
|
||||
};
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class BuildRunnerService implements OnModuleInit, OnModuleDestroy {
|
||||
private readonly logger = new Logger(BuildRunnerService.name);
|
||||
private timer?: NodeJS.Timeout;
|
||||
private running = false;
|
||||
private giteaUsername?: string;
|
||||
private r2Client?: S3Client;
|
||||
private readonly activeProcesses = new Map<string, ChildProcess>();
|
||||
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly configService: ConfigService,
|
||||
) {}
|
||||
|
||||
onModuleInit() {
|
||||
if (this.configService.get<string>('BUILD_RUNNER_ENABLED', 'true') === 'false') {
|
||||
this.logger.log('Build runner disabled by BUILD_RUNNER_ENABLED=false');
|
||||
return;
|
||||
}
|
||||
|
||||
const intervalMs = Number(
|
||||
this.configService.get<string>('BUILD_RUNNER_INTERVAL_MS', '5000'),
|
||||
);
|
||||
|
||||
this.timer = setInterval(() => void this.tick(), intervalMs);
|
||||
void this.tick();
|
||||
}
|
||||
|
||||
onModuleDestroy() {
|
||||
if (this.timer) {
|
||||
clearInterval(this.timer);
|
||||
}
|
||||
|
||||
for (const child of this.activeProcesses.values()) {
|
||||
terminateProcess(child);
|
||||
}
|
||||
}
|
||||
|
||||
cancelJob(jobId: string) {
|
||||
const child = this.activeProcesses.get(jobId);
|
||||
if (child) {
|
||||
terminateProcess(child);
|
||||
}
|
||||
}
|
||||
|
||||
private async tick() {
|
||||
if (this.running) return;
|
||||
|
||||
this.running = true;
|
||||
try {
|
||||
const job = await this.claimNextJob();
|
||||
if (job) {
|
||||
await this.runJob(job);
|
||||
}
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Unknown build runner error';
|
||||
this.logger.error(message);
|
||||
} finally {
|
||||
this.running = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async claimNextJob() {
|
||||
const pending = await this.prisma.buildJob.findFirst({
|
||||
where: { status: 'PENDING' },
|
||||
orderBy: { createdAt: 'asc' },
|
||||
include: {
|
||||
component: {
|
||||
include: {
|
||||
app: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!pending) return null;
|
||||
|
||||
const claimed = await this.prisma.buildJob.updateMany({
|
||||
where: { id: pending.id, status: 'PENDING' },
|
||||
data: {
|
||||
status: 'RUNNING',
|
||||
startedAt: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
if (claimed.count === 0) return null;
|
||||
|
||||
return this.prisma.buildJob.findUnique({
|
||||
where: { id: pending.id },
|
||||
include: {
|
||||
component: {
|
||||
include: {
|
||||
app: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}) as Promise<BuildJobWithComponent | null>;
|
||||
}
|
||||
|
||||
private async runJob(job: BuildJobWithComponent) {
|
||||
const lines = splitLogs(job.logs);
|
||||
const token = this.configService.get<string>('GITEA_ADMIN_TOKEN') || '';
|
||||
const append = async (line: string) => {
|
||||
lines.push(redact(line, token));
|
||||
await this.prisma.buildJob.update({
|
||||
where: { id: job.id },
|
||||
data: { logs: truncateLogs(lines.join('\n')) },
|
||||
});
|
||||
};
|
||||
|
||||
const workRoot = this.configService.get<string>(
|
||||
'BUILD_WORKDIR',
|
||||
path.join(os.tmpdir(), 'studio-admin-builds'),
|
||||
);
|
||||
const workDir = path.join(workRoot, job.id);
|
||||
|
||||
try {
|
||||
await fs.rm(workDir, { recursive: true, force: true });
|
||||
await fs.mkdir(workRoot, { recursive: true });
|
||||
|
||||
await append(`[running] build runner picked job ${job.id}`);
|
||||
await this.cloneRepository(job, workDir, append);
|
||||
await this.executeByComponentType(job, workDir, append);
|
||||
|
||||
await append('[done] build job completed');
|
||||
if (await this.isCanceled(job.id)) {
|
||||
await append('[cancelled] build job was cancelled');
|
||||
return;
|
||||
}
|
||||
await this.prisma.buildJob.update({
|
||||
where: { id: job.id },
|
||||
data: {
|
||||
status: 'SUCCEEDED',
|
||||
finishedAt: new Date(),
|
||||
logs: truncateLogs(lines.join('\n')),
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
if (await this.isCanceled(job.id)) {
|
||||
lines.push('[cancelled] build job stopped');
|
||||
await this.prisma.buildJob.update({
|
||||
where: { id: job.id },
|
||||
data: {
|
||||
status: 'CANCELED',
|
||||
finishedAt: new Date(),
|
||||
logs: truncateLogs(lines.join('\n')),
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const message = error instanceof Error ? error.message : 'Unknown build failure';
|
||||
lines.push(`[failed] ${redact(message, token)}`);
|
||||
await this.prisma.buildJob.update({
|
||||
where: { id: job.id },
|
||||
data: {
|
||||
status: 'FAILED',
|
||||
finishedAt: new Date(),
|
||||
logs: truncateLogs(lines.join('\n')),
|
||||
},
|
||||
});
|
||||
} finally {
|
||||
if (this.configService.get<string>('BUILD_KEEP_WORKDIR', 'false') !== 'true') {
|
||||
await fs.rm(workDir, { recursive: true, force: true }).catch(() => undefined);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async isCanceled(jobId: string) {
|
||||
const job = await this.prisma.buildJob.findUnique({
|
||||
where: { id: jobId },
|
||||
select: { status: true },
|
||||
});
|
||||
|
||||
return job?.status === 'CANCELED';
|
||||
}
|
||||
|
||||
private async cloneRepository(
|
||||
job: BuildJobWithComponent,
|
||||
workDir: string,
|
||||
append: (line: string) => Promise<void>,
|
||||
) {
|
||||
const safeUrl = this.getHttpCloneUrl(job.component.repository!);
|
||||
const cloneUrl = await this.getAuthenticatedHttpCloneUrl(
|
||||
job.component.repository!,
|
||||
);
|
||||
|
||||
await append(`[clone] ${safeUrl}`);
|
||||
await runCommand({
|
||||
jobId: job.id,
|
||||
command: this.configService.get<string>('GIT_BIN', 'git'),
|
||||
args: ['clone', '--branch', job.branch, cloneUrl, workDir],
|
||||
safeArgs: ['clone', '--branch', job.branch, safeUrl, workDir],
|
||||
cwd: process.cwd(),
|
||||
append,
|
||||
onProcess: (child) => this.activeProcesses.set(job.id, child),
|
||||
onProcessEnd: () => this.activeProcesses.delete(job.id),
|
||||
});
|
||||
|
||||
if (job.commitSha) {
|
||||
await append(`[checkout] ${job.commitSha}`);
|
||||
await runCommand({
|
||||
jobId: job.id,
|
||||
command: this.configService.get<string>('GIT_BIN', 'git'),
|
||||
args: ['checkout', job.commitSha],
|
||||
cwd: workDir,
|
||||
append,
|
||||
onProcess: (child) => this.activeProcesses.set(job.id, child),
|
||||
onProcessEnd: () => this.activeProcesses.delete(job.id),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async executeByComponentType(
|
||||
job: BuildJobWithComponent,
|
||||
workDir: string,
|
||||
append: (line: string) => Promise<void>,
|
||||
) {
|
||||
if (job.image) {
|
||||
await this.buildAndPushImage(job, workDir, append);
|
||||
return;
|
||||
}
|
||||
|
||||
if (job.component.type === 'MIGRATION') {
|
||||
await append('[migration] repository checkout verified');
|
||||
await append('[migration] actual DB migration is handled by deployment step');
|
||||
return;
|
||||
}
|
||||
|
||||
if (job.component.type === 'ELECTRON') {
|
||||
await this.buildArtifact(job, workDir, append);
|
||||
return;
|
||||
}
|
||||
|
||||
await append(`[skip] no build action configured for ${job.component.type}`);
|
||||
}
|
||||
|
||||
private async buildAndPushImage(
|
||||
job: BuildJobWithComponent,
|
||||
workDir: string,
|
||||
append: (line: string) => Promise<void>,
|
||||
) {
|
||||
const image = this.resolveImage(job.image!);
|
||||
const builder = this.configService.get<string>('BUILD_IMAGE_BUILDER', 'docker');
|
||||
const dockerfile = this.configService.get<string>('BUILD_DOCKERFILE', 'Dockerfile');
|
||||
const contextPath = this.configService.get<string>('BUILD_CONTEXT_PATH', '.');
|
||||
const contextDir = path.resolve(workDir, contextPath);
|
||||
const pushEnabled = this.configService.get<string>('BUILD_PUSH_ENABLED', 'true') !== 'false';
|
||||
|
||||
if (image !== job.image) {
|
||||
await this.prisma.buildJob.update({
|
||||
where: { id: job.id },
|
||||
data: { image },
|
||||
});
|
||||
await append(`[image] rewritten to ${image}`);
|
||||
}
|
||||
|
||||
if (builder === 'nerdctl') {
|
||||
const namespace = this.configService.get<string>('BUILD_NERDCTL_NAMESPACE');
|
||||
const prefix = namespace ? ['--namespace', namespace] : [];
|
||||
|
||||
await runCommand({
|
||||
jobId: job.id,
|
||||
command: this.configService.get<string>('NERDCTL_BIN', 'nerdctl'),
|
||||
args: [...prefix, 'build', '-t', image, '-f', dockerfile, '.'],
|
||||
cwd: contextDir,
|
||||
append,
|
||||
onProcess: (child) => this.activeProcesses.set(job.id, child),
|
||||
onProcessEnd: () => this.activeProcesses.delete(job.id),
|
||||
});
|
||||
|
||||
if (pushEnabled) {
|
||||
await runCommand({
|
||||
jobId: job.id,
|
||||
command: this.configService.get<string>('NERDCTL_BIN', 'nerdctl'),
|
||||
args: [...prefix, 'push', image],
|
||||
cwd: contextDir,
|
||||
append,
|
||||
onProcess: (child) => this.activeProcesses.set(job.id, child),
|
||||
onProcessEnd: () => this.activeProcesses.delete(job.id),
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
await runCommand({
|
||||
jobId: job.id,
|
||||
command: this.configService.get<string>('DOCKER_BIN', 'docker'),
|
||||
args: ['build', '-t', image, '-f', dockerfile, '.'],
|
||||
cwd: contextDir,
|
||||
append,
|
||||
onProcess: (child) => this.activeProcesses.set(job.id, child),
|
||||
onProcessEnd: () => this.activeProcesses.delete(job.id),
|
||||
});
|
||||
|
||||
if (pushEnabled) {
|
||||
await runCommand({
|
||||
jobId: job.id,
|
||||
command: this.configService.get<string>('DOCKER_BIN', 'docker'),
|
||||
args: ['push', image],
|
||||
cwd: contextDir,
|
||||
append,
|
||||
onProcess: (child) => this.activeProcesses.set(job.id, child),
|
||||
onProcessEnd: () => this.activeProcesses.delete(job.id),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private resolveImage(image: string) {
|
||||
const registry = this.configService.get<string>('BUILD_REGISTRY');
|
||||
if (!registry) return image;
|
||||
|
||||
const imagePath = image.includes('/') ? image.slice(image.indexOf('/') + 1) : image;
|
||||
return `${registry.replace(/\/$/, '')}/${imagePath}`;
|
||||
}
|
||||
|
||||
private async buildArtifact(
|
||||
job: BuildJobWithComponent,
|
||||
workDir: string,
|
||||
append: (line: string) => Promise<void>,
|
||||
) {
|
||||
const installCommand = this.configService.get<string>(
|
||||
'BUILD_ARTIFACT_INSTALL_COMMAND',
|
||||
'npm ci',
|
||||
);
|
||||
const buildCommand = this.configService.get<string>(
|
||||
'BUILD_ELECTRON_COMMAND',
|
||||
'npm run build:linux',
|
||||
);
|
||||
const outputDir = this.configService.get<string>(
|
||||
'BUILD_ARTIFACT_OUTPUT_DIR',
|
||||
'dist',
|
||||
);
|
||||
const artifactDir = this.configService.get<string>(
|
||||
'BUILD_ARTIFACT_DIR',
|
||||
path.join(os.tmpdir(), 'studio-admin-artifacts'),
|
||||
);
|
||||
|
||||
await append(`[artifact] install ${installCommand}`);
|
||||
await runShellCommand({
|
||||
jobId: job.id,
|
||||
command: installCommand,
|
||||
cwd: workDir,
|
||||
append,
|
||||
onProcess: (child) => this.activeProcesses.set(job.id, child),
|
||||
onProcessEnd: () => this.activeProcesses.delete(job.id),
|
||||
});
|
||||
|
||||
await append(`[artifact] build ${buildCommand}`);
|
||||
await runShellCommand({
|
||||
jobId: job.id,
|
||||
command: buildCommand,
|
||||
cwd: workDir,
|
||||
append,
|
||||
onProcess: (child) => this.activeProcesses.set(job.id, child),
|
||||
onProcessEnd: () => this.activeProcesses.delete(job.id),
|
||||
});
|
||||
|
||||
const artifact = await findNewestArtifact(path.resolve(workDir, outputDir));
|
||||
if (!artifact) {
|
||||
throw new Error(`산출물을 찾을 수 없습니다: ${outputDir}`);
|
||||
}
|
||||
|
||||
await fs.mkdir(artifactDir, { recursive: true });
|
||||
const destinationName = job.artifact || path.basename(artifact);
|
||||
const destination = path.join(artifactDir, destinationName);
|
||||
await fs.copyFile(artifact, destination);
|
||||
|
||||
const uploadedArtifact = await this.uploadArtifactIfConfigured(
|
||||
job,
|
||||
destination,
|
||||
destinationName,
|
||||
append,
|
||||
);
|
||||
const publicBaseUrl = this.configService.get<string>('BUILD_ARTIFACT_BASE_URL');
|
||||
const artifactValue = uploadedArtifact
|
||||
?? (publicBaseUrl
|
||||
? `${publicBaseUrl.replace(/\/$/, '')}/${encodeURIComponent(destinationName)}`
|
||||
: destination);
|
||||
|
||||
await this.prisma.buildJob.update({
|
||||
where: { id: job.id },
|
||||
data: { artifact: artifactValue },
|
||||
});
|
||||
job.artifact = artifactValue;
|
||||
|
||||
await append(`[artifact] found ${path.relative(workDir, artifact)}`);
|
||||
await append(`[artifact] copied to ${artifactValue}`);
|
||||
}
|
||||
|
||||
private async uploadArtifactIfConfigured(
|
||||
job: BuildJobWithComponent,
|
||||
artifactPath: string,
|
||||
artifactName: string,
|
||||
append: (line: string) => Promise<void>,
|
||||
) {
|
||||
const bucket = this.configService.get<string>('R2_BUCKET');
|
||||
const publicBaseUrl = this.configService.get<string>('R2_PUBLIC_BASE_URL');
|
||||
const accessKeyId = this.configService.get<string>('R2_ACCESS_KEY_ID');
|
||||
const secretAccessKey = this.configService.get<string>('R2_SECRET_ACCESS_KEY');
|
||||
|
||||
if (!bucket || !publicBaseUrl || !accessKeyId || !secretAccessKey) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const key = this.createArtifactObjectKey(job, artifactName);
|
||||
const stat = await fs.stat(artifactPath);
|
||||
const sha256 = await sha256File(artifactPath);
|
||||
const client = this.getR2Client(accessKeyId, secretAccessKey);
|
||||
|
||||
await append(`[r2] upload ${key}`);
|
||||
await client.send(new PutObjectCommand({
|
||||
Bucket: bucket,
|
||||
Key: key,
|
||||
Body: createReadStream(artifactPath),
|
||||
ContentLength: stat.size,
|
||||
ContentType: contentTypeForFile(artifactName),
|
||||
ContentDisposition: contentDispositionForFile(artifactName),
|
||||
Metadata: {
|
||||
sha256,
|
||||
app: safeMetadataValue(job.component.app.key),
|
||||
component: safeMetadataValue(job.component.name),
|
||||
branch: safeMetadataValue(job.branch),
|
||||
commit: safeMetadataValue(job.commitSha ?? ''),
|
||||
},
|
||||
}));
|
||||
|
||||
await client.send(new PutObjectCommand({
|
||||
Bucket: bucket,
|
||||
Key: `${key}.sha256`,
|
||||
Body: `${sha256} ${artifactName}\n`,
|
||||
ContentType: 'text/plain; charset=utf-8',
|
||||
}));
|
||||
|
||||
const url = `${publicBaseUrl.replace(/\/$/, '')}/${encodeObjectKey(key)}`;
|
||||
await append(`[r2] uploaded ${url}`);
|
||||
await append(`[r2] sha256 ${sha256}`);
|
||||
|
||||
return url;
|
||||
}
|
||||
|
||||
private getR2Client(accessKeyId: string, secretAccessKey: string) {
|
||||
if (this.r2Client) return this.r2Client;
|
||||
|
||||
const endpoint = this.configService.get<string>('R2_ENDPOINT')
|
||||
|| `https://${this.configService.get<string>('R2_ACCOUNT_ID')}.r2.cloudflarestorage.com`;
|
||||
|
||||
this.r2Client = new S3Client({
|
||||
region: this.configService.get<string>('R2_REGION', 'auto'),
|
||||
endpoint,
|
||||
credentials: {
|
||||
accessKeyId,
|
||||
secretAccessKey,
|
||||
},
|
||||
forcePathStyle: this.configService.get<string>('R2_FORCE_PATH_STYLE', 'true') !== 'false',
|
||||
});
|
||||
|
||||
return this.r2Client;
|
||||
}
|
||||
|
||||
private createArtifactObjectKey(job: BuildJobWithComponent, artifactName: string) {
|
||||
const prefix = this.configService.get<string>('R2_ARTIFACT_PREFIX', 'studio-artifacts');
|
||||
const revision = job.commitSha?.trim().slice(0, 7) || job.id;
|
||||
return [
|
||||
prefix,
|
||||
'apps',
|
||||
job.component.app.key,
|
||||
job.component.name,
|
||||
job.branch,
|
||||
revision,
|
||||
artifactName,
|
||||
].map(sanitizeObjectKeyPart).join('/');
|
||||
}
|
||||
|
||||
private getHttpCloneUrl(repository: string) {
|
||||
return `${this.configService
|
||||
.get<string>('GITEA_BASE_URL', 'http://localhost:3000')
|
||||
.replace(/\/$/, '')}/${repository}.git`;
|
||||
}
|
||||
|
||||
private async getAuthenticatedHttpCloneUrl(repository: string) {
|
||||
const baseUrl = this.getHttpCloneUrl(repository);
|
||||
const token = this.configService.get<string>('GITEA_ADMIN_TOKEN');
|
||||
if (!token || this.configService.get<string>('BUILD_GIT_AUTH', 'token') === 'none') {
|
||||
return baseUrl;
|
||||
}
|
||||
|
||||
const username =
|
||||
this.configService.get<string>('BUILD_GIT_USERNAME') ??
|
||||
(await this.getGiteaUsername(token));
|
||||
const url = new URL(baseUrl);
|
||||
url.username = username;
|
||||
url.password = token;
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
private async getGiteaUsername(token: string) {
|
||||
if (this.giteaUsername) return this.giteaUsername;
|
||||
|
||||
const response = await fetch(
|
||||
`${this.configService
|
||||
.get<string>('GITEA_BASE_URL', 'http://localhost:3000')
|
||||
.replace(/\/$/, '')}/api/v1/user`,
|
||||
{
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
Authorization: `token ${token}`,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Gitea 사용자 확인 실패: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = (await response.json()) as { login?: string };
|
||||
if (!data.login) {
|
||||
throw new Error('Gitea 토큰 사용자명을 확인할 수 없습니다.');
|
||||
}
|
||||
|
||||
this.giteaUsername = data.login;
|
||||
return data.login;
|
||||
}
|
||||
}
|
||||
|
||||
async function runCommand({
|
||||
jobId,
|
||||
command,
|
||||
args,
|
||||
safeArgs = args,
|
||||
cwd,
|
||||
append,
|
||||
onProcess,
|
||||
onProcessEnd,
|
||||
}: {
|
||||
jobId: string;
|
||||
command: string;
|
||||
args: string[];
|
||||
safeArgs?: string[];
|
||||
cwd: string;
|
||||
append: (line: string) => Promise<void>;
|
||||
onProcess: (child: ChildProcess) => void;
|
||||
onProcessEnd: () => void;
|
||||
}) {
|
||||
await append(`[run] ${command} ${safeArgs.join(' ')}`);
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const child = spawn(command, args, {
|
||||
cwd,
|
||||
env: process.env,
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
detached: true,
|
||||
});
|
||||
let output = '';
|
||||
onProcess(child);
|
||||
|
||||
child.stdout.on('data', (chunk: Buffer) => {
|
||||
output += chunk.toString();
|
||||
});
|
||||
|
||||
child.stderr.on('data', (chunk: Buffer) => {
|
||||
output += chunk.toString();
|
||||
});
|
||||
|
||||
child.on('error', (error) => {
|
||||
onProcessEnd();
|
||||
reject(error);
|
||||
});
|
||||
child.on('close', async (code) => {
|
||||
onProcessEnd();
|
||||
try {
|
||||
for (const line of output.split(/\r?\n/).filter(Boolean)) {
|
||||
await append(`[out] ${line}`);
|
||||
}
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (code === 0) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
reject(new Error(`${command} for job ${jobId} exited with code ${code}`));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function runShellCommand({
|
||||
jobId,
|
||||
command,
|
||||
cwd,
|
||||
append,
|
||||
onProcess,
|
||||
onProcessEnd,
|
||||
}: {
|
||||
jobId: string;
|
||||
command: string;
|
||||
cwd: string;
|
||||
append: (line: string) => Promise<void>;
|
||||
onProcess: (child: ChildProcess) => void;
|
||||
onProcessEnd: () => void;
|
||||
}) {
|
||||
await append(`[run] ${command}`);
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const child = spawn(command, {
|
||||
cwd,
|
||||
env: process.env,
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
detached: true,
|
||||
shell: true,
|
||||
});
|
||||
let output = '';
|
||||
onProcess(child);
|
||||
|
||||
child.stdout.on('data', (chunk: Buffer) => {
|
||||
output += chunk.toString();
|
||||
});
|
||||
|
||||
child.stderr.on('data', (chunk: Buffer) => {
|
||||
output += chunk.toString();
|
||||
});
|
||||
|
||||
child.on('error', (error) => {
|
||||
onProcessEnd();
|
||||
reject(error);
|
||||
});
|
||||
child.on('close', async (code) => {
|
||||
onProcessEnd();
|
||||
try {
|
||||
for (const line of output.split(/\r?\n/).filter(Boolean)) {
|
||||
await append(`[out] ${line}`);
|
||||
}
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (code === 0) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
reject(new Error(`${command} for job ${jobId} exited with code ${code}`));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function findNewestArtifact(dir: string) {
|
||||
const candidates = await findArtifactCandidates(dir).catch(() => []);
|
||||
candidates.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
||||
return candidates[0]?.path ?? null;
|
||||
}
|
||||
|
||||
async function findArtifactCandidates(dir: string): Promise<Array<{ path: string; mtimeMs: number }>> {
|
||||
const entries = await fs.readdir(dir, { withFileTypes: true });
|
||||
const results: Array<{ path: string; mtimeMs: number }> = [];
|
||||
|
||||
for (const entry of entries) {
|
||||
const entryPath = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
results.push(...await findArtifactCandidates(entryPath));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!/\.(AppImage|deb|rpm|zip|tar\.gz)$/i.test(entry.name)) continue;
|
||||
const stat = await fs.stat(entryPath);
|
||||
results.push({ path: entryPath, mtimeMs: stat.mtimeMs });
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
async function sha256File(filePath: string) {
|
||||
const hash = createHash('sha256');
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const stream = createReadStream(filePath);
|
||||
stream.on('data', (chunk) => hash.update(chunk));
|
||||
stream.on('error', reject);
|
||||
stream.on('end', resolve);
|
||||
});
|
||||
return hash.digest('hex');
|
||||
}
|
||||
|
||||
function contentTypeForFile(fileName: string) {
|
||||
const lowerName = fileName.toLowerCase();
|
||||
if (lowerName.endsWith('.tar.gz')) return 'application/gzip';
|
||||
|
||||
const extension = path.extname(lowerName);
|
||||
const types: Record<string, string> = {
|
||||
'.appimage': 'application/octet-stream',
|
||||
'.deb': 'application/vnd.debian.binary-package',
|
||||
'.rpm': 'application/x-rpm',
|
||||
'.zip': 'application/zip',
|
||||
'.json': 'application/json',
|
||||
'.txt': 'text/plain; charset=utf-8',
|
||||
'.yml': 'application/x-yaml',
|
||||
'.yaml': 'application/x-yaml',
|
||||
};
|
||||
|
||||
return types[extension] ?? 'application/octet-stream';
|
||||
}
|
||||
|
||||
function sanitizeObjectKeyPart(value: string) {
|
||||
return value
|
||||
.trim()
|
||||
.replace(/^\/+|\/+$/g, '')
|
||||
.replace(/[^a-zA-Z0-9._@+=-]+/g, '-')
|
||||
|| 'unknown';
|
||||
}
|
||||
|
||||
function encodeObjectKey(key: string) {
|
||||
return key.split('/').map(encodeURIComponent).join('/');
|
||||
}
|
||||
|
||||
function contentDispositionForFile(fileName: string) {
|
||||
const fallbackName = fileName.replace(/[\\\"\r\n]/g, '_');
|
||||
return `attachment; filename="${fallbackName}"; filename*=UTF-8''${encodeURIComponent(fileName)}`;
|
||||
}
|
||||
|
||||
function safeMetadataValue(value: string) {
|
||||
return value.replace(/[^\x20-\x7E]/g, '').slice(0, 512);
|
||||
}
|
||||
|
||||
function terminateProcess(child: ChildProcess) {
|
||||
if (!child.pid || child.killed) return;
|
||||
|
||||
try {
|
||||
process.kill(-child.pid, 'SIGTERM');
|
||||
setTimeout(() => {
|
||||
if (!child.killed && child.pid) {
|
||||
try {
|
||||
process.kill(-child.pid, 'SIGKILL');
|
||||
} catch {
|
||||
// Process may have already exited.
|
||||
}
|
||||
}
|
||||
}, 3000).unref();
|
||||
} catch {
|
||||
child.kill('SIGTERM');
|
||||
}
|
||||
}
|
||||
|
||||
function splitLogs(logs: string | null) {
|
||||
return logs ? logs.split('\n').filter(Boolean) : [];
|
||||
}
|
||||
|
||||
function truncateLogs(logs: string) {
|
||||
return logs.length > 200_000 ? logs.slice(-200_000) : logs;
|
||||
}
|
||||
|
||||
function redact(value: string, secret: string) {
|
||||
if (!secret) return value;
|
||||
return value.split(secret).join('<redacted>');
|
||||
}
|
||||
+222
@@ -0,0 +1,222 @@
|
||||
import { ArrayNotEmpty, IsArray, IsBoolean, IsEnum, IsInt, IsOptional, IsString, IsUUID, Min } from 'class-validator';
|
||||
import { AppStatus, ComponentType } from './app-enums';
|
||||
|
||||
export class CreateAppDto {
|
||||
@IsString()
|
||||
key!: string;
|
||||
|
||||
@IsString()
|
||||
name!: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
description?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
namespace?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsEnum(AppStatus)
|
||||
status?: AppStatus;
|
||||
}
|
||||
|
||||
export class CreateAppComponentDto {
|
||||
@IsString()
|
||||
name!: string;
|
||||
|
||||
@IsEnum(ComponentType)
|
||||
type!: ComponentType;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
imageSource?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
externalImage?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
workloadType?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@Min(0)
|
||||
replicas?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
serviceType?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
storageEnabled?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
storageSize?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
storageMountPath?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
storageRetain?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
repository?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
branch?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
buildTarget?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
port?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
targetPort?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
ports?: Array<{
|
||||
name?: string;
|
||||
port?: number;
|
||||
targetPort?: number;
|
||||
protocol?: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
export class CreateRuntimeIngressRuleDto {
|
||||
@IsUUID()
|
||||
componentId!: string;
|
||||
|
||||
@IsString()
|
||||
host!: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
path?: string;
|
||||
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
servicePort!: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
tlsEnabled?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
tlsSecretName?: string;
|
||||
}
|
||||
|
||||
export class CreateBuildJobDto {
|
||||
@IsUUID()
|
||||
componentId!: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
branch?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
commitSha?: string;
|
||||
}
|
||||
|
||||
export class CreateAppReleaseDto {
|
||||
@IsUUID()
|
||||
appId!: string;
|
||||
|
||||
@IsString()
|
||||
version!: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
branch?: string;
|
||||
|
||||
@IsArray()
|
||||
@ArrayNotEmpty()
|
||||
@IsUUID(undefined, { each: true })
|
||||
buildJobIds!: string[];
|
||||
}
|
||||
|
||||
export class CreateDeploymentJobDto {
|
||||
@IsUUID()
|
||||
releaseId!: string;
|
||||
|
||||
@IsString()
|
||||
environment!: string;
|
||||
|
||||
@IsString()
|
||||
namespace!: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
updateLauncherAppVersion?: boolean;
|
||||
}
|
||||
|
||||
export class CreateRuntimeEnvProfileDto {
|
||||
@IsUUID()
|
||||
appId!: string;
|
||||
|
||||
@IsString()
|
||||
name!: string;
|
||||
|
||||
@IsString()
|
||||
environment!: string;
|
||||
|
||||
@IsString()
|
||||
namespace!: string;
|
||||
}
|
||||
|
||||
export class CreateRuntimeEnvVariableDto {
|
||||
@IsOptional()
|
||||
@IsUUID()
|
||||
componentId?: string;
|
||||
|
||||
@IsString()
|
||||
key!: string;
|
||||
|
||||
@IsString()
|
||||
value!: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
isSecret?: boolean;
|
||||
}
|
||||
|
||||
export class EnsureRuntimeNamespaceDto {
|
||||
@IsString()
|
||||
namespace!: string;
|
||||
}
|
||||
|
||||
export class CreateMigrationJobDto {
|
||||
@IsUUID()
|
||||
releaseId!: string;
|
||||
|
||||
@IsUUID()
|
||||
migrationComponentId!: string;
|
||||
|
||||
@IsString()
|
||||
imageComponentName!: string;
|
||||
|
||||
@IsString()
|
||||
namespace!: string;
|
||||
|
||||
@IsString()
|
||||
command!: string;
|
||||
|
||||
@IsString()
|
||||
envSecretName!: string;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { AuthPrismaService } from './auth-prisma.service';
|
||||
|
||||
@Module({
|
||||
providers: [AuthPrismaService],
|
||||
exports: [AuthPrismaService],
|
||||
})
|
||||
export class AuthPrismaModule {}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { Injectable, OnModuleDestroy, OnModuleInit } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
|
||||
import { PrismaClient } from '@prisma/auth-client';
|
||||
|
||||
@Injectable()
|
||||
export class AuthPrismaService
|
||||
extends PrismaClient
|
||||
implements OnModuleInit, OnModuleDestroy
|
||||
{
|
||||
constructor(configService: ConfigService) {
|
||||
super({
|
||||
datasources: {
|
||||
db: {
|
||||
url: configService.getOrThrow<string>('AUTH_DATABASE_URL'),
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async onModuleInit() {
|
||||
await this.$connect();
|
||||
}
|
||||
|
||||
async onModuleDestroy() {
|
||||
await this.$disconnect();
|
||||
}
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
export * from "./index"
|
||||
@@ -0,0 +1,5 @@
|
||||
|
||||
/* !!! This is code generated by Prisma. Do not edit directly. !!!
|
||||
/* eslint-disable */
|
||||
// biome-ignore-all lint: generated file
|
||||
module.exports = { ...require('.') }
|
||||
+1
@@ -0,0 +1 @@
|
||||
export * from "./index"
|
||||
@@ -0,0 +1,5 @@
|
||||
|
||||
/* !!! This is code generated by Prisma. Do not edit directly. !!!
|
||||
/* eslint-disable */
|
||||
// biome-ignore-all lint: generated file
|
||||
module.exports = { ...require('#main-entry-point') }
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
export * from "./default"
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,217 @@
|
||||
|
||||
/* !!! This is code generated by Prisma. Do not edit directly. !!!
|
||||
/* eslint-disable */
|
||||
// biome-ignore-all lint: generated file
|
||||
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
|
||||
const {
|
||||
Decimal,
|
||||
objectEnumValues,
|
||||
makeStrictEnum,
|
||||
Public,
|
||||
getRuntime,
|
||||
skip
|
||||
} = require('./runtime/index-browser.js')
|
||||
|
||||
|
||||
const Prisma = {}
|
||||
|
||||
exports.Prisma = Prisma
|
||||
exports.$Enums = {}
|
||||
|
||||
/**
|
||||
* Prisma Client JS version: 6.19.3
|
||||
* Query Engine version: c2990dca591cba766e3b7ef5d9e8a84796e47ab7
|
||||
*/
|
||||
Prisma.prismaVersion = {
|
||||
client: "6.19.3",
|
||||
engine: "c2990dca591cba766e3b7ef5d9e8a84796e47ab7"
|
||||
}
|
||||
|
||||
Prisma.PrismaClientKnownRequestError = () => {
|
||||
const runtimeName = getRuntime().prettyName;
|
||||
throw new Error(`PrismaClientKnownRequestError is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
|
||||
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
|
||||
)};
|
||||
Prisma.PrismaClientUnknownRequestError = () => {
|
||||
const runtimeName = getRuntime().prettyName;
|
||||
throw new Error(`PrismaClientUnknownRequestError is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
|
||||
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
|
||||
)}
|
||||
Prisma.PrismaClientRustPanicError = () => {
|
||||
const runtimeName = getRuntime().prettyName;
|
||||
throw new Error(`PrismaClientRustPanicError is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
|
||||
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
|
||||
)}
|
||||
Prisma.PrismaClientInitializationError = () => {
|
||||
const runtimeName = getRuntime().prettyName;
|
||||
throw new Error(`PrismaClientInitializationError is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
|
||||
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
|
||||
)}
|
||||
Prisma.PrismaClientValidationError = () => {
|
||||
const runtimeName = getRuntime().prettyName;
|
||||
throw new Error(`PrismaClientValidationError is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
|
||||
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
|
||||
)}
|
||||
Prisma.Decimal = Decimal
|
||||
|
||||
/**
|
||||
* Re-export of sql-template-tag
|
||||
*/
|
||||
Prisma.sql = () => {
|
||||
const runtimeName = getRuntime().prettyName;
|
||||
throw new Error(`sqltag is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
|
||||
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
|
||||
)}
|
||||
Prisma.empty = () => {
|
||||
const runtimeName = getRuntime().prettyName;
|
||||
throw new Error(`empty is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
|
||||
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
|
||||
)}
|
||||
Prisma.join = () => {
|
||||
const runtimeName = getRuntime().prettyName;
|
||||
throw new Error(`join is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
|
||||
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
|
||||
)}
|
||||
Prisma.raw = () => {
|
||||
const runtimeName = getRuntime().prettyName;
|
||||
throw new Error(`raw is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
|
||||
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
|
||||
)}
|
||||
Prisma.validator = Public.validator
|
||||
|
||||
/**
|
||||
* Extensions
|
||||
*/
|
||||
Prisma.getExtensionContext = () => {
|
||||
const runtimeName = getRuntime().prettyName;
|
||||
throw new Error(`Extensions.getExtensionContext is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
|
||||
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
|
||||
)}
|
||||
Prisma.defineExtension = () => {
|
||||
const runtimeName = getRuntime().prettyName;
|
||||
throw new Error(`Extensions.defineExtension is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
|
||||
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
|
||||
)}
|
||||
|
||||
/**
|
||||
* Shorthand utilities for JSON filtering
|
||||
*/
|
||||
Prisma.DbNull = objectEnumValues.instances.DbNull
|
||||
Prisma.JsonNull = objectEnumValues.instances.JsonNull
|
||||
Prisma.AnyNull = objectEnumValues.instances.AnyNull
|
||||
|
||||
Prisma.NullTypes = {
|
||||
DbNull: objectEnumValues.classes.DbNull,
|
||||
JsonNull: objectEnumValues.classes.JsonNull,
|
||||
AnyNull: objectEnumValues.classes.AnyNull
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Enums
|
||||
*/
|
||||
|
||||
exports.Prisma.TransactionIsolationLevel = makeStrictEnum({
|
||||
ReadUncommitted: 'ReadUncommitted',
|
||||
ReadCommitted: 'ReadCommitted',
|
||||
RepeatableRead: 'RepeatableRead',
|
||||
Serializable: 'Serializable'
|
||||
});
|
||||
|
||||
exports.Prisma.UserScalarFieldEnum = {
|
||||
id: 'id',
|
||||
email: 'email',
|
||||
passwordHash: 'passwordHash',
|
||||
name: 'name',
|
||||
role: 'role',
|
||||
isActive: 'isActive',
|
||||
lastLoginAt: 'lastLoginAt',
|
||||
createdAt: 'createdAt',
|
||||
updatedAt: 'updatedAt'
|
||||
};
|
||||
|
||||
exports.Prisma.LicenseScalarFieldEnum = {
|
||||
id: 'id',
|
||||
code: 'code',
|
||||
name: 'name',
|
||||
description: 'description',
|
||||
defaultDurationDays: 'defaultDurationDays',
|
||||
isActive: 'isActive',
|
||||
createdAt: 'createdAt',
|
||||
updatedAt: 'updatedAt'
|
||||
};
|
||||
|
||||
exports.Prisma.UserLicenseScalarFieldEnum = {
|
||||
id: 'id',
|
||||
userId: 'userId',
|
||||
licenseId: 'licenseId',
|
||||
startsAt: 'startsAt',
|
||||
expiresAt: 'expiresAt',
|
||||
assignedById: 'assignedById',
|
||||
createdAt: 'createdAt'
|
||||
};
|
||||
|
||||
exports.Prisma.SortOrder = {
|
||||
asc: 'asc',
|
||||
desc: 'desc'
|
||||
};
|
||||
|
||||
exports.Prisma.QueryMode = {
|
||||
default: 'default',
|
||||
insensitive: 'insensitive'
|
||||
};
|
||||
|
||||
exports.Prisma.NullsOrder = {
|
||||
first: 'first',
|
||||
last: 'last'
|
||||
};
|
||||
exports.UserRole = exports.$Enums.UserRole = {
|
||||
ADMIN: 'ADMIN',
|
||||
USER: 'USER'
|
||||
};
|
||||
|
||||
exports.LicenseCode = exports.$Enums.LicenseCode = {
|
||||
FREE: 'FREE',
|
||||
EDUCATION: 'EDUCATION',
|
||||
PROFESSIONAL: 'PROFESSIONAL'
|
||||
};
|
||||
|
||||
exports.Prisma.ModelName = {
|
||||
User: 'User',
|
||||
License: 'License',
|
||||
UserLicense: 'UserLicense'
|
||||
};
|
||||
|
||||
/**
|
||||
* This is a stub Prisma Client that will error at runtime if called.
|
||||
*/
|
||||
class PrismaClient {
|
||||
constructor() {
|
||||
return new Proxy(this, {
|
||||
get(target, prop) {
|
||||
let message
|
||||
const runtime = getRuntime()
|
||||
if (runtime.isEdge) {
|
||||
message = `PrismaClient is not configured to run in ${runtime.prettyName}. In order to run Prisma Client on edge runtime, either:
|
||||
- Use Prisma Accelerate: https://pris.ly/d/accelerate
|
||||
- Use Driver Adapters: https://pris.ly/d/driver-adapters
|
||||
`;
|
||||
} else {
|
||||
message = 'PrismaClient is unable to run in this browser environment, or has been bundled for the browser (running in `' + runtime.prettyName + '`).'
|
||||
}
|
||||
|
||||
message += `
|
||||
If this is unexpected, please open an issue: https://pris.ly/prisma-prisma-bug-report`
|
||||
|
||||
throw new Error(message)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
exports.PrismaClient = PrismaClient
|
||||
|
||||
Object.assign(exports, Prisma)
|
||||
Vendored
+6218
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
Binary file not shown.
@@ -0,0 +1,183 @@
|
||||
{
|
||||
"name": "prisma-client-378612c311cf1c6fac598c21e6ea51068a0548bfbc7fdc08b65098bb7a2e5523",
|
||||
"main": "index.js",
|
||||
"types": "index.d.ts",
|
||||
"browser": "default.js",
|
||||
"exports": {
|
||||
"./client": {
|
||||
"require": {
|
||||
"node": "./index.js",
|
||||
"edge-light": "./wasm.js",
|
||||
"workerd": "./wasm.js",
|
||||
"worker": "./wasm.js",
|
||||
"browser": "./index-browser.js",
|
||||
"default": "./index.js"
|
||||
},
|
||||
"import": {
|
||||
"node": "./index.js",
|
||||
"edge-light": "./wasm.js",
|
||||
"workerd": "./wasm.js",
|
||||
"worker": "./wasm.js",
|
||||
"browser": "./index-browser.js",
|
||||
"default": "./index.js"
|
||||
},
|
||||
"default": "./index.js"
|
||||
},
|
||||
"./package.json": "./package.json",
|
||||
".": {
|
||||
"require": {
|
||||
"node": "./index.js",
|
||||
"edge-light": "./wasm.js",
|
||||
"workerd": "./wasm.js",
|
||||
"worker": "./wasm.js",
|
||||
"browser": "./index-browser.js",
|
||||
"default": "./index.js"
|
||||
},
|
||||
"import": {
|
||||
"node": "./index.js",
|
||||
"edge-light": "./wasm.js",
|
||||
"workerd": "./wasm.js",
|
||||
"worker": "./wasm.js",
|
||||
"browser": "./index-browser.js",
|
||||
"default": "./index.js"
|
||||
},
|
||||
"default": "./index.js"
|
||||
},
|
||||
"./edge": {
|
||||
"types": "./edge.d.ts",
|
||||
"require": "./edge.js",
|
||||
"import": "./edge.js",
|
||||
"default": "./edge.js"
|
||||
},
|
||||
"./react-native": {
|
||||
"types": "./react-native.d.ts",
|
||||
"require": "./react-native.js",
|
||||
"import": "./react-native.js",
|
||||
"default": "./react-native.js"
|
||||
},
|
||||
"./extension": {
|
||||
"types": "./extension.d.ts",
|
||||
"require": "./extension.js",
|
||||
"import": "./extension.js",
|
||||
"default": "./extension.js"
|
||||
},
|
||||
"./index-browser": {
|
||||
"types": "./index.d.ts",
|
||||
"require": "./index-browser.js",
|
||||
"import": "./index-browser.js",
|
||||
"default": "./index-browser.js"
|
||||
},
|
||||
"./index": {
|
||||
"types": "./index.d.ts",
|
||||
"require": "./index.js",
|
||||
"import": "./index.js",
|
||||
"default": "./index.js"
|
||||
},
|
||||
"./wasm": {
|
||||
"types": "./wasm.d.ts",
|
||||
"require": "./wasm.js",
|
||||
"import": "./wasm.mjs",
|
||||
"default": "./wasm.mjs"
|
||||
},
|
||||
"./runtime/client": {
|
||||
"types": "./runtime/client.d.ts",
|
||||
"node": {
|
||||
"require": "./runtime/client.js",
|
||||
"default": "./runtime/client.js"
|
||||
},
|
||||
"require": "./runtime/client.js",
|
||||
"import": "./runtime/client.mjs",
|
||||
"default": "./runtime/client.mjs"
|
||||
},
|
||||
"./runtime/library": {
|
||||
"types": "./runtime/library.d.ts",
|
||||
"require": "./runtime/library.js",
|
||||
"import": "./runtime/library.mjs",
|
||||
"default": "./runtime/library.mjs"
|
||||
},
|
||||
"./runtime/binary": {
|
||||
"types": "./runtime/binary.d.ts",
|
||||
"require": "./runtime/binary.js",
|
||||
"import": "./runtime/binary.mjs",
|
||||
"default": "./runtime/binary.mjs"
|
||||
},
|
||||
"./runtime/wasm-engine-edge": {
|
||||
"types": "./runtime/wasm-engine-edge.d.ts",
|
||||
"require": "./runtime/wasm-engine-edge.js",
|
||||
"import": "./runtime/wasm-engine-edge.mjs",
|
||||
"default": "./runtime/wasm-engine-edge.mjs"
|
||||
},
|
||||
"./runtime/wasm-compiler-edge": {
|
||||
"types": "./runtime/wasm-compiler-edge.d.ts",
|
||||
"require": "./runtime/wasm-compiler-edge.js",
|
||||
"import": "./runtime/wasm-compiler-edge.mjs",
|
||||
"default": "./runtime/wasm-compiler-edge.mjs"
|
||||
},
|
||||
"./runtime/edge": {
|
||||
"types": "./runtime/edge.d.ts",
|
||||
"require": "./runtime/edge.js",
|
||||
"import": "./runtime/edge-esm.js",
|
||||
"default": "./runtime/edge-esm.js"
|
||||
},
|
||||
"./runtime/react-native": {
|
||||
"types": "./runtime/react-native.d.ts",
|
||||
"require": "./runtime/react-native.js",
|
||||
"import": "./runtime/react-native.js",
|
||||
"default": "./runtime/react-native.js"
|
||||
},
|
||||
"./runtime/index-browser": {
|
||||
"types": "./runtime/index-browser.d.ts",
|
||||
"require": "./runtime/index-browser.js",
|
||||
"import": "./runtime/index-browser.mjs",
|
||||
"default": "./runtime/index-browser.mjs"
|
||||
},
|
||||
"./generator-build": {
|
||||
"require": "./generator-build/index.js",
|
||||
"import": "./generator-build/index.js",
|
||||
"default": "./generator-build/index.js"
|
||||
},
|
||||
"./sql": {
|
||||
"require": {
|
||||
"types": "./sql.d.ts",
|
||||
"node": "./sql.js",
|
||||
"default": "./sql.js"
|
||||
},
|
||||
"import": {
|
||||
"types": "./sql.d.ts",
|
||||
"node": "./sql.mjs",
|
||||
"default": "./sql.mjs"
|
||||
},
|
||||
"default": "./sql.js"
|
||||
},
|
||||
"./*": "./*"
|
||||
},
|
||||
"version": "6.19.3",
|
||||
"sideEffects": false,
|
||||
"imports": {
|
||||
"#wasm-engine-loader": {
|
||||
"edge-light": "./wasm-edge-light-loader.mjs",
|
||||
"workerd": "./wasm-worker-loader.mjs",
|
||||
"worker": "./wasm-worker-loader.mjs",
|
||||
"default": "./wasm-worker-loader.mjs"
|
||||
},
|
||||
"#main-entry-point": {
|
||||
"require": {
|
||||
"node": "./index.js",
|
||||
"edge-light": "./wasm.js",
|
||||
"workerd": "./wasm.js",
|
||||
"worker": "./wasm.js",
|
||||
"browser": "./index-browser.js",
|
||||
"default": "./index.js"
|
||||
},
|
||||
"import": {
|
||||
"node": "./index.js",
|
||||
"edge-light": "./wasm.js",
|
||||
"workerd": "./wasm.js",
|
||||
"worker": "./wasm.js",
|
||||
"browser": "./index-browser.js",
|
||||
"default": "./index.js"
|
||||
},
|
||||
"default": "./index.js"
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,370 @@
|
||||
declare class AnyNull extends NullTypesEnumValue {
|
||||
#private;
|
||||
}
|
||||
|
||||
declare type Args<T, F extends Operation> = T extends {
|
||||
[K: symbol]: {
|
||||
types: {
|
||||
operations: {
|
||||
[K in F]: {
|
||||
args: any;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
} ? T[symbol]['types']['operations'][F]['args'] : any;
|
||||
|
||||
declare class DbNull extends NullTypesEnumValue {
|
||||
#private;
|
||||
}
|
||||
|
||||
export declare function Decimal(n: Decimal.Value): Decimal;
|
||||
|
||||
export declare namespace Decimal {
|
||||
export type Constructor = typeof Decimal;
|
||||
export type Instance = Decimal;
|
||||
export type Rounding = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8;
|
||||
export type Modulo = Rounding | 9;
|
||||
export type Value = string | number | Decimal;
|
||||
|
||||
// http://mikemcl.github.io/decimal.js/#constructor-properties
|
||||
export interface Config {
|
||||
precision?: number;
|
||||
rounding?: Rounding;
|
||||
toExpNeg?: number;
|
||||
toExpPos?: number;
|
||||
minE?: number;
|
||||
maxE?: number;
|
||||
crypto?: boolean;
|
||||
modulo?: Modulo;
|
||||
defaults?: boolean;
|
||||
}
|
||||
}
|
||||
|
||||
export declare class Decimal {
|
||||
readonly d: number[];
|
||||
readonly e: number;
|
||||
readonly s: number;
|
||||
|
||||
constructor(n: Decimal.Value);
|
||||
|
||||
absoluteValue(): Decimal;
|
||||
abs(): Decimal;
|
||||
|
||||
ceil(): Decimal;
|
||||
|
||||
clampedTo(min: Decimal.Value, max: Decimal.Value): Decimal;
|
||||
clamp(min: Decimal.Value, max: Decimal.Value): Decimal;
|
||||
|
||||
comparedTo(n: Decimal.Value): number;
|
||||
cmp(n: Decimal.Value): number;
|
||||
|
||||
cosine(): Decimal;
|
||||
cos(): Decimal;
|
||||
|
||||
cubeRoot(): Decimal;
|
||||
cbrt(): Decimal;
|
||||
|
||||
decimalPlaces(): number;
|
||||
dp(): number;
|
||||
|
||||
dividedBy(n: Decimal.Value): Decimal;
|
||||
div(n: Decimal.Value): Decimal;
|
||||
|
||||
dividedToIntegerBy(n: Decimal.Value): Decimal;
|
||||
divToInt(n: Decimal.Value): Decimal;
|
||||
|
||||
equals(n: Decimal.Value): boolean;
|
||||
eq(n: Decimal.Value): boolean;
|
||||
|
||||
floor(): Decimal;
|
||||
|
||||
greaterThan(n: Decimal.Value): boolean;
|
||||
gt(n: Decimal.Value): boolean;
|
||||
|
||||
greaterThanOrEqualTo(n: Decimal.Value): boolean;
|
||||
gte(n: Decimal.Value): boolean;
|
||||
|
||||
hyperbolicCosine(): Decimal;
|
||||
cosh(): Decimal;
|
||||
|
||||
hyperbolicSine(): Decimal;
|
||||
sinh(): Decimal;
|
||||
|
||||
hyperbolicTangent(): Decimal;
|
||||
tanh(): Decimal;
|
||||
|
||||
inverseCosine(): Decimal;
|
||||
acos(): Decimal;
|
||||
|
||||
inverseHyperbolicCosine(): Decimal;
|
||||
acosh(): Decimal;
|
||||
|
||||
inverseHyperbolicSine(): Decimal;
|
||||
asinh(): Decimal;
|
||||
|
||||
inverseHyperbolicTangent(): Decimal;
|
||||
atanh(): Decimal;
|
||||
|
||||
inverseSine(): Decimal;
|
||||
asin(): Decimal;
|
||||
|
||||
inverseTangent(): Decimal;
|
||||
atan(): Decimal;
|
||||
|
||||
isFinite(): boolean;
|
||||
|
||||
isInteger(): boolean;
|
||||
isInt(): boolean;
|
||||
|
||||
isNaN(): boolean;
|
||||
|
||||
isNegative(): boolean;
|
||||
isNeg(): boolean;
|
||||
|
||||
isPositive(): boolean;
|
||||
isPos(): boolean;
|
||||
|
||||
isZero(): boolean;
|
||||
|
||||
lessThan(n: Decimal.Value): boolean;
|
||||
lt(n: Decimal.Value): boolean;
|
||||
|
||||
lessThanOrEqualTo(n: Decimal.Value): boolean;
|
||||
lte(n: Decimal.Value): boolean;
|
||||
|
||||
logarithm(n?: Decimal.Value): Decimal;
|
||||
log(n?: Decimal.Value): Decimal;
|
||||
|
||||
minus(n: Decimal.Value): Decimal;
|
||||
sub(n: Decimal.Value): Decimal;
|
||||
|
||||
modulo(n: Decimal.Value): Decimal;
|
||||
mod(n: Decimal.Value): Decimal;
|
||||
|
||||
naturalExponential(): Decimal;
|
||||
exp(): Decimal;
|
||||
|
||||
naturalLogarithm(): Decimal;
|
||||
ln(): Decimal;
|
||||
|
||||
negated(): Decimal;
|
||||
neg(): Decimal;
|
||||
|
||||
plus(n: Decimal.Value): Decimal;
|
||||
add(n: Decimal.Value): Decimal;
|
||||
|
||||
precision(includeZeros?: boolean): number;
|
||||
sd(includeZeros?: boolean): number;
|
||||
|
||||
round(): Decimal;
|
||||
|
||||
sine() : Decimal;
|
||||
sin() : Decimal;
|
||||
|
||||
squareRoot(): Decimal;
|
||||
sqrt(): Decimal;
|
||||
|
||||
tangent() : Decimal;
|
||||
tan() : Decimal;
|
||||
|
||||
times(n: Decimal.Value): Decimal;
|
||||
mul(n: Decimal.Value) : Decimal;
|
||||
|
||||
toBinary(significantDigits?: number): string;
|
||||
toBinary(significantDigits: number, rounding: Decimal.Rounding): string;
|
||||
|
||||
toDecimalPlaces(decimalPlaces?: number): Decimal;
|
||||
toDecimalPlaces(decimalPlaces: number, rounding: Decimal.Rounding): Decimal;
|
||||
toDP(decimalPlaces?: number): Decimal;
|
||||
toDP(decimalPlaces: number, rounding: Decimal.Rounding): Decimal;
|
||||
|
||||
toExponential(decimalPlaces?: number): string;
|
||||
toExponential(decimalPlaces: number, rounding: Decimal.Rounding): string;
|
||||
|
||||
toFixed(decimalPlaces?: number): string;
|
||||
toFixed(decimalPlaces: number, rounding: Decimal.Rounding): string;
|
||||
|
||||
toFraction(max_denominator?: Decimal.Value): Decimal[];
|
||||
|
||||
toHexadecimal(significantDigits?: number): string;
|
||||
toHexadecimal(significantDigits: number, rounding: Decimal.Rounding): string;
|
||||
toHex(significantDigits?: number): string;
|
||||
toHex(significantDigits: number, rounding?: Decimal.Rounding): string;
|
||||
|
||||
toJSON(): string;
|
||||
|
||||
toNearest(n: Decimal.Value, rounding?: Decimal.Rounding): Decimal;
|
||||
|
||||
toNumber(): number;
|
||||
|
||||
toOctal(significantDigits?: number): string;
|
||||
toOctal(significantDigits: number, rounding: Decimal.Rounding): string;
|
||||
|
||||
toPower(n: Decimal.Value): Decimal;
|
||||
pow(n: Decimal.Value): Decimal;
|
||||
|
||||
toPrecision(significantDigits?: number): string;
|
||||
toPrecision(significantDigits: number, rounding: Decimal.Rounding): string;
|
||||
|
||||
toSignificantDigits(significantDigits?: number): Decimal;
|
||||
toSignificantDigits(significantDigits: number, rounding: Decimal.Rounding): Decimal;
|
||||
toSD(significantDigits?: number): Decimal;
|
||||
toSD(significantDigits: number, rounding: Decimal.Rounding): Decimal;
|
||||
|
||||
toString(): string;
|
||||
|
||||
truncated(): Decimal;
|
||||
trunc(): Decimal;
|
||||
|
||||
valueOf(): string;
|
||||
|
||||
static abs(n: Decimal.Value): Decimal;
|
||||
static acos(n: Decimal.Value): Decimal;
|
||||
static acosh(n: Decimal.Value): Decimal;
|
||||
static add(x: Decimal.Value, y: Decimal.Value): Decimal;
|
||||
static asin(n: Decimal.Value): Decimal;
|
||||
static asinh(n: Decimal.Value): Decimal;
|
||||
static atan(n: Decimal.Value): Decimal;
|
||||
static atanh(n: Decimal.Value): Decimal;
|
||||
static atan2(y: Decimal.Value, x: Decimal.Value): Decimal;
|
||||
static cbrt(n: Decimal.Value): Decimal;
|
||||
static ceil(n: Decimal.Value): Decimal;
|
||||
static clamp(n: Decimal.Value, min: Decimal.Value, max: Decimal.Value): Decimal;
|
||||
static clone(object?: Decimal.Config): Decimal.Constructor;
|
||||
static config(object: Decimal.Config): Decimal.Constructor;
|
||||
static cos(n: Decimal.Value): Decimal;
|
||||
static cosh(n: Decimal.Value): Decimal;
|
||||
static div(x: Decimal.Value, y: Decimal.Value): Decimal;
|
||||
static exp(n: Decimal.Value): Decimal;
|
||||
static floor(n: Decimal.Value): Decimal;
|
||||
static hypot(...n: Decimal.Value[]): Decimal;
|
||||
static isDecimal(object: any): object is Decimal;
|
||||
static ln(n: Decimal.Value): Decimal;
|
||||
static log(n: Decimal.Value, base?: Decimal.Value): Decimal;
|
||||
static log2(n: Decimal.Value): Decimal;
|
||||
static log10(n: Decimal.Value): Decimal;
|
||||
static max(...n: Decimal.Value[]): Decimal;
|
||||
static min(...n: Decimal.Value[]): Decimal;
|
||||
static mod(x: Decimal.Value, y: Decimal.Value): Decimal;
|
||||
static mul(x: Decimal.Value, y: Decimal.Value): Decimal;
|
||||
static noConflict(): Decimal.Constructor; // Browser only
|
||||
static pow(base: Decimal.Value, exponent: Decimal.Value): Decimal;
|
||||
static random(significantDigits?: number): Decimal;
|
||||
static round(n: Decimal.Value): Decimal;
|
||||
static set(object: Decimal.Config): Decimal.Constructor;
|
||||
static sign(n: Decimal.Value): number;
|
||||
static sin(n: Decimal.Value): Decimal;
|
||||
static sinh(n: Decimal.Value): Decimal;
|
||||
static sqrt(n: Decimal.Value): Decimal;
|
||||
static sub(x: Decimal.Value, y: Decimal.Value): Decimal;
|
||||
static sum(...n: Decimal.Value[]): Decimal;
|
||||
static tan(n: Decimal.Value): Decimal;
|
||||
static tanh(n: Decimal.Value): Decimal;
|
||||
static trunc(n: Decimal.Value): Decimal;
|
||||
|
||||
static readonly default?: Decimal.Constructor;
|
||||
static readonly Decimal?: Decimal.Constructor;
|
||||
|
||||
static readonly precision: number;
|
||||
static readonly rounding: Decimal.Rounding;
|
||||
static readonly toExpNeg: number;
|
||||
static readonly toExpPos: number;
|
||||
static readonly minE: number;
|
||||
static readonly maxE: number;
|
||||
static readonly crypto: boolean;
|
||||
static readonly modulo: Decimal.Modulo;
|
||||
|
||||
static readonly ROUND_UP: 0;
|
||||
static readonly ROUND_DOWN: 1;
|
||||
static readonly ROUND_CEIL: 2;
|
||||
static readonly ROUND_FLOOR: 3;
|
||||
static readonly ROUND_HALF_UP: 4;
|
||||
static readonly ROUND_HALF_DOWN: 5;
|
||||
static readonly ROUND_HALF_EVEN: 6;
|
||||
static readonly ROUND_HALF_CEIL: 7;
|
||||
static readonly ROUND_HALF_FLOOR: 8;
|
||||
static readonly EUCLID: 9;
|
||||
}
|
||||
|
||||
declare type Exact<A, W> = (A extends unknown ? (W extends A ? {
|
||||
[K in keyof A]: Exact<A[K], W[K]>;
|
||||
} : W) : never) | (A extends Narrowable ? A : never);
|
||||
|
||||
export declare function getRuntime(): GetRuntimeOutput;
|
||||
|
||||
declare type GetRuntimeOutput = {
|
||||
id: RuntimeName;
|
||||
prettyName: string;
|
||||
isEdge: boolean;
|
||||
};
|
||||
|
||||
declare class JsonNull extends NullTypesEnumValue {
|
||||
#private;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates more strict variant of an enum which, unlike regular enum,
|
||||
* throws on non-existing property access. This can be useful in following situations:
|
||||
* - we have an API, that accepts both `undefined` and `SomeEnumType` as an input
|
||||
* - enum values are generated dynamically from DMMF.
|
||||
*
|
||||
* In that case, if using normal enums and no compile-time typechecking, using non-existing property
|
||||
* will result in `undefined` value being used, which will be accepted. Using strict enum
|
||||
* in this case will help to have a runtime exception, telling you that you are probably doing something wrong.
|
||||
*
|
||||
* Note: if you need to check for existence of a value in the enum you can still use either
|
||||
* `in` operator or `hasOwnProperty` function.
|
||||
*
|
||||
* @param definition
|
||||
* @returns
|
||||
*/
|
||||
export declare function makeStrictEnum<T extends Record<PropertyKey, string | number>>(definition: T): T;
|
||||
|
||||
declare type Narrowable = string | number | bigint | boolean | [];
|
||||
|
||||
declare class NullTypesEnumValue extends ObjectEnumValue {
|
||||
_getNamespace(): string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Base class for unique values of object-valued enums.
|
||||
*/
|
||||
declare abstract class ObjectEnumValue {
|
||||
constructor(arg?: symbol);
|
||||
abstract _getNamespace(): string;
|
||||
_getName(): string;
|
||||
toString(): string;
|
||||
}
|
||||
|
||||
export declare const objectEnumValues: {
|
||||
classes: {
|
||||
DbNull: typeof DbNull;
|
||||
JsonNull: typeof JsonNull;
|
||||
AnyNull: typeof AnyNull;
|
||||
};
|
||||
instances: {
|
||||
DbNull: DbNull;
|
||||
JsonNull: JsonNull;
|
||||
AnyNull: AnyNull;
|
||||
};
|
||||
};
|
||||
|
||||
declare type Operation = 'findFirst' | 'findFirstOrThrow' | 'findUnique' | 'findUniqueOrThrow' | 'findMany' | 'create' | 'createMany' | 'createManyAndReturn' | 'update' | 'updateMany' | 'updateManyAndReturn' | 'upsert' | 'delete' | 'deleteMany' | 'aggregate' | 'count' | 'groupBy' | '$queryRaw' | '$executeRaw' | '$queryRawUnsafe' | '$executeRawUnsafe' | 'findRaw' | 'aggregateRaw' | '$runCommandRaw';
|
||||
|
||||
declare namespace Public {
|
||||
export {
|
||||
validator
|
||||
}
|
||||
}
|
||||
export { Public }
|
||||
|
||||
declare type RuntimeName = 'workerd' | 'deno' | 'netlify' | 'node' | 'bun' | 'edge-light' | '';
|
||||
|
||||
declare function validator<V>(): <S>(select: Exact<S, V>) => S;
|
||||
|
||||
declare function validator<C, M extends Exclude<keyof C, `$${string}`>, O extends keyof C[M] & Operation>(client: C, model: M, operation: O): <S>(select: Exact<S, Args<C[M], O>>) => S;
|
||||
|
||||
declare function validator<C, M extends Exclude<keyof C, `$${string}`>, O extends keyof C[M] & Operation, P extends keyof Args<C[M], O>>(client: C, model: M, operation: O, prop: P): <S>(select: Exact<S, Args<C[M], O>[P]>) => S;
|
||||
|
||||
export { }
|
||||
File diff suppressed because one or more lines are too long
+3982
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,66 @@
|
||||
generator authClient {
|
||||
provider = "prisma-client-js"
|
||||
output = "../src/generated/auth-client"
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
url = env("AUTH_DATABASE_URL")
|
||||
}
|
||||
|
||||
enum UserRole {
|
||||
ADMIN
|
||||
USER
|
||||
}
|
||||
|
||||
enum LicenseCode {
|
||||
FREE
|
||||
EDUCATION
|
||||
PROFESSIONAL
|
||||
}
|
||||
|
||||
model User {
|
||||
id String @id @default(uuid()) @db.Uuid
|
||||
email String @unique
|
||||
passwordHash String @map("password_hash")
|
||||
name String?
|
||||
role UserRole @default(USER)
|
||||
isActive Boolean @default(true) @map("is_active")
|
||||
lastLoginAt DateTime? @map("last_login_at") @db.Timestamptz(3)
|
||||
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)
|
||||
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(3)
|
||||
userLicenses UserLicense[]
|
||||
|
||||
@@map("users")
|
||||
}
|
||||
|
||||
model License {
|
||||
id String @id @default(uuid()) @db.Uuid
|
||||
code LicenseCode @unique
|
||||
name String
|
||||
description String?
|
||||
defaultDurationDays Int? @default(365) @map("default_duration_days")
|
||||
isActive Boolean @default(true) @map("is_active")
|
||||
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)
|
||||
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(3)
|
||||
userLicenses UserLicense[]
|
||||
|
||||
@@index([isActive])
|
||||
@@map("licenses")
|
||||
}
|
||||
|
||||
model UserLicense {
|
||||
id String @id @default(uuid()) @db.Uuid
|
||||
userId String @map("user_id") @db.Uuid
|
||||
licenseId String @map("license_id") @db.Uuid
|
||||
startsAt DateTime @default(now()) @map("starts_at") @db.Timestamptz(3)
|
||||
expiresAt DateTime? @map("expires_at") @db.Timestamptz(3)
|
||||
assignedById String? @map("assigned_by") @db.Uuid
|
||||
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
license License @relation(fields: [licenseId], references: [id], onDelete: Restrict)
|
||||
|
||||
@@index([userId, startsAt])
|
||||
@@index([licenseId])
|
||||
@@map("user_licenses")
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
|
||||
/* !!! This is code generated by Prisma. Do not edit directly. !!!
|
||||
/* eslint-disable */
|
||||
// biome-ignore-all lint: generated file
|
||||
export default import('./query_engine_bg.wasm?module')
|
||||
@@ -0,0 +1,5 @@
|
||||
|
||||
/* !!! This is code generated by Prisma. Do not edit directly. !!!
|
||||
/* eslint-disable */
|
||||
// biome-ignore-all lint: generated file
|
||||
export default import('./query_engine_bg.wasm')
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
export * from "./default"
|
||||
@@ -0,0 +1,238 @@
|
||||
|
||||
/* !!! This is code generated by Prisma. Do not edit directly. !!!
|
||||
/* eslint-disable */
|
||||
// biome-ignore-all lint: generated file
|
||||
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
|
||||
const {
|
||||
PrismaClientKnownRequestError,
|
||||
PrismaClientUnknownRequestError,
|
||||
PrismaClientRustPanicError,
|
||||
PrismaClientInitializationError,
|
||||
PrismaClientValidationError,
|
||||
getPrismaClient,
|
||||
sqltag,
|
||||
empty,
|
||||
join,
|
||||
raw,
|
||||
skip,
|
||||
Decimal,
|
||||
Debug,
|
||||
objectEnumValues,
|
||||
makeStrictEnum,
|
||||
Extensions,
|
||||
warnOnce,
|
||||
defineDmmfProperty,
|
||||
Public,
|
||||
getRuntime,
|
||||
createParam,
|
||||
} = require('./runtime/wasm-engine-edge.js')
|
||||
|
||||
|
||||
const Prisma = {}
|
||||
|
||||
exports.Prisma = Prisma
|
||||
exports.$Enums = {}
|
||||
|
||||
/**
|
||||
* Prisma Client JS version: 6.19.3
|
||||
* Query Engine version: c2990dca591cba766e3b7ef5d9e8a84796e47ab7
|
||||
*/
|
||||
Prisma.prismaVersion = {
|
||||
client: "6.19.3",
|
||||
engine: "c2990dca591cba766e3b7ef5d9e8a84796e47ab7"
|
||||
}
|
||||
|
||||
Prisma.PrismaClientKnownRequestError = PrismaClientKnownRequestError;
|
||||
Prisma.PrismaClientUnknownRequestError = PrismaClientUnknownRequestError
|
||||
Prisma.PrismaClientRustPanicError = PrismaClientRustPanicError
|
||||
Prisma.PrismaClientInitializationError = PrismaClientInitializationError
|
||||
Prisma.PrismaClientValidationError = PrismaClientValidationError
|
||||
Prisma.Decimal = Decimal
|
||||
|
||||
/**
|
||||
* Re-export of sql-template-tag
|
||||
*/
|
||||
Prisma.sql = sqltag
|
||||
Prisma.empty = empty
|
||||
Prisma.join = join
|
||||
Prisma.raw = raw
|
||||
Prisma.validator = Public.validator
|
||||
|
||||
/**
|
||||
* Extensions
|
||||
*/
|
||||
Prisma.getExtensionContext = Extensions.getExtensionContext
|
||||
Prisma.defineExtension = Extensions.defineExtension
|
||||
|
||||
/**
|
||||
* Shorthand utilities for JSON filtering
|
||||
*/
|
||||
Prisma.DbNull = objectEnumValues.instances.DbNull
|
||||
Prisma.JsonNull = objectEnumValues.instances.JsonNull
|
||||
Prisma.AnyNull = objectEnumValues.instances.AnyNull
|
||||
|
||||
Prisma.NullTypes = {
|
||||
DbNull: objectEnumValues.classes.DbNull,
|
||||
JsonNull: objectEnumValues.classes.JsonNull,
|
||||
AnyNull: objectEnumValues.classes.AnyNull
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Enums
|
||||
*/
|
||||
exports.Prisma.TransactionIsolationLevel = makeStrictEnum({
|
||||
ReadUncommitted: 'ReadUncommitted',
|
||||
ReadCommitted: 'ReadCommitted',
|
||||
RepeatableRead: 'RepeatableRead',
|
||||
Serializable: 'Serializable'
|
||||
});
|
||||
|
||||
exports.Prisma.UserScalarFieldEnum = {
|
||||
id: 'id',
|
||||
email: 'email',
|
||||
passwordHash: 'passwordHash',
|
||||
name: 'name',
|
||||
role: 'role',
|
||||
isActive: 'isActive',
|
||||
lastLoginAt: 'lastLoginAt',
|
||||
createdAt: 'createdAt',
|
||||
updatedAt: 'updatedAt'
|
||||
};
|
||||
|
||||
exports.Prisma.LicenseScalarFieldEnum = {
|
||||
id: 'id',
|
||||
code: 'code',
|
||||
name: 'name',
|
||||
description: 'description',
|
||||
defaultDurationDays: 'defaultDurationDays',
|
||||
isActive: 'isActive',
|
||||
createdAt: 'createdAt',
|
||||
updatedAt: 'updatedAt'
|
||||
};
|
||||
|
||||
exports.Prisma.UserLicenseScalarFieldEnum = {
|
||||
id: 'id',
|
||||
userId: 'userId',
|
||||
licenseId: 'licenseId',
|
||||
startsAt: 'startsAt',
|
||||
expiresAt: 'expiresAt',
|
||||
assignedById: 'assignedById',
|
||||
createdAt: 'createdAt'
|
||||
};
|
||||
|
||||
exports.Prisma.SortOrder = {
|
||||
asc: 'asc',
|
||||
desc: 'desc'
|
||||
};
|
||||
|
||||
exports.Prisma.QueryMode = {
|
||||
default: 'default',
|
||||
insensitive: 'insensitive'
|
||||
};
|
||||
|
||||
exports.Prisma.NullsOrder = {
|
||||
first: 'first',
|
||||
last: 'last'
|
||||
};
|
||||
exports.UserRole = exports.$Enums.UserRole = {
|
||||
ADMIN: 'ADMIN',
|
||||
USER: 'USER'
|
||||
};
|
||||
|
||||
exports.LicenseCode = exports.$Enums.LicenseCode = {
|
||||
FREE: 'FREE',
|
||||
EDUCATION: 'EDUCATION',
|
||||
PROFESSIONAL: 'PROFESSIONAL'
|
||||
};
|
||||
|
||||
exports.Prisma.ModelName = {
|
||||
User: 'User',
|
||||
License: 'License',
|
||||
UserLicense: 'UserLicense'
|
||||
};
|
||||
/**
|
||||
* Create the Client
|
||||
*/
|
||||
const config = {
|
||||
"generator": {
|
||||
"name": "authClient",
|
||||
"provider": {
|
||||
"fromEnvVar": null,
|
||||
"value": "prisma-client-js"
|
||||
},
|
||||
"output": {
|
||||
"value": "/home/chyoo/tmp/atnmy-stdo-v2/atnmy-stdo-adm-v2-be/src/generated/auth-client",
|
||||
"fromEnvVar": null
|
||||
},
|
||||
"config": {
|
||||
"engineType": "library"
|
||||
},
|
||||
"binaryTargets": [
|
||||
{
|
||||
"fromEnvVar": null,
|
||||
"value": "debian-openssl-3.0.x",
|
||||
"native": true
|
||||
}
|
||||
],
|
||||
"previewFeatures": [],
|
||||
"sourceFilePath": "/home/chyoo/tmp/atnmy-stdo-v2/atnmy-stdo-adm-v2-be/prisma/auth.schema.prisma",
|
||||
"isCustomOutput": true
|
||||
},
|
||||
"relativeEnvPaths": {
|
||||
"rootEnvPath": null,
|
||||
"schemaEnvPath": "../../../.env"
|
||||
},
|
||||
"relativePath": "../../../prisma",
|
||||
"clientVersion": "6.19.3",
|
||||
"engineVersion": "c2990dca591cba766e3b7ef5d9e8a84796e47ab7",
|
||||
"datasourceNames": [
|
||||
"db"
|
||||
],
|
||||
"activeProvider": "postgresql",
|
||||
"postinstall": false,
|
||||
"inlineDatasources": {
|
||||
"db": {
|
||||
"url": {
|
||||
"fromEnvVar": "AUTH_DATABASE_URL",
|
||||
"value": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"inlineSchema": "generator authClient {\n provider = \"prisma-client-js\"\n output = \"../src/generated/auth-client\"\n}\n\ndatasource db {\n provider = \"postgresql\"\n url = env(\"AUTH_DATABASE_URL\")\n}\n\nenum UserRole {\n ADMIN\n USER\n}\n\nenum LicenseCode {\n FREE\n EDUCATION\n PROFESSIONAL\n}\n\nmodel User {\n id String @id @default(uuid()) @db.Uuid\n email String @unique\n passwordHash String @map(\"password_hash\")\n name String?\n role UserRole @default(USER)\n isActive Boolean @default(true) @map(\"is_active\")\n lastLoginAt DateTime? @map(\"last_login_at\") @db.Timestamptz(3)\n createdAt DateTime @default(now()) @map(\"created_at\") @db.Timestamptz(3)\n updatedAt DateTime @updatedAt @map(\"updated_at\") @db.Timestamptz(3)\n userLicenses UserLicense[]\n\n @@map(\"users\")\n}\n\nmodel License {\n id String @id @default(uuid()) @db.Uuid\n code LicenseCode @unique\n name String\n description String?\n defaultDurationDays Int? @default(365) @map(\"default_duration_days\")\n isActive Boolean @default(true) @map(\"is_active\")\n createdAt DateTime @default(now()) @map(\"created_at\") @db.Timestamptz(3)\n updatedAt DateTime @updatedAt @map(\"updated_at\") @db.Timestamptz(3)\n userLicenses UserLicense[]\n\n @@index([isActive])\n @@map(\"licenses\")\n}\n\nmodel UserLicense {\n id String @id @default(uuid()) @db.Uuid\n userId String @map(\"user_id\") @db.Uuid\n licenseId String @map(\"license_id\") @db.Uuid\n startsAt DateTime @default(now()) @map(\"starts_at\") @db.Timestamptz(3)\n expiresAt DateTime? @map(\"expires_at\") @db.Timestamptz(3)\n assignedById String? @map(\"assigned_by\") @db.Uuid\n createdAt DateTime @default(now()) @map(\"created_at\") @db.Timestamptz(3)\n user User @relation(fields: [userId], references: [id], onDelete: Cascade)\n license License @relation(fields: [licenseId], references: [id], onDelete: Restrict)\n\n @@index([userId, startsAt])\n @@index([licenseId])\n @@map(\"user_licenses\")\n}\n",
|
||||
"inlineSchemaHash": "b41ead22c04f404adbe239bd44c2751b9ed9adf3c5b7b7013be05252086f79d7",
|
||||
"copyEngine": true
|
||||
}
|
||||
config.dirname = '/'
|
||||
|
||||
config.runtimeDataModel = JSON.parse("{\"models\":{\"User\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"email\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"passwordHash\",\"kind\":\"scalar\",\"type\":\"String\",\"dbName\":\"password_hash\"},{\"name\":\"name\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"role\",\"kind\":\"enum\",\"type\":\"UserRole\"},{\"name\":\"isActive\",\"kind\":\"scalar\",\"type\":\"Boolean\",\"dbName\":\"is_active\"},{\"name\":\"lastLoginAt\",\"kind\":\"scalar\",\"type\":\"DateTime\",\"dbName\":\"last_login_at\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\",\"dbName\":\"created_at\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\",\"dbName\":\"updated_at\"},{\"name\":\"userLicenses\",\"kind\":\"object\",\"type\":\"UserLicense\",\"relationName\":\"UserToUserLicense\"}],\"dbName\":\"users\"},\"License\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"code\",\"kind\":\"enum\",\"type\":\"LicenseCode\"},{\"name\":\"name\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"description\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"defaultDurationDays\",\"kind\":\"scalar\",\"type\":\"Int\",\"dbName\":\"default_duration_days\"},{\"name\":\"isActive\",\"kind\":\"scalar\",\"type\":\"Boolean\",\"dbName\":\"is_active\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\",\"dbName\":\"created_at\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\",\"dbName\":\"updated_at\"},{\"name\":\"userLicenses\",\"kind\":\"object\",\"type\":\"UserLicense\",\"relationName\":\"LicenseToUserLicense\"}],\"dbName\":\"licenses\"},\"UserLicense\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"userId\",\"kind\":\"scalar\",\"type\":\"String\",\"dbName\":\"user_id\"},{\"name\":\"licenseId\",\"kind\":\"scalar\",\"type\":\"String\",\"dbName\":\"license_id\"},{\"name\":\"startsAt\",\"kind\":\"scalar\",\"type\":\"DateTime\",\"dbName\":\"starts_at\"},{\"name\":\"expiresAt\",\"kind\":\"scalar\",\"type\":\"DateTime\",\"dbName\":\"expires_at\"},{\"name\":\"assignedById\",\"kind\":\"scalar\",\"type\":\"String\",\"dbName\":\"assigned_by\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\",\"dbName\":\"created_at\"},{\"name\":\"user\",\"kind\":\"object\",\"type\":\"User\",\"relationName\":\"UserToUserLicense\"},{\"name\":\"license\",\"kind\":\"object\",\"type\":\"License\",\"relationName\":\"LicenseToUserLicense\"}],\"dbName\":\"user_licenses\"}},\"enums\":{},\"types\":{}}")
|
||||
defineDmmfProperty(exports.Prisma, config.runtimeDataModel)
|
||||
config.engineWasm = {
|
||||
getRuntime: async () => require('./query_engine_bg.js'),
|
||||
getQueryEngineWasmModule: async () => {
|
||||
const loader = (await import('#wasm-engine-loader')).default
|
||||
const engine = (await loader).default
|
||||
return engine
|
||||
}
|
||||
}
|
||||
config.compilerWasm = undefined
|
||||
|
||||
config.injectableEdgeEnv = () => ({
|
||||
parsed: {
|
||||
AUTH_DATABASE_URL: typeof globalThis !== 'undefined' && globalThis['AUTH_DATABASE_URL'] || typeof process !== 'undefined' && process.env && process.env.AUTH_DATABASE_URL || undefined
|
||||
}
|
||||
})
|
||||
|
||||
if (typeof globalThis !== 'undefined' && globalThis['DEBUG'] || typeof process !== 'undefined' && process.env && process.env.DEBUG || undefined) {
|
||||
Debug.enable(typeof globalThis !== 'undefined' && globalThis['DEBUG'] || typeof process !== 'undefined' && process.env && process.env.DEBUG || undefined)
|
||||
}
|
||||
|
||||
const PrismaClient = getPrismaClient(config)
|
||||
exports.PrismaClient = PrismaClient
|
||||
Object.assign(exports, Prisma)
|
||||
|
||||
+1
@@ -0,0 +1 @@
|
||||
export * from "./index"
|
||||
@@ -0,0 +1,5 @@
|
||||
|
||||
/* !!! This is code generated by Prisma. Do not edit directly. !!!
|
||||
/* eslint-disable */
|
||||
// biome-ignore-all lint: generated file
|
||||
module.exports = { ...require('.') }
|
||||
+1
@@ -0,0 +1 @@
|
||||
export * from "./index"
|
||||
@@ -0,0 +1,5 @@
|
||||
|
||||
/* !!! This is code generated by Prisma. Do not edit directly. !!!
|
||||
/* eslint-disable */
|
||||
// biome-ignore-all lint: generated file
|
||||
module.exports = { ...require('#main-entry-point') }
|
||||
+1
@@ -0,0 +1 @@
|
||||
export * from "./default"
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,233 @@
|
||||
|
||||
/* !!! This is code generated by Prisma. Do not edit directly. !!!
|
||||
/* eslint-disable */
|
||||
// biome-ignore-all lint: generated file
|
||||
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
|
||||
const {
|
||||
Decimal,
|
||||
objectEnumValues,
|
||||
makeStrictEnum,
|
||||
Public,
|
||||
getRuntime,
|
||||
skip
|
||||
} = require('./runtime/index-browser.js')
|
||||
|
||||
|
||||
const Prisma = {}
|
||||
|
||||
exports.Prisma = Prisma
|
||||
exports.$Enums = {}
|
||||
|
||||
/**
|
||||
* Prisma Client JS version: 6.19.3
|
||||
* Query Engine version: c2990dca591cba766e3b7ef5d9e8a84796e47ab7
|
||||
*/
|
||||
Prisma.prismaVersion = {
|
||||
client: "6.19.3",
|
||||
engine: "c2990dca591cba766e3b7ef5d9e8a84796e47ab7"
|
||||
}
|
||||
|
||||
Prisma.PrismaClientKnownRequestError = () => {
|
||||
const runtimeName = getRuntime().prettyName;
|
||||
throw new Error(`PrismaClientKnownRequestError is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
|
||||
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
|
||||
)};
|
||||
Prisma.PrismaClientUnknownRequestError = () => {
|
||||
const runtimeName = getRuntime().prettyName;
|
||||
throw new Error(`PrismaClientUnknownRequestError is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
|
||||
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
|
||||
)}
|
||||
Prisma.PrismaClientRustPanicError = () => {
|
||||
const runtimeName = getRuntime().prettyName;
|
||||
throw new Error(`PrismaClientRustPanicError is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
|
||||
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
|
||||
)}
|
||||
Prisma.PrismaClientInitializationError = () => {
|
||||
const runtimeName = getRuntime().prettyName;
|
||||
throw new Error(`PrismaClientInitializationError is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
|
||||
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
|
||||
)}
|
||||
Prisma.PrismaClientValidationError = () => {
|
||||
const runtimeName = getRuntime().prettyName;
|
||||
throw new Error(`PrismaClientValidationError is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
|
||||
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
|
||||
)}
|
||||
Prisma.Decimal = Decimal
|
||||
|
||||
/**
|
||||
* Re-export of sql-template-tag
|
||||
*/
|
||||
Prisma.sql = () => {
|
||||
const runtimeName = getRuntime().prettyName;
|
||||
throw new Error(`sqltag is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
|
||||
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
|
||||
)}
|
||||
Prisma.empty = () => {
|
||||
const runtimeName = getRuntime().prettyName;
|
||||
throw new Error(`empty is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
|
||||
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
|
||||
)}
|
||||
Prisma.join = () => {
|
||||
const runtimeName = getRuntime().prettyName;
|
||||
throw new Error(`join is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
|
||||
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
|
||||
)}
|
||||
Prisma.raw = () => {
|
||||
const runtimeName = getRuntime().prettyName;
|
||||
throw new Error(`raw is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
|
||||
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
|
||||
)}
|
||||
Prisma.validator = Public.validator
|
||||
|
||||
/**
|
||||
* Extensions
|
||||
*/
|
||||
Prisma.getExtensionContext = () => {
|
||||
const runtimeName = getRuntime().prettyName;
|
||||
throw new Error(`Extensions.getExtensionContext is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
|
||||
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
|
||||
)}
|
||||
Prisma.defineExtension = () => {
|
||||
const runtimeName = getRuntime().prettyName;
|
||||
throw new Error(`Extensions.defineExtension is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
|
||||
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
|
||||
)}
|
||||
|
||||
/**
|
||||
* Shorthand utilities for JSON filtering
|
||||
*/
|
||||
Prisma.DbNull = objectEnumValues.instances.DbNull
|
||||
Prisma.JsonNull = objectEnumValues.instances.JsonNull
|
||||
Prisma.AnyNull = objectEnumValues.instances.AnyNull
|
||||
|
||||
Prisma.NullTypes = {
|
||||
DbNull: objectEnumValues.classes.DbNull,
|
||||
JsonNull: objectEnumValues.classes.JsonNull,
|
||||
AnyNull: objectEnumValues.classes.AnyNull
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Enums
|
||||
*/
|
||||
|
||||
exports.Prisma.TransactionIsolationLevel = makeStrictEnum({
|
||||
ReadUncommitted: 'ReadUncommitted',
|
||||
ReadCommitted: 'ReadCommitted',
|
||||
RepeatableRead: 'RepeatableRead',
|
||||
Serializable: 'Serializable'
|
||||
});
|
||||
|
||||
exports.Prisma.LauncherNoticeScalarFieldEnum = {
|
||||
id: 'id',
|
||||
appKey: 'appKey',
|
||||
title: 'title',
|
||||
body: 'body',
|
||||
linkUrl: 'linkUrl',
|
||||
isActive: 'isActive',
|
||||
startsAt: 'startsAt',
|
||||
endsAt: 'endsAt',
|
||||
priority: 'priority',
|
||||
createdAt: 'createdAt',
|
||||
updatedAt: 'updatedAt'
|
||||
};
|
||||
|
||||
exports.Prisma.ProvisionedDeviceScalarFieldEnum = {
|
||||
id: 'id',
|
||||
accountEmail: 'accountEmail',
|
||||
authUserId: 'authUserId',
|
||||
hostname: 'hostname',
|
||||
tailscaleHostname: 'tailscaleHostname',
|
||||
sshUser: 'sshUser',
|
||||
workspaceRoot: 'workspaceRoot',
|
||||
usageType: 'usageType',
|
||||
os: 'os',
|
||||
arch: 'arch',
|
||||
kernel: 'kernel',
|
||||
distro: 'distro',
|
||||
device: 'device',
|
||||
installedAt: 'installedAt',
|
||||
lastProvisionedAt: 'lastProvisionedAt',
|
||||
createdAt: 'createdAt',
|
||||
updatedAt: 'updatedAt'
|
||||
};
|
||||
|
||||
exports.Prisma.LauncherSshKeyScalarFieldEnum = {
|
||||
id: 'id',
|
||||
accountEmail: 'accountEmail',
|
||||
authUserId: 'authUserId',
|
||||
publicKey: 'publicKey',
|
||||
publicKeyHash: 'publicKeyHash',
|
||||
keyType: 'keyType',
|
||||
label: 'label',
|
||||
lastProvisionedAt: 'lastProvisionedAt',
|
||||
createdAt: 'createdAt',
|
||||
updatedAt: 'updatedAt'
|
||||
};
|
||||
|
||||
exports.Prisma.SortOrder = {
|
||||
asc: 'asc',
|
||||
desc: 'desc'
|
||||
};
|
||||
|
||||
exports.Prisma.NullableJsonNullValueInput = {
|
||||
DbNull: Prisma.DbNull,
|
||||
JsonNull: Prisma.JsonNull
|
||||
};
|
||||
|
||||
exports.Prisma.QueryMode = {
|
||||
default: 'default',
|
||||
insensitive: 'insensitive'
|
||||
};
|
||||
|
||||
exports.Prisma.NullsOrder = {
|
||||
first: 'first',
|
||||
last: 'last'
|
||||
};
|
||||
|
||||
exports.Prisma.JsonNullValueFilter = {
|
||||
DbNull: Prisma.DbNull,
|
||||
JsonNull: Prisma.JsonNull,
|
||||
AnyNull: Prisma.AnyNull
|
||||
};
|
||||
|
||||
|
||||
exports.Prisma.ModelName = {
|
||||
LauncherNotice: 'LauncherNotice',
|
||||
ProvisionedDevice: 'ProvisionedDevice',
|
||||
LauncherSshKey: 'LauncherSshKey'
|
||||
};
|
||||
|
||||
/**
|
||||
* This is a stub Prisma Client that will error at runtime if called.
|
||||
*/
|
||||
class PrismaClient {
|
||||
constructor() {
|
||||
return new Proxy(this, {
|
||||
get(target, prop) {
|
||||
let message
|
||||
const runtime = getRuntime()
|
||||
if (runtime.isEdge) {
|
||||
message = `PrismaClient is not configured to run in ${runtime.prettyName}. In order to run Prisma Client on edge runtime, either:
|
||||
- Use Prisma Accelerate: https://pris.ly/d/accelerate
|
||||
- Use Driver Adapters: https://pris.ly/d/driver-adapters
|
||||
`;
|
||||
} else {
|
||||
message = 'PrismaClient is unable to run in this browser environment, or has been bundled for the browser (running in `' + runtime.prettyName + '`).'
|
||||
}
|
||||
|
||||
message += `
|
||||
If this is unexpected, please open an issue: https://pris.ly/prisma-prisma-bug-report`
|
||||
|
||||
throw new Error(message)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
exports.PrismaClient = PrismaClient
|
||||
|
||||
Object.assign(exports, Prisma)
|
||||
+5870
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
Binary file not shown.
@@ -0,0 +1,183 @@
|
||||
{
|
||||
"name": "prisma-client-2cc4fd701019d0254ce12699ae25121c6ed85eba27006e31abbdac7779ae92f0",
|
||||
"main": "index.js",
|
||||
"types": "index.d.ts",
|
||||
"browser": "default.js",
|
||||
"exports": {
|
||||
"./client": {
|
||||
"require": {
|
||||
"node": "./index.js",
|
||||
"edge-light": "./wasm.js",
|
||||
"workerd": "./wasm.js",
|
||||
"worker": "./wasm.js",
|
||||
"browser": "./index-browser.js",
|
||||
"default": "./index.js"
|
||||
},
|
||||
"import": {
|
||||
"node": "./index.js",
|
||||
"edge-light": "./wasm.js",
|
||||
"workerd": "./wasm.js",
|
||||
"worker": "./wasm.js",
|
||||
"browser": "./index-browser.js",
|
||||
"default": "./index.js"
|
||||
},
|
||||
"default": "./index.js"
|
||||
},
|
||||
"./package.json": "./package.json",
|
||||
".": {
|
||||
"require": {
|
||||
"node": "./index.js",
|
||||
"edge-light": "./wasm.js",
|
||||
"workerd": "./wasm.js",
|
||||
"worker": "./wasm.js",
|
||||
"browser": "./index-browser.js",
|
||||
"default": "./index.js"
|
||||
},
|
||||
"import": {
|
||||
"node": "./index.js",
|
||||
"edge-light": "./wasm.js",
|
||||
"workerd": "./wasm.js",
|
||||
"worker": "./wasm.js",
|
||||
"browser": "./index-browser.js",
|
||||
"default": "./index.js"
|
||||
},
|
||||
"default": "./index.js"
|
||||
},
|
||||
"./edge": {
|
||||
"types": "./edge.d.ts",
|
||||
"require": "./edge.js",
|
||||
"import": "./edge.js",
|
||||
"default": "./edge.js"
|
||||
},
|
||||
"./react-native": {
|
||||
"types": "./react-native.d.ts",
|
||||
"require": "./react-native.js",
|
||||
"import": "./react-native.js",
|
||||
"default": "./react-native.js"
|
||||
},
|
||||
"./extension": {
|
||||
"types": "./extension.d.ts",
|
||||
"require": "./extension.js",
|
||||
"import": "./extension.js",
|
||||
"default": "./extension.js"
|
||||
},
|
||||
"./index-browser": {
|
||||
"types": "./index.d.ts",
|
||||
"require": "./index-browser.js",
|
||||
"import": "./index-browser.js",
|
||||
"default": "./index-browser.js"
|
||||
},
|
||||
"./index": {
|
||||
"types": "./index.d.ts",
|
||||
"require": "./index.js",
|
||||
"import": "./index.js",
|
||||
"default": "./index.js"
|
||||
},
|
||||
"./wasm": {
|
||||
"types": "./wasm.d.ts",
|
||||
"require": "./wasm.js",
|
||||
"import": "./wasm.mjs",
|
||||
"default": "./wasm.mjs"
|
||||
},
|
||||
"./runtime/client": {
|
||||
"types": "./runtime/client.d.ts",
|
||||
"node": {
|
||||
"require": "./runtime/client.js",
|
||||
"default": "./runtime/client.js"
|
||||
},
|
||||
"require": "./runtime/client.js",
|
||||
"import": "./runtime/client.mjs",
|
||||
"default": "./runtime/client.mjs"
|
||||
},
|
||||
"./runtime/library": {
|
||||
"types": "./runtime/library.d.ts",
|
||||
"require": "./runtime/library.js",
|
||||
"import": "./runtime/library.mjs",
|
||||
"default": "./runtime/library.mjs"
|
||||
},
|
||||
"./runtime/binary": {
|
||||
"types": "./runtime/binary.d.ts",
|
||||
"require": "./runtime/binary.js",
|
||||
"import": "./runtime/binary.mjs",
|
||||
"default": "./runtime/binary.mjs"
|
||||
},
|
||||
"./runtime/wasm-engine-edge": {
|
||||
"types": "./runtime/wasm-engine-edge.d.ts",
|
||||
"require": "./runtime/wasm-engine-edge.js",
|
||||
"import": "./runtime/wasm-engine-edge.mjs",
|
||||
"default": "./runtime/wasm-engine-edge.mjs"
|
||||
},
|
||||
"./runtime/wasm-compiler-edge": {
|
||||
"types": "./runtime/wasm-compiler-edge.d.ts",
|
||||
"require": "./runtime/wasm-compiler-edge.js",
|
||||
"import": "./runtime/wasm-compiler-edge.mjs",
|
||||
"default": "./runtime/wasm-compiler-edge.mjs"
|
||||
},
|
||||
"./runtime/edge": {
|
||||
"types": "./runtime/edge.d.ts",
|
||||
"require": "./runtime/edge.js",
|
||||
"import": "./runtime/edge-esm.js",
|
||||
"default": "./runtime/edge-esm.js"
|
||||
},
|
||||
"./runtime/react-native": {
|
||||
"types": "./runtime/react-native.d.ts",
|
||||
"require": "./runtime/react-native.js",
|
||||
"import": "./runtime/react-native.js",
|
||||
"default": "./runtime/react-native.js"
|
||||
},
|
||||
"./runtime/index-browser": {
|
||||
"types": "./runtime/index-browser.d.ts",
|
||||
"require": "./runtime/index-browser.js",
|
||||
"import": "./runtime/index-browser.mjs",
|
||||
"default": "./runtime/index-browser.mjs"
|
||||
},
|
||||
"./generator-build": {
|
||||
"require": "./generator-build/index.js",
|
||||
"import": "./generator-build/index.js",
|
||||
"default": "./generator-build/index.js"
|
||||
},
|
||||
"./sql": {
|
||||
"require": {
|
||||
"types": "./sql.d.ts",
|
||||
"node": "./sql.js",
|
||||
"default": "./sql.js"
|
||||
},
|
||||
"import": {
|
||||
"types": "./sql.d.ts",
|
||||
"node": "./sql.mjs",
|
||||
"default": "./sql.mjs"
|
||||
},
|
||||
"default": "./sql.js"
|
||||
},
|
||||
"./*": "./*"
|
||||
},
|
||||
"version": "6.19.3",
|
||||
"sideEffects": false,
|
||||
"imports": {
|
||||
"#wasm-engine-loader": {
|
||||
"edge-light": "./wasm-edge-light-loader.mjs",
|
||||
"workerd": "./wasm-worker-loader.mjs",
|
||||
"worker": "./wasm-worker-loader.mjs",
|
||||
"default": "./wasm-worker-loader.mjs"
|
||||
},
|
||||
"#main-entry-point": {
|
||||
"require": {
|
||||
"node": "./index.js",
|
||||
"edge-light": "./wasm.js",
|
||||
"workerd": "./wasm.js",
|
||||
"worker": "./wasm.js",
|
||||
"browser": "./index-browser.js",
|
||||
"default": "./index.js"
|
||||
},
|
||||
"import": {
|
||||
"node": "./index.js",
|
||||
"edge-light": "./wasm.js",
|
||||
"workerd": "./wasm.js",
|
||||
"worker": "./wasm.js",
|
||||
"browser": "./index-browser.js",
|
||||
"default": "./index.js"
|
||||
},
|
||||
"default": "./index.js"
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,370 @@
|
||||
declare class AnyNull extends NullTypesEnumValue {
|
||||
#private;
|
||||
}
|
||||
|
||||
declare type Args<T, F extends Operation> = T extends {
|
||||
[K: symbol]: {
|
||||
types: {
|
||||
operations: {
|
||||
[K in F]: {
|
||||
args: any;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
} ? T[symbol]['types']['operations'][F]['args'] : any;
|
||||
|
||||
declare class DbNull extends NullTypesEnumValue {
|
||||
#private;
|
||||
}
|
||||
|
||||
export declare function Decimal(n: Decimal.Value): Decimal;
|
||||
|
||||
export declare namespace Decimal {
|
||||
export type Constructor = typeof Decimal;
|
||||
export type Instance = Decimal;
|
||||
export type Rounding = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8;
|
||||
export type Modulo = Rounding | 9;
|
||||
export type Value = string | number | Decimal;
|
||||
|
||||
// http://mikemcl.github.io/decimal.js/#constructor-properties
|
||||
export interface Config {
|
||||
precision?: number;
|
||||
rounding?: Rounding;
|
||||
toExpNeg?: number;
|
||||
toExpPos?: number;
|
||||
minE?: number;
|
||||
maxE?: number;
|
||||
crypto?: boolean;
|
||||
modulo?: Modulo;
|
||||
defaults?: boolean;
|
||||
}
|
||||
}
|
||||
|
||||
export declare class Decimal {
|
||||
readonly d: number[];
|
||||
readonly e: number;
|
||||
readonly s: number;
|
||||
|
||||
constructor(n: Decimal.Value);
|
||||
|
||||
absoluteValue(): Decimal;
|
||||
abs(): Decimal;
|
||||
|
||||
ceil(): Decimal;
|
||||
|
||||
clampedTo(min: Decimal.Value, max: Decimal.Value): Decimal;
|
||||
clamp(min: Decimal.Value, max: Decimal.Value): Decimal;
|
||||
|
||||
comparedTo(n: Decimal.Value): number;
|
||||
cmp(n: Decimal.Value): number;
|
||||
|
||||
cosine(): Decimal;
|
||||
cos(): Decimal;
|
||||
|
||||
cubeRoot(): Decimal;
|
||||
cbrt(): Decimal;
|
||||
|
||||
decimalPlaces(): number;
|
||||
dp(): number;
|
||||
|
||||
dividedBy(n: Decimal.Value): Decimal;
|
||||
div(n: Decimal.Value): Decimal;
|
||||
|
||||
dividedToIntegerBy(n: Decimal.Value): Decimal;
|
||||
divToInt(n: Decimal.Value): Decimal;
|
||||
|
||||
equals(n: Decimal.Value): boolean;
|
||||
eq(n: Decimal.Value): boolean;
|
||||
|
||||
floor(): Decimal;
|
||||
|
||||
greaterThan(n: Decimal.Value): boolean;
|
||||
gt(n: Decimal.Value): boolean;
|
||||
|
||||
greaterThanOrEqualTo(n: Decimal.Value): boolean;
|
||||
gte(n: Decimal.Value): boolean;
|
||||
|
||||
hyperbolicCosine(): Decimal;
|
||||
cosh(): Decimal;
|
||||
|
||||
hyperbolicSine(): Decimal;
|
||||
sinh(): Decimal;
|
||||
|
||||
hyperbolicTangent(): Decimal;
|
||||
tanh(): Decimal;
|
||||
|
||||
inverseCosine(): Decimal;
|
||||
acos(): Decimal;
|
||||
|
||||
inverseHyperbolicCosine(): Decimal;
|
||||
acosh(): Decimal;
|
||||
|
||||
inverseHyperbolicSine(): Decimal;
|
||||
asinh(): Decimal;
|
||||
|
||||
inverseHyperbolicTangent(): Decimal;
|
||||
atanh(): Decimal;
|
||||
|
||||
inverseSine(): Decimal;
|
||||
asin(): Decimal;
|
||||
|
||||
inverseTangent(): Decimal;
|
||||
atan(): Decimal;
|
||||
|
||||
isFinite(): boolean;
|
||||
|
||||
isInteger(): boolean;
|
||||
isInt(): boolean;
|
||||
|
||||
isNaN(): boolean;
|
||||
|
||||
isNegative(): boolean;
|
||||
isNeg(): boolean;
|
||||
|
||||
isPositive(): boolean;
|
||||
isPos(): boolean;
|
||||
|
||||
isZero(): boolean;
|
||||
|
||||
lessThan(n: Decimal.Value): boolean;
|
||||
lt(n: Decimal.Value): boolean;
|
||||
|
||||
lessThanOrEqualTo(n: Decimal.Value): boolean;
|
||||
lte(n: Decimal.Value): boolean;
|
||||
|
||||
logarithm(n?: Decimal.Value): Decimal;
|
||||
log(n?: Decimal.Value): Decimal;
|
||||
|
||||
minus(n: Decimal.Value): Decimal;
|
||||
sub(n: Decimal.Value): Decimal;
|
||||
|
||||
modulo(n: Decimal.Value): Decimal;
|
||||
mod(n: Decimal.Value): Decimal;
|
||||
|
||||
naturalExponential(): Decimal;
|
||||
exp(): Decimal;
|
||||
|
||||
naturalLogarithm(): Decimal;
|
||||
ln(): Decimal;
|
||||
|
||||
negated(): Decimal;
|
||||
neg(): Decimal;
|
||||
|
||||
plus(n: Decimal.Value): Decimal;
|
||||
add(n: Decimal.Value): Decimal;
|
||||
|
||||
precision(includeZeros?: boolean): number;
|
||||
sd(includeZeros?: boolean): number;
|
||||
|
||||
round(): Decimal;
|
||||
|
||||
sine() : Decimal;
|
||||
sin() : Decimal;
|
||||
|
||||
squareRoot(): Decimal;
|
||||
sqrt(): Decimal;
|
||||
|
||||
tangent() : Decimal;
|
||||
tan() : Decimal;
|
||||
|
||||
times(n: Decimal.Value): Decimal;
|
||||
mul(n: Decimal.Value) : Decimal;
|
||||
|
||||
toBinary(significantDigits?: number): string;
|
||||
toBinary(significantDigits: number, rounding: Decimal.Rounding): string;
|
||||
|
||||
toDecimalPlaces(decimalPlaces?: number): Decimal;
|
||||
toDecimalPlaces(decimalPlaces: number, rounding: Decimal.Rounding): Decimal;
|
||||
toDP(decimalPlaces?: number): Decimal;
|
||||
toDP(decimalPlaces: number, rounding: Decimal.Rounding): Decimal;
|
||||
|
||||
toExponential(decimalPlaces?: number): string;
|
||||
toExponential(decimalPlaces: number, rounding: Decimal.Rounding): string;
|
||||
|
||||
toFixed(decimalPlaces?: number): string;
|
||||
toFixed(decimalPlaces: number, rounding: Decimal.Rounding): string;
|
||||
|
||||
toFraction(max_denominator?: Decimal.Value): Decimal[];
|
||||
|
||||
toHexadecimal(significantDigits?: number): string;
|
||||
toHexadecimal(significantDigits: number, rounding: Decimal.Rounding): string;
|
||||
toHex(significantDigits?: number): string;
|
||||
toHex(significantDigits: number, rounding?: Decimal.Rounding): string;
|
||||
|
||||
toJSON(): string;
|
||||
|
||||
toNearest(n: Decimal.Value, rounding?: Decimal.Rounding): Decimal;
|
||||
|
||||
toNumber(): number;
|
||||
|
||||
toOctal(significantDigits?: number): string;
|
||||
toOctal(significantDigits: number, rounding: Decimal.Rounding): string;
|
||||
|
||||
toPower(n: Decimal.Value): Decimal;
|
||||
pow(n: Decimal.Value): Decimal;
|
||||
|
||||
toPrecision(significantDigits?: number): string;
|
||||
toPrecision(significantDigits: number, rounding: Decimal.Rounding): string;
|
||||
|
||||
toSignificantDigits(significantDigits?: number): Decimal;
|
||||
toSignificantDigits(significantDigits: number, rounding: Decimal.Rounding): Decimal;
|
||||
toSD(significantDigits?: number): Decimal;
|
||||
toSD(significantDigits: number, rounding: Decimal.Rounding): Decimal;
|
||||
|
||||
toString(): string;
|
||||
|
||||
truncated(): Decimal;
|
||||
trunc(): Decimal;
|
||||
|
||||
valueOf(): string;
|
||||
|
||||
static abs(n: Decimal.Value): Decimal;
|
||||
static acos(n: Decimal.Value): Decimal;
|
||||
static acosh(n: Decimal.Value): Decimal;
|
||||
static add(x: Decimal.Value, y: Decimal.Value): Decimal;
|
||||
static asin(n: Decimal.Value): Decimal;
|
||||
static asinh(n: Decimal.Value): Decimal;
|
||||
static atan(n: Decimal.Value): Decimal;
|
||||
static atanh(n: Decimal.Value): Decimal;
|
||||
static atan2(y: Decimal.Value, x: Decimal.Value): Decimal;
|
||||
static cbrt(n: Decimal.Value): Decimal;
|
||||
static ceil(n: Decimal.Value): Decimal;
|
||||
static clamp(n: Decimal.Value, min: Decimal.Value, max: Decimal.Value): Decimal;
|
||||
static clone(object?: Decimal.Config): Decimal.Constructor;
|
||||
static config(object: Decimal.Config): Decimal.Constructor;
|
||||
static cos(n: Decimal.Value): Decimal;
|
||||
static cosh(n: Decimal.Value): Decimal;
|
||||
static div(x: Decimal.Value, y: Decimal.Value): Decimal;
|
||||
static exp(n: Decimal.Value): Decimal;
|
||||
static floor(n: Decimal.Value): Decimal;
|
||||
static hypot(...n: Decimal.Value[]): Decimal;
|
||||
static isDecimal(object: any): object is Decimal;
|
||||
static ln(n: Decimal.Value): Decimal;
|
||||
static log(n: Decimal.Value, base?: Decimal.Value): Decimal;
|
||||
static log2(n: Decimal.Value): Decimal;
|
||||
static log10(n: Decimal.Value): Decimal;
|
||||
static max(...n: Decimal.Value[]): Decimal;
|
||||
static min(...n: Decimal.Value[]): Decimal;
|
||||
static mod(x: Decimal.Value, y: Decimal.Value): Decimal;
|
||||
static mul(x: Decimal.Value, y: Decimal.Value): Decimal;
|
||||
static noConflict(): Decimal.Constructor; // Browser only
|
||||
static pow(base: Decimal.Value, exponent: Decimal.Value): Decimal;
|
||||
static random(significantDigits?: number): Decimal;
|
||||
static round(n: Decimal.Value): Decimal;
|
||||
static set(object: Decimal.Config): Decimal.Constructor;
|
||||
static sign(n: Decimal.Value): number;
|
||||
static sin(n: Decimal.Value): Decimal;
|
||||
static sinh(n: Decimal.Value): Decimal;
|
||||
static sqrt(n: Decimal.Value): Decimal;
|
||||
static sub(x: Decimal.Value, y: Decimal.Value): Decimal;
|
||||
static sum(...n: Decimal.Value[]): Decimal;
|
||||
static tan(n: Decimal.Value): Decimal;
|
||||
static tanh(n: Decimal.Value): Decimal;
|
||||
static trunc(n: Decimal.Value): Decimal;
|
||||
|
||||
static readonly default?: Decimal.Constructor;
|
||||
static readonly Decimal?: Decimal.Constructor;
|
||||
|
||||
static readonly precision: number;
|
||||
static readonly rounding: Decimal.Rounding;
|
||||
static readonly toExpNeg: number;
|
||||
static readonly toExpPos: number;
|
||||
static readonly minE: number;
|
||||
static readonly maxE: number;
|
||||
static readonly crypto: boolean;
|
||||
static readonly modulo: Decimal.Modulo;
|
||||
|
||||
static readonly ROUND_UP: 0;
|
||||
static readonly ROUND_DOWN: 1;
|
||||
static readonly ROUND_CEIL: 2;
|
||||
static readonly ROUND_FLOOR: 3;
|
||||
static readonly ROUND_HALF_UP: 4;
|
||||
static readonly ROUND_HALF_DOWN: 5;
|
||||
static readonly ROUND_HALF_EVEN: 6;
|
||||
static readonly ROUND_HALF_CEIL: 7;
|
||||
static readonly ROUND_HALF_FLOOR: 8;
|
||||
static readonly EUCLID: 9;
|
||||
}
|
||||
|
||||
declare type Exact<A, W> = (A extends unknown ? (W extends A ? {
|
||||
[K in keyof A]: Exact<A[K], W[K]>;
|
||||
} : W) : never) | (A extends Narrowable ? A : never);
|
||||
|
||||
export declare function getRuntime(): GetRuntimeOutput;
|
||||
|
||||
declare type GetRuntimeOutput = {
|
||||
id: RuntimeName;
|
||||
prettyName: string;
|
||||
isEdge: boolean;
|
||||
};
|
||||
|
||||
declare class JsonNull extends NullTypesEnumValue {
|
||||
#private;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates more strict variant of an enum which, unlike regular enum,
|
||||
* throws on non-existing property access. This can be useful in following situations:
|
||||
* - we have an API, that accepts both `undefined` and `SomeEnumType` as an input
|
||||
* - enum values are generated dynamically from DMMF.
|
||||
*
|
||||
* In that case, if using normal enums and no compile-time typechecking, using non-existing property
|
||||
* will result in `undefined` value being used, which will be accepted. Using strict enum
|
||||
* in this case will help to have a runtime exception, telling you that you are probably doing something wrong.
|
||||
*
|
||||
* Note: if you need to check for existence of a value in the enum you can still use either
|
||||
* `in` operator or `hasOwnProperty` function.
|
||||
*
|
||||
* @param definition
|
||||
* @returns
|
||||
*/
|
||||
export declare function makeStrictEnum<T extends Record<PropertyKey, string | number>>(definition: T): T;
|
||||
|
||||
declare type Narrowable = string | number | bigint | boolean | [];
|
||||
|
||||
declare class NullTypesEnumValue extends ObjectEnumValue {
|
||||
_getNamespace(): string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Base class for unique values of object-valued enums.
|
||||
*/
|
||||
declare abstract class ObjectEnumValue {
|
||||
constructor(arg?: symbol);
|
||||
abstract _getNamespace(): string;
|
||||
_getName(): string;
|
||||
toString(): string;
|
||||
}
|
||||
|
||||
export declare const objectEnumValues: {
|
||||
classes: {
|
||||
DbNull: typeof DbNull;
|
||||
JsonNull: typeof JsonNull;
|
||||
AnyNull: typeof AnyNull;
|
||||
};
|
||||
instances: {
|
||||
DbNull: DbNull;
|
||||
JsonNull: JsonNull;
|
||||
AnyNull: AnyNull;
|
||||
};
|
||||
};
|
||||
|
||||
declare type Operation = 'findFirst' | 'findFirstOrThrow' | 'findUnique' | 'findUniqueOrThrow' | 'findMany' | 'create' | 'createMany' | 'createManyAndReturn' | 'update' | 'updateMany' | 'updateManyAndReturn' | 'upsert' | 'delete' | 'deleteMany' | 'aggregate' | 'count' | 'groupBy' | '$queryRaw' | '$executeRaw' | '$queryRawUnsafe' | '$executeRawUnsafe' | 'findRaw' | 'aggregateRaw' | '$runCommandRaw';
|
||||
|
||||
declare namespace Public {
|
||||
export {
|
||||
validator
|
||||
}
|
||||
}
|
||||
export { Public }
|
||||
|
||||
declare type RuntimeName = 'workerd' | 'deno' | 'netlify' | 'node' | 'bun' | 'edge-light' | '';
|
||||
|
||||
declare function validator<V>(): <S>(select: Exact<S, V>) => S;
|
||||
|
||||
declare function validator<C, M extends Exclude<keyof C, `$${string}`>, O extends keyof C[M] & Operation>(client: C, model: M, operation: O): <S>(select: Exact<S, Args<C[M], O>>) => S;
|
||||
|
||||
declare function validator<C, M extends Exclude<keyof C, `$${string}`>, O extends keyof C[M] & Operation, P extends keyof Args<C[M], O>>(client: C, model: M, operation: O, prop: P): <S>(select: Exact<S, Args<C[M], O>[P]>) => S;
|
||||
|
||||
export { }
|
||||
File diff suppressed because one or more lines are too long
+3982
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user