mirror of
https://github.com/eliasstepanik/core.git
synced 2026-01-11 10:08:27 +00:00
Feat: added database
This commit is contained in:
parent
d3f81404db
commit
72edab887e
30
apps/webapp/app/env.server.ts
Normal file
30
apps/webapp/app/env.server.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import { z } from "zod";
|
||||
import { isValidDatabaseUrl } from "./utils/db";
|
||||
|
||||
const EnvironmentSchema = z.object({
|
||||
NODE_ENV: z.union([z.literal("development"), z.literal("production"), z.literal("test")]),
|
||||
DATABASE_URL: z
|
||||
.string()
|
||||
.refine(
|
||||
isValidDatabaseUrl,
|
||||
"DATABASE_URL is invalid, for details please check the additional output above this message."
|
||||
),
|
||||
DATABASE_CONNECTION_LIMIT: z.coerce.number().int().default(10),
|
||||
DATABASE_POOL_TIMEOUT: z.coerce.number().int().default(60),
|
||||
DATABASE_CONNECTION_TIMEOUT: z.coerce.number().int().default(20),
|
||||
DIRECT_URL: z
|
||||
.string()
|
||||
.refine(
|
||||
isValidDatabaseUrl,
|
||||
"DIRECT_URL is invalid, for details please check the additional output above this message."
|
||||
),
|
||||
DATABASE_READ_REPLICA_URL: z.string().optional(),
|
||||
SESSION_SECRET: z.string(),
|
||||
|
||||
APP_ENV: z.string().default(process.env.NODE_ENV),
|
||||
APP_ORIGIN: z.string().default("http://localhost:5173"),
|
||||
POSTHOG_PROJECT_KEY: z.string().default(""),
|
||||
});
|
||||
|
||||
export type Environment = z.infer<typeof EnvironmentSchema>;
|
||||
export const env = EnvironmentSchema.parse(process.env);
|
||||
177
apps/webapp/app/models/message.server.ts
Normal file
177
apps/webapp/app/models/message.server.ts
Normal file
@ -0,0 +1,177 @@
|
||||
import { json, type Session , createCookieSessionStorage } from "@remix-run/node";
|
||||
import { redirect } from "remix-typedjson";
|
||||
import { env } from "~/env.server";
|
||||
|
||||
export type ToastMessage = {
|
||||
message: string;
|
||||
type: "success" | "error";
|
||||
options: Required<ToastMessageOptions>;
|
||||
};
|
||||
|
||||
export type ToastMessageOptions = {
|
||||
/** Ephemeral means it disappears after a delay, defaults to true */
|
||||
ephemeral?: boolean;
|
||||
};
|
||||
|
||||
const ONE_YEAR = 1000 * 60 * 60 * 24 * 365;
|
||||
|
||||
export const { commitSession, getSession } = createCookieSessionStorage({
|
||||
cookie: {
|
||||
name: "__message",
|
||||
path: "/",
|
||||
httpOnly: true,
|
||||
sameSite: "lax",
|
||||
secrets: [env.SESSION_SECRET],
|
||||
secure: env.NODE_ENV === "production",
|
||||
},
|
||||
});
|
||||
|
||||
export function setSuccessMessage(
|
||||
session: Session,
|
||||
message: string,
|
||||
options?: ToastMessageOptions
|
||||
) {
|
||||
session.flash("toastMessage", {
|
||||
message,
|
||||
type: "success",
|
||||
options: {
|
||||
ephemeral: options?.ephemeral ?? true,
|
||||
},
|
||||
} as ToastMessage);
|
||||
}
|
||||
|
||||
export function setErrorMessage(session: Session, message: string, options?: ToastMessageOptions) {
|
||||
session.flash("toastMessage", {
|
||||
message,
|
||||
type: "error",
|
||||
options: {
|
||||
ephemeral: options?.ephemeral ?? true,
|
||||
},
|
||||
} as ToastMessage);
|
||||
}
|
||||
|
||||
export async function setRequestErrorMessage(
|
||||
request: Request,
|
||||
message: string,
|
||||
options?: ToastMessageOptions
|
||||
) {
|
||||
const session = await getSession(request.headers.get("cookie"));
|
||||
|
||||
setErrorMessage(session, message, options);
|
||||
|
||||
return session;
|
||||
}
|
||||
|
||||
export async function setRequestSuccessMessage(
|
||||
request: Request,
|
||||
message: string,
|
||||
options?: ToastMessageOptions
|
||||
) {
|
||||
const session = await getSession(request.headers.get("cookie"));
|
||||
|
||||
setSuccessMessage(session, message, options);
|
||||
|
||||
return session;
|
||||
}
|
||||
|
||||
export async function setToastMessageCookie(session: Session) {
|
||||
return {
|
||||
"Set-Cookie": await commitSession(session, {
|
||||
expires: new Date(Date.now() + ONE_YEAR),
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
export async function jsonWithSuccessMessage(
|
||||
data: any,
|
||||
request: Request,
|
||||
message: string,
|
||||
options?: ToastMessageOptions
|
||||
) {
|
||||
const session = await getSession(request.headers.get("cookie"));
|
||||
|
||||
setSuccessMessage(session, message, options);
|
||||
|
||||
return json(data, {
|
||||
headers: {
|
||||
"Set-Cookie": await commitSession(session, {
|
||||
expires: new Date(Date.now() + ONE_YEAR),
|
||||
}),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function jsonWithErrorMessage(
|
||||
data: any,
|
||||
request: Request,
|
||||
message: string,
|
||||
options?: ToastMessageOptions
|
||||
) {
|
||||
const session = await getSession(request.headers.get("cookie"));
|
||||
|
||||
setErrorMessage(session, message, options);
|
||||
|
||||
return json(data, {
|
||||
headers: {
|
||||
"Set-Cookie": await commitSession(session, {
|
||||
expires: new Date(Date.now() + ONE_YEAR),
|
||||
}),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function redirectWithSuccessMessage(
|
||||
path: string,
|
||||
request: Request,
|
||||
message: string,
|
||||
options?: ToastMessageOptions
|
||||
) {
|
||||
const session = await getSession(request.headers.get("cookie"));
|
||||
|
||||
setSuccessMessage(session, message, options);
|
||||
|
||||
return redirect(path, {
|
||||
headers: {
|
||||
"Set-Cookie": await commitSession(session, {
|
||||
expires: new Date(Date.now() + ONE_YEAR),
|
||||
}),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function redirectWithErrorMessage(
|
||||
path: string,
|
||||
request: Request,
|
||||
message: string,
|
||||
options?: ToastMessageOptions
|
||||
) {
|
||||
const session = await getSession(request.headers.get("cookie"));
|
||||
|
||||
setErrorMessage(session, message, options);
|
||||
|
||||
return redirect(path, {
|
||||
headers: {
|
||||
"Set-Cookie": await commitSession(session, {
|
||||
expires: new Date(Date.now() + ONE_YEAR),
|
||||
}),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function redirectBackWithErrorMessage(
|
||||
request: Request,
|
||||
message: string,
|
||||
options?: ToastMessageOptions
|
||||
) {
|
||||
const url = new URL(request.url);
|
||||
return redirectWithErrorMessage(url.pathname, request, message, options);
|
||||
}
|
||||
|
||||
export async function redirectBackWithSuccessMessage(
|
||||
request: Request,
|
||||
message: string,
|
||||
options?: ToastMessageOptions
|
||||
) {
|
||||
const url = new URL(request.url);
|
||||
return redirectWithSuccessMessage(url.pathname, request, message, options);
|
||||
}
|
||||
@ -1,15 +1,11 @@
|
||||
import {
|
||||
Links,
|
||||
Meta,
|
||||
Outlet,
|
||||
Scripts,
|
||||
ScrollRestoration,
|
||||
} from "@remix-run/react";
|
||||
import type { LinksFunction } from "@remix-run/node";
|
||||
import { Links, Meta, Outlet, Scripts, ScrollRestoration } from "@remix-run/react";
|
||||
import type { LinksFunction, LoaderFunctionArgs, MetaFunction } from "@remix-run/node";
|
||||
import { type UseDataFunctionReturn, typedjson, useTypedLoaderData } from "remix-typedjson";
|
||||
|
||||
import tailwindStylesheetUrl from "~/tailwind.css";
|
||||
import { appEnvTitleTag } from "./utils";
|
||||
|
||||
import { commitSession, getSession, type ToastMessage } from "./models/message.server";
|
||||
import { env } from "./env.server";
|
||||
|
||||
export const links: LinksFunction = () => [
|
||||
{ rel: "preconnect", href: "https://fonts.googleapis.com" },
|
||||
@ -22,9 +18,26 @@ export const links: LinksFunction = () => [
|
||||
rel: "stylesheet",
|
||||
href: "https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap",
|
||||
},
|
||||
{ rel: "stylesheet", href: tailwindStylesheetUrl }
|
||||
{ rel: "stylesheet", href: tailwindStylesheetUrl },
|
||||
];
|
||||
|
||||
export const loader = async ({ request }: LoaderFunctionArgs) => {
|
||||
const session = await getSession(request.headers.get("cookie"));
|
||||
const toastMessage = session.get("toastMessage") as ToastMessage;
|
||||
const posthogProjectKey = env.POSTHOG_PROJECT_KEY;
|
||||
|
||||
return typedjson(
|
||||
{
|
||||
user: await getUser(request),
|
||||
toastMessage,
|
||||
posthogProjectKey,
|
||||
appEnv: env.APP_ENV,
|
||||
appOrigin: env.APP_ORIGIN,
|
||||
},
|
||||
{ headers: { "Set-Cookie": await commitSession(session) } }
|
||||
);
|
||||
};
|
||||
|
||||
export const meta: MetaFunction = ({ data }) => {
|
||||
const typedData = data as UseDataFunctionReturn<typeof loader>;
|
||||
return [
|
||||
@ -36,7 +49,7 @@ export const meta: MetaFunction = ({ data }) => {
|
||||
{
|
||||
name: "robots",
|
||||
content:
|
||||
typeof window === "undefined" || window.location.hostname !== "cloud.trigger.dev"
|
||||
typeof window === "undefined" || window.location.hostname !== "echo.mysigma.ai"
|
||||
? "noindex, nofollow"
|
||||
: "index, follow",
|
||||
},
|
||||
|
||||
0
apps/webapp/app/services/session.server.ts
Normal file
0
apps/webapp/app/services/session.server.ts
Normal file
@ -96,7 +96,6 @@ export function titleCase(original: string): string {
|
||||
.join(" ");
|
||||
}
|
||||
|
||||
// Takes an api key (either trigger_live_xxxx or trigger_development_xxxx) and returns trigger_live_********
|
||||
export const obfuscateApiKey = (apiKey: string) => {
|
||||
const [prefix, slug, secretPart] = apiKey.split("_");
|
||||
return `${prefix}_${slug}_${"*".repeat(secretPart.length)}`;
|
||||
|
||||
9
apps/webapp/app/utils/boolEnv.ts
Normal file
9
apps/webapp/app/utils/boolEnv.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const BoolEnv = z.preprocess((val) => {
|
||||
if (typeof val !== "string") {
|
||||
return val;
|
||||
}
|
||||
|
||||
return ["true", "1"].includes(val.toLowerCase().trim());
|
||||
}, z.boolean());
|
||||
18
apps/webapp/app/utils/db.ts
Normal file
18
apps/webapp/app/utils/db.ts
Normal file
@ -0,0 +1,18 @@
|
||||
export function isValidDatabaseUrl(url: string) {
|
||||
try {
|
||||
const databaseUrl = new URL(url);
|
||||
const schemaFromSearchParam = databaseUrl.searchParams.get("schema");
|
||||
|
||||
if (schemaFromSearchParam === "") {
|
||||
console.error(
|
||||
"Invalid Database URL: The schema search param can't have an empty value. To use the `public` schema, either omit the schema param entirely or specify it in full: `?schema=public`"
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
8
apps/webapp/app/utils/regex.ts
Normal file
8
apps/webapp/app/utils/regex.ts
Normal file
@ -0,0 +1,8 @@
|
||||
export function isValidRegex(regex: string) {
|
||||
try {
|
||||
new RegExp(regex);
|
||||
return true;
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@ -15,6 +15,7 @@
|
||||
"@remix-run/react": "^2.16.7",
|
||||
"@remix-run/serve": "^2.16.7",
|
||||
"@tailwindcss/container-queries": "^0.1.1",
|
||||
"@echo/database": "workspace:*",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"remix-typedjson": "0.3.1",
|
||||
"clsx": "^2.1.1",
|
||||
@ -26,7 +27,10 @@
|
||||
"tailwind-scrollbar-hide": "^1.1.7",
|
||||
"tailwindcss-animate": "^1.0.5",
|
||||
"tailwindcss-textshadow": "^2.1.3",
|
||||
"non.geist": "^1.0.2"
|
||||
"non.geist": "^1.0.2",
|
||||
"zod": "3.23.8",
|
||||
"zod-error": "1.5.0",
|
||||
"zod-validation-error": "^1.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@remix-run/dev": "^2.16.7",
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
"exclude": [],
|
||||
"include": ["remix.env.d.ts", "global.d.ts", "**/*.ts", "**/*.tsx"],
|
||||
"compilerOptions": {
|
||||
"types": ["vitest/globals"],
|
||||
"types": [],
|
||||
"lib": ["DOM", "DOM.Iterable", "DOM.AsyncIterable", "ES2020"],
|
||||
"isolatedModules": true,
|
||||
"esModuleInterop": true,
|
||||
@ -22,6 +22,6 @@
|
||||
"~/*": ["./app/*"],
|
||||
"@/*": ["./*"]
|
||||
},
|
||||
"noEmit": true,
|
||||
"noEmit": true
|
||||
}
|
||||
}
|
||||
|
||||
@ -9,7 +9,12 @@
|
||||
"dev": "turbo run dev",
|
||||
"lint": "turbo run lint",
|
||||
"format": "prettier --write \"**/*.{ts,tsx,md}\"",
|
||||
"check-types": "turbo run check-types"
|
||||
"check-types": "turbo run check-types",
|
||||
"db:migrate": "turbo run db:migrate:deploy generate",
|
||||
"db:seed": "turbo run db:seed",
|
||||
"db:studio": "turbo run db:studio",
|
||||
"db:populate": "turbo run db:populate",
|
||||
"generate": "turbo run generate"
|
||||
},
|
||||
"devDependencies": {
|
||||
"prettier": "^3.5.3",
|
||||
|
||||
1
packages/database/.env
Normal file
1
packages/database/.env
Normal file
@ -0,0 +1 @@
|
||||
../../.env
|
||||
3
packages/database/.gitignore
vendored
Normal file
3
packages/database/.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
node_modules
|
||||
# Ensure the .env symlink is not removed by accident
|
||||
!.env
|
||||
26
packages/database/package.json
Normal file
26
packages/database/package.json
Normal file
@ -0,0 +1,26 @@
|
||||
{
|
||||
"name": "@echo/database",
|
||||
"private": true,
|
||||
"version": "0.0.1",
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"dependencies": {
|
||||
"@prisma/client": "5.4.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"prisma": "5.4.1",
|
||||
"rimraf": "6.0.1"
|
||||
},
|
||||
"scripts": {
|
||||
"clean": "rimraf dist",
|
||||
"generate": "prisma generate",
|
||||
"db:migrate:dev:create": "prisma migrate dev --create-only",
|
||||
"db:migrate:deploy": "prisma migrate deploy",
|
||||
"db:push": "prisma db push",
|
||||
"db:studio": "prisma studio",
|
||||
"db:reset": "prisma migrate reset",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"build": "pnpm run clean && tsc --noEmit false --outDir dist --declaration",
|
||||
"dev": "tsc --noEmit false --outDir dist --declaration --watch"
|
||||
}
|
||||
}
|
||||
92
packages/database/prisma/schema.prisma
Normal file
92
packages/database/prisma/schema.prisma
Normal file
@ -0,0 +1,92 @@
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
url = env("DATABASE_URL")
|
||||
directUrl = env("DIRECT_URL")
|
||||
}
|
||||
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
binaryTargets = ["native", "debian-openssl-1.1.x"]
|
||||
previewFeatures = ["tracing"]
|
||||
}
|
||||
|
||||
model User {
|
||||
id String @id @default(cuid())
|
||||
email String @unique
|
||||
|
||||
authenticationMethod AuthenticationMethod
|
||||
authenticationProfile Json?
|
||||
authenticationExtraParams Json?
|
||||
authIdentifier String? @unique
|
||||
|
||||
displayName String?
|
||||
name String?
|
||||
avatarUrl String?
|
||||
|
||||
admin Boolean @default(false)
|
||||
|
||||
/// Preferences for the dashboard
|
||||
dashboardPreferences Json?
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
/// @deprecated
|
||||
isOnCloudWaitlist Boolean @default(false)
|
||||
/// @deprecated
|
||||
featureCloud Boolean @default(false)
|
||||
/// @deprecated
|
||||
isOnHostedRepoWaitlist Boolean @default(false)
|
||||
|
||||
marketingEmails Boolean @default(true)
|
||||
confirmedBasicDetails Boolean @default(false)
|
||||
|
||||
referralSource String?
|
||||
|
||||
personalAccessTokens PersonalAccessToken[]
|
||||
}
|
||||
|
||||
enum AuthenticationMethod {
|
||||
GOOGLE
|
||||
}
|
||||
|
||||
/// Used to generate PersonalAccessTokens, they're one-time use
|
||||
model AuthorizationCode {
|
||||
id String @id @default(cuid())
|
||||
|
||||
code String @unique
|
||||
|
||||
personalAccessToken PersonalAccessToken? @relation(fields: [personalAccessTokenId], references: [id], onDelete: Cascade, onUpdate: Cascade)
|
||||
personalAccessTokenId String?
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
// Used by User's to perform API actions
|
||||
model PersonalAccessToken {
|
||||
id String @id @default(cuid())
|
||||
|
||||
/// If generated by the CLI this will be "cli", otherwise user-provided
|
||||
name String
|
||||
|
||||
/// This is the token encrypted using the ENCRYPTION_KEY
|
||||
encryptedToken Json
|
||||
|
||||
/// This is shown in the UI, with ********
|
||||
obfuscatedToken String
|
||||
|
||||
/// This is used to find the token in the database
|
||||
hashedToken String @unique
|
||||
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
userId String
|
||||
|
||||
revokedAt DateTime?
|
||||
lastAccessedAt DateTime?
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
authorizationCodes AuthorizationCode[]
|
||||
}
|
||||
2
packages/database/src/index.ts
Normal file
2
packages/database/src/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from "@prisma/client";
|
||||
export * from "./transaction";
|
||||
131
packages/database/src/transaction.ts
Normal file
131
packages/database/src/transaction.ts
Normal file
@ -0,0 +1,131 @@
|
||||
import { Prisma, PrismaClient } from "@prisma/client";
|
||||
|
||||
export type PrismaTransactionClient = Omit<
|
||||
PrismaClient,
|
||||
"$connect" | "$disconnect" | "$on" | "$transaction" | "$use" | "$extends"
|
||||
>;
|
||||
|
||||
export type PrismaClientOrTransaction = PrismaClient | PrismaTransactionClient;
|
||||
|
||||
export type PrismaReplicaClient = Omit<PrismaClient, "$transaction">;
|
||||
|
||||
function isTransactionClient(prisma: PrismaClientOrTransaction): prisma is PrismaTransactionClient {
|
||||
return !("$transaction" in prisma);
|
||||
}
|
||||
|
||||
export function isPrismaKnownError(error: unknown): error is Prisma.PrismaClientKnownRequestError {
|
||||
return (
|
||||
typeof error === "object" && error !== null && "code" in error && typeof error.code === "string"
|
||||
);
|
||||
}
|
||||
|
||||
/*
|
||||
• P2024: Connection timeout errors
|
||||
• P2028: Transaction timeout errors
|
||||
• P2034: Transaction deadlock/conflict errors
|
||||
*/
|
||||
const retryCodes = ["P2024", "P2028", "P2034"];
|
||||
|
||||
export function isPrismaRetriableError(error: unknown): boolean {
|
||||
if (!isPrismaKnownError(error)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return retryCodes.includes(error.code);
|
||||
}
|
||||
|
||||
/*
|
||||
• P2025: Record not found errors (in race conditions) [not included for now]
|
||||
*/
|
||||
export function isPrismaRaceConditionError(error: unknown): boolean {
|
||||
if (!isPrismaKnownError(error)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return error.code === "P2025";
|
||||
}
|
||||
|
||||
export type PrismaTransactionOptions = {
|
||||
/** The maximum amount of time (in ms) Prisma Client will wait to acquire a transaction from the database. The default value is 2000ms. */
|
||||
maxWait?: number;
|
||||
|
||||
/** The maximum amount of time (in ms) the interactive transaction can run before being canceled and rolled back. The default value is 5000ms. */
|
||||
timeout?: number;
|
||||
|
||||
/** Sets the transaction isolation level. By default this is set to the value currently configured in your database. */
|
||||
isolationLevel?: Prisma.TransactionIsolationLevel;
|
||||
|
||||
swallowPrismaErrors?: boolean;
|
||||
|
||||
/**
|
||||
* The maximum number of times the transaction will be retried in case of a serialization failure. The default value is 0.
|
||||
*
|
||||
* See https://www.prisma.io/docs/orm/prisma-client/queries/transactions#transaction-timing-issues
|
||||
*/
|
||||
maxRetries?: number;
|
||||
};
|
||||
|
||||
export async function $transaction<R>(
|
||||
prisma: PrismaClientOrTransaction,
|
||||
fn: (prisma: PrismaTransactionClient) => Promise<R>,
|
||||
prismaError: (error: Prisma.PrismaClientKnownRequestError) => void,
|
||||
options?: PrismaTransactionOptions,
|
||||
attempt = 0
|
||||
): Promise<R | undefined> {
|
||||
if (isTransactionClient(prisma)) {
|
||||
return fn(prisma);
|
||||
}
|
||||
|
||||
try {
|
||||
return await (prisma as PrismaClient).$transaction(fn, options);
|
||||
} catch (error) {
|
||||
if (isPrismaKnownError(error)) {
|
||||
if (
|
||||
retryCodes.includes(error.code) &&
|
||||
typeof options?.maxRetries === "number" &&
|
||||
attempt < options.maxRetries
|
||||
) {
|
||||
return $transaction(prisma, fn, prismaError, options, attempt + 1);
|
||||
}
|
||||
|
||||
prismaError(error);
|
||||
|
||||
if (options?.swallowPrismaErrors) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export function isUniqueConstraintError<T extends readonly string[]>(
|
||||
error: unknown,
|
||||
columns: T
|
||||
): boolean {
|
||||
if (!isPrismaKnownError(error)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (error.code !== "P2002") {
|
||||
return false;
|
||||
}
|
||||
|
||||
const target = error.meta?.target;
|
||||
|
||||
if (!Array.isArray(target)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (target.length !== columns.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (let i = 0; i < columns.length; i++) {
|
||||
if (target[i] !== columns[i]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
18
packages/database/tsconfig.json
Normal file
18
packages/database/tsconfig.json
Normal file
@ -0,0 +1,18 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2016",
|
||||
"lib": ["es2016", "dom"],
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"isolatedModules": true,
|
||||
"moduleResolution": "node",
|
||||
"preserveWatchOutput": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": false,
|
||||
"outDir": "./dist",
|
||||
"declaration": true,
|
||||
"declarationDir": "./dist"
|
||||
},
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
@ -1,3 +0,0 @@
|
||||
# `@turbo/eslint-config`
|
||||
|
||||
Collection of internal eslint configurations.
|
||||
@ -1,32 +0,0 @@
|
||||
import js from "@eslint/js";
|
||||
import eslintConfigPrettier from "eslint-config-prettier";
|
||||
import turboPlugin from "eslint-plugin-turbo";
|
||||
import tseslint from "typescript-eslint";
|
||||
import onlyWarn from "eslint-plugin-only-warn";
|
||||
|
||||
/**
|
||||
* A shared ESLint configuration for the repository.
|
||||
*
|
||||
* @type {import("eslint").Linter.Config[]}
|
||||
* */
|
||||
export const config = [
|
||||
js.configs.recommended,
|
||||
eslintConfigPrettier,
|
||||
...tseslint.configs.recommended,
|
||||
{
|
||||
plugins: {
|
||||
turbo: turboPlugin,
|
||||
},
|
||||
rules: {
|
||||
"turbo/no-undeclared-env-vars": "warn",
|
||||
},
|
||||
},
|
||||
{
|
||||
plugins: {
|
||||
onlyWarn,
|
||||
},
|
||||
},
|
||||
{
|
||||
ignores: ["dist/**"],
|
||||
},
|
||||
];
|
||||
@ -1,49 +0,0 @@
|
||||
import js from "@eslint/js";
|
||||
import eslintConfigPrettier from "eslint-config-prettier";
|
||||
import tseslint from "typescript-eslint";
|
||||
import pluginReactHooks from "eslint-plugin-react-hooks";
|
||||
import pluginReact from "eslint-plugin-react";
|
||||
import globals from "globals";
|
||||
import pluginNext from "@next/eslint-plugin-next";
|
||||
import { config as baseConfig } from "./base.js";
|
||||
|
||||
/**
|
||||
* A custom ESLint configuration for libraries that use Next.js.
|
||||
*
|
||||
* @type {import("eslint").Linter.Config[]}
|
||||
* */
|
||||
export const nextJsConfig = [
|
||||
...baseConfig,
|
||||
js.configs.recommended,
|
||||
eslintConfigPrettier,
|
||||
...tseslint.configs.recommended,
|
||||
{
|
||||
...pluginReact.configs.flat.recommended,
|
||||
languageOptions: {
|
||||
...pluginReact.configs.flat.recommended.languageOptions,
|
||||
globals: {
|
||||
...globals.serviceworker,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
plugins: {
|
||||
"@next/next": pluginNext,
|
||||
},
|
||||
rules: {
|
||||
...pluginNext.configs.recommended.rules,
|
||||
...pluginNext.configs["core-web-vitals"].rules,
|
||||
},
|
||||
},
|
||||
{
|
||||
plugins: {
|
||||
"react-hooks": pluginReactHooks,
|
||||
},
|
||||
settings: { react: { version: "detect" } },
|
||||
rules: {
|
||||
...pluginReactHooks.configs.recommended.rules,
|
||||
// React scope no longer necessary with new JSX transform.
|
||||
"react/react-in-jsx-scope": "off",
|
||||
},
|
||||
},
|
||||
];
|
||||
@ -1,24 +0,0 @@
|
||||
{
|
||||
"name": "@repo/eslint-config",
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"exports": {
|
||||
"./base": "./base.js",
|
||||
"./next-js": "./next.js",
|
||||
"./react-internal": "./react-internal.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.27.0",
|
||||
"@next/eslint-plugin-next": "^15.3.0",
|
||||
"eslint": "^9.27.0",
|
||||
"eslint-config-prettier": "^10.1.1",
|
||||
"eslint-plugin-only-warn": "^1.1.0",
|
||||
"eslint-plugin-react": "^7.37.4",
|
||||
"eslint-plugin-react-hooks": "^5.2.0",
|
||||
"eslint-plugin-turbo": "^2.5.0",
|
||||
"globals": "^16.1.0",
|
||||
"typescript": "^5.8.2",
|
||||
"typescript-eslint": "^8.32.0"
|
||||
}
|
||||
}
|
||||
39
packages/eslint-config/react-internal.js
vendored
39
packages/eslint-config/react-internal.js
vendored
@ -1,39 +0,0 @@
|
||||
import js from "@eslint/js";
|
||||
import eslintConfigPrettier from "eslint-config-prettier";
|
||||
import tseslint from "typescript-eslint";
|
||||
import pluginReactHooks from "eslint-plugin-react-hooks";
|
||||
import pluginReact from "eslint-plugin-react";
|
||||
import globals from "globals";
|
||||
import { config as baseConfig } from "./base.js";
|
||||
|
||||
/**
|
||||
* A custom ESLint configuration for libraries that use React.
|
||||
*
|
||||
* @type {import("eslint").Linter.Config[]} */
|
||||
export const config = [
|
||||
...baseConfig,
|
||||
js.configs.recommended,
|
||||
eslintConfigPrettier,
|
||||
...tseslint.configs.recommended,
|
||||
pluginReact.configs.flat.recommended,
|
||||
{
|
||||
languageOptions: {
|
||||
...pluginReact.configs.flat.recommended.languageOptions,
|
||||
globals: {
|
||||
...globals.serviceworker,
|
||||
...globals.browser,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
plugins: {
|
||||
"react-hooks": pluginReactHooks,
|
||||
},
|
||||
settings: { react: { version: "detect" } },
|
||||
rules: {
|
||||
...pluginReactHooks.configs.recommended.rules,
|
||||
// React scope no longer necessary with new JSX transform.
|
||||
"react/react-in-jsx-scope": "off",
|
||||
},
|
||||
},
|
||||
];
|
||||
@ -1,19 +0,0 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"compilerOptions": {
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"esModuleInterop": true,
|
||||
"incremental": false,
|
||||
"isolatedModules": true,
|
||||
"lib": ["es2022", "DOM", "DOM.Iterable"],
|
||||
"module": "NodeNext",
|
||||
"moduleDetection": "force",
|
||||
"moduleResolution": "NodeNext",
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"resolveJsonModule": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"target": "ES2022"
|
||||
}
|
||||
}
|
||||
@ -1,12 +0,0 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"extends": "./base.json",
|
||||
"compilerOptions": {
|
||||
"plugins": [{ "name": "next" }],
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"allowJs": true,
|
||||
"jsx": "preserve",
|
||||
"noEmit": true
|
||||
}
|
||||
}
|
||||
@ -1,9 +0,0 @@
|
||||
{
|
||||
"name": "@repo/typescript-config",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"license": "MIT",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
}
|
||||
}
|
||||
@ -1,7 +0,0 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"extends": "./base.json",
|
||||
"compilerOptions": {
|
||||
"jsx": "react-jsx"
|
||||
}
|
||||
}
|
||||
@ -1,4 +0,0 @@
|
||||
import { config } from "@repo/eslint-config/react-internal";
|
||||
|
||||
/** @type {import("eslint").Linter.Config} */
|
||||
export default config;
|
||||
@ -1,27 +0,0 @@
|
||||
{
|
||||
"name": "@repo/ui",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"exports": {
|
||||
"./*": "./src/*.tsx"
|
||||
},
|
||||
"scripts": {
|
||||
"lint": "eslint . --max-warnings 0",
|
||||
"generate:component": "turbo gen react-component",
|
||||
"check-types": "tsc --noEmit"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@repo/eslint-config": "workspace:*",
|
||||
"@repo/typescript-config": "workspace:*",
|
||||
"@turbo/gen": "^2.5.0",
|
||||
"@types/node": "^22.15.3",
|
||||
"@types/react": "19.1.0",
|
||||
"@types/react-dom": "19.1.1",
|
||||
"eslint": "^9.27.0",
|
||||
"typescript": "5.8.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0"
|
||||
}
|
||||
}
|
||||
@ -1,20 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { ReactNode } from "react";
|
||||
|
||||
interface ButtonProps {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
appName: string;
|
||||
}
|
||||
|
||||
export const Button = ({ children, className, appName }: ButtonProps) => {
|
||||
return (
|
||||
<button
|
||||
className={className}
|
||||
onClick={() => alert(`Hello from your ${appName} app!`)}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
@ -1,27 +0,0 @@
|
||||
import { type JSX } from "react";
|
||||
|
||||
export function Card({
|
||||
className,
|
||||
title,
|
||||
children,
|
||||
href,
|
||||
}: {
|
||||
className?: string;
|
||||
title: string;
|
||||
children: React.ReactNode;
|
||||
href: string;
|
||||
}): JSX.Element {
|
||||
return (
|
||||
<a
|
||||
className={className}
|
||||
href={`${href}?utm_source=create-turbo&utm_medium=basic&utm_campaign=create-turbo"`}
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
<h2>
|
||||
{title} <span>-></span>
|
||||
</h2>
|
||||
<p>{children}</p>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
@ -1,11 +0,0 @@
|
||||
import { type JSX } from "react";
|
||||
|
||||
export function Code({
|
||||
children,
|
||||
className,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}): JSX.Element {
|
||||
return <code className={className}>{children}</code>;
|
||||
}
|
||||
@ -1,8 +0,0 @@
|
||||
{
|
||||
"extends": "@repo/typescript-config/react-library.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "dist"
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
@ -1,30 +0,0 @@
|
||||
import type { PlopTypes } from "@turbo/gen";
|
||||
|
||||
// Learn more about Turborepo Generators at https://turborepo.com/docs/guides/generating-code
|
||||
|
||||
export default function generator(plop: PlopTypes.NodePlopAPI): void {
|
||||
// A simple generator to add a new React component to the internal UI library
|
||||
plop.setGenerator("react-component", {
|
||||
description: "Adds a new react component",
|
||||
prompts: [
|
||||
{
|
||||
type: "input",
|
||||
name: "name",
|
||||
message: "What is the name of the component?",
|
||||
},
|
||||
],
|
||||
actions: [
|
||||
{
|
||||
type: "add",
|
||||
path: "src/{{kebabCase name}}.tsx",
|
||||
templateFile: "templates/component.hbs",
|
||||
},
|
||||
{
|
||||
type: "append",
|
||||
path: "package.json",
|
||||
pattern: /"exports": {(?<insertion>)/g,
|
||||
template: ' "./{{kebabCase name}}": "./src/{{kebabCase name}}.tsx",',
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
@ -1,8 +0,0 @@
|
||||
export const {{ pascalCase name }} = ({ children }: { children: React.ReactNode }) => {
|
||||
return (
|
||||
<div>
|
||||
<h1>{{ pascalCase name }} Component</h1>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
1482
pnpm-lock.yaml
generated
1482
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
14
turbo.json
14
turbo.json
@ -16,6 +16,20 @@
|
||||
"dev": {
|
||||
"cache": false,
|
||||
"persistent": true
|
||||
},
|
||||
"db:generate": {
|
||||
"cache": false
|
||||
},
|
||||
"db:migrate:deploy": {
|
||||
"cache": false
|
||||
},
|
||||
"db:studio": {
|
||||
"cache": false
|
||||
},
|
||||
"generate": {
|
||||
"dependsOn": [
|
||||
"^generate"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user