@goplusvn/core 0.1.4 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,49 @@
1
+ # Changelog
2
+
3
+ ## Unreleased — platform foundation extraction
4
+
5
+ Promoted the cross-cutting platform layer out of the Vinh Hoa reference app into
6
+ core so every app inherits the same design system + infrastructure and only
7
+ builds its own business logic. All additive — existing subpaths are unchanged.
8
+
9
+ ### Added
10
+
11
+ - **`@goerp/core/styles/base.css`** — the canonical design-system tokens (Radix
12
+ scale → shadcn semantic → Plane semantic; status, charts, sidebar, scrollbar,
13
+ animations). Apps `@import` this from their Tailwind v4 entry stylesheet and
14
+ override only brand tokens (`--primary`, `--sidebar-*`). Brand default: indigo.
15
+ Also ships `@goerp/core/styles/radix-themes.css` (the HSL Radix scale).
16
+
17
+ - **`@goerp/core/errors`** — `AppError` + `toAppError` (maps Prisma
18
+ P2002/P2025/P2003/P2014 → friendly message + stable `errorId`),
19
+ `withErrorHandler` / `createErrorResponse` / `errorResponse`, and the
20
+ db-injected factories `createErrorLogger(db)` (fingerprint-deduped persistence
21
+ to an `error_logs` table) + `buildServerError({ logServerError })` /
22
+ `createServerError({ db })`. Granular client-safe paths:
23
+ `@goerp/core/errors/{app-error,error-handler,server-error}`.
24
+
25
+ - **`@goerp/core/export`** — `useExport()` (the unified "Xuất Excel" hook),
26
+ `triggerBlobDownload` / `filenameFromContentDisposition`, and `toCsv` /
27
+ `escapeCsvField` (RFC 4180). The crud `exportToCSV` now delegates to `toCsv`,
28
+ fixing unescaped commas/quotes/newlines and falsy `0`/`false`. Granular paths:
29
+ `@goerp/core/export/{to-csv,download-file,use-export}`.
30
+
31
+ - **`@goerp/core/print`** — `DirectPrintProvider` (document-agnostic print device
32
+ layer: hidden-iframe URL printing with a mobile same-tab fallback, one-page
33
+ sizing, loading overlay) driven by a `renderContent` render-prop, the
34
+ `useDirectPrint` hook, and `PrintStyles` (A4/A5 `@media print`). Granular paths:
35
+ `@goerp/core/print/{direct-print-provider,print-styles}`.
36
+
37
+ - **`@goerp/core/repository`** — `casUpdate` / `casUpdateById`: the compare-and-
38
+ swap invariant for money/stock mutations (`updateMany` on expected state,
39
+ throw on zero rows so an enclosing transaction rolls back).
40
+
41
+ - **`@goerp/core/utils/{serialize,fetcher,date-utils,cccd-parser}`** —
42
+ `serializeDecimalFields` (Prisma Decimal → string for Client Components),
43
+ `swrFetcher` / `swrListFetcher` / `unwrapList`, Vietnam-timezone date helpers,
44
+ and the CCCD (VN ID-card) QR parser.
45
+
46
+ ### Fixed
47
+
48
+ - `exportToCSV` (crud) no longer breaks column alignment on values containing
49
+ commas / quotes / newlines, and no longer drops falsy `0` / `false`.
package/PLATFORM.md ADDED
@@ -0,0 +1,111 @@
1
+ # Building an app on `@goerp/core`
2
+
3
+ Core is the platform foundation: design system, layout, RBAC, CRUD engine, and
4
+ the cross-cutting infrastructure below. An app should only implement its own
5
+ business domain (entities, routes, document templates, domain services) and wire
6
+ the platform pieces — not re-implement them. The Vinh Hoa app is the reference
7
+ implementation of every pattern here.
8
+
9
+ > When you find yourself copying a util, an error helper, a CSV builder, a print
10
+ > mechanism, or a CAS update into an app, stop — it belongs in core. Promote it
11
+ > and re-export a thin shim from the app so call sites don't change.
12
+
13
+ ## Design system (styles)
14
+
15
+ One import gives the whole token system; override only your brand.
16
+
17
+ ```css
18
+ /* app: src/app/globals.css */
19
+ @import "tailwindcss";
20
+ @import "tw-animate-css";
21
+ @import "@goerp/core/styles/base.css"; /* Radix → shadcn → Plane tokens */
22
+ @source "../node_modules/@goerp/core/src/**/*.tsx"; /* scan core classes */
23
+
24
+ :root {
25
+ --primary: var(--violet-9); /* re-skin: brand default is indigo */
26
+ --sidebar-background: 262 60% 20%;
27
+ }
28
+ ```
29
+
30
+ Use semantic tokens, never the raw Tailwind palette: `text-muted-foreground` not
31
+ `text-slate-500`, `bg-card` not `bg-white`, `text-destructive` not `text-red-600`.
32
+ Editing `base.css` reskins every app at once.
33
+
34
+ ## Errors
35
+
36
+ Bind the db-injected factories once, then use `serverError` in every catch block.
37
+
38
+ ```ts
39
+ // app: src/lib/errors/log-server-error.ts
40
+ import { createErrorLogger, type ErrorLoggerDb } from "@goerp/core/errors/server-error"
41
+ import { db } from "@/lib/prisma"
42
+ export const { logServerError } = createErrorLogger(db as unknown as ErrorLoggerDb)
43
+
44
+ // app: src/lib/errors/server-error.ts
45
+ import { buildServerError } from "@goerp/core/errors/server-error"
46
+ import { logServerError } from "./log-server-error"
47
+ export const serverError = buildServerError({ logServerError })
48
+ ```
49
+
50
+ ```ts
51
+ // in a route catch block
52
+ } catch (error) {
53
+ return serverError(error, request, { message: "Lỗi xử lý thao tác" })
54
+ }
55
+ ```
56
+
57
+ The app provides an `error_logs` Prisma model matching `ErrorLogDelegate`
58
+ (fingerprint, occurrenceCount, lastSeenAt, severity, …) and keeps its own
59
+ domain error codes; core owns the machinery.
60
+
61
+ ## Export
62
+
63
+ ```ts
64
+ const { exportFile, isExporting } = useExport() // client buttons
65
+ exportFile(`/api/customers/export?${params}`) // never window.open
66
+ ```
67
+
68
+ ```ts
69
+ import { toCsv } from "@goerp/core/export/to-csv" // server CSV routes
70
+ return new Response(toCsv(rows, columns), { headers: { ... } })
71
+ ```
72
+
73
+ ## Print
74
+
75
+ Core owns *how* to print; the app supplies *what* via `renderContent`.
76
+
77
+ ```tsx
78
+ // app: wrap core's provider once near the root
79
+ <DirectPrintProvider renderContent={(p) => <PrintContent type={p.type} data={p.data} />}>
80
+ {children}
81
+ </DirectPrintProvider>
82
+
83
+ // anywhere
84
+ const { triggerDirectPrint } = useDirectPrint()
85
+ triggerDirectPrint({ printUrl: `/${lang}/print/direct?type=invoice&id=${id}` })
86
+ ```
87
+
88
+ ## Repository / money + stock mutations
89
+
90
+ Mutate money/stock with compare-and-swap, never read-modify-write.
91
+
92
+ ```ts
93
+ import { casUpdateById } from "@goerp/core/repository"
94
+
95
+ // only writes if the order is still unpaid; throws otherwise → tx rolls back
96
+ await casUpdateById(
97
+ tx.salesOrder,
98
+ orderId,
99
+ { paymentStatus: { not: "paid" } },
100
+ { paymentStatus: "paid" },
101
+ new Error("Đơn đã được thanh toán bởi thao tác khác")
102
+ )
103
+ ```
104
+
105
+ ## Utils
106
+
107
+ `@goerp/core/utils` (formatCurrency, formatDate, cn, …) plus the granular:
108
+ `@goerp/core/utils/serialize` (`serializeDecimalFields` before returning Prisma
109
+ rows to Client Components), `.../fetcher` (`swrFetcher`/`swrListFetcher`),
110
+ `.../date-utils` (Vietnam-timezone formatting + Prisma day/month bounds),
111
+ `.../cccd-parser` (VN ID-card QR).
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@goplusvn/core",
3
3
  "description": "GoPlusVN Platform Kit - ERP kernel: layout, RBAC, CRUD, multi-tenant, system pages",
4
- "version": "0.1.4",
4
+ "version": "0.1.6",
5
5
  "private": false,
6
6
  "publishConfig": {
7
7
  "registry": "https://registry.npmjs.org",
@@ -14,7 +14,8 @@
14
14
  "files": [
15
15
  "src",
16
16
  "README.md",
17
- "CHANGELOG.md"
17
+ "CHANGELOG.md",
18
+ "PLATFORM.md"
18
19
  ],
19
20
  "repository": {
20
21
  "type": "git",
@@ -34,6 +35,20 @@
34
35
  "default": "./src/*/index.ts"
35
36
  },
36
37
  "./assets/*": "./src/assets/*",
38
+ "./styles/*": "./src/styles/*",
39
+ "./errors/app-error": "./src/errors/app-error.ts",
40
+ "./errors/error-handler": "./src/errors/error-handler.ts",
41
+ "./errors/server-error": "./src/errors/server-error.ts",
42
+ "./export/to-csv": "./src/export/to-csv.ts",
43
+ "./export/download-file": "./src/export/download-file.ts",
44
+ "./export/use-export": "./src/export/use-export.ts",
45
+ "./print/direct-print-provider": "./src/print/direct-print-provider.tsx",
46
+ "./print/print-styles": "./src/print/print-styles.tsx",
47
+ "./utils/serialize": "./src/utils/serialize.ts",
48
+ "./utils/fetcher": "./src/utils/fetcher.ts",
49
+ "./utils/date-utils": "./src/utils/date-utils.ts",
50
+ "./utils/cccd-parser": "./src/utils/cccd-parser.ts",
51
+ "./configs/status": "./src/configs/status.ts",
37
52
  "./package.json": "./package.json"
38
53
  },
39
54
  "peerDependencies": {
@@ -136,6 +136,22 @@ export function CrudDialog({
136
136
  };
137
137
  }, [open]);
138
138
 
139
+ // FIX (lỗi conflict event kinh điển): Radix Dialog + Select/Combobox/Popover
140
+ // lồng nhau đôi khi để sót `pointer-events: none` / scroll-lock trên <body> sau
141
+ // khi đóng, khiến TOÀN trang không click được. Reset thủ công SAU khi đóng, có
142
+ // guard `=== "none"` và clearTimeout khi mở lại để không gỡ khoá lúc đang mở.
143
+ useEffect(() => {
144
+ if (open) return;
145
+ const timer = setTimeout(() => {
146
+ if (document.body.style.pointerEvents === "none") {
147
+ document.body.style.pointerEvents = "";
148
+ }
149
+ document.body.style.overflow = "";
150
+ document.body.removeAttribute("data-scroll-locked");
151
+ }, 300);
152
+ return () => clearTimeout(timer);
153
+ }, [open]);
154
+
139
155
  // Keyboard shortcuts
140
156
  useEffect(() => {
141
157
  if (!open) return;
@@ -139,6 +139,22 @@ export function CrudSheet({
139
139
  };
140
140
  }, [open]);
141
141
 
142
+ // FIX (lỗi conflict event kinh điển): Radix Sheet + Select/Combobox/Popover lồng
143
+ // nhau đôi khi để sót `pointer-events: none` / scroll-lock trên <body> sau khi
144
+ // đóng, khiến TOÀN trang không click được. Reset thủ công SAU khi đóng, có guard
145
+ // `=== "none"` và clearTimeout khi mở lại để không gỡ khoá lúc đang mở.
146
+ useEffect(() => {
147
+ if (open) return;
148
+ const timer = setTimeout(() => {
149
+ if (document.body.style.pointerEvents === "none") {
150
+ document.body.style.pointerEvents = "";
151
+ }
152
+ document.body.style.overflow = "";
153
+ document.body.removeAttribute("data-scroll-locked");
154
+ }, 300);
155
+ return () => clearTimeout(timer);
156
+ }, [open]);
157
+
142
158
  // Keyboard shortcuts
143
159
  useEffect(() => {
144
160
  if (!open) return;
@@ -1,4 +1,5 @@
1
1
  import type { EntityConfig, ImportOptions, ImportResult } from "../../types";
2
+ import { toCsv } from "../../export/to-csv";
2
3
 
3
4
  function getRowValue(
4
5
  row: Record<string, unknown>,
@@ -257,15 +258,9 @@ export function exportToCSV(
257
258
  data: Record<string, unknown>[],
258
259
  fields?: string[],
259
260
  ): string {
260
- if (data.length === 0) return "";
261
-
262
- const keys = fields || Object.keys(data[0]);
263
- const headers = keys.join(",");
264
- const rows = data.map((row) =>
265
- keys.map((key) => String(row[key] || "")).join(","),
266
- );
267
-
268
- return [headers, ...rows].join("\n");
261
+ // Delegates to the shared RFC 4180 writer so values with commas/quotes/
262
+ // newlines (and falsy 0/false) survive instead of breaking column alignment.
263
+ return toCsv(data, fields);
269
264
  }
270
265
 
271
266
  /**
@@ -0,0 +1,236 @@
1
+ /**
2
+ * Application Error class with structured error information.
3
+ * Provides error code, user-friendly message, and context for debugging.
4
+ */
5
+ export class AppError extends Error {
6
+ /** Machine-readable error code (e.g. CUSTOMER_DUPLICATE_PHONE) */
7
+ readonly code: string
8
+
9
+ /** User-friendly message in Vietnamese */
10
+ readonly userMessage: string
11
+
12
+ /** HTTP status code */
13
+ readonly statusCode: number
14
+
15
+ /** Additional context for debugging */
16
+ readonly context?: Record<string, unknown>
17
+
18
+ /** HTTP Request context (URL, method, body, status) */
19
+ readonly requestContext?: Record<string, unknown>
20
+
21
+ /** Timestamp of the error */
22
+ readonly timestamp: string
23
+
24
+ /** Unique error ID for tracing */
25
+ readonly errorId: string
26
+
27
+ constructor(options: {
28
+ code: string
29
+ message: string
30
+ userMessage?: string
31
+ statusCode?: number
32
+ context?: Record<string, unknown>
33
+ requestContext?: Record<string, unknown>
34
+ cause?: unknown
35
+ }) {
36
+ super(options.message, { cause: options.cause })
37
+ this.name = "AppError"
38
+ this.code = options.code
39
+ this.userMessage = options.userMessage || options.message
40
+ this.statusCode = options.statusCode || 500
41
+ this.context = options.context
42
+ this.requestContext = options.requestContext
43
+ this.timestamp = new Date().toISOString()
44
+ this.errorId = generateErrorId()
45
+ }
46
+
47
+ /**
48
+ * Convert to a serializable object for API responses.
49
+ * Contains user-safe info only (no stack traces in production).
50
+ */
51
+ toResponse() {
52
+ return {
53
+ error: this.userMessage,
54
+ code: this.code,
55
+ errorId: this.errorId,
56
+ timestamp: this.timestamp,
57
+ ...(process.env.NODE_ENV === "development" && {
58
+ debug: {
59
+ message: this.message,
60
+ context: this.context,
61
+ requestContext: this.requestContext,
62
+ stack: this.stack,
63
+ },
64
+ }),
65
+ }
66
+ }
67
+
68
+ /**
69
+ * Convert to a full diagnostic string for copy/paste support.
70
+ */
71
+ toDiagnosticString(): string {
72
+ const parts = [
73
+ `=== Thông tin lỗi ===`,
74
+ `Mã lỗi: ${this.code}`,
75
+ `ID: ${this.errorId}`,
76
+ `Thời gian: ${formatTimestampVN(this.timestamp)}`,
77
+ `Mô tả: ${this.userMessage}`,
78
+ ]
79
+
80
+ if (this.context && Object.keys(this.context).length > 0) {
81
+ parts.push(`Chi tiết: ${JSON.stringify(this.context, null, 2)}`)
82
+ }
83
+
84
+ if (this.requestContext && Object.keys(this.requestContext).length > 0) {
85
+ parts.push(`Request Context:\n${JSON.stringify(this.requestContext, null, 2)}`)
86
+ }
87
+
88
+ if (this.message !== this.userMessage) {
89
+ parts.push(`Technical: ${this.message}`)
90
+ }
91
+
92
+ if (this.stack) {
93
+ parts.push(`\nStack Trace:\n${this.stack}`)
94
+ }
95
+
96
+ return parts.join("\n")
97
+ }
98
+ }
99
+
100
+ /**
101
+ * Convert any error to AppError.
102
+ * Handles Prisma errors, standard errors, and unknown errors.
103
+ */
104
+ export function toAppError(error: unknown, fallbackUserMessage?: string): AppError {
105
+ // Already AppError
106
+ if (error instanceof AppError) return error
107
+
108
+ // Prisma known errors
109
+ if (isPrismaError(error)) {
110
+ return prismaToAppError(error)
111
+ }
112
+
113
+ // Standard Error
114
+ if (error instanceof Error) {
115
+ return new AppError({
116
+ code: "SYSTEM_ERROR",
117
+ message: error.message,
118
+ userMessage: fallbackUserMessage || "Đã xảy ra lỗi hệ thống",
119
+ statusCode: 500,
120
+ cause: error,
121
+ })
122
+ }
123
+
124
+ // Unknown
125
+ return new AppError({
126
+ code: "UNKNOWN_ERROR",
127
+ message: String(error),
128
+ userMessage: fallbackUserMessage || "Đã xảy ra lỗi không xác định",
129
+ statusCode: 500,
130
+ })
131
+ }
132
+
133
+ // ============================================================================
134
+ // Prisma Error Handling
135
+ // ============================================================================
136
+
137
+ interface PrismaError {
138
+ code: string
139
+ message: string
140
+ meta?: { target?: string[]; modelName?: string; cause?: string }
141
+ }
142
+
143
+ function isPrismaError(error: unknown): error is PrismaError {
144
+ return (
145
+ typeof error === "object" &&
146
+ error !== null &&
147
+ "code" in error &&
148
+ typeof (error as any).code === "string" &&
149
+ (error as any).code.startsWith("P")
150
+ )
151
+ }
152
+
153
+ function prismaToAppError(error: PrismaError): AppError {
154
+ const target = error.meta?.target?.join(", ") || ""
155
+ const model = error.meta?.modelName || ""
156
+
157
+ switch (error.code) {
158
+ case "P2002": // Unique constraint violation
159
+ return new AppError({
160
+ code: "VALIDATION_DUPLICATE",
161
+ message: `Duplicate value for ${target} in ${model}`,
162
+ userMessage: `Giá trị đã tồn tại${target ? ` (${target})` : ""}. Vui lòng sử dụng giá trị khác.`,
163
+ statusCode: 409,
164
+ context: { prismaCode: error.code, target, model },
165
+ cause: error,
166
+ })
167
+
168
+ case "P2025": // Record not found
169
+ return new AppError({
170
+ code: "NOT_FOUND",
171
+ message: `Record not found in ${model}`,
172
+ userMessage: `Không tìm thấy dữ liệu${model ? ` (${model})` : ""}`,
173
+ statusCode: 404,
174
+ context: { prismaCode: error.code, model },
175
+ cause: error,
176
+ })
177
+
178
+ case "P2003": // Foreign key constraint violation
179
+ return new AppError({
180
+ code: "VALIDATION_REFERENCE",
181
+ message: `Foreign key constraint failed on ${target}`,
182
+ userMessage: `Không thể xóa vì có dữ liệu liên quan`,
183
+ statusCode: 409,
184
+ context: { prismaCode: error.code, target, model },
185
+ cause: error,
186
+ })
187
+
188
+ case "P2014": // Required relation violation
189
+ return new AppError({
190
+ code: "VALIDATION_RELATION",
191
+ message: `Required relation violation in ${model}`,
192
+ userMessage: `Thiếu dữ liệu bắt buộc liên quan`,
193
+ statusCode: 400,
194
+ context: { prismaCode: error.code, model },
195
+ cause: error,
196
+ })
197
+
198
+ default:
199
+ return new AppError({
200
+ code: `DATABASE_${error.code}`,
201
+ message: error.message,
202
+ userMessage: "Lỗi cơ sở dữ liệu. Vui lòng thử lại.",
203
+ statusCode: 500,
204
+ context: { prismaCode: error.code, model, cause: error.meta?.cause },
205
+ cause: error,
206
+ })
207
+ }
208
+ }
209
+
210
+ // ============================================================================
211
+ // Helpers
212
+ // ============================================================================
213
+
214
+ function generateErrorId(): string {
215
+ // Short 8 char ID: timestamp base36 + random
216
+ const ts = Date.now().toString(36).slice(-4)
217
+ const rand = Math.random().toString(36).slice(2, 6)
218
+ return `${ts}${rand}`.toUpperCase()
219
+ }
220
+
221
+ function formatTimestampVN(isoString: string): string {
222
+ try {
223
+ const date = new Date(isoString)
224
+ return date.toLocaleString("vi-VN", {
225
+ timeZone: "Asia/Ho_Chi_Minh",
226
+ day: "2-digit",
227
+ month: "2-digit",
228
+ year: "numeric",
229
+ hour: "2-digit",
230
+ minute: "2-digit",
231
+ second: "2-digit",
232
+ })
233
+ } catch {
234
+ return isoString
235
+ }
236
+ }
@@ -0,0 +1,89 @@
1
+ import { NextResponse } from "next/server"
2
+ import type { NextRequest } from "next/server"
3
+
4
+ import { AppError, toAppError } from "./app-error"
5
+
6
+ /**
7
+ * Wrap an API route handler with centralized error handling.
8
+ * Catches all errors and returns structured JSON responses.
9
+ *
10
+ * Usage:
11
+ * export async function POST(req: NextRequest) {
12
+ * return withErrorHandler(async () => {
13
+ * // your logic here
14
+ * return NextResponse.json({ data: result })
15
+ * }, { operation: "createCustomer" })
16
+ * }
17
+ */
18
+ export async function withErrorHandler(
19
+ handler: () => Promise<Response>,
20
+ options?: { operation?: string; req?: NextRequest | Request }
21
+ ): Promise<Response> {
22
+ try {
23
+ return await handler()
24
+ } catch (error) {
25
+ const appError = toAppError(error)
26
+
27
+ const requestContext = options?.req
28
+ ? {
29
+ url: options.req.url,
30
+ method: options.req.method,
31
+ }
32
+ : undefined
33
+
34
+ // If the error doesn't already have a requestContext, attach it from the request
35
+ if (requestContext && !appError.requestContext) {
36
+ Object.assign(appError, { requestContext })
37
+ }
38
+
39
+ // Dev/terminal visibility. The persisted error_logs row (via serverError)
40
+ // is the durable record; this is just the in-process trace.
41
+ console.error(
42
+ `[${options?.operation || "API"}] ${appError.code}: ${appError.message}`,
43
+ {
44
+ errorId: appError.errorId,
45
+ code: appError.code,
46
+ statusCode: appError.statusCode,
47
+ context: appError.context,
48
+ requestContext: appError.requestContext,
49
+ operation: options?.operation,
50
+ stack: appError.stack,
51
+ }
52
+ )
53
+
54
+ return createErrorResponse(appError)
55
+ }
56
+ }
57
+
58
+ /**
59
+ * Create a structured error response from an AppError.
60
+ */
61
+ export function createErrorResponse(error: AppError): NextResponse {
62
+ return NextResponse.json(error.toResponse(), {
63
+ status: error.statusCode,
64
+ headers: {
65
+ "X-Error-Id": error.errorId,
66
+ },
67
+ })
68
+ }
69
+
70
+ /**
71
+ * Create an AppError and return as response.
72
+ * Convenience for direct use in routes without wrapping.
73
+ */
74
+ export function errorResponse(
75
+ code: string,
76
+ userMessage: string,
77
+ statusCode: number = 500,
78
+ options?: { message?: string; context?: Record<string, unknown> }
79
+ ): NextResponse {
80
+ const error = new AppError({
81
+ code,
82
+ message: options?.message || userMessage,
83
+ userMessage,
84
+ statusCode,
85
+ context: options?.context,
86
+ })
87
+
88
+ return createErrorResponse(error)
89
+ }
@@ -0,0 +1,17 @@
1
+ export { AppError, toAppError } from "./app-error"
2
+ export {
3
+ withErrorHandler,
4
+ createErrorResponse,
5
+ errorResponse,
6
+ } from "./error-handler"
7
+ export {
8
+ createErrorLogger,
9
+ buildServerError,
10
+ createServerError,
11
+ } from "./server-error"
12
+ export type {
13
+ ServerErrorContext,
14
+ ErrorLogDelegate,
15
+ ErrorLoggerDb,
16
+ LogServerError,
17
+ } from "./server-error"