@aerostack/sdk 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +327 -0
- package/dist/index.d.mts +577 -0
- package/dist/index.d.ts +577 -0
- package/dist/index.js +1210 -0
- package/dist/index.mjs +1169 -0
- package/package.json +49 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,1169 @@
|
|
|
1
|
+
// src/client-errors.ts
|
|
2
|
+
var ClientErrorCode = /* @__PURE__ */ ((ClientErrorCode2) => {
|
|
3
|
+
ClientErrorCode2["AUTH_INVALID_CREDENTIALS"] = "AUTH_INVALID_CREDENTIALS";
|
|
4
|
+
ClientErrorCode2["AUTH_USER_EXISTS"] = "AUTH_USER_EXISTS";
|
|
5
|
+
ClientErrorCode2["AUTH_EMAIL_NOT_VERIFIED"] = "AUTH_EMAIL_NOT_VERIFIED";
|
|
6
|
+
ClientErrorCode2["AUTH_TOKEN_EXPIRED"] = "AUTH_TOKEN_EXPIRED";
|
|
7
|
+
ClientErrorCode2["AUTH_TOKEN_INVALID"] = "AUTH_TOKEN_INVALID";
|
|
8
|
+
ClientErrorCode2["AUTH_OTP_EXPIRED"] = "AUTH_OTP_EXPIRED";
|
|
9
|
+
ClientErrorCode2["AUTH_OTP_INVALID"] = "AUTH_OTP_INVALID";
|
|
10
|
+
ClientErrorCode2["AUTH_PASSWORD_WEAK"] = "AUTH_PASSWORD_WEAK";
|
|
11
|
+
ClientErrorCode2["AUTH_RESET_TOKEN_INVALID"] = "AUTH_RESET_TOKEN_INVALID";
|
|
12
|
+
ClientErrorCode2["NETWORK_ERROR"] = "NETWORK_ERROR";
|
|
13
|
+
ClientErrorCode2["NETWORK_TIMEOUT"] = "NETWORK_TIMEOUT";
|
|
14
|
+
ClientErrorCode2["VALIDATION_ERROR"] = "VALIDATION_ERROR";
|
|
15
|
+
ClientErrorCode2["REQUEST_FAILED"] = "REQUEST_FAILED";
|
|
16
|
+
ClientErrorCode2["UNKNOWN_ERROR"] = "UNKNOWN_ERROR";
|
|
17
|
+
return ClientErrorCode2;
|
|
18
|
+
})(ClientErrorCode || {});
|
|
19
|
+
var ClientError = class _ClientError extends Error {
|
|
20
|
+
constructor(code, message, details, statusCode) {
|
|
21
|
+
super(message);
|
|
22
|
+
this.code = code;
|
|
23
|
+
this.details = details;
|
|
24
|
+
this.statusCode = statusCode;
|
|
25
|
+
this.name = "ClientError";
|
|
26
|
+
Object.setPrototypeOf(this, _ClientError.prototype);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Check if this is an authentication error
|
|
30
|
+
*/
|
|
31
|
+
isAuthError() {
|
|
32
|
+
return this.code.toString().startsWith("AUTH_");
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Check if this is a network error
|
|
36
|
+
*/
|
|
37
|
+
isNetworkError() {
|
|
38
|
+
return this.code === "NETWORK_ERROR" /* NETWORK_ERROR */ || this.code === "NETWORK_TIMEOUT" /* NETWORK_TIMEOUT */;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Check if this is a validation error
|
|
42
|
+
*/
|
|
43
|
+
isValidationError() {
|
|
44
|
+
return this.code === "VALIDATION_ERROR" /* VALIDATION_ERROR */;
|
|
45
|
+
}
|
|
46
|
+
toJSON() {
|
|
47
|
+
return {
|
|
48
|
+
name: this.name,
|
|
49
|
+
code: this.code,
|
|
50
|
+
message: this.message,
|
|
51
|
+
details: this.details,
|
|
52
|
+
statusCode: this.statusCode
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
var AuthenticationError = class _AuthenticationError extends ClientError {
|
|
57
|
+
constructor(code, message, details, statusCode) {
|
|
58
|
+
super(code, message, details, statusCode);
|
|
59
|
+
this.name = "AuthenticationError";
|
|
60
|
+
Object.setPrototypeOf(this, _AuthenticationError.prototype);
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
var ValidationError = class _ValidationError extends ClientError {
|
|
64
|
+
constructor(message, field, suggestion) {
|
|
65
|
+
super(
|
|
66
|
+
"VALIDATION_ERROR" /* VALIDATION_ERROR */,
|
|
67
|
+
message,
|
|
68
|
+
{ field, suggestion },
|
|
69
|
+
400
|
|
70
|
+
);
|
|
71
|
+
this.name = "ValidationError";
|
|
72
|
+
Object.setPrototypeOf(this, _ValidationError.prototype);
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
var NetworkError = class _NetworkError extends ClientError {
|
|
76
|
+
constructor(message, suggestion) {
|
|
77
|
+
super(
|
|
78
|
+
"NETWORK_ERROR" /* NETWORK_ERROR */,
|
|
79
|
+
message,
|
|
80
|
+
{ suggestion }
|
|
81
|
+
);
|
|
82
|
+
this.name = "NetworkError";
|
|
83
|
+
Object.setPrototypeOf(this, _NetworkError.prototype);
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
// src/client.ts
|
|
88
|
+
var AerostackClient = class {
|
|
89
|
+
projectSlug;
|
|
90
|
+
baseUrl;
|
|
91
|
+
constructor(config) {
|
|
92
|
+
this.projectSlug = config.projectSlug;
|
|
93
|
+
this.baseUrl = config.baseUrl || "https://api.aerostack.app";
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Authentication operations
|
|
97
|
+
*/
|
|
98
|
+
get auth() {
|
|
99
|
+
return {
|
|
100
|
+
/**
|
|
101
|
+
* Register a new user
|
|
102
|
+
*/
|
|
103
|
+
register: async (data) => {
|
|
104
|
+
if (!data.email || !data.email.includes("@")) {
|
|
105
|
+
throw new ValidationError("Invalid email address", "email", "Provide a valid email address");
|
|
106
|
+
}
|
|
107
|
+
if (!data.password || data.password.length < 8) {
|
|
108
|
+
throw new ValidationError(
|
|
109
|
+
"Password must be at least 8 characters",
|
|
110
|
+
"password",
|
|
111
|
+
"Use a stronger password with minimum 8 characters"
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
return this.request("/auth/register", "POST", data);
|
|
115
|
+
},
|
|
116
|
+
/**
|
|
117
|
+
* Login with email and password
|
|
118
|
+
*/
|
|
119
|
+
login: async (email, password) => {
|
|
120
|
+
if (!email || !password) {
|
|
121
|
+
throw new ValidationError("Email and password are required", "email");
|
|
122
|
+
}
|
|
123
|
+
return this.request("/auth/login", "POST", { email, password });
|
|
124
|
+
},
|
|
125
|
+
/**
|
|
126
|
+
* Send OTP code to email
|
|
127
|
+
*/
|
|
128
|
+
sendOTP: async (email) => {
|
|
129
|
+
if (!email || !email.includes("@")) {
|
|
130
|
+
throw new ValidationError("Invalid email address", "email");
|
|
131
|
+
}
|
|
132
|
+
return this.request("/auth/otp/send", "POST", { email });
|
|
133
|
+
},
|
|
134
|
+
/**
|
|
135
|
+
* Verify OTP code and login
|
|
136
|
+
*/
|
|
137
|
+
verifyOTP: async (email, code) => {
|
|
138
|
+
if (!email || !code) {
|
|
139
|
+
throw new ValidationError("Email and code are required");
|
|
140
|
+
}
|
|
141
|
+
if (code.length !== 6 || !/^\d+$/.test(code)) {
|
|
142
|
+
throw new ValidationError("OTP code must be 6 digits", "code");
|
|
143
|
+
}
|
|
144
|
+
return this.request("/auth/otp/verify", "POST", { email, code });
|
|
145
|
+
},
|
|
146
|
+
/**
|
|
147
|
+
* Verify email with token
|
|
148
|
+
*/
|
|
149
|
+
verifyEmail: async (token) => {
|
|
150
|
+
if (!token) {
|
|
151
|
+
throw new ValidationError("Verification token is required", "token");
|
|
152
|
+
}
|
|
153
|
+
return this.request(`/auth/verify-email?token=${token}`, "GET");
|
|
154
|
+
},
|
|
155
|
+
/**
|
|
156
|
+
* Request password reset email
|
|
157
|
+
*/
|
|
158
|
+
requestPasswordReset: async (email) => {
|
|
159
|
+
if (!email || !email.includes("@")) {
|
|
160
|
+
throw new ValidationError("Invalid email address", "email");
|
|
161
|
+
}
|
|
162
|
+
return this.request("/auth/password-reset/request", "POST", { email });
|
|
163
|
+
},
|
|
164
|
+
/**
|
|
165
|
+
* Reset password with token
|
|
166
|
+
*/
|
|
167
|
+
resetPassword: async (token, newPassword) => {
|
|
168
|
+
if (!token) {
|
|
169
|
+
throw new ValidationError("Reset token is required", "token");
|
|
170
|
+
}
|
|
171
|
+
if (!newPassword || newPassword.length < 8) {
|
|
172
|
+
throw new ValidationError(
|
|
173
|
+
"Password must be at least 8 characters",
|
|
174
|
+
"password",
|
|
175
|
+
"Use a stronger password"
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
return this.request("/auth/password-reset/confirm", "POST", { token, newPassword });
|
|
179
|
+
},
|
|
180
|
+
/**
|
|
181
|
+
* Refresh access token using refresh token
|
|
182
|
+
*/
|
|
183
|
+
refreshToken: async (refreshToken) => {
|
|
184
|
+
if (!refreshToken) {
|
|
185
|
+
throw new ValidationError("Refresh token is required", "refreshToken");
|
|
186
|
+
}
|
|
187
|
+
return this.request("/auth/refresh", "POST", { refreshToken });
|
|
188
|
+
},
|
|
189
|
+
/**
|
|
190
|
+
* Logout and invalidate tokens
|
|
191
|
+
*/
|
|
192
|
+
logout: async (token) => {
|
|
193
|
+
return this.request("/auth/logout", "POST", {}, token);
|
|
194
|
+
},
|
|
195
|
+
/**
|
|
196
|
+
* Get current user profile
|
|
197
|
+
*/
|
|
198
|
+
getCurrentUser: async (token) => {
|
|
199
|
+
if (!token) {
|
|
200
|
+
throw new AuthenticationError(
|
|
201
|
+
"AUTH_TOKEN_INVALID" /* AUTH_TOKEN_INVALID */,
|
|
202
|
+
"Authentication token is required",
|
|
203
|
+
{ suggestion: "Please login first" }
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
const response = await this.request("/auth/me", "GET", void 0, token);
|
|
207
|
+
return response.user;
|
|
208
|
+
},
|
|
209
|
+
/**
|
|
210
|
+
* Update user profile
|
|
211
|
+
*/
|
|
212
|
+
updateProfile: async (token, updates) => {
|
|
213
|
+
if (!token) {
|
|
214
|
+
throw new AuthenticationError(
|
|
215
|
+
"AUTH_TOKEN_INVALID" /* AUTH_TOKEN_INVALID */,
|
|
216
|
+
"Authentication token is required",
|
|
217
|
+
{ suggestion: "Please login first" }
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
const response = await this.request("/auth/me", "PATCH", updates, token);
|
|
221
|
+
return response.user;
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Make HTTP request with comprehensive error handling
|
|
227
|
+
*/
|
|
228
|
+
async request(path, method, body, token) {
|
|
229
|
+
const url = `${this.baseUrl}/api/v1/public/projects/${this.projectSlug}${path}`;
|
|
230
|
+
const options = {
|
|
231
|
+
method,
|
|
232
|
+
headers: {
|
|
233
|
+
"Content-Type": "application/json",
|
|
234
|
+
...token && { Authorization: `Bearer ${token}` }
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
if (body) {
|
|
238
|
+
options.body = JSON.stringify(body);
|
|
239
|
+
}
|
|
240
|
+
try {
|
|
241
|
+
const response = await fetch(url, options);
|
|
242
|
+
const data = await response.json();
|
|
243
|
+
if (!response.ok) {
|
|
244
|
+
const errorCode = this.mapErrorCode(data.code, response.status);
|
|
245
|
+
const errorMessage = data.message || data.error || "Request failed";
|
|
246
|
+
throw new ClientError(
|
|
247
|
+
errorCode,
|
|
248
|
+
errorMessage,
|
|
249
|
+
{
|
|
250
|
+
suggestion: this.getSuggestion(errorCode, data),
|
|
251
|
+
field: data.field
|
|
252
|
+
},
|
|
253
|
+
response.status
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
return data;
|
|
257
|
+
} catch (err) {
|
|
258
|
+
if (err instanceof ClientError) {
|
|
259
|
+
throw err;
|
|
260
|
+
}
|
|
261
|
+
if (err.name === "TypeError" && err.message.includes("fetch")) {
|
|
262
|
+
throw new NetworkError("Network request failed", "Check your internet connection");
|
|
263
|
+
}
|
|
264
|
+
if (err.name === "AbortError") {
|
|
265
|
+
throw new NetworkError("Request timeout", "The request took too long. Please try again");
|
|
266
|
+
}
|
|
267
|
+
throw new ClientError(
|
|
268
|
+
"UNKNOWN_ERROR" /* UNKNOWN_ERROR */,
|
|
269
|
+
err.message || "An unexpected error occurred",
|
|
270
|
+
{
|
|
271
|
+
suggestion: "Please try again or contact support"
|
|
272
|
+
}
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* Map API error codes to client error codes
|
|
278
|
+
*/
|
|
279
|
+
mapErrorCode(apiCode, statusCode) {
|
|
280
|
+
if (apiCode) {
|
|
281
|
+
if (Object.values(ClientErrorCode).includes(apiCode)) {
|
|
282
|
+
return apiCode;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
switch (statusCode) {
|
|
286
|
+
case 401:
|
|
287
|
+
return "AUTH_INVALID_CREDENTIALS" /* AUTH_INVALID_CREDENTIALS */;
|
|
288
|
+
case 409:
|
|
289
|
+
return "AUTH_USER_EXISTS" /* AUTH_USER_EXISTS */;
|
|
290
|
+
case 400:
|
|
291
|
+
return "VALIDATION_ERROR" /* VALIDATION_ERROR */;
|
|
292
|
+
case 403:
|
|
293
|
+
return "AUTH_TOKEN_INVALID" /* AUTH_TOKEN_INVALID */;
|
|
294
|
+
default:
|
|
295
|
+
return "REQUEST_FAILED" /* REQUEST_FAILED */;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Get helpful suggestion based on error code
|
|
300
|
+
*/
|
|
301
|
+
getSuggestion(errorCode, data) {
|
|
302
|
+
switch (errorCode) {
|
|
303
|
+
case "AUTH_INVALID_CREDENTIALS" /* AUTH_INVALID_CREDENTIALS */:
|
|
304
|
+
return "Double-check your email and password";
|
|
305
|
+
case "AUTH_USER_EXISTS" /* AUTH_USER_EXISTS */:
|
|
306
|
+
return "Try logging in instead, or use password reset if you forgot your password";
|
|
307
|
+
case "AUTH_EMAIL_NOT_VERIFIED" /* AUTH_EMAIL_NOT_VERIFIED */:
|
|
308
|
+
return "Check your email for verification link";
|
|
309
|
+
case "AUTH_TOKEN_EXPIRED" /* AUTH_TOKEN_EXPIRED */:
|
|
310
|
+
return "Your session has expired. Please login again";
|
|
311
|
+
case "AUTH_OTP_EXPIRED" /* AUTH_OTP_EXPIRED */:
|
|
312
|
+
return "Request a new OTP code";
|
|
313
|
+
case "AUTH_OTP_INVALID" /* AUTH_OTP_INVALID */:
|
|
314
|
+
return "Check the code and try again, or request a new one";
|
|
315
|
+
case "AUTH_PASSWORD_WEAK" /* AUTH_PASSWORD_WEAK */:
|
|
316
|
+
return "Use at least 8 characters with a mix of letters, numbers, and symbols";
|
|
317
|
+
case "NETWORK_ERROR" /* NETWORK_ERROR */:
|
|
318
|
+
return "Check your internet connection and try again";
|
|
319
|
+
default:
|
|
320
|
+
return data?.details?.suggestion || "Please try again or contact support";
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
// src/server.ts
|
|
326
|
+
import { Pool } from "@neondatabase/serverless";
|
|
327
|
+
|
|
328
|
+
// src/server-errors.ts
|
|
329
|
+
var ErrorCode = /* @__PURE__ */ ((ErrorCode2) => {
|
|
330
|
+
ErrorCode2["DB_CONNECTION_FAILED"] = "DB_CONNECTION_FAILED";
|
|
331
|
+
ErrorCode2["DB_QUERY_FAILED"] = "DB_QUERY_FAILED";
|
|
332
|
+
ErrorCode2["DB_TABLE_NOT_FOUND"] = "DB_TABLE_NOT_FOUND";
|
|
333
|
+
ErrorCode2["DB_COLUMN_NOT_FOUND"] = "DB_COLUMN_NOT_FOUND";
|
|
334
|
+
ErrorCode2["DB_AUTH_FAILED"] = "DB_AUTH_FAILED";
|
|
335
|
+
ErrorCode2["DB_MIGRATION_FAILED"] = "DB_MIGRATION_FAILED";
|
|
336
|
+
ErrorCode2["DB_TRANSACTION_FAILED"] = "DB_TRANSACTION_FAILED";
|
|
337
|
+
ErrorCode2["CACHE_GET_FAILED"] = "CACHE_GET_FAILED";
|
|
338
|
+
ErrorCode2["CACHE_SET_FAILED"] = "CACHE_SET_FAILED";
|
|
339
|
+
ErrorCode2["CACHE_DELETE_FAILED"] = "CACHE_DELETE_FAILED";
|
|
340
|
+
ErrorCode2["CACHE_NOT_CONFIGURED"] = "CACHE_NOT_CONFIGURED";
|
|
341
|
+
ErrorCode2["QUEUE_ENQUEUE_FAILED"] = "QUEUE_ENQUEUE_FAILED";
|
|
342
|
+
ErrorCode2["QUEUE_NOT_CONFIGURED"] = "QUEUE_NOT_CONFIGURED";
|
|
343
|
+
ErrorCode2["QUEUE_JOB_NOT_FOUND"] = "QUEUE_JOB_NOT_FOUND";
|
|
344
|
+
ErrorCode2["STORAGE_UPLOAD_FAILED"] = "STORAGE_UPLOAD_FAILED";
|
|
345
|
+
ErrorCode2["STORAGE_DELETE_FAILED"] = "STORAGE_DELETE_FAILED";
|
|
346
|
+
ErrorCode2["STORAGE_NOT_CONFIGURED"] = "STORAGE_NOT_CONFIGURED";
|
|
347
|
+
ErrorCode2["STORAGE_FILE_TOO_LARGE"] = "STORAGE_FILE_TOO_LARGE";
|
|
348
|
+
ErrorCode2["AI_REQUEST_FAILED"] = "AI_REQUEST_FAILED";
|
|
349
|
+
ErrorCode2["AI_NOT_CONFIGURED"] = "AI_NOT_CONFIGURED";
|
|
350
|
+
ErrorCode2["AI_RATE_LIMIT"] = "AI_RATE_LIMIT";
|
|
351
|
+
ErrorCode2["SERVICE_INVOKE_FAILED"] = "SERVICE_INVOKE_FAILED";
|
|
352
|
+
ErrorCode2["SERVICE_NOT_FOUND"] = "SERVICE_NOT_FOUND";
|
|
353
|
+
ErrorCode2["CONFIGURATION_ERROR"] = "CONFIGURATION_ERROR";
|
|
354
|
+
ErrorCode2["VALIDATION_ERROR"] = "VALIDATION_ERROR";
|
|
355
|
+
ErrorCode2["INTERNAL_ERROR"] = "INTERNAL_ERROR";
|
|
356
|
+
return ErrorCode2;
|
|
357
|
+
})(ErrorCode || {});
|
|
358
|
+
var ServerError = class _ServerError extends Error {
|
|
359
|
+
constructor(code, message, details, context) {
|
|
360
|
+
super(message);
|
|
361
|
+
this.code = code;
|
|
362
|
+
this.details = details;
|
|
363
|
+
this.context = context;
|
|
364
|
+
this.name = "ServerError";
|
|
365
|
+
Object.setPrototypeOf(this, _ServerError.prototype);
|
|
366
|
+
}
|
|
367
|
+
toJSON() {
|
|
368
|
+
return {
|
|
369
|
+
name: this.name,
|
|
370
|
+
code: this.code,
|
|
371
|
+
message: this.message,
|
|
372
|
+
details: this.details,
|
|
373
|
+
context: this.context
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
};
|
|
377
|
+
var DatabaseError = class _DatabaseError extends ServerError {
|
|
378
|
+
constructor(code, message, details, context) {
|
|
379
|
+
super(code, message, details, context);
|
|
380
|
+
this.name = "DatabaseError";
|
|
381
|
+
Object.setPrototypeOf(this, _DatabaseError.prototype);
|
|
382
|
+
}
|
|
383
|
+
/**
|
|
384
|
+
* Create error from Postgres error
|
|
385
|
+
*/
|
|
386
|
+
static fromPostgresError(err, context) {
|
|
387
|
+
switch (err.code) {
|
|
388
|
+
case "42P01":
|
|
389
|
+
return new _DatabaseError(
|
|
390
|
+
"DB_TABLE_NOT_FOUND" /* DB_TABLE_NOT_FOUND */,
|
|
391
|
+
`Table does not exist: ${err.table || "unknown"}`,
|
|
392
|
+
{
|
|
393
|
+
suggestion: "Run migrations first: aerostack db migrate apply",
|
|
394
|
+
recoveryAction: "CREATE_TABLE",
|
|
395
|
+
cause: err.message
|
|
396
|
+
},
|
|
397
|
+
context
|
|
398
|
+
);
|
|
399
|
+
case "42703":
|
|
400
|
+
return new _DatabaseError(
|
|
401
|
+
"DB_COLUMN_NOT_FOUND" /* DB_COLUMN_NOT_FOUND */,
|
|
402
|
+
`Column does not exist: ${err.column || "unknown"}`,
|
|
403
|
+
{
|
|
404
|
+
suggestion: "Check your schema or run latest migrations",
|
|
405
|
+
recoveryAction: "ALTER_TABLE",
|
|
406
|
+
cause: err.message
|
|
407
|
+
},
|
|
408
|
+
context
|
|
409
|
+
);
|
|
410
|
+
case "28P01":
|
|
411
|
+
// invalid_password
|
|
412
|
+
case "28000":
|
|
413
|
+
return new _DatabaseError(
|
|
414
|
+
"DB_AUTH_FAILED" /* DB_AUTH_FAILED */,
|
|
415
|
+
"Database authentication failed",
|
|
416
|
+
{
|
|
417
|
+
suggestion: "Check your DATABASE_URL environment variable",
|
|
418
|
+
recoveryAction: "UPDATE_CREDENTIALS",
|
|
419
|
+
cause: err.message
|
|
420
|
+
},
|
|
421
|
+
context
|
|
422
|
+
);
|
|
423
|
+
case "08006":
|
|
424
|
+
// connection_failure
|
|
425
|
+
case "08003":
|
|
426
|
+
return new _DatabaseError(
|
|
427
|
+
"DB_CONNECTION_FAILED" /* DB_CONNECTION_FAILED */,
|
|
428
|
+
"Failed to connect to database",
|
|
429
|
+
{
|
|
430
|
+
suggestion: "Verify your database connection string and network connectivity",
|
|
431
|
+
recoveryAction: "CHECK_CONNECTION",
|
|
432
|
+
cause: err.message
|
|
433
|
+
},
|
|
434
|
+
context
|
|
435
|
+
);
|
|
436
|
+
default:
|
|
437
|
+
return new _DatabaseError(
|
|
438
|
+
"DB_QUERY_FAILED" /* DB_QUERY_FAILED */,
|
|
439
|
+
err.message || "Database query failed",
|
|
440
|
+
{
|
|
441
|
+
suggestion: "Check your query syntax and parameters",
|
|
442
|
+
cause: err.code ? `Postgres error ${err.code}` : void 0
|
|
443
|
+
},
|
|
444
|
+
context
|
|
445
|
+
);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
/**
|
|
449
|
+
* Create error from D1 error
|
|
450
|
+
*/
|
|
451
|
+
static fromD1Error(err, context) {
|
|
452
|
+
const message = err.message || "D1 query failed";
|
|
453
|
+
if (message.includes("no such table")) {
|
|
454
|
+
return new _DatabaseError(
|
|
455
|
+
"DB_TABLE_NOT_FOUND" /* DB_TABLE_NOT_FOUND */,
|
|
456
|
+
message,
|
|
457
|
+
{
|
|
458
|
+
suggestion: "Run D1 migrations: aerostack db migrate apply",
|
|
459
|
+
recoveryAction: "CREATE_TABLE",
|
|
460
|
+
cause: err.message
|
|
461
|
+
},
|
|
462
|
+
context
|
|
463
|
+
);
|
|
464
|
+
}
|
|
465
|
+
if (message.includes("no such column")) {
|
|
466
|
+
return new _DatabaseError(
|
|
467
|
+
"DB_COLUMN_NOT_FOUND" /* DB_COLUMN_NOT_FOUND */,
|
|
468
|
+
message,
|
|
469
|
+
{
|
|
470
|
+
suggestion: "Check your schema or update migrations",
|
|
471
|
+
recoveryAction: "ALTER_TABLE",
|
|
472
|
+
cause: err.message
|
|
473
|
+
},
|
|
474
|
+
context
|
|
475
|
+
);
|
|
476
|
+
}
|
|
477
|
+
return new _DatabaseError(
|
|
478
|
+
"DB_QUERY_FAILED" /* DB_QUERY_FAILED */,
|
|
479
|
+
message,
|
|
480
|
+
{
|
|
481
|
+
suggestion: "Check your SQL syntax and D1 configuration",
|
|
482
|
+
cause: err.message
|
|
483
|
+
},
|
|
484
|
+
context
|
|
485
|
+
);
|
|
486
|
+
}
|
|
487
|
+
};
|
|
488
|
+
var CacheError = class _CacheError extends ServerError {
|
|
489
|
+
constructor(code, message, details, context) {
|
|
490
|
+
super(code, message, details, context);
|
|
491
|
+
this.name = "CacheError";
|
|
492
|
+
Object.setPrototypeOf(this, _CacheError.prototype);
|
|
493
|
+
}
|
|
494
|
+
};
|
|
495
|
+
var QueueError = class _QueueError extends ServerError {
|
|
496
|
+
constructor(code, message, details, context) {
|
|
497
|
+
super(code, message, details, context);
|
|
498
|
+
this.name = "QueueError";
|
|
499
|
+
Object.setPrototypeOf(this, _QueueError.prototype);
|
|
500
|
+
}
|
|
501
|
+
};
|
|
502
|
+
var StorageError = class _StorageError extends ServerError {
|
|
503
|
+
constructor(code, message, details, context) {
|
|
504
|
+
super(code, message, details, context);
|
|
505
|
+
this.name = "StorageError";
|
|
506
|
+
Object.setPrototypeOf(this, _StorageError.prototype);
|
|
507
|
+
}
|
|
508
|
+
};
|
|
509
|
+
var AIError = class _AIError extends ServerError {
|
|
510
|
+
constructor(code, message, details, context) {
|
|
511
|
+
super(code, message, details, context);
|
|
512
|
+
this.name = "AIError";
|
|
513
|
+
Object.setPrototypeOf(this, _AIError.prototype);
|
|
514
|
+
}
|
|
515
|
+
};
|
|
516
|
+
var ServiceError = class _ServiceError extends ServerError {
|
|
517
|
+
constructor(code, message, details, context) {
|
|
518
|
+
super(code, message, details, context);
|
|
519
|
+
this.name = "ServiceError";
|
|
520
|
+
Object.setPrototypeOf(this, _ServiceError.prototype);
|
|
521
|
+
}
|
|
522
|
+
};
|
|
523
|
+
|
|
524
|
+
// src/server.ts
|
|
525
|
+
var AerostackServer = class {
|
|
526
|
+
pgPool;
|
|
527
|
+
_d1;
|
|
528
|
+
_kv;
|
|
529
|
+
_queue;
|
|
530
|
+
_storage;
|
|
531
|
+
_ai;
|
|
532
|
+
_dispatcher;
|
|
533
|
+
routingRules;
|
|
534
|
+
env;
|
|
535
|
+
constructor(env, options = {}) {
|
|
536
|
+
this.env = env;
|
|
537
|
+
this._d1 = env.DB;
|
|
538
|
+
this._kv = env.CACHE;
|
|
539
|
+
this._queue = env.QUEUE;
|
|
540
|
+
this._storage = env.STORAGE;
|
|
541
|
+
this._ai = env.AI;
|
|
542
|
+
this._dispatcher = env.DISPATCHER;
|
|
543
|
+
this.routingRules = options.routing || { tables: {} };
|
|
544
|
+
const pgConnStr = this.findPostgresConnStr(env);
|
|
545
|
+
if (pgConnStr) {
|
|
546
|
+
this.pgPool = new Pool({ connectionString: pgConnStr });
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
/**
|
|
550
|
+
* Database operations with intelligent routing between D1 and Postgres
|
|
551
|
+
*/
|
|
552
|
+
get db() {
|
|
553
|
+
return {
|
|
554
|
+
/**
|
|
555
|
+
* Execute a SQL query with automatic routing
|
|
556
|
+
*/
|
|
557
|
+
query: async (sql, params = []) => {
|
|
558
|
+
return this.routeQuery(sql, params);
|
|
559
|
+
},
|
|
560
|
+
/**
|
|
561
|
+
* Get database schema information
|
|
562
|
+
*/
|
|
563
|
+
getSchema: async (binding) => {
|
|
564
|
+
if (binding) {
|
|
565
|
+
if (this.pgPool && binding.toLowerCase().includes("postgres")) {
|
|
566
|
+
return this.introspectPostgres();
|
|
567
|
+
}
|
|
568
|
+
if (this._d1) {
|
|
569
|
+
return this.introspectD1();
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
if (this._d1) {
|
|
573
|
+
return this.introspectD1();
|
|
574
|
+
}
|
|
575
|
+
if (this.pgPool) {
|
|
576
|
+
return this.introspectPostgres();
|
|
577
|
+
}
|
|
578
|
+
throw new DatabaseError(
|
|
579
|
+
"DB_CONNECTION_FAILED" /* DB_CONNECTION_FAILED */,
|
|
580
|
+
"No database connection available",
|
|
581
|
+
{
|
|
582
|
+
suggestion: "Configure DB or Postgres connection in aerostack.toml"
|
|
583
|
+
}
|
|
584
|
+
);
|
|
585
|
+
},
|
|
586
|
+
/**
|
|
587
|
+
* Execute multiple queries in a batch
|
|
588
|
+
*/
|
|
589
|
+
batch: async (queries) => {
|
|
590
|
+
const results = [];
|
|
591
|
+
const errors = [];
|
|
592
|
+
let success = true;
|
|
593
|
+
for (let i = 0; i < queries.length; i++) {
|
|
594
|
+
try {
|
|
595
|
+
const result = await this.routeQuery(queries[i].sql, queries[i].params || []);
|
|
596
|
+
results.push(result);
|
|
597
|
+
} catch (error) {
|
|
598
|
+
success = false;
|
|
599
|
+
errors.push({ index: i, error });
|
|
600
|
+
results.push({
|
|
601
|
+
results: [],
|
|
602
|
+
success: false,
|
|
603
|
+
meta: { target: "d1" }
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
return { results, success, errors: errors.length > 0 ? errors : void 0 };
|
|
608
|
+
}
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
/**
|
|
612
|
+
* KV Cache operations
|
|
613
|
+
*/
|
|
614
|
+
get cache() {
|
|
615
|
+
return {
|
|
616
|
+
/**
|
|
617
|
+
* Get value from cache
|
|
618
|
+
*/
|
|
619
|
+
get: async (key, options) => {
|
|
620
|
+
if (!this._kv) {
|
|
621
|
+
throw new CacheError(
|
|
622
|
+
"CACHE_NOT_CONFIGURED" /* CACHE_NOT_CONFIGURED */,
|
|
623
|
+
"KV cache not configured",
|
|
624
|
+
{
|
|
625
|
+
suggestion: "Add [[kv_namespaces]] binding to aerostack.toml"
|
|
626
|
+
}
|
|
627
|
+
);
|
|
628
|
+
}
|
|
629
|
+
try {
|
|
630
|
+
const type = options?.type || "json";
|
|
631
|
+
const value = type === "json" ? await this._kv.get(key, "json") : type === "text" ? await this._kv.get(key, "text") : type === "arrayBuffer" ? await this._kv.get(key, "arrayBuffer") : await this._kv.get(key, "stream");
|
|
632
|
+
return value;
|
|
633
|
+
} catch (err) {
|
|
634
|
+
throw new CacheError(
|
|
635
|
+
"CACHE_GET_FAILED" /* CACHE_GET_FAILED */,
|
|
636
|
+
`Failed to get cache key: ${key}`,
|
|
637
|
+
{
|
|
638
|
+
suggestion: "Check key format and KV namespace binding",
|
|
639
|
+
cause: err.message
|
|
640
|
+
},
|
|
641
|
+
{ key }
|
|
642
|
+
);
|
|
643
|
+
}
|
|
644
|
+
},
|
|
645
|
+
/**
|
|
646
|
+
* Set value in cache
|
|
647
|
+
*/
|
|
648
|
+
set: async (key, value, options) => {
|
|
649
|
+
if (!this._kv) {
|
|
650
|
+
throw new CacheError(
|
|
651
|
+
"CACHE_NOT_CONFIGURED" /* CACHE_NOT_CONFIGURED */,
|
|
652
|
+
"KV cache not configured",
|
|
653
|
+
{
|
|
654
|
+
suggestion: "Add [[kv_namespaces]] binding to aerostack.toml"
|
|
655
|
+
}
|
|
656
|
+
);
|
|
657
|
+
}
|
|
658
|
+
try {
|
|
659
|
+
const ttl = options?.ttl || options?.expirationTtl;
|
|
660
|
+
await this._kv.put(key, JSON.stringify(value), ttl ? { expirationTtl: ttl } : void 0);
|
|
661
|
+
} catch (err) {
|
|
662
|
+
throw new CacheError(
|
|
663
|
+
"CACHE_SET_FAILED" /* CACHE_SET_FAILED */,
|
|
664
|
+
`Failed to set cache key: ${key}`,
|
|
665
|
+
{
|
|
666
|
+
suggestion: "Check value serialization and KV limits (25MB)",
|
|
667
|
+
cause: err.message
|
|
668
|
+
},
|
|
669
|
+
{ key, value }
|
|
670
|
+
);
|
|
671
|
+
}
|
|
672
|
+
},
|
|
673
|
+
/**
|
|
674
|
+
* Delete value from cache
|
|
675
|
+
*/
|
|
676
|
+
delete: async (key) => {
|
|
677
|
+
if (!this._kv) {
|
|
678
|
+
throw new CacheError(
|
|
679
|
+
"CACHE_NOT_CONFIGURED" /* CACHE_NOT_CONFIGURED */,
|
|
680
|
+
"KV cache not configured",
|
|
681
|
+
{
|
|
682
|
+
suggestion: "Add [[kv_namespaces]] binding to aerostack.toml"
|
|
683
|
+
}
|
|
684
|
+
);
|
|
685
|
+
}
|
|
686
|
+
try {
|
|
687
|
+
await this._kv.delete(key);
|
|
688
|
+
} catch (err) {
|
|
689
|
+
throw new CacheError(
|
|
690
|
+
"CACHE_DELETE_FAILED" /* CACHE_DELETE_FAILED */,
|
|
691
|
+
`Failed to delete cache key: ${key}`,
|
|
692
|
+
{
|
|
693
|
+
cause: err.message
|
|
694
|
+
},
|
|
695
|
+
{ key }
|
|
696
|
+
);
|
|
697
|
+
}
|
|
698
|
+
},
|
|
699
|
+
/**
|
|
700
|
+
* Check if key exists in cache
|
|
701
|
+
*/
|
|
702
|
+
exists: async (key) => {
|
|
703
|
+
if (!this._kv) {
|
|
704
|
+
return false;
|
|
705
|
+
}
|
|
706
|
+
try {
|
|
707
|
+
const value = await this._kv.get(key);
|
|
708
|
+
return value !== null;
|
|
709
|
+
} catch {
|
|
710
|
+
return false;
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
};
|
|
714
|
+
}
|
|
715
|
+
/**
|
|
716
|
+
* Queue operations for background jobs
|
|
717
|
+
*/
|
|
718
|
+
get queue() {
|
|
719
|
+
return {
|
|
720
|
+
/**
|
|
721
|
+
* Add job to queue
|
|
722
|
+
*/
|
|
723
|
+
enqueue: async (job) => {
|
|
724
|
+
if (!this._queue) {
|
|
725
|
+
throw new QueueError(
|
|
726
|
+
"QUEUE_NOT_CONFIGURED" /* QUEUE_NOT_CONFIGURED */,
|
|
727
|
+
"Queue not configured",
|
|
728
|
+
{
|
|
729
|
+
suggestion: "Add [[queues]] binding to aerostack.toml"
|
|
730
|
+
}
|
|
731
|
+
);
|
|
732
|
+
}
|
|
733
|
+
try {
|
|
734
|
+
const message = {
|
|
735
|
+
type: job.type,
|
|
736
|
+
data: job.data,
|
|
737
|
+
queuedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
738
|
+
};
|
|
739
|
+
await this._queue.send(message, {
|
|
740
|
+
delaySeconds: job.delay
|
|
741
|
+
});
|
|
742
|
+
const jobId = `job_${Date.now()}_${Math.random().toString(36).substring(7)}`;
|
|
743
|
+
return {
|
|
744
|
+
jobId,
|
|
745
|
+
status: "queued",
|
|
746
|
+
queuedAt: /* @__PURE__ */ new Date()
|
|
747
|
+
};
|
|
748
|
+
} catch (err) {
|
|
749
|
+
throw new QueueError(
|
|
750
|
+
"QUEUE_ENQUEUE_FAILED" /* QUEUE_ENQUEUE_FAILED */,
|
|
751
|
+
"Failed to enqueue job",
|
|
752
|
+
{
|
|
753
|
+
suggestion: "Check queue binding and message size limits",
|
|
754
|
+
cause: err.message
|
|
755
|
+
},
|
|
756
|
+
{ job }
|
|
757
|
+
);
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
};
|
|
761
|
+
}
|
|
762
|
+
/**
|
|
763
|
+
* R2 Storage operations
|
|
764
|
+
*/
|
|
765
|
+
get storage() {
|
|
766
|
+
return {
|
|
767
|
+
/**
|
|
768
|
+
* Upload file to R2 storage
|
|
769
|
+
*/
|
|
770
|
+
upload: async (file, key, options) => {
|
|
771
|
+
if (!this._storage) {
|
|
772
|
+
throw new StorageError(
|
|
773
|
+
"STORAGE_NOT_CONFIGURED" /* STORAGE_NOT_CONFIGURED */,
|
|
774
|
+
"R2 storage not configured",
|
|
775
|
+
{
|
|
776
|
+
suggestion: "Add [[r2_buckets]] binding to aerostack.toml"
|
|
777
|
+
}
|
|
778
|
+
);
|
|
779
|
+
}
|
|
780
|
+
try {
|
|
781
|
+
await this._storage.put(key, file, {
|
|
782
|
+
httpMetadata: {
|
|
783
|
+
contentType: options?.contentType,
|
|
784
|
+
cacheControl: options?.cacheControl
|
|
785
|
+
},
|
|
786
|
+
customMetadata: options?.metadata
|
|
787
|
+
});
|
|
788
|
+
const obj = await this._storage.get(key);
|
|
789
|
+
const size = obj?.size || 0;
|
|
790
|
+
return {
|
|
791
|
+
key,
|
|
792
|
+
url: `https://${this.env.STORAGE_PUBLIC_URL || "storage.aerostack.ai"}/${key}`,
|
|
793
|
+
size,
|
|
794
|
+
contentType: options?.contentType || "application/octet-stream"
|
|
795
|
+
};
|
|
796
|
+
} catch (err) {
|
|
797
|
+
throw new StorageError(
|
|
798
|
+
"STORAGE_UPLOAD_FAILED" /* STORAGE_UPLOAD_FAILED */,
|
|
799
|
+
`Failed to upload file: ${key}`,
|
|
800
|
+
{
|
|
801
|
+
suggestion: "Check R2 bucket binding and file size limits",
|
|
802
|
+
cause: err.message
|
|
803
|
+
},
|
|
804
|
+
{ key }
|
|
805
|
+
);
|
|
806
|
+
}
|
|
807
|
+
},
|
|
808
|
+
/**
|
|
809
|
+
* Get presigned URL for object
|
|
810
|
+
*/
|
|
811
|
+
getUrl: async (key, options) => {
|
|
812
|
+
if (!this._storage) {
|
|
813
|
+
throw new StorageError(
|
|
814
|
+
"STORAGE_NOT_CONFIGURED" /* STORAGE_NOT_CONFIGURED */,
|
|
815
|
+
"R2 storage not configured",
|
|
816
|
+
{
|
|
817
|
+
suggestion: "Add [[r2_buckets]] binding to aerostack.toml"
|
|
818
|
+
}
|
|
819
|
+
);
|
|
820
|
+
}
|
|
821
|
+
return `https://${this.env.STORAGE_PUBLIC_URL || "storage.aerostack.ai"}/${key}`;
|
|
822
|
+
},
|
|
823
|
+
/**
|
|
824
|
+
* Delete object from storage
|
|
825
|
+
*/
|
|
826
|
+
delete: async (key) => {
|
|
827
|
+
if (!this._storage) {
|
|
828
|
+
throw new StorageError(
|
|
829
|
+
"STORAGE_NOT_CONFIGURED" /* STORAGE_NOT_CONFIGURED */,
|
|
830
|
+
"R2 storage not configured",
|
|
831
|
+
{
|
|
832
|
+
suggestion: "Add [[r2_buckets]] binding to aerostack.toml"
|
|
833
|
+
}
|
|
834
|
+
);
|
|
835
|
+
}
|
|
836
|
+
try {
|
|
837
|
+
await this._storage.delete(key);
|
|
838
|
+
} catch (err) {
|
|
839
|
+
throw new StorageError(
|
|
840
|
+
"STORAGE_DELETE_FAILED" /* STORAGE_DELETE_FAILED */,
|
|
841
|
+
`Failed to delete file: ${key}`,
|
|
842
|
+
{
|
|
843
|
+
cause: err.message
|
|
844
|
+
},
|
|
845
|
+
{ key }
|
|
846
|
+
);
|
|
847
|
+
}
|
|
848
|
+
},
|
|
849
|
+
/**
|
|
850
|
+
* List objects in storage
|
|
851
|
+
*/
|
|
852
|
+
list: async (prefix) => {
|
|
853
|
+
if (!this._storage) {
|
|
854
|
+
throw new StorageError(
|
|
855
|
+
"STORAGE_NOT_CONFIGURED" /* STORAGE_NOT_CONFIGURED */,
|
|
856
|
+
"R2 storage not configured",
|
|
857
|
+
{
|
|
858
|
+
suggestion: "Add [[r2_buckets]] binding to aerostack.toml"
|
|
859
|
+
}
|
|
860
|
+
);
|
|
861
|
+
}
|
|
862
|
+
try {
|
|
863
|
+
const listed = await this._storage.list({ prefix });
|
|
864
|
+
return listed.objects.map((obj) => ({
|
|
865
|
+
key: obj.key,
|
|
866
|
+
size: obj.size,
|
|
867
|
+
uploaded: obj.uploaded,
|
|
868
|
+
contentType: obj.httpMetadata?.contentType
|
|
869
|
+
}));
|
|
870
|
+
} catch (err) {
|
|
871
|
+
throw new StorageError(
|
|
872
|
+
"STORAGE_DELETE_FAILED" /* STORAGE_DELETE_FAILED */,
|
|
873
|
+
"Failed to list storage objects",
|
|
874
|
+
{
|
|
875
|
+
cause: err.message
|
|
876
|
+
},
|
|
877
|
+
{ prefix }
|
|
878
|
+
);
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
};
|
|
882
|
+
}
|
|
883
|
+
/**
|
|
884
|
+
* AI operations using Cloudflare AI
|
|
885
|
+
*/
|
|
886
|
+
get ai() {
|
|
887
|
+
return {
|
|
888
|
+
/**
|
|
889
|
+
* Generate chat completion
|
|
890
|
+
*/
|
|
891
|
+
chat: async (messages, options) => {
|
|
892
|
+
if (!this._ai) {
|
|
893
|
+
throw new AIError(
|
|
894
|
+
"AI_NOT_CONFIGURED" /* AI_NOT_CONFIGURED */,
|
|
895
|
+
"AI binding not configured",
|
|
896
|
+
{
|
|
897
|
+
suggestion: "AI binding is automatically available in Workers"
|
|
898
|
+
}
|
|
899
|
+
);
|
|
900
|
+
}
|
|
901
|
+
try {
|
|
902
|
+
const model = options?.model || "@cf/meta/llama-3-8b-instruct";
|
|
903
|
+
const result = await this._ai.run(model, {
|
|
904
|
+
messages,
|
|
905
|
+
temperature: options?.temperature,
|
|
906
|
+
max_tokens: options?.maxTokens,
|
|
907
|
+
stream: options?.stream || false
|
|
908
|
+
});
|
|
909
|
+
return {
|
|
910
|
+
response: result.response || "",
|
|
911
|
+
usage: result.usage
|
|
912
|
+
};
|
|
913
|
+
} catch (err) {
|
|
914
|
+
throw new AIError(
|
|
915
|
+
"AI_REQUEST_FAILED" /* AI_REQUEST_FAILED */,
|
|
916
|
+
"AI chat request failed",
|
|
917
|
+
{
|
|
918
|
+
suggestion: "Check model name and message format",
|
|
919
|
+
cause: err.message
|
|
920
|
+
},
|
|
921
|
+
{ messages, options }
|
|
922
|
+
);
|
|
923
|
+
}
|
|
924
|
+
},
|
|
925
|
+
/**
|
|
926
|
+
* Generate text embeddings
|
|
927
|
+
*/
|
|
928
|
+
embed: async (text, options) => {
|
|
929
|
+
if (!this._ai) {
|
|
930
|
+
throw new AIError(
|
|
931
|
+
"AI_NOT_CONFIGURED" /* AI_NOT_CONFIGURED */,
|
|
932
|
+
"AI binding not configured",
|
|
933
|
+
{
|
|
934
|
+
suggestion: "AI binding is automatically available in Workers"
|
|
935
|
+
}
|
|
936
|
+
);
|
|
937
|
+
}
|
|
938
|
+
try {
|
|
939
|
+
const model = options?.model || "@cf/baai/bge-base-en-v1.5";
|
|
940
|
+
const result = await this._ai.run(model, { text });
|
|
941
|
+
return {
|
|
942
|
+
embedding: result.data[0],
|
|
943
|
+
model
|
|
944
|
+
};
|
|
945
|
+
} catch (err) {
|
|
946
|
+
throw new AIError(
|
|
947
|
+
"AI_REQUEST_FAILED" /* AI_REQUEST_FAILED */,
|
|
948
|
+
"AI embedding request failed",
|
|
949
|
+
{
|
|
950
|
+
suggestion: "Check model name and text input",
|
|
951
|
+
cause: err.message
|
|
952
|
+
},
|
|
953
|
+
{ text, options }
|
|
954
|
+
);
|
|
955
|
+
}
|
|
956
|
+
},
|
|
957
|
+
/**
|
|
958
|
+
* Generate text from prompt
|
|
959
|
+
*/
|
|
960
|
+
generate: async (prompt, options) => {
|
|
961
|
+
if (!this._ai) {
|
|
962
|
+
throw new AIError(
|
|
963
|
+
"AI_NOT_CONFIGURED" /* AI_NOT_CONFIGURED */,
|
|
964
|
+
"AI binding not configured",
|
|
965
|
+
{
|
|
966
|
+
suggestion: "AI binding is automatically available in Workers"
|
|
967
|
+
}
|
|
968
|
+
);
|
|
969
|
+
}
|
|
970
|
+
try {
|
|
971
|
+
const model = options?.model || "@cf/meta/llama-3-8b-instruct";
|
|
972
|
+
const result = await this._ai.run(model, {
|
|
973
|
+
prompt,
|
|
974
|
+
temperature: options?.temperature,
|
|
975
|
+
max_tokens: options?.maxTokens
|
|
976
|
+
});
|
|
977
|
+
return {
|
|
978
|
+
text: result.response || "",
|
|
979
|
+
usage: result.usage
|
|
980
|
+
};
|
|
981
|
+
} catch (err) {
|
|
982
|
+
throw new AIError(
|
|
983
|
+
"AI_REQUEST_FAILED" /* AI_REQUEST_FAILED */,
|
|
984
|
+
"AI generation request failed",
|
|
985
|
+
{
|
|
986
|
+
suggestion: "Check model name and prompt",
|
|
987
|
+
cause: err.message
|
|
988
|
+
},
|
|
989
|
+
{ prompt, options }
|
|
990
|
+
);
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
};
|
|
994
|
+
}
|
|
995
|
+
/**
|
|
996
|
+
* Service invocation via Workers Dispatch
|
|
997
|
+
*/
|
|
998
|
+
get services() {
|
|
999
|
+
return {
|
|
1000
|
+
/**
|
|
1001
|
+
* Invoke another service via RPC
|
|
1002
|
+
*/
|
|
1003
|
+
invoke: async (serviceName, data, options) => {
|
|
1004
|
+
if (!this._dispatcher) {
|
|
1005
|
+
throw new ServiceError(
|
|
1006
|
+
"SERVICE_INVOKE_FAILED" /* SERVICE_INVOKE_FAILED */,
|
|
1007
|
+
"Service dispatcher not configured",
|
|
1008
|
+
{
|
|
1009
|
+
suggestion: "Configure Workers Dispatch namespace in aerostack.toml"
|
|
1010
|
+
}
|
|
1011
|
+
);
|
|
1012
|
+
}
|
|
1013
|
+
try {
|
|
1014
|
+
const id = this._dispatcher.idFromName(serviceName);
|
|
1015
|
+
const stub = this._dispatcher.get(id);
|
|
1016
|
+
const timeout = options?.timeout || 3e4;
|
|
1017
|
+
const controller = new AbortController();
|
|
1018
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
1019
|
+
const response = await stub.fetch(
|
|
1020
|
+
new Request("https://internal/", {
|
|
1021
|
+
method: "POST",
|
|
1022
|
+
body: JSON.stringify(data),
|
|
1023
|
+
signal: controller.signal
|
|
1024
|
+
})
|
|
1025
|
+
);
|
|
1026
|
+
clearTimeout(timeoutId);
|
|
1027
|
+
return await response.json();
|
|
1028
|
+
} catch (err) {
|
|
1029
|
+
throw new ServiceError(
|
|
1030
|
+
"SERVICE_INVOKE_FAILED" /* SERVICE_INVOKE_FAILED */,
|
|
1031
|
+
`Failed to invoke service: ${serviceName}`,
|
|
1032
|
+
{
|
|
1033
|
+
suggestion: "Check service name and dispatcher binding",
|
|
1034
|
+
cause: err.message
|
|
1035
|
+
},
|
|
1036
|
+
{ serviceName, data }
|
|
1037
|
+
);
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
};
|
|
1041
|
+
}
|
|
1042
|
+
// ========== Private Helper Methods ==========
|
|
1043
|
+
async routeQuery(sql, params) {
|
|
1044
|
+
const target = this.determineTarget(sql);
|
|
1045
|
+
if (target === "postgres" && this.pgPool) {
|
|
1046
|
+
try {
|
|
1047
|
+
const result = await this.pgPool.query(sql, params);
|
|
1048
|
+
return {
|
|
1049
|
+
results: result.rows,
|
|
1050
|
+
success: true,
|
|
1051
|
+
meta: { target: "postgres", rowCount: result.rowCount || 0 }
|
|
1052
|
+
};
|
|
1053
|
+
} catch (err) {
|
|
1054
|
+
throw DatabaseError.fromPostgresError(err, { sql, params, target: "postgres" });
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
if (this._d1) {
|
|
1058
|
+
try {
|
|
1059
|
+
const result = await this._d1.prepare(sql).bind(...params).all();
|
|
1060
|
+
return {
|
|
1061
|
+
results: result.results || [],
|
|
1062
|
+
success: result.success,
|
|
1063
|
+
meta: { target: "d1", duration: result.meta?.duration }
|
|
1064
|
+
};
|
|
1065
|
+
} catch (err) {
|
|
1066
|
+
throw DatabaseError.fromD1Error(err, { sql, params, target: "d1" });
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
throw new DatabaseError(
|
|
1070
|
+
"DB_CONNECTION_FAILED" /* DB_CONNECTION_FAILED */,
|
|
1071
|
+
"No database connection available",
|
|
1072
|
+
{
|
|
1073
|
+
suggestion: "Configure DB or Postgres connection in aerostack.toml"
|
|
1074
|
+
}
|
|
1075
|
+
);
|
|
1076
|
+
}
|
|
1077
|
+
determineTarget(sql) {
|
|
1078
|
+
const normalized = sql.toLowerCase();
|
|
1079
|
+
if (normalized.includes("aerostack:target=postgres")) return "postgres";
|
|
1080
|
+
if (normalized.includes("aerostack:target=d1")) return "d1";
|
|
1081
|
+
for (const [table, target] of Object.entries(this.routingRules.tables)) {
|
|
1082
|
+
if (normalized.includes(table.toLowerCase())) {
|
|
1083
|
+
return target;
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
const complexTriggers = ["join", "group by", "having", "union", "intersect", "except"];
|
|
1087
|
+
if (complexTriggers.some((trigger) => normalized.includes(trigger))) {
|
|
1088
|
+
return "postgres";
|
|
1089
|
+
}
|
|
1090
|
+
return "d1";
|
|
1091
|
+
}
|
|
1092
|
+
async introspectD1() {
|
|
1093
|
+
if (!this._d1) {
|
|
1094
|
+
throw new DatabaseError("DB_CONNECTION_FAILED" /* DB_CONNECTION_FAILED */, "D1 not configured");
|
|
1095
|
+
}
|
|
1096
|
+
const result = await this._d1.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'").all();
|
|
1097
|
+
const tables = [];
|
|
1098
|
+
for (const row of result.results) {
|
|
1099
|
+
const tableName = row.name;
|
|
1100
|
+
const columns = await this._d1.prepare(`PRAGMA table_info(${tableName})`).all();
|
|
1101
|
+
tables.push({
|
|
1102
|
+
name: tableName,
|
|
1103
|
+
columns: columns.results.map((col) => ({
|
|
1104
|
+
name: col.name,
|
|
1105
|
+
type: col.type,
|
|
1106
|
+
nullable: col.notnull === 0,
|
|
1107
|
+
defaultValue: col.dflt_value,
|
|
1108
|
+
isPrimaryKey: col.pk === 1
|
|
1109
|
+
})),
|
|
1110
|
+
database: "d1"
|
|
1111
|
+
});
|
|
1112
|
+
}
|
|
1113
|
+
return { tables, database: "d1" };
|
|
1114
|
+
}
|
|
1115
|
+
async introspectPostgres() {
|
|
1116
|
+
if (!this.pgPool) {
|
|
1117
|
+
throw new DatabaseError("DB_CONNECTION_FAILED" /* DB_CONNECTION_FAILED */, "Postgres not configured");
|
|
1118
|
+
}
|
|
1119
|
+
const result = await this.pgPool.query(`
|
|
1120
|
+
SELECT table_name
|
|
1121
|
+
FROM information_schema.tables
|
|
1122
|
+
WHERE table_schema = 'public'
|
|
1123
|
+
`);
|
|
1124
|
+
const tables = [];
|
|
1125
|
+
for (const row of result.rows) {
|
|
1126
|
+
const tableName = row.table_name;
|
|
1127
|
+
const columns = await this.pgPool.query(
|
|
1128
|
+
`
|
|
1129
|
+
SELECT column_name, data_type, is_nullable, column_default
|
|
1130
|
+
FROM information_schema.columns
|
|
1131
|
+
WHERE table_name = $1
|
|
1132
|
+
`,
|
|
1133
|
+
[tableName]
|
|
1134
|
+
);
|
|
1135
|
+
tables.push({
|
|
1136
|
+
name: tableName,
|
|
1137
|
+
columns: columns.rows.map((col) => ({
|
|
1138
|
+
name: col.column_name,
|
|
1139
|
+
type: col.data_type,
|
|
1140
|
+
nullable: col.is_nullable === "YES",
|
|
1141
|
+
defaultValue: col.column_default
|
|
1142
|
+
})),
|
|
1143
|
+
database: "postgres"
|
|
1144
|
+
});
|
|
1145
|
+
}
|
|
1146
|
+
return { tables, database: "postgres" };
|
|
1147
|
+
}
|
|
1148
|
+
findPostgresConnStr(env) {
|
|
1149
|
+
const entry = Object.entries(env).find(([key]) => key.endsWith("_DATABASE_URL"));
|
|
1150
|
+
return entry ? entry[1] : void 0;
|
|
1151
|
+
}
|
|
1152
|
+
};
|
|
1153
|
+
export {
|
|
1154
|
+
AIError,
|
|
1155
|
+
AerostackClient,
|
|
1156
|
+
AerostackServer,
|
|
1157
|
+
AuthenticationError,
|
|
1158
|
+
CacheError,
|
|
1159
|
+
ClientError,
|
|
1160
|
+
ClientErrorCode,
|
|
1161
|
+
DatabaseError,
|
|
1162
|
+
ErrorCode,
|
|
1163
|
+
NetworkError,
|
|
1164
|
+
QueueError,
|
|
1165
|
+
ServerError,
|
|
1166
|
+
ServiceError,
|
|
1167
|
+
StorageError,
|
|
1168
|
+
ValidationError
|
|
1169
|
+
};
|