@kurly-growth/growthman 0.1.14 → 0.1.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/app/api/endpoints/[id]/route.ts +63 -0
- package/app/api/endpoints/bulk/route.ts +23 -0
- package/app/api/endpoints/import/route.ts +62 -0
- package/app/api/endpoints/route.ts +39 -0
- package/app/api/endpoints/sync/route.ts +89 -0
- package/app/api/mock/[...slug]/route.ts +82 -0
- package/lib/openapi-parser.ts +270 -0
- package/lib/prisma.ts +9 -0
- package/lib/utils.ts +6 -0
- package/lib/validation.ts +19 -0
- package/next-env.d.ts +1 -1
- package/next.config.ts +7 -0
- package/package.json +1 -1
- package/prisma/seed.ts +29 -0
- package/tsconfig.json +34 -0
- package/types/endpoint.ts +10 -0
- package/prisma/prisma/dev.db +0 -0
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server'
|
|
2
|
+
import { prisma } from '@/lib/prisma'
|
|
3
|
+
import { validatePath, validateMethod, validateStatusCode } from '@/lib/validation'
|
|
4
|
+
|
|
5
|
+
type RouteParams = {
|
|
6
|
+
params: Promise<{ id: string }>
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
// GET: 단일 엔드포인트 조회
|
|
10
|
+
export async function GET(_request: NextRequest, { params }: RouteParams) {
|
|
11
|
+
const { id } = await params
|
|
12
|
+
|
|
13
|
+
const endpoint = await prisma.mockEndpoint.findUnique({
|
|
14
|
+
where: { id },
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
if (!endpoint) {
|
|
18
|
+
return NextResponse.json({ error: 'Endpoint not found' }, { status: 404 })
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return NextResponse.json(endpoint)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// PUT: 엔드포인트 수정
|
|
25
|
+
export async function PUT(request: NextRequest, { params }: RouteParams) {
|
|
26
|
+
const { id } = await params
|
|
27
|
+
const body = await request.json()
|
|
28
|
+
const { path, method, description, statusCode, responseBody } = body
|
|
29
|
+
|
|
30
|
+
if (path !== undefined && !validatePath(path)) {
|
|
31
|
+
return NextResponse.json({ error: 'Invalid path' }, { status: 400 })
|
|
32
|
+
}
|
|
33
|
+
if (method !== undefined && !validateMethod(method)) {
|
|
34
|
+
return NextResponse.json({ error: 'Invalid method' }, { status: 400 })
|
|
35
|
+
}
|
|
36
|
+
if (!validateStatusCode(statusCode)) {
|
|
37
|
+
return NextResponse.json({ error: 'Invalid status code' }, { status: 400 })
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const endpoint = await prisma.mockEndpoint.update({
|
|
41
|
+
where: { id },
|
|
42
|
+
data: {
|
|
43
|
+
path,
|
|
44
|
+
method: method?.toUpperCase(),
|
|
45
|
+
description,
|
|
46
|
+
statusCode,
|
|
47
|
+
responseBody,
|
|
48
|
+
},
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
return NextResponse.json(endpoint)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// DELETE: 엔드포인트 삭제
|
|
55
|
+
export async function DELETE(_request: NextRequest, { params }: RouteParams) {
|
|
56
|
+
const { id } = await params
|
|
57
|
+
|
|
58
|
+
await prisma.mockEndpoint.delete({
|
|
59
|
+
where: { id },
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
return NextResponse.json({ success: true })
|
|
63
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server'
|
|
2
|
+
import { prisma } from '@/lib/prisma'
|
|
3
|
+
|
|
4
|
+
// DELETE: 여러 엔드포인트 일괄 삭제
|
|
5
|
+
export async function DELETE(request: NextRequest) {
|
|
6
|
+
const body = await request.json()
|
|
7
|
+
const { ids } = body
|
|
8
|
+
|
|
9
|
+
if (!Array.isArray(ids) || ids.length === 0) {
|
|
10
|
+
return NextResponse.json(
|
|
11
|
+
{ error: 'ids must be a non-empty array' },
|
|
12
|
+
{ status: 400 }
|
|
13
|
+
)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const result = await prisma.mockEndpoint.deleteMany({
|
|
17
|
+
where: {
|
|
18
|
+
id: { in: ids },
|
|
19
|
+
},
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
return NextResponse.json({ deleted: result.count })
|
|
23
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server'
|
|
2
|
+
import { Prisma } from '@/generated/prisma'
|
|
3
|
+
import { prisma } from '@/lib/prisma'
|
|
4
|
+
import { parseOpenAPISpec } from '@/lib/openapi-parser'
|
|
5
|
+
|
|
6
|
+
export async function POST(request: NextRequest) {
|
|
7
|
+
const body = await request.json()
|
|
8
|
+
const { spec, pathPrefix } = body
|
|
9
|
+
|
|
10
|
+
// Handle both old format (spec directly) and new format ({ spec, pathPrefix })
|
|
11
|
+
const actualSpec = spec ?? body
|
|
12
|
+
const endpoints = parseOpenAPISpec(actualSpec)
|
|
13
|
+
|
|
14
|
+
// Apply path prefix if provided
|
|
15
|
+
const normalizedPrefix = pathPrefix ? pathPrefix.replace(/\/+$/, '') : ''
|
|
16
|
+
const processedEndpoints = endpoints.map((ep) => ({
|
|
17
|
+
...ep,
|
|
18
|
+
path: normalizedPrefix ? `${normalizedPrefix}${ep.path}` : ep.path,
|
|
19
|
+
}))
|
|
20
|
+
|
|
21
|
+
if (processedEndpoints.length === 0) {
|
|
22
|
+
return NextResponse.json(
|
|
23
|
+
{ error: 'No valid endpoints found in OpenAPI spec' },
|
|
24
|
+
{ status: 400 }
|
|
25
|
+
)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const results = await Promise.allSettled(
|
|
29
|
+
processedEndpoints.map((ep) =>
|
|
30
|
+
prisma.mockEndpoint.upsert({
|
|
31
|
+
where: {
|
|
32
|
+
path_method: {
|
|
33
|
+
path: ep.path,
|
|
34
|
+
method: ep.method,
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
update: {
|
|
38
|
+
description: ep.description,
|
|
39
|
+
statusCode: ep.statusCode,
|
|
40
|
+
responseBody: ep.responseBody as Prisma.InputJsonValue,
|
|
41
|
+
},
|
|
42
|
+
create: {
|
|
43
|
+
path: ep.path,
|
|
44
|
+
method: ep.method,
|
|
45
|
+
description: ep.description,
|
|
46
|
+
statusCode: ep.statusCode,
|
|
47
|
+
responseBody: ep.responseBody as Prisma.InputJsonValue,
|
|
48
|
+
},
|
|
49
|
+
})
|
|
50
|
+
)
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
const created = results.filter((r) => r.status === 'fulfilled').length
|
|
54
|
+
const failed = results.filter((r) => r.status === 'rejected').length
|
|
55
|
+
|
|
56
|
+
return NextResponse.json({
|
|
57
|
+
message: `Imported ${created} endpoints`,
|
|
58
|
+
created,
|
|
59
|
+
failed,
|
|
60
|
+
total: processedEndpoints.length,
|
|
61
|
+
})
|
|
62
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server'
|
|
2
|
+
import { prisma } from '@/lib/prisma'
|
|
3
|
+
import { validatePath, validateMethod, validateStatusCode } from '@/lib/validation'
|
|
4
|
+
|
|
5
|
+
// GET: 모든 엔드포인트 조회
|
|
6
|
+
export async function GET() {
|
|
7
|
+
const endpoints = await prisma.mockEndpoint.findMany({
|
|
8
|
+
orderBy: { createdAt: 'desc' },
|
|
9
|
+
})
|
|
10
|
+
return NextResponse.json(endpoints)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// POST: 새 엔드포인트 생성
|
|
14
|
+
export async function POST(request: NextRequest) {
|
|
15
|
+
const body = await request.json()
|
|
16
|
+
const { path, method, description, statusCode, responseBody } = body
|
|
17
|
+
|
|
18
|
+
if (!validatePath(path)) {
|
|
19
|
+
return NextResponse.json({ error: 'Invalid path' }, { status: 400 })
|
|
20
|
+
}
|
|
21
|
+
if (!validateMethod(method)) {
|
|
22
|
+
return NextResponse.json({ error: 'Invalid method' }, { status: 400 })
|
|
23
|
+
}
|
|
24
|
+
if (!validateStatusCode(statusCode)) {
|
|
25
|
+
return NextResponse.json({ error: 'Invalid status code' }, { status: 400 })
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const endpoint = await prisma.mockEndpoint.create({
|
|
29
|
+
data: {
|
|
30
|
+
path,
|
|
31
|
+
method: method.toUpperCase(),
|
|
32
|
+
description,
|
|
33
|
+
statusCode: statusCode ?? 200,
|
|
34
|
+
responseBody,
|
|
35
|
+
},
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
return NextResponse.json(endpoint, { status: 201 })
|
|
39
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server'
|
|
2
|
+
import { Prisma } from '@/generated/prisma'
|
|
3
|
+
import { prisma } from '@/lib/prisma'
|
|
4
|
+
import { parseOpenAPISpec } from '@/lib/openapi-parser'
|
|
5
|
+
|
|
6
|
+
export async function POST(request: NextRequest) {
|
|
7
|
+
const { url, pathPrefix } = await request.json()
|
|
8
|
+
|
|
9
|
+
if (!url) {
|
|
10
|
+
return NextResponse.json({ error: 'URL is required' }, { status: 400 })
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Normalize path prefix (remove trailing slashes)
|
|
14
|
+
const normalizedPrefix = pathPrefix ? pathPrefix.replace(/\/+$/, '') : ''
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
// OpenAPI JSON 가져오기
|
|
18
|
+
const response = await fetch(url, {
|
|
19
|
+
headers: {
|
|
20
|
+
Accept: 'application/json',
|
|
21
|
+
},
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
if (!response.ok) {
|
|
25
|
+
return NextResponse.json(
|
|
26
|
+
{ error: `Failed to fetch OpenAPI spec: ${response.statusText}` },
|
|
27
|
+
{ status: 400 }
|
|
28
|
+
)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const spec = await response.json()
|
|
32
|
+
const endpoints = parseOpenAPISpec(spec)
|
|
33
|
+
|
|
34
|
+
// Apply path prefix if provided
|
|
35
|
+
const processedEndpoints = endpoints.map((ep) => ({
|
|
36
|
+
...ep,
|
|
37
|
+
path: normalizedPrefix ? `${normalizedPrefix}${ep.path}` : ep.path,
|
|
38
|
+
}))
|
|
39
|
+
|
|
40
|
+
if (processedEndpoints.length === 0) {
|
|
41
|
+
return NextResponse.json(
|
|
42
|
+
{ error: 'No valid endpoints found in OpenAPI spec' },
|
|
43
|
+
{ status: 400 }
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Upsert로 새로 추가하거나 업데이트
|
|
48
|
+
const results = await Promise.allSettled(
|
|
49
|
+
processedEndpoints.map((ep) =>
|
|
50
|
+
prisma.mockEndpoint.upsert({
|
|
51
|
+
where: {
|
|
52
|
+
path_method: {
|
|
53
|
+
path: ep.path,
|
|
54
|
+
method: ep.method,
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
update: {
|
|
58
|
+
description: ep.description,
|
|
59
|
+
statusCode: ep.statusCode,
|
|
60
|
+
responseBody: ep.responseBody as Prisma.InputJsonValue,
|
|
61
|
+
},
|
|
62
|
+
create: {
|
|
63
|
+
path: ep.path,
|
|
64
|
+
method: ep.method,
|
|
65
|
+
description: ep.description,
|
|
66
|
+
statusCode: ep.statusCode,
|
|
67
|
+
responseBody: ep.responseBody as Prisma.InputJsonValue,
|
|
68
|
+
},
|
|
69
|
+
})
|
|
70
|
+
)
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
const succeeded = results.filter((r) => r.status === 'fulfilled').length
|
|
74
|
+
const failed = results.filter((r) => r.status === 'rejected').length
|
|
75
|
+
|
|
76
|
+
return NextResponse.json({
|
|
77
|
+
message: `Synced ${succeeded} endpoints`,
|
|
78
|
+
succeeded,
|
|
79
|
+
failed,
|
|
80
|
+
total: processedEndpoints.length,
|
|
81
|
+
})
|
|
82
|
+
} catch (error) {
|
|
83
|
+
console.error('Sync error:', error)
|
|
84
|
+
return NextResponse.json(
|
|
85
|
+
{ error: error instanceof Error ? error.message : 'Sync failed' },
|
|
86
|
+
{ status: 500 }
|
|
87
|
+
)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server'
|
|
2
|
+
import { match } from 'path-to-regexp'
|
|
3
|
+
import { prisma } from '@/lib/prisma'
|
|
4
|
+
|
|
5
|
+
type RouteParams = {
|
|
6
|
+
params: Promise<{ slug: string[] }>
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
async function findMatchingEndpoint(requestPath: string, method: string) {
|
|
10
|
+
// DB에서 해당 method의 모든 엔드포인트 조회
|
|
11
|
+
const endpoints = await prisma.mockEndpoint.findMany({
|
|
12
|
+
where: { method },
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
// 정확히 일치하는 경로 먼저 확인
|
|
16
|
+
const exactMatch = endpoints.find((ep) => ep.path === requestPath)
|
|
17
|
+
if (exactMatch) {
|
|
18
|
+
return { endpoint: exactMatch, params: {} }
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// path-to-regexp를 사용한 패턴 매칭
|
|
22
|
+
for (const endpoint of endpoints) {
|
|
23
|
+
try {
|
|
24
|
+
const matchFn = match(endpoint.path, { decode: decodeURIComponent })
|
|
25
|
+
const result = matchFn(requestPath)
|
|
26
|
+
|
|
27
|
+
if (result) {
|
|
28
|
+
return { endpoint, params: result.params }
|
|
29
|
+
}
|
|
30
|
+
} catch {
|
|
31
|
+
// 잘못된 패턴은 무시
|
|
32
|
+
continue
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return null
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function handleRequest(request: NextRequest, { params }: RouteParams) {
|
|
40
|
+
const { slug } = await params
|
|
41
|
+
const requestPath = '/' + slug.join('/')
|
|
42
|
+
const method = request.method
|
|
43
|
+
|
|
44
|
+
const result = await findMatchingEndpoint(requestPath, method)
|
|
45
|
+
|
|
46
|
+
if (!result) {
|
|
47
|
+
return NextResponse.json(
|
|
48
|
+
{ error: 'Endpoint not found', path: requestPath, method },
|
|
49
|
+
{ status: 404 }
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const { endpoint, params: pathParams } = result
|
|
54
|
+
|
|
55
|
+
// responseBody에 pathParams를 포함하여 반환 (선택적)
|
|
56
|
+
const responseData =
|
|
57
|
+
Object.keys(pathParams).length > 0
|
|
58
|
+
? { ...endpoint.responseBody as object, _pathParams: pathParams }
|
|
59
|
+
: endpoint.responseBody
|
|
60
|
+
|
|
61
|
+
return NextResponse.json(responseData, { status: endpoint.statusCode })
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export async function GET(request: NextRequest, context: RouteParams) {
|
|
65
|
+
return handleRequest(request, context)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export async function POST(request: NextRequest, context: RouteParams) {
|
|
69
|
+
return handleRequest(request, context)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export async function PUT(request: NextRequest, context: RouteParams) {
|
|
73
|
+
return handleRequest(request, context)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export async function PATCH(request: NextRequest, context: RouteParams) {
|
|
77
|
+
return handleRequest(request, context)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export async function DELETE(request: NextRequest, context: RouteParams) {
|
|
81
|
+
return handleRequest(request, context)
|
|
82
|
+
}
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
interface OpenAPISpec {
|
|
2
|
+
paths?: Record<string, Record<string, OpenAPIOperation>>
|
|
3
|
+
components?: {
|
|
4
|
+
schemas?: Record<string, OpenAPISchema>
|
|
5
|
+
}
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
interface OpenAPIOperation {
|
|
9
|
+
summary?: string
|
|
10
|
+
description?: string
|
|
11
|
+
responses?: Record<string, OpenAPIResponse>
|
|
12
|
+
requestBody?: {
|
|
13
|
+
content?: Record<string, { schema?: OpenAPISchema; example?: unknown }>
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface OpenAPIResponse {
|
|
18
|
+
description?: string
|
|
19
|
+
content?: Record<string, { schema?: OpenAPISchema; example?: unknown }>
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface OpenAPISchema {
|
|
23
|
+
type?: string
|
|
24
|
+
format?: string
|
|
25
|
+
properties?: Record<string, OpenAPISchema>
|
|
26
|
+
items?: OpenAPISchema
|
|
27
|
+
example?: unknown
|
|
28
|
+
default?: unknown
|
|
29
|
+
enum?: unknown[]
|
|
30
|
+
$ref?: string
|
|
31
|
+
allOf?: OpenAPISchema[]
|
|
32
|
+
oneOf?: OpenAPISchema[]
|
|
33
|
+
anyOf?: OpenAPISchema[]
|
|
34
|
+
nullable?: boolean
|
|
35
|
+
required?: string[]
|
|
36
|
+
additionalProperties?: boolean | OpenAPISchema
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface ParsedEndpoint {
|
|
40
|
+
path: string
|
|
41
|
+
method: string
|
|
42
|
+
description: string | null
|
|
43
|
+
statusCode: number
|
|
44
|
+
responseBody: unknown
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function resolveRef(ref: string, spec: OpenAPISpec): OpenAPISchema | null {
|
|
48
|
+
// $ref format: "#/components/schemas/SchemaName"
|
|
49
|
+
const parts = ref.split('/')
|
|
50
|
+
if (parts[0] !== '#' || parts[1] !== 'components' || parts[2] !== 'schemas') {
|
|
51
|
+
return null
|
|
52
|
+
}
|
|
53
|
+
const schemaName = parts[3]
|
|
54
|
+
return spec.components?.schemas?.[schemaName] || null
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function generateMockValue(
|
|
58
|
+
schema: OpenAPISchema,
|
|
59
|
+
spec: OpenAPISpec,
|
|
60
|
+
visited: Set<string> = new Set()
|
|
61
|
+
): unknown {
|
|
62
|
+
// Handle $ref
|
|
63
|
+
if (schema.$ref) {
|
|
64
|
+
// Prevent circular references
|
|
65
|
+
if (visited.has(schema.$ref)) {
|
|
66
|
+
return {}
|
|
67
|
+
}
|
|
68
|
+
visited.add(schema.$ref)
|
|
69
|
+
const resolved = resolveRef(schema.$ref, spec)
|
|
70
|
+
if (resolved) {
|
|
71
|
+
return generateMockValue(resolved, spec, visited)
|
|
72
|
+
}
|
|
73
|
+
return {}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Handle allOf (merge schemas)
|
|
77
|
+
if (schema.allOf && schema.allOf.length > 0) {
|
|
78
|
+
const merged: Record<string, unknown> = {}
|
|
79
|
+
for (const subSchema of schema.allOf) {
|
|
80
|
+
const value = generateMockValue(subSchema, spec, visited)
|
|
81
|
+
if (typeof value === 'object' && value !== null) {
|
|
82
|
+
Object.assign(merged, value)
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return merged
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Handle oneOf/anyOf (use first option)
|
|
89
|
+
if (schema.oneOf && schema.oneOf.length > 0) {
|
|
90
|
+
return generateMockValue(schema.oneOf[0], spec, visited)
|
|
91
|
+
}
|
|
92
|
+
if (schema.anyOf && schema.anyOf.length > 0) {
|
|
93
|
+
return generateMockValue(schema.anyOf[0], spec, visited)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Use example or default if available
|
|
97
|
+
if (schema.example !== undefined) return schema.example
|
|
98
|
+
if (schema.default !== undefined) return schema.default
|
|
99
|
+
if (schema.enum && schema.enum.length > 0) return schema.enum[0]
|
|
100
|
+
|
|
101
|
+
// Generate based on type and format
|
|
102
|
+
switch (schema.type) {
|
|
103
|
+
case 'string':
|
|
104
|
+
return generateStringValue(schema.format)
|
|
105
|
+
case 'number':
|
|
106
|
+
return generateNumberValue(schema.format)
|
|
107
|
+
case 'integer':
|
|
108
|
+
return generateIntegerValue(schema.format)
|
|
109
|
+
case 'boolean':
|
|
110
|
+
return true
|
|
111
|
+
case 'array':
|
|
112
|
+
if (schema.items) {
|
|
113
|
+
return [generateMockValue(schema.items, spec, visited)]
|
|
114
|
+
}
|
|
115
|
+
return []
|
|
116
|
+
case 'object':
|
|
117
|
+
return generateObjectValue(schema, spec, visited)
|
|
118
|
+
default:
|
|
119
|
+
// If no type but has properties, treat as object
|
|
120
|
+
if (schema.properties) {
|
|
121
|
+
return generateObjectValue(schema, spec, visited)
|
|
122
|
+
}
|
|
123
|
+
return null
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function generateStringValue(format?: string): string {
|
|
128
|
+
switch (format) {
|
|
129
|
+
case 'date':
|
|
130
|
+
return '2024-01-15'
|
|
131
|
+
case 'date-time':
|
|
132
|
+
return '2024-01-15T09:30:00Z'
|
|
133
|
+
case 'time':
|
|
134
|
+
return '09:30:00'
|
|
135
|
+
case 'email':
|
|
136
|
+
return 'user@example.com'
|
|
137
|
+
case 'uri':
|
|
138
|
+
case 'url':
|
|
139
|
+
return 'https://example.com'
|
|
140
|
+
case 'uuid':
|
|
141
|
+
return '550e8400-e29b-41d4-a716-446655440000'
|
|
142
|
+
case 'hostname':
|
|
143
|
+
return 'example.com'
|
|
144
|
+
case 'ipv4':
|
|
145
|
+
return '192.168.1.1'
|
|
146
|
+
case 'ipv6':
|
|
147
|
+
return '2001:0db8:85a3:0000:0000:8a2e:0370:7334'
|
|
148
|
+
case 'byte':
|
|
149
|
+
return 'SGVsbG8gV29ybGQ='
|
|
150
|
+
case 'binary':
|
|
151
|
+
return '<binary>'
|
|
152
|
+
case 'password':
|
|
153
|
+
return '********'
|
|
154
|
+
case 'phone':
|
|
155
|
+
return '+1-555-555-5555'
|
|
156
|
+
default:
|
|
157
|
+
return 'string'
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function generateNumberValue(format?: string): number {
|
|
162
|
+
switch (format) {
|
|
163
|
+
case 'float':
|
|
164
|
+
return 1.5
|
|
165
|
+
case 'double':
|
|
166
|
+
return 1.23456789
|
|
167
|
+
default:
|
|
168
|
+
return 0
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function generateIntegerValue(format?: string): number {
|
|
173
|
+
switch (format) {
|
|
174
|
+
case 'int32':
|
|
175
|
+
return 123
|
|
176
|
+
case 'int64':
|
|
177
|
+
return 1234567890
|
|
178
|
+
default:
|
|
179
|
+
return 0
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function generateObjectValue(
|
|
184
|
+
schema: OpenAPISchema,
|
|
185
|
+
spec: OpenAPISpec,
|
|
186
|
+
visited: Set<string>
|
|
187
|
+
): Record<string, unknown> {
|
|
188
|
+
const obj: Record<string, unknown> = {}
|
|
189
|
+
|
|
190
|
+
if (schema.properties) {
|
|
191
|
+
for (const [key, propSchema] of Object.entries(schema.properties)) {
|
|
192
|
+
obj[key] = generateMockValue(propSchema, spec, visited)
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return obj
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export function parseOpenAPISpec(spec: OpenAPISpec): ParsedEndpoint[] {
|
|
200
|
+
const endpoints: ParsedEndpoint[] = []
|
|
201
|
+
|
|
202
|
+
if (!spec.paths) return endpoints
|
|
203
|
+
|
|
204
|
+
for (const [path, methods] of Object.entries(spec.paths)) {
|
|
205
|
+
for (const [method, operation] of Object.entries(methods)) {
|
|
206
|
+
// Skip non-HTTP method properties like 'parameters'
|
|
207
|
+
if (
|
|
208
|
+
!['get', 'post', 'put', 'patch', 'delete', 'head', 'options'].includes(
|
|
209
|
+
method.toLowerCase()
|
|
210
|
+
)
|
|
211
|
+
) {
|
|
212
|
+
continue
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const description = operation.summary || operation.description || null
|
|
216
|
+
|
|
217
|
+
// Find successful response (2xx)
|
|
218
|
+
let statusCode = 200
|
|
219
|
+
let responseBody: unknown = {}
|
|
220
|
+
|
|
221
|
+
if (operation.responses) {
|
|
222
|
+
// Sort response codes to prefer 200, then other 2xx
|
|
223
|
+
const responseCodes = Object.keys(operation.responses).sort((a, b) => {
|
|
224
|
+
if (a === '200') return -1
|
|
225
|
+
if (b === '200') return 1
|
|
226
|
+
return parseInt(a, 10) - parseInt(b, 10)
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
for (const code of responseCodes) {
|
|
230
|
+
const codeNum = parseInt(code, 10)
|
|
231
|
+
if (codeNum >= 200 && codeNum < 300) {
|
|
232
|
+
statusCode = codeNum
|
|
233
|
+
const response = operation.responses[code]
|
|
234
|
+
|
|
235
|
+
if (response.content) {
|
|
236
|
+
// Try application/json first, then any JSON-like content type
|
|
237
|
+
const jsonContent =
|
|
238
|
+
response.content['application/json'] ||
|
|
239
|
+
response.content['application/hal+json'] ||
|
|
240
|
+
response.content['application/vnd.api+json'] ||
|
|
241
|
+
Object.values(response.content).find((c) => c.schema)
|
|
242
|
+
|
|
243
|
+
if (jsonContent) {
|
|
244
|
+
if (jsonContent.example) {
|
|
245
|
+
responseBody = jsonContent.example
|
|
246
|
+
} else if (jsonContent.schema) {
|
|
247
|
+
responseBody = generateMockValue(jsonContent.schema, spec)
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
break
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Convert OpenAPI path params {id} to Express-style :id
|
|
257
|
+
const normalizedPath = path.replace(/\{(\w+)\}/g, ':$1')
|
|
258
|
+
|
|
259
|
+
endpoints.push({
|
|
260
|
+
path: normalizedPath,
|
|
261
|
+
method: method.toUpperCase(),
|
|
262
|
+
description,
|
|
263
|
+
statusCode,
|
|
264
|
+
responseBody,
|
|
265
|
+
})
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return endpoints
|
|
270
|
+
}
|
package/lib/prisma.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { PrismaClient } from '@/generated/prisma'
|
|
2
|
+
|
|
3
|
+
const globalForPrisma = globalThis as unknown as {
|
|
4
|
+
prisma: PrismaClient | undefined
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export const prisma = globalForPrisma.prisma ?? new PrismaClient()
|
|
8
|
+
|
|
9
|
+
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma
|
package/lib/utils.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
const VALID_METHODS = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS']
|
|
2
|
+
|
|
3
|
+
export function validatePath(path: unknown): path is string {
|
|
4
|
+
if (!path || typeof path !== 'string') return false
|
|
5
|
+
if (!path.startsWith('/')) return false
|
|
6
|
+
if (path.length > 1000) return false
|
|
7
|
+
return true
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function validateMethod(method: unknown): method is string {
|
|
11
|
+
if (typeof method !== 'string') return false
|
|
12
|
+
return VALID_METHODS.includes(method.toUpperCase())
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function validateStatusCode(statusCode: unknown): statusCode is number {
|
|
16
|
+
if (statusCode === undefined || statusCode === null) return true // optional
|
|
17
|
+
if (typeof statusCode !== 'number') return false
|
|
18
|
+
return statusCode >= 100 && statusCode < 600
|
|
19
|
+
}
|
package/next-env.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/// <reference types="next" />
|
|
2
2
|
/// <reference types="next/image-types/global" />
|
|
3
|
-
import "./.next/types/routes.d.ts";
|
|
3
|
+
import "./.next/dev/types/routes.d.ts";
|
|
4
4
|
|
|
5
5
|
// NOTE: This file should not be edited
|
|
6
6
|
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
package/next.config.ts
ADDED
package/package.json
CHANGED
package/prisma/seed.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { PrismaClient } from '../generated/prisma'
|
|
2
|
+
|
|
3
|
+
const prisma = new PrismaClient()
|
|
4
|
+
|
|
5
|
+
async function main() {
|
|
6
|
+
// 기존 데이터 삭제
|
|
7
|
+
await prisma.mockEndpoint.deleteMany()
|
|
8
|
+
console.log('Cleared all mock endpoints')
|
|
9
|
+
|
|
10
|
+
// 샘플 데이터 추가 (선택사항)
|
|
11
|
+
// await prisma.mockEndpoint.create({
|
|
12
|
+
// data: {
|
|
13
|
+
// path: '/v1/health',
|
|
14
|
+
// method: 'GET',
|
|
15
|
+
// description: 'Health check',
|
|
16
|
+
// statusCode: 200,
|
|
17
|
+
// responseBody: { status: 'ok' },
|
|
18
|
+
// },
|
|
19
|
+
// })
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
main()
|
|
23
|
+
.catch((e) => {
|
|
24
|
+
console.error(e)
|
|
25
|
+
process.exit(1)
|
|
26
|
+
})
|
|
27
|
+
.finally(async () => {
|
|
28
|
+
await prisma.$disconnect()
|
|
29
|
+
})
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2017",
|
|
4
|
+
"lib": ["dom", "dom.iterable", "esnext"],
|
|
5
|
+
"allowJs": true,
|
|
6
|
+
"skipLibCheck": true,
|
|
7
|
+
"strict": true,
|
|
8
|
+
"noEmit": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"module": "esnext",
|
|
11
|
+
"moduleResolution": "bundler",
|
|
12
|
+
"resolveJsonModule": true,
|
|
13
|
+
"isolatedModules": true,
|
|
14
|
+
"jsx": "react-jsx",
|
|
15
|
+
"incremental": true,
|
|
16
|
+
"plugins": [
|
|
17
|
+
{
|
|
18
|
+
"name": "next"
|
|
19
|
+
}
|
|
20
|
+
],
|
|
21
|
+
"paths": {
|
|
22
|
+
"@/*": ["./*"]
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"include": [
|
|
26
|
+
"next-env.d.ts",
|
|
27
|
+
"**/*.ts",
|
|
28
|
+
"**/*.tsx",
|
|
29
|
+
".next/types/**/*.ts",
|
|
30
|
+
".next/dev/types/**/*.ts",
|
|
31
|
+
"**/*.mts"
|
|
32
|
+
],
|
|
33
|
+
"exclude": ["node_modules"]
|
|
34
|
+
}
|
package/prisma/prisma/dev.db
DELETED
|
Binary file
|