Compare commits

...

12 Commits

Author SHA1 Message Date
chyoo 9f67ee5719 fix(admin): filter non-container releases from deployments
ci/woodpecker/push/woodpecker Pipeline was successful
2026-08-13 17:53:48 +09:00
chyoo a6a4426249 add instructor invite code..
ci/woodpecker/push/woodpecker Pipeline was successful
2026-08-07 11:57:55 +09:00
chyoo 8b7ec648cd add tailscale node delete!
ci/woodpecker/push/woodpecker Pipeline was successful
2026-08-07 10:25:49 +09:00
chyoo 8151ec094d feat: display platform agent status and version
ci/woodpecker/push/woodpecker Pipeline was successful
2026-08-06 16:03:26 +09:00
chyoo 63e85d5306 fix: exclude database components from app releases and deployments 2026-07-15 16:10:36 +09:00
chyoo 3945e42c53 feat: add quick deploy for launcher backend builds
ci/woodpecker/push/woodpecker Pipeline was successful
2026-07-15 16:01:09 +09:00
chyoo 53b4909df7 fix: add lms web verision url
ci/woodpecker/push/woodpecker Pipeline was successful
2026-07-15 14:55:19 +09:00
chyoo a80e6eab20 fix: apply deployment resource settings to Kubernetes workloads
ci/woodpecker/push/woodpecker Pipeline was successful
2026-07-15 14:28:36 +09:00
chyoo 1c240e895b fix: install native build tools for launcher client builds
ci/woodpecker/push/woodpecker Pipeline was successful
2026-07-15 14:15:05 +09:00
chyoo 03d065bccb chore: remove stale generated prisma clients
ci/woodpecker/push/woodpecker Pipeline was successful
2026-07-14 15:13:39 +09:00
chyoo f4a748b5f3 improve runtime environment variable modal usability
ci/woodpecker/push/woodpecker Pipeline was successful
2026-07-14 15:08:50 +09:00
chyoo d922b873e4 add infrastructure resource YAML viewer and fix PVC visibility
ci/woodpecker/push/woodpecker Pipeline was successful
2026-07-14 13:35:59 +09:00
77 changed files with 734 additions and 24340 deletions
+1 -1
View File
@@ -10,7 +10,7 @@ WORKDIR /app
ENV NODE_ENV=production ENV NODE_ENV=production
ARG DOCKER_VERSION=27.5.1 ARG DOCKER_VERSION=27.5.1
RUN apt-get update \ RUN apt-get update \
&& apt-get install -y --no-install-recommends ca-certificates curl git \ && apt-get install -y --no-install-recommends ca-certificates curl git python3 make g++ \
&& curl -fsSL "https://download.docker.com/linux/static/stable/x86_64/docker-${DOCKER_VERSION}.tgz" \ && curl -fsSL "https://download.docker.com/linux/static/stable/x86_64/docker-${DOCKER_VERSION}.tgz" \
| tar -xz -C /tmp \ | tar -xz -C /tmp \
&& install -m 0755 /tmp/docker/docker /usr/local/bin/docker \ && install -m 0755 /tmp/docker/docker /usr/local/bin/docker \
+21
View File
@@ -44,6 +44,7 @@ model User {
userLicenses UserLicense[] userLicenses UserLicense[]
refreshTokens RefreshToken[] refreshTokens RefreshToken[]
affiliations UserAffiliation[] affiliations UserAffiliation[]
appRoles UserAppRole[]
@@map("users") @@map("users")
} }
@@ -113,6 +114,7 @@ model Organization {
type OrganizationType @default(OTHER) type OrganizationType @default(OTHER)
domain String? domain String?
defaultLicenseCode String? @map("default_license_code") defaultLicenseCode String? @map("default_license_code")
instructorInviteCode String @unique @map("instructor_invite_code")
isActive Boolean @default(true) @map("is_active") isActive Boolean @default(true) @map("is_active")
createdById String? @map("created_by_id") @db.Uuid createdById String? @map("created_by_id") @db.Uuid
createdByName String? @map("created_by_name") createdByName String? @map("created_by_name")
@@ -145,3 +147,22 @@ model UserAffiliation {
@@index([organizationId]) @@index([organizationId])
@@map("user_affiliations") @@map("user_affiliations")
} }
model UserAppRole {
id String @id @default(uuid()) @db.Uuid
userId String @map("user_id") @db.Uuid
appKey String @map("app_key")
roleCode String @map("role_code")
assignedById String? @map("assigned_by") @db.Uuid
assignedAt DateTime @default(now()) @map("assigned_at") @db.Timestamptz(3)
startsAt DateTime? @map("starts_at") @db.Timestamptz(3)
endsAt DateTime? @map("ends_at") @db.Timestamptz(3)
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(3)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([userId, appKey, roleCode])
@@index([appKey, roleCode])
@@index([userId, appKey])
@@map("user_app_roles")
}
+26
View File
@@ -77,6 +77,9 @@ model ProvisionedDevice {
kernel String? kernel String?
distro String? distro String?
device Json? device Json?
agentVersion String? @map("agent_version")
agentUpdatedAt DateTime? @map("agent_updated_at") @db.Timestamptz(3)
agentLastSeenAt DateTime? @map("agent_last_seen_at") @db.Timestamptz(3)
installedAt DateTime @default(now()) @map("installed_at") @db.Timestamptz(3) installedAt DateTime @default(now()) @map("installed_at") @db.Timestamptz(3)
lastProvisionedAt DateTime @default(now()) @map("last_provisioned_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) createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)
@@ -85,9 +88,32 @@ model ProvisionedDevice {
@@unique([accountEmail, tailscaleHostname]) @@unique([accountEmail, tailscaleHostname])
@@index([accountEmail]) @@index([accountEmail])
@@index([tailscaleHostname]) @@index([tailscaleHostname])
@@index([agentLastSeenAt])
@@map("provisioned_devices") @@map("provisioned_devices")
} }
model DeviceAuthorization {
id String @id @default(uuid()) @db.Uuid
deviceCodeHash String @unique @map("device_code_hash")
userCode String @unique @map("user_code")
status String @default("PENDING")
authUserId String? @map("auth_user_id") @db.Uuid
accountEmail String? @map("account_email")
authUserName String? @map("auth_user_name")
authUserRole String? @map("auth_user_role")
authUserRoles Json? @map("auth_user_roles")
device Json?
expiresAt DateTime @map("expires_at") @db.Timestamptz(3)
approvedAt DateTime? @map("approved_at") @db.Timestamptz(3)
consumedAt DateTime? @map("consumed_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, expiresAt])
@@index([authUserId, createdAt])
@@map("device_authorizations")
}
model LauncherSshKey { model LauncherSshKey {
id String @id @default(uuid()) @db.Uuid id String @id @default(uuid()) @db.Uuid
accountEmail String @map("account_email") accountEmail String @map("account_email")
@@ -0,0 +1,2 @@
ALTER TABLE "build_jobs"
ADD COLUMN "artifact_sha256" TEXT;
+1
View File
@@ -215,6 +215,7 @@ model BuildJob {
branch String branch String
commitSha String? @map("commit_sha") commitSha String? @map("commit_sha")
artifact String? artifact String?
artifactSha256 String? @map("artifact_sha256")
image String? image String?
status JobStatus @default(PENDING) status JobStatus @default(PENDING)
requestedById String? @map("requested_by_id") @db.Uuid requestedById String? @map("requested_by_id") @db.Uuid
+23 -1
View File
@@ -1,7 +1,9 @@
import { Controller, Delete, Get, Param, Patch, Query, Body } from '@nestjs/common'; import { Body, Controller, Delete, Get, Param, Patch, Post, Query } from '@nestjs/common';
import { import {
ListLauncherSshKeysQuery, ListLauncherSshKeysQuery,
CreateDeviceAuthorizationDto,
ListDeviceAuthorizationUsersQuery,
ListProvisionedDevicesQuery, ListProvisionedDevicesQuery,
UpdateProvisionedDeviceDto, UpdateProvisionedDeviceDto,
} from './dto'; } from './dto';
@@ -38,4 +40,24 @@ export class AgentsController {
removeSshKey(@Param('id') id: string) { removeSshKey(@Param('id') id: string) {
return this.agentsService.removeSshKey(id); return this.agentsService.removeSshKey(id);
} }
@Get('registration-users')
listRegistrationUsers(@Query() query: ListDeviceAuthorizationUsersQuery) {
return this.agentsService.listRegistrationUsers(query);
}
@Get('device-authorizations')
listDeviceAuthorizations() {
return this.agentsService.listDeviceAuthorizations();
}
@Post('device-authorizations')
createDeviceAuthorization(@Body() body: CreateDeviceAuthorizationDto) {
return this.agentsService.createDeviceAuthorization(body.userId);
}
@Post('device-authorizations/:id/cancel')
cancelDeviceAuthorization(@Param('id') id: string) {
return this.agentsService.cancelDeviceAuthorization(id);
}
} }
+2 -1
View File
@@ -1,11 +1,12 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { AuthPrismaModule } from '../auth-prisma/auth-prisma.module';
import { LauncherPrismaModule } from '../launcher-prisma/launcher-prisma.module'; import { LauncherPrismaModule } from '../launcher-prisma/launcher-prisma.module';
import { AgentsController } from './agents.controller'; import { AgentsController } from './agents.controller';
import { AgentsService } from './agents.service'; import { AgentsService } from './agents.service';
@Module({ @Module({
imports: [LauncherPrismaModule], imports: [AuthPrismaModule, LauncherPrismaModule],
controllers: [AgentsController], controllers: [AgentsController],
providers: [AgentsService], providers: [AgentsService],
}) })
+115 -1
View File
@@ -1,7 +1,10 @@
import { Injectable, NotFoundException } from '@nestjs/common'; import { Injectable, NotFoundException } from '@nestjs/common';
import { createHash, randomBytes } from 'node:crypto';
import { AuthPrismaService } from '../auth-prisma/auth-prisma.service';
import { LauncherPrismaService } from '../launcher-prisma/launcher-prisma.service'; import { LauncherPrismaService } from '../launcher-prisma/launcher-prisma.service';
import { import {
ListDeviceAuthorizationUsersQuery,
ListLauncherSshKeysQuery, ListLauncherSshKeysQuery,
ListProvisionedDevicesQuery, ListProvisionedDevicesQuery,
UpdateProvisionedDeviceDto, UpdateProvisionedDeviceDto,
@@ -9,7 +12,10 @@ import {
@Injectable() @Injectable()
export class AgentsService { export class AgentsService {
constructor(private readonly launcherPrisma: LauncherPrismaService) {} constructor(
private readonly authPrisma: AuthPrismaService,
private readonly launcherPrisma: LauncherPrismaService,
) {}
async listDevices(query: ListProvisionedDevicesQuery) { async listDevices(query: ListProvisionedDevicesQuery) {
const keyword = query.keyword?.trim(); const keyword = query.keyword?.trim();
@@ -109,6 +115,114 @@ export class AgentsService {
return { ok: true }; return { ok: true };
} }
async listRegistrationUsers(query: ListDeviceAuthorizationUsersQuery) {
const keyword = query.keyword?.trim();
const users = await this.authPrisma.user.findMany({
where: {
role: 'USER',
isActive: true,
...(keyword
? {
OR: [
{ email: { contains: keyword, mode: 'insensitive' } },
{ name: { contains: keyword, mode: 'insensitive' } },
],
}
: {}),
},
orderBy: [{ name: 'asc' }, { email: 'asc' }],
take: 20,
select: { id: true, name: true, email: true },
});
return { users };
}
async listDeviceAuthorizations() {
const now = new Date();
const authorizations = await this.launcherPrisma.deviceAuthorization.findMany({
where: {
OR: [
{ status: { in: ['APPROVED', 'CONSUMING'] } },
{ createdAt: { gte: new Date(now.getTime() - 24 * 60 * 60 * 1000) } },
],
},
orderBy: { createdAt: 'desc' },
take: 50,
select: {
id: true,
userCode: true,
status: true,
authUserId: true,
accountEmail: true,
authUserName: true,
expiresAt: true,
createdAt: true,
},
});
return {
authorizations: authorizations.map((authorization) => ({
...authorization,
status: authorization.status === 'APPROVED' && authorization.expiresAt <= now ? 'EXPIRED' : authorization.status,
})),
};
}
async createDeviceAuthorization(userId: string) {
const user = await this.authPrisma.user.findFirst({
where: { id: userId, role: 'USER', isActive: true },
select: { id: true, email: true, name: true, role: true },
});
if (!user) {
throw new NotFoundException('플랫폼 등록 대상 사용자를 찾을 수 없습니다.');
}
const now = new Date();
const expiresAt = new Date(now.getTime() + 5 * 60 * 1000);
await this.launcherPrisma.deviceAuthorization.updateMany({
where: {
authUserId: user.id,
accountEmail: user.email,
status: 'APPROVED',
expiresAt: { gt: now },
},
data: { status: 'CANCELLED' },
});
const authorization = await this.launcherPrisma.deviceAuthorization.create({
data: {
deviceCodeHash: createHash('sha256').update(randomBytes(32).toString('base64url')).digest('hex'),
userCode: String(randomBytes(4).readUInt32BE() % 1_000_000).padStart(6, '0'),
status: 'APPROVED',
authUserId: user.id,
accountEmail: user.email,
authUserName: user.name,
authUserRole: user.role,
authUserRoles: [user.role],
expiresAt,
approvedAt: now,
},
select: { id: true, userCode: true, expiresAt: true, accountEmail: true, authUserName: true, status: true },
});
return { authorization };
}
async cancelDeviceAuthorization(id: string) {
const result = await this.launcherPrisma.deviceAuthorization.updateMany({
where: { id, status: { in: ['APPROVED', 'CONSUMING'] } },
data: { status: 'CANCELLED' },
});
if (!result.count) {
throw new NotFoundException('취소할 플랫폼 등록 키를 찾을 수 없습니다.');
}
return { ok: true };
}
private async ensureDeviceExists(id: string) { private async ensureDeviceExists(id: string) {
const device = await this.launcherPrisma.provisionedDevice.findUnique({ const device = await this.launcherPrisma.provisionedDevice.findUnique({
where: { id }, where: { id },
+12 -1
View File
@@ -1,4 +1,4 @@
import { IsOptional, IsString } from 'class-validator'; import { IsOptional, IsString, IsUUID } from 'class-validator';
export class ListProvisionedDevicesQuery { export class ListProvisionedDevicesQuery {
@IsOptional() @IsOptional()
@@ -37,3 +37,14 @@ export class ListLauncherSshKeysQuery {
@IsString() @IsString()
keyword?: string; keyword?: string;
} }
export class ListDeviceAuthorizationUsersQuery {
@IsOptional()
@IsString()
keyword?: string;
}
export class CreateDeviceAuthorizationDto {
@IsUUID()
userId!: string;
}
+9
View File
@@ -195,6 +195,15 @@ export class AppsController {
return this.appsService.listRuntimeResources(namespace); return this.appsService.listRuntimeResources(namespace);
} }
@Get('runtime-namespaces/:namespace/resources/:kind/:name/yaml')
getRuntimeResourceYaml(
@Param('namespace') namespace: string,
@Param('kind') kind: string,
@Param('name') name: string,
) {
return this.appsService.getRuntimeResourceYaml(namespace, kind, name);
}
@Post('runtime-namespaces') @Post('runtime-namespaces')
ensureRuntimeNamespace(@Body() body: EnsureRuntimeNamespaceDto) { ensureRuntimeNamespace(@Body() body: EnsureRuntimeNamespaceDto) {
return this.appsService.ensureRuntimeNamespace(body); return this.appsService.ensureRuntimeNamespace(body);
+287 -124
View File
@@ -509,6 +509,15 @@ export class AppsService {
throw new Error('릴리즈 기준 빌드 작업을 찾을 수 없습니다.'); throw new Error('릴리즈 기준 빌드 작업을 찾을 수 없습니다.');
} }
const missingArtifactMetadata = sourceBuildJobs.find((job) => (
job.component.type === 'ELECTRON' && (!job.artifact || !job.artifactSha256)
));
if (missingArtifactMetadata) {
throw new Error(
`${missingArtifactMetadata.component.name} 빌드에 R2 산출물 또는 SHA-256 정보가 없습니다. 새 빌드 후 릴리즈하세요.`,
);
}
for (const job of sourceBuildJobs) { for (const job of sourceBuildJobs) {
await this.appendReleaseLog(releaseId, `[source] ${job.component.name} develop ${job.commitSha?.slice(0, 7) ?? '-'}`); await this.appendReleaseLog(releaseId, `[source] ${job.component.name} develop ${job.commitSha?.slice(0, 7) ?? '-'}`);
} }
@@ -516,21 +525,10 @@ export class AppsService {
await this.promoteReleaseBranches(sourceBuildJobs, 'develop', release.branch); await this.promoteReleaseBranches(sourceBuildJobs, 'develop', release.branch);
await this.appendReleaseLog(releaseId, '[promote] completed'); await this.appendReleaseLog(releaseId, '[promote] completed');
const releaseBuildJobs = await this.createReleaseBuildJobs(sourceBuildJobs, release.branch); await this.appendReleaseLog(releaseId, '[artifact] reusing selected develop build artifacts');
for (const job of releaseBuildJobs) { const commitShas = [...new Set(sourceBuildJobs.map((job) => job.commitSha).filter(Boolean))];
await this.appendReleaseLog(releaseId, `[build] queued ${job.component.name} ${job.branch} ${job.commitSha?.slice(0, 7) ?? '-'}`); const artifact = sourceBuildJobs.find((job) => job.artifact)?.artifact ?? null;
} const imageTags = createReleaseImageTags(sourceBuildJobs, release.app.components);
await this.appendReleaseLog(releaseId, '[wait] waiting for release build jobs');
const completedBuildJobs = await this.waitForBuildJobs(releaseBuildJobs.map((job) => job.id));
const failed = completedBuildJobs.find((job) => job.status !== 'SUCCEEDED');
if (failed) {
throw new Error(`${failed.component.name} release 빌드 실패: ${failed.status}`);
}
await this.appendReleaseLog(releaseId, '[build] release build jobs completed');
const commitShas = [...new Set(completedBuildJobs.map((job) => job.commitSha).filter(Boolean))];
const artifact = completedBuildJobs.find((job) => job.artifact)?.artifact ?? null;
const imageTags = createReleaseImageTags(completedBuildJobs, release.app.components);
await this.appendReleaseLog(releaseId, '[done] release completed'); await this.appendReleaseLog(releaseId, '[done] release completed');
await this.prisma.appRelease.update({ await this.prisma.appRelease.update({
@@ -572,87 +570,6 @@ export class AppsService {
}); });
} }
private async createReleaseBuildJobs(
sourceBuildJobs: Array<{
componentId: string;
commitSha: string | null;
requestedById?: string | null;
requestedByName?: string | null;
requestedByEmail?: string | null;
component: {
id: string;
name: string;
type: string;
repository: string | null;
buildTarget: string | null;
app: { name: string };
};
}>,
branch: string,
) {
const jobs = [];
for (const sourceJob of sourceBuildJobs) {
const component = sourceJob.component;
const artifact = getBuildArtifact(component, branch, sourceJob.commitSha);
const image = getBuildImage(
component,
branch,
sourceJob.commitSha,
this.configService.get<string>('BUILD_REGISTRY', 'registry.local'),
);
const job = await this.prisma.buildJob.create({
data: {
componentId: component.id,
branch,
commitSha: sourceJob.commitSha,
artifact,
image,
status: 'PENDING',
requestedById: sourceJob.requestedById,
requestedByName: sourceJob.requestedByName,
requestedByEmail: sourceJob.requestedByEmail,
logs: getBuildPlanLogs({
appName: component.app.name,
componentName: component.name,
componentType: component.type,
repository: component.repository || '',
branch,
commitSha: sourceJob.commitSha,
buildTarget: component.buildTarget,
artifact,
image,
giteaBaseUrl: this.configService.get<string>(
'GITEA_BASE_URL',
'http://localhost:3000',
),
}).join('\n'),
},
include: { component: true },
});
jobs.push(job);
}
return jobs;
}
private async waitForBuildJobs(buildJobIds: string[]) {
const timeoutMs = Number(this.configService.get<string>('RELEASE_BUILD_TIMEOUT_MS', '1800000'));
const startedAt = Date.now();
while (Date.now() - startedAt < timeoutMs) {
const jobs = await this.prisma.buildJob.findMany({
where: { id: { in: buildJobIds } },
include: { component: true },
});
if (jobs.length === buildJobIds.length && jobs.every((job) => ['SUCCEEDED', 'FAILED', 'CANCELED'].includes(job.status))) {
return jobs;
}
await delay(3000);
}
throw new Error('release 빌드 완료 대기 시간이 초과되었습니다.');
}
private async promoteReleaseBranches( private async promoteReleaseBranches(
buildJobs: Array<{ buildJobs: Array<{
id: string; id: string;
@@ -954,7 +871,12 @@ export class AppsService {
async createDeploymentJob(body: CreateDeploymentJobDto, requester?: AdminRequester) { async createDeploymentJob(body: CreateDeploymentJobDto, requester?: AdminRequester) {
const namespace = normalizeNamespace(body.namespace); const namespace = normalizeNamespace(body.namespace);
const environment = body.environment.trim(); const environment = body.environment.trim();
const release = await this.prisma.appRelease.findUnique({ if (body.releaseId && body.buildJobId) {
throw new BadRequestException('릴리즈 또는 빌드 중 하나만 배포 대상으로 선택할 수 있습니다.');
}
const release = body.releaseId
? await this.prisma.appRelease.findUnique({
where: { id: body.releaseId }, where: { id: body.releaseId },
include: { include: {
app: { app: {
@@ -963,21 +885,71 @@ export class AppsService {
}, },
}, },
}, },
}); })
: null;
const quickBuild = body.buildJobId
? await this.prisma.buildJob.findUnique({
where: { id: body.buildJobId },
include: {
component: {
include: {
app: {
include: { components: true },
},
},
},
},
})
: null;
if (!release) { if (body.releaseId && !release) {
throw new NotFoundException('릴리즈를 찾을 수 없습니다.'); throw new NotFoundException('릴리즈를 찾을 수 없습니다.');
} }
if (body.buildJobId && !quickBuild) {
if (release.status !== 'SUCCEEDED') { throw new NotFoundException('빌드 작업을 찾을 수 없습니다.');
}
if (!release && !quickBuild) {
throw new BadRequestException('배포할 릴리즈 또는 빌드를 선택하세요.');
}
if (release && release.status !== 'SUCCEEDED') {
throw new BadRequestException('성공한 릴리즈만 배포할 수 있습니다.'); throw new BadRequestException('성공한 릴리즈만 배포할 수 있습니다.');
} }
if (quickBuild && quickBuild.status !== 'SUCCEEDED') {
throw new BadRequestException('성공한 빌드만 빠른 배포할 수 있습니다.');
}
if (quickBuild && !['CONTAINER', 'STATIC_WEB'].includes(quickBuild.component.type)) {
throw new BadRequestException('컨테이너 이미지 빌드만 빠른 배포할 수 있습니다.');
}
if (quickBuild && !quickBuild.image) {
throw new BadRequestException('배포할 컨테이너 이미지가 없습니다.');
}
const imageTags = parseReleaseImageTags(release.imageTags); const app = release?.app ?? quickBuild!.component.app;
const images = imageTags.images; const releaseImages = release
const runtimeProfile = await this.prisma.runtimeEnvProfile.findFirst({ ? parseReleaseImageTags(release.imageTags).images
: { [quickBuild!.component.name]: quickBuild!.image! };
const images = Object.fromEntries(
Object.entries(releaseImages).filter(([componentName]) => {
const componentType = app.components.find((component) => component.name === componentName)?.type;
return componentType === 'CONTAINER' || componentType === 'STATIC_WEB';
}),
);
const hasKubernetesWorkload = Object.keys(images).length > 0;
const isLauncherArtifactRelease = Boolean(
release
&& !hasKubernetesWorkload
&& body.updateLauncherAppVersion !== false
&& app.components.some((component) => component.type === 'ELECTRON')
&& hasReleaseArtifact(release.artifact, release.imageTags),
);
const deploymentLabel = release
? `${app.name} ${release.version}`
: `${app.name} ${quickBuild!.component.name} ${quickBuild!.commitSha?.slice(0, 7) ?? 'quick'}`;
const requestedResources = body.resources;
const runtimeProfile = hasKubernetesWorkload
? await this.prisma.runtimeEnvProfile.findFirst({
where: { where: {
appId: release.appId, appId: app.id,
environment, environment,
namespace, namespace,
}, },
@@ -987,22 +959,28 @@ export class AppsService {
orderBy: { host: 'asc' }, orderBy: { host: 'asc' },
}, },
}, },
}); })
if (Object.keys(images).length === 0) { : null;
throw new BadRequestException('릴리즈에 배포 가능한 이미지가 없습니다.'); if (!hasKubernetesWorkload && !isLauncherArtifactRelease) {
if (release && hasReleaseArtifact(release.artifact, release.imageTags)) {
throw new BadRequestException('AppImage 릴리즈는 런처 앱 버전 업데이트를 선택해야 합니다.');
}
throw new BadRequestException('릴리즈에 배포 가능한 이미지 또는 AppImage 산출물이 없습니다.');
} }
if (hasKubernetesWorkload) {
const namespaceExists = await this.namespaceExists(namespace); const namespaceExists = await this.namespaceExists(namespace);
if (!namespaceExists) { if (!namespaceExists) {
throw new BadRequestException( throw new BadRequestException(
`${namespace} namespace가 없습니다. 런타임 구성에서 Namespace 생성/확인을 먼저 실행하세요.`, `${namespace} namespace가 없습니다. 런타임 구성에서 Namespace 생성/확인을 먼저 실행하세요.`,
); );
} }
}
const deploymentJob = await this.prisma.deploymentJob.create({ const deploymentJob = await this.prisma.deploymentJob.create({
data: { data: {
appId: release.appId, appId: app.id,
releaseId: release.id, releaseId: release?.id,
environment, environment,
namespace, namespace,
status: 'RUNNING', status: 'RUNNING',
@@ -1010,8 +988,10 @@ export class AppsService {
...this.requesterData(requester), ...this.requesterData(requester),
startedAt: new Date(), startedAt: new Date(),
logs: [ logs: [
`[running] deployment started: ${release.app.name} ${release.version}`, `[running] deployment started: ${deploymentLabel}`,
...(quickBuild ? [`[source] quick build ${quickBuild.id} ${quickBuild.image}`] : []),
`[target] environment=${environment} namespace=${namespace}`, `[target] environment=${environment} namespace=${namespace}`,
...(isLauncherArtifactRelease ? ['[launcher] AppImage release; Kubernetes rollout skipped'] : []),
].join('\n'), ].join('\n'),
}, },
include: { include: {
@@ -1025,7 +1005,7 @@ export class AppsService {
for (const [componentName, image] of Object.entries(images)) { for (const [componentName, image] of Object.entries(images)) {
const deploymentName = sanitizeK8sName(componentName); const deploymentName = sanitizeK8sName(componentName);
const component = release.app.components.find((item) => item.name === componentName); const component = app.components.find((item) => item.name === componentName);
const workloadType = normalizeWorkloadType(component?.workloadType, component?.type); const workloadType = normalizeWorkloadType(component?.workloadType, component?.type);
const deploymentExists = workloadType === 'StatefulSet' const deploymentExists = workloadType === 'StatefulSet'
? await this.statefulSetExists(namespace, deploymentName) ? await this.statefulSetExists(namespace, deploymentName)
@@ -1053,14 +1033,25 @@ export class AppsService {
deploymentName, deploymentName,
componentName, componentName,
image, image,
replicas: component?.replicas ?? 1, replicas: requestedResources?.replicas ?? component?.replicas ?? 1,
ports: getComponentPorts(component), ports: getComponentPorts(component),
resources: requestedResources,
}) })
), ),
); );
} }
const workloadRef = `${workloadType.toLowerCase()}/${deploymentName}`; const workloadRef = `${workloadType.toLowerCase()}/${deploymentName}`;
if (requestedResources?.replicas) {
logs.push(`[scale] ${workloadRef} replicas=${requestedResources.replicas}`);
appendOutput(logs, await this.kubectl([
'scale',
workloadRef,
`--replicas=${requestedResources.replicas}`,
'-n',
namespace,
]));
}
logs.push(`[set-image] ${workloadRef} ${deploymentName}=${image}`); logs.push(`[set-image] ${workloadRef} ${deploymentName}=${image}`);
const setImageOutput = await this.kubectl([ const setImageOutput = await this.kubectl([
'set', 'set',
@@ -1072,6 +1063,31 @@ export class AppsService {
]); ]);
appendOutput(logs, setImageOutput); appendOutput(logs, setImageOutput);
const resourceArgs = [
'set',
'resources',
workloadRef,
`--containers=${deploymentName}`,
'-n',
namespace,
...(requestedResources?.cpuRequest || requestedResources?.memoryRequest
? [`--requests=${[
requestedResources.cpuRequest ? `cpu=${requestedResources.cpuRequest}` : null,
requestedResources.memoryRequest ? `memory=${requestedResources.memoryRequest}` : null,
].filter(Boolean).join(',')}`]
: []),
...(requestedResources?.cpuLimit || requestedResources?.memoryLimit
? [`--limits=${[
requestedResources.cpuLimit ? `cpu=${requestedResources.cpuLimit}` : null,
requestedResources.memoryLimit ? `memory=${requestedResources.memoryLimit}` : null,
].filter(Boolean).join(',')}`]
: []),
];
if (resourceArgs.length > 6) {
logs.push(`[resources] ${workloadRef}`);
appendOutput(logs, await this.kubectl(resourceArgs));
}
logs.push(`[rollout] ${workloadRef}`); logs.push(`[rollout] ${workloadRef}`);
const rolloutOutput = await this.kubectl([ const rolloutOutput = await this.kubectl([
'rollout', 'rollout',
@@ -1136,7 +1152,7 @@ export class AppsService {
} }
} }
if (body.updateLauncherAppVersion !== false) { if (release && body.updateLauncherAppVersion !== false) {
try { try {
await this.launcherService.publishAppReleaseToLauncher(release.id); await this.launcherService.publishAppReleaseToLauncher(release.id);
logs.push('[launcher] app version updated'); logs.push('[launcher] app version updated');
@@ -1145,7 +1161,7 @@ export class AppsService {
logs.push(`[launcher] app version update failed: ${launcherMessage}`); logs.push(`[launcher] app version update failed: ${launcherMessage}`);
} }
} else { } else {
logs.push('[launcher] app version update skipped'); logs.push(quickBuild ? '[launcher] app version update skipped for quick deployment' : '[launcher] app version update skipped');
} }
logs.push('[done] deployment completed'); logs.push('[done] deployment completed');
@@ -1506,7 +1522,7 @@ export class AppsService {
{ kind: 'StatefulSet', command: 'statefulsets' }, { kind: 'StatefulSet', command: 'statefulsets' },
{ kind: 'Service', command: 'services' }, { kind: 'Service', command: 'services' },
{ kind: 'Ingress', command: 'ingress' }, { kind: 'Ingress', command: 'ingress' },
{ kind: 'PVC', command: 'pvc' }, { kind: 'PersistentVolumeClaim', command: 'pvc' },
]; ];
const resources = []; const resources = [];
@@ -1530,12 +1546,32 @@ export class AppsService {
deployments: resources.filter((resource) => resource.kind === 'Deployment').length, deployments: resources.filter((resource) => resource.kind === 'Deployment').length,
statefulSets: resources.filter((resource) => resource.kind === 'StatefulSet').length, statefulSets: resources.filter((resource) => resource.kind === 'StatefulSet').length,
services: resources.filter((resource) => resource.kind === 'Service').length, services: resources.filter((resource) => resource.kind === 'Service').length,
persistentVolumeClaims: resources.filter((resource) => resource.kind === 'PVC').length, persistentVolumeClaims: resources.filter((resource) => resource.kind === 'PersistentVolumeClaim').length,
}, },
resources, resources,
}; };
} }
async getRuntimeResourceYaml(namespace: string, kind: string, name: string) {
const normalizedNamespace = normalizeNamespace(namespace);
const resourceType = getKubectlResourceType(kind);
if (!resourceType) {
throw new BadRequestException('YAML 조회를 지원하지 않는 리소스 유형입니다.');
}
const yaml = await this.kubectl([
'get',
resourceType,
name,
'-n',
normalizedNamespace,
'-o',
'yaml',
]);
return { namespace: normalizedNamespace, kind, name, yaml };
}
async ensureRuntimeNamespace(body: EnsureRuntimeNamespaceDto) { async ensureRuntimeNamespace(body: EnsureRuntimeNamespaceDto) {
const namespace = normalizeNamespace(body.namespace); const namespace = normalizeNamespace(body.namespace);
const logs: string[] = []; const logs: string[] = [];
@@ -1634,13 +1670,33 @@ export class AppsService {
async deleteRuntimeProfile(profileId: string) { async deleteRuntimeProfile(profileId: string) {
const profile = await this.prisma.runtimeEnvProfile.findUnique({ const profile = await this.prisma.runtimeEnvProfile.findUnique({
where: { id: profileId }, where: { id: profileId },
select: { id: true, name: true }, include: {
variables: {
include: {
component: {
select: {
name: true,
type: true,
workloadType: true,
},
},
},
},
},
}); });
if (!profile) { if (!profile) {
throw new NotFoundException('실행 설정 프로필을 찾을 수 없습니다.'); throw new NotFoundException('실행 설정 프로필을 찾을 수 없습니다.');
} }
for (const variable of profile.variables) {
await this.removeRuntimeVariableFromWorkload(
normalizeNamespace(profile.namespace),
variable.component,
variable.key,
);
}
await this.prisma.runtimeEnvProfile.delete({ await this.prisma.runtimeEnvProfile.delete({
where: { id: profileId }, where: { id: profileId },
}); });
@@ -1743,13 +1799,32 @@ export class AppsService {
async deleteRuntimeVariable(profileId: string, variableId: string) { async deleteRuntimeVariable(profileId: string, variableId: string) {
const variable = await this.prisma.runtimeEnvVariable.findFirst({ const variable = await this.prisma.runtimeEnvVariable.findFirst({
where: { id: variableId, profileId }, where: { id: variableId, profileId },
select: { id: true }, include: {
profile: {
select: {
namespace: true,
},
},
component: {
select: {
name: true,
type: true,
workloadType: true,
},
},
},
}); });
if (!variable) { if (!variable) {
throw new NotFoundException('환경변수를 찾을 수 없습니다.'); throw new NotFoundException('환경변수를 찾을 수 없습니다.');
} }
await this.removeRuntimeVariableFromWorkload(
normalizeNamespace(variable.profile.namespace),
variable.component,
variable.key,
);
await this.prisma.runtimeEnvVariable.delete({ await this.prisma.runtimeEnvVariable.delete({
where: { id: variable.id }, where: { id: variable.id },
}); });
@@ -2103,6 +2178,32 @@ export class AppsService {
} }
} }
private async removeRuntimeVariableFromWorkload(
namespace: string,
component: { name: string; type: string; workloadType: string | null } | null,
key: string,
) {
if (!component || component.type === 'DATABASE') {
return;
}
const workloadType = normalizeWorkloadType(component.workloadType, component.type);
if (workloadType === 'Job') {
return;
}
const workloadRef = `${workloadType.toLowerCase()}/${sanitizeK8sName(component.name)}`;
try {
await this.kubectl(['set', 'env', workloadRef, `${key}-`, '-n', namespace]);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
if (/\bnot\s*found\b/i.test(message)) {
return;
}
throw error;
}
}
private async kubectlEnv() { private async kubectlEnv() {
const inClusterHost = process.env.KUBERNETES_SERVICE_HOST; const inClusterHost = process.env.KUBERNETES_SERVICE_HOST;
if (inClusterHost) { if (inClusterHost) {
@@ -2754,6 +2855,26 @@ function getNumber(value: unknown, fallback = 0) {
return typeof value === 'number' ? value : fallback; return typeof value === 'number' ? value : fallback;
} }
function getKubectlResourceType(kind: string) {
const resourceTypes: Record<string, string> = {
Deployment: 'deployment',
StatefulSet: 'statefulset',
DaemonSet: 'daemonset',
Pod: 'pod',
Job: 'job',
CronJob: 'cronjob',
ReplicaSet: 'replicaset',
Service: 'service',
Ingress: 'ingress',
PersistentVolumeClaim: 'pvc',
PVC: 'pvc',
Endpoint: 'endpoints',
Endpoints: 'endpoints',
};
return resourceTypes[kind];
}
function getAge(createdAt: string) { function getAge(createdAt: string) {
const created = new Date(createdAt).getTime(); const created = new Date(createdAt).getTime();
if (!created || Number.isNaN(created)) return '-'; if (!created || Number.isNaN(created)) return '-';
@@ -2774,6 +2895,12 @@ function createDeploymentManifest(input: {
image: string; image: string;
replicas?: number | null; replicas?: number | null;
ports?: ComponentPort[]; ports?: ComponentPort[];
resources?: {
cpuRequest?: string;
cpuLimit?: string;
memoryRequest?: string;
memoryLimit?: string;
};
}) { }) {
const labels = { const labels = {
'app.kubernetes.io/name': input.deploymentName, 'app.kubernetes.io/name': input.deploymentName,
@@ -2806,6 +2933,28 @@ function createDeploymentManifest(input: {
name: input.deploymentName, name: input.deploymentName,
image: input.image, image: input.image,
imagePullPolicy: 'IfNotPresent', imagePullPolicy: 'IfNotPresent',
...(input.resources?.cpuRequest || input.resources?.memoryRequest || input.resources?.cpuLimit || input.resources?.memoryLimit
? {
resources: {
...(input.resources.cpuRequest || input.resources.memoryRequest
? {
requests: {
...(input.resources.cpuRequest ? { cpu: input.resources.cpuRequest } : {}),
...(input.resources.memoryRequest ? { memory: input.resources.memoryRequest } : {}),
},
}
: {}),
...(input.resources.cpuLimit || input.resources.memoryLimit
? {
limits: {
...(input.resources.cpuLimit ? { cpu: input.resources.cpuLimit } : {}),
...(input.resources.memoryLimit ? { memory: input.resources.memoryLimit } : {}),
},
}
: {}),
},
}
: {}),
...(input.ports?.length ...(input.ports?.length
? { ? {
ports: input.ports.map((port) => ({ ports: input.ports.map((port) => ({
@@ -3253,6 +3402,16 @@ function parseReleaseImageTags(value: Prisma.JsonValue | null) {
return { images }; return { images };
} }
function hasReleaseArtifact(artifact: string | null, imageTags: Prisma.JsonValue | null) {
if (artifact?.trim()) return true;
if (!imageTags || typeof imageTags !== 'object' || Array.isArray(imageTags)) return false;
const artifacts = (imageTags as Record<string, unknown>).artifacts;
if (!artifacts || typeof artifacts !== 'object' || Array.isArray(artifacts)) return false;
return Object.values(artifacts).some((value) => typeof value === 'string' && value.trim());
}
function getUsedReleaseBuildIds(releases: Array<{ imageTags: Prisma.JsonValue | null }>) { function getUsedReleaseBuildIds(releases: Array<{ imageTags: Prisma.JsonValue | null }>) {
const ids = new Set<string>(); const ids = new Set<string>();
for (const release of releases) { for (const release of releases) {
@@ -3322,6 +3481,7 @@ function createReleaseImageTags(
branch: string; branch: string;
commitSha: string | null; commitSha: string | null;
artifact: string | null; artifact: string | null;
artifactSha256: string | null;
image: string | null; image: string | null;
component: { component: {
id: string; id: string;
@@ -3339,6 +3499,7 @@ function createReleaseImageTags(
): Prisma.InputJsonObject { ): Prisma.InputJsonObject {
const images: Record<string, string> = {}; const images: Record<string, string> = {};
const artifacts: Record<string, string> = {}; const artifacts: Record<string, string> = {};
const artifactChecksums: Record<string, string> = {};
for (const job of buildJobs) { for (const job of buildJobs) {
if (job.image && !images[job.component.name]) { if (job.image && !images[job.component.name]) {
@@ -3347,13 +3508,17 @@ function createReleaseImageTags(
if (job.artifact && !artifacts[job.component.name]) { if (job.artifact && !artifacts[job.component.name]) {
artifacts[job.component.name] = job.artifact; artifacts[job.component.name] = job.artifact;
} }
if (job.artifactSha256 && !artifactChecksums[job.component.name]) {
artifactChecksums[job.component.name] = job.artifactSha256;
}
} }
for (const component of components) { for (const component of components) {
const imageSource = normalizeImageSource(component.imageSource, component.type); const imageSource = normalizeImageSource(component.imageSource, component.type);
if ( if (
!images[component.name] !images[component.name]
&& (imageSource === 'EXTERNAL' || imageSource === 'DATABASE') && imageSource === 'EXTERNAL'
&& component.type !== 'DATABASE'
&& component.externalImage?.trim() && component.externalImage?.trim()
) { ) {
images[component.name] = component.externalImage.trim(); images[component.name] = component.externalImage.trim();
@@ -3363,6 +3528,7 @@ function createReleaseImageTags(
return { return {
images, images,
artifacts, artifacts,
artifactChecksums,
buildJobs: buildJobs.map((job) => ({ buildJobs: buildJobs.map((job) => ({
id: job.id, id: job.id,
componentId: job.component.id, componentId: job.component.id,
@@ -3372,6 +3538,7 @@ function createReleaseImageTags(
commitSha: job.commitSha, commitSha: job.commitSha,
image: job.image, image: job.image,
artifact: job.artifact, artifact: job.artifact,
artifactSha256: job.artifactSha256,
})), })),
}; };
} }
@@ -3461,7 +3628,3 @@ function getBuildPlanLogs(input: {
return lines; return lines;
} }
function delay(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
+26 -4
View File
@@ -300,6 +300,25 @@ export class BuildRunnerService implements OnModuleInit, OnModuleDestroy {
const contextPath = this.configService.get<string>('BUILD_CONTEXT_PATH', '.'); const contextPath = this.configService.get<string>('BUILD_CONTEXT_PATH', '.');
const contextDir = path.resolve(workDir, contextPath); const contextDir = path.resolve(workDir, contextPath);
const pushEnabled = this.configService.get<string>('BUILD_PUSH_ENABLED', 'true') !== 'false'; const pushEnabled = this.configService.get<string>('BUILD_PUSH_ENABLED', 'true') !== 'false';
const viteBuildVariables = job.component.buildVariables
.filter((variable) => !variable.isSecret && variable.key.trim().startsWith('VITE_'));
const buildArgs = job.component.buildVariables
.filter((variable) => !variable.isSecret && variable.key.trim())
.filter((variable) => !variable.key.trim().startsWith('VITE_'))
.flatMap((variable) => ['--build-arg', `${variable.key}=${variable.value}`]);
if (viteBuildVariables.length > 0) {
await fs.writeFile(
path.join(contextDir, '.env.production.local'),
`${viteBuildVariables.map((variable) => `${variable.key}=${JSON.stringify(variable.value)}`).join('\n')}\n`,
'utf8',
);
await append(`[image] Vite build env ${viteBuildVariables.map((variable) => variable.key).join(', ')}`);
}
if (buildArgs.length > 0) {
await append(`[image] build args ${job.component.buildVariables.filter((variable) => !variable.isSecret && variable.key.trim() && !variable.key.trim().startsWith('VITE_')).map((variable) => variable.key).join(', ')}`);
}
if (image !== job.image) { if (image !== job.image) {
await this.prisma.buildJob.update({ await this.prisma.buildJob.update({
@@ -316,7 +335,7 @@ export class BuildRunnerService implements OnModuleInit, OnModuleDestroy {
await runCommand({ await runCommand({
jobId: job.id, jobId: job.id,
command: this.configService.get<string>('NERDCTL_BIN', 'nerdctl'), command: this.configService.get<string>('NERDCTL_BIN', 'nerdctl'),
args: [...prefix, 'build', '-t', image, '-f', dockerfile, '.'], args: [...prefix, 'build', ...buildArgs, '-t', image, '-f', dockerfile, '.'],
cwd: contextDir, cwd: contextDir,
append, append,
onProcess: (child) => this.activeProcesses.set(job.id, child), onProcess: (child) => this.activeProcesses.set(job.id, child),
@@ -340,7 +359,7 @@ export class BuildRunnerService implements OnModuleInit, OnModuleDestroy {
await runCommand({ await runCommand({
jobId: job.id, jobId: job.id,
command: this.configService.get<string>('DOCKER_BIN', 'docker'), command: this.configService.get<string>('DOCKER_BIN', 'docker'),
args: ['build', '-t', image, '-f', dockerfile, '.'], args: ['build', ...buildArgs, '-t', image, '-f', dockerfile, '.'],
cwd: contextDir, cwd: contextDir,
append, append,
onProcess: (child) => this.activeProcesses.set(job.id, child), onProcess: (child) => this.activeProcesses.set(job.id, child),
@@ -428,11 +447,13 @@ export class BuildRunnerService implements OnModuleInit, OnModuleDestroy {
const destination = path.join(artifactDir, destinationName); const destination = path.join(artifactDir, destinationName);
await fs.copyFile(artifact, destination); await fs.copyFile(artifact, destination);
const artifactSha256 = await sha256File(artifact);
const uploadedArtifact = await this.uploadArtifactIfConfigured( const uploadedArtifact = await this.uploadArtifactIfConfigured(
job, job,
destination, destination,
destinationName, destinationName,
artifactVersion, artifactVersion,
artifactSha256,
append, append,
); );
const publicBaseUrl = this.configService.get<string>('BUILD_ARTIFACT_BASE_URL'); const publicBaseUrl = this.configService.get<string>('BUILD_ARTIFACT_BASE_URL');
@@ -443,9 +464,10 @@ export class BuildRunnerService implements OnModuleInit, OnModuleDestroy {
await this.prisma.buildJob.update({ await this.prisma.buildJob.update({
where: { id: job.id }, where: { id: job.id },
data: { artifact: artifactValue }, data: { artifact: artifactValue, artifactSha256 },
}); });
job.artifact = artifactValue; job.artifact = artifactValue;
job.artifactSha256 = artifactSha256;
await append(`[artifact] found ${path.relative(workDir, artifact)}`); await append(`[artifact] found ${path.relative(workDir, artifact)}`);
await append(`[artifact] copied to ${artifactValue}`); await append(`[artifact] copied to ${artifactValue}`);
@@ -456,6 +478,7 @@ export class BuildRunnerService implements OnModuleInit, OnModuleDestroy {
artifactPath: string, artifactPath: string,
artifactName: string, artifactName: string,
artifactVersion: string | null, artifactVersion: string | null,
sha256: string,
append: (line: string) => Promise<void>, append: (line: string) => Promise<void>,
) { ) {
const bucket = this.configService.get<string>('R2_BUCKET'); const bucket = this.configService.get<string>('R2_BUCKET');
@@ -469,7 +492,6 @@ export class BuildRunnerService implements OnModuleInit, OnModuleDestroy {
const key = this.createArtifactObjectKey(job, artifactName, artifactVersion); const key = this.createArtifactObjectKey(job, artifactName, artifactVersion);
const stat = await fs.stat(artifactPath); const stat = await fs.stat(artifactPath);
const sha256 = await sha256File(artifactPath);
const client = this.getR2Client(accessKeyId, secretAccessKey); const client = this.getR2Client(accessKeyId, secretAccessKey);
await append(`[r2] upload ${key}`); await append(`[r2] upload ${key}`);
+36 -2
View File
@@ -1,4 +1,5 @@
import { ArrayNotEmpty, IsArray, IsBoolean, IsEnum, IsInt, IsOptional, IsString, IsUUID, Min } from 'class-validator'; import { Type } from 'class-transformer';
import { ArrayNotEmpty, IsArray, IsBoolean, IsEnum, IsInt, IsOptional, IsString, IsUUID, Min, ValidateNested } from 'class-validator';
import { AppStatus, ComponentType } from './app-enums'; import { AppStatus, ComponentType } from './app-enums';
export class CreateAppDto { export class CreateAppDto {
@@ -195,9 +196,37 @@ export class CreateAppReleaseDto {
buildJobIds!: string[]; buildJobIds!: string[];
} }
export class DeploymentResourceLimitsDto {
@IsOptional()
@Type(() => Number)
@IsInt()
replicas?: number;
@IsOptional()
@IsString()
cpuRequest?: string;
@IsOptional()
@IsString()
cpuLimit?: string;
@IsOptional()
@IsString()
memoryRequest?: string;
@IsOptional()
@IsString()
memoryLimit?: string;
}
export class CreateDeploymentJobDto { export class CreateDeploymentJobDto {
@IsOptional()
@IsUUID() @IsUUID()
releaseId!: string; releaseId?: string;
@IsOptional()
@IsUUID()
buildJobId?: string;
@IsString() @IsString()
environment!: string; environment!: string;
@@ -208,6 +237,11 @@ export class CreateDeploymentJobDto {
@IsOptional() @IsOptional()
@IsBoolean() @IsBoolean()
updateLauncherAppVersion?: boolean; updateLauncherAppVersion?: boolean;
@IsOptional()
@ValidateNested()
@Type(() => DeploymentResourceLimitsDto)
resources?: DeploymentResourceLimitsDto;
} }
export class CreateRuntimeEnvProfileDto { export class CreateRuntimeEnvProfileDto {
-1
View File
@@ -1 +0,0 @@
export * from "./index"
-5
View File
@@ -1,5 +0,0 @@
/* !!! This is code generated by Prisma. Do not edit directly. !!!
/* eslint-disable */
// biome-ignore-all lint: generated file
module.exports = { ...require('.') }
-1
View File
@@ -1 +0,0 @@
export * from "./index"
-5
View File
@@ -1,5 +0,0 @@
/* !!! 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
View File
@@ -1 +0,0 @@
export * from "./default"
File diff suppressed because one or more lines are too long
-217
View File
@@ -1,217 +0,0 @@
/* !!! 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)
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
-183
View File
@@ -1,183 +0,0 @@
{
"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
-370
View File
@@ -1,370 +0,0 @@
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
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
-66
View File
@@ -1,66 +0,0 @@
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")
}
@@ -1,5 +0,0 @@
/* !!! 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')
@@ -1,5 +0,0 @@
/* !!! 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')
-1
View File
@@ -1 +0,0 @@
export * from "./default"
-238
View File
@@ -1,238 +0,0 @@
/* !!! 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
View File
@@ -1 +0,0 @@
export * from "./index"
-5
View File
@@ -1,5 +0,0 @@
/* !!! This is code generated by Prisma. Do not edit directly. !!!
/* eslint-disable */
// biome-ignore-all lint: generated file
module.exports = { ...require('.') }
-1
View File
@@ -1 +0,0 @@
export * from "./index"
-5
View File
@@ -1,5 +0,0 @@
/* !!! 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
View File
@@ -1 +0,0 @@
export * from "./default"
File diff suppressed because one or more lines are too long
@@ -1,233 +0,0 @@
/* !!! 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)
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
-183
View File
@@ -1,183 +0,0 @@
{
"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
-370
View File
@@ -1,370 +0,0 @@
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
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
@@ -1,68 +0,0 @@
generator launcherClient {
provider = "prisma-client-js"
output = "../src/generated/launcher-client"
}
datasource db {
provider = "postgresql"
url = env("LAUNCHER_DATABASE_URL")
}
model LauncherNotice {
id String @id @default(uuid()) @db.Uuid
appKey String @default("login") @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")
}
@@ -1,5 +0,0 @@
/* !!! 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')
@@ -1,5 +0,0 @@
/* !!! 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')
-1
View File
@@ -1 +0,0 @@
export * from "./default"
-254
View File
@@ -1,254 +0,0 @@
/* !!! 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.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'
};
/**
* Create the Client
*/
const config = {
"generator": {
"name": "launcherClient",
"provider": {
"fromEnvVar": null,
"value": "prisma-client-js"
},
"output": {
"value": "/home/chyoo/tmp/atnmy-stdo-v2/atnmy-stdo-adm-v2-be/src/generated/launcher-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/launcher.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": "LAUNCHER_DATABASE_URL",
"value": null
}
}
},
"inlineSchema": "generator launcherClient {\n provider = \"prisma-client-js\"\n output = \"../src/generated/launcher-client\"\n}\n\ndatasource db {\n provider = \"postgresql\"\n url = env(\"LAUNCHER_DATABASE_URL\")\n}\n\nmodel LauncherNotice {\n id String @id @default(uuid()) @db.Uuid\n appKey String @default(\"login\") @map(\"app_key\")\n title String\n body String\n linkUrl String? @map(\"link_url\")\n isActive Boolean @default(true) @map(\"is_active\")\n startsAt DateTime? @map(\"starts_at\") @db.Timestamptz(3)\n endsAt DateTime? @map(\"ends_at\") @db.Timestamptz(3)\n priority Int @default(0)\n createdAt DateTime @default(now()) @map(\"created_at\") @db.Timestamptz(3)\n updatedAt DateTime @updatedAt @map(\"updated_at\") @db.Timestamptz(3)\n\n @@index([appKey, isActive, priority])\n @@map(\"launcher_notices\")\n}\n\nmodel ProvisionedDevice {\n id String @id @default(uuid()) @db.Uuid\n accountEmail String @map(\"account_email\")\n authUserId String? @map(\"auth_user_id\") @db.Uuid\n hostname String\n tailscaleHostname String @map(\"tailscale_hostname\")\n sshUser String? @map(\"ssh_user\")\n workspaceRoot String? @map(\"workspace_root\")\n usageType String @default(\"personal\") @map(\"usage_type\")\n os String?\n arch String?\n kernel String?\n distro String?\n device Json?\n installedAt DateTime @default(now()) @map(\"installed_at\") @db.Timestamptz(3)\n lastProvisionedAt DateTime @default(now()) @map(\"last_provisioned_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\n @@unique([accountEmail, tailscaleHostname])\n @@index([accountEmail])\n @@index([tailscaleHostname])\n @@map(\"provisioned_devices\")\n}\n\nmodel LauncherSshKey {\n id String @id @default(uuid()) @db.Uuid\n accountEmail String @map(\"account_email\")\n authUserId String? @map(\"auth_user_id\") @db.Uuid\n publicKey String @map(\"public_key\")\n publicKeyHash String @map(\"public_key_hash\")\n keyType String? @map(\"key_type\")\n label String?\n lastProvisionedAt DateTime @default(now()) @map(\"last_provisioned_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\n @@unique([accountEmail, publicKeyHash])\n @@index([accountEmail])\n @@map(\"launcher_ssh_keys\")\n}\n",
"inlineSchemaHash": "ad20160732d203c5e1ea82cbcac0488ce94e47432c3136a3833df4661c0a049b",
"copyEngine": true
}
config.dirname = '/'
config.runtimeDataModel = JSON.parse("{\"models\":{\"LauncherNotice\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"appKey\",\"kind\":\"scalar\",\"type\":\"String\",\"dbName\":\"app_key\"},{\"name\":\"title\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"body\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"linkUrl\",\"kind\":\"scalar\",\"type\":\"String\",\"dbName\":\"link_url\"},{\"name\":\"isActive\",\"kind\":\"scalar\",\"type\":\"Boolean\",\"dbName\":\"is_active\"},{\"name\":\"startsAt\",\"kind\":\"scalar\",\"type\":\"DateTime\",\"dbName\":\"starts_at\"},{\"name\":\"endsAt\",\"kind\":\"scalar\",\"type\":\"DateTime\",\"dbName\":\"ends_at\"},{\"name\":\"priority\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\",\"dbName\":\"created_at\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\",\"dbName\":\"updated_at\"}],\"dbName\":\"launcher_notices\"},\"ProvisionedDevice\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"accountEmail\",\"kind\":\"scalar\",\"type\":\"String\",\"dbName\":\"account_email\"},{\"name\":\"authUserId\",\"kind\":\"scalar\",\"type\":\"String\",\"dbName\":\"auth_user_id\"},{\"name\":\"hostname\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"tailscaleHostname\",\"kind\":\"scalar\",\"type\":\"String\",\"dbName\":\"tailscale_hostname\"},{\"name\":\"sshUser\",\"kind\":\"scalar\",\"type\":\"String\",\"dbName\":\"ssh_user\"},{\"name\":\"workspaceRoot\",\"kind\":\"scalar\",\"type\":\"String\",\"dbName\":\"workspace_root\"},{\"name\":\"usageType\",\"kind\":\"scalar\",\"type\":\"String\",\"dbName\":\"usage_type\"},{\"name\":\"os\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"arch\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"kernel\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"distro\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"device\",\"kind\":\"scalar\",\"type\":\"Json\"},{\"name\":\"installedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\",\"dbName\":\"installed_at\"},{\"name\":\"lastProvisionedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\",\"dbName\":\"last_provisioned_at\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\",\"dbName\":\"created_at\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\",\"dbName\":\"updated_at\"}],\"dbName\":\"provisioned_devices\"},\"LauncherSshKey\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"accountEmail\",\"kind\":\"scalar\",\"type\":\"String\",\"dbName\":\"account_email\"},{\"name\":\"authUserId\",\"kind\":\"scalar\",\"type\":\"String\",\"dbName\":\"auth_user_id\"},{\"name\":\"publicKey\",\"kind\":\"scalar\",\"type\":\"String\",\"dbName\":\"public_key\"},{\"name\":\"publicKeyHash\",\"kind\":\"scalar\",\"type\":\"String\",\"dbName\":\"public_key_hash\"},{\"name\":\"keyType\",\"kind\":\"scalar\",\"type\":\"String\",\"dbName\":\"key_type\"},{\"name\":\"label\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"lastProvisionedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\",\"dbName\":\"last_provisioned_at\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\",\"dbName\":\"created_at\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\",\"dbName\":\"updated_at\"}],\"dbName\":\"launcher_ssh_keys\"}},\"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: {
LAUNCHER_DATABASE_URL: typeof globalThis !== 'undefined' && globalThis['LAUNCHER_DATABASE_URL'] || typeof process !== 'undefined' && process.env && process.env.LAUNCHER_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)
+5
View File
@@ -1,4 +1,5 @@
import { import {
BadRequestException,
BadGatewayException, BadGatewayException,
Injectable, Injectable,
UnauthorizedException, UnauthorizedException,
@@ -118,6 +119,10 @@ export class GiteaService {
params.set('page', String(page)); params.set('page', String(page));
if (query.sha) { if (query.sha) {
params.set('sha', query.sha); params.set('sha', query.sha);
const { branches } = await this.listBranches(owner, repo) as { branches: Array<{ name?: string }> };
if (!branches.some((branch) => branch.name === query.sha)) {
throw new BadRequestException('해당 브랜치가 없습니다.');
}
} }
if (query.since) { if (query.since) {
params.set('since', query.since); params.set('since', query.since);
+13 -5
View File
@@ -1,4 +1,4 @@
import { Body, Controller, Delete, Get, Param, Patch, Post, Put, Query } from '@nestjs/common'; import { Body, Controller, Delete, Get, Param, Patch, Post, Put, Query, Req } from '@nestjs/common';
import { AssignLicenseDto, ListUserLicensesQuery, UpdateLicenseAppPermissionsDto, UpdateLicensePlanDto } from './dto'; import { AssignLicenseDto, ListUserLicensesQuery, UpdateLicenseAppPermissionsDto, UpdateLicensePlanDto } from './dto';
import { LicensesService } from './licenses.service'; import { LicensesService } from './licenses.service';
@@ -38,13 +38,21 @@ export class LicensesController {
} }
@Post('users/:userId') @Post('users/:userId')
assign(@Param('userId') userId: string, @Body() body: AssignLicenseDto) { assign(
return this.licensesService.assign(userId, body); @Param('userId') userId: string,
@Body() body: AssignLicenseDto,
@Req() request: { admin?: { id: string } },
) {
return this.licensesService.assign(userId, body, request.admin?.id);
} }
@Patch('user-licenses/:id') @Patch('user-licenses/:id')
update(@Param('id') id: string, @Body() body: AssignLicenseDto) { update(
return this.licensesService.update(id, body); @Param('id') id: string,
@Body() body: AssignLicenseDto,
@Req() request: { admin?: { id: string } },
) {
return this.licensesService.update(id, body, request.admin?.id);
} }
@Delete('user-licenses/:id') @Delete('user-licenses/:id')
+2 -1
View File
@@ -2,11 +2,12 @@ import { Module } from '@nestjs/common';
import { AuthPrismaModule } from '../auth-prisma/auth-prisma.module'; import { AuthPrismaModule } from '../auth-prisma/auth-prisma.module';
import { LauncherPrismaModule } from '../launcher-prisma/launcher-prisma.module'; import { LauncherPrismaModule } from '../launcher-prisma/launcher-prisma.module';
import { PrismaModule } from '../prisma/prisma.module';
import { LicensesController } from './licenses.controller'; import { LicensesController } from './licenses.controller';
import { LicensesService } from './licenses.service'; import { LicensesService } from './licenses.service';
@Module({ @Module({
imports: [AuthPrismaModule, LauncherPrismaModule], imports: [AuthPrismaModule, LauncherPrismaModule, PrismaModule],
controllers: [LicensesController], controllers: [LicensesController],
providers: [LicensesService], providers: [LicensesService],
}) })
+18 -5
View File
@@ -2,6 +2,7 @@ import { BadRequestException, ConflictException, Injectable, NotFoundException }
import { AuthPrismaService } from '../auth-prisma/auth-prisma.service'; import { AuthPrismaService } from '../auth-prisma/auth-prisma.service';
import { LauncherPrismaService } from '../launcher-prisma/launcher-prisma.service'; import { LauncherPrismaService } from '../launcher-prisma/launcher-prisma.service';
import { PrismaService } from '../prisma/prisma.service';
import { AssignLicenseDto, ListUserLicensesQuery, UpdateLicenseAppPermissionsDto, UpdateLicensePlanDto } from './dto'; import { AssignLicenseDto, ListUserLicensesQuery, UpdateLicenseAppPermissionsDto, UpdateLicensePlanDto } from './dto';
type LauncherAppPermissionRow = { type LauncherAppPermissionRow = {
@@ -18,6 +19,7 @@ export class LicensesService {
constructor( constructor(
private readonly authPrisma: AuthPrismaService, private readonly authPrisma: AuthPrismaService,
private readonly launcherPrisma: LauncherPrismaService, private readonly launcherPrisma: LauncherPrismaService,
private readonly prisma: PrismaService,
) {} ) {}
async listPlans() { async listPlans() {
@@ -141,6 +143,15 @@ export class LicensesService {
this.authPrisma.userLicense.count({ where }), this.authPrisma.userLicense.count({ where }),
]); ]);
const assignedByIds = [...new Set(items.flatMap((item) => item.assignedById ? [item.assignedById] : []))];
const admins = assignedByIds.length
? await this.prisma.admin.findMany({
where: { id: { in: assignedByIds } },
select: { id: true, name: true },
})
: [];
const assignedByName = new Map(admins.map((admin) => [admin.id, admin.name]));
return { return {
assignments: items.map((userLicense, index) => ({ assignments: items.map((userLicense, index) => ({
id: userLicense.id, id: userLicense.id,
@@ -153,7 +164,9 @@ export class LicensesService {
licenseName: userLicense.license.name, licenseName: userLicense.license.name,
startsAt: userLicense.startsAt, startsAt: userLicense.startsAt,
expiresAt: userLicense.expiresAt, expiresAt: userLicense.expiresAt,
assignedBy: userLicense.assignedById, assignedBy: userLicense.assignedById
? assignedByName.get(userLicense.assignedById) ?? '-'
: '-',
status: isActiveLicense(userLicense.startsAt, userLicense.expiresAt, now) ? '활성' : '만료', status: isActiveLicense(userLicense.startsAt, userLicense.expiresAt, now) ? '활성' : '만료',
})), })),
total, total,
@@ -279,7 +292,7 @@ export class LicensesService {
return userIds.length; return userIds.length;
} }
async assign(userId: string, body: AssignLicenseDto) { async assign(userId: string, body: AssignLicenseDto, assignedById?: string) {
const user = await this.authPrisma.user.findUnique({ const user = await this.authPrisma.user.findUnique({
where: { id: userId }, where: { id: userId },
select: { id: true }, select: { id: true },
@@ -321,7 +334,7 @@ export class LicensesService {
licenseId: license.id, licenseId: license.id,
startsAt, startsAt,
expiresAt, expiresAt,
assignedById: body.assignedById, assignedById,
}, },
include: { license: true, user: true }, include: { license: true, user: true },
}); });
@@ -331,7 +344,7 @@ export class LicensesService {
return { userLicense }; return { userLicense };
} }
async update(id: string, body: AssignLicenseDto) { async update(id: string, body: AssignLicenseDto, assignedById?: string) {
const current = await this.authPrisma.userLicense.findUnique({ const current = await this.authPrisma.userLicense.findUnique({
where: { id }, where: { id },
select: { id: true }, select: { id: true },
@@ -360,7 +373,7 @@ export class LicensesService {
licenseId: license.id, licenseId: license.id,
startsAt, startsAt,
expiresAt, expiresAt,
assignedById: body.assignedById, assignedById,
}, },
include: { license: true, user: true }, include: { license: true, user: true },
}); });
@@ -1,4 +1,5 @@
import { ConflictException, Injectable, NotFoundException } from '@nestjs/common'; import { ConflictException, Injectable, NotFoundException } from '@nestjs/common';
import { randomBytes } from 'crypto';
import { ConfigService } from '@nestjs/config'; import { ConfigService } from '@nestjs/config';
import { Prisma } from '@prisma/auth-client'; import { Prisma } from '@prisma/auth-client';
@@ -48,6 +49,7 @@ export class OrganizationsService {
type: true, type: true,
domain: true, domain: true,
defaultLicenseCode: true, defaultLicenseCode: true,
instructorInviteCode: true,
createdById: true, createdById: true,
createdByName: true, createdByName: true,
createdByEmail: true, createdByEmail: true,
@@ -76,11 +78,13 @@ export class OrganizationsService {
async create(body: CreateOrganizationDto, requester?: AdminRequester) { async create(body: CreateOrganizationDto, requester?: AdminRequester) {
try { try {
const instructorInviteCode = await this.createInstructorInviteCode();
const organization = await this.authPrisma.organization.create({ const organization = await this.authPrisma.organization.create({
data: { data: {
name: body.name.trim(), name: body.name.trim(),
type: body.type, type: body.type,
domain: body.domain?.trim() || null, domain: body.domain?.trim() || null,
instructorInviteCode,
...(requester ...(requester
? { ? {
createdById: requester.id, createdById: requester.id,
@@ -95,6 +99,7 @@ export class OrganizationsService {
type: true, type: true,
domain: true, domain: true,
defaultLicenseCode: true, defaultLicenseCode: true,
instructorInviteCode: true,
createdById: true, createdById: true,
createdByName: true, createdByName: true,
createdByEmail: true, createdByEmail: true,
@@ -135,6 +140,7 @@ export class OrganizationsService {
type: true, type: true,
domain: true, domain: true,
defaultLicenseCode: true, defaultLicenseCode: true,
instructorInviteCode: true,
createdById: true, createdById: true,
createdByName: true, createdByName: true,
createdByEmail: true, createdByEmail: true,
@@ -262,6 +268,21 @@ export class OrganizationsService {
updatedUserCount: license ? targetUserIds.length : 0, updatedUserCount: license ? targetUserIds.length : 0,
}; };
} }
private async createInstructorInviteCode(): Promise<string> {
const alphabet = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
for (let attempt = 0; attempt < 10; attempt += 1) {
const bytes = randomBytes(6);
const code = Array.from(bytes, (byte) => alphabet[byte % alphabet.length]).join('');
const existing = await this.authPrisma.organization.findUnique({
where: { instructorInviteCode: code },
select: { id: true },
});
if (!existing) return code;
}
throw new ConflictException('강사 가입 코드 생성에 실패했습니다. 다시 시도해주세요.');
}
} }
function toPositiveInt(value: string | number | undefined, fallback: number) { function toPositiveInt(value: string | number | undefined, fallback: number) {
+6 -1
View File
@@ -1,4 +1,4 @@
import { Controller, Get } from '@nestjs/common'; import { Controller, Delete, Get, Param } from '@nestjs/common';
import { TailscaleService } from './tailscale.service'; import { TailscaleService } from './tailscale.service';
@@ -11,6 +11,11 @@ export class TailscaleController {
return this.tailscaleService.listNodes(); return this.tailscaleService.listNodes();
} }
@Delete('nodes/:nodeId')
deleteNode(@Param('nodeId') nodeId: string) {
return this.tailscaleService.deleteNode(nodeId);
}
@Get('routes') @Get('routes')
listRoutes() { listRoutes() {
return this.tailscaleService.listRoutes(); return this.tailscaleService.listRoutes();
+19 -1
View File
@@ -63,6 +63,22 @@ export class TailscaleService {
} }
} }
async deleteNode(nodeId: string) {
const normalizedNodeId = nodeId.trim();
if (!normalizedNodeId) {
throw new BadGatewayException('삭제할 Headscale 노드 ID가 없습니다.');
}
if (!this.gatewayUrl || !this.gatewayToken) {
throw new BadGatewayException('PLATFORM_GATEWAY_URL 또는 PLATFORM_GATEWAY_TOKEN이 설정되어 있지 않습니다.');
}
await this.request<{ deleted?: boolean }>(`/api/admin/tailscale/nodes/${encodeURIComponent(normalizedNodeId)}`, {
method: 'DELETE',
});
return { deleted: true, nodeId: normalizedNodeId };
}
async listRoutes() { async listRoutes() {
if (!this.gatewayUrl || !this.gatewayToken) { if (!this.gatewayUrl || !this.gatewayToken) {
return { return {
@@ -118,11 +134,13 @@ export class TailscaleService {
} }
} }
private async request<T>(path: string): Promise<T> { private async request<T>(path: string, init?: RequestInit): Promise<T> {
const response = await fetch(`${this.gatewayUrl}${path}`, { const response = await fetch(`${this.gatewayUrl}${path}`, {
...init,
headers: { headers: {
Accept: 'application/json', Accept: 'application/json',
'x-platform-gateway-token': this.gatewayToken ?? '', 'x-platform-gateway-token': this.gatewayToken ?? '',
...(init?.headers ?? {}),
}, },
}); });
+6 -1
View File
@@ -1,4 +1,4 @@
import { IsEmail, IsIn, IsOptional, IsString, IsUUID, MinLength } from 'class-validator'; import { IsBoolean, IsEmail, IsIn, IsOptional, IsString, IsUUID, MinLength } from 'class-validator';
export class ListUserSessionsQuery { export class ListUserSessionsQuery {
@IsOptional() @IsOptional()
@@ -45,3 +45,8 @@ export class CreateManagedUserDto {
@IsIn(['FREE', 'EDU', 'PRO']) @IsIn(['FREE', 'EDU', 'PRO'])
licenseCode?: 'FREE' | 'EDU' | 'PRO'; licenseCode?: 'FREE' | 'EDU' | 'PRO';
} }
export class UpdateLmsInstructorDto {
@IsBoolean()
isInstructor!: boolean;
}
+9 -1
View File
@@ -1,6 +1,6 @@
import { Body, Controller, Delete, Get, Param, Patch, Post, Query } from '@nestjs/common'; import { Body, Controller, Delete, Get, Param, Patch, Post, Query } from '@nestjs/common';
import { CreateManagedUserDto, ListUserSessionsQuery } from './dto'; import { CreateManagedUserDto, ListUserSessionsQuery, UpdateLmsInstructorDto } from './dto';
import { UsersService } from './users.service'; import { UsersService } from './users.service';
@Controller('users') @Controller('users')
@@ -27,6 +27,14 @@ export class UsersController {
return this.usersService.unlockUser(userId); return this.usersService.unlockUser(userId);
} }
@Patch(':userId/lms-instructor')
updateLmsInstructor(
@Param('userId') userId: string,
@Body() body: UpdateLmsInstructorDto,
) {
return this.usersService.updateLmsInstructor(userId, body.isInstructor);
}
@Delete('sessions/:sessionId') @Delete('sessions/:sessionId')
terminateSession(@Param('sessionId') sessionId: string) { terminateSession(@Param('sessionId') sessionId: string) {
return this.usersService.terminateSession(sessionId); return this.usersService.terminateSession(sessionId);
+50
View File
@@ -22,6 +22,7 @@ type UserSessionRow = {
expiresAt: Date | null; expiresAt: Date | null;
lastSeen: Date | null; lastSeen: Date | null;
state: string; state: string;
isLmsInstructor: boolean;
primaryAffiliation: { primaryAffiliation: {
id: string; id: string;
organizationId: string; organizationId: string;
@@ -82,6 +83,13 @@ export class UsersService {
}, },
}, },
}, },
appRoles: {
where: {
appKey: 'LMS',
roleCode: 'INSTRUCTOR',
},
select: { id: true },
},
}, },
}); });
@@ -104,6 +112,7 @@ export class UsersService {
position: user.affiliations[0].position, position: user.affiliations[0].position,
} }
: null; : null;
const isLmsInstructor = user.appRoles.length > 0;
if (user.refreshTokens.length === 0) { if (user.refreshTokens.length === 0) {
return [ return [
@@ -120,6 +129,7 @@ export class UsersService {
expiresAt: null, expiresAt: null,
lastSeen: user.lastLoginAt, lastSeen: user.lastLoginAt,
state: user.isActive ? '세션 없음' : '잠김', state: user.isActive ? '세션 없음' : '잠김',
isLmsInstructor,
primaryAffiliation, primaryAffiliation,
}, },
]; ];
@@ -138,6 +148,7 @@ export class UsersService {
expiresAt: token.expiresAt, expiresAt: token.expiresAt,
lastSeen: user.lastLoginAt ?? token.createdAt, lastSeen: user.lastLoginAt ?? token.createdAt,
state: getSessionState(user.isActive, token.revokedAt, token.expiresAt, now), state: getSessionState(user.isActive, token.revokedAt, token.expiresAt, now),
isLmsInstructor,
primaryAffiliation, primaryAffiliation,
})); }));
}); });
@@ -260,6 +271,45 @@ export class UsersService {
return { ok: true }; return { ok: true };
} }
async updateLmsInstructor(userId: string, isInstructor: boolean) {
const user = await this.authPrisma.user.findUnique({
where: { id: userId },
select: { id: true },
});
if (!user) {
throw new NotFoundException('사용자를 찾을 수 없습니다.');
}
if (isInstructor) {
await this.authPrisma.userAppRole.upsert({
where: {
userId_appKey_roleCode: {
userId,
appKey: 'LMS',
roleCode: 'INSTRUCTOR',
},
},
create: {
userId,
appKey: 'LMS',
roleCode: 'INSTRUCTOR',
},
update: { endsAt: null },
});
} else {
await this.authPrisma.userAppRole.deleteMany({
where: {
userId,
appKey: 'LMS',
roleCode: 'INSTRUCTOR',
},
});
}
return { userId, isInstructor };
}
async terminateSession(sessionId: string) { async terminateSession(sessionId: string) {
const session = await this.authPrisma.refreshToken.findUnique({ const session = await this.authPrisma.refreshToken.findUnique({
where: { id: sessionId }, where: { id: sessionId },