@slates/provider 1.0.0-rc.1 → 1.0.0-rc.11
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/dist/index.cjs +1882 -0
- package/dist/index.d.cts +632 -0
- package/dist/index.d.ts +632 -0
- package/dist/index.module.js +1810 -0
- package/package.json +14 -6
- package/src/action/action.ts +18 -3
- package/src/action/attachment.ts +41 -0
- package/src/action/builder.ts +21 -4
- package/src/action/index.ts +2 -2
- package/src/action/scopes.test.ts +77 -0
- package/src/action/scopes.ts +101 -0
- package/src/action/tool.ts +8 -4
- package/src/action/trigger.ts +11 -11
- package/src/auth/auth.ts +3 -3
- package/src/auth/types.ts +21 -6
- package/src/axios/index.ts +59 -11
- package/src/axios/trace.test.ts +378 -0
- package/src/axios/trace.ts +776 -0
- package/src/config/config.ts +3 -3
- package/src/context/context.ts +29 -2
- package/src/context/hook.ts +9 -4
- package/src/context/index.ts +1 -0
- package/src/error/axios.ts +220 -0
- package/src/error/base.test.ts +178 -0
- package/src/error/base.ts +108 -2
- package/src/error/declaration.ts +7 -2
- package/src/error/defaults.ts +50 -0
- package/src/error/index.ts +3 -0
- package/src/error/service.ts +160 -0
- package/src/error/types.ts +75 -0
- package/src/error/utils.ts +157 -0
- package/src/index.ts +1 -0
- package/src/logger/logger.test.ts +57 -0
- package/src/logger/logger.ts +72 -11
- package/src/specification/slate.ts +2 -2
- package/src/specification/specification.ts +2 -2
- package/tsconfig.json +0 -8
package/src/config/config.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import z from 'zod';
|
|
1
|
+
import type z from 'zod';
|
|
2
2
|
|
|
3
3
|
export class SlateConfig<ConfigType extends {}> {
|
|
4
4
|
#configSchema: z.ZodType<ConfigType>;
|
|
@@ -6,7 +6,7 @@ export class SlateConfig<ConfigType extends {}> {
|
|
|
6
6
|
| ((params: {
|
|
7
7
|
previousConfig: ConfigType | null;
|
|
8
8
|
newConfig: ConfigType;
|
|
9
|
-
}) => { config: ConfigType | undefined } |
|
|
9
|
+
}) => { config: ConfigType | undefined } | undefined)
|
|
10
10
|
| null = null;
|
|
11
11
|
#getDefaultConfig: (() => ConfigType) | null = null;
|
|
12
12
|
|
|
@@ -29,7 +29,7 @@ export class SlateConfig<ConfigType extends {}> {
|
|
|
29
29
|
handler: (params: {
|
|
30
30
|
previousConfig: ConfigType | null;
|
|
31
31
|
newConfig: ConfigType;
|
|
32
|
-
}) => { config: ConfigType | undefined } |
|
|
32
|
+
}) => { config: ConfigType | undefined } | undefined
|
|
33
33
|
): SlateConfig<ConfigType> {
|
|
34
34
|
this.#configChanged = handler;
|
|
35
35
|
return this;
|
package/src/context/context.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import type { SlateHttpTrace } from '../axios/trace';
|
|
2
|
+
import type { SlateLogger, SlateLogMessageInput } from '../logger';
|
|
3
|
+
import type { SlateSpecification } from '../specification/specification';
|
|
3
4
|
|
|
4
5
|
export class SlateContext<ConfigType extends {}, AuthType extends {}, InputType extends {}> {
|
|
5
6
|
#config: ConfigType;
|
|
6
7
|
#input: InputType;
|
|
7
8
|
#auth: AuthType;
|
|
9
|
+
#httpTraces: SlateHttpTrace[] = [];
|
|
8
10
|
|
|
9
11
|
constructor(
|
|
10
12
|
config: ConfigType,
|
|
@@ -50,6 +52,31 @@ export class SlateContext<ConfigType extends {}, AuthType extends {}, InputType
|
|
|
50
52
|
return Object.freeze(this.#auth);
|
|
51
53
|
}
|
|
52
54
|
|
|
55
|
+
recordHttpTrace(trace: SlateHttpTrace) {
|
|
56
|
+
this.#httpTraces.push(trace);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
getHttpTraces() {
|
|
60
|
+
return this.#httpTraces.map(trace => ({
|
|
61
|
+
...trace,
|
|
62
|
+
request: {
|
|
63
|
+
...trace.request,
|
|
64
|
+
...(trace.request.headers ? { headers: { ...trace.request.headers } } : {}),
|
|
65
|
+
...(trace.request.body ? { body: { ...trace.request.body } } : {})
|
|
66
|
+
},
|
|
67
|
+
...(trace.response
|
|
68
|
+
? {
|
|
69
|
+
response: {
|
|
70
|
+
...trace.response,
|
|
71
|
+
...(trace.response.headers ? { headers: { ...trace.response.headers } } : {}),
|
|
72
|
+
...(trace.response.body ? { body: { ...trace.response.body } } : {})
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
: {}),
|
|
76
|
+
...(trace.error ? { error: { ...trace.error } } : {})
|
|
77
|
+
}));
|
|
78
|
+
}
|
|
79
|
+
|
|
53
80
|
info(message: SlateLogMessageInput) {
|
|
54
81
|
this.logger.info(message);
|
|
55
82
|
}
|
package/src/context/hook.ts
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
import { AsyncLocalStorage } from 'async_hooks';
|
|
2
|
-
import { SlateContext } from './context';
|
|
2
|
+
import type { SlateContext } from './context';
|
|
3
3
|
|
|
4
4
|
let asyncLocalStorage = new AsyncLocalStorage<SlateContext<any, any, any>>();
|
|
5
5
|
|
|
6
|
-
export let runWithContext = <
|
|
6
|
+
export let runWithContext = <
|
|
7
|
+
ConfigType extends {},
|
|
8
|
+
AuthType extends {},
|
|
9
|
+
InputType extends {},
|
|
10
|
+
RV
|
|
11
|
+
>(
|
|
7
12
|
context: SlateContext<ConfigType, AuthType, InputType>,
|
|
8
|
-
fn: () => Promise<
|
|
9
|
-
) => {
|
|
13
|
+
fn: () => Promise<RV>
|
|
14
|
+
): Promise<RV> => {
|
|
10
15
|
return asyncLocalStorage.run(context, fn);
|
|
11
16
|
};
|
|
12
17
|
|
package/src/context/index.ts
CHANGED
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import type { AxiosError } from 'axios';
|
|
2
|
+
import { isAxiosError } from 'axios';
|
|
3
|
+
import { getCurrentContext } from '../context/hook';
|
|
4
|
+
import type { SlateAxiosErrorOptions, SlateErrorResponse } from './types';
|
|
5
|
+
import {
|
|
6
|
+
getDefaultMessage,
|
|
7
|
+
isRecord,
|
|
8
|
+
mergeRecords,
|
|
9
|
+
normalizeKind,
|
|
10
|
+
normalizeRetryable,
|
|
11
|
+
normalizeSlateErrorInput,
|
|
12
|
+
sanitizeForBaggage
|
|
13
|
+
} from './utils';
|
|
14
|
+
|
|
15
|
+
let getResponseField = (value: unknown, key: string): unknown => {
|
|
16
|
+
if (!isRecord(value)) return undefined;
|
|
17
|
+
return value[key];
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
let getNestedMessage = (value: unknown): string | undefined => {
|
|
21
|
+
if (typeof value === 'string') return value;
|
|
22
|
+
if (!isRecord(value)) return undefined;
|
|
23
|
+
|
|
24
|
+
let message = getResponseField(value, 'message');
|
|
25
|
+
if (typeof message === 'string' && message.trim()) return message;
|
|
26
|
+
|
|
27
|
+
let description = getResponseField(value, 'description');
|
|
28
|
+
if (typeof description === 'string' && description.trim()) return description;
|
|
29
|
+
|
|
30
|
+
let error = getResponseField(value, 'error');
|
|
31
|
+
if (typeof error === 'string' && error.trim()) return error;
|
|
32
|
+
if (isRecord(error)) {
|
|
33
|
+
let nested = getNestedMessage(error);
|
|
34
|
+
if (nested) return nested;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
let errorDescription = getResponseField(value, 'error_description');
|
|
38
|
+
if (typeof errorDescription === 'string' && errorDescription.trim()) {
|
|
39
|
+
return errorDescription;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return undefined;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
let getNestedCode = (value: unknown): string | undefined => {
|
|
46
|
+
if (!isRecord(value)) return undefined;
|
|
47
|
+
|
|
48
|
+
let directCode = getResponseField(value, 'code');
|
|
49
|
+
if (typeof directCode === 'string' && directCode.trim()) return directCode;
|
|
50
|
+
|
|
51
|
+
let errorCode = getResponseField(value, 'error_code');
|
|
52
|
+
if (typeof errorCode === 'string' && errorCode.trim()) return errorCode;
|
|
53
|
+
|
|
54
|
+
let type = getResponseField(value, 'type');
|
|
55
|
+
if (typeof type === 'string' && type.trim()) return type;
|
|
56
|
+
|
|
57
|
+
let error = getResponseField(value, 'error');
|
|
58
|
+
if (typeof error === 'string' && error.trim()) return error;
|
|
59
|
+
if (isRecord(error)) return getNestedCode(error);
|
|
60
|
+
|
|
61
|
+
return undefined;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
let getNestedType = (value: unknown): string | undefined => {
|
|
65
|
+
if (!isRecord(value)) return undefined;
|
|
66
|
+
|
|
67
|
+
let type = getResponseField(value, 'type');
|
|
68
|
+
if (typeof type === 'string' && type.trim()) return type;
|
|
69
|
+
|
|
70
|
+
let error = getResponseField(value, 'error');
|
|
71
|
+
if (isRecord(error)) {
|
|
72
|
+
let nestedType = getNestedType(error);
|
|
73
|
+
if (nestedType) return nestedType;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return undefined;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
let getHeaderValue = (headers: unknown, key: string): string | undefined => {
|
|
80
|
+
if (!headers) return undefined;
|
|
81
|
+
|
|
82
|
+
if (typeof (headers as { get?: unknown }).get === 'function') {
|
|
83
|
+
let value = (headers as { get(name: string): unknown }).get(key);
|
|
84
|
+
if (typeof value === 'string' && value.trim()) return value;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (isRecord(headers)) {
|
|
88
|
+
for (let [headerKey, headerValue] of Object.entries(headers)) {
|
|
89
|
+
if (headerKey.toLowerCase() !== key.toLowerCase()) continue;
|
|
90
|
+
if (typeof headerValue === 'string' && headerValue.trim()) return headerValue;
|
|
91
|
+
if (Array.isArray(headerValue)) {
|
|
92
|
+
let first = headerValue.find(item => typeof item === 'string' && item.trim());
|
|
93
|
+
if (typeof first === 'string') return first;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return undefined;
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
let buildRequestUrl = (error: AxiosError) => {
|
|
102
|
+
let baseURL = error.config?.baseURL;
|
|
103
|
+
let url = error.config?.url;
|
|
104
|
+
|
|
105
|
+
if (!url) return undefined;
|
|
106
|
+
if (!baseURL) return url;
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
return new URL(url, baseURL).toString();
|
|
110
|
+
} catch {
|
|
111
|
+
return url;
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
let getProviderDefaults = (): Partial<SlateErrorResponse> => {
|
|
116
|
+
try {
|
|
117
|
+
let ctx = getCurrentContext();
|
|
118
|
+
return {
|
|
119
|
+
provider: {
|
|
120
|
+
key: ctx.specification.key
|
|
121
|
+
},
|
|
122
|
+
...(ctx.getHttpTraces().length > 0 ? { requestTraces: ctx.getHttpTraces() } : {})
|
|
123
|
+
};
|
|
124
|
+
} catch {
|
|
125
|
+
return {};
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
let inferAxiosCode = (error: AxiosError, data: unknown) => {
|
|
130
|
+
let status = error.response?.status;
|
|
131
|
+
let axiosCode = error.code?.toUpperCase();
|
|
132
|
+
let remoteCode = getNestedCode(data)?.toLowerCase();
|
|
133
|
+
|
|
134
|
+
if (
|
|
135
|
+
axiosCode === 'ECONNABORTED' ||
|
|
136
|
+
axiosCode === 'ETIMEDOUT' ||
|
|
137
|
+
status === 408 ||
|
|
138
|
+
remoteCode?.includes('timeout')
|
|
139
|
+
) {
|
|
140
|
+
return 'upstream.timeout';
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (!error.response) {
|
|
144
|
+
return 'upstream.network_error';
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (status === 401) return 'auth.invalid';
|
|
148
|
+
if (status === 403) {
|
|
149
|
+
return remoteCode?.includes('rate') ? 'upstream.rate_limited' : 'permission.denied';
|
|
150
|
+
}
|
|
151
|
+
if (status === 404) return 'resource.not_found';
|
|
152
|
+
if (status === 409) return 'resource.conflict';
|
|
153
|
+
if (status === 422 || status === 400) return 'upstream.invalid_request';
|
|
154
|
+
if (status === 429) return 'upstream.rate_limited';
|
|
155
|
+
if (status === 502 || status === 503 || status === 504) return 'upstream.unavailable';
|
|
156
|
+
if (status && status >= 500) return 'upstream.error';
|
|
157
|
+
|
|
158
|
+
return 'upstream.error';
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
let inferAxiosMessage = (error: AxiosError, data: unknown, code: string) => {
|
|
162
|
+
return (
|
|
163
|
+
getNestedMessage(data) ??
|
|
164
|
+
error.response?.statusText ??
|
|
165
|
+
error.message ??
|
|
166
|
+
getDefaultMessage(code)
|
|
167
|
+
);
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
let inferAxiosRetryable = (error: AxiosError, code: string) => {
|
|
171
|
+
let headerValue = getHeaderValue(error.response?.headers, 'retry-after');
|
|
172
|
+
if (headerValue) return true;
|
|
173
|
+
return normalizeRetryable(code, undefined);
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
export let isAxiosErrorLike = (error: unknown): error is AxiosError => isAxiosError(error);
|
|
177
|
+
|
|
178
|
+
export let inferSlateErrorFromAxios = (
|
|
179
|
+
error: AxiosError,
|
|
180
|
+
options: SlateAxiosErrorOptions = {}
|
|
181
|
+
): SlateErrorResponse => {
|
|
182
|
+
let responseData = error.response
|
|
183
|
+
? (options.extractResponseData?.(error.response) ?? error.response.data)
|
|
184
|
+
: undefined;
|
|
185
|
+
|
|
186
|
+
let code = inferAxiosCode(error, responseData);
|
|
187
|
+
|
|
188
|
+
return normalizeSlateErrorInput({
|
|
189
|
+
...getProviderDefaults(),
|
|
190
|
+
...options.defaults,
|
|
191
|
+
code: options.defaults?.code ?? code,
|
|
192
|
+
message: options.defaults?.message ?? inferAxiosMessage(error, responseData, code),
|
|
193
|
+
kind: options.defaults?.kind ?? normalizeKind(options.defaults?.code ?? code),
|
|
194
|
+
retryable:
|
|
195
|
+
options.defaults?.retryable ??
|
|
196
|
+
inferAxiosRetryable(error, options.defaults?.code ?? code),
|
|
197
|
+
status: options.defaults?.status ?? error.response?.status,
|
|
198
|
+
provider: {
|
|
199
|
+
...getProviderDefaults().provider,
|
|
200
|
+
...(options.defaults?.provider ?? {})
|
|
201
|
+
},
|
|
202
|
+
upstream: {
|
|
203
|
+
...(options.defaults?.upstream ?? {}),
|
|
204
|
+
status: error.response?.status ?? options.defaults?.upstream?.status,
|
|
205
|
+
code: getNestedCode(responseData) ?? options.defaults?.upstream?.code,
|
|
206
|
+
type: getNestedType(responseData) ?? options.defaults?.upstream?.type,
|
|
207
|
+
requestId:
|
|
208
|
+
getHeaderValue(error.response?.headers, 'x-request-id') ??
|
|
209
|
+
getHeaderValue(error.response?.headers, 'request-id') ??
|
|
210
|
+
getHeaderValue(error.response?.headers, 'x-correlation-id') ??
|
|
211
|
+
options.defaults?.upstream?.requestId,
|
|
212
|
+
method: error.config?.method?.toUpperCase() ?? options.defaults?.upstream?.method,
|
|
213
|
+
url: buildRequestUrl(error) ?? options.defaults?.upstream?.url
|
|
214
|
+
},
|
|
215
|
+
baggage: mergeRecords(options.defaults?.baggage, {
|
|
216
|
+
axiosCode: error.code,
|
|
217
|
+
response: sanitizeForBaggage(responseData)
|
|
218
|
+
})
|
|
219
|
+
});
|
|
220
|
+
};
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import {
|
|
2
|
+
badRequestError,
|
|
3
|
+
conflictError,
|
|
4
|
+
forbiddenError,
|
|
5
|
+
goneError,
|
|
6
|
+
internalServerError,
|
|
7
|
+
invalidVersion,
|
|
8
|
+
notAcceptableError,
|
|
9
|
+
notFoundError,
|
|
10
|
+
notImplementedError,
|
|
11
|
+
paymentRequiredError,
|
|
12
|
+
preconditionFailedError,
|
|
13
|
+
timeoutError,
|
|
14
|
+
tooManyRequestsError,
|
|
15
|
+
unauthorizedError,
|
|
16
|
+
validationError
|
|
17
|
+
} from '@lowerdeck/error';
|
|
18
|
+
import { AxiosError, type AxiosResponse, type InternalAxiosRequestConfig } from 'axios';
|
|
19
|
+
import { describe, expect, it } from 'vitest';
|
|
20
|
+
import { SlateError } from './base';
|
|
21
|
+
|
|
22
|
+
let createConfig = (overrides: Partial<InternalAxiosRequestConfig> = {}) =>
|
|
23
|
+
({
|
|
24
|
+
url: '/issues',
|
|
25
|
+
method: 'get',
|
|
26
|
+
headers: {},
|
|
27
|
+
...overrides
|
|
28
|
+
}) as InternalAxiosRequestConfig;
|
|
29
|
+
|
|
30
|
+
let createResponse = (overrides: Partial<AxiosResponse> = {}) =>
|
|
31
|
+
({
|
|
32
|
+
status: 429,
|
|
33
|
+
statusText: 'Too Many Requests',
|
|
34
|
+
headers: {
|
|
35
|
+
'x-request-id': 'req_123'
|
|
36
|
+
},
|
|
37
|
+
config: createConfig(),
|
|
38
|
+
data: {
|
|
39
|
+
error: {
|
|
40
|
+
code: 'rate_limited',
|
|
41
|
+
message: 'GitHub API rate limit exceeded',
|
|
42
|
+
type: 'rate_limit'
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
...overrides
|
|
46
|
+
}) as AxiosResponse;
|
|
47
|
+
|
|
48
|
+
describe('SlateError', () => {
|
|
49
|
+
it('maps axios responses to structured slate errors', () => {
|
|
50
|
+
let axiosError = new AxiosError(
|
|
51
|
+
'Request failed with status code 429',
|
|
52
|
+
'ERR_BAD_REQUEST',
|
|
53
|
+
createConfig(),
|
|
54
|
+
undefined,
|
|
55
|
+
createResponse()
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
let error = SlateError.fromAxios(axiosError, {
|
|
59
|
+
defaults: {
|
|
60
|
+
provider: {
|
|
61
|
+
service: 'github',
|
|
62
|
+
operation: 'issues.list'
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
expect(error).toBeInstanceOf(SlateError);
|
|
68
|
+
expect(error.code).toBe('upstream.rate_limited');
|
|
69
|
+
expect(error.status).toBe(429);
|
|
70
|
+
expect(error.retryable).toBe(true);
|
|
71
|
+
expect(error.data.provider).toEqual({
|
|
72
|
+
service: 'github',
|
|
73
|
+
operation: 'issues.list'
|
|
74
|
+
});
|
|
75
|
+
expect(error.data.upstream).toMatchObject({
|
|
76
|
+
status: 429,
|
|
77
|
+
code: 'rate_limited',
|
|
78
|
+
type: 'rate_limit',
|
|
79
|
+
requestId: 'req_123',
|
|
80
|
+
method: 'GET',
|
|
81
|
+
url: '/issues'
|
|
82
|
+
});
|
|
83
|
+
expect(error.data.baggage?.response).toBeTruthy();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('maps transport failures without responses', () => {
|
|
87
|
+
let axiosError = new AxiosError('socket hang up', 'ECONNRESET', createConfig());
|
|
88
|
+
|
|
89
|
+
let error = SlateError.fromAxios(axiosError);
|
|
90
|
+
|
|
91
|
+
expect(error.code).toBe('upstream.network_error');
|
|
92
|
+
expect(error.kind).toBe('upstream');
|
|
93
|
+
expect(error.retryable).toBe(true);
|
|
94
|
+
expect(error.data.upstream).toMatchObject({
|
|
95
|
+
method: 'GET',
|
|
96
|
+
url: '/issues'
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('lets providers override inferred axios mappings', () => {
|
|
101
|
+
let axiosError = new AxiosError(
|
|
102
|
+
'Request failed with status code 404',
|
|
103
|
+
'ERR_BAD_REQUEST',
|
|
104
|
+
createConfig({ method: 'post', url: '/contacts' }),
|
|
105
|
+
undefined,
|
|
106
|
+
createResponse({
|
|
107
|
+
status: 404,
|
|
108
|
+
statusText: 'Not Found',
|
|
109
|
+
data: {
|
|
110
|
+
error: {
|
|
111
|
+
code: 'contact_missing',
|
|
112
|
+
message: 'Contact does not exist'
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
})
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
let error = SlateError.fromAxios(axiosError, {
|
|
119
|
+
mapAxiosError: (_, inferred) => ({
|
|
120
|
+
...inferred,
|
|
121
|
+
code: 'resource.not_found',
|
|
122
|
+
message: 'Salesforce contact was not found',
|
|
123
|
+
provider: {
|
|
124
|
+
...inferred.provider,
|
|
125
|
+
service: 'salesforce',
|
|
126
|
+
operation: 'contacts.create'
|
|
127
|
+
},
|
|
128
|
+
baggage: {
|
|
129
|
+
...inferred.baggage,
|
|
130
|
+
entity: 'contact'
|
|
131
|
+
}
|
|
132
|
+
})
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
expect(error.code).toBe('resource.not_found');
|
|
136
|
+
expect(error.message).toBe('Salesforce contact was not found');
|
|
137
|
+
expect(error.data.provider).toEqual({
|
|
138
|
+
service: 'salesforce',
|
|
139
|
+
operation: 'contacts.create'
|
|
140
|
+
});
|
|
141
|
+
expect(error.data.baggage?.entity).toBe('contact');
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('maps service errors to canonical slate errors', () => {
|
|
145
|
+
let serviceErrors = [
|
|
146
|
+
[
|
|
147
|
+
validationError({ entity: 'request', errors: [], message: 'Invalid request data' }),
|
|
148
|
+
'input.invalid'
|
|
149
|
+
],
|
|
150
|
+
[badRequestError({ message: 'Bad request' }), 'request.bad'],
|
|
151
|
+
[notFoundError('handler', 'missing'), 'resource.not_found'],
|
|
152
|
+
[unauthorizedError({ message: 'Please log in' }), 'auth.required'],
|
|
153
|
+
[forbiddenError({ message: 'Forbidden' }), 'permission.denied'],
|
|
154
|
+
[invalidVersion({ message: 'Invalid version' }), 'request.invalid_version'],
|
|
155
|
+
[conflictError({ message: 'Conflict' }), 'resource.conflict'],
|
|
156
|
+
[goneError({ message: 'Gone' }), 'resource.gone'],
|
|
157
|
+
[paymentRequiredError({ message: 'Payment required' }), 'payment.required'],
|
|
158
|
+
[
|
|
159
|
+
preconditionFailedError({ message: 'Precondition failed' }),
|
|
160
|
+
'request.precondition_failed'
|
|
161
|
+
],
|
|
162
|
+
[notAcceptableError({ message: 'Not acceptable' }), 'request.not_acceptable'],
|
|
163
|
+
[notImplementedError({ message: 'Not implemented' }), 'operation.not_implemented'],
|
|
164
|
+
[tooManyRequestsError({ message: 'Too many requests' }), 'request.rate_limited'],
|
|
165
|
+
[timeoutError({ message: 'Timed out' }), 'internal.timeout'],
|
|
166
|
+
[internalServerError({ message: 'Internal error' }), 'internal.unexpected']
|
|
167
|
+
] as const;
|
|
168
|
+
|
|
169
|
+
for (let [serviceError, code] of serviceErrors) {
|
|
170
|
+
let error = SlateError.fromServiceError(serviceError);
|
|
171
|
+
expect(error.code).toBe(code);
|
|
172
|
+
expect(error.data.baggage?.serviceError).toMatchObject({
|
|
173
|
+
code: serviceError.data.code,
|
|
174
|
+
status: serviceError.data.status
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
});
|
package/src/error/base.ts
CHANGED
|
@@ -1,10 +1,116 @@
|
|
|
1
|
+
import { inferSlateErrorFromAxios, isAxiosErrorLike } from './axios';
|
|
2
|
+
import { DEFAULT_CODE } from './defaults';
|
|
3
|
+
import { getServiceErrorData, mapServiceErrorToSlateErrorInput } from './service';
|
|
4
|
+
import type { SlateAxiosErrorOptions, SlateErrorInput, SlateErrorResponse } from './types';
|
|
5
|
+
import {
|
|
6
|
+
getDefaultMessage,
|
|
7
|
+
isSlateErrorResponse,
|
|
8
|
+
mergeRecords,
|
|
9
|
+
mergeSlateErrorData,
|
|
10
|
+
normalizeSlateErrorInput,
|
|
11
|
+
sanitizeForBaggage
|
|
12
|
+
} from './utils';
|
|
13
|
+
|
|
1
14
|
export class SlateError extends Error {
|
|
2
|
-
|
|
3
|
-
|
|
15
|
+
cause?: unknown;
|
|
16
|
+
data: SlateErrorResponse;
|
|
17
|
+
|
|
18
|
+
constructor(input: SlateErrorInput) {
|
|
19
|
+
let normalized = normalizeSlateErrorInput(input);
|
|
20
|
+
super(normalized.message);
|
|
4
21
|
this.name = 'SlateError';
|
|
22
|
+
this.data = normalized;
|
|
23
|
+
this.cause = input.cause;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
get code() {
|
|
27
|
+
return this.data.code;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
get kind() {
|
|
31
|
+
return this.data.kind;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
get status() {
|
|
35
|
+
return this.data.status;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
get retryable() {
|
|
39
|
+
return this.data.retryable;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
toResponse(): SlateErrorResponse {
|
|
43
|
+
return {
|
|
44
|
+
...this.data
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
toJSON(): SlateErrorResponse {
|
|
49
|
+
return this.toResponse();
|
|
5
50
|
}
|
|
6
51
|
|
|
7
52
|
static is(error: unknown): error is SlateError {
|
|
8
53
|
return error instanceof SlateError;
|
|
9
54
|
}
|
|
55
|
+
|
|
56
|
+
static fromUnknown(error: unknown, defaults: Partial<SlateErrorResponse> = {}): SlateError {
|
|
57
|
+
if (SlateError.is(error)) return error;
|
|
58
|
+
if (isSlateErrorResponse(error)) return new SlateError({ ...defaults, ...error });
|
|
59
|
+
if (getServiceErrorData(error)) return SlateError.fromServiceError(error, defaults);
|
|
60
|
+
if (isAxiosErrorLike(error)) return SlateError.fromAxios(error, { defaults });
|
|
61
|
+
|
|
62
|
+
if (error instanceof Error) {
|
|
63
|
+
return new SlateError({
|
|
64
|
+
code: defaults.code ?? DEFAULT_CODE,
|
|
65
|
+
message: error.message,
|
|
66
|
+
kind: defaults.kind,
|
|
67
|
+
retryable: defaults.retryable,
|
|
68
|
+
status: defaults.status,
|
|
69
|
+
issues: defaults.issues,
|
|
70
|
+
provider: defaults.provider,
|
|
71
|
+
upstream: defaults.upstream,
|
|
72
|
+
baggage: mergeRecords(defaults.baggage, {
|
|
73
|
+
originalName: error.name
|
|
74
|
+
}),
|
|
75
|
+
cause: error
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return new SlateError({
|
|
80
|
+
code: defaults.code ?? DEFAULT_CODE,
|
|
81
|
+
message: getDefaultMessage(defaults.code ?? DEFAULT_CODE, defaults.message),
|
|
82
|
+
kind: defaults.kind,
|
|
83
|
+
retryable: defaults.retryable,
|
|
84
|
+
status: defaults.status,
|
|
85
|
+
issues: defaults.issues,
|
|
86
|
+
provider: defaults.provider,
|
|
87
|
+
upstream: defaults.upstream,
|
|
88
|
+
baggage: mergeRecords(defaults.baggage, {
|
|
89
|
+
originalValue: sanitizeForBaggage(error)
|
|
90
|
+
})
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
static fromServiceError(
|
|
95
|
+
error: unknown,
|
|
96
|
+
defaults: Partial<SlateErrorInput> = {}
|
|
97
|
+
): SlateError {
|
|
98
|
+
return new SlateError(mapServiceErrorToSlateErrorInput(error as any, defaults));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
static fromAxios(error: unknown, options: SlateAxiosErrorOptions = {}): SlateError {
|
|
102
|
+
if (SlateError.is(error)) return error;
|
|
103
|
+
if (getServiceErrorData(error))
|
|
104
|
+
return SlateError.fromServiceError(error, options.defaults);
|
|
105
|
+
if (!isAxiosErrorLike(error)) return SlateError.fromUnknown(error, options.defaults);
|
|
106
|
+
|
|
107
|
+
let inferred = inferSlateErrorFromAxios(error, options);
|
|
108
|
+
|
|
109
|
+
let mapped = options.mapAxiosError?.(error, inferred);
|
|
110
|
+
if (SlateError.is(mapped)) return mapped;
|
|
111
|
+
|
|
112
|
+
return new SlateError(mergeSlateErrorData(inferred, mapped ?? undefined));
|
|
113
|
+
}
|
|
10
114
|
}
|
|
115
|
+
|
|
116
|
+
export let createSlateError = (input: SlateErrorInput) => new SlateError(input);
|
package/src/error/declaration.ts
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
import { SlateError } from './base';
|
|
2
2
|
|
|
3
3
|
export class SlateDeclarationError extends SlateError {
|
|
4
|
-
constructor(message: string) {
|
|
5
|
-
super(
|
|
4
|
+
constructor(message: string, input: Partial<SlateError['data']> = {}) {
|
|
5
|
+
super({
|
|
6
|
+
code: 'declaration.invalid_definition',
|
|
7
|
+
kind: 'declaration',
|
|
8
|
+
message,
|
|
9
|
+
...input
|
|
10
|
+
});
|
|
6
11
|
this.name = 'SlateError.DeclarationError';
|
|
7
12
|
}
|
|
8
13
|
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export let DEFAULT_CODE = 'internal.unexpected';
|
|
2
|
+
|
|
3
|
+
export let DEFAULT_MESSAGES: Record<string, string> = {
|
|
4
|
+
'declaration.invalid_definition': 'The slate definition is invalid.',
|
|
5
|
+
'context.missing': 'No Slate context is available.',
|
|
6
|
+
'input.invalid': 'The provided input is invalid.',
|
|
7
|
+
'request.bad': 'The request is invalid.',
|
|
8
|
+
'request.invalid_version': 'The requested version is invalid.',
|
|
9
|
+
'request.precondition_failed': 'The request precondition failed.',
|
|
10
|
+
'request.not_acceptable': 'The request is not acceptable.',
|
|
11
|
+
'request.rate_limited': 'Too many requests were made in a short period of time.',
|
|
12
|
+
'config.invalid': 'The provided configuration is invalid.',
|
|
13
|
+
'auth.required': 'Authentication is required.',
|
|
14
|
+
'auth.invalid': 'Authentication failed.',
|
|
15
|
+
'auth.expired': 'Authentication has expired.',
|
|
16
|
+
'permission.denied': 'Permission was denied.',
|
|
17
|
+
'resource.not_found': 'The requested resource was not found.',
|
|
18
|
+
'resource.conflict': 'The requested resource is in conflict.',
|
|
19
|
+
'resource.gone': 'The requested resource is no longer available.',
|
|
20
|
+
'payment.required': 'Payment is required to continue.',
|
|
21
|
+
'operation.not_implemented': 'The requested operation is not implemented.',
|
|
22
|
+
'upstream.invalid_request': 'The upstream service rejected the request.',
|
|
23
|
+
'upstream.rate_limited': 'The upstream service rate limited the request.',
|
|
24
|
+
'upstream.timeout': 'The upstream service timed out.',
|
|
25
|
+
'upstream.network_error': 'A network error occurred while contacting the upstream service.',
|
|
26
|
+
'upstream.unavailable': 'The upstream service is unavailable.',
|
|
27
|
+
'upstream.invalid_response': 'The upstream service returned an invalid response.',
|
|
28
|
+
'upstream.error': 'The upstream service returned an error.',
|
|
29
|
+
'transport.invoke_failed': 'The provider transport failed.',
|
|
30
|
+
'internal.timeout': 'The operation timed out.',
|
|
31
|
+
'internal.unexpected': 'An unexpected internal error occurred.'
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export let SERVICE_ERROR_CODE_MAP: Record<string, string> = {
|
|
35
|
+
invalid_data: 'input.invalid',
|
|
36
|
+
bad_request: 'request.bad',
|
|
37
|
+
not_found: 'resource.not_found',
|
|
38
|
+
unauthorized: 'auth.required',
|
|
39
|
+
forbidden: 'permission.denied',
|
|
40
|
+
invalid_version: 'request.invalid_version',
|
|
41
|
+
conflict: 'resource.conflict',
|
|
42
|
+
gone: 'resource.gone',
|
|
43
|
+
payment_required: 'payment.required',
|
|
44
|
+
precondition_failed: 'request.precondition_failed',
|
|
45
|
+
not_acceptable: 'request.not_acceptable',
|
|
46
|
+
not_implemented: 'operation.not_implemented',
|
|
47
|
+
too_many_requests: 'request.rate_limited',
|
|
48
|
+
timeout: 'internal.timeout',
|
|
49
|
+
internal_server_error: 'internal.unexpected'
|
|
50
|
+
};
|