forked from imbytecat/fullstack-starter
refactor: 优化项目结构 — 修复拼写、提取共享 interceptor、扁平化 db 目录、清理空包
This commit is contained in:
@@ -12,7 +12,7 @@ Guidelines for AI agents working in this Bun monorepo.
|
|||||||
- **Apps**:
|
- **Apps**:
|
||||||
- `apps/server` - TanStack Start fullstack web app (see `apps/server/AGENTS.md`)
|
- `apps/server` - TanStack Start fullstack web app (see `apps/server/AGENTS.md`)
|
||||||
- `apps/desktop` - Electron desktop shell, sidecar server pattern (see `apps/desktop/AGENTS.md`)
|
- `apps/desktop` - Electron desktop shell, sidecar server pattern (see `apps/desktop/AGENTS.md`)
|
||||||
- **Packages**: `packages/utils`, `packages/tsconfig` (shared configs)
|
- **Packages**: `packages/tsconfig` (shared TS configs)
|
||||||
|
|
||||||
## Build / Lint / Test Commands
|
## Build / Lint / Test Commands
|
||||||
|
|
||||||
@@ -207,8 +207,7 @@ export const myTable = pgTable('my_table', {
|
|||||||
│ ├── electron-builder.yml # Packaging config
|
│ ├── electron-builder.yml # Packaging config
|
||||||
│ └── AGENTS.md
|
│ └── AGENTS.md
|
||||||
├── packages/
|
├── packages/
|
||||||
│ ├── tsconfig/ # Shared TS configs
|
│ └── tsconfig/ # Shared TS configs
|
||||||
│ └── utils/ # Shared utilities
|
|
||||||
├── biome.json # Linting/formatting config
|
├── biome.json # Linting/formatting config
|
||||||
├── turbo.json # Turbo task orchestration
|
├── turbo.json # Turbo task orchestration
|
||||||
└── package.json # Workspace root + dependency catalog
|
└── package.json # Workspace root + dependency catalog
|
||||||
|
|||||||
@@ -54,24 +54,28 @@ bun test -t "pattern" # Run tests matching pattern
|
|||||||
```
|
```
|
||||||
src/
|
src/
|
||||||
├── client/ # Client-side code
|
├── client/ # Client-side code
|
||||||
│ ├── orpc.ts # ORPC isomorphic client
|
│ ├── orpc.ts # ORPC isomorphic client (internal)
|
||||||
│ └── query-client.ts # TanStack Query client
|
│ └── query-client.ts # TanStack Query utils (used by components)
|
||||||
├── components/ # React components
|
├── components/ # React components
|
||||||
├── routes/ # TanStack Router file routes
|
├── routes/ # TanStack Router file routes
|
||||||
│ ├── __root.tsx # Root layout
|
│ ├── __root.tsx # Root layout
|
||||||
│ ├── index.tsx # Home page
|
│ ├── index.tsx # Home page
|
||||||
│ └── api/
|
│ └── api/
|
||||||
│ └── rpc.$.ts # ORPC HTTP endpoint
|
│ ├── $.ts # OpenAPI handler + Scalar docs
|
||||||
|
│ ├── health.ts # Health check endpoint
|
||||||
|
│ └── rpc.$.ts # ORPC RPC handler
|
||||||
├── server/ # Server-side code
|
├── server/ # Server-side code
|
||||||
│ ├── api/ # ORPC layer
|
│ ├── api/ # ORPC layer
|
||||||
│ │ ├── contracts/ # Input/output schemas (Zod)
|
│ │ ├── contracts/ # Input/output schemas (Zod)
|
||||||
│ │ ├── middlewares/ # Middleware (db provider, auth)
|
│ │ ├── middlewares/ # Middleware (db provider, auth)
|
||||||
│ │ ├── routers/ # Handler implementations
|
│ │ ├── routers/ # Handler implementations
|
||||||
|
│ │ ├── interceptors.ts # Shared error interceptors
|
||||||
│ │ ├── context.ts # Request context
|
│ │ ├── context.ts # Request context
|
||||||
│ │ ├── server.ts # ORPC server instance
|
│ │ ├── server.ts # ORPC server instance
|
||||||
│ │ └── types.ts # Type exports
|
│ │ └── types.ts # Type exports
|
||||||
│ └── db/
|
│ └── db/
|
||||||
│ ├── schema/ # Drizzle table definitions
|
│ ├── schema/ # Drizzle table definitions
|
||||||
|
│ ├── fields.ts # Shared field builders (id, createdAt, updatedAt)
|
||||||
│ ├── relations.ts # Drizzle relations (defineRelations, RQBv2)
|
│ ├── relations.ts # Drizzle relations (defineRelations, RQBv2)
|
||||||
│ └── index.ts # Database instance (postgres-js driver)
|
│ └── index.ts # Database instance (postgres-js driver)
|
||||||
├── env.ts # Environment variable validation
|
├── env.ts # Environment variable validation
|
||||||
@@ -122,7 +126,7 @@ export const router = os.router({ feature })
|
|||||||
### 4. Use in Components
|
### 4. Use in Components
|
||||||
```typescript
|
```typescript
|
||||||
import { useSuspenseQuery, useMutation } from '@tanstack/react-query'
|
import { useSuspenseQuery, useMutation } from '@tanstack/react-query'
|
||||||
import { orpc } from '@/client/orpc'
|
import { orpc } from '@/client/query-client'
|
||||||
|
|
||||||
const { data } = useSuspenseQuery(orpc.feature.list.queryOptions())
|
const { data } = useSuspenseQuery(orpc.feature.list.queryOptions())
|
||||||
const mutation = useMutation(orpc.feature.create.mutationOptions())
|
const mutation = useMutation(orpc.feature.create.mutationOptions())
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { createTanstackQueryUtils } from '@orpc/tanstack-query'
|
import { createTanstackQueryUtils } from '@orpc/tanstack-query'
|
||||||
import { orpc as orpcClient } from './orpc'
|
import { orpc as client } from './orpc'
|
||||||
|
|
||||||
export const orpc = createTanstackQueryUtils(orpcClient, {
|
export const orpc = createTanstackQueryUtils(client, {
|
||||||
experimental_defaults: {
|
experimental_defaults: {
|
||||||
todo: {
|
todo: {
|
||||||
create: {
|
create: {
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
import { TanStackRouterDevtoolsPanel } from '@tanstack/react-router-devtools'
|
import { TanStackRouterDevtoolsPanel } from '@tanstack/react-router-devtools'
|
||||||
import type { ReactNode } from 'react'
|
import type { ReactNode } from 'react'
|
||||||
import { ErrorComponent } from '@/components/Error'
|
import { ErrorComponent } from '@/components/Error'
|
||||||
import { NotFoundComponent } from '@/components/NotFount'
|
import { NotFoundComponent } from '@/components/NotFound'
|
||||||
import appCss from '@/styles.css?url'
|
import appCss from '@/styles.css?url'
|
||||||
|
|
||||||
export interface RouterContext {
|
export interface RouterContext {
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { OpenAPIHandler } from '@orpc/openapi/fetch'
|
import { OpenAPIHandler } from '@orpc/openapi/fetch'
|
||||||
import { OpenAPIReferencePlugin } from '@orpc/openapi/plugins'
|
import { OpenAPIReferencePlugin } from '@orpc/openapi/plugins'
|
||||||
import { ORPCError, onError, ValidationError } from '@orpc/server'
|
import { onError } from '@orpc/server'
|
||||||
import { ZodToJsonSchemaConverter } from '@orpc/zod/zod4'
|
import { ZodToJsonSchemaConverter } from '@orpc/zod/zod4'
|
||||||
import { createFileRoute } from '@tanstack/react-router'
|
import { createFileRoute } from '@tanstack/react-router'
|
||||||
import { z } from 'zod'
|
|
||||||
import { name, version } from '@/../package.json'
|
import { name, version } from '@/../package.json'
|
||||||
|
import { handleValidationError, logError } from '@/server/api/interceptors'
|
||||||
import { router } from '@/server/api/routers'
|
import { router } from '@/server/api/routers'
|
||||||
|
|
||||||
const handler = new OpenAPIHandler(router, {
|
const handler = new OpenAPIHandler(router, {
|
||||||
@@ -17,55 +17,13 @@ const handler = new OpenAPIHandler(router, {
|
|||||||
title: name,
|
title: name,
|
||||||
version,
|
version,
|
||||||
},
|
},
|
||||||
// components: {
|
|
||||||
// securitySchemes: {
|
|
||||||
// bearerAuth: {
|
|
||||||
// type: 'http',
|
|
||||||
// scheme: 'bearer',
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
},
|
},
|
||||||
docsPath: '/docs',
|
docsPath: '/docs',
|
||||||
specPath: '/spec.json',
|
specPath: '/spec.json',
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
interceptors: [
|
interceptors: [onError(logError)],
|
||||||
onError((error) => {
|
clientInterceptors: [onError(handleValidationError)],
|
||||||
console.error(error)
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
clientInterceptors: [
|
|
||||||
onError((error) => {
|
|
||||||
if (
|
|
||||||
error instanceof ORPCError &&
|
|
||||||
error.code === 'BAD_REQUEST' &&
|
|
||||||
error.cause instanceof ValidationError
|
|
||||||
) {
|
|
||||||
// If you only use Zod you can safely cast to ZodIssue[]
|
|
||||||
const zodError = new z.ZodError(
|
|
||||||
error.cause.issues as z.core.$ZodIssue[],
|
|
||||||
)
|
|
||||||
|
|
||||||
throw new ORPCError('INPUT_VALIDATION_FAILED', {
|
|
||||||
status: 422,
|
|
||||||
message: z.prettifyError(zodError),
|
|
||||||
data: z.flattenError(zodError),
|
|
||||||
cause: error.cause,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
error instanceof ORPCError &&
|
|
||||||
error.code === 'INTERNAL_SERVER_ERROR' &&
|
|
||||||
error.cause instanceof ValidationError
|
|
||||||
) {
|
|
||||||
throw new ORPCError('OUTPUT_VALIDATION_FAILED', {
|
|
||||||
cause: error.cause,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
})
|
})
|
||||||
|
|
||||||
export const Route = createFileRoute('/api/$')({
|
export const Route = createFileRoute('/api/$')({
|
||||||
|
|||||||
@@ -1,46 +1,12 @@
|
|||||||
import { ORPCError, onError, ValidationError } from '@orpc/server'
|
import { onError } from '@orpc/server'
|
||||||
import { RPCHandler } from '@orpc/server/fetch'
|
import { RPCHandler } from '@orpc/server/fetch'
|
||||||
import { createFileRoute } from '@tanstack/react-router'
|
import { createFileRoute } from '@tanstack/react-router'
|
||||||
import { z } from 'zod'
|
import { handleValidationError, logError } from '@/server/api/interceptors'
|
||||||
import { router } from '@/server/api/routers'
|
import { router } from '@/server/api/routers'
|
||||||
|
|
||||||
const handler = new RPCHandler(router, {
|
const handler = new RPCHandler(router, {
|
||||||
interceptors: [
|
interceptors: [onError(logError)],
|
||||||
onError((error) => {
|
clientInterceptors: [onError(handleValidationError)],
|
||||||
console.error(error)
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
clientInterceptors: [
|
|
||||||
onError((error) => {
|
|
||||||
if (
|
|
||||||
error instanceof ORPCError &&
|
|
||||||
error.code === 'BAD_REQUEST' &&
|
|
||||||
error.cause instanceof ValidationError
|
|
||||||
) {
|
|
||||||
// If you only use Zod you can safely cast to ZodIssue[]
|
|
||||||
const zodError = new z.ZodError(
|
|
||||||
error.cause.issues as z.core.$ZodIssue[],
|
|
||||||
)
|
|
||||||
|
|
||||||
throw new ORPCError('INPUT_VALIDATION_FAILED', {
|
|
||||||
status: 422,
|
|
||||||
message: z.prettifyError(zodError),
|
|
||||||
data: z.flattenError(zodError),
|
|
||||||
cause: error.cause,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
error instanceof ORPCError &&
|
|
||||||
error.code === 'INTERNAL_SERVER_ERROR' &&
|
|
||||||
error.cause instanceof ValidationError
|
|
||||||
) {
|
|
||||||
throw new ORPCError('OUTPUT_VALIDATION_FAILED', {
|
|
||||||
cause: error.cause,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
})
|
})
|
||||||
|
|
||||||
export const Route = createFileRoute('/api/rpc/$')({
|
export const Route = createFileRoute('/api/rpc/$')({
|
||||||
|
|||||||
33
apps/server/src/server/api/interceptors.ts
Normal file
33
apps/server/src/server/api/interceptors.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import { ORPCError, ValidationError } from '@orpc/server'
|
||||||
|
import { z } from 'zod'
|
||||||
|
|
||||||
|
export const logError = (error: unknown) => {
|
||||||
|
console.error(error)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const handleValidationError = (error: unknown) => {
|
||||||
|
if (
|
||||||
|
error instanceof ORPCError &&
|
||||||
|
error.code === 'BAD_REQUEST' &&
|
||||||
|
error.cause instanceof ValidationError
|
||||||
|
) {
|
||||||
|
const zodError = new z.ZodError(error.cause.issues as z.core.$ZodIssue[])
|
||||||
|
|
||||||
|
throw new ORPCError('INPUT_VALIDATION_FAILED', {
|
||||||
|
status: 422,
|
||||||
|
message: z.prettifyError(zodError),
|
||||||
|
data: z.flattenError(zodError),
|
||||||
|
cause: error.cause,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
error instanceof ORPCError &&
|
||||||
|
error.code === 'INTERNAL_SERVER_ERROR' &&
|
||||||
|
error.cause instanceof ValidationError
|
||||||
|
) {
|
||||||
|
throw new ORPCError('OUTPUT_VALIDATION_FAILED', {
|
||||||
|
cause: error.cause,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { boolean, pgTable, text } from 'drizzle-orm/pg-core'
|
import { boolean, pgTable, text } from 'drizzle-orm/pg-core'
|
||||||
import { generatedFields } from './utils/field'
|
import { generatedFields } from '../fields'
|
||||||
|
|
||||||
export const todoTable = pgTable('todo', {
|
export const todoTable = pgTable('todo', {
|
||||||
...generatedFields,
|
...generatedFields,
|
||||||
|
|||||||
9
bun.lock
9
bun.lock
@@ -74,13 +74,6 @@
|
|||||||
"name": "@furtherverse/tsconfig",
|
"name": "@furtherverse/tsconfig",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
},
|
},
|
||||||
"packages/utils": {
|
|
||||||
"name": "@furtherverse/utils",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"devDependencies": {
|
|
||||||
"@furtherverse/tsconfig": "workspace:*",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
"overrides": {
|
"overrides": {
|
||||||
"@types/node": "catalog:",
|
"@types/node": "catalog:",
|
||||||
@@ -309,8 +302,6 @@
|
|||||||
|
|
||||||
"@furtherverse/tsconfig": ["@furtherverse/tsconfig@workspace:packages/tsconfig"],
|
"@furtherverse/tsconfig": ["@furtherverse/tsconfig@workspace:packages/tsconfig"],
|
||||||
|
|
||||||
"@furtherverse/utils": ["@furtherverse/utils@workspace:packages/utils"],
|
|
||||||
|
|
||||||
"@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="],
|
"@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="],
|
||||||
|
|
||||||
"@isaacs/fs-minipass": ["@isaacs/fs-minipass@4.0.1", "", { "dependencies": { "minipass": "^7.0.4" } }, "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w=="],
|
"@isaacs/fs-minipass": ["@isaacs/fs-minipass@4.0.1", "", { "dependencies": { "minipass": "^7.0.4" } }, "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w=="],
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "@furtherverse/utils",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"private": true,
|
|
||||||
"type": "module",
|
|
||||||
"imports": {
|
|
||||||
"#*": "./src/*"
|
|
||||||
},
|
|
||||||
"exports": {
|
|
||||||
".": "./src/index.ts",
|
|
||||||
"./*": "./src/*.ts"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@furtherverse/tsconfig": "workspace:*"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export {}
|
|
||||||
Reference in New Issue
Block a user