diff --git a/packages/db/prisma/migrations/20240408181306_rename_columns/migration.sql b/packages/db/prisma/migrations/20240408181306_rename_columns/migration.sql
new file mode 100644
index 00000000..ea81f8c5
--- /dev/null
+++ b/packages/db/prisma/migrations/20240408181306_rename_columns/migration.sql
@@ -0,0 +1,97 @@
+-- Project
+-- organization_slug -> organizationSlug
+ALTER TABLE
+ IF EXISTS "projects" RENAME COLUMN "organization_slug" TO "organizationSlug";
+
+-- ProjectAccess
+-- project_id -> projectId
+-- organization_slug -> organizationSlug
+-- user_id -> userId
+ALTER TABLE
+ IF EXISTS "project_access" RENAME COLUMN "project_id" TO "projectId";
+
+ALTER TABLE
+ IF EXISTS "project_access" RENAME COLUMN "organization_slug" TO "organizationSlug";
+
+ALTER TABLE
+ IF EXISTS "project_access" RENAME COLUMN "user_id" TO "userId";
+
+-- Event
+-- project_id -> projectId
+-- profile_id -> profileId
+ALTER TABLE
+ IF EXISTS "events" RENAME COLUMN "project_id" TO "projectId";
+
+ALTER TABLE
+ IF EXISTS "events" RENAME COLUMN "profile_id" TO "profileId";
+
+-- Profile
+-- external_id -> externalId
+-- first_name -> firstName
+-- last_name -> lastName
+-- project_id -> projectId
+ALTER TABLE
+ IF EXISTS "profiles" RENAME COLUMN "external_id" TO "externalId";
+
+ALTER TABLE
+ IF EXISTS "profiles" RENAME COLUMN "first_name" TO "firstName";
+
+ALTER TABLE
+ IF EXISTS "profiles" RENAME COLUMN "last_name" TO "lastName";
+
+ALTER TABLE
+ IF EXISTS "profiles" RENAME COLUMN "project_id" TO "projectId";
+
+-- Client
+-- project_id -> projectId
+-- organization_slug -> organizationSlug
+ALTER TABLE
+ IF EXISTS "clients" RENAME COLUMN "project_id" TO "projectId";
+
+ALTER TABLE
+ IF EXISTS "clients" RENAME COLUMN "organization_slug" TO "organizationSlug";
+
+-- Dashboard
+-- organization_slug -> organizationSlug
+-- project_id -> projectId
+ALTER TABLE
+ IF EXISTS "dashboards" RENAME COLUMN "organization_slug" TO "organizationSlug";
+
+ALTER TABLE
+ IF EXISTS "dashboards" RENAME COLUMN "project_id" TO "projectId";
+
+-- Report
+-- chart_type -> chartType
+-- line_type -> lineType
+-- project_id -> projectId
+-- dashboard_id -> dashboardId
+ALTER TABLE
+ IF EXISTS "reports" RENAME COLUMN "chart_type" TO "chartType";
+
+ALTER TABLE
+ IF EXISTS "reports" RENAME COLUMN "line_type" TO "lineType";
+
+ALTER TABLE
+ IF EXISTS "reports" RENAME COLUMN "project_id" TO "projectId";
+
+ALTER TABLE
+ IF EXISTS "reports" RENAME COLUMN "dashboard_id" TO "dashboardId";
+
+-- ShareOverview
+-- project_id -> projectId
+-- organization_slug -> organizationSlug
+ALTER TABLE
+ IF EXISTS "shares" RENAME COLUMN "project_id" TO "projectId";
+
+ALTER TABLE
+ IF EXISTS "shares" RENAME COLUMN "organization_slug" TO "organizationSlug";
+
+-- EventMeta (ta bort constraint)
+-- project_id -> projectId
+ALTER TABLE
+ IF EXISTS "event_meta" RENAME COLUMN "project_id" TO "projectId";
+
+-- Reference
+-- project_id -> projectId
+ALTER TABLE
+ IF EXISTS "references" RENAME COLUMN "project_id" TO "projectId";
\ No newline at end of file
diff --git a/packages/db/prisma/migrations/20240408184129_rename_colimns_2/migration.sql b/packages/db/prisma/migrations/20240408184129_rename_colimns_2/migration.sql
new file mode 100644
index 00000000..d48e7702
--- /dev/null
+++ b/packages/db/prisma/migrations/20240408184129_rename_colimns_2/migration.sql
@@ -0,0 +1,35 @@
+-- RenameForeignKey
+ALTER TABLE "clients" RENAME CONSTRAINT "clients_project_id_fkey" TO "clients_projectId_fkey";
+
+-- RenameForeignKey
+ALTER TABLE "dashboards" RENAME CONSTRAINT "dashboards_project_id_fkey" TO "dashboards_projectId_fkey";
+
+-- RenameForeignKey
+ALTER TABLE "event_meta" RENAME CONSTRAINT "event_meta_project_id_fkey" TO "event_meta_projectId_fkey";
+
+-- RenameForeignKey
+ALTER TABLE "events" RENAME CONSTRAINT "events_project_id_fkey" TO "events_projectId_fkey";
+
+-- RenameForeignKey
+ALTER TABLE "profiles" RENAME CONSTRAINT "profiles_project_id_fkey" TO "profiles_projectId_fkey";
+
+-- RenameForeignKey
+ALTER TABLE "project_access" RENAME CONSTRAINT "project_access_project_id_fkey" TO "project_access_projectId_fkey";
+
+-- RenameForeignKey
+ALTER TABLE "references" RENAME CONSTRAINT "references_project_id_fkey" TO "references_projectId_fkey";
+
+-- RenameForeignKey
+ALTER TABLE "reports" RENAME CONSTRAINT "reports_dashboard_id_fkey" TO "reports_dashboardId_fkey";
+
+-- RenameForeignKey
+ALTER TABLE "reports" RENAME CONSTRAINT "reports_project_id_fkey" TO "reports_projectId_fkey";
+
+-- RenameForeignKey
+ALTER TABLE "shares" RENAME CONSTRAINT "shares_project_id_fkey" TO "shares_projectId_fkey";
+
+-- RenameIndex
+ALTER INDEX "event_meta_name_project_id_key" RENAME TO "event_meta_name_projectId_key";
+
+-- RenameIndex
+ALTER INDEX "shares_project_id_key" RENAME TO "shares_projectId_key";
diff --git a/packages/db/prisma/schema.prisma b/packages/db/prisma/schema.prisma
index 0adb4fc1..77cdf9dd 100644
--- a/packages/db/prisma/schema.prisma
+++ b/packages/db/prisma/schema.prisma
@@ -11,23 +11,23 @@ datasource db {
}
model Project {
- id String @id @default(dbgenerated("gen_random_uuid()"))
- name String
- organization_slug String
- events Event[]
- eventsCount Int @default(0)
- profiles Profile[]
- clients Client[]
+ id String @id @default(dbgenerated("gen_random_uuid()"))
+ name String
+ organizationSlug String
+ eventsCount Int @default(0)
- createdAt DateTime @default(now())
- updatedAt DateTime @default(now()) @updatedAt
+ events Event[]
+ profiles Profile[]
+ clients Client[]
reports Report[]
dashboards Dashboard[]
share ShareOverview?
- EventMeta EventMeta[]
- Reference Reference[]
+ meta EventMeta[]
+ references Reference[]
+ access ProjectAccess[]
- access ProjectAccess[]
+ createdAt DateTime @default(now())
+ updatedAt DateTime @default(now()) @updatedAt
@@map("projects")
}
@@ -39,14 +39,14 @@ enum AccessLevel {
}
model ProjectAccess {
- id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
- project_id String
- project Project @relation(fields: [project_id], references: [id])
- organization_slug String
- user_id String
- level AccessLevel
- createdAt DateTime @default(now())
- updatedAt DateTime @default(now()) @updatedAt
+ id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
+ projectId String
+ project Project @relation(fields: [projectId], references: [id])
+ organizationSlug String
+ userId String
+ level AccessLevel
+ createdAt DateTime @default(now())
+ updatedAt DateTime @default(now()) @updatedAt
@@map("project_access")
}
@@ -55,10 +55,10 @@ model Event {
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
name String
properties Json
- project_id String
- project Project @relation(fields: [project_id], references: [id])
+ projectId String
+ project Project @relation(fields: [projectId], references: [id])
- profile_id String?
+ profileId String?
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
@@ -75,29 +75,29 @@ model Salt {
}
model Profile {
- id String @id
- external_id String?
- first_name String?
- last_name String?
- email String?
- avatar String?
- properties Json
- project_id String
- project Project @relation(fields: [project_id], references: [id])
- createdAt DateTime @default(now())
- updatedAt DateTime @default(now()) @updatedAt
+ id String @id
+ externalId String?
+ firstName String?
+ lastName String?
+ email String?
+ avatar String?
+ properties Json
+ projectId String
+ project Project @relation(fields: [projectId], references: [id])
+ createdAt DateTime @default(now())
+ updatedAt DateTime @default(now()) @updatedAt
@@map("profiles")
}
model Client {
- id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
- name String
- secret String?
- project_id String
- project Project @relation(fields: [project_id], references: [id])
- organization_slug String
- cors String @default("*")
+ id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
+ name String
+ secret String?
+ projectId String
+ project Project @relation(fields: [projectId], references: [id])
+ organizationSlug String
+ cors String @default("*")
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
@@ -124,12 +124,12 @@ enum ChartType {
}
model Dashboard {
- id String @id @default(dbgenerated("gen_random_uuid()"))
- name String
- organization_slug String
- project_id String
- project Project @relation(fields: [project_id], references: [id])
- reports Report[]
+ id String @id @default(dbgenerated("gen_random_uuid()"))
+ name String
+ organizationSlug String
+ projectId String
+ project Project @relation(fields: [projectId], references: [id])
+ reports Report[]
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
@@ -149,19 +149,19 @@ model Report {
name String
interval Interval
range String @default("1m")
- chart_type ChartType
- line_type String @default("monotone")
+ chartType ChartType
+ lineType String @default("monotone")
breakdowns Json
events Json
formula String?
unit String?
metric Metric @default(sum)
- project_id String
- project Project @relation(fields: [project_id], references: [id])
+ projectId String
+ project Project @relation(fields: [projectId], references: [id])
previous Boolean @default(false)
- dashboard_id String
- dashboard Dashboard @relation(fields: [dashboard_id], references: [id])
+ dashboardId String
+ dashboard Dashboard @relation(fields: [dashboardId], references: [id])
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
@@ -180,14 +180,14 @@ model Waitlist {
}
model ShareOverview {
- id String @unique
- project_id String @unique
- project Project @relation(fields: [project_id], references: [id])
- organization_slug String
- public Boolean @default(false)
- password String?
- createdAt DateTime @default(now())
- updatedAt DateTime @default(now()) @updatedAt
+ id String @unique
+ projectId String @unique
+ project Project @relation(fields: [projectId], references: [id])
+ organizationSlug String
+ public Boolean @default(false)
+ password String?
+ createdAt DateTime @default(now())
+ updatedAt DateTime @default(now()) @updatedAt
@@map("shares")
}
@@ -198,13 +198,13 @@ model EventMeta {
conversion Boolean?
color String?
icon String?
- project_id String
- project Project @relation(fields: [project_id], references: [id])
+ projectId String
+ project Project @relation(fields: [projectId], references: [id])
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
- @@unique([name, project_id])
+ @@unique([name, projectId])
@@map("event_meta")
}
@@ -213,8 +213,8 @@ model Reference {
title String
description String?
date DateTime @default(now())
- project_id String
- project Project @relation(fields: [project_id], references: [id])
+ projectId String
+ project Project @relation(fields: [projectId], references: [id])
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
diff --git a/packages/db/src/services/clients.service.ts b/packages/db/src/services/clients.service.ts
index 417d3df2..d0f6e3ff 100644
--- a/packages/db/src/services/clients.service.ts
+++ b/packages/db/src/services/clients.service.ts
@@ -1,38 +1,20 @@
-import type { Client } from '../prisma-client';
+import type { Client, Prisma } from '../prisma-client';
import { db } from '../prisma-client';
-import { transformProject } from './project.service';
-import type { IServiceProject } from './project.service';
-export type IServiceClient = ReturnType;
-export type IServiceClientWithProject = IServiceClient & {
- project: Exclude;
-};
-
-export function transformClient({ organization_slug, ...client }: Client) {
- return {
- ...client,
- organizationSlug: organization_slug,
+export type IServiceClient = Client;
+export type IServiceClientWithProject = Prisma.ClientGetPayload<{
+ include: {
+ project: true;
};
-}
+}>;
-export async function getClientsByOrganizationId(organizationId: string) {
- const clients = await db.client.findMany({
+export async function getClientsByOrganizationSlug(organizationSlug: string) {
+ return db.client.findMany({
where: {
- organization_slug: organizationId,
+ organizationSlug,
},
include: {
project: true,
},
});
-
- return clients
- .map((client) => {
- return {
- ...transformClient(client),
- project: transformProject(client.project),
- };
- })
- .filter(
- (client): client is IServiceClientWithProject => client.project !== null
- );
}
diff --git a/packages/db/src/services/dashboard.service.ts b/packages/db/src/services/dashboard.service.ts
index 16f4e65e..58afcf9e 100644
--- a/packages/db/src/services/dashboard.service.ts
+++ b/packages/db/src/services/dashboard.service.ts
@@ -1,15 +1,18 @@
+import type { Dashboard, Prisma } from '../prisma-client';
import { db } from '../prisma-client';
-export type IServiceDashboard = Awaited>;
-export type IServiceDashboards = Awaited<
- ReturnType
->;
+export type IServiceDashboard = Dashboard;
+export type IServiceDashboards = Prisma.DashboardGetPayload<{
+ include: {
+ project: true;
+ };
+}>[];
export async function getDashboardById(id: string, projectId: string) {
const dashboard = await db.dashboard.findUnique({
where: {
id,
- project_id: projectId,
+ projectId,
},
include: {
project: true,
@@ -26,7 +29,7 @@ export async function getDashboardById(id: string, projectId: string) {
export function getDashboardsByProjectId(projectId: string) {
return db.dashboard.findMany({
where: {
- project_id: projectId,
+ projectId,
},
include: {
project: true,
diff --git a/packages/db/src/services/event.service.ts b/packages/db/src/services/event.service.ts
index 203a9522..a4e074a7 100644
--- a/packages/db/src/services/event.service.ts
+++ b/packages/db/src/services/event.service.ts
@@ -144,7 +144,7 @@ export async function getEvents(
name: {
in: names,
},
- project_id: events[0]?.project_id,
+ projectId: events[0]?.project_id,
},
select: options.meta === true ? undefined : options.meta,
});
@@ -265,7 +265,7 @@ export async function getEventList({
sb.where.projectId = `project_id = ${escape(projectId)}`;
if (profileId) {
- sb.where.deviceId = `device_id IN (SELECT device_id as did FROM openpanel.events WHERE profile_id = ${escape(profileId)} group by did)`;
+ sb.where.deviceId = `device_id IN (SELECT device_id as did FROM events WHERE profile_id = ${escape(profileId)} group by did)`;
}
if (events && events.length > 0) {
@@ -357,7 +357,7 @@ export function createBotEvent({
export function getConversionEventNames(projectId: string) {
return db.eventMeta.findMany({
where: {
- project_id: projectId,
+ projectId,
conversion: true,
},
});
diff --git a/packages/db/src/services/organization.service.ts b/packages/db/src/services/organization.service.ts
index cb0821c9..9dba8edd 100644
--- a/packages/db/src/services/organization.service.ts
+++ b/packages/db/src/services/organization.service.ts
@@ -11,7 +11,7 @@ import { db } from '../prisma-client';
export type IServiceOrganization = ReturnType;
export type IServiceInvite = ReturnType;
export type IServiceMember = ReturnType;
-export type IServiceProjectAccess = ReturnType;
+export type IServiceProjectAccess = ProjectAccess;
export function transformOrganization(org: Organization) {
return {
@@ -21,15 +21,6 @@ export function transformOrganization(org: Organization) {
};
}
-export function transformAccess(access: ProjectAccess) {
- return {
- projectId: access.project_id,
- userId: access.user_id,
- level: access.level,
- organizationSlug: access.organization_slug,
- };
-}
-
export async function getCurrentOrganizations() {
const session = auth();
const organizations = await clerkClient.users.getOrganizationMembershipList({
@@ -53,7 +44,7 @@ export async function getOrganizationByProjectId(projectId: string) {
});
return clerkClient.organizations.getOrganization({
- slug: project.organization_slug,
+ slug: project.organizationSlug,
});
}
@@ -110,7 +101,7 @@ export async function getMembers(organizationSlug: string) {
}),
db.projectAccess.findMany({
where: {
- organization_slug: organizationSlug,
+ organizationSlug,
},
}),
]);
@@ -118,11 +109,11 @@ export async function getMembers(organizationSlug: string) {
return members
.map((member) => {
const projectAccess = access.filter(
- (item) => item.user_id === member.publicUserData?.userId
+ (item) => item.userId === member.publicUserData?.userId
);
return {
...member,
- access: projectAccess.map(transformAccess),
+ access: projectAccess,
};
})
.map(transformMember);
diff --git a/packages/db/src/services/project.service.ts b/packages/db/src/services/project.service.ts
index 1c82d38e..24042c28 100644
--- a/packages/db/src/services/project.service.ts
+++ b/packages/db/src/services/project.service.ts
@@ -1,17 +1,9 @@
import { auth } from '@clerk/nextjs';
-import { project } from 'ramda';
import type { Project } from '../prisma-client';
import { db } from '../prisma-client';
-export type IServiceProject = ReturnType;
-
-export function transformProject({ organization_slug, ...project }: Project) {
- return {
- organizationSlug: organization_slug,
- ...project,
- };
-}
+export type IServiceProject = Project;
export async function getProjectById(id: string) {
const res = await db.project.findUnique({
@@ -24,46 +16,32 @@ export async function getProjectById(id: string) {
return null;
}
- return transformProject(res);
+ return res;
}
-export async function getProjectsByOrganizationSlug(slug: string) {
- const res = await db.project.findMany({
+export async function getProjectsByOrganizationSlug(organizationSlug: string) {
+ return db.project.findMany({
where: {
- organization_slug: slug,
+ organizationSlug,
},
orderBy: {
createdAt: 'desc',
},
});
-
- return res.map(transformProject);
}
-export async function getCurrentProjects(slug: string) {
+export async function getCurrentProjects(organizationSlug: string) {
const session = auth();
if (!session.userId) {
return [];
}
- const access = await db.projectAccess.findMany({
+ return db.project.findMany({
where: {
- organization_slug: slug,
- user_id: session.userId,
+ organizationSlug,
+ },
+ include: {
+ access: true,
},
});
-
- const res = await db.project.findMany({
- where: {
- organization_slug: slug,
- },
- });
-
- if (access.length === 0) {
- return res.map(transformProject);
- }
-
- return res
- .filter((project) => access.some((a) => a.project_id === project.id))
- .map(transformProject);
}
diff --git a/packages/db/src/services/reference.service.ts b/packages/db/src/services/reference.service.ts
index 043a6cc0..951fe5b4 100644
--- a/packages/db/src/services/reference.service.ts
+++ b/packages/db/src/services/reference.service.ts
@@ -1,19 +1,7 @@
import type { Prisma, Reference } from '../prisma-client';
import { db } from '../prisma-client';
-export type IServiceReference = Omit & {
- projectId: string;
-};
-
-export function transformReference({
- project_id,
- ...item
-}: Reference): IServiceReference {
- return {
- ...item,
- projectId: project_id,
- };
-}
+export type IServiceReference = Reference;
export async function getReferenceById(id: string) {
const reference = await db.reference.findUnique({
@@ -26,7 +14,7 @@ export async function getReferenceById(id: string) {
return null;
}
- return transformReference(reference);
+ return reference;
}
export async function getReferences({
@@ -38,11 +26,9 @@ export async function getReferences({
take?: number;
skip?: number;
}) {
- const references = await db.reference.findMany({
+ return db.reference.findMany({
where,
take: take ?? 50,
skip,
});
-
- return references.map(transformReference);
}
diff --git a/packages/db/src/services/reports.service.ts b/packages/db/src/services/reports.service.ts
index 86703076..b33fb950 100644
--- a/packages/db/src/services/reports.service.ts
+++ b/packages/db/src/services/reports.service.ts
@@ -45,11 +45,11 @@ export function transformReport(
): IChartInput & { id: string } {
return {
id: report.id,
- projectId: report.project_id,
+ projectId: report.projectId,
events: (report.events as IChartEvent[]).map(transformReportEvent),
breakdowns: report.breakdowns as IChartBreakdown[],
- chartType: report.chart_type,
- lineType: (report.line_type as IChartLineType) ?? lineTypes.monotone,
+ chartType: report.chartType,
+ lineType: (report.lineType as IChartLineType) ?? lineTypes.monotone,
interval: report.interval,
name: report.name || 'Untitled',
range: (report.range as IChartRange) ?? timeRanges['1m'],
@@ -64,7 +64,7 @@ export function getReportsByDashboardId(dashboardId: string) {
return db.report
.findMany({
where: {
- dashboard_id: dashboardId,
+ dashboardId,
},
})
.then((reports) => reports.map(transformReport));
diff --git a/packages/db/src/services/share.service.ts b/packages/db/src/services/share.service.ts
index ba527388..a1149f52 100644
--- a/packages/db/src/services/share.service.ts
+++ b/packages/db/src/services/share.service.ts
@@ -10,3 +10,11 @@ export function getShareOverviewById(id: string) {
},
});
}
+
+export function getShareByProjectId(projectId: string) {
+ return db.shareOverview.findUnique({
+ where: {
+ projectId,
+ },
+ });
+}
diff --git a/packages/validation/src/index.ts b/packages/validation/src/index.ts
index 01713bb6..875816b7 100644
--- a/packages/validation/src/index.ts
+++ b/packages/validation/src/index.ts
@@ -85,7 +85,7 @@ export const zInviteUser = z.object({
});
export const zShareOverview = z.object({
- organizationId: z.string(),
+ organizationSlug: z.string(),
projectId: z.string(),
password: z.string().nullable(),
public: z.boolean(),
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 020f2893..034343a3 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -301,8 +301,8 @@ importers:
specifier: ^0.1.3
version: 0.1.3
pushmodal:
- specifier: ^0.0.8
- version: 0.0.8(@radix-ui/react-dialog@1.0.5)(react-dom@18.2.0)(react@18.2.0)
+ specifier: ^1.0.0
+ version: 1.0.0(@radix-ui/react-dialog@1.0.5)(react-dom@18.2.0)(react@18.2.0)
ramda:
specifier: ^0.29.1
version: 0.29.1
@@ -14709,8 +14709,8 @@ packages:
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
engines: {node: '>=6'}
- /pushmodal@0.0.8(@radix-ui/react-dialog@1.0.5)(react-dom@18.2.0)(react@18.2.0):
- resolution: {integrity: sha512-Fmsx9Fadnv51WPcXvX7x4N8BbLxMDdSStpfJ1WY4pyjqXEEht+3H51TwjZJc5PkGMURdsvi7py76SFgsmrujnw==}
+ /pushmodal@1.0.0(@radix-ui/react-dialog@1.0.5)(react-dom@18.2.0)(react@18.2.0):
+ resolution: {integrity: sha512-34JSZHJHGTcLqBgYk9Fyiw5vBYJZrcgoDE7GfHehKKzxBt/Ro2bSLTIGRnzQ+NRv389GxH6WXCBUH+6VJ1wvTg==}
peerDependencies:
'@radix-ui/react-dialog': ^1.0.0
react: ^16.12.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^20.0.0 || ^21.0.0