@tthr/vue 0.0.13 → 0.0.14
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/nuxt/module.ts +9 -1
- package/nuxt/runtime/server/utils/tether.ts +338 -0
- package/package.json +1 -1
package/nuxt/module.ts
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
* - TETHER_API_KEY: Your project's API key (required, kept server-side)
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
|
-
import { defineNuxtModule, addPlugin, createResolver, addImports, addComponent, addServerHandler } from '@nuxt/kit';
|
|
22
|
+
import { defineNuxtModule, addPlugin, createResolver, addImports, addComponent, addServerHandler, addServerImports } from '@nuxt/kit';
|
|
23
23
|
|
|
24
24
|
export interface TetherModuleOptions {
|
|
25
25
|
/** Project ID from Tether dashboard */
|
|
@@ -101,5 +101,13 @@ export default defineNuxtModule<TetherModuleOptions>({
|
|
|
101
101
|
name: 'TetherWelcome',
|
|
102
102
|
filePath: resolver.resolve('./runtime/components/TetherWelcome.vue'),
|
|
103
103
|
});
|
|
104
|
+
|
|
105
|
+
// Auto-import server utilities (available in server/ directory)
|
|
106
|
+
addServerImports([
|
|
107
|
+
{
|
|
108
|
+
name: 'useTetherServer',
|
|
109
|
+
from: resolver.resolve('./runtime/server/utils/tether'),
|
|
110
|
+
},
|
|
111
|
+
]);
|
|
104
112
|
},
|
|
105
113
|
});
|
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server-side Tether utilities for Nuxt
|
|
3
|
+
*
|
|
4
|
+
* Use these in your Nuxt server endpoints (e.g., server/api/*.ts)
|
|
5
|
+
* to query and mutate Tether data with full type safety.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```ts
|
|
9
|
+
* // server/api/my-endpoint.ts
|
|
10
|
+
* export default defineEventHandler(async (event) => {
|
|
11
|
+
* const tether = useTetherServer(event);
|
|
12
|
+
*
|
|
13
|
+
* const todos = await tether.query('getTodos', { completed: false });
|
|
14
|
+
* await tether.mutation('createTodo', { title: 'New todo' });
|
|
15
|
+
*
|
|
16
|
+
* return { todos };
|
|
17
|
+
* });
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { useRuntimeConfig, createError, type H3Event } from '#imports';
|
|
22
|
+
|
|
23
|
+
export interface TetherServerClient {
|
|
24
|
+
/**
|
|
25
|
+
* Execute a query on the Tether server
|
|
26
|
+
*/
|
|
27
|
+
query: <TResult = unknown>(
|
|
28
|
+
name: string,
|
|
29
|
+
args?: Record<string, unknown>
|
|
30
|
+
) => Promise<TResult>;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Execute a mutation on the Tether server
|
|
34
|
+
*/
|
|
35
|
+
mutation: <TResult = unknown>(
|
|
36
|
+
name: string,
|
|
37
|
+
args?: Record<string, unknown>
|
|
38
|
+
) => Promise<TResult>;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* File storage operations
|
|
42
|
+
*/
|
|
43
|
+
storage: {
|
|
44
|
+
/**
|
|
45
|
+
* Generate a presigned upload URL
|
|
46
|
+
*/
|
|
47
|
+
generateUploadUrl: (options: {
|
|
48
|
+
filename: string;
|
|
49
|
+
contentType: string;
|
|
50
|
+
metadata?: Record<string, unknown>;
|
|
51
|
+
}) => Promise<{ assetId: string; uploadUrl: string; expiresAt: string }>;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Confirm an upload has completed
|
|
55
|
+
*/
|
|
56
|
+
confirmUpload: (assetId: string, sha256?: string) => Promise<AssetMetadata>;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Get a presigned download URL for an asset
|
|
60
|
+
*/
|
|
61
|
+
getUrl: (assetId: string) => Promise<string>;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Get metadata for an asset
|
|
65
|
+
*/
|
|
66
|
+
getMetadata: (assetId: string) => Promise<AssetMetadata>;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Delete an asset
|
|
70
|
+
*/
|
|
71
|
+
delete: (assetId: string) => Promise<void>;
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* List assets with optional pagination and filtering
|
|
75
|
+
*/
|
|
76
|
+
list: (options?: {
|
|
77
|
+
limit?: number;
|
|
78
|
+
offset?: number;
|
|
79
|
+
contentType?: string;
|
|
80
|
+
}) => Promise<{ assets: AssetMetadata[]; totalCount: number }>;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* The project ID
|
|
85
|
+
*/
|
|
86
|
+
projectId: string;
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* The API URL
|
|
90
|
+
*/
|
|
91
|
+
url: string;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export interface AssetMetadata {
|
|
95
|
+
id: string;
|
|
96
|
+
filename: string;
|
|
97
|
+
contentType: string;
|
|
98
|
+
size: number;
|
|
99
|
+
sha256?: string;
|
|
100
|
+
createdAt: string;
|
|
101
|
+
metadata?: Record<string, unknown>;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Get a Tether client for use in server-side code
|
|
106
|
+
*
|
|
107
|
+
* @param event - The H3 event (optional, used for request context)
|
|
108
|
+
* @returns A Tether client with query, mutation, and storage methods
|
|
109
|
+
*/
|
|
110
|
+
export function useTetherServer(_event?: H3Event): TetherServerClient {
|
|
111
|
+
const config = useRuntimeConfig();
|
|
112
|
+
|
|
113
|
+
const apiKey = config.tether?.apiKey || process.env.TETHER_API_KEY;
|
|
114
|
+
const url = config.tether?.url || process.env.TETHER_URL || 'https://tether-api.strands.gg';
|
|
115
|
+
const projectId = config.tether?.projectId || process.env.TETHER_PROJECT_ID;
|
|
116
|
+
|
|
117
|
+
if (!apiKey) {
|
|
118
|
+
throw createError({
|
|
119
|
+
statusCode: 500,
|
|
120
|
+
message: 'Tether API key not configured. Set TETHER_API_KEY environment variable.',
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (!projectId) {
|
|
125
|
+
throw createError({
|
|
126
|
+
statusCode: 500,
|
|
127
|
+
message: 'Tether project ID not configured. Set TETHER_PROJECT_ID or configure in nuxt.config.ts.',
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const headers = {
|
|
132
|
+
'Content-Type': 'application/json',
|
|
133
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
async function query<TResult = unknown>(
|
|
137
|
+
name: string,
|
|
138
|
+
args?: Record<string, unknown>
|
|
139
|
+
): Promise<TResult> {
|
|
140
|
+
const response = await fetch(`${url}/api/v1/projects/${projectId}/query`, {
|
|
141
|
+
method: 'POST',
|
|
142
|
+
headers,
|
|
143
|
+
body: JSON.stringify({ function: name, args }),
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
if (!response.ok) {
|
|
147
|
+
const error = await response.json().catch(() => ({ error: 'Unknown error' }));
|
|
148
|
+
throw createError({
|
|
149
|
+
statusCode: response.status,
|
|
150
|
+
message: error.error || `Query '${name}' failed`,
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const result = await response.json();
|
|
155
|
+
return result.data as TResult;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async function mutation<TResult = unknown>(
|
|
159
|
+
name: string,
|
|
160
|
+
args?: Record<string, unknown>
|
|
161
|
+
): Promise<TResult> {
|
|
162
|
+
const response = await fetch(`${url}/api/v1/projects/${projectId}/mutation`, {
|
|
163
|
+
method: 'POST',
|
|
164
|
+
headers,
|
|
165
|
+
body: JSON.stringify({ function: name, args }),
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
if (!response.ok) {
|
|
169
|
+
const error = await response.json().catch(() => ({ error: 'Unknown error' }));
|
|
170
|
+
throw createError({
|
|
171
|
+
statusCode: response.status,
|
|
172
|
+
message: error.error || `Mutation '${name}' failed`,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const result = await response.json();
|
|
177
|
+
return result.data as TResult;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const storage = {
|
|
181
|
+
async generateUploadUrl(options: {
|
|
182
|
+
filename: string;
|
|
183
|
+
contentType: string;
|
|
184
|
+
metadata?: Record<string, unknown>;
|
|
185
|
+
}) {
|
|
186
|
+
const response = await fetch(`${url}/api/v1/projects/${projectId}/assets/upload-url`, {
|
|
187
|
+
method: 'POST',
|
|
188
|
+
headers,
|
|
189
|
+
body: JSON.stringify({
|
|
190
|
+
filename: options.filename,
|
|
191
|
+
content_type: options.contentType,
|
|
192
|
+
metadata: options.metadata,
|
|
193
|
+
}),
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
if (!response.ok) {
|
|
197
|
+
const error = await response.json().catch(() => ({ error: 'Unknown error' }));
|
|
198
|
+
throw createError({
|
|
199
|
+
statusCode: response.status,
|
|
200
|
+
message: error.error || 'Failed to generate upload URL',
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const result = await response.json();
|
|
205
|
+
return {
|
|
206
|
+
assetId: result.asset_id,
|
|
207
|
+
uploadUrl: result.upload_url,
|
|
208
|
+
expiresAt: result.expires_at,
|
|
209
|
+
};
|
|
210
|
+
},
|
|
211
|
+
|
|
212
|
+
async confirmUpload(assetId: string, sha256?: string): Promise<AssetMetadata> {
|
|
213
|
+
const response = await fetch(`${url}/api/v1/projects/${projectId}/assets/${assetId}/complete`, {
|
|
214
|
+
method: 'POST',
|
|
215
|
+
headers,
|
|
216
|
+
body: JSON.stringify({ sha256 }),
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
if (!response.ok) {
|
|
220
|
+
const error = await response.json().catch(() => ({ error: 'Unknown error' }));
|
|
221
|
+
throw createError({
|
|
222
|
+
statusCode: response.status,
|
|
223
|
+
message: error.error || 'Failed to confirm upload',
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const result = await response.json();
|
|
228
|
+
return mapAssetMetadata(result.asset);
|
|
229
|
+
},
|
|
230
|
+
|
|
231
|
+
async getUrl(assetId: string): Promise<string> {
|
|
232
|
+
const response = await fetch(`${url}/api/v1/projects/${projectId}/assets/${assetId}/url`, {
|
|
233
|
+
method: 'GET',
|
|
234
|
+
headers,
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
if (!response.ok) {
|
|
238
|
+
const error = await response.json().catch(() => ({ error: 'Unknown error' }));
|
|
239
|
+
throw createError({
|
|
240
|
+
statusCode: response.status,
|
|
241
|
+
message: error.error || 'Failed to get asset URL',
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const result = await response.json();
|
|
246
|
+
return result.url;
|
|
247
|
+
},
|
|
248
|
+
|
|
249
|
+
async getMetadata(assetId: string): Promise<AssetMetadata> {
|
|
250
|
+
const response = await fetch(`${url}/api/v1/projects/${projectId}/assets/${assetId}`, {
|
|
251
|
+
method: 'GET',
|
|
252
|
+
headers,
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
if (!response.ok) {
|
|
256
|
+
const error = await response.json().catch(() => ({ error: 'Unknown error' }));
|
|
257
|
+
throw createError({
|
|
258
|
+
statusCode: response.status,
|
|
259
|
+
message: error.error || 'Failed to get asset metadata',
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const result = await response.json();
|
|
264
|
+
return mapAssetMetadata(result.asset);
|
|
265
|
+
},
|
|
266
|
+
|
|
267
|
+
async delete(assetId: string): Promise<void> {
|
|
268
|
+
const response = await fetch(`${url}/api/v1/projects/${projectId}/assets/${assetId}`, {
|
|
269
|
+
method: 'DELETE',
|
|
270
|
+
headers,
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
if (!response.ok) {
|
|
274
|
+
const error = await response.json().catch(() => ({ error: 'Unknown error' }));
|
|
275
|
+
throw createError({
|
|
276
|
+
statusCode: response.status,
|
|
277
|
+
message: error.error || 'Failed to delete asset',
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
},
|
|
281
|
+
|
|
282
|
+
async list(options?: {
|
|
283
|
+
limit?: number;
|
|
284
|
+
offset?: number;
|
|
285
|
+
contentType?: string;
|
|
286
|
+
}): Promise<{ assets: AssetMetadata[]; totalCount: number }> {
|
|
287
|
+
const params = new URLSearchParams();
|
|
288
|
+
if (options?.limit) params.set('limit', String(options.limit));
|
|
289
|
+
if (options?.offset) params.set('offset', String(options.offset));
|
|
290
|
+
if (options?.contentType) params.set('content_type', options.contentType);
|
|
291
|
+
|
|
292
|
+
const queryString = params.toString();
|
|
293
|
+
const fetchUrl = `${url}/api/v1/projects/${projectId}/assets${queryString ? `?${queryString}` : ''}`;
|
|
294
|
+
|
|
295
|
+
const response = await fetch(fetchUrl, {
|
|
296
|
+
method: 'GET',
|
|
297
|
+
headers,
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
if (!response.ok) {
|
|
301
|
+
const error = await response.json().catch(() => ({ error: 'Unknown error' }));
|
|
302
|
+
throw createError({
|
|
303
|
+
statusCode: response.status,
|
|
304
|
+
message: error.error || 'Failed to list assets',
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const result = await response.json();
|
|
309
|
+
return {
|
|
310
|
+
assets: result.assets.map(mapAssetMetadata),
|
|
311
|
+
totalCount: result.total_count,
|
|
312
|
+
};
|
|
313
|
+
},
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
return {
|
|
317
|
+
query,
|
|
318
|
+
mutation,
|
|
319
|
+
storage,
|
|
320
|
+
projectId,
|
|
321
|
+
url,
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Map snake_case API response to camelCase
|
|
327
|
+
*/
|
|
328
|
+
function mapAssetMetadata(asset: Record<string, unknown>): AssetMetadata {
|
|
329
|
+
return {
|
|
330
|
+
id: asset.id as string,
|
|
331
|
+
filename: asset.filename as string,
|
|
332
|
+
contentType: asset.content_type as string,
|
|
333
|
+
size: asset.size as number,
|
|
334
|
+
sha256: asset.sha256 as string | undefined,
|
|
335
|
+
createdAt: asset.created_at as string,
|
|
336
|
+
metadata: asset.metadata as Record<string, unknown> | undefined,
|
|
337
|
+
};
|
|
338
|
+
}
|