@imjp/writenex-astro 0.1.0
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/README.md +539 -0
- package/dist/chunk-5PM6EQE5.js +151 -0
- package/dist/chunk-5PM6EQE5.js.map +1 -0
- package/dist/chunk-7XU5X6CW.js +1331 -0
- package/dist/chunk-7XU5X6CW.js.map +1 -0
- package/dist/chunk-AAOQHQPU.js +574 -0
- package/dist/chunk-AAOQHQPU.js.map +1 -0
- package/dist/chunk-CF2XXJFF.js +1410 -0
- package/dist/chunk-CF2XXJFF.js.map +1 -0
- package/dist/chunk-CRPZUUDU.js +52 -0
- package/dist/chunk-CRPZUUDU.js.map +1 -0
- package/dist/chunk-CYLDJ3HZ.js +310 -0
- package/dist/chunk-CYLDJ3HZ.js.map +1 -0
- package/dist/chunk-KIKIPIFA.js +1 -0
- package/dist/chunk-KIKIPIFA.js.map +1 -0
- package/dist/chunk-XNTQTTJU.js +145 -0
- package/dist/chunk-XNTQTTJU.js.map +1 -0
- package/dist/client/index.css +2 -0
- package/dist/client/index.css.map +1 -0
- package/dist/client/index.js +375 -0
- package/dist/client/index.js.map +1 -0
- package/dist/client/styles.css +584 -0
- package/dist/client/variables.css +304 -0
- package/dist/config/index.d.ts +54 -0
- package/dist/config/index.js +38 -0
- package/dist/config/index.js.map +1 -0
- package/dist/config-BmEdBDo_.d.ts +220 -0
- package/dist/content-BWR52vD-.d.ts +64 -0
- package/dist/discovery/index.d.ts +310 -0
- package/dist/discovery/index.js +38 -0
- package/dist/discovery/index.js.map +1 -0
- package/dist/errors-C0iYiDTv.d.ts +107 -0
- package/dist/filesystem/index.d.ts +1292 -0
- package/dist/filesystem/index.js +203 -0
- package/dist/filesystem/index.js.map +1 -0
- package/dist/image-FP7w5ZIs.d.ts +47 -0
- package/dist/index.d.ts +64 -0
- package/dist/index.js +151 -0
- package/dist/index.js.map +1 -0
- package/dist/loader-55LWCXHA.js +12 -0
- package/dist/loader-55LWCXHA.js.map +1 -0
- package/dist/loader-CrdnaAWR.d.ts +327 -0
- package/dist/server/index.d.ts +357 -0
- package/dist/server/index.js +37 -0
- package/dist/server/index.js.map +1 -0
- package/package.json +94 -0
- package/src/client/App.tsx +900 -0
- package/src/client/components/ConfigPanel/ConfigPanel.css +553 -0
- package/src/client/components/ConfigPanel/ConfigPanel.tsx +396 -0
- package/src/client/components/ConfigPanel/index.ts +6 -0
- package/src/client/components/CreateContentModal/CreateContentModal.css +327 -0
- package/src/client/components/CreateContentModal/CreateContentModal.tsx +216 -0
- package/src/client/components/CreateContentModal/index.ts +7 -0
- package/src/client/components/Editor/Editor.css +885 -0
- package/src/client/components/Editor/Editor.tsx +484 -0
- package/src/client/components/Editor/ImageDialog.css +344 -0
- package/src/client/components/Editor/ImageDialog.tsx +367 -0
- package/src/client/components/Editor/LinkDialog.css +326 -0
- package/src/client/components/Editor/LinkDialog.tsx +332 -0
- package/src/client/components/Editor/index.ts +6 -0
- package/src/client/components/FrontmatterForm/FrontmatterForm.css +468 -0
- package/src/client/components/FrontmatterForm/FrontmatterForm.tsx +914 -0
- package/src/client/components/FrontmatterForm/index.ts +7 -0
- package/src/client/components/Header/Header.css +300 -0
- package/src/client/components/Header/Header.tsx +300 -0
- package/src/client/components/Header/index.ts +7 -0
- package/src/client/components/KeyboardShortcuts/KeyboardShortcuts.css +239 -0
- package/src/client/components/KeyboardShortcuts/KeyboardShortcuts.tsx +151 -0
- package/src/client/components/KeyboardShortcuts/index.ts +6 -0
- package/src/client/components/LazyEditor.tsx +75 -0
- package/src/client/components/LiveRegion/LiveRegion.css +19 -0
- package/src/client/components/LiveRegion/LiveRegion.tsx +60 -0
- package/src/client/components/LiveRegion/index.ts +7 -0
- package/src/client/components/SearchReplace/SearchReplacePanel.css +300 -0
- package/src/client/components/SearchReplace/SearchReplacePanel.tsx +332 -0
- package/src/client/components/SearchReplace/index.ts +7 -0
- package/src/client/components/SelectCollectionModal/SelectCollectionModal.css +308 -0
- package/src/client/components/SelectCollectionModal/SelectCollectionModal.tsx +223 -0
- package/src/client/components/SelectCollectionModal/index.ts +7 -0
- package/src/client/components/Sidebar/Sidebar.css +570 -0
- package/src/client/components/Sidebar/Sidebar.tsx +617 -0
- package/src/client/components/Sidebar/index.ts +7 -0
- package/src/client/components/SkipLink/SkipLink.css +51 -0
- package/src/client/components/SkipLink/SkipLink.tsx +67 -0
- package/src/client/components/SkipLink/index.ts +7 -0
- package/src/client/components/UnsavedChangesModal/UnsavedChangesModal.css +233 -0
- package/src/client/components/UnsavedChangesModal/UnsavedChangesModal.tsx +160 -0
- package/src/client/components/UnsavedChangesModal/index.ts +1 -0
- package/src/client/components/VersionHistory/DiffViewer.css +430 -0
- package/src/client/components/VersionHistory/DiffViewer.tsx +383 -0
- package/src/client/components/VersionHistory/VersionActions.css +318 -0
- package/src/client/components/VersionHistory/VersionActions.tsx +277 -0
- package/src/client/components/VersionHistory/VersionHistoryPanel.css +369 -0
- package/src/client/components/VersionHistory/VersionHistoryPanel.tsx +469 -0
- package/src/client/components/VersionHistory/index.ts +9 -0
- package/src/client/context/ApiContext.tsx +154 -0
- package/src/client/context/ThemeContext.tsx +172 -0
- package/src/client/hooks/useAnnounce.ts +201 -0
- package/src/client/hooks/useApi.ts +374 -0
- package/src/client/hooks/useArrowNavigation.ts +286 -0
- package/src/client/hooks/useAutosave.ts +241 -0
- package/src/client/hooks/useFocusTrap.ts +178 -0
- package/src/client/hooks/useKeyboardShortcuts.ts +203 -0
- package/src/client/hooks/useSearch.ts +206 -0
- package/src/client/hooks/useVersionHistory.ts +451 -0
- package/src/client/index.tsx +70 -0
- package/src/client/styles.css +584 -0
- package/src/client/utils/focus.ts +57 -0
- package/src/client/utils/openInEditor.ts +130 -0
- package/src/client/variables.css +304 -0
- package/src/config/defaults.ts +109 -0
- package/src/config/index.ts +32 -0
- package/src/config/loader.ts +174 -0
- package/src/config/schema.ts +161 -0
- package/src/core/constants.ts +39 -0
- package/src/core/errors.ts +739 -0
- package/src/core/index.ts +11 -0
- package/src/discovery/collections.ts +216 -0
- package/src/discovery/index.ts +33 -0
- package/src/discovery/patterns.ts +702 -0
- package/src/discovery/schema.ts +453 -0
- package/src/filesystem/images.ts +798 -0
- package/src/filesystem/index.ts +107 -0
- package/src/filesystem/reader.ts +452 -0
- package/src/filesystem/version-config.ts +390 -0
- package/src/filesystem/versions.ts +1339 -0
- package/src/filesystem/watcher.ts +226 -0
- package/src/filesystem/writer.ts +540 -0
- package/src/index.ts +61 -0
- package/src/integration.ts +228 -0
- package/src/server/assets.ts +254 -0
- package/src/server/cache.ts +355 -0
- package/src/server/index.ts +33 -0
- package/src/server/middleware.ts +209 -0
- package/src/server/routes.ts +1428 -0
- package/src/types/api.ts +61 -0
- package/src/types/config.ts +134 -0
- package/src/types/content.ts +64 -0
- package/src/types/image.ts +48 -0
- package/src/types/index.ts +58 -0
- package/src/types/version.ts +117 -0
|
@@ -0,0 +1,739 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Custom error classes for @writenex/astro
|
|
3
|
+
*
|
|
4
|
+
* This module provides categorized error types for better error handling,
|
|
5
|
+
* debugging, and user-facing error messages across the integration.
|
|
6
|
+
*
|
|
7
|
+
* ## Error Categories:
|
|
8
|
+
* - Configuration errors (invalid config, missing files)
|
|
9
|
+
* - Filesystem errors (read/write failures, permissions)
|
|
10
|
+
* - Content errors (parsing, validation, not found)
|
|
11
|
+
* - API errors (request handling, validation)
|
|
12
|
+
* - Version history errors (manifest, storage)
|
|
13
|
+
*
|
|
14
|
+
* @module @writenex/astro/core/errors
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Error codes for categorization and i18n support
|
|
19
|
+
*/
|
|
20
|
+
export enum WritenexErrorCode {
|
|
21
|
+
// Configuration errors (1xx)
|
|
22
|
+
CONFIG_NOT_FOUND = "CONFIG_NOT_FOUND",
|
|
23
|
+
CONFIG_INVALID = "CONFIG_INVALID",
|
|
24
|
+
CONFIG_PARSE_ERROR = "CONFIG_PARSE_ERROR",
|
|
25
|
+
|
|
26
|
+
// Filesystem errors (2xx)
|
|
27
|
+
FS_READ_ERROR = "FS_READ_ERROR",
|
|
28
|
+
FS_WRITE_ERROR = "FS_WRITE_ERROR",
|
|
29
|
+
FS_DELETE_ERROR = "FS_DELETE_ERROR",
|
|
30
|
+
FS_PERMISSION_DENIED = "FS_PERMISSION_DENIED",
|
|
31
|
+
FS_PATH_NOT_FOUND = "FS_PATH_NOT_FOUND",
|
|
32
|
+
FS_PATH_TRAVERSAL = "FS_PATH_TRAVERSAL",
|
|
33
|
+
|
|
34
|
+
// Content errors (3xx)
|
|
35
|
+
CONTENT_NOT_FOUND = "CONTENT_NOT_FOUND",
|
|
36
|
+
CONTENT_PARSE_ERROR = "CONTENT_PARSE_ERROR",
|
|
37
|
+
CONTENT_VALIDATION_ERROR = "CONTENT_VALIDATION_ERROR",
|
|
38
|
+
CONTENT_ALREADY_EXISTS = "CONTENT_ALREADY_EXISTS",
|
|
39
|
+
CONTENT_INVALID_SLUG = "CONTENT_INVALID_SLUG",
|
|
40
|
+
CONTENT_CONFLICT = "CONTENT_CONFLICT",
|
|
41
|
+
|
|
42
|
+
// Collection errors (4xx)
|
|
43
|
+
COLLECTION_NOT_FOUND = "COLLECTION_NOT_FOUND",
|
|
44
|
+
COLLECTION_EMPTY = "COLLECTION_EMPTY",
|
|
45
|
+
COLLECTION_DISCOVERY_ERROR = "COLLECTION_DISCOVERY_ERROR",
|
|
46
|
+
|
|
47
|
+
// API errors (5xx)
|
|
48
|
+
API_BAD_REQUEST = "API_BAD_REQUEST",
|
|
49
|
+
API_METHOD_NOT_ALLOWED = "API_METHOD_NOT_ALLOWED",
|
|
50
|
+
API_INTERNAL_ERROR = "API_INTERNAL_ERROR",
|
|
51
|
+
API_TIMEOUT = "API_TIMEOUT",
|
|
52
|
+
|
|
53
|
+
// Image errors (6xx)
|
|
54
|
+
IMAGE_INVALID_TYPE = "IMAGE_INVALID_TYPE",
|
|
55
|
+
IMAGE_TOO_LARGE = "IMAGE_TOO_LARGE",
|
|
56
|
+
IMAGE_UPLOAD_ERROR = "IMAGE_UPLOAD_ERROR",
|
|
57
|
+
IMAGE_NOT_FOUND = "IMAGE_NOT_FOUND",
|
|
58
|
+
|
|
59
|
+
// Version history errors (7xx)
|
|
60
|
+
VERSION_NOT_FOUND = "VERSION_NOT_FOUND",
|
|
61
|
+
VERSION_MANIFEST_CORRUPT = "VERSION_MANIFEST_CORRUPT",
|
|
62
|
+
VERSION_LOCK_TIMEOUT = "VERSION_LOCK_TIMEOUT",
|
|
63
|
+
VERSION_SAVE_ERROR = "VERSION_SAVE_ERROR",
|
|
64
|
+
VERSION_RESTORE_ERROR = "VERSION_RESTORE_ERROR",
|
|
65
|
+
|
|
66
|
+
// Pattern errors (8xx)
|
|
67
|
+
PATTERN_INVALID = "PATTERN_INVALID",
|
|
68
|
+
PATTERN_MISSING_TOKEN = "PATTERN_MISSING_TOKEN",
|
|
69
|
+
|
|
70
|
+
// Unknown
|
|
71
|
+
UNKNOWN_ERROR = "UNKNOWN_ERROR",
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* HTTP status codes mapped to error categories
|
|
76
|
+
*/
|
|
77
|
+
export const ERROR_HTTP_STATUS: Record<WritenexErrorCode, number> = {
|
|
78
|
+
// Configuration errors - 500 (server misconfiguration)
|
|
79
|
+
[WritenexErrorCode.CONFIG_NOT_FOUND]: 500,
|
|
80
|
+
[WritenexErrorCode.CONFIG_INVALID]: 500,
|
|
81
|
+
[WritenexErrorCode.CONFIG_PARSE_ERROR]: 500,
|
|
82
|
+
|
|
83
|
+
// Filesystem errors
|
|
84
|
+
[WritenexErrorCode.FS_READ_ERROR]: 500,
|
|
85
|
+
[WritenexErrorCode.FS_WRITE_ERROR]: 500,
|
|
86
|
+
[WritenexErrorCode.FS_DELETE_ERROR]: 500,
|
|
87
|
+
[WritenexErrorCode.FS_PERMISSION_DENIED]: 403,
|
|
88
|
+
[WritenexErrorCode.FS_PATH_NOT_FOUND]: 404,
|
|
89
|
+
[WritenexErrorCode.FS_PATH_TRAVERSAL]: 400,
|
|
90
|
+
|
|
91
|
+
// Content errors
|
|
92
|
+
[WritenexErrorCode.CONTENT_NOT_FOUND]: 404,
|
|
93
|
+
[WritenexErrorCode.CONTENT_PARSE_ERROR]: 500,
|
|
94
|
+
[WritenexErrorCode.CONTENT_VALIDATION_ERROR]: 400,
|
|
95
|
+
[WritenexErrorCode.CONTENT_ALREADY_EXISTS]: 409,
|
|
96
|
+
[WritenexErrorCode.CONTENT_INVALID_SLUG]: 400,
|
|
97
|
+
[WritenexErrorCode.CONTENT_CONFLICT]: 409,
|
|
98
|
+
|
|
99
|
+
// Collection errors
|
|
100
|
+
[WritenexErrorCode.COLLECTION_NOT_FOUND]: 404,
|
|
101
|
+
[WritenexErrorCode.COLLECTION_EMPTY]: 404,
|
|
102
|
+
[WritenexErrorCode.COLLECTION_DISCOVERY_ERROR]: 500,
|
|
103
|
+
|
|
104
|
+
// API errors
|
|
105
|
+
[WritenexErrorCode.API_BAD_REQUEST]: 400,
|
|
106
|
+
[WritenexErrorCode.API_METHOD_NOT_ALLOWED]: 405,
|
|
107
|
+
[WritenexErrorCode.API_INTERNAL_ERROR]: 500,
|
|
108
|
+
[WritenexErrorCode.API_TIMEOUT]: 504,
|
|
109
|
+
|
|
110
|
+
// Image errors
|
|
111
|
+
[WritenexErrorCode.IMAGE_INVALID_TYPE]: 400,
|
|
112
|
+
[WritenexErrorCode.IMAGE_TOO_LARGE]: 413,
|
|
113
|
+
[WritenexErrorCode.IMAGE_UPLOAD_ERROR]: 500,
|
|
114
|
+
[WritenexErrorCode.IMAGE_NOT_FOUND]: 404,
|
|
115
|
+
|
|
116
|
+
// Version history errors
|
|
117
|
+
[WritenexErrorCode.VERSION_NOT_FOUND]: 404,
|
|
118
|
+
[WritenexErrorCode.VERSION_MANIFEST_CORRUPT]: 500,
|
|
119
|
+
[WritenexErrorCode.VERSION_LOCK_TIMEOUT]: 503,
|
|
120
|
+
[WritenexErrorCode.VERSION_SAVE_ERROR]: 500,
|
|
121
|
+
[WritenexErrorCode.VERSION_RESTORE_ERROR]: 500,
|
|
122
|
+
|
|
123
|
+
// Pattern errors
|
|
124
|
+
[WritenexErrorCode.PATTERN_INVALID]: 400,
|
|
125
|
+
[WritenexErrorCode.PATTERN_MISSING_TOKEN]: 400,
|
|
126
|
+
|
|
127
|
+
// Unknown
|
|
128
|
+
[WritenexErrorCode.UNKNOWN_ERROR]: 500,
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Base error class for all Writenex errors
|
|
133
|
+
*
|
|
134
|
+
* Provides structured error information including error code,
|
|
135
|
+
* HTTP status, and optional context data for debugging.
|
|
136
|
+
*/
|
|
137
|
+
export class WritenexError extends Error {
|
|
138
|
+
/** Error code for categorization */
|
|
139
|
+
readonly code: WritenexErrorCode;
|
|
140
|
+
|
|
141
|
+
/** HTTP status code for API responses */
|
|
142
|
+
readonly httpStatus: number;
|
|
143
|
+
|
|
144
|
+
/** Additional context data for debugging */
|
|
145
|
+
readonly context?: Record<string, unknown>;
|
|
146
|
+
|
|
147
|
+
/** Original error if this wraps another error */
|
|
148
|
+
readonly cause?: Error;
|
|
149
|
+
|
|
150
|
+
constructor(
|
|
151
|
+
code: WritenexErrorCode,
|
|
152
|
+
message: string,
|
|
153
|
+
options?: {
|
|
154
|
+
context?: Record<string, unknown>;
|
|
155
|
+
cause?: Error;
|
|
156
|
+
}
|
|
157
|
+
) {
|
|
158
|
+
super(message);
|
|
159
|
+
this.name = "WritenexError";
|
|
160
|
+
this.code = code;
|
|
161
|
+
this.httpStatus = ERROR_HTTP_STATUS[code];
|
|
162
|
+
this.context = options?.context;
|
|
163
|
+
this.cause = options?.cause;
|
|
164
|
+
|
|
165
|
+
// Maintains proper stack trace for where error was thrown
|
|
166
|
+
if (Error.captureStackTrace) {
|
|
167
|
+
Error.captureStackTrace(this, WritenexError);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Convert error to JSON for API responses
|
|
173
|
+
*/
|
|
174
|
+
toJSON(): Record<string, unknown> {
|
|
175
|
+
return {
|
|
176
|
+
error: this.message,
|
|
177
|
+
code: this.code,
|
|
178
|
+
...(this.context ? { context: this.context } : {}),
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Create a user-friendly error message
|
|
184
|
+
*/
|
|
185
|
+
toUserMessage(): string {
|
|
186
|
+
return this.message;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// =============================================================================
|
|
191
|
+
// Configuration Errors
|
|
192
|
+
// =============================================================================
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Error thrown when configuration file is not found
|
|
196
|
+
*/
|
|
197
|
+
export class ConfigNotFoundError extends WritenexError {
|
|
198
|
+
constructor(searchPaths: string[]) {
|
|
199
|
+
super(
|
|
200
|
+
WritenexErrorCode.CONFIG_NOT_FOUND,
|
|
201
|
+
`Configuration file not found. Searched: ${searchPaths.join(", ")}`,
|
|
202
|
+
{ context: { searchPaths } }
|
|
203
|
+
);
|
|
204
|
+
this.name = "ConfigNotFoundError";
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Error thrown when configuration is invalid
|
|
210
|
+
*/
|
|
211
|
+
export class ConfigInvalidError extends WritenexError {
|
|
212
|
+
constructor(errors: string[], configPath?: string) {
|
|
213
|
+
super(
|
|
214
|
+
WritenexErrorCode.CONFIG_INVALID,
|
|
215
|
+
`Invalid configuration: ${errors.join("; ")}`,
|
|
216
|
+
{ context: { errors, configPath } }
|
|
217
|
+
);
|
|
218
|
+
this.name = "ConfigInvalidError";
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Error thrown when configuration file cannot be parsed
|
|
224
|
+
*/
|
|
225
|
+
export class ConfigParseError extends WritenexError {
|
|
226
|
+
constructor(configPath: string, cause?: Error) {
|
|
227
|
+
super(
|
|
228
|
+
WritenexErrorCode.CONFIG_PARSE_ERROR,
|
|
229
|
+
`Failed to parse configuration file: ${configPath}`,
|
|
230
|
+
{ context: { configPath }, cause }
|
|
231
|
+
);
|
|
232
|
+
this.name = "ConfigParseError";
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// =============================================================================
|
|
237
|
+
// Filesystem Errors
|
|
238
|
+
// =============================================================================
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Error thrown when file read operation fails
|
|
242
|
+
*/
|
|
243
|
+
export class FileReadError extends WritenexError {
|
|
244
|
+
constructor(filePath: string, cause?: Error) {
|
|
245
|
+
super(WritenexErrorCode.FS_READ_ERROR, `Failed to read file: ${filePath}`, {
|
|
246
|
+
context: { filePath },
|
|
247
|
+
cause,
|
|
248
|
+
});
|
|
249
|
+
this.name = "FileReadError";
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Error thrown when file write operation fails
|
|
255
|
+
*/
|
|
256
|
+
export class FileWriteError extends WritenexError {
|
|
257
|
+
constructor(filePath: string, cause?: Error) {
|
|
258
|
+
super(
|
|
259
|
+
WritenexErrorCode.FS_WRITE_ERROR,
|
|
260
|
+
`Failed to write file: ${filePath}`,
|
|
261
|
+
{ context: { filePath }, cause }
|
|
262
|
+
);
|
|
263
|
+
this.name = "FileWriteError";
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Error thrown when file delete operation fails
|
|
269
|
+
*/
|
|
270
|
+
export class FileDeleteError extends WritenexError {
|
|
271
|
+
constructor(filePath: string, cause?: Error) {
|
|
272
|
+
super(
|
|
273
|
+
WritenexErrorCode.FS_DELETE_ERROR,
|
|
274
|
+
`Failed to delete file: ${filePath}`,
|
|
275
|
+
{ context: { filePath }, cause }
|
|
276
|
+
);
|
|
277
|
+
this.name = "FileDeleteError";
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Error thrown when path is not found
|
|
283
|
+
*/
|
|
284
|
+
export class PathNotFoundError extends WritenexError {
|
|
285
|
+
constructor(path: string, type: "file" | "directory" = "file") {
|
|
286
|
+
super(
|
|
287
|
+
WritenexErrorCode.FS_PATH_NOT_FOUND,
|
|
288
|
+
`${type === "directory" ? "Directory" : "File"} not found: ${path}`,
|
|
289
|
+
{ context: { path, type } }
|
|
290
|
+
);
|
|
291
|
+
this.name = "PathNotFoundError";
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Error thrown when path traversal attack is detected
|
|
297
|
+
*/
|
|
298
|
+
export class PathTraversalError extends WritenexError {
|
|
299
|
+
constructor(requestedPath: string, basePath: string) {
|
|
300
|
+
super(
|
|
301
|
+
WritenexErrorCode.FS_PATH_TRAVERSAL,
|
|
302
|
+
"Invalid path: attempted path traversal detected",
|
|
303
|
+
{ context: { requestedPath, basePath } }
|
|
304
|
+
);
|
|
305
|
+
this.name = "PathTraversalError";
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// =============================================================================
|
|
310
|
+
// Content Errors
|
|
311
|
+
// =============================================================================
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Error thrown when content item is not found
|
|
315
|
+
*/
|
|
316
|
+
export class ContentNotFoundError extends WritenexError {
|
|
317
|
+
constructor(collection: string, contentId: string) {
|
|
318
|
+
super(
|
|
319
|
+
WritenexErrorCode.CONTENT_NOT_FOUND,
|
|
320
|
+
`Content '${contentId}' not found in collection '${collection}'`,
|
|
321
|
+
{ context: { collection, contentId } }
|
|
322
|
+
);
|
|
323
|
+
this.name = "ContentNotFoundError";
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Error thrown when content parsing fails
|
|
329
|
+
*/
|
|
330
|
+
export class ContentParseError extends WritenexError {
|
|
331
|
+
constructor(filePath: string, cause?: Error) {
|
|
332
|
+
super(
|
|
333
|
+
WritenexErrorCode.CONTENT_PARSE_ERROR,
|
|
334
|
+
`Failed to parse content file: ${filePath}`,
|
|
335
|
+
{ context: { filePath }, cause }
|
|
336
|
+
);
|
|
337
|
+
this.name = "ContentParseError";
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Error thrown when content validation fails
|
|
343
|
+
*/
|
|
344
|
+
export class ContentValidationError extends WritenexError {
|
|
345
|
+
constructor(errors: string[], contentId?: string) {
|
|
346
|
+
super(
|
|
347
|
+
WritenexErrorCode.CONTENT_VALIDATION_ERROR,
|
|
348
|
+
`Content validation failed: ${errors.join("; ")}`,
|
|
349
|
+
{ context: { errors, contentId } }
|
|
350
|
+
);
|
|
351
|
+
this.name = "ContentValidationError";
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Error thrown when content already exists
|
|
357
|
+
*/
|
|
358
|
+
export class ContentAlreadyExistsError extends WritenexError {
|
|
359
|
+
constructor(collection: string, slug: string) {
|
|
360
|
+
super(
|
|
361
|
+
WritenexErrorCode.CONTENT_ALREADY_EXISTS,
|
|
362
|
+
`Content with slug '${slug}' already exists in collection '${collection}'`,
|
|
363
|
+
{ context: { collection, slug } }
|
|
364
|
+
);
|
|
365
|
+
this.name = "ContentAlreadyExistsError";
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Error thrown when content was modified externally (conflict detection)
|
|
371
|
+
*
|
|
372
|
+
* This error includes both the server version and the client's expected mtime
|
|
373
|
+
* to help resolve the conflict.
|
|
374
|
+
*/
|
|
375
|
+
export class ContentConflictError extends WritenexError {
|
|
376
|
+
/** Current content on disk */
|
|
377
|
+
readonly serverContent: string;
|
|
378
|
+
/** Server's current mtime */
|
|
379
|
+
readonly serverMtime: number;
|
|
380
|
+
/** Client's expected mtime */
|
|
381
|
+
readonly clientMtime: number;
|
|
382
|
+
|
|
383
|
+
constructor(
|
|
384
|
+
collection: string,
|
|
385
|
+
contentId: string,
|
|
386
|
+
serverContent: string,
|
|
387
|
+
serverMtime: number,
|
|
388
|
+
clientMtime: number
|
|
389
|
+
) {
|
|
390
|
+
super(
|
|
391
|
+
WritenexErrorCode.CONTENT_CONFLICT,
|
|
392
|
+
`Content '${contentId}' in '${collection}' was modified externally. ` +
|
|
393
|
+
`Expected mtime: ${clientMtime}, actual: ${serverMtime}`,
|
|
394
|
+
{
|
|
395
|
+
context: {
|
|
396
|
+
collection,
|
|
397
|
+
contentId,
|
|
398
|
+
serverMtime,
|
|
399
|
+
clientMtime,
|
|
400
|
+
timeDiff: serverMtime - clientMtime,
|
|
401
|
+
},
|
|
402
|
+
}
|
|
403
|
+
);
|
|
404
|
+
this.name = "ContentConflictError";
|
|
405
|
+
this.serverContent = serverContent;
|
|
406
|
+
this.serverMtime = serverMtime;
|
|
407
|
+
this.clientMtime = clientMtime;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Override toJSON to include conflict-specific data
|
|
412
|
+
*/
|
|
413
|
+
override toJSON(): Record<string, unknown> {
|
|
414
|
+
return {
|
|
415
|
+
...super.toJSON(),
|
|
416
|
+
serverContent: this.serverContent,
|
|
417
|
+
serverMtime: this.serverMtime,
|
|
418
|
+
clientMtime: this.clientMtime,
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// =============================================================================
|
|
424
|
+
// Collection Errors
|
|
425
|
+
// =============================================================================
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Error thrown when collection is not found
|
|
429
|
+
*/
|
|
430
|
+
export class CollectionNotFoundError extends WritenexError {
|
|
431
|
+
constructor(collectionName: string) {
|
|
432
|
+
super(
|
|
433
|
+
WritenexErrorCode.COLLECTION_NOT_FOUND,
|
|
434
|
+
`Collection '${collectionName}' not found`,
|
|
435
|
+
{ context: { collectionName } }
|
|
436
|
+
);
|
|
437
|
+
this.name = "CollectionNotFoundError";
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* Error thrown when collection discovery fails
|
|
443
|
+
*/
|
|
444
|
+
export class CollectionDiscoveryError extends WritenexError {
|
|
445
|
+
constructor(contentPath: string, cause?: Error) {
|
|
446
|
+
super(
|
|
447
|
+
WritenexErrorCode.COLLECTION_DISCOVERY_ERROR,
|
|
448
|
+
`Failed to discover collections in: ${contentPath}`,
|
|
449
|
+
{ context: { contentPath }, cause }
|
|
450
|
+
);
|
|
451
|
+
this.name = "CollectionDiscoveryError";
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// =============================================================================
|
|
456
|
+
// API Errors
|
|
457
|
+
// =============================================================================
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Error thrown for bad API requests
|
|
461
|
+
*/
|
|
462
|
+
export class ApiBadRequestError extends WritenexError {
|
|
463
|
+
constructor(message: string, details?: Record<string, unknown>) {
|
|
464
|
+
super(WritenexErrorCode.API_BAD_REQUEST, message, { context: details });
|
|
465
|
+
this.name = "ApiBadRequestError";
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* Error thrown when HTTP method is not allowed
|
|
471
|
+
*/
|
|
472
|
+
export class ApiMethodNotAllowedError extends WritenexError {
|
|
473
|
+
constructor(method: string, allowedMethods: string[]) {
|
|
474
|
+
super(
|
|
475
|
+
WritenexErrorCode.API_METHOD_NOT_ALLOWED,
|
|
476
|
+
`Method ${method} not allowed. Allowed: ${allowedMethods.join(", ")}`,
|
|
477
|
+
{ context: { method, allowedMethods } }
|
|
478
|
+
);
|
|
479
|
+
this.name = "ApiMethodNotAllowedError";
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* Error thrown for API timeout
|
|
485
|
+
*/
|
|
486
|
+
export class ApiTimeoutError extends WritenexError {
|
|
487
|
+
constructor(operation: string, timeoutMs: number) {
|
|
488
|
+
super(
|
|
489
|
+
WritenexErrorCode.API_TIMEOUT,
|
|
490
|
+
`Operation '${operation}' timed out after ${timeoutMs}ms`,
|
|
491
|
+
{ context: { operation, timeoutMs } }
|
|
492
|
+
);
|
|
493
|
+
this.name = "ApiTimeoutError";
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// =============================================================================
|
|
498
|
+
// Image Errors
|
|
499
|
+
// =============================================================================
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* Error thrown when image type is invalid
|
|
503
|
+
*/
|
|
504
|
+
export class ImageInvalidTypeError extends WritenexError {
|
|
505
|
+
constructor(filename: string, supportedTypes: string[]) {
|
|
506
|
+
super(
|
|
507
|
+
WritenexErrorCode.IMAGE_INVALID_TYPE,
|
|
508
|
+
`Invalid image type for '${filename}'. Supported: ${supportedTypes.join(", ")}`,
|
|
509
|
+
{ context: { filename, supportedTypes } }
|
|
510
|
+
);
|
|
511
|
+
this.name = "ImageInvalidTypeError";
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/**
|
|
516
|
+
* Error thrown when image is too large
|
|
517
|
+
*/
|
|
518
|
+
export class ImageTooLargeError extends WritenexError {
|
|
519
|
+
constructor(filename: string, size: number, maxSize: number) {
|
|
520
|
+
super(
|
|
521
|
+
WritenexErrorCode.IMAGE_TOO_LARGE,
|
|
522
|
+
`Image '${filename}' is too large (${formatBytes(size)}). Maximum: ${formatBytes(maxSize)}`,
|
|
523
|
+
{ context: { filename, size, maxSize } }
|
|
524
|
+
);
|
|
525
|
+
this.name = "ImageTooLargeError";
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/**
|
|
530
|
+
* Error thrown when image upload fails
|
|
531
|
+
*/
|
|
532
|
+
export class ImageUploadError extends WritenexError {
|
|
533
|
+
constructor(filename: string, cause?: Error) {
|
|
534
|
+
super(
|
|
535
|
+
WritenexErrorCode.IMAGE_UPLOAD_ERROR,
|
|
536
|
+
`Failed to upload image: ${filename}`,
|
|
537
|
+
{ context: { filename }, cause }
|
|
538
|
+
);
|
|
539
|
+
this.name = "ImageUploadError";
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
/**
|
|
544
|
+
* Error thrown when image is not found
|
|
545
|
+
*/
|
|
546
|
+
export class ImageNotFoundError extends WritenexError {
|
|
547
|
+
constructor(imagePath: string) {
|
|
548
|
+
super(WritenexErrorCode.IMAGE_NOT_FOUND, `Image not found: ${imagePath}`, {
|
|
549
|
+
context: { imagePath },
|
|
550
|
+
});
|
|
551
|
+
this.name = "ImageNotFoundError";
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// =============================================================================
|
|
556
|
+
// Version History Errors
|
|
557
|
+
// =============================================================================
|
|
558
|
+
|
|
559
|
+
/**
|
|
560
|
+
* Error thrown when version is not found
|
|
561
|
+
*/
|
|
562
|
+
export class VersionNotFoundError extends WritenexError {
|
|
563
|
+
constructor(collection: string, contentId: string, versionId: string) {
|
|
564
|
+
super(
|
|
565
|
+
WritenexErrorCode.VERSION_NOT_FOUND,
|
|
566
|
+
`Version '${versionId}' not found for content '${contentId}' in '${collection}'`,
|
|
567
|
+
{ context: { collection, contentId, versionId } }
|
|
568
|
+
);
|
|
569
|
+
this.name = "VersionNotFoundError";
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
/**
|
|
574
|
+
* Error thrown when version manifest is corrupt
|
|
575
|
+
*/
|
|
576
|
+
export class VersionManifestCorruptError extends WritenexError {
|
|
577
|
+
constructor(manifestPath: string, cause?: Error) {
|
|
578
|
+
super(
|
|
579
|
+
WritenexErrorCode.VERSION_MANIFEST_CORRUPT,
|
|
580
|
+
`Version manifest is corrupt: ${manifestPath}`,
|
|
581
|
+
{ context: { manifestPath }, cause }
|
|
582
|
+
);
|
|
583
|
+
this.name = "VersionManifestCorruptError";
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
/**
|
|
588
|
+
* Error thrown when version lock times out
|
|
589
|
+
*/
|
|
590
|
+
export class VersionLockTimeoutError extends WritenexError {
|
|
591
|
+
constructor(storagePath: string, timeoutMs: number) {
|
|
592
|
+
super(
|
|
593
|
+
WritenexErrorCode.VERSION_LOCK_TIMEOUT,
|
|
594
|
+
`Timeout waiting for version lock on ${storagePath} after ${timeoutMs}ms`,
|
|
595
|
+
{ context: { storagePath, timeoutMs } }
|
|
596
|
+
);
|
|
597
|
+
this.name = "VersionLockTimeoutError";
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
/**
|
|
602
|
+
* Error thrown when version save fails
|
|
603
|
+
*/
|
|
604
|
+
export class VersionSaveError extends WritenexError {
|
|
605
|
+
constructor(collection: string, contentId: string, cause?: Error) {
|
|
606
|
+
super(
|
|
607
|
+
WritenexErrorCode.VERSION_SAVE_ERROR,
|
|
608
|
+
`Failed to save version for '${contentId}' in '${collection}'`,
|
|
609
|
+
{ context: { collection, contentId }, cause }
|
|
610
|
+
);
|
|
611
|
+
this.name = "VersionSaveError";
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
/**
|
|
616
|
+
* Error thrown when version restore fails
|
|
617
|
+
*/
|
|
618
|
+
export class VersionRestoreError extends WritenexError {
|
|
619
|
+
constructor(
|
|
620
|
+
collection: string,
|
|
621
|
+
contentId: string,
|
|
622
|
+
versionId: string,
|
|
623
|
+
cause?: Error
|
|
624
|
+
) {
|
|
625
|
+
super(
|
|
626
|
+
WritenexErrorCode.VERSION_RESTORE_ERROR,
|
|
627
|
+
`Failed to restore version '${versionId}' for '${contentId}' in '${collection}'`,
|
|
628
|
+
{ context: { collection, contentId, versionId }, cause }
|
|
629
|
+
);
|
|
630
|
+
this.name = "VersionRestoreError";
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// =============================================================================
|
|
635
|
+
// Pattern Errors
|
|
636
|
+
// =============================================================================
|
|
637
|
+
|
|
638
|
+
/**
|
|
639
|
+
* Error thrown when file pattern is invalid
|
|
640
|
+
*/
|
|
641
|
+
export class PatternInvalidError extends WritenexError {
|
|
642
|
+
constructor(pattern: string, reason: string) {
|
|
643
|
+
super(
|
|
644
|
+
WritenexErrorCode.PATTERN_INVALID,
|
|
645
|
+
`Invalid file pattern '${pattern}': ${reason}`,
|
|
646
|
+
{ context: { pattern, reason } }
|
|
647
|
+
);
|
|
648
|
+
this.name = "PatternInvalidError";
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
/**
|
|
653
|
+
* Error thrown when required pattern token is missing
|
|
654
|
+
*/
|
|
655
|
+
export class PatternMissingTokenError extends WritenexError {
|
|
656
|
+
constructor(pattern: string, missingToken: string) {
|
|
657
|
+
super(
|
|
658
|
+
WritenexErrorCode.PATTERN_MISSING_TOKEN,
|
|
659
|
+
`Pattern '${pattern}' is missing required token: {${missingToken}}`,
|
|
660
|
+
{ context: { pattern, missingToken } }
|
|
661
|
+
);
|
|
662
|
+
this.name = "PatternMissingTokenError";
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
// =============================================================================
|
|
667
|
+
// Utility Functions
|
|
668
|
+
// =============================================================================
|
|
669
|
+
|
|
670
|
+
/**
|
|
671
|
+
* Format bytes to human-readable string
|
|
672
|
+
*/
|
|
673
|
+
function formatBytes(bytes: number): string {
|
|
674
|
+
if (bytes === 0) return "0 Bytes";
|
|
675
|
+
const k = 1024;
|
|
676
|
+
const sizes = ["Bytes", "KB", "MB", "GB"];
|
|
677
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
678
|
+
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
/**
|
|
682
|
+
* Check if an error is a WritenexError
|
|
683
|
+
*/
|
|
684
|
+
export function isWritenexError(error: unknown): error is WritenexError {
|
|
685
|
+
return error instanceof WritenexError;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
/**
|
|
689
|
+
* Wrap an unknown error as a WritenexError
|
|
690
|
+
*
|
|
691
|
+
* Useful for catch blocks where the error type is unknown.
|
|
692
|
+
*/
|
|
693
|
+
export function wrapError(
|
|
694
|
+
error: unknown,
|
|
695
|
+
defaultCode: WritenexErrorCode = WritenexErrorCode.UNKNOWN_ERROR
|
|
696
|
+
): WritenexError {
|
|
697
|
+
if (isWritenexError(error)) {
|
|
698
|
+
return error;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
702
|
+
const cause = error instanceof Error ? error : undefined;
|
|
703
|
+
|
|
704
|
+
return new WritenexError(defaultCode, message, { cause });
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
/**
|
|
708
|
+
* Create error from Node.js filesystem error
|
|
709
|
+
*/
|
|
710
|
+
export function fromNodeError(
|
|
711
|
+
error: NodeJS.ErrnoException,
|
|
712
|
+
filePath: string
|
|
713
|
+
): WritenexError {
|
|
714
|
+
switch (error.code) {
|
|
715
|
+
case "ENOENT":
|
|
716
|
+
return new PathNotFoundError(filePath);
|
|
717
|
+
case "EACCES":
|
|
718
|
+
case "EPERM":
|
|
719
|
+
return new WritenexError(
|
|
720
|
+
WritenexErrorCode.FS_PERMISSION_DENIED,
|
|
721
|
+
`Permission denied: ${filePath}`,
|
|
722
|
+
{ context: { filePath, errno: error.code }, cause: error }
|
|
723
|
+
);
|
|
724
|
+
case "EISDIR":
|
|
725
|
+
return new WritenexError(
|
|
726
|
+
WritenexErrorCode.FS_READ_ERROR,
|
|
727
|
+
`Expected file but found directory: ${filePath}`,
|
|
728
|
+
{ context: { filePath }, cause: error }
|
|
729
|
+
);
|
|
730
|
+
case "ENOTDIR":
|
|
731
|
+
return new WritenexError(
|
|
732
|
+
WritenexErrorCode.FS_READ_ERROR,
|
|
733
|
+
`Expected directory but found file: ${filePath}`,
|
|
734
|
+
{ context: { filePath }, cause: error }
|
|
735
|
+
);
|
|
736
|
+
default:
|
|
737
|
+
return new FileReadError(filePath, error);
|
|
738
|
+
}
|
|
739
|
+
}
|