@aurabase/functions 0.5.3 → 0.6.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 +37 -14
- package/dist/index.cjs +137 -9
- package/dist/index.d.cts +12 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +138 -10
- 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
|
|
|
@@ -144,6 +154,14 @@ const { data: parNom } = await aura.functions.get('hello-world')
|
|
|
144
154
|
### Jobs asynchrones (extra vs `@supabase/functions-js`)
|
|
145
155
|
|
|
146
156
|
```ts
|
|
157
|
+
// Enfiler un nouveau job d'exécution en arrière-plan
|
|
158
|
+
const { data: jobInfo, error } = await aura.functions.enqueueJob('generate-report', {
|
|
159
|
+
payload: { month: '2026-10', sendEmail: true },
|
|
160
|
+
maxAttempts: 3,
|
|
161
|
+
delaySecs: 60, // exécuter dans 60 secondes
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
// Consulter les jobs
|
|
147
165
|
const { data: jobs } = await aura.functions.listJobs({ status: 'pending', limit: 20 })
|
|
148
166
|
const { data: job } = await aura.functions.getJob(jobs![0]!.id)
|
|
149
167
|
```
|
|
@@ -159,14 +177,19 @@ const { data: crons } = await aura.functions.listCronJobs()
|
|
|
159
177
|
await aura.functions.deleteCronJob('nightly-cleanup')
|
|
160
178
|
```
|
|
161
179
|
|
|
180
|
+
## Contrat d'invocation et comportement des erreurs
|
|
181
|
+
|
|
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é.
|
|
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.
|
|
184
|
+
- **Relais et disponibilité :** En cas d'indisponibilité du runtime ou de coupure réseau, une `FunctionsRelayError` est renvoyée.
|
|
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.
|
|
186
|
+
|
|
162
187
|
## Limitations
|
|
163
188
|
|
|
164
|
-
- Pas de streaming de réponse : `invoke()` bufferise
|
|
165
|
-
- Pas de sélection de région d'exécution.
|
|
166
|
-
-
|
|
167
|
-
-
|
|
168
|
-
- Pas de `getCronJob()` ni `updateCronJob()` : seuls `listCronJobs`, `createCronJob` et `deleteCronJob` sont exposés. Pas de `deploy()` / `delete()` de fonction depuis le SDK : le déploiement passe par l'API HTTP, la CLI `aura functions deploy` ou le Studio.
|
|
169
|
-
- `deleteCronJob(name)` et les routes `PATCH`/`DELETE`/`invoke` d'une fonction attendent un **nom** ; seul `get()` accepte aussi l'identifiant.
|
|
189
|
+
- Pas de streaming de réponse : `invoke()` bufferise l'intégralité du corps avant de résoudre. `responseType` configure le décodage du corps (`json`, `text`, `blob`, `arrayBuffer`).
|
|
190
|
+
- Pas de sélection de région d'exécution (exécution sur le cluster Edge actif).
|
|
191
|
+
- Les en-têtes de cadrage de transport (`content-length`, `connection`, `host`, etc.) ne sont pas transmissibles à la fonction.
|
|
192
|
+
- Pas de `getCronJob()` ni `updateCronJob()` : seuls `listCronJobs`, `createCronJob` et `deleteCronJob` sont exposés. Le déploiement s'effectue via le CLI `aura functions deploy` ou Aurabase Studio.
|
|
170
193
|
|
|
171
194
|
## Documentation
|
|
172
195
|
|
package/dist/index.cjs
CHANGED
|
@@ -29,6 +29,30 @@ function toFunctionsError(err, context) {
|
|
|
29
29
|
const body = err.details != null ? err.details : { error: { code: err.code, message: err.message } };
|
|
30
30
|
return isRelayStatus(err.status) ? new FunctionsRelayError(err.status, body, err.details) : new FunctionsHttpError(err.status, body, err.details);
|
|
31
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");
|
|
55
|
+
}
|
|
32
56
|
var FunctionsService = class extends core.BaseService {
|
|
33
57
|
constructor(client) {
|
|
34
58
|
super(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,11 +141,63 @@ 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();
|
|
110
148
|
if (res2.error) return { data: null, error: toFunctionsError(res2.error) };
|
|
149
|
+
const rawData = res2.data;
|
|
150
|
+
if (rawData && typeof rawData === "object" && "body" in rawData && "status" in rawData) {
|
|
151
|
+
const invokeStatus = rawData.status;
|
|
152
|
+
const invokeBody = rawData.body;
|
|
153
|
+
if (typeof invokeStatus === "number" && invokeStatus >= 400) {
|
|
154
|
+
const errPayload = typeof invokeBody === "object" && invokeBody !== null ? invokeBody : invokeBody === null || invokeBody === void 0 || invokeBody === "" ? {} : { message: typeof invokeBody === "string" ? invokeBody : JSON.stringify(invokeBody) };
|
|
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}`;
|
|
156
|
+
const normalizedBody = {
|
|
157
|
+
...errPayload,
|
|
158
|
+
message: errorMsg,
|
|
159
|
+
error: {
|
|
160
|
+
code: typeof errPayload.code === "string" ? errPayload.code : `http_${invokeStatus}`,
|
|
161
|
+
message: errorMsg
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
return {
|
|
165
|
+
data: null,
|
|
166
|
+
error: new FunctionsHttpError(invokeStatus, normalizedBody, invokeBody)
|
|
167
|
+
};
|
|
168
|
+
}
|
|
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;
|
|
181
|
+
if (unwrappedData !== null && typeof unwrappedData === "object") {
|
|
182
|
+
if (!("body" in unwrappedData)) {
|
|
183
|
+
Object.defineProperty(unwrappedData, "body", {
|
|
184
|
+
value: unwrappedData,
|
|
185
|
+
enumerable: false,
|
|
186
|
+
writable: true,
|
|
187
|
+
configurable: true
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
if (!("status" in unwrappedData)) {
|
|
191
|
+
Object.defineProperty(unwrappedData, "status", {
|
|
192
|
+
value: invokeStatus,
|
|
193
|
+
enumerable: false,
|
|
194
|
+
writable: true,
|
|
195
|
+
configurable: true
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return { data: unwrappedData, error: null, meta: res2.meta };
|
|
200
|
+
}
|
|
111
201
|
return res2;
|
|
112
202
|
}
|
|
113
203
|
const client = this.client;
|
|
@@ -125,19 +215,34 @@ var FunctionsService = class extends core.BaseService {
|
|
|
125
215
|
};
|
|
126
216
|
if (store.token) headers["Authorization"] = `Bearer ${store.token}`;
|
|
127
217
|
if (client.apiKey) headers["apikey"] = client.apiKey;
|
|
128
|
-
const res = await client.fetchFn(this.url(`${this.prefix}/${slug}/invoke`), {
|
|
218
|
+
const res = await client.fetchFn(this.url(`${this.prefix}/${seg(slug)}/invoke`), {
|
|
129
219
|
method: "POST",
|
|
130
220
|
headers,
|
|
131
221
|
body: JSON.stringify(envelope),
|
|
132
222
|
signal: controller.signal
|
|
133
223
|
});
|
|
134
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
|
+
}
|
|
135
231
|
let parsed = null;
|
|
136
232
|
try {
|
|
137
|
-
parsed =
|
|
233
|
+
parsed = text.trim() ? JSON.parse(text) : null;
|
|
138
234
|
} catch {
|
|
139
235
|
}
|
|
140
|
-
|
|
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)) };
|
|
141
246
|
}
|
|
142
247
|
let data;
|
|
143
248
|
if (responseType === "text") data = await res.text();
|
|
@@ -171,7 +276,7 @@ var FunctionsService = class extends core.BaseService {
|
|
|
171
276
|
* le NOM l'emporte. Une valeur qui n'est ni l'un ni l'autre produit un 404 qui le dit.
|
|
172
277
|
*/
|
|
173
278
|
async get(nameOrId) {
|
|
174
|
-
return this.request("GET", `${this.prefix}/${nameOrId}`);
|
|
279
|
+
return this.request("GET", `${this.prefix}/${seg(nameOrId)}`);
|
|
175
280
|
}
|
|
176
281
|
// -- Jobs -------------------------------------------------------------------
|
|
177
282
|
async listJobs(options) {
|
|
@@ -182,7 +287,30 @@ var FunctionsService = class extends core.BaseService {
|
|
|
182
287
|
return this.request("GET", `${this.prefix}/jobs${qs}`);
|
|
183
288
|
}
|
|
184
289
|
async getJob(jobId) {
|
|
185
|
-
return this.request("GET", `${this.prefix}/jobs/${jobId}`);
|
|
290
|
+
return this.request("GET", `${this.prefix}/jobs/${seg(jobId)}`);
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Enfile un job d'exécution asynchrone en arrière-plan pour une fonction.
|
|
294
|
+
*
|
|
295
|
+
* @param functionName Nom de la fonction à exécuter.
|
|
296
|
+
* @param options Paramètres optionnels : charge utile (`payload`), délai avant exécution (`delaySecs`)
|
|
297
|
+
* et nombre maximum de tentatives (`maxAttempts`).
|
|
298
|
+
*/
|
|
299
|
+
async enqueueJob(functionName, options) {
|
|
300
|
+
if (!functionName || typeof functionName !== "string" || functionName.trim() === "") {
|
|
301
|
+
return {
|
|
302
|
+
data: null,
|
|
303
|
+
error: new core.AuraError(400, {
|
|
304
|
+
error: { code: "bad_request", message: "Function name is required to enqueue a job" }
|
|
305
|
+
})
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
return this.request("POST", `${this.prefix}/jobs`, {
|
|
309
|
+
function_name: functionName.trim(),
|
|
310
|
+
payload: options?.payload,
|
|
311
|
+
delay_secs: options?.delaySecs,
|
|
312
|
+
max_attempts: options?.maxAttempts
|
|
313
|
+
});
|
|
186
314
|
}
|
|
187
315
|
// -- Cron -------------------------------------------------------------------
|
|
188
316
|
async listCronJobs() {
|
|
@@ -197,7 +325,7 @@ var FunctionsService = class extends core.BaseService {
|
|
|
197
325
|
});
|
|
198
326
|
}
|
|
199
327
|
async deleteCronJob(name) {
|
|
200
|
-
return this.request("DELETE", `${this.prefix}/cron/${name}`);
|
|
328
|
+
return this.request("DELETE", `${this.prefix}/cron/${seg(name)}`);
|
|
201
329
|
}
|
|
202
330
|
};
|
|
203
331
|
|
package/dist/index.d.cts
CHANGED
|
@@ -98,6 +98,18 @@ declare class FunctionsService extends BaseService {
|
|
|
98
98
|
limit?: number;
|
|
99
99
|
}): Promise<AuraResponse<Job[]>>;
|
|
100
100
|
getJob(jobId: string): Promise<AuraResponse<Job>>;
|
|
101
|
+
/**
|
|
102
|
+
* Enfile un job d'exécution asynchrone en arrière-plan pour une fonction.
|
|
103
|
+
*
|
|
104
|
+
* @param functionName Nom de la fonction à exécuter.
|
|
105
|
+
* @param options Paramètres optionnels : charge utile (`payload`), délai avant exécution (`delaySecs`)
|
|
106
|
+
* et nombre maximum de tentatives (`maxAttempts`).
|
|
107
|
+
*/
|
|
108
|
+
enqueueJob(functionName: string, options?: {
|
|
109
|
+
payload?: unknown;
|
|
110
|
+
delaySecs?: number;
|
|
111
|
+
maxAttempts?: number;
|
|
112
|
+
}): Promise<AuraResponse<Job>>;
|
|
101
113
|
listCronJobs(): Promise<AuraResponse<CronJob[]>>;
|
|
102
114
|
createCronJob(name: string, schedule: string, functionName: string, options?: {
|
|
103
115
|
payload?: unknown;
|
package/dist/index.d.ts
CHANGED
|
@@ -98,6 +98,18 @@ declare class FunctionsService extends BaseService {
|
|
|
98
98
|
limit?: number;
|
|
99
99
|
}): Promise<AuraResponse<Job[]>>;
|
|
100
100
|
getJob(jobId: string): Promise<AuraResponse<Job>>;
|
|
101
|
+
/**
|
|
102
|
+
* Enfile un job d'exécution asynchrone en arrière-plan pour une fonction.
|
|
103
|
+
*
|
|
104
|
+
* @param functionName Nom de la fonction à exécuter.
|
|
105
|
+
* @param options Paramètres optionnels : charge utile (`payload`), délai avant exécution (`delaySecs`)
|
|
106
|
+
* et nombre maximum de tentatives (`maxAttempts`).
|
|
107
|
+
*/
|
|
108
|
+
enqueueJob(functionName: string, options?: {
|
|
109
|
+
payload?: unknown;
|
|
110
|
+
delaySecs?: number;
|
|
111
|
+
maxAttempts?: number;
|
|
112
|
+
}): Promise<AuraResponse<Job>>;
|
|
101
113
|
listCronJobs(): Promise<AuraResponse<CronJob[]>>;
|
|
102
114
|
createCronJob(name: string, schedule: string, functionName: string, options?: {
|
|
103
115
|
payload?: unknown;
|
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]);
|
|
@@ -27,6 +27,30 @@ function toFunctionsError(err, context) {
|
|
|
27
27
|
const body = err.details != null ? err.details : { error: { code: err.code, message: err.message } };
|
|
28
28
|
return isRelayStatus(err.status) ? new FunctionsRelayError(err.status, body, err.details) : new FunctionsHttpError(err.status, body, err.details);
|
|
29
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");
|
|
53
|
+
}
|
|
30
54
|
var FunctionsService = class extends BaseService {
|
|
31
55
|
constructor(client) {
|
|
32
56
|
super(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,11 +139,63 @@ 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();
|
|
108
146
|
if (res2.error) return { data: null, error: toFunctionsError(res2.error) };
|
|
147
|
+
const rawData = res2.data;
|
|
148
|
+
if (rawData && typeof rawData === "object" && "body" in rawData && "status" in rawData) {
|
|
149
|
+
const invokeStatus = rawData.status;
|
|
150
|
+
const invokeBody = rawData.body;
|
|
151
|
+
if (typeof invokeStatus === "number" && invokeStatus >= 400) {
|
|
152
|
+
const errPayload = typeof invokeBody === "object" && invokeBody !== null ? invokeBody : invokeBody === null || invokeBody === void 0 || invokeBody === "" ? {} : { message: typeof invokeBody === "string" ? invokeBody : JSON.stringify(invokeBody) };
|
|
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}`;
|
|
154
|
+
const normalizedBody = {
|
|
155
|
+
...errPayload,
|
|
156
|
+
message: errorMsg,
|
|
157
|
+
error: {
|
|
158
|
+
code: typeof errPayload.code === "string" ? errPayload.code : `http_${invokeStatus}`,
|
|
159
|
+
message: errorMsg
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
return {
|
|
163
|
+
data: null,
|
|
164
|
+
error: new FunctionsHttpError(invokeStatus, normalizedBody, invokeBody)
|
|
165
|
+
};
|
|
166
|
+
}
|
|
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;
|
|
179
|
+
if (unwrappedData !== null && typeof unwrappedData === "object") {
|
|
180
|
+
if (!("body" in unwrappedData)) {
|
|
181
|
+
Object.defineProperty(unwrappedData, "body", {
|
|
182
|
+
value: unwrappedData,
|
|
183
|
+
enumerable: false,
|
|
184
|
+
writable: true,
|
|
185
|
+
configurable: true
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
if (!("status" in unwrappedData)) {
|
|
189
|
+
Object.defineProperty(unwrappedData, "status", {
|
|
190
|
+
value: invokeStatus,
|
|
191
|
+
enumerable: false,
|
|
192
|
+
writable: true,
|
|
193
|
+
configurable: true
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return { data: unwrappedData, error: null, meta: res2.meta };
|
|
198
|
+
}
|
|
109
199
|
return res2;
|
|
110
200
|
}
|
|
111
201
|
const client = this.client;
|
|
@@ -123,19 +213,34 @@ var FunctionsService = class extends BaseService {
|
|
|
123
213
|
};
|
|
124
214
|
if (store.token) headers["Authorization"] = `Bearer ${store.token}`;
|
|
125
215
|
if (client.apiKey) headers["apikey"] = client.apiKey;
|
|
126
|
-
const res = await client.fetchFn(this.url(`${this.prefix}/${slug}/invoke`), {
|
|
216
|
+
const res = await client.fetchFn(this.url(`${this.prefix}/${seg(slug)}/invoke`), {
|
|
127
217
|
method: "POST",
|
|
128
218
|
headers,
|
|
129
219
|
body: JSON.stringify(envelope),
|
|
130
220
|
signal: controller.signal
|
|
131
221
|
});
|
|
132
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
|
+
}
|
|
133
229
|
let parsed = null;
|
|
134
230
|
try {
|
|
135
|
-
parsed =
|
|
231
|
+
parsed = text.trim() ? JSON.parse(text) : null;
|
|
136
232
|
} catch {
|
|
137
233
|
}
|
|
138
|
-
|
|
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)) };
|
|
139
244
|
}
|
|
140
245
|
let data;
|
|
141
246
|
if (responseType === "text") data = await res.text();
|
|
@@ -169,7 +274,7 @@ var FunctionsService = class extends BaseService {
|
|
|
169
274
|
* le NOM l'emporte. Une valeur qui n'est ni l'un ni l'autre produit un 404 qui le dit.
|
|
170
275
|
*/
|
|
171
276
|
async get(nameOrId) {
|
|
172
|
-
return this.request("GET", `${this.prefix}/${nameOrId}`);
|
|
277
|
+
return this.request("GET", `${this.prefix}/${seg(nameOrId)}`);
|
|
173
278
|
}
|
|
174
279
|
// -- Jobs -------------------------------------------------------------------
|
|
175
280
|
async listJobs(options) {
|
|
@@ -180,7 +285,30 @@ var FunctionsService = class extends BaseService {
|
|
|
180
285
|
return this.request("GET", `${this.prefix}/jobs${qs}`);
|
|
181
286
|
}
|
|
182
287
|
async getJob(jobId) {
|
|
183
|
-
return this.request("GET", `${this.prefix}/jobs/${jobId}`);
|
|
288
|
+
return this.request("GET", `${this.prefix}/jobs/${seg(jobId)}`);
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Enfile un job d'exécution asynchrone en arrière-plan pour une fonction.
|
|
292
|
+
*
|
|
293
|
+
* @param functionName Nom de la fonction à exécuter.
|
|
294
|
+
* @param options Paramètres optionnels : charge utile (`payload`), délai avant exécution (`delaySecs`)
|
|
295
|
+
* et nombre maximum de tentatives (`maxAttempts`).
|
|
296
|
+
*/
|
|
297
|
+
async enqueueJob(functionName, options) {
|
|
298
|
+
if (!functionName || typeof functionName !== "string" || functionName.trim() === "") {
|
|
299
|
+
return {
|
|
300
|
+
data: null,
|
|
301
|
+
error: new AuraError(400, {
|
|
302
|
+
error: { code: "bad_request", message: "Function name is required to enqueue a job" }
|
|
303
|
+
})
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
return this.request("POST", `${this.prefix}/jobs`, {
|
|
307
|
+
function_name: functionName.trim(),
|
|
308
|
+
payload: options?.payload,
|
|
309
|
+
delay_secs: options?.delaySecs,
|
|
310
|
+
max_attempts: options?.maxAttempts
|
|
311
|
+
});
|
|
184
312
|
}
|
|
185
313
|
// -- Cron -------------------------------------------------------------------
|
|
186
314
|
async listCronJobs() {
|
|
@@ -195,7 +323,7 @@ var FunctionsService = class extends BaseService {
|
|
|
195
323
|
});
|
|
196
324
|
}
|
|
197
325
|
async deleteCronJob(name) {
|
|
198
|
-
return this.request("DELETE", `${this.prefix}/cron/${name}`);
|
|
326
|
+
return this.request("DELETE", `${this.prefix}/cron/${seg(name)}`);
|
|
199
327
|
}
|
|
200
328
|
};
|
|
201
329
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aurabase/functions",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
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.0"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
51
|
"@types/node": "20.14.15",
|