@tpzdsp/next-toolkit 1.6.0 → 1.7.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 +23 -46
- package/package.json +20 -3
- package/src/components/ErrorBoundary/ErrorFallback.stories.tsx +2 -2
- package/src/components/ErrorBoundary/ErrorFallback.test.tsx +2 -2
- package/src/components/ErrorBoundary/ErrorFallback.tsx +21 -5
- package/src/components/Modal/Modal.tsx +2 -1
- package/src/components/skipLink/SkipLink.tsx +2 -1
- package/src/errors/ApiError.ts +97 -23
- package/src/http/constants.ts +111 -0
- package/src/http/fetch.ts +263 -0
- package/src/http/index.ts +6 -0
- package/src/http/logger.ts +163 -0
- package/src/http/proxy.ts +269 -0
- package/src/http/query.ts +287 -0
- package/src/http/stream.ts +77 -0
- package/src/types/api.ts +25 -0
- package/src/utils/http.ts +2 -30
- package/src/utils/schema.ts +30 -0
package/README.md
CHANGED
|
@@ -262,7 +262,7 @@ yarn preview
|
|
|
262
262
|
|
|
263
263
|
## Local Development with Yalc
|
|
264
264
|
|
|
265
|
-
This library works
|
|
265
|
+
This library works well with `yalc` for local testing before publishing to npm. However, **hot reloading doesn't work reliably** with yalc - you'll need to manually push changes and restart your development server.
|
|
266
266
|
|
|
267
267
|
### Setup Yalc (one-time)
|
|
268
268
|
|
|
@@ -275,8 +275,6 @@ npm install -g yalc
|
|
|
275
275
|
|
|
276
276
|
```bash
|
|
277
277
|
# In this library directory
|
|
278
|
-
yarn yalc:publish
|
|
279
|
-
# or
|
|
280
278
|
yalc publish
|
|
281
279
|
```
|
|
282
280
|
|
|
@@ -284,9 +282,9 @@ yalc publish
|
|
|
284
282
|
|
|
285
283
|
```bash
|
|
286
284
|
# In your Next.js app directory
|
|
287
|
-
yalc add @
|
|
285
|
+
yalc add @tpzdsp/next-toolkit
|
|
288
286
|
|
|
289
|
-
# Install dependencies
|
|
287
|
+
# Install dependencies
|
|
290
288
|
yarn install
|
|
291
289
|
```
|
|
292
290
|
|
|
@@ -297,69 +295,48 @@ Add to your `next.config.js`:
|
|
|
297
295
|
```js
|
|
298
296
|
/** @type {import('next').NextConfig} */
|
|
299
297
|
const nextConfig = {
|
|
300
|
-
transpilePackages: ['@
|
|
301
|
-
// Enable if you want faster refresh during development
|
|
298
|
+
transpilePackages: ['@tpzdsp/next-toolkit'],
|
|
302
299
|
experimental: {
|
|
303
|
-
externalDir: true,
|
|
300
|
+
externalDir: true, // May help with yalc symlinks
|
|
304
301
|
},
|
|
305
302
|
};
|
|
306
303
|
|
|
307
304
|
module.exports = nextConfig;
|
|
308
305
|
```
|
|
309
306
|
|
|
310
|
-
### Development Workflow
|
|
307
|
+
### Development Workflow (Manual Process)
|
|
311
308
|
|
|
312
309
|
```bash
|
|
313
|
-
# 1. Make changes to the library
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
# or
|
|
310
|
+
# 1. Make changes to the library components/hooks/etc.
|
|
311
|
+
|
|
312
|
+
# 2. Push changes to connected apps
|
|
317
313
|
yalc push
|
|
318
314
|
|
|
319
|
-
# 3.
|
|
315
|
+
# 3. In your test app, clear Next.js cache and restart
|
|
316
|
+
rm -rf .next && yarn dev
|
|
320
317
|
```
|
|
321
318
|
|
|
319
|
+
### Why No Hot Reloading?
|
|
320
|
+
|
|
321
|
+
- **Symlink Issues**: Next.js file watching doesn't reliably detect changes in yalc-linked packages
|
|
322
|
+
- **Module Resolution**: TypeScript and bundler caching can prevent updates from being picked up
|
|
323
|
+
- **Source Distribution**: While our source distribution strategy helps with tree-shaking, it doesn't solve the hot reload problem with yalc
|
|
324
|
+
|
|
322
325
|
### Removing from Test App
|
|
323
326
|
|
|
324
327
|
```bash
|
|
325
328
|
# In your Next.js app directory
|
|
326
|
-
yalc remove @
|
|
329
|
+
yalc remove @tpzdsp/next-toolkit
|
|
327
330
|
yarn install
|
|
328
331
|
```
|
|
329
332
|
|
|
330
|
-
### Benefits
|
|
333
|
+
### Benefits Despite Manual Process
|
|
331
334
|
|
|
332
|
-
- ✅ **
|
|
335
|
+
- ✅ **Real Environment Testing**: Test in an actual Next.js app setup
|
|
333
336
|
- ✅ **Full TypeScript**: Complete type checking and IntelliSense
|
|
334
|
-
- ✅ **Tree Shaking**: Your app's bundler handles optimization
|
|
335
|
-
- ✅ **
|
|
336
|
-
-
|
|
337
|
-
|
|
338
|
-
## 🚀 Quick Yalc Workflow
|
|
339
|
-
|
|
340
|
-
### Library Development (this repo):
|
|
341
|
-
|
|
342
|
-
```bash
|
|
343
|
-
# First time setup
|
|
344
|
-
yalc publish
|
|
345
|
-
|
|
346
|
-
# After making changes
|
|
347
|
-
yalc push # Pushes to all connected apps
|
|
348
|
-
```
|
|
349
|
-
|
|
350
|
-
### In Your Next.js Test App:
|
|
351
|
-
|
|
352
|
-
```bash
|
|
353
|
-
# Add the library
|
|
354
|
-
yalc add @your-org/nextjs-library
|
|
355
|
-
yarn install
|
|
356
|
-
|
|
357
|
-
# Configure next.config.js (transpilePackages: ['@your-org/nextjs-library'])
|
|
358
|
-
|
|
359
|
-
# Remove when done testing
|
|
360
|
-
yalc remove @your-org/nextjs-library
|
|
361
|
-
yarn install
|
|
362
|
-
```
|
|
337
|
+
- ✅ **Tree Shaking**: Your app's bundler handles optimization
|
|
338
|
+
- ✅ **No Publishing**: Test without polluting npm registry
|
|
339
|
+
- ❌ **Manual Refresh Required**: No automatic hot reloading
|
|
363
340
|
|
|
364
341
|
## 🛠️ Adding New Components
|
|
365
342
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tpzdsp/next-toolkit",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.0",
|
|
4
4
|
"description": "A reusable React component library for Next.js applications",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"private": false,
|
|
@@ -38,6 +38,11 @@
|
|
|
38
38
|
"import": "./src/utils/http.ts",
|
|
39
39
|
"require": "./src/utils/http.ts"
|
|
40
40
|
},
|
|
41
|
+
"./utils/schema": {
|
|
42
|
+
"types": "./src/utils/schema.ts",
|
|
43
|
+
"import": "./src/utils/schema.ts",
|
|
44
|
+
"require": "./src/utils/schema.ts"
|
|
45
|
+
},
|
|
41
46
|
"./components": {
|
|
42
47
|
"types": "./src/components/index.ts",
|
|
43
48
|
"import": "./src/components/index.ts",
|
|
@@ -48,6 +53,11 @@
|
|
|
48
53
|
"import": "./src/components/select/index.ts",
|
|
49
54
|
"require": "./src/components/select/index.ts"
|
|
50
55
|
},
|
|
56
|
+
"./http": {
|
|
57
|
+
"types": "./src/http/index.ts",
|
|
58
|
+
"import": "./src/http/index.ts",
|
|
59
|
+
"require": "./src/http/index.ts"
|
|
60
|
+
},
|
|
51
61
|
"./map": {
|
|
52
62
|
"types": "./src/map/index.ts",
|
|
53
63
|
"import": "./src/map/index.ts",
|
|
@@ -104,6 +114,7 @@
|
|
|
104
114
|
"release": "semantic-release"
|
|
105
115
|
},
|
|
106
116
|
"devDependencies": {
|
|
117
|
+
"@better-fetch/fetch": "1.1.19-beta.1",
|
|
107
118
|
"@eslint/js": "^9.30.1",
|
|
108
119
|
"@semantic-release/changelog": "^6.0.3",
|
|
109
120
|
"@semantic-release/git": "^10.0.1",
|
|
@@ -113,11 +124,13 @@
|
|
|
113
124
|
"@storybook/addon-interactions": "^8.6.14",
|
|
114
125
|
"@storybook/addon-links": "8.6.14",
|
|
115
126
|
"@storybook/addon-onboarding": "8.6.14",
|
|
127
|
+
"@storybook/preview-api": "^8.6.14",
|
|
116
128
|
"@storybook/react": "8.6.14",
|
|
117
129
|
"@storybook/react-vite": "8.6.14",
|
|
118
130
|
"@storybook/test": "^8.6.14",
|
|
119
131
|
"@storybook/types": "8.6.14",
|
|
120
132
|
"@tailwindcss/typography": "^0.5.16",
|
|
133
|
+
"@tanstack/react-query": "^5.89.0",
|
|
121
134
|
"@testing-library/dom": "^10.4.0",
|
|
122
135
|
"@testing-library/jest-dom": "^6.6.3",
|
|
123
136
|
"@testing-library/react": "^16.3.0",
|
|
@@ -182,9 +195,12 @@
|
|
|
182
195
|
"util": "^0.12.5",
|
|
183
196
|
"vite": "^7.0.4",
|
|
184
197
|
"vite-plugin-dts": "^4.5.4",
|
|
185
|
-
"vitest": "^3.2.4"
|
|
198
|
+
"vitest": "^3.2.4",
|
|
199
|
+
"zod": "^4.1.8"
|
|
186
200
|
},
|
|
187
201
|
"peerDependencies": {
|
|
202
|
+
"@better-fetch/fetch": "1.1.19-beta.1",
|
|
203
|
+
"@tanstack/react-query": "^5.89.0",
|
|
188
204
|
"@testing-library/react": "^16.0.0",
|
|
189
205
|
"@testing-library/user-event": "^14.6.1",
|
|
190
206
|
"@turf/turf": "^7.2.0",
|
|
@@ -200,7 +216,8 @@
|
|
|
200
216
|
"react": "^19.1.0",
|
|
201
217
|
"react-dom": "^19.1.0",
|
|
202
218
|
"react-error-boundary": "^6.0.0",
|
|
203
|
-
"react-icons": "^5.5.0"
|
|
219
|
+
"react-icons": "^5.5.0",
|
|
220
|
+
"zod": "^4.1.8"
|
|
204
221
|
},
|
|
205
222
|
"peerDependenciesMeta": {
|
|
206
223
|
"@turf/turf": {
|
|
@@ -3,7 +3,7 @@ import type { Meta, StoryObj } from '@storybook/react';
|
|
|
3
3
|
|
|
4
4
|
import { ErrorFallback } from './ErrorFallback';
|
|
5
5
|
import { ApiError } from '../../errors/ApiError';
|
|
6
|
-
import {
|
|
6
|
+
import { HttpStatus } from '../../http';
|
|
7
7
|
|
|
8
8
|
const meta = {
|
|
9
9
|
title: 'Components/ErrorFallback',
|
|
@@ -42,7 +42,7 @@ export const Default: Story = {
|
|
|
42
42
|
|
|
43
43
|
export const WithApiError: Story = {
|
|
44
44
|
args: {
|
|
45
|
-
error: new ApiError('Request failed',
|
|
45
|
+
error: new ApiError('Request failed', HttpStatus.InternalServerError, 'Extra details'),
|
|
46
46
|
resetErrorBoundary: mockReset,
|
|
47
47
|
},
|
|
48
48
|
parameters: {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { ErrorFallback } from './ErrorFallback';
|
|
2
2
|
import { ApiError } from '../../errors';
|
|
3
|
+
import { HttpStatus } from '../../http';
|
|
3
4
|
import { render, screen, userEvent } from '../../test/renderers';
|
|
4
|
-
import { Http } from '../../utils/http';
|
|
5
5
|
import { identityFn } from '../../utils/utils';
|
|
6
6
|
|
|
7
7
|
describe('ErrorFallback', () => {
|
|
@@ -23,7 +23,7 @@ describe('ErrorFallback', () => {
|
|
|
23
23
|
});
|
|
24
24
|
|
|
25
25
|
it('renders message and cause for ApiError instance', () => {
|
|
26
|
-
const apiError = new ApiError('Fake Reason',
|
|
26
|
+
const apiError = new ApiError('Fake Reason', HttpStatus.InternalServerError);
|
|
27
27
|
|
|
28
28
|
render(<ErrorFallback error={apiError} resetErrorBoundary={identityFn} />);
|
|
29
29
|
|
|
@@ -6,13 +6,15 @@ import { ApiError } from '../../errors/ApiError';
|
|
|
6
6
|
|
|
7
7
|
export const ErrorFallback = ({ resetErrorBoundary, error }: FallbackProps) => {
|
|
8
8
|
let message;
|
|
9
|
-
let
|
|
9
|
+
let details = undefined;
|
|
10
|
+
let digest = undefined;
|
|
10
11
|
|
|
11
12
|
if (error instanceof ApiError) {
|
|
12
13
|
message = error.message;
|
|
13
|
-
|
|
14
|
+
details = error.details;
|
|
14
15
|
} else if (error instanceof Error) {
|
|
15
|
-
|
|
16
|
+
message = error.message;
|
|
17
|
+
details = error.cause?.toString();
|
|
16
18
|
} else {
|
|
17
19
|
message = 'Unknown';
|
|
18
20
|
}
|
|
@@ -20,10 +22,24 @@ export const ErrorFallback = ({ resetErrorBoundary, error }: FallbackProps) => {
|
|
|
20
22
|
return (
|
|
21
23
|
<div>
|
|
22
24
|
<ErrorText>
|
|
23
|
-
|
|
24
|
-
|
|
25
|
+
There was an error. <br />
|
|
26
|
+
Reason: {message}
|
|
25
27
|
</ErrorText>
|
|
28
|
+
{details ? (
|
|
29
|
+
<span>
|
|
30
|
+
<br />
|
|
31
|
+
|
|
32
|
+
<details>
|
|
33
|
+
<summary>Details</summary>
|
|
26
34
|
|
|
35
|
+
<pre>{details}</pre>
|
|
36
|
+
</details>
|
|
37
|
+
</span>
|
|
38
|
+
) : (
|
|
39
|
+
<></>
|
|
40
|
+
)}
|
|
41
|
+
<br />
|
|
42
|
+
Digest: {digest ?? 'No digest provided'}
|
|
27
43
|
<Button onClick={resetErrorBoundary}>Try Again</Button>
|
|
28
44
|
</div>
|
|
29
45
|
);
|
|
@@ -36,7 +36,8 @@ export const Modal = ({ isOpen, onClose, children }: ModalProps) => {
|
|
|
36
36
|
// dialog elements do have a key handler as you can close them with `Escape`, so this can be ignored
|
|
37
37
|
// eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-noninteractive-element-interactions
|
|
38
38
|
<dialog
|
|
39
|
-
className="fixed inset-0 flex items-center justify-center w-full h-full m-0 bg-transparent
|
|
39
|
+
className="fixed inset-0 flex items-center justify-center w-full h-full m-0 bg-transparent
|
|
40
|
+
backdrop:bg-black/50"
|
|
40
41
|
ref={modalRef}
|
|
41
42
|
onCancel={(event) => {
|
|
42
43
|
event.preventDefault();
|
|
@@ -22,7 +22,8 @@ export const SkipLink = ({ mainContentId = 'main-content' }: SkipLinkProps) => {
|
|
|
22
22
|
return (
|
|
23
23
|
<nav aria-label="Skip navigation">
|
|
24
24
|
<Link
|
|
25
|
-
className="absolute w-full p-3 text-black bg-focus focus:relative focus:top-0 -top-full
|
|
25
|
+
className="absolute w-full p-3 text-black bg-focus focus:relative focus:top-0 -top-full
|
|
26
|
+
visited:text-black hover:text-black skip-link"
|
|
26
27
|
href={`#${mainContentId}`}
|
|
27
28
|
onClick={handleActivate}
|
|
28
29
|
onKeyDown={(event) => {
|
package/src/errors/ApiError.ts
CHANGED
|
@@ -1,33 +1,95 @@
|
|
|
1
1
|
/* eslint-disable no-restricted-syntax */
|
|
2
2
|
|
|
3
|
-
import
|
|
3
|
+
import z from 'zod/v4';
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
import { HttpStatus, HttpStatusText } from '../http';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Schema defining the JSON shape of error responses returned by a server. (The proxy is where
|
|
9
|
+
* the error is used the most, though it isn't unique to the proxy and may be used elsewhere).
|
|
10
|
+
*
|
|
11
|
+
* Note: This schema intentionally excludes the HTTP `status` field,
|
|
12
|
+
* as that value is carried in the HTTP response itself (via the status code)
|
|
13
|
+
* and in the in-memory {@link ApiError} class for internal use.
|
|
14
|
+
*/
|
|
15
|
+
export const ApiErrorSchema = z.object({
|
|
16
|
+
message: z.string(),
|
|
17
|
+
details: z.string().nullable(),
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
export type ApiErrorSchemaOutput = z.output<typeof ApiErrorSchema>;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Represents an error that can be returned by an API or proxy endpoint.
|
|
24
|
+
*
|
|
25
|
+
* Use this class to consistently capture errors along with an HTTP status
|
|
26
|
+
* code and optional detailed message. This allows:
|
|
27
|
+
* - Returning structured errors from API handlers or internal proxies.
|
|
28
|
+
* - Setting the status code for the response when an error has occurred.
|
|
29
|
+
* - Including additional context in `details` for debugging.
|
|
30
|
+
*
|
|
31
|
+
* Note:
|
|
32
|
+
* - Although it contains the same members, this class is separate from {@link ApiErrorSchema}.
|
|
33
|
+
* The schema defines the serialized JSON payload returned to clients, while this class
|
|
34
|
+
* *also* includes meta information of the response, such as the HTTP status code.
|
|
35
|
+
* The status code is typically only used internally inside the proxy (to forward status codes),
|
|
36
|
+
* but it could be accessed if a request 1 made with `throw` set to `false.`
|
|
37
|
+
*/
|
|
38
|
+
export class ApiError extends Error implements z.output<typeof ApiErrorSchema> {
|
|
39
|
+
public readonly details: string | null;
|
|
40
|
+
public readonly digest: string;
|
|
6
41
|
public readonly status: number;
|
|
7
|
-
|
|
8
|
-
|
|
42
|
+
// `true` if this class was reconstructed on the client from an response from the proxy
|
|
43
|
+
private readonly rehydrated: boolean;
|
|
9
44
|
|
|
10
|
-
constructor(
|
|
45
|
+
constructor(
|
|
46
|
+
message: string,
|
|
47
|
+
status: number,
|
|
48
|
+
details?: string | null,
|
|
49
|
+
options?: { rehydrated?: boolean },
|
|
50
|
+
) {
|
|
11
51
|
super(message);
|
|
52
|
+
|
|
12
53
|
this.name = 'ApiError';
|
|
13
54
|
this.status = status;
|
|
14
|
-
this.
|
|
15
|
-
this.
|
|
55
|
+
this.details = details ?? null;
|
|
56
|
+
this.digest = crypto.randomUUID();
|
|
57
|
+
this.rehydrated = options?.rehydrated ?? false;
|
|
16
58
|
|
|
17
59
|
// Maintains proper stack trace for where our error was thrown (only available on V8)
|
|
18
60
|
if (Error.captureStackTrace) {
|
|
19
61
|
Error.captureStackTrace(this, ApiError);
|
|
20
62
|
}
|
|
63
|
+
|
|
64
|
+
this.logError();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
private logError() {
|
|
68
|
+
const trimmedStack = this.stack
|
|
69
|
+
?.split('\n')
|
|
70
|
+
// Filter out noisy frames but keep traces from toolkit for debugging
|
|
71
|
+
.filter((line) => !line.includes('node_modules') || line.includes('next-toolkit'))
|
|
72
|
+
.join('\n');
|
|
73
|
+
|
|
74
|
+
const prefix =
|
|
75
|
+
!this.rehydrated && typeof window !== 'undefined' ? 'Client API Error' : 'Server API Error';
|
|
76
|
+
|
|
77
|
+
console.error(
|
|
78
|
+
// eslint-disable-next-line sonarjs/no-nested-template-literals
|
|
79
|
+
`[${prefix}]: ${this.message}${this.details ? ` (${this.details})` : ''}. Digest: ${
|
|
80
|
+
this.digest
|
|
81
|
+
}\n${!this.rehydrated ? (trimmedStack ?? 'aa') : 'bb'}`,
|
|
82
|
+
);
|
|
21
83
|
}
|
|
22
84
|
|
|
23
85
|
// Helper method to check if it's a client error (4xx)
|
|
24
86
|
get isClientError(): boolean {
|
|
25
|
-
return this.status >=
|
|
87
|
+
return this.status >= HttpStatus.BadRequest && this.status < HttpStatus.InternalServerError;
|
|
26
88
|
}
|
|
27
89
|
|
|
28
90
|
// Helper method to check if it's a server error (5xx)
|
|
29
91
|
get isServerError(): boolean {
|
|
30
|
-
return this.status >=
|
|
92
|
+
return this.status >= HttpStatus.InternalServerError;
|
|
31
93
|
}
|
|
32
94
|
|
|
33
95
|
// Convert to a plain object for JSON serialization
|
|
@@ -36,36 +98,48 @@ export class ApiError extends Error {
|
|
|
36
98
|
name: this.name,
|
|
37
99
|
message: this.message,
|
|
38
100
|
status: this.status,
|
|
39
|
-
code: this.code,
|
|
40
101
|
details: this.details,
|
|
41
102
|
};
|
|
42
103
|
}
|
|
43
104
|
|
|
44
105
|
// Static factory methods for common error types
|
|
45
|
-
static badRequest(
|
|
46
|
-
return new ApiError(
|
|
106
|
+
static badRequest(details?: string): ApiError {
|
|
107
|
+
return new ApiError(HttpStatusText.BadRequest, HttpStatus.BadRequest, details);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
static notFound(details?: string): ApiError {
|
|
111
|
+
return new ApiError(HttpStatusText.NotFound, HttpStatus.NotFound, details);
|
|
47
112
|
}
|
|
48
113
|
|
|
49
|
-
static
|
|
50
|
-
return new ApiError(
|
|
114
|
+
static unauthorized(details?: string): ApiError {
|
|
115
|
+
return new ApiError(HttpStatusText.Unauthorized, HttpStatus.Unauthorized, details);
|
|
51
116
|
}
|
|
52
117
|
|
|
53
|
-
static
|
|
54
|
-
return new ApiError(
|
|
118
|
+
static notAllowed(details?: string): ApiError {
|
|
119
|
+
return new ApiError(HttpStatusText.NotAllowed, HttpStatus.NotAllowed, details);
|
|
55
120
|
}
|
|
56
121
|
|
|
57
|
-
static
|
|
58
|
-
return new ApiError(
|
|
122
|
+
static notImplemented(details?: string): ApiError {
|
|
123
|
+
return new ApiError(HttpStatusText.NotImplemented, HttpStatus.NotImplemented, details);
|
|
59
124
|
}
|
|
60
125
|
|
|
61
|
-
static
|
|
62
|
-
return new ApiError(
|
|
126
|
+
static forbidden(details?: string): ApiError {
|
|
127
|
+
return new ApiError(HttpStatusText.Forbidden, HttpStatus.Forbidden, details);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
static unprocessableContent(details?: string): ApiError {
|
|
131
|
+
return new ApiError(
|
|
132
|
+
HttpStatusText.UnprocessableContent,
|
|
133
|
+
HttpStatus.UnprocessableContent,
|
|
134
|
+
details,
|
|
135
|
+
);
|
|
63
136
|
}
|
|
64
137
|
|
|
65
|
-
static
|
|
138
|
+
static internalServerError(details?: string): ApiError {
|
|
66
139
|
return new ApiError(
|
|
67
|
-
|
|
68
|
-
|
|
140
|
+
HttpStatusText.InternalServerError,
|
|
141
|
+
HttpStatus.InternalServerError,
|
|
142
|
+
details,
|
|
69
143
|
);
|
|
70
144
|
}
|
|
71
145
|
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Use the {@link MimeTypes} type for a union type containing these mime types.
|
|
3
|
+
*/
|
|
4
|
+
export const MimeType = {
|
|
5
|
+
Json: 'application/json',
|
|
6
|
+
JsonLd: 'application/ld+json',
|
|
7
|
+
GeoJson: 'application/geo+json',
|
|
8
|
+
Png: 'image/png',
|
|
9
|
+
XJsonLines: 'application/x-jsonlines',
|
|
10
|
+
Csv: 'text/csv',
|
|
11
|
+
Form: 'application/x-www-form-urlencoded',
|
|
12
|
+
Text: 'text/plain',
|
|
13
|
+
} as const;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Use the {@link MimeType} object for named constants of each mime type.
|
|
17
|
+
*/
|
|
18
|
+
export type MimeTypes = (typeof MimeType)[keyof typeof MimeType];
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Returns `true` if the mime type is a non-streamed JSON mime type (I.E. not JSON X-Lines).
|
|
22
|
+
*/
|
|
23
|
+
export const isJsonMimeType = (mime: MimeTypes | string | null): boolean => {
|
|
24
|
+
return ([MimeType.Json, MimeType.GeoJson, MimeType.JsonLd] as string[]).includes(mime ?? '');
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Returns `true` if the mime type is a plain-text mime type.
|
|
29
|
+
*/
|
|
30
|
+
export const isTextMimeType = (mime: MimeTypes | string | null): boolean => {
|
|
31
|
+
return ([MimeType.Text, MimeType.Csv] as string[]).includes(mime ?? '');
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Use the {@link HttpStatuses} type for a union type containing these status codes.
|
|
36
|
+
*
|
|
37
|
+
* Use the {@link HttpStatusText} object for the status text represented by each code.
|
|
38
|
+
*/
|
|
39
|
+
export const HttpStatus = {
|
|
40
|
+
Ok: 200,
|
|
41
|
+
Created: 201,
|
|
42
|
+
NoContent: 204,
|
|
43
|
+
BadRequest: 400,
|
|
44
|
+
Unauthorized: 401,
|
|
45
|
+
Forbidden: 403,
|
|
46
|
+
NotFound: 404,
|
|
47
|
+
NotAllowed: 405,
|
|
48
|
+
UnprocessableContent: 422,
|
|
49
|
+
InternalServerError: 500,
|
|
50
|
+
NotImplemented: 501,
|
|
51
|
+
} as const;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Use the {@link HttpStatus} object for named constants of each status code.
|
|
55
|
+
*/
|
|
56
|
+
export type HttpStatuses = (typeof HttpStatus)[keyof typeof HttpStatus];
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Status texts for each status code.
|
|
60
|
+
*/
|
|
61
|
+
export const HttpStatusText = {
|
|
62
|
+
Ok: 'OK',
|
|
63
|
+
Created: 'Created',
|
|
64
|
+
NoContent: 'No Content',
|
|
65
|
+
BadRequest: 'Bad Request',
|
|
66
|
+
Unauthorized: 'Unauthorized',
|
|
67
|
+
Forbidden: 'Forbidden',
|
|
68
|
+
NotFound: 'Not Found',
|
|
69
|
+
NotAllowed: 'Method Not Allowed',
|
|
70
|
+
UnprocessableContent: 'Unprocessable Content',
|
|
71
|
+
InternalServerError: 'Internal Server Error',
|
|
72
|
+
NotImplemented: 'Not Implemented',
|
|
73
|
+
} as const;
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Use the {@link HttpMethods} type for a union type containing these method verbs.
|
|
77
|
+
*/
|
|
78
|
+
export const HttpMethod = {
|
|
79
|
+
Get: 'get',
|
|
80
|
+
Post: 'post',
|
|
81
|
+
Put: 'put',
|
|
82
|
+
Patch: 'patch',
|
|
83
|
+
Delete: 'delete',
|
|
84
|
+
} as const;
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Use the {@link HttpMethod} object for named constants of each method verb.
|
|
88
|
+
*/
|
|
89
|
+
export type HttpMethods = (typeof HttpMethod)[keyof typeof HttpMethod];
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Use the {@link Headers} type for a union type containing these headers.
|
|
93
|
+
*/
|
|
94
|
+
export const Header = {
|
|
95
|
+
ContentType: 'Content-Type',
|
|
96
|
+
ContentDisposition: 'Content-Disposition',
|
|
97
|
+
Accept: 'Accept',
|
|
98
|
+
AcceptCrs: 'Accept-Crs',
|
|
99
|
+
CsvHeader: 'CSV-Header',
|
|
100
|
+
XTotalItems: 'X-Total-Items',
|
|
101
|
+
XRequestId: 'X-Request-ID',
|
|
102
|
+
} as const;
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Use the {@link Header} object for named constants of each header.
|
|
106
|
+
*/
|
|
107
|
+
export type Headers = (typeof Header)[keyof typeof Header];
|
|
108
|
+
|
|
109
|
+
// Special types used for downloading files via a `POST` request
|
|
110
|
+
export const SPECIAL_FORM_DATA_TYPE = '_type';
|
|
111
|
+
export const SPECIAL_FORM_DOWNLOAD_POST = 'file-download';
|