fix: resolver avisos de build e tipagem
This commit is contained in:
parent
741f1d7f9c
commit
fa9efdb5af
7 changed files with 154 additions and 59 deletions
|
|
@ -1,7 +1,8 @@
|
||||||
import { v } from "convex/values"
|
import { v } from "convex/values"
|
||||||
|
|
||||||
import type { Id } from "./_generated/dataModel"
|
import type { Doc, Id } from "./_generated/dataModel"
|
||||||
import { query } from "./_generated/server"
|
import { query } from "./_generated/server"
|
||||||
|
import type { QueryCtx } from "./_generated/server"
|
||||||
import {
|
import {
|
||||||
OPEN_STATUSES,
|
OPEN_STATUSES,
|
||||||
ONE_DAY_MS,
|
ONE_DAY_MS,
|
||||||
|
|
@ -26,7 +27,7 @@ type MetricRunPayload = {
|
||||||
data: unknown
|
data: unknown
|
||||||
}
|
}
|
||||||
|
|
||||||
type MetricResolver = (ctx: Parameters<typeof query>[0], input: MetricResolverInput) => Promise<MetricRunPayload>
|
type MetricResolver = (ctx: QueryCtx, input: MetricResolverInput) => Promise<MetricRunPayload>
|
||||||
|
|
||||||
function parseRange(params?: Record<string, unknown>): number {
|
function parseRange(params?: Record<string, unknown>): number {
|
||||||
const value = params?.range
|
const value = params?.range
|
||||||
|
|
@ -380,10 +381,12 @@ const metricResolvers: Record<string, MetricResolver> = {
|
||||||
const lastHeartbeatAt = machine.lastHeartbeatAt ?? null
|
const lastHeartbeatAt = machine.lastHeartbeatAt ?? null
|
||||||
const minutesSinceHeartbeat = lastHeartbeatAt ? Math.round((now - lastHeartbeatAt) / 60000) : null
|
const minutesSinceHeartbeat = lastHeartbeatAt ? Math.round((now - lastHeartbeatAt) / 60000) : null
|
||||||
const status = deriveMachineStatus(machine, now)
|
const status = deriveMachineStatus(machine, now)
|
||||||
const cpu = clampPercent(machine.cpuUsagePercent)
|
const cpu = clampPercent(pickMachineMetric(machine, ["cpuUsagePercent", "cpu_usage_percent"]))
|
||||||
const memory = clampPercent(machine.memoryUsedPercent)
|
const memory = clampPercent(pickMachineMetric(machine, ["memoryUsedPercent", "memory_usage_percent"]))
|
||||||
const disk = clampPercent(machine.diskUsedPercent ?? machine.diskUsagePercent)
|
const disk = clampPercent(pickMachineMetric(machine, ["diskUsedPercent", "diskUsagePercent", "storageUsedPercent"]))
|
||||||
const alerts = Array.isArray(machine.postureAlerts) ? machine.postureAlerts.length : machine.postureAlertsCount ?? 0
|
const alerts = readMachineAlertsCount(machine)
|
||||||
|
const fallbackHostname = readString((machine as unknown as Record<string, unknown>)["computerName"])
|
||||||
|
const hostname = machine.hostname ?? fallbackHostname ?? "Dispositivo sem nome"
|
||||||
const attention =
|
const attention =
|
||||||
(cpu ?? 0) > 85 ||
|
(cpu ?? 0) > 85 ||
|
||||||
(memory ?? 0) > 90 ||
|
(memory ?? 0) > 90 ||
|
||||||
|
|
@ -392,7 +395,7 @@ const metricResolvers: Record<string, MetricResolver> = {
|
||||||
alerts > 0
|
alerts > 0
|
||||||
return {
|
return {
|
||||||
id: machine._id,
|
id: machine._id,
|
||||||
hostname: machine.hostname ?? machine.computerName ?? "Dispositivo sem nome",
|
hostname,
|
||||||
status,
|
status,
|
||||||
cpuUsagePercent: cpu,
|
cpuUsagePercent: cpu,
|
||||||
memoryUsedPercent: memory,
|
memoryUsedPercent: memory,
|
||||||
|
|
@ -467,3 +470,72 @@ function clampPercent(value: unknown) {
|
||||||
if (value > 100) return 100
|
if (value > 100) return 100
|
||||||
return Math.round(value * 10) / 10
|
return Math.round(value * 10) / 10
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function readNumeric(value: unknown): number | null {
|
||||||
|
if (typeof value === "number" && Number.isFinite(value)) {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
if (typeof value === "string") {
|
||||||
|
const parsed = Number(value)
|
||||||
|
if (Number.isFinite(parsed)) {
|
||||||
|
return parsed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
function pickMachineMetric(machine: Doc<"machines">, keys: string[]): number | null {
|
||||||
|
const record = machine as unknown as Record<string, unknown>
|
||||||
|
for (const key of keys) {
|
||||||
|
const direct = readNumeric(record[key])
|
||||||
|
if (direct !== null) {
|
||||||
|
return direct
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const metadata = record["metadata"]
|
||||||
|
if (metadata && typeof metadata === "object" && !Array.isArray(metadata)) {
|
||||||
|
const metrics = (metadata as Record<string, unknown>)["metrics"]
|
||||||
|
if (metrics && typeof metrics === "object" && !Array.isArray(metrics)) {
|
||||||
|
const metricsRecord = metrics as Record<string, unknown>
|
||||||
|
for (const key of keys) {
|
||||||
|
const value = readNumeric(metricsRecord[key])
|
||||||
|
if (value !== null) {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
function readMachineAlertsCount(machine: Doc<"machines">): number {
|
||||||
|
const record = machine as unknown as Record<string, unknown>
|
||||||
|
const directCount = readNumeric(record["postureAlertsCount"])
|
||||||
|
if (directCount !== null) {
|
||||||
|
return directCount
|
||||||
|
}
|
||||||
|
const directArray = record["postureAlerts"]
|
||||||
|
if (Array.isArray(directArray)) {
|
||||||
|
return directArray.length
|
||||||
|
}
|
||||||
|
const metadata = record["metadata"]
|
||||||
|
if (metadata && typeof metadata === "object" && !Array.isArray(metadata)) {
|
||||||
|
const metadataRecord = metadata as Record<string, unknown>
|
||||||
|
const metaCount = readNumeric(metadataRecord["postureAlertsCount"])
|
||||||
|
if (metaCount !== null) {
|
||||||
|
return metaCount
|
||||||
|
}
|
||||||
|
const metaAlerts = metadataRecord["postureAlerts"]
|
||||||
|
if (Array.isArray(metaAlerts)) {
|
||||||
|
return metaAlerts.length
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
function readString(value: unknown): string | null {
|
||||||
|
if (typeof value === "string" && value.trim().length > 0) {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1898,12 +1898,15 @@ export const addComment = mutation({
|
||||||
try {
|
try {
|
||||||
const snapshotEmail = (ticketDoc.requesterSnapshot as { email?: string } | undefined)?.email
|
const snapshotEmail = (ticketDoc.requesterSnapshot as { email?: string } | undefined)?.email
|
||||||
if (requestedVisibility === "PUBLIC" && snapshotEmail && String(ticketDoc.requesterId) !== String(args.authorId)) {
|
if (requestedVisibility === "PUBLIC" && snapshotEmail && String(ticketDoc.requesterId) !== String(args.authorId)) {
|
||||||
await ctx.scheduler.runAfter(0, api.ticketNotifications.sendPublicCommentEmail, {
|
const schedulerRunAfter = ctx.scheduler?.runAfter
|
||||||
to: snapshotEmail,
|
if (typeof schedulerRunAfter === "function") {
|
||||||
ticketId: String(ticketDoc._id),
|
await schedulerRunAfter(0, api.ticketNotifications.sendPublicCommentEmail, {
|
||||||
reference: ticketDoc.reference ?? 0,
|
to: snapshotEmail,
|
||||||
subject: ticketDoc.subject ?? "",
|
ticketId: String(ticketDoc._id),
|
||||||
})
|
reference: ticketDoc.reference ?? 0,
|
||||||
|
subject: ticketDoc.subject ?? "",
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn("[tickets] Falha ao agendar e-mail de comentário", e)
|
console.warn("[tickets] Falha ao agendar e-mail de comentário", e)
|
||||||
|
|
@ -2148,12 +2151,15 @@ export async function resolveTicketHandler(
|
||||||
const requesterDoc = await ctx.db.get(ticketDoc.requesterId)
|
const requesterDoc = await ctx.db.get(ticketDoc.requesterId)
|
||||||
const email = (requesterDoc as Doc<"users"> | null)?.email || (ticketDoc.requesterSnapshot as { email?: string } | undefined)?.email || null
|
const email = (requesterDoc as Doc<"users"> | null)?.email || (ticketDoc.requesterSnapshot as { email?: string } | undefined)?.email || null
|
||||||
if (email) {
|
if (email) {
|
||||||
await ctx.scheduler.runAfter(0, api.ticketNotifications.sendResolvedEmail, {
|
const schedulerRunAfter = ctx.scheduler?.runAfter
|
||||||
to: email,
|
if (typeof schedulerRunAfter === "function") {
|
||||||
ticketId: String(ticketId),
|
await schedulerRunAfter(0, api.ticketNotifications.sendResolvedEmail, {
|
||||||
reference: ticketDoc.reference ?? 0,
|
to: email,
|
||||||
subject: ticketDoc.subject ?? "",
|
ticketId: String(ticketId),
|
||||||
})
|
reference: ticketDoc.reference ?? 0,
|
||||||
|
subject: ticketDoc.subject ?? "",
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn("[tickets] Falha ao agendar e-mail de encerramento", e)
|
console.warn("[tickets] Falha ao agendar e-mail de encerramento", e)
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,8 @@ export async function POST(request: Request) {
|
||||||
printBackground: true,
|
printBackground: true,
|
||||||
pageRanges: "1",
|
pageRanges: "1",
|
||||||
})
|
})
|
||||||
return new NextResponse(pdf, {
|
const pdfBytes = new Uint8Array(pdf)
|
||||||
|
return new NextResponse(pdfBytes, {
|
||||||
status: 200,
|
status: 200,
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/pdf",
|
"Content-Type": "application/pdf",
|
||||||
|
|
@ -59,7 +60,8 @@ export async function POST(request: Request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const screenshot = await page.screenshot({ type: "png", fullPage: true })
|
const screenshot = await page.screenshot({ type: "png", fullPage: true })
|
||||||
return new NextResponse(screenshot, {
|
const screenshotBytes = new Uint8Array(screenshot)
|
||||||
|
return new NextResponse(screenshotBytes, {
|
||||||
status: 200,
|
status: 200,
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "image/png",
|
"Content-Type": "image/png",
|
||||||
|
|
|
||||||
|
|
@ -11,8 +11,8 @@ import {
|
||||||
TrendingUp,
|
TrendingUp,
|
||||||
PanelsTopLeft,
|
PanelsTopLeft,
|
||||||
UserCog,
|
UserCog,
|
||||||
|
Building,
|
||||||
Building2,
|
Building2,
|
||||||
Skyscraper,
|
|
||||||
Waypoints,
|
Waypoints,
|
||||||
Clock4,
|
Clock4,
|
||||||
Timer,
|
Timer,
|
||||||
|
|
@ -108,7 +108,7 @@ const navigation: NavigationGroup[] = [
|
||||||
{
|
{
|
||||||
title: "Empresas",
|
title: "Empresas",
|
||||||
url: "/admin/companies",
|
url: "/admin/companies",
|
||||||
icon: Skyscraper,
|
icon: Building,
|
||||||
requiredRole: "admin",
|
requiredRole: "admin",
|
||||||
},
|
},
|
||||||
{ title: "Usuários", url: "/admin/users", icon: Users, requiredRole: "admin" },
|
{ title: "Usuários", url: "/admin/users", icon: Users, requiredRole: "admin" },
|
||||||
|
|
|
||||||
|
|
@ -150,6 +150,14 @@ type PackedLayoutItem = LayoutStateItem & {
|
||||||
y: number
|
y: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type CanvasRenderableItem = {
|
||||||
|
key: string
|
||||||
|
layout: PackedLayoutItem
|
||||||
|
minW?: number
|
||||||
|
minH?: number
|
||||||
|
element: React.ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
type DashboardDetailResult = {
|
type DashboardDetailResult = {
|
||||||
dashboard: DashboardRecord
|
dashboard: DashboardRecord
|
||||||
widgets: DashboardWidgetRecord[]
|
widgets: DashboardWidgetRecord[]
|
||||||
|
|
@ -542,8 +550,6 @@ export function DashboardBuilder({ dashboardId, editable = true, mode = "edit" }
|
||||||
return {
|
return {
|
||||||
key: item.i,
|
key: item.i,
|
||||||
layout: item,
|
layout: item,
|
||||||
minW: item.minW,
|
|
||||||
minH: item.minH,
|
|
||||||
element: (
|
element: (
|
||||||
<BuilderWidgetCard
|
<BuilderWidgetCard
|
||||||
key={widget.widgetKey}
|
key={widget.widgetKey}
|
||||||
|
|
@ -561,9 +567,11 @@ export function DashboardBuilder({ dashboardId, editable = true, mode = "edit" }
|
||||||
onReadyChange={(ready) => handleWidgetReady(widget.widgetKey, ready)}
|
onReadyChange={(ready) => handleWidgetReady(widget.widgetKey, ready)}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
}
|
...(item.minW !== undefined ? { minW: item.minW } : {}),
|
||||||
|
...(item.minH !== undefined ? { minH: item.minH } : {}),
|
||||||
|
} satisfies CanvasRenderableItem
|
||||||
})
|
})
|
||||||
.filter((item): item is { key: string; layout: PackedLayoutItem; minW?: number; minH?: number; element: React.ReactNode } => Boolean(item))
|
.filter(Boolean) as CanvasRenderableItem[]
|
||||||
|
|
||||||
const allWidgetsReady = canvasItems.length > 0 && canvasItems.every((item) => readyWidgets.has(item.key))
|
const allWidgetsReady = canvasItems.length > 0 && canvasItems.every((item) => readyWidgets.has(item.key))
|
||||||
|
|
||||||
|
|
@ -1177,34 +1185,37 @@ function WidgetConfigDialog({
|
||||||
widget: DashboardWidgetRecord | null
|
widget: DashboardWidgetRecord | null
|
||||||
onSubmit: (values: WidgetConfigFormValues) => Promise<void>
|
onSubmit: (values: WidgetConfigFormValues) => Promise<void>
|
||||||
}) {
|
}) {
|
||||||
|
const normalizedConfig = getWidgetConfigForWidget(widget)
|
||||||
const form = useForm<WidgetConfigFormValues>({
|
const form = useForm<WidgetConfigFormValues>({
|
||||||
resolver: zodResolver(widgetConfigSchema),
|
resolver: zodResolver(widgetConfigSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
title: widget?.title ?? widget?.config?.title ?? "",
|
title: normalizedConfig?.title ?? widget?.title ?? "",
|
||||||
type: widget?.type ?? "kpi",
|
type: normalizedConfig?.type ?? widget?.type ?? "kpi",
|
||||||
metricKey: widget?.config?.dataSource?.metricKey ?? "",
|
metricKey: normalizedConfig?.dataSource?.metricKey ?? "",
|
||||||
stacked: Boolean((widget?.config as { encoding?: { stacked?: boolean } })?.encoding?.stacked ?? false),
|
stacked: Boolean(normalizedConfig?.encoding && "stacked" in normalizedConfig.encoding ? normalizedConfig.encoding.stacked : false),
|
||||||
legend: Boolean((widget?.config as { options?: { legend?: boolean } })?.options?.legend ?? true),
|
legend: Boolean(normalizedConfig?.options && "legend" in normalizedConfig.options ? normalizedConfig.options.legend : true),
|
||||||
rangeOverride: typeof (widget?.config as { dataSource?: { params?: Record<string, unknown> } })?.dataSource?.params?.range === "string"
|
rangeOverride:
|
||||||
? ((widget?.config as { dataSource?: { params?: Record<string, unknown> } })?.dataSource?.params?.range as string)
|
typeof normalizedConfig?.dataSource?.params?.range === "string"
|
||||||
: "",
|
? (normalizedConfig.dataSource?.params?.range as string)
|
||||||
showTooltip: Boolean((widget?.config as { options?: { tooltip?: boolean } })?.options?.tooltip ?? true),
|
: "",
|
||||||
|
showTooltip: Boolean(normalizedConfig?.options && "tooltip" in normalizedConfig.options ? normalizedConfig.options.tooltip : true),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!widget) return
|
if (!widget) return
|
||||||
|
const config = getWidgetConfigForWidget(widget)
|
||||||
form.reset({
|
form.reset({
|
||||||
title: widget.title ?? (widget.config as { title?: string })?.title ?? "",
|
title: config?.title ?? widget.title ?? "",
|
||||||
type: widget.type,
|
type: config?.type ?? widget.type,
|
||||||
metricKey: (widget.config as { dataSource?: { metricKey?: string } })?.dataSource?.metricKey ?? "",
|
metricKey: config?.dataSource?.metricKey ?? "",
|
||||||
stacked: Boolean((widget.config as { encoding?: { stacked?: boolean } })?.encoding?.stacked ?? false),
|
stacked: Boolean(config?.encoding && "stacked" in config.encoding ? config.encoding.stacked : false),
|
||||||
legend: Boolean((widget.config as { options?: { legend?: boolean } })?.options?.legend ?? true),
|
legend: Boolean(config?.options && "legend" in config.options ? config.options.legend : true),
|
||||||
rangeOverride:
|
rangeOverride:
|
||||||
typeof (widget.config as { dataSource?: { params?: Record<string, unknown> } })?.dataSource?.params?.range === "string"
|
typeof config?.dataSource?.params?.range === "string"
|
||||||
? ((widget.config as { dataSource?: { params?: Record<string, unknown> } })?.dataSource?.params?.range as string)
|
? (config.dataSource?.params?.range as string)
|
||||||
: "",
|
: "",
|
||||||
showTooltip: Boolean((widget.config as { options?: { tooltip?: boolean } })?.options?.tooltip ?? true),
|
showTooltip: Boolean(config?.options && "tooltip" in config.options ? config.options.tooltip : true),
|
||||||
})
|
})
|
||||||
}, [widget, form])
|
}, [widget, form])
|
||||||
|
|
||||||
|
|
@ -1249,19 +1260,19 @@ function WidgetConfigDialog({
|
||||||
<SwitchField
|
<SwitchField
|
||||||
label="Legend"
|
label="Legend"
|
||||||
description="Mostra a legenda na visualização."
|
description="Mostra a legenda na visualização."
|
||||||
checked={form.watch("legend")}
|
checked={Boolean(form.watch("legend"))}
|
||||||
onCheckedChange={(checked) => form.setValue("legend", checked)}
|
onCheckedChange={(checked) => form.setValue("legend", checked)}
|
||||||
/>
|
/>
|
||||||
<SwitchField
|
<SwitchField
|
||||||
label="Stacked"
|
label="Stacked"
|
||||||
description="Acumula as séries no eixo Y."
|
description="Acumula as séries no eixo Y."
|
||||||
checked={form.watch("stacked")}
|
checked={Boolean(form.watch("stacked"))}
|
||||||
onCheckedChange={(checked) => form.setValue("stacked", checked)}
|
onCheckedChange={(checked) => form.setValue("stacked", checked)}
|
||||||
/>
|
/>
|
||||||
<SwitchField
|
<SwitchField
|
||||||
label="Tooltip"
|
label="Tooltip"
|
||||||
description="Mantém o tooltip interativo."
|
description="Mantém o tooltip interativo."
|
||||||
checked={form.watch("showTooltip")}
|
checked={Boolean(form.watch("showTooltip"))}
|
||||||
onCheckedChange={(checked) => form.setValue("showTooltip", checked)}
|
onCheckedChange={(checked) => form.setValue("showTooltip", checked)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -236,7 +236,7 @@ export function WidgetRenderer({ widget, filters, mode = "edit", onReadyChange }
|
||||||
const widgetType = (config.type ?? widget.type ?? "text").toLowerCase()
|
const widgetType = (config.type ?? widget.type ?? "text").toLowerCase()
|
||||||
const title = config.title ?? widget.title ?? "Widget"
|
const title = config.title ?? widget.title ?? "Widget"
|
||||||
const description = config.description
|
const description = config.description
|
||||||
const mergedParams = useMemo(() => mergeFilterParams(config.dataSource?.params, filters), [config.dataSource?.params, filters])
|
const mergedParams = mergeFilterParams(config.dataSource?.params, filters)
|
||||||
const metric = useMetricData({
|
const metric = useMetricData({
|
||||||
metricKey: config.dataSource?.metricKey,
|
metricKey: config.dataSource?.metricKey,
|
||||||
params: mergedParams,
|
params: mergedParams,
|
||||||
|
|
@ -708,13 +708,7 @@ function renderGauge({
|
||||||
outerRadius={110}
|
outerRadius={110}
|
||||||
data={[{ name: "SLA", value: display }]}
|
data={[{ name: "SLA", value: display }]}
|
||||||
>
|
>
|
||||||
<RadialBar
|
<RadialBar background dataKey="value" cornerRadius={5} fill="var(--color-value)" />
|
||||||
minAngle={15}
|
|
||||||
background
|
|
||||||
dataKey="value"
|
|
||||||
cornerRadius={5}
|
|
||||||
fill="var(--color-value)"
|
|
||||||
/>
|
|
||||||
<RechartsTooltip
|
<RechartsTooltip
|
||||||
cursor={false}
|
cursor={false}
|
||||||
content={<ChartTooltipContent hideLabel valueFormatter={(val) => percentFormatter.format(Number(val ?? 0))} />}
|
content={<ChartTooltipContent hideLabel valueFormatter={(val) => percentFormatter.format(Number(val ?? 0))} />}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import path from "node:path"
|
import path from "node:path"
|
||||||
|
import { fileURLToPath, pathToFileURL } from "node:url"
|
||||||
|
|
||||||
import { PrismaClient } from "@prisma/client"
|
import { PrismaClient } from "@prisma/client"
|
||||||
|
|
||||||
|
|
@ -7,6 +8,12 @@ declare global {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resolve a robust DATABASE_URL for all runtimes (prod/dev)
|
// Resolve a robust DATABASE_URL for all runtimes (prod/dev)
|
||||||
|
const PROJECT_ROOT = process.cwd()
|
||||||
|
const PROJECT_ROOT_URL = (() => {
|
||||||
|
const baseHref = pathToFileURL(PROJECT_ROOT).href
|
||||||
|
return new URL(baseHref.endsWith("/") ? baseHref : `${baseHref}/`)
|
||||||
|
})()
|
||||||
|
|
||||||
function resolveFileUrl(url: string) {
|
function resolveFileUrl(url: string) {
|
||||||
if (!url.startsWith("file:")) {
|
if (!url.startsWith("file:")) {
|
||||||
return url
|
return url
|
||||||
|
|
@ -14,13 +21,16 @@ function resolveFileUrl(url: string) {
|
||||||
|
|
||||||
const filePath = url.slice("file:".length)
|
const filePath = url.slice("file:".length)
|
||||||
if (filePath.startsWith("./") || filePath.startsWith("../")) {
|
if (filePath.startsWith("./") || filePath.startsWith("../")) {
|
||||||
const schemaDir = path.resolve(process.cwd(), "prisma")
|
const normalized = path.normalize(filePath)
|
||||||
const absolutePath = path.resolve(schemaDir, filePath)
|
const targetUrl = new URL(normalized, PROJECT_ROOT_URL)
|
||||||
|
const absolutePath = fileURLToPath(targetUrl)
|
||||||
|
if (!absolutePath.startsWith(PROJECT_ROOT)) {
|
||||||
|
throw new Error(`DATABASE_URL path escapes project directory: ${filePath}`)
|
||||||
|
}
|
||||||
return `file:${absolutePath}`
|
return `file:${absolutePath}`
|
||||||
}
|
}
|
||||||
if (!filePath.startsWith("/")) {
|
if (!filePath.startsWith("/")) {
|
||||||
const absolutePath = path.resolve(process.cwd(), filePath)
|
return resolveFileUrl(`file:./${filePath}`)
|
||||||
return `file:${absolutePath}`
|
|
||||||
}
|
}
|
||||||
return url
|
return url
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue