@aurabase/functions 0.5.4 → 0.6.1
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 +19 -9
- package/dist/index.cjs +96 -13
- package/dist/index.d.cts +20 -1
- package/dist/index.d.ts +20 -1
- package/dist/index.js +97 -14
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -26,6 +26,10 @@ import { createClient } from '@aurabase/aurabase-js'
|
|
|
26
26
|
const aura = createClient('https://mon-projet.aurabase.cloud', 'aura_anon_xxx')
|
|
27
27
|
```
|
|
28
28
|
|
|
29
|
+
### Qui peut appeler quoi
|
|
30
|
+
|
|
31
|
+
Seule l'**invocation** (`invoke`) est ouverte à la clé publique `anon` et aux JWT utilisateur. Tout le reste (`list`, `get`, `listJobs`, `getJob`, `enqueueJob`, `listCronJobs`, `createCronJob`, `deleteCronJob`) exige une clé `service_role` et s'utilise **côté serveur** : une `FunctionDef` contient le code source et les variables d'environnement de la fonction, donc ces appels répondent `403` à une clé publique. `createClient` refuse d'ailleurs une clé `service_role` dans un navigateur.
|
|
32
|
+
|
|
29
33
|
### Invocation
|
|
30
34
|
|
|
31
35
|
`invoke<T>()` décrit la requête que votre fonction doit recevoir (`{ method, path, headers, body }`) et dé-wrap la réponse JSON du backend — tout est bufferisé, pas de streaming (voir Limitations).
|
|
@@ -44,6 +48,8 @@ console.log(data.ok)
|
|
|
44
48
|
|
|
45
49
|
Options complètes : `body`, `method`, `path` (sous-route à l'intérieur de la fonction), `headers` (transmis à la fonction elle-même, pas des headers de transport), `signal`, `timeout`, `responseType`.
|
|
46
50
|
|
|
51
|
+
Le `body` doit être sérialisable en JSON (objet, tableau, chaîne, nombre, booléen). Un `Blob`, un `ArrayBuffer`, un tableau typé, un `FormData`, un `URLSearchParams` ou un `ReadableStream` est refusé avant tout appel réseau avec le code `invalid_body` : le runtime ne reçoit que du JSON, et ces objets seraient transmis vides ou déformés. Encodez-les (par exemple en base64) ou passez par Storage.
|
|
52
|
+
|
|
47
53
|
```ts
|
|
48
54
|
const controller = new AbortController()
|
|
49
55
|
|
|
@@ -85,7 +91,7 @@ Deno.serve(async (req) => {
|
|
|
85
91
|
})
|
|
86
92
|
```
|
|
87
93
|
|
|
88
|
-
Défauts si vous ne précisez rien : `POST` sur `/`. `GET` et `HEAD` sont envoyés sans corps (la spec Fetch l'interdit) ; un `body` non-chaîne est sérialisé en JSON
|
|
94
|
+
Défauts si vous ne précisez rien : `POST` sur `/`. `GET` et `HEAD` sont envoyés sans corps (la spec Fetch l'interdit) ; un `body` non-chaîne est sérialisé en JSON ; `content-type: application/json` est posé si vous n'en fixez pas un — y compris pour une chaîne, que la fonction reçoit alors telle quelle : lisez-la avec `await req.text()` plutôt que `req.json()`.
|
|
89
95
|
|
|
90
96
|
Sans `body`, la fonction reçoit une requête **sans corps** (parité `@supabase/functions-js`) : un `await req.json()` inconditionnel y échoue. Écrivez `await req.json().catch(() => ({}))` si l'appel peut être fait sans charge utile.
|
|
91
97
|
|
|
@@ -95,7 +101,7 @@ Les en-têtes de cadrage/transport (`content-length`, `transfer-encoding`, `conn
|
|
|
95
101
|
|
|
96
102
|
#### Type de réponse (`responseType`)
|
|
97
103
|
|
|
98
|
-
Défaut `'json'` : le corps est parsé et dé-wrappé (`{data}` → contenu). Les autres valeurs renvoient le corps brut de la fonction, sans dé-wrapping, et `error.status` porte alors le **vrai** statut HTTP de la fonction :
|
|
104
|
+
Défaut `'json'` : le corps est parsé et dé-wrappé (`{data}` → contenu). Une réponse texte (`text/plain`…) arrive telle quelle, en chaîne. Un contenu **binaire** (image, PDF…) ne peut pas passer par ce mode : le SDK renvoie `invalid_response` en vous orientant vers `responseType`. Les autres valeurs renvoient le corps brut de la fonction, sans dé-wrapping, et `error.status` porte alors le **vrai** statut HTTP de la fonction :
|
|
99
105
|
|
|
100
106
|
```ts
|
|
101
107
|
const { data: csv } = await aura.functions.invoke<string>('export', { responseType: 'text' })
|
|
@@ -109,27 +115,31 @@ Parité `@supabase/functions-js`. Les deux classes sous-classent `AuraError` (do
|
|
|
109
115
|
|
|
110
116
|
| Classe | Quand | Statuts |
|
|
111
117
|
|---|---|---|
|
|
112
|
-
| `FunctionsHttpError` | **votre fonction** a répondu en erreur | tout non-2xx
|
|
113
|
-
| `FunctionsRelayError` | la réponse de votre fonction n'est jamais arrivée | 0 (réseau), 502, 503, 504 |
|
|
118
|
+
| `FunctionsHttpError` | **votre fonction** a répondu en erreur, ou une route de gestion a échoué | tout non-2xx venant de la fonction (400, 404, 500, **y compris 502/503/504 qu'elle renvoie elle-même**) |
|
|
119
|
+
| `FunctionsRelayError` | la **plateforme** a échoué : la réponse de votre fonction n'est jamais arrivée | 0 (réseau), 502, 503, 504 produits par la passerelle ou le runtime |
|
|
120
|
+
|
|
121
|
+
Le message d'une `FunctionsHttpError` est celui que votre fonction a renvoyé : texte brut, `{ "message": "…" }`, `{ "error": "…" }` ou `{ "error": { "message": "…" } }`. `error.context` conserve le corps d'origine (objet JSON ou texte). Une fonction qui répond 500 sans corps donne « Function returned HTTP 500 ».
|
|
114
122
|
|
|
115
123
|
Deux statuts de relais à ne pas confondre — ils ne se rejouent pas de la même façon :
|
|
116
124
|
|
|
117
125
|
- **503 `SERVICE_UNAVAILABLE`** : l'edge-runtime est injoignable, votre fonction **n'a pas** été exécutée → réessayer est sûr ;
|
|
118
|
-
- **504 `TIMEOUT`** :
|
|
126
|
+
- **504 `TIMEOUT`** : l'attente a dépassé le budget de la fonction (`permissions.timeout_secs` + marge). La fonction **a été lancée** et a pu produire ses effets de bord ; seule la réponse manque → ne rejouez pas une opération non idempotente à l'aveugle.
|
|
119
127
|
|
|
120
128
|
```ts
|
|
121
|
-
import { FunctionsHttpError, FunctionsRelayError, isRelayStatus } from '@aurabase/
|
|
129
|
+
import { FunctionsHttpError, FunctionsRelayError, isRelayStatus } from '@aurabase/aurabase-js'
|
|
122
130
|
|
|
123
131
|
const { error } = await aura.functions.invoke('checkout', { responseType: 'text' })
|
|
124
132
|
if (error instanceof FunctionsRelayError) {
|
|
125
|
-
// rien n'a été
|
|
133
|
+
// panne de plateforme : 503 → rien n'a été exécuté, 504 → peut-être exécuté, vérifiez avant de rejouer
|
|
126
134
|
} else if (error instanceof FunctionsHttpError) {
|
|
127
135
|
// la fonction a bien tourné et a répondu en erreur → déboguer VOTRE code
|
|
128
136
|
}
|
|
129
137
|
isRelayStatus(error?.status ?? 0) // même décision, sans instanceof
|
|
130
138
|
```
|
|
131
139
|
|
|
132
|
-
Une annulation (`timeout`/`signal`) reste une `AuraError` de code `timeout`, jamais une de ces deux classes.
|
|
140
|
+
Une annulation (`timeout`/`signal`) reste une `AuraError` de code `timeout`, jamais une de ces deux classes. Sans `timeout` ni `signal`, le délai par défaut du client (`requestTimeoutMs`, 30 s) s'applique à tous les modes, `responseType` compris.
|
|
141
|
+
|
|
142
|
+
Les segments passés à `invoke`, `get`, `getJob` et `deleteCronJob` sont encodés (`encodeURIComponent`) : un nom contenant `/`, `?` ou `#` ne peut pas sortir de sa position dans l'URL.
|
|
133
143
|
|
|
134
144
|
### Fonctions (métadonnées)
|
|
135
145
|
|
|
@@ -170,7 +180,7 @@ await aura.functions.deleteCronJob('nightly-cleanup')
|
|
|
170
180
|
## Contrat d'invocation et comportement des erreurs
|
|
171
181
|
|
|
172
182
|
- **Déballage transparent :** En mode `responseType: 'json'` (défaut), `data` contient directement le type attendu `T` renvoyé par votre fonction. Pour préserver la compatibilité avec le code existant lisant l'enveloppe, les propriétés `status` et `body` restent accessibles sur l'objet retourné.
|
|
173
|
-
- **Propagation des erreurs HTTP applicatives :** Si votre fonction répond avec un statut HTTP
|
|
183
|
+
- **Propagation des erreurs HTTP applicatives :** Si votre fonction répond avec un statut HTTP supérieur ou égal à 400, le SDK renvoie une `FunctionsHttpError` avec le statut réel de votre fonction (`error.status`) et son message d'erreur, même lorsque le transport réseau sous-jacent utilise une enveloppe.
|
|
174
184
|
- **Relais et disponibilité :** En cas d'indisponibilité du runtime ou de coupure réseau, une `FunctionsRelayError` est renvoyée.
|
|
175
185
|
- **Propagation d'identité sécurisée :** Les en-têtes d'autorisation (`Authorization: Bearer <token>`, `apikey`) sont validés par la passerelle Aurabase et propagés de façon étanche vers le worker de la fonction (`x-aura-user-id`, `x-aura-role`), protégeant votre backend contre toute falsification client.
|
|
176
186
|
|
package/dist/index.cjs
CHANGED
|
@@ -27,7 +27,31 @@ var FunctionsRelayError = class _FunctionsRelayError extends core.AuraError {
|
|
|
27
27
|
};
|
|
28
28
|
function toFunctionsError(err, context) {
|
|
29
29
|
const body = err.details != null ? err.details : { error: { code: err.code, message: err.message } };
|
|
30
|
-
return isRelayStatus(err.status) ? new FunctionsRelayError(err.status, body,
|
|
30
|
+
return isRelayStatus(err.status) ? new FunctionsRelayError(err.status, body, err.details) : new FunctionsHttpError(err.status, body, err.details);
|
|
31
|
+
}
|
|
32
|
+
var INVOKE_ORIGIN_HEADER = "x-aurabase-invoke-origin";
|
|
33
|
+
var seg = encodeURIComponent;
|
|
34
|
+
function unserializableBodyKind(body) {
|
|
35
|
+
if (body === null || typeof body !== "object") return null;
|
|
36
|
+
const g = globalThis;
|
|
37
|
+
for (const name of ["Blob", "FormData", "URLSearchParams", "ReadableStream"]) {
|
|
38
|
+
const ctor = g[name];
|
|
39
|
+
if (typeof ctor === "function" && body instanceof ctor) return name;
|
|
40
|
+
}
|
|
41
|
+
if (body instanceof ArrayBuffer) return "ArrayBuffer";
|
|
42
|
+
if (ArrayBuffer.isView(body)) return body.constructor.name;
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
function envelopeContentType(headers) {
|
|
46
|
+
if (headers === null || typeof headers !== "object") return void 0;
|
|
47
|
+
for (const [k, v] of Object.entries(headers)) {
|
|
48
|
+
if (k.toLowerCase() === "content-type" && typeof v === "string") return v.toLowerCase();
|
|
49
|
+
}
|
|
50
|
+
return void 0;
|
|
51
|
+
}
|
|
52
|
+
function isTextual(contentType) {
|
|
53
|
+
if (!contentType) return true;
|
|
54
|
+
return contentType.includes("json") || contentType.startsWith("text/") || contentType.includes("xml");
|
|
31
55
|
}
|
|
32
56
|
var FunctionsService = class extends core.BaseService {
|
|
33
57
|
constructor(client) {
|
|
@@ -74,11 +98,26 @@ var FunctionsService = class extends core.BaseService {
|
|
|
74
98
|
* `signal` produisent le code `timeout`.
|
|
75
99
|
*/
|
|
76
100
|
async invoke(slug, options) {
|
|
101
|
+
const badBody = unserializableBodyKind(options?.body);
|
|
102
|
+
if (badBody) {
|
|
103
|
+
return {
|
|
104
|
+
data: null,
|
|
105
|
+
error: new core.AuraError(0, {
|
|
106
|
+
error: {
|
|
107
|
+
code: "invalid_body",
|
|
108
|
+
message: `Le corps d'invocation doit \xEAtre s\xE9rialisable en JSON (objet, tableau, cha\xEEne, nombre, bool\xE9en) : un ${badBody} serait transmis vide ou d\xE9form\xE9. Encodez-le (par exemple en base64) ou passez par Storage.`
|
|
109
|
+
}
|
|
110
|
+
})
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
const responseType = options?.responseType ?? "json";
|
|
77
114
|
const controller = new AbortController();
|
|
78
115
|
let timeoutId;
|
|
79
116
|
const userSignal = options?.signal;
|
|
80
117
|
const onAbort = () => controller.abort();
|
|
81
|
-
|
|
118
|
+
const clientTimeout = this.client.requestTimeoutMs ?? core.DEFAULT_REQUEST_TIMEOUT_MS;
|
|
119
|
+
const timeoutMs = options?.timeout || (responseType !== "json" && userSignal === void 0 && clientTimeout > 0 ? clientTimeout : 0);
|
|
120
|
+
if (timeoutMs) timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
82
121
|
if (userSignal) {
|
|
83
122
|
if (userSignal.aborted) controller.abort();
|
|
84
123
|
else userSignal.addEventListener("abort", onAbort, { once: true });
|
|
@@ -93,7 +132,6 @@ var FunctionsService = class extends core.BaseService {
|
|
|
93
132
|
headers: options?.headers,
|
|
94
133
|
body: options?.body
|
|
95
134
|
};
|
|
96
|
-
const responseType = options?.responseType ?? "json";
|
|
97
135
|
const timeoutError = () => ({
|
|
98
136
|
data: null,
|
|
99
137
|
error: new core.AuraError(0, {
|
|
@@ -103,7 +141,7 @@ var FunctionsService = class extends core.BaseService {
|
|
|
103
141
|
try {
|
|
104
142
|
if (responseType === "json") {
|
|
105
143
|
const hasCanceller = options?.timeout !== void 0 || userSignal !== void 0;
|
|
106
|
-
const res2 = await this.request("POST", `${this.prefix}/${slug}/invoke`, envelope, {
|
|
144
|
+
const res2 = await this.request("POST", `${this.prefix}/${seg(slug)}/invoke`, envelope, {
|
|
107
145
|
...hasCanceller ? { signal: controller.signal } : {}
|
|
108
146
|
});
|
|
109
147
|
if (res2.error && controller.signal.aborted) return timeoutError();
|
|
@@ -113,7 +151,7 @@ var FunctionsService = class extends core.BaseService {
|
|
|
113
151
|
const invokeStatus = rawData.status;
|
|
114
152
|
const invokeBody = rawData.body;
|
|
115
153
|
if (typeof invokeStatus === "number" && invokeStatus >= 400) {
|
|
116
|
-
const errPayload = typeof invokeBody === "object" && invokeBody !== null ? invokeBody : { message:
|
|
154
|
+
const errPayload = typeof invokeBody === "object" && invokeBody !== null ? invokeBody : invokeBody === null || invokeBody === void 0 || invokeBody === "" ? {} : { message: typeof invokeBody === "string" ? invokeBody : JSON.stringify(invokeBody) };
|
|
117
155
|
const errorMsg = typeof errPayload.message === "string" ? errPayload.message : typeof errPayload.error === "string" ? errPayload.error : typeof errPayload.error?.message === "string" ? String(errPayload.error.message) : `Function returned HTTP ${invokeStatus}`;
|
|
118
156
|
const normalizedBody = {
|
|
119
157
|
...errPayload,
|
|
@@ -125,10 +163,21 @@ var FunctionsService = class extends core.BaseService {
|
|
|
125
163
|
};
|
|
126
164
|
return {
|
|
127
165
|
data: null,
|
|
128
|
-
error:
|
|
166
|
+
error: new FunctionsHttpError(invokeStatus, normalizedBody, invokeBody)
|
|
129
167
|
};
|
|
130
168
|
}
|
|
131
|
-
|
|
169
|
+
if (invokeBody === null && invokeStatus !== 204 && !isTextual(envelopeContentType(rawData.headers))) {
|
|
170
|
+
return {
|
|
171
|
+
data: null,
|
|
172
|
+
error: new core.AuraError(0, {
|
|
173
|
+
error: {
|
|
174
|
+
code: "invalid_response",
|
|
175
|
+
message: `La fonction a r\xE9pondu un contenu binaire (HTTP ${invokeStatus}) que le mode JSON ne peut pas transmettre : utilisez responseType 'blob' ou 'arrayBuffer'.`
|
|
176
|
+
}
|
|
177
|
+
})
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
const unwrappedData = invokeBody;
|
|
132
181
|
if (unwrappedData !== null && typeof unwrappedData === "object") {
|
|
133
182
|
if (!("body" in unwrappedData)) {
|
|
134
183
|
Object.defineProperty(unwrappedData, "body", {
|
|
@@ -166,19 +215,34 @@ var FunctionsService = class extends core.BaseService {
|
|
|
166
215
|
};
|
|
167
216
|
if (store.token) headers["Authorization"] = `Bearer ${store.token}`;
|
|
168
217
|
if (client.apiKey) headers["apikey"] = client.apiKey;
|
|
169
|
-
const res = await client.fetchFn(this.url(`${this.prefix}/${slug}/invoke`), {
|
|
218
|
+
const res = await client.fetchFn(this.url(`${this.prefix}/${seg(slug)}/invoke`), {
|
|
170
219
|
method: "POST",
|
|
171
220
|
headers,
|
|
172
221
|
body: JSON.stringify(envelope),
|
|
173
222
|
signal: controller.signal
|
|
174
223
|
});
|
|
175
224
|
if (!res.ok) {
|
|
225
|
+
let text = "";
|
|
226
|
+
try {
|
|
227
|
+
text = await res.text();
|
|
228
|
+
} catch (e) {
|
|
229
|
+
if (controller.signal.aborted || core.isAbortError(e)) throw e;
|
|
230
|
+
}
|
|
176
231
|
let parsed = null;
|
|
177
232
|
try {
|
|
178
|
-
parsed =
|
|
233
|
+
parsed = text.trim() ? JSON.parse(text) : null;
|
|
179
234
|
} catch {
|
|
180
235
|
}
|
|
181
|
-
|
|
236
|
+
const isObject = parsed !== null && typeof parsed === "object";
|
|
237
|
+
const stringError = isObject && typeof parsed.error === "string" ? parsed.error : void 0;
|
|
238
|
+
const body = isObject ? stringError !== void 0 && typeof parsed.message !== "string" ? { ...parsed, message: stringError } : parsed : text.trim() ? { message: text } : null;
|
|
239
|
+
if (res.headers.get(INVOKE_ORIGIN_HEADER) === "function") {
|
|
240
|
+
return {
|
|
241
|
+
data: null,
|
|
242
|
+
error: new FunctionsHttpError(res.status, body, isObject ? parsed : text || null)
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
return { data: null, error: toFunctionsError(new core.AuraError(res.status, body, res.headers)) };
|
|
182
246
|
}
|
|
183
247
|
let data;
|
|
184
248
|
if (responseType === "text") data = await res.text();
|
|
@@ -201,6 +265,10 @@ var FunctionsService = class extends core.BaseService {
|
|
|
201
265
|
}
|
|
202
266
|
}
|
|
203
267
|
// -- Function CRUD ----------------------------------------------------------
|
|
268
|
+
/**
|
|
269
|
+
* Liste les fonctions déployées du projet, avec leur code source et leurs variables.
|
|
270
|
+
* Exige une clé `service_role` : une clé publique reçoit `403`.
|
|
271
|
+
*/
|
|
204
272
|
async list() {
|
|
205
273
|
return this.request("GET", `${this.prefix}`);
|
|
206
274
|
}
|
|
@@ -212,9 +280,13 @@ var FunctionsService = class extends core.BaseService {
|
|
|
212
280
|
* le NOM l'emporte. Une valeur qui n'est ni l'un ni l'autre produit un 404 qui le dit.
|
|
213
281
|
*/
|
|
214
282
|
async get(nameOrId) {
|
|
215
|
-
return this.request("GET", `${this.prefix}/${nameOrId}`);
|
|
283
|
+
return this.request("GET", `${this.prefix}/${seg(nameOrId)}`);
|
|
216
284
|
}
|
|
217
285
|
// -- Jobs -------------------------------------------------------------------
|
|
286
|
+
/**
|
|
287
|
+
* Liste les jobs asynchrones du projet, filtrés par `status` (`pending`, `running`, `completed`,
|
|
288
|
+
* `failed`, `dead`). `limit` va de 1 à 500 (50 par défaut). Exige une clé `service_role`.
|
|
289
|
+
*/
|
|
218
290
|
async listJobs(options) {
|
|
219
291
|
const params = new URLSearchParams();
|
|
220
292
|
if (options?.status) params.set("status", options.status);
|
|
@@ -222,8 +294,9 @@ var FunctionsService = class extends core.BaseService {
|
|
|
222
294
|
const qs = params.toString() ? `?${params.toString()}` : "";
|
|
223
295
|
return this.request("GET", `${this.prefix}/jobs${qs}`);
|
|
224
296
|
}
|
|
297
|
+
/** Lit un job par son identifiant. Exige une clé `service_role`. */
|
|
225
298
|
async getJob(jobId) {
|
|
226
|
-
return this.request("GET", `${this.prefix}/jobs/${jobId}`);
|
|
299
|
+
return this.request("GET", `${this.prefix}/jobs/${seg(jobId)}`);
|
|
227
300
|
}
|
|
228
301
|
/**
|
|
229
302
|
* Enfile un job d'exécution asynchrone en arrière-plan pour une fonction.
|
|
@@ -249,9 +322,15 @@ var FunctionsService = class extends core.BaseService {
|
|
|
249
322
|
});
|
|
250
323
|
}
|
|
251
324
|
// -- Cron -------------------------------------------------------------------
|
|
325
|
+
/** Liste les tâches cron du projet. Exige une clé `service_role`. */
|
|
252
326
|
async listCronJobs() {
|
|
253
327
|
return this.request("GET", `${this.prefix}/cron`);
|
|
254
328
|
}
|
|
329
|
+
/**
|
|
330
|
+
* Crée une tâche cron : à chaque échéance, un job est mis en file pour `functionName`.
|
|
331
|
+
* `schedule` compte 5 champs (`min heure jour mois jour-semaine`) ou 6 (avec les secondes), en UTC ;
|
|
332
|
+
* les alias comme `@daily` sont refusés. Le `payload` est figé à la création. Exige une clé `service_role`.
|
|
333
|
+
*/
|
|
255
334
|
async createCronJob(name, schedule, functionName, options) {
|
|
256
335
|
return this.request("POST", `${this.prefix}/cron`, {
|
|
257
336
|
name,
|
|
@@ -260,8 +339,12 @@ var FunctionsService = class extends core.BaseService {
|
|
|
260
339
|
payload: options?.payload
|
|
261
340
|
});
|
|
262
341
|
}
|
|
342
|
+
/**
|
|
343
|
+
* Supprime une tâche cron par son NOM (et non son identifiant) ; un nom inconnu renvoie 404.
|
|
344
|
+
* La tâche cesse aussi de se déclencher. Exige une clé `service_role`.
|
|
345
|
+
*/
|
|
263
346
|
async deleteCronJob(name) {
|
|
264
|
-
return this.request("DELETE", `${this.prefix}/cron/${name}`);
|
|
347
|
+
return this.request("DELETE", `${this.prefix}/cron/${seg(name)}`);
|
|
265
348
|
}
|
|
266
349
|
};
|
|
267
350
|
|
package/dist/index.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { AuraError, BaseService, AuraClientLike, AuraResponse, FunctionDef, Job, CronJob } from '@aurabase/core';
|
|
2
|
-
export { AuraResponse, CronJob, FunctionDef, Job } from '@aurabase/core';
|
|
2
|
+
export { AuraResponse, CronJob, FunctionDef, FunctionPermissions, Job } from '@aurabase/core';
|
|
3
3
|
|
|
4
4
|
declare function isRelayStatus(status: number): boolean;
|
|
5
5
|
/**
|
|
@@ -84,6 +84,10 @@ declare class FunctionsService extends BaseService {
|
|
|
84
84
|
timeout?: number;
|
|
85
85
|
responseType?: FunctionsResponseType;
|
|
86
86
|
}): Promise<AuraResponse<T>>;
|
|
87
|
+
/**
|
|
88
|
+
* Liste les fonctions déployées du projet, avec leur code source et leurs variables.
|
|
89
|
+
* Exige une clé `service_role` : une clé publique reçoit `403`.
|
|
90
|
+
*/
|
|
87
91
|
list(): Promise<AuraResponse<FunctionDef[]>>;
|
|
88
92
|
/**
|
|
89
93
|
* Lit une fonction par son **nom** ou par son **identifiant** (B10).
|
|
@@ -93,10 +97,15 @@ declare class FunctionsService extends BaseService {
|
|
|
93
97
|
* le NOM l'emporte. Une valeur qui n'est ni l'un ni l'autre produit un 404 qui le dit.
|
|
94
98
|
*/
|
|
95
99
|
get(nameOrId: string): Promise<AuraResponse<FunctionDef>>;
|
|
100
|
+
/**
|
|
101
|
+
* Liste les jobs asynchrones du projet, filtrés par `status` (`pending`, `running`, `completed`,
|
|
102
|
+
* `failed`, `dead`). `limit` va de 1 à 500 (50 par défaut). Exige une clé `service_role`.
|
|
103
|
+
*/
|
|
96
104
|
listJobs(options?: {
|
|
97
105
|
status?: string;
|
|
98
106
|
limit?: number;
|
|
99
107
|
}): Promise<AuraResponse<Job[]>>;
|
|
108
|
+
/** Lit un job par son identifiant. Exige une clé `service_role`. */
|
|
100
109
|
getJob(jobId: string): Promise<AuraResponse<Job>>;
|
|
101
110
|
/**
|
|
102
111
|
* Enfile un job d'exécution asynchrone en arrière-plan pour une fonction.
|
|
@@ -110,10 +119,20 @@ declare class FunctionsService extends BaseService {
|
|
|
110
119
|
delaySecs?: number;
|
|
111
120
|
maxAttempts?: number;
|
|
112
121
|
}): Promise<AuraResponse<Job>>;
|
|
122
|
+
/** Liste les tâches cron du projet. Exige une clé `service_role`. */
|
|
113
123
|
listCronJobs(): Promise<AuraResponse<CronJob[]>>;
|
|
124
|
+
/**
|
|
125
|
+
* Crée une tâche cron : à chaque échéance, un job est mis en file pour `functionName`.
|
|
126
|
+
* `schedule` compte 5 champs (`min heure jour mois jour-semaine`) ou 6 (avec les secondes), en UTC ;
|
|
127
|
+
* les alias comme `@daily` sont refusés. Le `payload` est figé à la création. Exige une clé `service_role`.
|
|
128
|
+
*/
|
|
114
129
|
createCronJob(name: string, schedule: string, functionName: string, options?: {
|
|
115
130
|
payload?: unknown;
|
|
116
131
|
}): Promise<AuraResponse<CronJob>>;
|
|
132
|
+
/**
|
|
133
|
+
* Supprime une tâche cron par son NOM (et non son identifiant) ; un nom inconnu renvoie 404.
|
|
134
|
+
* La tâche cesse aussi de se déclencher. Exige une clé `service_role`.
|
|
135
|
+
*/
|
|
117
136
|
deleteCronJob(name: string): Promise<AuraResponse<null>>;
|
|
118
137
|
}
|
|
119
138
|
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { AuraError, BaseService, AuraClientLike, AuraResponse, FunctionDef, Job, CronJob } from '@aurabase/core';
|
|
2
|
-
export { AuraResponse, CronJob, FunctionDef, Job } from '@aurabase/core';
|
|
2
|
+
export { AuraResponse, CronJob, FunctionDef, FunctionPermissions, Job } from '@aurabase/core';
|
|
3
3
|
|
|
4
4
|
declare function isRelayStatus(status: number): boolean;
|
|
5
5
|
/**
|
|
@@ -84,6 +84,10 @@ declare class FunctionsService extends BaseService {
|
|
|
84
84
|
timeout?: number;
|
|
85
85
|
responseType?: FunctionsResponseType;
|
|
86
86
|
}): Promise<AuraResponse<T>>;
|
|
87
|
+
/**
|
|
88
|
+
* Liste les fonctions déployées du projet, avec leur code source et leurs variables.
|
|
89
|
+
* Exige une clé `service_role` : une clé publique reçoit `403`.
|
|
90
|
+
*/
|
|
87
91
|
list(): Promise<AuraResponse<FunctionDef[]>>;
|
|
88
92
|
/**
|
|
89
93
|
* Lit une fonction par son **nom** ou par son **identifiant** (B10).
|
|
@@ -93,10 +97,15 @@ declare class FunctionsService extends BaseService {
|
|
|
93
97
|
* le NOM l'emporte. Une valeur qui n'est ni l'un ni l'autre produit un 404 qui le dit.
|
|
94
98
|
*/
|
|
95
99
|
get(nameOrId: string): Promise<AuraResponse<FunctionDef>>;
|
|
100
|
+
/**
|
|
101
|
+
* Liste les jobs asynchrones du projet, filtrés par `status` (`pending`, `running`, `completed`,
|
|
102
|
+
* `failed`, `dead`). `limit` va de 1 à 500 (50 par défaut). Exige une clé `service_role`.
|
|
103
|
+
*/
|
|
96
104
|
listJobs(options?: {
|
|
97
105
|
status?: string;
|
|
98
106
|
limit?: number;
|
|
99
107
|
}): Promise<AuraResponse<Job[]>>;
|
|
108
|
+
/** Lit un job par son identifiant. Exige une clé `service_role`. */
|
|
100
109
|
getJob(jobId: string): Promise<AuraResponse<Job>>;
|
|
101
110
|
/**
|
|
102
111
|
* Enfile un job d'exécution asynchrone en arrière-plan pour une fonction.
|
|
@@ -110,10 +119,20 @@ declare class FunctionsService extends BaseService {
|
|
|
110
119
|
delaySecs?: number;
|
|
111
120
|
maxAttempts?: number;
|
|
112
121
|
}): Promise<AuraResponse<Job>>;
|
|
122
|
+
/** Liste les tâches cron du projet. Exige une clé `service_role`. */
|
|
113
123
|
listCronJobs(): Promise<AuraResponse<CronJob[]>>;
|
|
124
|
+
/**
|
|
125
|
+
* Crée une tâche cron : à chaque échéance, un job est mis en file pour `functionName`.
|
|
126
|
+
* `schedule` compte 5 champs (`min heure jour mois jour-semaine`) ou 6 (avec les secondes), en UTC ;
|
|
127
|
+
* les alias comme `@daily` sont refusés. Le `payload` est figé à la création. Exige une clé `service_role`.
|
|
128
|
+
*/
|
|
114
129
|
createCronJob(name: string, schedule: string, functionName: string, options?: {
|
|
115
130
|
payload?: unknown;
|
|
116
131
|
}): Promise<AuraResponse<CronJob>>;
|
|
132
|
+
/**
|
|
133
|
+
* Supprime une tâche cron par son NOM (et non son identifiant) ; un nom inconnu renvoie 404.
|
|
134
|
+
* La tâche cesse aussi de se déclencher. Exige une clé `service_role`.
|
|
135
|
+
*/
|
|
117
136
|
deleteCronJob(name: string): Promise<AuraResponse<null>>;
|
|
118
137
|
}
|
|
119
138
|
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AuraError, BaseService, isTokenExpired, refreshOnce, isAbortError } from '@aurabase/core';
|
|
1
|
+
import { AuraError, BaseService, DEFAULT_REQUEST_TIMEOUT_MS, isTokenExpired, refreshOnce, isAbortError } from '@aurabase/core';
|
|
2
2
|
|
|
3
3
|
// src/errors.ts
|
|
4
4
|
var RELAY_STATUSES = /* @__PURE__ */ new Set([502, 503, 504]);
|
|
@@ -25,7 +25,31 @@ var FunctionsRelayError = class _FunctionsRelayError extends AuraError {
|
|
|
25
25
|
};
|
|
26
26
|
function toFunctionsError(err, context) {
|
|
27
27
|
const body = err.details != null ? err.details : { error: { code: err.code, message: err.message } };
|
|
28
|
-
return isRelayStatus(err.status) ? new FunctionsRelayError(err.status, body,
|
|
28
|
+
return isRelayStatus(err.status) ? new FunctionsRelayError(err.status, body, err.details) : new FunctionsHttpError(err.status, body, err.details);
|
|
29
|
+
}
|
|
30
|
+
var INVOKE_ORIGIN_HEADER = "x-aurabase-invoke-origin";
|
|
31
|
+
var seg = encodeURIComponent;
|
|
32
|
+
function unserializableBodyKind(body) {
|
|
33
|
+
if (body === null || typeof body !== "object") return null;
|
|
34
|
+
const g = globalThis;
|
|
35
|
+
for (const name of ["Blob", "FormData", "URLSearchParams", "ReadableStream"]) {
|
|
36
|
+
const ctor = g[name];
|
|
37
|
+
if (typeof ctor === "function" && body instanceof ctor) return name;
|
|
38
|
+
}
|
|
39
|
+
if (body instanceof ArrayBuffer) return "ArrayBuffer";
|
|
40
|
+
if (ArrayBuffer.isView(body)) return body.constructor.name;
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
function envelopeContentType(headers) {
|
|
44
|
+
if (headers === null || typeof headers !== "object") return void 0;
|
|
45
|
+
for (const [k, v] of Object.entries(headers)) {
|
|
46
|
+
if (k.toLowerCase() === "content-type" && typeof v === "string") return v.toLowerCase();
|
|
47
|
+
}
|
|
48
|
+
return void 0;
|
|
49
|
+
}
|
|
50
|
+
function isTextual(contentType) {
|
|
51
|
+
if (!contentType) return true;
|
|
52
|
+
return contentType.includes("json") || contentType.startsWith("text/") || contentType.includes("xml");
|
|
29
53
|
}
|
|
30
54
|
var FunctionsService = class extends BaseService {
|
|
31
55
|
constructor(client) {
|
|
@@ -72,11 +96,26 @@ var FunctionsService = class extends BaseService {
|
|
|
72
96
|
* `signal` produisent le code `timeout`.
|
|
73
97
|
*/
|
|
74
98
|
async invoke(slug, options) {
|
|
99
|
+
const badBody = unserializableBodyKind(options?.body);
|
|
100
|
+
if (badBody) {
|
|
101
|
+
return {
|
|
102
|
+
data: null,
|
|
103
|
+
error: new AuraError(0, {
|
|
104
|
+
error: {
|
|
105
|
+
code: "invalid_body",
|
|
106
|
+
message: `Le corps d'invocation doit \xEAtre s\xE9rialisable en JSON (objet, tableau, cha\xEEne, nombre, bool\xE9en) : un ${badBody} serait transmis vide ou d\xE9form\xE9. Encodez-le (par exemple en base64) ou passez par Storage.`
|
|
107
|
+
}
|
|
108
|
+
})
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
const responseType = options?.responseType ?? "json";
|
|
75
112
|
const controller = new AbortController();
|
|
76
113
|
let timeoutId;
|
|
77
114
|
const userSignal = options?.signal;
|
|
78
115
|
const onAbort = () => controller.abort();
|
|
79
|
-
|
|
116
|
+
const clientTimeout = this.client.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
117
|
+
const timeoutMs = options?.timeout || (responseType !== "json" && userSignal === void 0 && clientTimeout > 0 ? clientTimeout : 0);
|
|
118
|
+
if (timeoutMs) timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
80
119
|
if (userSignal) {
|
|
81
120
|
if (userSignal.aborted) controller.abort();
|
|
82
121
|
else userSignal.addEventListener("abort", onAbort, { once: true });
|
|
@@ -91,7 +130,6 @@ var FunctionsService = class extends BaseService {
|
|
|
91
130
|
headers: options?.headers,
|
|
92
131
|
body: options?.body
|
|
93
132
|
};
|
|
94
|
-
const responseType = options?.responseType ?? "json";
|
|
95
133
|
const timeoutError = () => ({
|
|
96
134
|
data: null,
|
|
97
135
|
error: new AuraError(0, {
|
|
@@ -101,7 +139,7 @@ var FunctionsService = class extends BaseService {
|
|
|
101
139
|
try {
|
|
102
140
|
if (responseType === "json") {
|
|
103
141
|
const hasCanceller = options?.timeout !== void 0 || userSignal !== void 0;
|
|
104
|
-
const res2 = await this.request("POST", `${this.prefix}/${slug}/invoke`, envelope, {
|
|
142
|
+
const res2 = await this.request("POST", `${this.prefix}/${seg(slug)}/invoke`, envelope, {
|
|
105
143
|
...hasCanceller ? { signal: controller.signal } : {}
|
|
106
144
|
});
|
|
107
145
|
if (res2.error && controller.signal.aborted) return timeoutError();
|
|
@@ -111,7 +149,7 @@ var FunctionsService = class extends BaseService {
|
|
|
111
149
|
const invokeStatus = rawData.status;
|
|
112
150
|
const invokeBody = rawData.body;
|
|
113
151
|
if (typeof invokeStatus === "number" && invokeStatus >= 400) {
|
|
114
|
-
const errPayload = typeof invokeBody === "object" && invokeBody !== null ? invokeBody : { message:
|
|
152
|
+
const errPayload = typeof invokeBody === "object" && invokeBody !== null ? invokeBody : invokeBody === null || invokeBody === void 0 || invokeBody === "" ? {} : { message: typeof invokeBody === "string" ? invokeBody : JSON.stringify(invokeBody) };
|
|
115
153
|
const errorMsg = typeof errPayload.message === "string" ? errPayload.message : typeof errPayload.error === "string" ? errPayload.error : typeof errPayload.error?.message === "string" ? String(errPayload.error.message) : `Function returned HTTP ${invokeStatus}`;
|
|
116
154
|
const normalizedBody = {
|
|
117
155
|
...errPayload,
|
|
@@ -123,10 +161,21 @@ var FunctionsService = class extends BaseService {
|
|
|
123
161
|
};
|
|
124
162
|
return {
|
|
125
163
|
data: null,
|
|
126
|
-
error:
|
|
164
|
+
error: new FunctionsHttpError(invokeStatus, normalizedBody, invokeBody)
|
|
127
165
|
};
|
|
128
166
|
}
|
|
129
|
-
|
|
167
|
+
if (invokeBody === null && invokeStatus !== 204 && !isTextual(envelopeContentType(rawData.headers))) {
|
|
168
|
+
return {
|
|
169
|
+
data: null,
|
|
170
|
+
error: new AuraError(0, {
|
|
171
|
+
error: {
|
|
172
|
+
code: "invalid_response",
|
|
173
|
+
message: `La fonction a r\xE9pondu un contenu binaire (HTTP ${invokeStatus}) que le mode JSON ne peut pas transmettre : utilisez responseType 'blob' ou 'arrayBuffer'.`
|
|
174
|
+
}
|
|
175
|
+
})
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
const unwrappedData = invokeBody;
|
|
130
179
|
if (unwrappedData !== null && typeof unwrappedData === "object") {
|
|
131
180
|
if (!("body" in unwrappedData)) {
|
|
132
181
|
Object.defineProperty(unwrappedData, "body", {
|
|
@@ -164,19 +213,34 @@ var FunctionsService = class extends BaseService {
|
|
|
164
213
|
};
|
|
165
214
|
if (store.token) headers["Authorization"] = `Bearer ${store.token}`;
|
|
166
215
|
if (client.apiKey) headers["apikey"] = client.apiKey;
|
|
167
|
-
const res = await client.fetchFn(this.url(`${this.prefix}/${slug}/invoke`), {
|
|
216
|
+
const res = await client.fetchFn(this.url(`${this.prefix}/${seg(slug)}/invoke`), {
|
|
168
217
|
method: "POST",
|
|
169
218
|
headers,
|
|
170
219
|
body: JSON.stringify(envelope),
|
|
171
220
|
signal: controller.signal
|
|
172
221
|
});
|
|
173
222
|
if (!res.ok) {
|
|
223
|
+
let text = "";
|
|
224
|
+
try {
|
|
225
|
+
text = await res.text();
|
|
226
|
+
} catch (e) {
|
|
227
|
+
if (controller.signal.aborted || isAbortError(e)) throw e;
|
|
228
|
+
}
|
|
174
229
|
let parsed = null;
|
|
175
230
|
try {
|
|
176
|
-
parsed =
|
|
231
|
+
parsed = text.trim() ? JSON.parse(text) : null;
|
|
177
232
|
} catch {
|
|
178
233
|
}
|
|
179
|
-
|
|
234
|
+
const isObject = parsed !== null && typeof parsed === "object";
|
|
235
|
+
const stringError = isObject && typeof parsed.error === "string" ? parsed.error : void 0;
|
|
236
|
+
const body = isObject ? stringError !== void 0 && typeof parsed.message !== "string" ? { ...parsed, message: stringError } : parsed : text.trim() ? { message: text } : null;
|
|
237
|
+
if (res.headers.get(INVOKE_ORIGIN_HEADER) === "function") {
|
|
238
|
+
return {
|
|
239
|
+
data: null,
|
|
240
|
+
error: new FunctionsHttpError(res.status, body, isObject ? parsed : text || null)
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
return { data: null, error: toFunctionsError(new AuraError(res.status, body, res.headers)) };
|
|
180
244
|
}
|
|
181
245
|
let data;
|
|
182
246
|
if (responseType === "text") data = await res.text();
|
|
@@ -199,6 +263,10 @@ var FunctionsService = class extends BaseService {
|
|
|
199
263
|
}
|
|
200
264
|
}
|
|
201
265
|
// -- Function CRUD ----------------------------------------------------------
|
|
266
|
+
/**
|
|
267
|
+
* Liste les fonctions déployées du projet, avec leur code source et leurs variables.
|
|
268
|
+
* Exige une clé `service_role` : une clé publique reçoit `403`.
|
|
269
|
+
*/
|
|
202
270
|
async list() {
|
|
203
271
|
return this.request("GET", `${this.prefix}`);
|
|
204
272
|
}
|
|
@@ -210,9 +278,13 @@ var FunctionsService = class extends BaseService {
|
|
|
210
278
|
* le NOM l'emporte. Une valeur qui n'est ni l'un ni l'autre produit un 404 qui le dit.
|
|
211
279
|
*/
|
|
212
280
|
async get(nameOrId) {
|
|
213
|
-
return this.request("GET", `${this.prefix}/${nameOrId}`);
|
|
281
|
+
return this.request("GET", `${this.prefix}/${seg(nameOrId)}`);
|
|
214
282
|
}
|
|
215
283
|
// -- Jobs -------------------------------------------------------------------
|
|
284
|
+
/**
|
|
285
|
+
* Liste les jobs asynchrones du projet, filtrés par `status` (`pending`, `running`, `completed`,
|
|
286
|
+
* `failed`, `dead`). `limit` va de 1 à 500 (50 par défaut). Exige une clé `service_role`.
|
|
287
|
+
*/
|
|
216
288
|
async listJobs(options) {
|
|
217
289
|
const params = new URLSearchParams();
|
|
218
290
|
if (options?.status) params.set("status", options.status);
|
|
@@ -220,8 +292,9 @@ var FunctionsService = class extends BaseService {
|
|
|
220
292
|
const qs = params.toString() ? `?${params.toString()}` : "";
|
|
221
293
|
return this.request("GET", `${this.prefix}/jobs${qs}`);
|
|
222
294
|
}
|
|
295
|
+
/** Lit un job par son identifiant. Exige une clé `service_role`. */
|
|
223
296
|
async getJob(jobId) {
|
|
224
|
-
return this.request("GET", `${this.prefix}/jobs/${jobId}`);
|
|
297
|
+
return this.request("GET", `${this.prefix}/jobs/${seg(jobId)}`);
|
|
225
298
|
}
|
|
226
299
|
/**
|
|
227
300
|
* Enfile un job d'exécution asynchrone en arrière-plan pour une fonction.
|
|
@@ -247,9 +320,15 @@ var FunctionsService = class extends BaseService {
|
|
|
247
320
|
});
|
|
248
321
|
}
|
|
249
322
|
// -- Cron -------------------------------------------------------------------
|
|
323
|
+
/** Liste les tâches cron du projet. Exige une clé `service_role`. */
|
|
250
324
|
async listCronJobs() {
|
|
251
325
|
return this.request("GET", `${this.prefix}/cron`);
|
|
252
326
|
}
|
|
327
|
+
/**
|
|
328
|
+
* Crée une tâche cron : à chaque échéance, un job est mis en file pour `functionName`.
|
|
329
|
+
* `schedule` compte 5 champs (`min heure jour mois jour-semaine`) ou 6 (avec les secondes), en UTC ;
|
|
330
|
+
* les alias comme `@daily` sont refusés. Le `payload` est figé à la création. Exige une clé `service_role`.
|
|
331
|
+
*/
|
|
253
332
|
async createCronJob(name, schedule, functionName, options) {
|
|
254
333
|
return this.request("POST", `${this.prefix}/cron`, {
|
|
255
334
|
name,
|
|
@@ -258,8 +337,12 @@ var FunctionsService = class extends BaseService {
|
|
|
258
337
|
payload: options?.payload
|
|
259
338
|
});
|
|
260
339
|
}
|
|
340
|
+
/**
|
|
341
|
+
* Supprime une tâche cron par son NOM (et non son identifiant) ; un nom inconnu renvoie 404.
|
|
342
|
+
* La tâche cesse aussi de se déclencher. Exige une clé `service_role`.
|
|
343
|
+
*/
|
|
261
344
|
async deleteCronJob(name) {
|
|
262
|
-
return this.request("DELETE", `${this.prefix}/cron/${name}`);
|
|
345
|
+
return this.request("DELETE", `${this.prefix}/cron/${seg(name)}`);
|
|
263
346
|
}
|
|
264
347
|
};
|
|
265
348
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aurabase/functions",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.1",
|
|
4
4
|
"description": "Service edge functions du SDK Aurabase (invoke, jobs asynchrones, cron)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.cjs",
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
"functions"
|
|
46
46
|
],
|
|
47
47
|
"dependencies": {
|
|
48
|
-
"@aurabase/core": "0.
|
|
48
|
+
"@aurabase/core": "0.6.1"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
51
|
"@types/node": "20.14.15",
|