@moogodev/client 0.0.0-stage → 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/dist/client.d.ts +155 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +546 -0
- package/dist/errors.d.ts +84 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +246 -0
- package/dist/esm/client.d.ts +155 -0
- package/dist/esm/client.d.ts.map +1 -0
- package/dist/esm/client.js +541 -0
- package/dist/esm/errors.d.ts +84 -0
- package/dist/esm/errors.d.ts.map +1 -0
- package/dist/esm/errors.js +220 -0
- package/dist/esm/index.d.ts +35 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/types.d.ts +183 -0
- package/dist/esm/types.d.ts.map +1 -0
- package/dist/esm/types.js +5 -0
- package/dist/index.d.ts +35 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +63 -0
- package/dist/index.mjs +35 -0
- package/dist/types.d.ts +183 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +6 -0
- package/package.json +43 -3
- package/src/client.ts +720 -0
- package/src/errors.ts +259 -0
- package/src/index.test.ts +277 -0
- package/src/index.ts +91 -0
- package/src/types.ts +206 -0
- package/tsconfig.json +23 -0
- package/vitest.config.ts +9 -0
- package/README.md +0 -3
package/src/errors.ts
ADDED
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error classes for Moogo client.
|
|
3
|
+
* All errors follow the API error codes.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export class MoogoError extends Error {
|
|
7
|
+
public readonly code: string;
|
|
8
|
+
public readonly status: number;
|
|
9
|
+
public readonly detail?: string;
|
|
10
|
+
|
|
11
|
+
constructor(code: string, message: string, status: number, detail?: string) {
|
|
12
|
+
super(message);
|
|
13
|
+
this.name = 'MoogoError';
|
|
14
|
+
this.code = code;
|
|
15
|
+
this.status = status;
|
|
16
|
+
this.detail = detail;
|
|
17
|
+
Object.setPrototypeOf(this, MoogoError.prototype);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
static fromResponse(response: Response, data: any): MoogoError {
|
|
21
|
+
const error = data?.error;
|
|
22
|
+
if (error) {
|
|
23
|
+
return new MoogoError(
|
|
24
|
+
error.code,
|
|
25
|
+
error.message,
|
|
26
|
+
response.status,
|
|
27
|
+
error.detail
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
return new MoogoError(
|
|
31
|
+
'unknown_error',
|
|
32
|
+
`HTTP ${response.status}: ${response.statusText}`,
|
|
33
|
+
response.status
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** 400 - Missing or invalid object key */
|
|
39
|
+
export class MissingKeyError extends MoogoError {
|
|
40
|
+
constructor(detail?: string) {
|
|
41
|
+
super('missing_key', 'No object key in the path', 400, detail);
|
|
42
|
+
this.name = 'MissingKeyError';
|
|
43
|
+
Object.setPrototypeOf(this, MissingKeyError.prototype);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export class InvalidKeyError extends MoogoError {
|
|
48
|
+
constructor(detail?: string) {
|
|
49
|
+
super('invalid_key', 'Key contains invalid characters', 400, detail);
|
|
50
|
+
this.name = 'InvalidKeyError';
|
|
51
|
+
Object.setPrototypeOf(this, InvalidKeyError.prototype);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export class KeyTooLongError extends MoogoError {
|
|
56
|
+
constructor(detail?: string) {
|
|
57
|
+
super('key_too_long', 'Object key exceeds maximum length', 400, detail);
|
|
58
|
+
this.name = 'KeyTooLongError';
|
|
59
|
+
Object.setPrototypeOf(this, KeyTooLongError.prototype);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export class KeyTakenError extends MoogoError {
|
|
64
|
+
constructor(detail?: string) {
|
|
65
|
+
super('key_taken', 'An object with that key already exists', 409, detail);
|
|
66
|
+
this.name = 'KeyTakenError';
|
|
67
|
+
Object.setPrototypeOf(this, KeyTakenError.prototype);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export class InvalidBucketError extends MoogoError {
|
|
72
|
+
constructor(detail?: string) {
|
|
73
|
+
super('invalid_bucket', 'Bucket name is not allowed or invalid', 400, detail);
|
|
74
|
+
this.name = 'InvalidBucketError';
|
|
75
|
+
Object.setPrototypeOf(this, InvalidBucketError.prototype);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export class InvalidCursorError extends MoogoError {
|
|
80
|
+
constructor(detail?: string) {
|
|
81
|
+
super('invalid_cursor', 'Cursor is malformed or belongs to a different listing', 400, detail);
|
|
82
|
+
this.name = 'InvalidCursorError';
|
|
83
|
+
Object.setPrototypeOf(this, InvalidCursorError.prototype);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export class InvalidOrderError extends MoogoError {
|
|
88
|
+
constructor(detail?: string) {
|
|
89
|
+
super('invalid_order', 'Unknown order or direction', 400, detail);
|
|
90
|
+
this.name = 'InvalidOrderError';
|
|
91
|
+
Object.setPrototypeOf(this, InvalidOrderError.prototype);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export class BodyTooLargeError extends MoogoError {
|
|
96
|
+
constructor(detail?: string) {
|
|
97
|
+
super('body_too_large', 'Request body exceeded the JSON or upload cap', 413, detail);
|
|
98
|
+
this.name = 'BodyTooLargeError';
|
|
99
|
+
Object.setPrototypeOf(this, BodyTooLargeError.prototype);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export class ObjectTooLargeError extends MoogoError {
|
|
104
|
+
constructor(detail?: string) {
|
|
105
|
+
super('object_too_large', 'Object over the bucket max size', 413, detail);
|
|
106
|
+
this.name = 'ObjectTooLargeError';
|
|
107
|
+
Object.setPrototypeOf(this, ObjectTooLargeError.prototype);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export class UnsupportedMediaTypeError extends MoogoError {
|
|
112
|
+
constructor(detail?: string) {
|
|
113
|
+
super('unsupported_media_type', 'Wrong Content-Type', 415, detail);
|
|
114
|
+
this.name = 'UnsupportedMediaTypeError';
|
|
115
|
+
Object.setPrototypeOf(this, UnsupportedMediaTypeError.prototype);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export class QuotaExceededError extends MoogoError {
|
|
120
|
+
constructor(detail?: string) {
|
|
121
|
+
super('quota_exceeded', 'Project or bucket storage would go over quota', 409, detail);
|
|
122
|
+
this.name = 'QuotaExceededError';
|
|
123
|
+
Object.setPrototypeOf(this, QuotaExceededError.prototype);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export class ProjectPausedError extends MoogoError {
|
|
128
|
+
constructor(detail?: string) {
|
|
129
|
+
super('project_paused', 'Project is paused', 403, detail);
|
|
130
|
+
this.name = 'ProjectPausedError';
|
|
131
|
+
Object.setPrototypeOf(this, ProjectPausedError.prototype);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export class LinkExpiredError extends MoogoError {
|
|
136
|
+
constructor(detail?: string) {
|
|
137
|
+
super('link_expired', 'Signed URL expired', 403, detail);
|
|
138
|
+
this.name = 'LinkExpiredError';
|
|
139
|
+
Object.setPrototypeOf(this, LinkExpiredError.prototype);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export class NotFoundError extends MoogoError {
|
|
144
|
+
constructor(detail?: string) {
|
|
145
|
+
super('not_found', 'Bucket/object missing or not published', 404, detail);
|
|
146
|
+
this.name = 'NotFoundError';
|
|
147
|
+
Object.setPrototypeOf(this, NotFoundError.prototype);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export class BucketNotFoundError extends MoogoError {
|
|
152
|
+
constructor(detail?: string) {
|
|
153
|
+
super('bucket_not_found', 'Bucket does not exist', 404, detail);
|
|
154
|
+
this.name = 'BucketNotFoundError';
|
|
155
|
+
Object.setPrototypeOf(this, BucketNotFoundError.prototype);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export class CredentialLimitError extends MoogoError {
|
|
160
|
+
constructor(detail?: string) {
|
|
161
|
+
super('credential_limit', 'Too many storage credentials', 409, detail);
|
|
162
|
+
this.name = 'CredentialLimitError';
|
|
163
|
+
Object.setPrototypeOf(this, CredentialLimitError.prototype);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export class RateLimitedError extends MoogoError {
|
|
168
|
+
public readonly retryAfter: number;
|
|
169
|
+
public readonly resetAt: number | null;
|
|
170
|
+
|
|
171
|
+
constructor(retryAfter: number, resetAt: number | null, detail?: string) {
|
|
172
|
+
super('rate_limited', 'Too many requests', 429, detail);
|
|
173
|
+
this.name = 'RateLimitedError';
|
|
174
|
+
this.retryAfter = retryAfter;
|
|
175
|
+
this.resetAt = resetAt;
|
|
176
|
+
Object.setPrototypeOf(this, RateLimitedError.prototype);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
static fromResponse(response: Response, data: any): RateLimitedError {
|
|
180
|
+
const retryAfter = parseInt(response.headers.get('Retry-After') || '1', 10);
|
|
181
|
+
const resetAt = response.headers.get('X-RateLimit-Reset')
|
|
182
|
+
? parseInt(response.headers.get('X-RateLimit-Reset')!, 10)
|
|
183
|
+
: null;
|
|
184
|
+
return new RateLimitedError(retryAfter, resetAt, data?.error?.detail);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export class InvalidBodyError extends MoogoError {
|
|
189
|
+
constructor(detail?: string) {
|
|
190
|
+
super('invalid_body', 'Malformed JSON or missing required fields', 400, detail);
|
|
191
|
+
this.name = 'InvalidBodyError';
|
|
192
|
+
Object.setPrototypeOf(this, InvalidBodyError.prototype);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export class InvalidExpiresInError extends MoogoError {
|
|
197
|
+
constructor(detail?: string) {
|
|
198
|
+
super('invalid_expires_in', 'expires_in is outside the 60-604800 second range', 400, detail);
|
|
199
|
+
this.name = 'InvalidExpiresInError';
|
|
200
|
+
Object.setPrototypeOf(this, InvalidExpiresInError.prototype);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export class InvalidMethodError extends MoogoError {
|
|
205
|
+
constructor(detail?: string) {
|
|
206
|
+
super('invalid_method', 'A signed URL was requested with a method other than GET or PUT', 400, detail);
|
|
207
|
+
this.name = 'InvalidMethodError';
|
|
208
|
+
Object.setPrototypeOf(this, InvalidMethodError.prototype);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export class UnauthorizedError extends MoogoError {
|
|
213
|
+
constructor(detail?: string) {
|
|
214
|
+
super('unauthorized', 'Authentication required', 401, detail);
|
|
215
|
+
this.name = 'UnauthorizedError';
|
|
216
|
+
Object.setPrototypeOf(this, UnauthorizedError.prototype);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** Map error codes to error classes */
|
|
221
|
+
const errorMap: Record<string, new (detail?: string) => MoogoError> = {
|
|
222
|
+
missing_key: MissingKeyError,
|
|
223
|
+
invalid_key: InvalidKeyError,
|
|
224
|
+
key_too_long: KeyTooLongError,
|
|
225
|
+
key_taken: KeyTakenError,
|
|
226
|
+
invalid_bucket: InvalidBucketError,
|
|
227
|
+
invalid_cursor: InvalidCursorError,
|
|
228
|
+
invalid_order: InvalidOrderError,
|
|
229
|
+
body_too_large: BodyTooLargeError,
|
|
230
|
+
object_too_large: ObjectTooLargeError,
|
|
231
|
+
unsupported_media_type: UnsupportedMediaTypeError,
|
|
232
|
+
quota_exceeded: QuotaExceededError,
|
|
233
|
+
project_paused: ProjectPausedError,
|
|
234
|
+
link_expired: LinkExpiredError,
|
|
235
|
+
not_found: NotFoundError,
|
|
236
|
+
bucket_not_found: BucketNotFoundError,
|
|
237
|
+
credential_limit: CredentialLimitError,
|
|
238
|
+
invalid_body: InvalidBodyError,
|
|
239
|
+
invalid_expires_in: InvalidExpiresInError,
|
|
240
|
+
invalid_method: InvalidMethodError,
|
|
241
|
+
unauthorized: UnauthorizedError,
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Create a typed error from an API response.
|
|
246
|
+
* Falls back to generic MoogoError for unknown codes.
|
|
247
|
+
*/
|
|
248
|
+
export function createErrorFromResponse(response: Response, data: any): MoogoError {
|
|
249
|
+
if (response.status === 429) {
|
|
250
|
+
return RateLimitedError.fromResponse(response, data);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const errorCode = data?.error?.code;
|
|
254
|
+
if (errorCode && errorMap[errorCode]) {
|
|
255
|
+
return new errorMap[errorCode](data.error.detail);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return MoogoError.fromResponse(response, data);
|
|
259
|
+
}
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Basic type and structure tests for the Moogo client.
|
|
3
|
+
* These verify the exports and types compile correctly.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
7
|
+
import {
|
|
8
|
+
MoogoClient,
|
|
9
|
+
createClientFromEnv,
|
|
10
|
+
MoogoError,
|
|
11
|
+
RateLimitedError,
|
|
12
|
+
NotFoundError,
|
|
13
|
+
createErrorFromResponse,
|
|
14
|
+
} from './index.js';
|
|
15
|
+
import type {
|
|
16
|
+
ClientConfig,
|
|
17
|
+
Bucket,
|
|
18
|
+
ObjectResponse,
|
|
19
|
+
ListObjectsQuery,
|
|
20
|
+
ListObjectsResponse,
|
|
21
|
+
StorageCredential,
|
|
22
|
+
} from './types.js';
|
|
23
|
+
|
|
24
|
+
describe('MoogoClient types', () => {
|
|
25
|
+
it('should accept valid config', () => {
|
|
26
|
+
const config: ClientConfig = {
|
|
27
|
+
baseUrl: 'https://api.moogo.dev',
|
|
28
|
+
projectId: 'test-project',
|
|
29
|
+
accessKeyId: 'test-key',
|
|
30
|
+
secretKey: 'test-secret',
|
|
31
|
+
};
|
|
32
|
+
expect(config.baseUrl).toBe('https://api.moogo.dev');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('should have correct Bucket type', () => {
|
|
36
|
+
const bucket: Bucket = {
|
|
37
|
+
id: 'bucket-1',
|
|
38
|
+
name: 'test-bucket',
|
|
39
|
+
object_count: 0,
|
|
40
|
+
size_bytes: 0,
|
|
41
|
+
created_at: '2024-01-01T00:00:00Z',
|
|
42
|
+
is_public: false,
|
|
43
|
+
allowed_types: ['image/*'],
|
|
44
|
+
max_object_size_bytes: 1024 * 1024,
|
|
45
|
+
quota_bytes: 1024 * 1024 * 100,
|
|
46
|
+
};
|
|
47
|
+
expect(bucket.name).toBe('test-bucket');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('should have correct ObjectResponse type', () => {
|
|
51
|
+
const obj: ObjectResponse = {
|
|
52
|
+
id: 'obj-1',
|
|
53
|
+
bucket_id: 'bucket-1',
|
|
54
|
+
bucket: 'test-bucket',
|
|
55
|
+
key: 'test.png',
|
|
56
|
+
size_bytes: 1024,
|
|
57
|
+
content_type: 'image/png',
|
|
58
|
+
is_public: true,
|
|
59
|
+
etag: '"abc123"',
|
|
60
|
+
url: 'https://api.moogo.dev/p/proj/bucket/test.png',
|
|
61
|
+
preview_url: '/api/projects/proj/bucket/test.png',
|
|
62
|
+
public_url: 'https://cdn.moogo.dev/pub/proj/test.png',
|
|
63
|
+
public_url_base: 'https://cdn.moogo.dev',
|
|
64
|
+
url_base: 'https://api.moogo.dev',
|
|
65
|
+
created_at: '2024-01-01T00:00:00Z',
|
|
66
|
+
updated_at: '2024-01-01T00:00:00Z',
|
|
67
|
+
last_modified: '2024-01-01T00:00:00Z',
|
|
68
|
+
};
|
|
69
|
+
expect(obj.public_url_base).toBe('https://cdn.moogo.dev');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('should have correct ListObjectsQuery type', () => {
|
|
73
|
+
const query: ListObjectsQuery = {
|
|
74
|
+
bucket: 'my-bucket',
|
|
75
|
+
prefix: 'images/',
|
|
76
|
+
search: 'png',
|
|
77
|
+
order_by: 'created',
|
|
78
|
+
order_dir: 'desc',
|
|
79
|
+
limit: 50,
|
|
80
|
+
cursor: 'cursor123',
|
|
81
|
+
};
|
|
82
|
+
expect(query.order_by).toBe('created');
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('should have correct ListObjectsResponse type', () => {
|
|
86
|
+
const response: ListObjectsResponse = {
|
|
87
|
+
success: true,
|
|
88
|
+
objects: [],
|
|
89
|
+
next_cursor: null,
|
|
90
|
+
total: 0,
|
|
91
|
+
};
|
|
92
|
+
expect(response.success).toBe(true);
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
describe('MoogoClient instantiation', () => {
|
|
97
|
+
it('should create client with config', () => {
|
|
98
|
+
const client = new MoogoClient({
|
|
99
|
+
baseUrl: 'https://api.moogo.dev',
|
|
100
|
+
projectId: 'test-project',
|
|
101
|
+
accessKeyId: 'test-key',
|
|
102
|
+
secretKey: 'test-secret',
|
|
103
|
+
});
|
|
104
|
+
expect(client).toBeInstanceOf(MoogoClient);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('should create client from env', () => {
|
|
108
|
+
const originalEnv = process.env;
|
|
109
|
+
process.env = {
|
|
110
|
+
...originalEnv,
|
|
111
|
+
MOOGO_API_URL: 'https://api.moogo.dev',
|
|
112
|
+
MOOGO_PROJECT_ID: 'test-project',
|
|
113
|
+
MOOGO_BUCKET_ACCESS_KEY_ID: 'test-key',
|
|
114
|
+
MOOGO_BUCKET_SECRET_KEY: 'test-secret',
|
|
115
|
+
};
|
|
116
|
+
const client = createClientFromEnv();
|
|
117
|
+
expect(client).toBeInstanceOf(MoogoClient);
|
|
118
|
+
process.env = originalEnv;
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('should throw on missing required config', () => {
|
|
122
|
+
expect(() => new MoogoClient({
|
|
123
|
+
baseUrl: '',
|
|
124
|
+
projectId: 'test',
|
|
125
|
+
accessKeyId: 'test',
|
|
126
|
+
secretKey: 'test',
|
|
127
|
+
})).toThrow('baseUrl is required');
|
|
128
|
+
|
|
129
|
+
expect(() => new MoogoClient({
|
|
130
|
+
baseUrl: 'https://api.moogo.dev',
|
|
131
|
+
projectId: '',
|
|
132
|
+
accessKeyId: 'test',
|
|
133
|
+
secretKey: 'test',
|
|
134
|
+
})).toThrow('projectId is required');
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
describe('Error classes', () => {
|
|
139
|
+
it('should create MoogoError with correct properties', () => {
|
|
140
|
+
const error = new MoogoError('test_code', 'Test message', 400, 'detail');
|
|
141
|
+
expect(error.code).toBe('test_code');
|
|
142
|
+
expect(error.message).toBe('Test message');
|
|
143
|
+
expect(error.status).toBe(400);
|
|
144
|
+
expect(error.detail).toBe('detail');
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('should create RateLimitedError with retry info', () => {
|
|
148
|
+
const error = new RateLimitedError(60, 1234567890, 'Too many requests');
|
|
149
|
+
expect(error.retryAfter).toBe(60);
|
|
150
|
+
expect(error.resetAt).toBe(1234567890);
|
|
151
|
+
expect(error.status).toBe(429);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('should create specific error types', () => {
|
|
155
|
+
const notFound = new NotFoundError('Object not found');
|
|
156
|
+
expect(notFound.code).toBe('not_found');
|
|
157
|
+
expect(notFound.status).toBe(404);
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
describe('createErrorFromResponse', () => {
|
|
162
|
+
const mockResponse = (status: number, headers: Record<string, string> = {}) => ({
|
|
163
|
+
status,
|
|
164
|
+
ok: status >= 200 && status < 300,
|
|
165
|
+
headers: {
|
|
166
|
+
get: (key: string) => headers[key.toLowerCase()] || null,
|
|
167
|
+
},
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('should create RateLimitedError for 429', () => {
|
|
171
|
+
const response = mockResponse(429, {
|
|
172
|
+
'retry-after': '60',
|
|
173
|
+
'x-ratelimit-reset': '1234567890',
|
|
174
|
+
});
|
|
175
|
+
const error = createErrorFromResponse(response as any, { error: { code: 'rate_limited', message: 'Too many', detail: 'detail' } });
|
|
176
|
+
expect(error).toBeInstanceOf(RateLimitedError);
|
|
177
|
+
expect(error.retryAfter).toBe(60);
|
|
178
|
+
expect(error.resetAt).toBe(1234567890);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('should create NotFoundError for not_found code', () => {
|
|
182
|
+
const response = mockResponse(404);
|
|
183
|
+
const error = createErrorFromResponse(response as any, { error: { code: 'not_found', message: 'Not found', detail: 'detail' } });
|
|
184
|
+
expect(error).toBeInstanceOf(NotFoundError);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('should fallback to generic MoogoError for unknown codes', () => {
|
|
188
|
+
const response = mockResponse(500);
|
|
189
|
+
const error = createErrorFromResponse(response as any, { error: { code: 'unknown_code', message: 'Unknown', detail: 'detail' } });
|
|
190
|
+
expect(error).toBeInstanceOf(MoogoError);
|
|
191
|
+
expect(error.code).toBe('unknown_code');
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
describe('Client method signatures', () => {
|
|
196
|
+
let client: MoogoClient;
|
|
197
|
+
|
|
198
|
+
beforeEach(() => {
|
|
199
|
+
client = new MoogoClient({
|
|
200
|
+
baseUrl: 'https://api.moogo.dev',
|
|
201
|
+
projectId: 'test-project',
|
|
202
|
+
accessKeyId: 'test-key',
|
|
203
|
+
secretKey: 'test-secret',
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it('should have bucket methods', () => {
|
|
208
|
+
expect(typeof client.listBuckets).toBe('function');
|
|
209
|
+
expect(typeof client.getBucketInfo).toBe('function');
|
|
210
|
+
expect(typeof client.createBucket).toBe('function');
|
|
211
|
+
expect(typeof client.updateBucket).toBe('function');
|
|
212
|
+
expect(typeof client.deleteBucket).toBe('function');
|
|
213
|
+
expect(typeof client.setBucketPublic).toBe('function');
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it('should have object methods', () => {
|
|
217
|
+
expect(typeof client.upload).toBe('function');
|
|
218
|
+
expect(typeof client.download).toBe('function');
|
|
219
|
+
expect(typeof client.downloadBlob).toBe('function');
|
|
220
|
+
expect(typeof client.downloadText).toBe('function');
|
|
221
|
+
expect(typeof client.downloadArrayBuffer).toBe('function');
|
|
222
|
+
expect(typeof client.headObject).toBe('function');
|
|
223
|
+
expect(typeof client.deleteObject).toBe('function');
|
|
224
|
+
expect(typeof client.listObjects).toBe('function');
|
|
225
|
+
expect(typeof client.getPresignedUrl).toBe('function');
|
|
226
|
+
expect(typeof client.createSignedUpload).toBe('function');
|
|
227
|
+
expect(typeof client.updateObject).toBe('function');
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it('should have credential methods', () => {
|
|
231
|
+
expect(typeof client.listCredentials).toBe('function');
|
|
232
|
+
expect(typeof client.createCredential).toBe('function');
|
|
233
|
+
expect(typeof client.rotateCredential).toBe('function');
|
|
234
|
+
expect(typeof client.revokeCredential).toBe('function');
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it('should have utility methods', () => {
|
|
238
|
+
expect(typeof client.getPublicUrl).toBe('function');
|
|
239
|
+
expect(typeof client.getAuthenticatedUrl).toBe('function');
|
|
240
|
+
expect(typeof client.objectExists).toBe('function');
|
|
241
|
+
expect(typeof client.getObjectSize).toBe('function');
|
|
242
|
+
expect(typeof client.getObjectETag).toBe('function');
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
describe('URL helpers', () => {
|
|
247
|
+
let client: MoogoClient;
|
|
248
|
+
|
|
249
|
+
beforeEach(() => {
|
|
250
|
+
client = new MoogoClient({
|
|
251
|
+
baseUrl: 'https://api.moogo.dev',
|
|
252
|
+
projectId: 'test-project',
|
|
253
|
+
accessKeyId: 'test-key',
|
|
254
|
+
secretKey: 'test-secret',
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it('should generate public URL', () => {
|
|
259
|
+
const url = client.getPublicUrl('test.png');
|
|
260
|
+
expect(url).toBe('https://api.moogo.dev/pub/test-project/test.png');
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it('should generate public URL with custom base', () => {
|
|
264
|
+
const url = client.getPublicUrl('test.png', 'https://cdn.example.com');
|
|
265
|
+
expect(url).toBe('https://cdn.example.com/pub/test-project/test.png');
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it('should generate authenticated URL', () => {
|
|
269
|
+
const url = client.getAuthenticatedUrl('test.png');
|
|
270
|
+
expect(url).toBe('https://api.moogo.dev/p/test-project/test.png');
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
it('should handle special characters in key', () => {
|
|
274
|
+
const url = client.getPublicUrl('path/to/file.png');
|
|
275
|
+
expect(url).toBe('https://api.moogo.dev/pub/test-project/path%2Fto%2Ffile.png');
|
|
276
|
+
});
|
|
277
|
+
});
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @moogodev/client - Official JavaScript/TypeScript client for Moogo object storage
|
|
3
|
+
*
|
|
4
|
+
* @example
|
|
5
|
+
* ```typescript
|
|
6
|
+
* import { MoogoClient, createClientFromEnv } from '@moogodev/client';
|
|
7
|
+
*
|
|
8
|
+
* // From environment variables
|
|
9
|
+
* const client = createClientFromEnv();
|
|
10
|
+
*
|
|
11
|
+
* // Or with explicit config
|
|
12
|
+
* const client = new MoogoClient({
|
|
13
|
+
* baseUrl: 'https://api.moogo.dev',
|
|
14
|
+
* projectId: 'your-project-id',
|
|
15
|
+
* accessKeyId: 'your-access-key-id',
|
|
16
|
+
* secretKey: 'your-secret-key',
|
|
17
|
+
* });
|
|
18
|
+
*
|
|
19
|
+
* // Upload a file
|
|
20
|
+
* const response = await client.upload('avatars/user.png', fileBlob, {
|
|
21
|
+
* contentType: 'image/png',
|
|
22
|
+
* cacheControl: 'public, max-age=31536000, immutable',
|
|
23
|
+
* });
|
|
24
|
+
*
|
|
25
|
+
* // List objects
|
|
26
|
+
* const { objects, nextCursor } = await client.listObjects({ prefix: 'avatars/' });
|
|
27
|
+
*
|
|
28
|
+
* // Download
|
|
29
|
+
* const blob = await client.downloadBlob('avatars/user.png');
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
// Types
|
|
34
|
+
export type {
|
|
35
|
+
ClientConfig,
|
|
36
|
+
ProjectId,
|
|
37
|
+
BucketId,
|
|
38
|
+
ObjectKey,
|
|
39
|
+
ApiError,
|
|
40
|
+
ApiErrorResponse,
|
|
41
|
+
ApiSuccessResponse,
|
|
42
|
+
ObjectBase,
|
|
43
|
+
ObjectResponse,
|
|
44
|
+
Bucket,
|
|
45
|
+
CreateBucketRequest,
|
|
46
|
+
UpdateBucketRequest,
|
|
47
|
+
BucketInfoResponse,
|
|
48
|
+
ListBucketsResponse,
|
|
49
|
+
UploadResponse,
|
|
50
|
+
ListObjectsQuery,
|
|
51
|
+
ListObjectsResponse,
|
|
52
|
+
PresignedUrlRequest,
|
|
53
|
+
PresignedUrlResponse,
|
|
54
|
+
SignedUploadResponse,
|
|
55
|
+
StorageCredential,
|
|
56
|
+
ListCredentialsResponse,
|
|
57
|
+
CreateCredentialRequest,
|
|
58
|
+
CreateCredentialResponse,
|
|
59
|
+
RotateCredentialResponse,
|
|
60
|
+
DeleteResponse,
|
|
61
|
+
} from './types.js';
|
|
62
|
+
|
|
63
|
+
// Errors
|
|
64
|
+
export {
|
|
65
|
+
MoogoError,
|
|
66
|
+
MissingKeyError,
|
|
67
|
+
InvalidKeyError,
|
|
68
|
+
KeyTooLongError,
|
|
69
|
+
KeyTakenError,
|
|
70
|
+
InvalidBucketError,
|
|
71
|
+
InvalidCursorError,
|
|
72
|
+
InvalidOrderError,
|
|
73
|
+
BodyTooLargeError,
|
|
74
|
+
ObjectTooLargeError,
|
|
75
|
+
UnsupportedMediaTypeError,
|
|
76
|
+
QuotaExceededError,
|
|
77
|
+
ProjectPausedError,
|
|
78
|
+
LinkExpiredError,
|
|
79
|
+
NotFoundError,
|
|
80
|
+
BucketNotFoundError,
|
|
81
|
+
CredentialLimitError,
|
|
82
|
+
RateLimitedError,
|
|
83
|
+
InvalidBodyError,
|
|
84
|
+
InvalidExpiresInError,
|
|
85
|
+
InvalidMethodError,
|
|
86
|
+
UnauthorizedError,
|
|
87
|
+
createErrorFromResponse,
|
|
88
|
+
} from './errors.js';
|
|
89
|
+
|
|
90
|
+
// Client
|
|
91
|
+
export { MoogoClient, createClientFromEnv } from './client.js';
|