mirror of
https://github.com/umami-software/umami.git
synced 2025-12-06 01:18:00 +01:00
Compare commits
9 commits
65f657dd23
...
1483241494
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1483241494 | ||
|
|
c427c6f547 | ||
|
|
33cb195fd0 | ||
|
|
64767b1896 | ||
|
|
be1b787789 | ||
|
|
dae7327ed3 | ||
|
|
a06490af74 | ||
|
|
3379cc6e89 | ||
|
|
d7fd22645c |
17 changed files with 1205 additions and 1186 deletions
18
Dockerfile
18
Dockerfile
|
|
@ -1,5 +1,7 @@
|
||||||
|
ARG NODE_IMAGE_VERSION="22-alpine"
|
||||||
|
|
||||||
# Install dependencies only when needed
|
# Install dependencies only when needed
|
||||||
FROM node:22-alpine AS deps
|
FROM node:${NODE_IMAGE_VERSION} AS deps
|
||||||
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
|
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
|
||||||
RUN apk add --no-cache libc6-compat
|
RUN apk add --no-cache libc6-compat
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
@ -8,7 +10,7 @@ RUN npm install -g pnpm
|
||||||
RUN pnpm install --frozen-lockfile
|
RUN pnpm install --frozen-lockfile
|
||||||
|
|
||||||
# Rebuild the source code only when needed
|
# Rebuild the source code only when needed
|
||||||
FROM node:22-alpine AS builder
|
FROM node:${NODE_IMAGE_VERSION} AS builder
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY --from=deps /app/node_modules ./node_modules
|
COPY --from=deps /app/node_modules ./node_modules
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
@ -25,9 +27,10 @@ ENV NEXT_TELEMETRY_DISABLED=1
|
||||||
RUN npm run build-docker
|
RUN npm run build-docker
|
||||||
|
|
||||||
# Production image, copy all the files and run next
|
# Production image, copy all the files and run next
|
||||||
FROM node:22-alpine AS runner
|
FROM node:${NODE_IMAGE_VERSION} AS runner
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
|
ARG PRISMA_VERSION="6.19.0"
|
||||||
ARG NODE_OPTIONS
|
ARG NODE_OPTIONS
|
||||||
|
|
||||||
ENV NODE_ENV=production
|
ENV NODE_ENV=production
|
||||||
|
|
@ -36,13 +39,14 @@ ENV NODE_OPTIONS=$NODE_OPTIONS
|
||||||
|
|
||||||
RUN addgroup --system --gid 1001 nodejs
|
RUN addgroup --system --gid 1001 nodejs
|
||||||
RUN adduser --system --uid 1001 nextjs
|
RUN adduser --system --uid 1001 nextjs
|
||||||
RUN npm install -g pnpm
|
|
||||||
|
|
||||||
RUN set -x \
|
RUN set -x \
|
||||||
&& apk add --no-cache curl
|
&& apk add --no-cache curl \
|
||||||
|
&& npm install -g pnpm
|
||||||
|
|
||||||
# Script dependencies
|
# Script dependencies
|
||||||
RUN pnpm --allow-build='@prisma/engines' add npm-run-all dotenv chalk semver prisma@6.18.0 @prisma/adapter-pg@6.18.0
|
RUN pnpm --allow-build='@prisma/engines' add npm-run-all dotenv chalk semver \
|
||||||
|
prisma@${PRISMA_VERSION} \
|
||||||
|
@prisma/adapter-pg@${PRISMA_VERSION}
|
||||||
|
|
||||||
COPY --from=builder --chown=nextjs:nodejs /app/public ./public
|
COPY --from=builder --chown=nextjs:nodejs /app/public ./public
|
||||||
COPY --from=builder /app/prisma ./prisma
|
COPY --from=builder /app/prisma ./prisma
|
||||||
|
|
|
||||||
24
package.json
24
package.json
|
|
@ -72,7 +72,7 @@
|
||||||
"@prisma/extension-read-replicas": "^0.4.1",
|
"@prisma/extension-read-replicas": "^0.4.1",
|
||||||
"@react-spring/web": "^10.0.3",
|
"@react-spring/web": "^10.0.3",
|
||||||
"@svgr/cli": "^8.1.0",
|
"@svgr/cli": "^8.1.0",
|
||||||
"@tanstack/react-query": "^5.90.5",
|
"@tanstack/react-query": "^5.90.11",
|
||||||
"@umami/react-zen": "^0.211.0",
|
"@umami/react-zen": "^0.211.0",
|
||||||
"@umami/redis-client": "^0.29.0",
|
"@umami/redis-client": "^0.29.0",
|
||||||
"bcryptjs": "^3.0.2",
|
"bcryptjs": "^3.0.2",
|
||||||
|
|
@ -92,7 +92,7 @@
|
||||||
"esbuild": "^0.25.11",
|
"esbuild": "^0.25.11",
|
||||||
"fs-extra": "^11.3.2",
|
"fs-extra": "^11.3.2",
|
||||||
"immer": "^10.2.0",
|
"immer": "^10.2.0",
|
||||||
"ipaddr.js": "^2.0.1",
|
"ipaddr.js": "^2.3.0",
|
||||||
"is-ci": "^3.0.1",
|
"is-ci": "^3.0.1",
|
||||||
"is-docker": "^3.0.0",
|
"is-docker": "^3.0.0",
|
||||||
"is-localhost-ip": "^2.0.0",
|
"is-localhost-ip": "^2.0.0",
|
||||||
|
|
@ -102,15 +102,15 @@
|
||||||
"kafkajs": "^2.1.0",
|
"kafkajs": "^2.1.0",
|
||||||
"lucide-react": "^0.543.0",
|
"lucide-react": "^0.543.0",
|
||||||
"maxmind": "^5.0.0",
|
"maxmind": "^5.0.0",
|
||||||
"next": "15.5.3",
|
"next": "^15.5.7",
|
||||||
"node-fetch": "^3.2.8",
|
"node-fetch": "^3.2.8",
|
||||||
"npm-run-all": "^4.1.5",
|
"npm-run-all": "^4.1.5",
|
||||||
"papaparse": "^5.5.3",
|
"papaparse": "^5.5.3",
|
||||||
"pg": "^8.16.3",
|
"pg": "^8.16.3",
|
||||||
"prisma": "^6.18.0",
|
"prisma": "^6.18.0",
|
||||||
"pure-rand": "^7.0.1",
|
"pure-rand": "^7.0.1",
|
||||||
"react": "^19.2.0",
|
"react": "^19.2.1",
|
||||||
"react-dom": "^19.2.0",
|
"react-dom": "^19.2.1",
|
||||||
"react-error-boundary": "^4.0.4",
|
"react-error-boundary": "^4.0.4",
|
||||||
"react-intl": "^7.1.14",
|
"react-intl": "^7.1.14",
|
||||||
"react-simple-maps": "^2.3.0",
|
"react-simple-maps": "^2.3.0",
|
||||||
|
|
@ -122,13 +122,13 @@
|
||||||
"thenby": "^1.3.4",
|
"thenby": "^1.3.4",
|
||||||
"ua-parser-js": "^2.0.6",
|
"ua-parser-js": "^2.0.6",
|
||||||
"uuid": "^11.1.0",
|
"uuid": "^11.1.0",
|
||||||
"zod": "^4.1.12",
|
"zod": "^4.1.13",
|
||||||
"zustand": "^5.0.8"
|
"zustand": "^5.0.9"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "^2.3.6",
|
"@biomejs/biome": "^2.3.8",
|
||||||
"@formatjs/cli": "^4.2.29",
|
"@formatjs/cli": "^4.2.29",
|
||||||
"@netlify/plugin-nextjs": "^5.14.4",
|
"@netlify/plugin-nextjs": "^5.15.1",
|
||||||
"@rollup/plugin-alias": "^5.0.0",
|
"@rollup/plugin-alias": "^5.0.0",
|
||||||
"@rollup/plugin-commonjs": "^25.0.4",
|
"@rollup/plugin-commonjs": "^25.0.4",
|
||||||
"@rollup/plugin-json": "^6.0.0",
|
"@rollup/plugin-json": "^6.0.0",
|
||||||
|
|
@ -138,7 +138,7 @@
|
||||||
"@rollup/plugin-typescript": "^12.3.0",
|
"@rollup/plugin-typescript": "^12.3.0",
|
||||||
"@types/jest": "^30.0.0",
|
"@types/jest": "^30.0.0",
|
||||||
"@types/node": "^24.9.2",
|
"@types/node": "^24.9.2",
|
||||||
"@types/react": "^19.2.2",
|
"@types/react": "^19.2.7",
|
||||||
"@types/react-dom": "^19.2.2",
|
"@types/react-dom": "^19.2.2",
|
||||||
"@types/react-window": "^1.8.8",
|
"@types/react-window": "^1.8.8",
|
||||||
"babel-plugin-react-compiler": "19.1.0-rc.2",
|
"babel-plugin-react-compiler": "19.1.0-rc.2",
|
||||||
|
|
@ -156,7 +156,7 @@
|
||||||
"rollup": "^4.52.5",
|
"rollup": "^4.52.5",
|
||||||
"rollup-plugin-copy": "^3.4.0",
|
"rollup-plugin-copy": "^3.4.0",
|
||||||
"rollup-plugin-delete": "^3.0.1",
|
"rollup-plugin-delete": "^3.0.1",
|
||||||
"rollup-plugin-dts": "^6.2.3",
|
"rollup-plugin-dts": "^6.3.0",
|
||||||
"rollup-plugin-node-externals": "^8.1.1",
|
"rollup-plugin-node-externals": "^8.1.1",
|
||||||
"rollup-plugin-peer-deps-external": "^2.2.4",
|
"rollup-plugin-peer-deps-external": "^2.2.4",
|
||||||
"rollup-plugin-postcss": "^4.0.2",
|
"rollup-plugin-postcss": "^4.0.2",
|
||||||
|
|
@ -165,7 +165,7 @@
|
||||||
"stylelint-config-prettier": "^9.0.3",
|
"stylelint-config-prettier": "^9.0.3",
|
||||||
"stylelint-config-recommended": "^14.0.0",
|
"stylelint-config-recommended": "^14.0.0",
|
||||||
"tar": "^6.1.2",
|
"tar": "^6.1.2",
|
||||||
"ts-jest": "^29.4.5",
|
"ts-jest": "^29.4.6",
|
||||||
"ts-node": "^10.9.1",
|
"ts-node": "^10.9.1",
|
||||||
"tsup": "^8.5.0",
|
"tsup": "^8.5.0",
|
||||||
"tsx": "^4.19.0",
|
"tsx": "^4.19.0",
|
||||||
|
|
|
||||||
2220
pnpm-lock.yaml
generated
2220
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
|
|
@ -21,6 +21,7 @@ export function AdminLayout({ children }: { children: ReactNode }) {
|
||||||
border="right"
|
border="right"
|
||||||
backgroundColor
|
backgroundColor
|
||||||
marginRight="2"
|
marginRight="2"
|
||||||
|
padding="3"
|
||||||
>
|
>
|
||||||
<AdminNav />
|
<AdminNav />
|
||||||
</Column>
|
</Column>
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,7 @@ export function UserEditForm({ userId, onSave }: { userId: string; onSave?: () =
|
||||||
label={formatMessage(labels.role)}
|
label={formatMessage(labels.role)}
|
||||||
rules={{ required: formatMessage(labels.required) }}
|
rules={{ required: formatMessage(labels.required) }}
|
||||||
>
|
>
|
||||||
<Select defaultSelectedKey={user.role}>
|
<Select defaultValue={user.role}>
|
||||||
<ListItem id={ROLES.viewOnly} data-test="dropdown-item-viewOnly">
|
<ListItem id={ROLES.viewOnly} data-test="dropdown-item-viewOnly">
|
||||||
{formatMessage(labels.viewOnly)}
|
{formatMessage(labels.viewOnly)}
|
||||||
</ListItem>
|
</ListItem>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { Icon, Text } from '@umami/react-zen';
|
import { IconLabel } from '@umami/react-zen';
|
||||||
import { LinkButton } from '@/components/common/LinkButton';
|
import { LinkButton } from '@/components/common/LinkButton';
|
||||||
import { PageHeader } from '@/components/common/PageHeader';
|
import { PageHeader } from '@/components/common/PageHeader';
|
||||||
import { useLink, useMessages, useSlug } from '@/components/hooks';
|
import { useLink, useMessages, useSlug } from '@/components/hooks';
|
||||||
|
|
@ -11,11 +11,8 @@ export function LinkHeader() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageHeader title={link.name} description={link.url} icon={<Link />}>
|
<PageHeader title={link.name} description={link.url} icon={<Link />}>
|
||||||
<LinkButton href={getSlugUrl(link.slug)} target="_blank">
|
<LinkButton href={getSlugUrl(link.slug)} target="_blank" prefetch={false} asAnchor>
|
||||||
<Icon>
|
<IconLabel icon={<ExternalLink />} label={formatMessage(labels.view)} />
|
||||||
<ExternalLink />
|
|
||||||
</Icon>
|
|
||||||
<Text>{formatMessage(labels.view)}</Text>
|
|
||||||
</LinkButton>
|
</LinkButton>
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { Icon, Text } from '@umami/react-zen';
|
import { IconLabel } from '@umami/react-zen';
|
||||||
import { LinkButton } from '@/components/common/LinkButton';
|
import { LinkButton } from '@/components/common/LinkButton';
|
||||||
import { PageHeader } from '@/components/common/PageHeader';
|
import { PageHeader } from '@/components/common/PageHeader';
|
||||||
import { useMessages, usePixel, useSlug } from '@/components/hooks';
|
import { useMessages, usePixel, useSlug } from '@/components/hooks';
|
||||||
|
|
@ -11,11 +11,8 @@ export function PixelHeader() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageHeader title={pixel.name} icon={<Grid2x2 />}>
|
<PageHeader title={pixel.name} icon={<Grid2x2 />}>
|
||||||
<LinkButton href={getSlugUrl(pixel.slug)} target="_blank" prefetch={false}>
|
<LinkButton href={getSlugUrl(pixel.slug)} target="_blank" prefetch={false} asAnchor>
|
||||||
<Icon>
|
<IconLabel icon={<ExternalLink />} label={formatMessage(labels.view)} />
|
||||||
<ExternalLink />
|
|
||||||
</Icon>
|
|
||||||
<Text>{formatMessage(labels.view)}</Text>
|
|
||||||
</LinkButton>
|
</LinkButton>
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ export function SettingsLayout({ children }: { children: ReactNode }) {
|
||||||
border="right"
|
border="right"
|
||||||
backgroundColor
|
backgroundColor
|
||||||
marginRight="2"
|
marginRight="2"
|
||||||
|
padding="3"
|
||||||
>
|
>
|
||||||
<SettingsNav />
|
<SettingsNav />
|
||||||
</Column>
|
</Column>
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import { SettingsLayout } from './SettingsLayout';
|
||||||
|
|
||||||
export default function ({ children }) {
|
export default function ({ children }) {
|
||||||
if (process.env.cloudMode) {
|
if (process.env.cloudMode) {
|
||||||
//return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <SettingsLayout>{children}</SettingsLayout>;
|
return <SettingsLayout>{children}</SettingsLayout>;
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ export interface LinkButtonProps extends ButtonProps {
|
||||||
scroll?: boolean;
|
scroll?: boolean;
|
||||||
variant?: any;
|
variant?: any;
|
||||||
prefetch?: boolean;
|
prefetch?: boolean;
|
||||||
|
asAnchor?: boolean;
|
||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -19,15 +20,22 @@ export function LinkButton({
|
||||||
target,
|
target,
|
||||||
prefetch,
|
prefetch,
|
||||||
children,
|
children,
|
||||||
|
asAnchor,
|
||||||
...props
|
...props
|
||||||
}: LinkButtonProps) {
|
}: LinkButtonProps) {
|
||||||
const { dir } = useLocale();
|
const { dir } = useLocale();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button {...props} variant={variant} asChild>
|
<Button {...props} variant={variant} asChild>
|
||||||
<Link href={href} dir={dir} scroll={scroll} target={target} prefetch={prefetch}>
|
{asAnchor ? (
|
||||||
{children}
|
<a href={href} target={target}>
|
||||||
</Link>
|
{children}
|
||||||
|
</a>
|
||||||
|
) : (
|
||||||
|
<Link href={href} dir={dir} scroll={scroll} target={target} prefetch={prefetch}>
|
||||||
|
{children}
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,9 @@ export function PageHeader({
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
</Column>
|
</Column>
|
||||||
<Row justifyContent="flex-end">{children}</Row>
|
<Row justifyContent="flex-end" alignItems="center">
|
||||||
|
{children}
|
||||||
|
</Row>
|
||||||
</Grid>
|
</Grid>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,7 @@ export function SideMenu({
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Column gap overflowY="auto" justifyContent="space-between">
|
<Column gap overflowY="auto" justifyContent="space-between" position="sticky" top="20px">
|
||||||
{title && (
|
{title && (
|
||||||
<Row padding>
|
<Row padding>
|
||||||
<Heading size="1">{title}</Heading>
|
<Heading size="1">{title}</Heading>
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
import { useToast } from '@umami/react-zen';
|
import { useToast } from '@umami/react-zen';
|
||||||
|
import type { ApiError } from '@/lib/types';
|
||||||
import { useApi } from '../useApi';
|
import { useApi } from '../useApi';
|
||||||
import { useModified } from '../useModified';
|
import { useModified } from '../useModified';
|
||||||
|
|
||||||
export function useUpdateQuery(path: string, params?: Record<string, any>) {
|
export function useUpdateQuery(path: string, params?: Record<string, any>) {
|
||||||
const { post, useMutation } = useApi();
|
const { post, useMutation } = useApi();
|
||||||
const query = useMutation({
|
const query = useMutation<any, ApiError, Record<string, any>>({
|
||||||
mutationFn: (data: Record<string, any>) => post(path, { ...data, ...params }),
|
mutationFn: (data: Record<string, any>) => post(path, { ...data, ...params }),
|
||||||
});
|
});
|
||||||
const { touch } = useModified();
|
const { touch } = useModified();
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { FormattedMessage, type MessageDescriptor, useIntl } from 'react-intl';
|
import { FormattedMessage, type MessageDescriptor, useIntl } from 'react-intl';
|
||||||
import { labels, messages } from '@/components/messages';
|
import { labels, messages } from '@/components/messages';
|
||||||
|
import type { ApiError } from '@/lib/types';
|
||||||
|
|
||||||
type FormatMessage = (
|
type FormatMessage = (
|
||||||
descriptor: MessageDescriptor,
|
descriptor: MessageDescriptor,
|
||||||
|
|
@ -12,7 +13,7 @@ interface UseMessages {
|
||||||
messages: typeof messages;
|
messages: typeof messages;
|
||||||
labels: typeof labels;
|
labels: typeof labels;
|
||||||
getMessage: (id: string) => string;
|
getMessage: (id: string) => string;
|
||||||
getErrorMessage: (error: unknown) => string | undefined;
|
getErrorMessage: (error: ApiError) => string | undefined;
|
||||||
FormattedMessage: typeof FormattedMessage;
|
FormattedMessage: typeof FormattedMessage;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -25,7 +26,7 @@ export function useMessages(): UseMessages {
|
||||||
return message ? formatMessage(message) : id;
|
return message ? formatMessage(message) : id;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getErrorMessage = (error: unknown) => {
|
const getErrorMessage = (error: ApiError) => {
|
||||||
if (!error) {
|
if (!error) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { Button, Icon, ListItem, Row, Select, Text } from '@umami/react-zen';
|
import { Button, Icon, ListItem, Row, Select, Text } from '@umami/react-zen';
|
||||||
import { isAfter } from 'date-fns';
|
import { isAfter } from 'date-fns';
|
||||||
import { useCallback, useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { useDateRange, useDateRangeQuery, useMessages, useNavigation } from '@/components/hooks';
|
import { useDateRange, useDateRangeQuery, useMessages, useNavigation } from '@/components/hooks';
|
||||||
import { ChevronRight } from '@/components/icons';
|
import { ChevronRight } from '@/components/icons';
|
||||||
import { getDateRangeValue } from '@/lib/date';
|
import { getDateRangeValue } from '@/lib/date';
|
||||||
|
|
@ -45,13 +45,9 @@ export function WebsiteDateFilter({
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleIncrement = useCallback(
|
const handleIncrement = increment => {
|
||||||
(increment: number) => {
|
router.push(updateParams({ offset: Number(offset) + increment }));
|
||||||
router.push(updateParams({ offset: +offset + increment }));
|
};
|
||||||
},
|
|
||||||
[offset],
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleSelect = (compare: any) => {
|
const handleSelect = (compare: any) => {
|
||||||
router.push(updateParams({ compare }));
|
router.push(updateParams({ compare }));
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -206,6 +206,10 @@ async function rawQuery(sql: string, data: Record<string, any>, name?: string):
|
||||||
return `$${params.length}${type ?? ''}`;
|
return `$${params.length}${type ?? ''}`;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (process.env.DATABASE_REPLICA_URL && '$replica' in client) {
|
||||||
|
return client.$replica().$queryRawUnsafe(query, ...params);
|
||||||
|
}
|
||||||
|
|
||||||
return client.$queryRawUnsafe(query, ...params);
|
return client.$queryRawUnsafe(query, ...params);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -296,10 +300,6 @@ function getSchema() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function getClient() {
|
function getClient() {
|
||||||
if (!process.env.DATABASE_URL) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const url = process.env.DATABASE_URL;
|
const url = process.env.DATABASE_URL;
|
||||||
const replicaUrl = process.env.DATABASE_REPLICA_URL;
|
const replicaUrl = process.env.DATABASE_REPLICA_URL;
|
||||||
const logQuery = process.env.LOG_QUERY;
|
const logQuery = process.env.LOG_QUERY;
|
||||||
|
|
@ -307,43 +307,49 @@ function getClient() {
|
||||||
const connectionUrl = new URL(url);
|
const connectionUrl = new URL(url);
|
||||||
const schema = connectionUrl.searchParams.get('schema') ?? undefined;
|
const schema = connectionUrl.searchParams.get('schema') ?? undefined;
|
||||||
|
|
||||||
const adapter = new PrismaPg({ connectionString: url.toString() }, { schema });
|
const baseAdapter = new PrismaPg({ connectionString: url }, { schema });
|
||||||
|
|
||||||
const prisma = new PrismaClient({
|
const baseClient = new PrismaClient({
|
||||||
adapter,
|
adapter: baseAdapter,
|
||||||
errorFormat: 'pretty',
|
errorFormat: 'pretty',
|
||||||
...(logQuery ? PRISMA_LOG_OPTIONS : {}),
|
...(logQuery ? PRISMA_LOG_OPTIONS : {}),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (replicaUrl) {
|
if (logQuery) {
|
||||||
const replicaAdapter = new PrismaPg({ connectionString: replicaUrl.toString() }, { schema });
|
baseClient.$on('query', log);
|
||||||
|
|
||||||
const replicaClient = new PrismaClient({
|
|
||||||
adapter: replicaAdapter,
|
|
||||||
...(logQuery ? PRISMA_LOG_OPTIONS : {}),
|
|
||||||
});
|
|
||||||
|
|
||||||
prisma.$extends(
|
|
||||||
readReplicas({
|
|
||||||
replicas: [replicaClient],
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!replicaUrl) {
|
||||||
|
log('Prisma initialized');
|
||||||
|
globalThis[PRISMA] ??= baseClient;
|
||||||
|
return baseClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
const replicaAdapter = new PrismaPg({ connectionString: replicaUrl }, { schema });
|
||||||
|
|
||||||
|
const replicaClient = new PrismaClient({
|
||||||
|
adapter: replicaAdapter,
|
||||||
|
errorFormat: 'pretty',
|
||||||
|
...(logQuery ? PRISMA_LOG_OPTIONS : {}),
|
||||||
|
});
|
||||||
|
|
||||||
if (logQuery) {
|
if (logQuery) {
|
||||||
prisma.$on('query' as never, log);
|
replicaClient.$on('query', log);
|
||||||
}
|
}
|
||||||
|
|
||||||
log('Prisma initialized');
|
const extended = baseClient.$extends(
|
||||||
|
readReplicas({
|
||||||
|
replicas: [replicaClient],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
if (!globalThis[PRISMA]) {
|
log('Prisma initialized (with replica)');
|
||||||
globalThis[PRISMA] = prisma;
|
globalThis[PRISMA] ??= extended;
|
||||||
}
|
|
||||||
|
|
||||||
return prisma;
|
return extended;
|
||||||
}
|
}
|
||||||
|
|
||||||
const client: PrismaClient = globalThis[PRISMA] || getClient();
|
const client = (globalThis[PRISMA] || getClient()) as ReturnType<typeof getClient>;
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
client,
|
client,
|
||||||
|
|
|
||||||
|
|
@ -136,3 +136,8 @@ export interface RealtimeData {
|
||||||
urls: Record<string, number>;
|
urls: Record<string, number>;
|
||||||
visitors: any[];
|
visitors: any[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ApiError extends Error {
|
||||||
|
code?: string;
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue