@hostwebhook/node-types 1.71.1 → 1.73.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bucket-operations.d.ts +40 -13
- package/dist/bucket-operations.js +175 -37
- package/dist/docs-operations.d.ts +39 -0
- package/dist/docs-operations.js +74 -4
- package/dist/esm/bucket-operations.d.ts +40 -13
- package/dist/esm/bucket-operations.js +175 -37
- package/dist/esm/docs-operations.d.ts +39 -0
- package/dist/esm/docs-operations.js +74 -4
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* El nodo de bucket: guardar, firmar y borrar objetos en un
|
|
3
|
-
* compatible con S3.
|
|
2
|
+
* El nodo de bucket: mirar, leer, guardar, firmar y borrar objetos en un
|
|
3
|
+
* almacenamiento compatible con S3.
|
|
4
4
|
*
|
|
5
5
|
* ## Por qué se llama `bucket` y no `r2`
|
|
6
6
|
*
|
|
@@ -19,24 +19,51 @@
|
|
|
19
19
|
* un enlace que se manda por correo. Sin esto no había forma de entregarle a un
|
|
20
20
|
* comprador una descarga temporal desde un flujo.
|
|
21
21
|
*
|
|
22
|
-
* ## Por qué
|
|
22
|
+
* ## Por qué siete, y qué sigue fuera
|
|
23
23
|
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
24
|
+
* Las cuatro primeras —`presignDownload`, `putObject`, `headObject`,
|
|
25
|
+
* `deleteObject`— actúan todas sobre un objeto que ya sabes nombrar. Faltaba lo
|
|
26
|
+
* contrario: **no había forma de mirar dentro del bucket**, ni de traerse el
|
|
27
|
+
* contenido de un objeto al flujo, ni de dejar que un tercero SUBA sin darle la
|
|
28
|
+
* credencial. Eso son las tres nuevas:
|
|
29
|
+
*
|
|
30
|
+
* - `listObjects` — qué ficheros y qué CARPETAS hay bajo un prefijo. S3 no
|
|
31
|
+
* tiene carpetas de verdad; con `delimiter` te las sintetiza como prefijos
|
|
32
|
+
* comunes, que es exactamente lo que una persona quiere ver.
|
|
33
|
+
* - `getObject` — el contenido, no un enlace al contenido. `presignDownload`
|
|
34
|
+
* da una URL para que la abra OTRO; cuando el paso de abajo necesita los
|
|
35
|
+
* bytes, una URL no sirve.
|
|
36
|
+
* - `presignUpload` — el simétrico de `presignDownload`.
|
|
37
|
+
*
|
|
38
|
+
* Aquí se decía que listar «pedía el SDK de AWS de verdad». No lo pide:
|
|
39
|
+
* `ListObjectsV2` contesta XML plano y un raspador corto lo lee. Lo que sí
|
|
40
|
+
* sigue pidiendo el SDK es la **subida multiparte** —reanudación, partes,
|
|
41
|
+
* reintentos por parte— y por eso ésa no está. Traer `@aws-sdk/*` el día que
|
|
42
|
+
* aparezca la operación que lo justifique sigue siendo más barato que al revés.
|
|
43
|
+
* Y un nodo con veinte operaciones es un nodo que nadie entiende.
|
|
29
44
|
*/
|
|
30
|
-
export declare const BUCKET_OPERATIONS: readonly ["presignDownload", "putObject", "headObject", "deleteObject"];
|
|
45
|
+
export declare const BUCKET_OPERATIONS: readonly ["presignDownload", "putObject", "headObject", "deleteObject", "listObjects", "getObject", "presignUpload"];
|
|
31
46
|
export type BucketOperation = (typeof BUCKET_OPERATIONS)[number];
|
|
32
47
|
export declare function isBucketOperation(value: unknown): value is BucketOperation;
|
|
33
48
|
/**
|
|
34
|
-
*
|
|
49
|
+
* Sólo `listObjects`.
|
|
35
50
|
*
|
|
36
51
|
* Una operación iterable es la que devuelve una COLECCIÓN sobre la que el nodo
|
|
37
|
-
* de abajo repite. Las
|
|
38
|
-
* viene detrás recibe un resultado, no una lista.
|
|
39
|
-
*
|
|
52
|
+
* de abajo repite. Las otras seis actúan sobre UN objeto —o devuelven UN
|
|
53
|
+
* enlace—, así que el que viene detrás recibe un resultado, no una lista.
|
|
54
|
+
*
|
|
55
|
+
* ⚠️ Dos cosas que hay que saber antes de implementarla, porque descubrirlas
|
|
56
|
+
* por las malas cuesta una tarde:
|
|
57
|
+
*
|
|
58
|
+
* 1. **Se itera `objects`, y sólo `objects`.** `folders` viaja en el
|
|
59
|
+
* contenedor junto a `truncated`, `nextToken`, `prefix` y `count`. Una
|
|
60
|
+
* operación tiene un campo iterable, y aquí es el de los ficheros; si las
|
|
61
|
+
* carpetas se iteraran también, el aviso de «hay más» se perdería justo en
|
|
62
|
+
* la operación que existe para darlo.
|
|
63
|
+
* 2. **Un prefijo sin nada da `count: 0`, y el nodo de abajo corre CERO
|
|
64
|
+
* veces**, sin error. Es la semántica correcta de una lista vacía, y es
|
|
65
|
+
* también la trampa clásica: parece que el flujo «no hizo nada» cuando en
|
|
66
|
+
* realidad hizo lo que debía.
|
|
40
67
|
*/
|
|
41
68
|
export declare const BUCKET_ITERABLE_OPERATIONS: readonly BucketOperation[];
|
|
42
69
|
export type BucketParamType =
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
* El nodo de bucket: guardar, firmar y borrar objetos en un
|
|
4
|
-
* compatible con S3.
|
|
3
|
+
* El nodo de bucket: mirar, leer, guardar, firmar y borrar objetos en un
|
|
4
|
+
* almacenamiento compatible con S3.
|
|
5
5
|
*
|
|
6
6
|
* ## Por qué se llama `bucket` y no `r2`
|
|
7
7
|
*
|
|
@@ -20,13 +20,28 @@
|
|
|
20
20
|
* un enlace que se manda por correo. Sin esto no había forma de entregarle a un
|
|
21
21
|
* comprador una descarga temporal desde un flujo.
|
|
22
22
|
*
|
|
23
|
-
* ## Por qué
|
|
23
|
+
* ## Por qué siete, y qué sigue fuera
|
|
24
24
|
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
25
|
+
* Las cuatro primeras —`presignDownload`, `putObject`, `headObject`,
|
|
26
|
+
* `deleteObject`— actúan todas sobre un objeto que ya sabes nombrar. Faltaba lo
|
|
27
|
+
* contrario: **no había forma de mirar dentro del bucket**, ni de traerse el
|
|
28
|
+
* contenido de un objeto al flujo, ni de dejar que un tercero SUBA sin darle la
|
|
29
|
+
* credencial. Eso son las tres nuevas:
|
|
30
|
+
*
|
|
31
|
+
* - `listObjects` — qué ficheros y qué CARPETAS hay bajo un prefijo. S3 no
|
|
32
|
+
* tiene carpetas de verdad; con `delimiter` te las sintetiza como prefijos
|
|
33
|
+
* comunes, que es exactamente lo que una persona quiere ver.
|
|
34
|
+
* - `getObject` — el contenido, no un enlace al contenido. `presignDownload`
|
|
35
|
+
* da una URL para que la abra OTRO; cuando el paso de abajo necesita los
|
|
36
|
+
* bytes, una URL no sirve.
|
|
37
|
+
* - `presignUpload` — el simétrico de `presignDownload`.
|
|
38
|
+
*
|
|
39
|
+
* Aquí se decía que listar «pedía el SDK de AWS de verdad». No lo pide:
|
|
40
|
+
* `ListObjectsV2` contesta XML plano y un raspador corto lo lee. Lo que sí
|
|
41
|
+
* sigue pidiendo el SDK es la **subida multiparte** —reanudación, partes,
|
|
42
|
+
* reintentos por parte— y por eso ésa no está. Traer `@aws-sdk/*` el día que
|
|
43
|
+
* aparezca la operación que lo justifique sigue siendo más barato que al revés.
|
|
44
|
+
* Y un nodo con veinte operaciones es un nodo que nadie entiende.
|
|
30
45
|
*/
|
|
31
46
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
32
47
|
exports.BUCKET_OPERATION_SPECS = exports.BUCKET_ITERABLE_OPERATIONS = exports.BUCKET_OPERATIONS = void 0;
|
|
@@ -36,20 +51,42 @@ exports.BUCKET_OPERATIONS = [
|
|
|
36
51
|
"putObject",
|
|
37
52
|
"headObject",
|
|
38
53
|
"deleteObject",
|
|
54
|
+
/* Las tres nuevas van AL FINAL y no intercaladas por tema: el orden de este
|
|
55
|
+
array es el orden del desplegable, y reordenar las cuatro de arriba le
|
|
56
|
+
mueve las filas de sitio a quien ya tiene el nodo en pantalla por un
|
|
57
|
+
cambio que no le aporta nada. El agrupado por tema vive en el dashboard,
|
|
58
|
+
que es donde se puede hacer sin tocar el contrato. */
|
|
59
|
+
"listObjects",
|
|
60
|
+
"getObject",
|
|
61
|
+
"presignUpload",
|
|
39
62
|
];
|
|
40
63
|
function isBucketOperation(value) {
|
|
41
64
|
return (typeof value === "string" &&
|
|
42
65
|
exports.BUCKET_OPERATIONS.includes(value));
|
|
43
66
|
}
|
|
44
67
|
/**
|
|
45
|
-
*
|
|
68
|
+
* Sólo `listObjects`.
|
|
46
69
|
*
|
|
47
70
|
* Una operación iterable es la que devuelve una COLECCIÓN sobre la que el nodo
|
|
48
|
-
* de abajo repite. Las
|
|
49
|
-
* viene detrás recibe un resultado, no una lista.
|
|
50
|
-
*
|
|
71
|
+
* de abajo repite. Las otras seis actúan sobre UN objeto —o devuelven UN
|
|
72
|
+
* enlace—, así que el que viene detrás recibe un resultado, no una lista.
|
|
73
|
+
*
|
|
74
|
+
* ⚠️ Dos cosas que hay que saber antes de implementarla, porque descubrirlas
|
|
75
|
+
* por las malas cuesta una tarde:
|
|
76
|
+
*
|
|
77
|
+
* 1. **Se itera `objects`, y sólo `objects`.** `folders` viaja en el
|
|
78
|
+
* contenedor junto a `truncated`, `nextToken`, `prefix` y `count`. Una
|
|
79
|
+
* operación tiene un campo iterable, y aquí es el de los ficheros; si las
|
|
80
|
+
* carpetas se iteraran también, el aviso de «hay más» se perdería justo en
|
|
81
|
+
* la operación que existe para darlo.
|
|
82
|
+
* 2. **Un prefijo sin nada da `count: 0`, y el nodo de abajo corre CERO
|
|
83
|
+
* veces**, sin error. Es la semántica correcta de una lista vacía, y es
|
|
84
|
+
* también la trampa clásica: parece que el flujo «no hizo nada» cuando en
|
|
85
|
+
* realidad hizo lo que debía.
|
|
51
86
|
*/
|
|
52
|
-
exports.BUCKET_ITERABLE_OPERATIONS = [
|
|
87
|
+
exports.BUCKET_ITERABLE_OPERATIONS = [
|
|
88
|
+
"listObjects",
|
|
89
|
+
];
|
|
53
90
|
/* Ayudantes — repetir el mismo objeto cuatro veces es cómo se desincronizan las
|
|
54
91
|
descripciones. */
|
|
55
92
|
const objectKey = (placeholder) => ({
|
|
@@ -60,6 +97,33 @@ const objectKey = (placeholder) => ({
|
|
|
60
97
|
placeholder,
|
|
61
98
|
description: "The path inside the bucket. No leading slash. Supports {{payload.x}}.",
|
|
62
99
|
});
|
|
100
|
+
/* El TTL de las dos operaciones que firman. El tope de siete días es de SigV4 y
|
|
101
|
+
no se puede pasar: firmar por más tiempo produce una URL que el proveedor
|
|
102
|
+
rechaza, y el error llega tarde y sin explicación. Va en un ayudante porque
|
|
103
|
+
con dos operaciones firmando, dos copias del número es una copia de más. */
|
|
104
|
+
const signatureTtl = () => ({
|
|
105
|
+
name: "expiresIn",
|
|
106
|
+
label: "Link valid for (seconds)",
|
|
107
|
+
type: "number",
|
|
108
|
+
required: true,
|
|
109
|
+
default: 3600,
|
|
110
|
+
min: 60,
|
|
111
|
+
max: 604800,
|
|
112
|
+
description: "From 60 s up to 7 days, the maximum the signature allows. Past that, the link stops working.",
|
|
113
|
+
});
|
|
114
|
+
/* El mismo interruptor en `headObject` y en `getObject`: las dos preguntan por
|
|
115
|
+
un objeto que puede no estar, y las dos lo resuelven con el `status` 404/200
|
|
116
|
+
y no con una excepción. Mismas etiquetas a propósito — son el mismo botón. */
|
|
117
|
+
const failIfMissing = () => ({
|
|
118
|
+
name: "failIfMissing",
|
|
119
|
+
label: "Fail when the object is missing",
|
|
120
|
+
type: "booleanSelect",
|
|
121
|
+
options: [
|
|
122
|
+
{ value: "true", label: "Yes — stop the flow" },
|
|
123
|
+
{ value: "false", label: "No — carry on with exists: false" },
|
|
124
|
+
],
|
|
125
|
+
description: 'With "No", the step succeeds and the node below decides. With "Yes", a missing object stops the flow.',
|
|
126
|
+
});
|
|
63
127
|
exports.BUCKET_OPERATION_SPECS = {
|
|
64
128
|
presignDownload: {
|
|
65
129
|
label: "Sign download link",
|
|
@@ -67,19 +131,7 @@ exports.BUCKET_OPERATION_SPECS = {
|
|
|
67
131
|
apiRoute: "GET (presigned)",
|
|
68
132
|
params: [
|
|
69
133
|
objectKey("cuadernillos/{{payload.orderId}}.pdf"),
|
|
70
|
-
|
|
71
|
-
name: "expiresIn",
|
|
72
|
-
label: "Link valid for (seconds)",
|
|
73
|
-
type: "number",
|
|
74
|
-
required: true,
|
|
75
|
-
default: 3600,
|
|
76
|
-
min: 60,
|
|
77
|
-
/* El tope de SigV4 es de siete días y no se puede pasar: firmar por
|
|
78
|
-
más tiempo produce una URL que el proveedor rechaza, y el error
|
|
79
|
-
llega tarde y sin explicación. */
|
|
80
|
-
max: 604800,
|
|
81
|
-
description: "From 60 s up to 7 days, the maximum the signature allows. Past that, the link stops working.",
|
|
82
|
-
},
|
|
134
|
+
signatureTtl(),
|
|
83
135
|
{
|
|
84
136
|
name: "downloadAs",
|
|
85
137
|
label: "Download as (filename)",
|
|
@@ -117,24 +169,110 @@ exports.BUCKET_OPERATION_SPECS = {
|
|
|
117
169
|
label: "Check object exists",
|
|
118
170
|
description: "Checks whether an object exists, and its size, without downloading it.",
|
|
119
171
|
apiRoute: "HEAD /{bucket}/{key}",
|
|
172
|
+
params: [objectKey("cuadernillos/{{payload.orderId}}.pdf"), failIfMissing()],
|
|
173
|
+
},
|
|
174
|
+
deleteObject: {
|
|
175
|
+
label: "Delete object",
|
|
176
|
+
description: "Deletes an object from the bucket. It cannot be undone.",
|
|
177
|
+
apiRoute: "DELETE /{bucket}/{key}",
|
|
178
|
+
params: [objectKey("temporales/{{payload.id}}.tmp")],
|
|
179
|
+
},
|
|
180
|
+
listObjects: {
|
|
181
|
+
label: "List objects",
|
|
182
|
+
description: "Lists the files and folders under a prefix. The node below runs once per file; folders travel alongside the list, they are not iterated. An empty prefix means zero runs, not an error.",
|
|
183
|
+
apiRoute: "GET /{bucket}?list-type=2",
|
|
120
184
|
params: [
|
|
121
|
-
objectKey(
|
|
185
|
+
/* NO usa `objectKey()`, y no es un descuido: ese ayudante marca
|
|
186
|
+
`required: true` y la validación de clave rechaza la cadena vacía. Un
|
|
187
|
+
prefijo puede ser vacío —el bucket entero— y puede acabar en barra,
|
|
188
|
+
que es justo lo que lo hace un prefijo y no una clave. */
|
|
122
189
|
{
|
|
123
|
-
name: "
|
|
124
|
-
label: "
|
|
190
|
+
name: "prefix",
|
|
191
|
+
label: "Prefix",
|
|
192
|
+
type: "template",
|
|
193
|
+
placeholder: "cuadernillos/2026/",
|
|
194
|
+
description: "Only keys starting with this. No leading slash. Leave it empty to list the whole bucket. Supports {{payload.x}}.",
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
name: "groupIntoFolders",
|
|
198
|
+
label: "Group into folders",
|
|
125
199
|
type: "booleanSelect",
|
|
126
200
|
options: [
|
|
127
|
-
{ value: "true", label: "Yes —
|
|
128
|
-
{ value: "false", label: "No —
|
|
201
|
+
{ value: "true", label: "Yes — one level, with folders" },
|
|
202
|
+
{ value: "false", label: "No — every key under the prefix" },
|
|
129
203
|
],
|
|
130
|
-
description: 'With "
|
|
204
|
+
description: 'With "Yes" you get the files at this level plus the folder names below it. With "No" you get every key under the prefix, however deep. Defaults to Yes.',
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
name: "maxKeys",
|
|
208
|
+
label: "Maximum objects",
|
|
209
|
+
type: "number",
|
|
210
|
+
default: 1000,
|
|
211
|
+
min: 1,
|
|
212
|
+
max: 5000,
|
|
213
|
+
/* El proveedor devuelve 1.000 por llamada como mucho, así que de 1.000
|
|
214
|
+
en adelante el ejecutor pagina por dentro. El techo de 5.000 no es
|
|
215
|
+
timidez: el contenedor viaja como JSON al nodo de abajo y se guarda
|
|
216
|
+
en el historial, y ahí ya hay un recorte a 256 KB por payload. */
|
|
217
|
+
description: "Up to 5000, paginated for you. When there are more, the step still succeeds: truncated says so and nextToken lets the next run carry on from there.",
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
name: "startAfter",
|
|
221
|
+
label: "Continue after",
|
|
222
|
+
type: "template",
|
|
223
|
+
advanced: true,
|
|
224
|
+
placeholder: "{{payload.nextToken}}",
|
|
225
|
+
description: "Resumes a listing: feed it the nextToken of the previous run. Leave it empty to start from the top.",
|
|
131
226
|
},
|
|
132
227
|
],
|
|
133
228
|
},
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
229
|
+
/* El tope de lectura NO es un parámetro: es del ejecutor, no una preferencia
|
|
230
|
+
por nodo. Y se dice en la descripción, no se descubre — al superarlo el
|
|
231
|
+
paso FALLA con el tamaño real y un puntero a `presignDownload`; nunca
|
|
232
|
+
devuelve un `content` a medias. Un contenido truncado se parsea, se manda
|
|
233
|
+
por correo y nadie se entera. */
|
|
234
|
+
getObject: {
|
|
235
|
+
label: "Read object",
|
|
236
|
+
description: "Downloads an object and puts its contents in the payload. Up to 5 MB; past that the step fails and points you at Sign download link.",
|
|
237
|
+
apiRoute: "GET /{bucket}/{key}",
|
|
238
|
+
params: [
|
|
239
|
+
objectKey("recibos/{{payload.orderId}}.json"),
|
|
240
|
+
{
|
|
241
|
+
name: "as",
|
|
242
|
+
label: "Read as",
|
|
243
|
+
type: "select",
|
|
244
|
+
required: true,
|
|
245
|
+
default: "text",
|
|
246
|
+
options: [
|
|
247
|
+
{ value: "text", label: "Text — UTF-8 into content" },
|
|
248
|
+
{ value: "json", label: "JSON — parsed into content" },
|
|
249
|
+
{ value: "base64", label: "Base64 — bytes into contentBase64" },
|
|
250
|
+
],
|
|
251
|
+
/* Lo binario se PIDE, no se adivina: un PDF leído como texto llega
|
|
252
|
+
como UTF-8 roto y el fallo aparece tres nodos más abajo. */
|
|
253
|
+
description: "Binary files need Base64 — read as text they arrive corrupted, and Base64 weighs a third more than the file. With JSON, a body that does not parse fails the step. Over 256 KB the contents still flow on whole, but the run history stores them trimmed.",
|
|
254
|
+
},
|
|
255
|
+
failIfMissing(),
|
|
256
|
+
],
|
|
257
|
+
},
|
|
258
|
+
presignUpload: {
|
|
259
|
+
label: "Sign upload link",
|
|
260
|
+
description: "Returns a temporary URL for someone else to PUT a file into the bucket, without giving them the credential.",
|
|
261
|
+
apiRoute: "PUT (presigned)",
|
|
262
|
+
params: [
|
|
263
|
+
objectKey("subidas/{{payload.orderId}}.pdf"),
|
|
264
|
+
signatureTtl(),
|
|
265
|
+
{
|
|
266
|
+
name: "contentType",
|
|
267
|
+
label: "Content type",
|
|
268
|
+
type: "template",
|
|
269
|
+
default: "application/octet-stream",
|
|
270
|
+
placeholder: "application/pdf",
|
|
271
|
+
/* NO se firma. Meterlo en `signedHeaders` obliga al que sube a mandar
|
|
272
|
+
esa cabecera exacta o comerse un 403 que no puede depurar, y quien
|
|
273
|
+
recibe el enlace no suele controlar su cliente HTTP. */
|
|
274
|
+
description: "What the object will be stored as. It is not enforced — the bucket stores whatever arrives.",
|
|
275
|
+
},
|
|
276
|
+
],
|
|
139
277
|
},
|
|
140
278
|
};
|
|
@@ -33,6 +33,28 @@
|
|
|
33
33
|
* Los `slot` siguen haciendo falta: separan la identidad del documento de los
|
|
34
34
|
* campos de la operación, aunque ahora las dos vayan debajo del selector. Un
|
|
35
35
|
* bucle único ordenaría por el array de params y no por ese criterio.
|
|
36
|
+
*
|
|
37
|
+
* ── Qué exige el motor (y por qué está escrito aquí) ──
|
|
38
|
+
* Este esquema declaraba CERO obligatorios en las cinco operaciones, y el
|
|
39
|
+
* ejecutor —`api/src/nodes/docs-actions/docs-actions.service.ts`— rechaza cinco
|
|
40
|
+
* cosas. Esa distancia no se veía como un error de validación sino como un
|
|
41
|
+
* nodo que no guía: sin ningún obligatorio no hay nada que esperar, ningún paso
|
|
42
|
+
* desbloquea al siguiente, y el guiado que el panel SÍ monta no se nota.
|
|
43
|
+
*
|
|
44
|
+
* Los cinco 400 del ejecutor, uno por línea, que son los cinco `required` de
|
|
45
|
+
* este fichero:
|
|
46
|
+
*
|
|
47
|
+
* :323 readDoc documentId 'Document ID is required'
|
|
48
|
+
* :440 appendText documentId 'Document ID is required'
|
|
49
|
+
* :447 appendText contentTemplate 'Content template is required'
|
|
50
|
+
* :498 replaceText documentId 'Document ID is required'
|
|
51
|
+
* :505 replaceText searchText 'Search text is required'
|
|
52
|
+
* :560 insertTable documentId 'Document ID is required'
|
|
53
|
+
*
|
|
54
|
+
* Y los cuatro que NO se marcan, con el defecto que los salva: `documentTitle`
|
|
55
|
+
* cae en `entity.name`, `contentTemplate` de `createDoc` en un documento vacío,
|
|
56
|
+
* `tableRows`/`tableCols` en `|| 3`, y `replaceWith` vacío es un BORRADO, no un
|
|
57
|
+
* hueco. Cada uno lo explica en su sitio.
|
|
36
58
|
*/
|
|
37
59
|
export declare const DOCS_OPERATIONS: readonly ["readDoc", "createDoc", "appendText", "replaceText", "insertTable"];
|
|
38
60
|
export type DocsOperation = (typeof DOCS_OPERATIONS)[number];
|
|
@@ -67,6 +89,23 @@ export interface DocsParamSpec {
|
|
|
67
89
|
label: string;
|
|
68
90
|
type: DocsParamType;
|
|
69
91
|
slot: DocsParamSlot;
|
|
92
|
+
/**
|
|
93
|
+
* Lo exige el EJECUTOR: sin él, `docs-actions.service.ts` devuelve un 400 y
|
|
94
|
+
* el nodo no corre.
|
|
95
|
+
*
|
|
96
|
+
* No es «lo que conviene rellenar», y la diferencia importa en las dos
|
|
97
|
+
* direcciones. De menos, un obligatorio que falta deja la familia con cero y
|
|
98
|
+
* `esGuiable` (`check-cuantos-faltan-por-guiar.mjs`) la echa del censo: el
|
|
99
|
+
* panel monta `useGuidedConfig`, pero sin nada que esperar ningún paso
|
|
100
|
+
* desbloquea al siguiente y el guiado se ve como si no existiera — que es el
|
|
101
|
+
* fallo que este campo arregla. De más, marcar algo que el ejecutor SÍ acepta
|
|
102
|
+
* vacío esconde tras un hueco imposible un nodo que habría funcionado, porque
|
|
103
|
+
* `frena()` sólo mira `obligatorio && !contestado`.
|
|
104
|
+
*
|
|
105
|
+
* Por eso cada `required: true` de este fichero apunta a la línea que lo
|
|
106
|
+
* exige, y los que no lo llevan dicen dónde el ejecutor pone su defecto.
|
|
107
|
+
*/
|
|
108
|
+
required?: boolean;
|
|
70
109
|
/** Texto de ayuda. Llano: el paquete no lleva React. */
|
|
71
110
|
description?: string;
|
|
72
111
|
placeholder?: string;
|
package/dist/docs-operations.js
CHANGED
|
@@ -34,6 +34,28 @@
|
|
|
34
34
|
* Los `slot` siguen haciendo falta: separan la identidad del documento de los
|
|
35
35
|
* campos de la operación, aunque ahora las dos vayan debajo del selector. Un
|
|
36
36
|
* bucle único ordenaría por el array de params y no por ese criterio.
|
|
37
|
+
*
|
|
38
|
+
* ── Qué exige el motor (y por qué está escrito aquí) ──
|
|
39
|
+
* Este esquema declaraba CERO obligatorios en las cinco operaciones, y el
|
|
40
|
+
* ejecutor —`api/src/nodes/docs-actions/docs-actions.service.ts`— rechaza cinco
|
|
41
|
+
* cosas. Esa distancia no se veía como un error de validación sino como un
|
|
42
|
+
* nodo que no guía: sin ningún obligatorio no hay nada que esperar, ningún paso
|
|
43
|
+
* desbloquea al siguiente, y el guiado que el panel SÍ monta no se nota.
|
|
44
|
+
*
|
|
45
|
+
* Los cinco 400 del ejecutor, uno por línea, que son los cinco `required` de
|
|
46
|
+
* este fichero:
|
|
47
|
+
*
|
|
48
|
+
* :323 readDoc documentId 'Document ID is required'
|
|
49
|
+
* :440 appendText documentId 'Document ID is required'
|
|
50
|
+
* :447 appendText contentTemplate 'Content template is required'
|
|
51
|
+
* :498 replaceText documentId 'Document ID is required'
|
|
52
|
+
* :505 replaceText searchText 'Search text is required'
|
|
53
|
+
* :560 insertTable documentId 'Document ID is required'
|
|
54
|
+
*
|
|
55
|
+
* Y los cuatro que NO se marcan, con el defecto que los salva: `documentTitle`
|
|
56
|
+
* cae en `entity.name`, `contentTemplate` de `createDoc` en un documento vacío,
|
|
57
|
+
* `tableRows`/`tableCols` en `|| 3`, y `replaceWith` vacío es un BORRADO, no un
|
|
58
|
+
* hueco. Cada uno lo explica en su sitio.
|
|
37
59
|
*/
|
|
38
60
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
61
|
exports.DOCS_OPERATION_SPECS = exports.DOCS_OPERATIONS = void 0;
|
|
@@ -50,19 +72,42 @@ function isDocsOperation(value) {
|
|
|
50
72
|
return typeof value === 'string' && exports.DOCS_OPERATIONS.includes(value);
|
|
51
73
|
}
|
|
52
74
|
/* Los campos que comparten varias operaciones. */
|
|
75
|
+
/**
|
|
76
|
+
* `required: true` va aquí y no operación por operación porque las CUATRO que
|
|
77
|
+
* lo usan lo exigen: `executeRead` (:323), `executeAppend` (:440),
|
|
78
|
+
* `executeReplace` (:498) y `executeInsertTable` (:560) devuelven todas el
|
|
79
|
+
* mismo 400, «Document ID is required». La quinta, `createDoc`, no llama a este
|
|
80
|
+
* ayudante — el documento aún no existe.
|
|
81
|
+
*/
|
|
53
82
|
const documentId = () => ({
|
|
54
83
|
name: 'documentId',
|
|
55
84
|
label: 'Or paste Document ID / template',
|
|
56
85
|
type: 'docPicker',
|
|
57
86
|
slot: 'document',
|
|
87
|
+
required: true,
|
|
58
88
|
placeholder: '1BxiMVs0XRA5nFMdKvBd... or {{payload.documentId}}',
|
|
59
89
|
});
|
|
60
|
-
|
|
90
|
+
/**
|
|
91
|
+
* Éste NO puede llevar `required` en el ayudante, y es la única asimetría del
|
|
92
|
+
* fichero: sus dos usuarios no piden lo mismo.
|
|
93
|
+
*
|
|
94
|
+
* `appendText` lo exige (`executeAppend` :447, «Content template is required»):
|
|
95
|
+
* añadir la cadena vacía al final de un documento no es una operación, es un
|
|
96
|
+
* viaje a Google para no hacer nada. `createDoc` no (`executeCreate` :362 lo
|
|
97
|
+
* lee con un ternario y :375 sólo inserta `if (content)`): un documento nuevo y
|
|
98
|
+
* vacío es un resultado legítimo, y bloquearlo sería inventarse un requisito
|
|
99
|
+
* que la api no tiene.
|
|
100
|
+
*
|
|
101
|
+
* De ahí el `over`, como el `columnMapping` de Sheets: el que lo exige lo dice
|
|
102
|
+
* en su sitio.
|
|
103
|
+
*/
|
|
104
|
+
const contentTemplate = (over = {}) => ({
|
|
61
105
|
name: 'contentTemplate',
|
|
62
106
|
label: 'Content',
|
|
63
107
|
type: 'htmlTemplate',
|
|
64
108
|
slot: 'config',
|
|
65
109
|
placeholder: 'Text to insert... supports {{payload.x}} templates',
|
|
110
|
+
...over,
|
|
66
111
|
});
|
|
67
112
|
exports.DOCS_OPERATION_SPECS = {
|
|
68
113
|
readDoc: {
|
|
@@ -77,13 +122,22 @@ exports.DOCS_OPERATION_SPECS = {
|
|
|
77
122
|
labelShort: 'Create',
|
|
78
123
|
description: 'Create a new Google Doc',
|
|
79
124
|
/* La única SIN `documentId`: aquí el documento no existe todavía. Eso es lo
|
|
80
|
-
que en la página eran dos compuertas negadas (`!== "createDoc"`).
|
|
125
|
+
que en la página eran dos compuertas negadas (`!== "createDoc"`).
|
|
126
|
+
|
|
127
|
+
Y la única con CERO obligatorios, porque el ejecutor no exige ninguno:
|
|
128
|
+
`executeCreate` no tiene un solo `return 400`. Un `createDoc` sin nada
|
|
129
|
+
contestado corre y crea un documento vacío con el nombre del nodo. */
|
|
81
130
|
params: [
|
|
82
131
|
{
|
|
83
132
|
name: 'documentTitle',
|
|
84
133
|
label: 'Document Title',
|
|
85
134
|
type: 'template',
|
|
86
135
|
slot: 'document',
|
|
136
|
+
/* Opcional a propósito: `executeCreate` (:361) lo lee como
|
|
137
|
+
`render(entity.documentTitle || entity.name)`. Sin título el
|
|
138
|
+
documento sale con el NOMBRE DEL NODO, que es un defecto de verdad y
|
|
139
|
+
no un descuido. Marcarlo obligatorio pararía la guía delante de un
|
|
140
|
+
campo que la api sabe rellenar sola. */
|
|
87
141
|
placeholder: 'Report — {{payload.date}} or My Document',
|
|
88
142
|
},
|
|
89
143
|
contentTemplate(),
|
|
@@ -93,7 +147,7 @@ exports.DOCS_OPERATION_SPECS = {
|
|
|
93
147
|
label: 'Append Text',
|
|
94
148
|
labelShort: 'Append',
|
|
95
149
|
description: 'Add text to end of document',
|
|
96
|
-
params: [documentId(), contentTemplate()],
|
|
150
|
+
params: [documentId(), contentTemplate({ required: true })],
|
|
97
151
|
},
|
|
98
152
|
replaceText: {
|
|
99
153
|
label: 'Replace Text',
|
|
@@ -106,6 +160,9 @@ exports.DOCS_OPERATION_SPECS = {
|
|
|
106
160
|
label: 'Search Text',
|
|
107
161
|
type: 'template',
|
|
108
162
|
slot: 'config',
|
|
163
|
+
/* `executeReplace` (:505): «Search text is required». Sin qué buscar,
|
|
164
|
+
`replaceAllText` no tiene nada que hacer. */
|
|
165
|
+
required: true,
|
|
109
166
|
placeholder: '{{PLACEHOLDER}} or text to find',
|
|
110
167
|
},
|
|
111
168
|
{
|
|
@@ -113,6 +170,11 @@ exports.DOCS_OPERATION_SPECS = {
|
|
|
113
170
|
label: 'Replace With',
|
|
114
171
|
type: 'template',
|
|
115
172
|
slot: 'config',
|
|
173
|
+
/* Opcional, y aquí el vacío SIGNIFICA algo: `executeReplace` lo pasa
|
|
174
|
+
tal cual a `replaceAllText`, así que dejarlo en blanco BORRA todas
|
|
175
|
+
las apariciones de `searchText`. Es la única forma de pedir un
|
|
176
|
+
borrado, y por eso el ejecutor no lo comprueba: hacerlo obligatorio
|
|
177
|
+
quitaría una operación que hoy funciona. */
|
|
116
178
|
placeholder: '{{payload.value}} or replacement text',
|
|
117
179
|
},
|
|
118
180
|
],
|
|
@@ -123,7 +185,15 @@ exports.DOCS_OPERATION_SPECS = {
|
|
|
123
185
|
description: 'Insert a table at end of document',
|
|
124
186
|
params: [
|
|
125
187
|
documentId(),
|
|
126
|
-
/* Los topes son distintos y no es un descuido: 50 filas, 20 columnas.
|
|
188
|
+
/* Los topes son distintos y no es un descuido: 50 filas, 20 columnas.
|
|
189
|
+
|
|
190
|
+
Y los dos siguen OPCIONALES teniendo `default: 3`, que es justo el
|
|
191
|
+
caso que el contrato vigila: `executeInsertTable` (:555) los lee con
|
|
192
|
+
`entity.tableRows || 3`, o sea que el 3×3 lo pone la api tanto si el
|
|
193
|
+
formulario lo manda como si no. Un obligatorio con `default` sería
|
|
194
|
+
además la trampa de la regla 10 — `defectoPintable` le borra el
|
|
195
|
+
`default` para no pintar como contestado algo que nadie eligió, y
|
|
196
|
+
estos dos sí quieren pintarlo. */
|
|
127
197
|
{ name: 'tableRows', label: 'Rows', type: 'number', slot: 'config', min: 1, max: 50, default: 3 },
|
|
128
198
|
{ name: 'tableCols', label: 'Columns', type: 'number', slot: 'config', min: 1, max: 20, default: 3 },
|
|
129
199
|
],
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* El nodo de bucket: guardar, firmar y borrar objetos en un
|
|
3
|
-
* compatible con S3.
|
|
2
|
+
* El nodo de bucket: mirar, leer, guardar, firmar y borrar objetos en un
|
|
3
|
+
* almacenamiento compatible con S3.
|
|
4
4
|
*
|
|
5
5
|
* ## Por qué se llama `bucket` y no `r2`
|
|
6
6
|
*
|
|
@@ -19,24 +19,51 @@
|
|
|
19
19
|
* un enlace que se manda por correo. Sin esto no había forma de entregarle a un
|
|
20
20
|
* comprador una descarga temporal desde un flujo.
|
|
21
21
|
*
|
|
22
|
-
* ## Por qué
|
|
22
|
+
* ## Por qué siete, y qué sigue fuera
|
|
23
23
|
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
24
|
+
* Las cuatro primeras —`presignDownload`, `putObject`, `headObject`,
|
|
25
|
+
* `deleteObject`— actúan todas sobre un objeto que ya sabes nombrar. Faltaba lo
|
|
26
|
+
* contrario: **no había forma de mirar dentro del bucket**, ni de traerse el
|
|
27
|
+
* contenido de un objeto al flujo, ni de dejar que un tercero SUBA sin darle la
|
|
28
|
+
* credencial. Eso son las tres nuevas:
|
|
29
|
+
*
|
|
30
|
+
* - `listObjects` — qué ficheros y qué CARPETAS hay bajo un prefijo. S3 no
|
|
31
|
+
* tiene carpetas de verdad; con `delimiter` te las sintetiza como prefijos
|
|
32
|
+
* comunes, que es exactamente lo que una persona quiere ver.
|
|
33
|
+
* - `getObject` — el contenido, no un enlace al contenido. `presignDownload`
|
|
34
|
+
* da una URL para que la abra OTRO; cuando el paso de abajo necesita los
|
|
35
|
+
* bytes, una URL no sirve.
|
|
36
|
+
* - `presignUpload` — el simétrico de `presignDownload`.
|
|
37
|
+
*
|
|
38
|
+
* Aquí se decía que listar «pedía el SDK de AWS de verdad». No lo pide:
|
|
39
|
+
* `ListObjectsV2` contesta XML plano y un raspador corto lo lee. Lo que sí
|
|
40
|
+
* sigue pidiendo el SDK es la **subida multiparte** —reanudación, partes,
|
|
41
|
+
* reintentos por parte— y por eso ésa no está. Traer `@aws-sdk/*` el día que
|
|
42
|
+
* aparezca la operación que lo justifique sigue siendo más barato que al revés.
|
|
43
|
+
* Y un nodo con veinte operaciones es un nodo que nadie entiende.
|
|
29
44
|
*/
|
|
30
|
-
export declare const BUCKET_OPERATIONS: readonly ["presignDownload", "putObject", "headObject", "deleteObject"];
|
|
45
|
+
export declare const BUCKET_OPERATIONS: readonly ["presignDownload", "putObject", "headObject", "deleteObject", "listObjects", "getObject", "presignUpload"];
|
|
31
46
|
export type BucketOperation = (typeof BUCKET_OPERATIONS)[number];
|
|
32
47
|
export declare function isBucketOperation(value: unknown): value is BucketOperation;
|
|
33
48
|
/**
|
|
34
|
-
*
|
|
49
|
+
* Sólo `listObjects`.
|
|
35
50
|
*
|
|
36
51
|
* Una operación iterable es la que devuelve una COLECCIÓN sobre la que el nodo
|
|
37
|
-
* de abajo repite. Las
|
|
38
|
-
* viene detrás recibe un resultado, no una lista.
|
|
39
|
-
*
|
|
52
|
+
* de abajo repite. Las otras seis actúan sobre UN objeto —o devuelven UN
|
|
53
|
+
* enlace—, así que el que viene detrás recibe un resultado, no una lista.
|
|
54
|
+
*
|
|
55
|
+
* ⚠️ Dos cosas que hay que saber antes de implementarla, porque descubrirlas
|
|
56
|
+
* por las malas cuesta una tarde:
|
|
57
|
+
*
|
|
58
|
+
* 1. **Se itera `objects`, y sólo `objects`.** `folders` viaja en el
|
|
59
|
+
* contenedor junto a `truncated`, `nextToken`, `prefix` y `count`. Una
|
|
60
|
+
* operación tiene un campo iterable, y aquí es el de los ficheros; si las
|
|
61
|
+
* carpetas se iteraran también, el aviso de «hay más» se perdería justo en
|
|
62
|
+
* la operación que existe para darlo.
|
|
63
|
+
* 2. **Un prefijo sin nada da `count: 0`, y el nodo de abajo corre CERO
|
|
64
|
+
* veces**, sin error. Es la semántica correcta de una lista vacía, y es
|
|
65
|
+
* también la trampa clásica: parece que el flujo «no hizo nada» cuando en
|
|
66
|
+
* realidad hizo lo que debía.
|
|
40
67
|
*/
|
|
41
68
|
export declare const BUCKET_ITERABLE_OPERATIONS: readonly BucketOperation[];
|
|
42
69
|
export type BucketParamType =
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* El nodo de bucket: guardar, firmar y borrar objetos en un
|
|
3
|
-
* compatible con S3.
|
|
2
|
+
* El nodo de bucket: mirar, leer, guardar, firmar y borrar objetos en un
|
|
3
|
+
* almacenamiento compatible con S3.
|
|
4
4
|
*
|
|
5
5
|
* ## Por qué se llama `bucket` y no `r2`
|
|
6
6
|
*
|
|
@@ -19,33 +19,70 @@
|
|
|
19
19
|
* un enlace que se manda por correo. Sin esto no había forma de entregarle a un
|
|
20
20
|
* comprador una descarga temporal desde un flujo.
|
|
21
21
|
*
|
|
22
|
-
* ## Por qué
|
|
22
|
+
* ## Por qué siete, y qué sigue fuera
|
|
23
23
|
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
24
|
+
* Las cuatro primeras —`presignDownload`, `putObject`, `headObject`,
|
|
25
|
+
* `deleteObject`— actúan todas sobre un objeto que ya sabes nombrar. Faltaba lo
|
|
26
|
+
* contrario: **no había forma de mirar dentro del bucket**, ni de traerse el
|
|
27
|
+
* contenido de un objeto al flujo, ni de dejar que un tercero SUBA sin darle la
|
|
28
|
+
* credencial. Eso son las tres nuevas:
|
|
29
|
+
*
|
|
30
|
+
* - `listObjects` — qué ficheros y qué CARPETAS hay bajo un prefijo. S3 no
|
|
31
|
+
* tiene carpetas de verdad; con `delimiter` te las sintetiza como prefijos
|
|
32
|
+
* comunes, que es exactamente lo que una persona quiere ver.
|
|
33
|
+
* - `getObject` — el contenido, no un enlace al contenido. `presignDownload`
|
|
34
|
+
* da una URL para que la abra OTRO; cuando el paso de abajo necesita los
|
|
35
|
+
* bytes, una URL no sirve.
|
|
36
|
+
* - `presignUpload` — el simétrico de `presignDownload`.
|
|
37
|
+
*
|
|
38
|
+
* Aquí se decía que listar «pedía el SDK de AWS de verdad». No lo pide:
|
|
39
|
+
* `ListObjectsV2` contesta XML plano y un raspador corto lo lee. Lo que sí
|
|
40
|
+
* sigue pidiendo el SDK es la **subida multiparte** —reanudación, partes,
|
|
41
|
+
* reintentos por parte— y por eso ésa no está. Traer `@aws-sdk/*` el día que
|
|
42
|
+
* aparezca la operación que lo justifique sigue siendo más barato que al revés.
|
|
43
|
+
* Y un nodo con veinte operaciones es un nodo que nadie entiende.
|
|
29
44
|
*/
|
|
30
45
|
export const BUCKET_OPERATIONS = [
|
|
31
46
|
"presignDownload",
|
|
32
47
|
"putObject",
|
|
33
48
|
"headObject",
|
|
34
49
|
"deleteObject",
|
|
50
|
+
/* Las tres nuevas van AL FINAL y no intercaladas por tema: el orden de este
|
|
51
|
+
array es el orden del desplegable, y reordenar las cuatro de arriba le
|
|
52
|
+
mueve las filas de sitio a quien ya tiene el nodo en pantalla por un
|
|
53
|
+
cambio que no le aporta nada. El agrupado por tema vive en el dashboard,
|
|
54
|
+
que es donde se puede hacer sin tocar el contrato. */
|
|
55
|
+
"listObjects",
|
|
56
|
+
"getObject",
|
|
57
|
+
"presignUpload",
|
|
35
58
|
];
|
|
36
59
|
export function isBucketOperation(value) {
|
|
37
60
|
return (typeof value === "string" &&
|
|
38
61
|
BUCKET_OPERATIONS.includes(value));
|
|
39
62
|
}
|
|
40
63
|
/**
|
|
41
|
-
*
|
|
64
|
+
* Sólo `listObjects`.
|
|
42
65
|
*
|
|
43
66
|
* Una operación iterable es la que devuelve una COLECCIÓN sobre la que el nodo
|
|
44
|
-
* de abajo repite. Las
|
|
45
|
-
* viene detrás recibe un resultado, no una lista.
|
|
46
|
-
*
|
|
67
|
+
* de abajo repite. Las otras seis actúan sobre UN objeto —o devuelven UN
|
|
68
|
+
* enlace—, así que el que viene detrás recibe un resultado, no una lista.
|
|
69
|
+
*
|
|
70
|
+
* ⚠️ Dos cosas que hay que saber antes de implementarla, porque descubrirlas
|
|
71
|
+
* por las malas cuesta una tarde:
|
|
72
|
+
*
|
|
73
|
+
* 1. **Se itera `objects`, y sólo `objects`.** `folders` viaja en el
|
|
74
|
+
* contenedor junto a `truncated`, `nextToken`, `prefix` y `count`. Una
|
|
75
|
+
* operación tiene un campo iterable, y aquí es el de los ficheros; si las
|
|
76
|
+
* carpetas se iteraran también, el aviso de «hay más» se perdería justo en
|
|
77
|
+
* la operación que existe para darlo.
|
|
78
|
+
* 2. **Un prefijo sin nada da `count: 0`, y el nodo de abajo corre CERO
|
|
79
|
+
* veces**, sin error. Es la semántica correcta de una lista vacía, y es
|
|
80
|
+
* también la trampa clásica: parece que el flujo «no hizo nada» cuando en
|
|
81
|
+
* realidad hizo lo que debía.
|
|
47
82
|
*/
|
|
48
|
-
export const BUCKET_ITERABLE_OPERATIONS = [
|
|
83
|
+
export const BUCKET_ITERABLE_OPERATIONS = [
|
|
84
|
+
"listObjects",
|
|
85
|
+
];
|
|
49
86
|
/* Ayudantes — repetir el mismo objeto cuatro veces es cómo se desincronizan las
|
|
50
87
|
descripciones. */
|
|
51
88
|
const objectKey = (placeholder) => ({
|
|
@@ -56,6 +93,33 @@ const objectKey = (placeholder) => ({
|
|
|
56
93
|
placeholder,
|
|
57
94
|
description: "The path inside the bucket. No leading slash. Supports {{payload.x}}.",
|
|
58
95
|
});
|
|
96
|
+
/* El TTL de las dos operaciones que firman. El tope de siete días es de SigV4 y
|
|
97
|
+
no se puede pasar: firmar por más tiempo produce una URL que el proveedor
|
|
98
|
+
rechaza, y el error llega tarde y sin explicación. Va en un ayudante porque
|
|
99
|
+
con dos operaciones firmando, dos copias del número es una copia de más. */
|
|
100
|
+
const signatureTtl = () => ({
|
|
101
|
+
name: "expiresIn",
|
|
102
|
+
label: "Link valid for (seconds)",
|
|
103
|
+
type: "number",
|
|
104
|
+
required: true,
|
|
105
|
+
default: 3600,
|
|
106
|
+
min: 60,
|
|
107
|
+
max: 604800,
|
|
108
|
+
description: "From 60 s up to 7 days, the maximum the signature allows. Past that, the link stops working.",
|
|
109
|
+
});
|
|
110
|
+
/* El mismo interruptor en `headObject` y en `getObject`: las dos preguntan por
|
|
111
|
+
un objeto que puede no estar, y las dos lo resuelven con el `status` 404/200
|
|
112
|
+
y no con una excepción. Mismas etiquetas a propósito — son el mismo botón. */
|
|
113
|
+
const failIfMissing = () => ({
|
|
114
|
+
name: "failIfMissing",
|
|
115
|
+
label: "Fail when the object is missing",
|
|
116
|
+
type: "booleanSelect",
|
|
117
|
+
options: [
|
|
118
|
+
{ value: "true", label: "Yes — stop the flow" },
|
|
119
|
+
{ value: "false", label: "No — carry on with exists: false" },
|
|
120
|
+
],
|
|
121
|
+
description: 'With "No", the step succeeds and the node below decides. With "Yes", a missing object stops the flow.',
|
|
122
|
+
});
|
|
59
123
|
export const BUCKET_OPERATION_SPECS = {
|
|
60
124
|
presignDownload: {
|
|
61
125
|
label: "Sign download link",
|
|
@@ -63,19 +127,7 @@ export const BUCKET_OPERATION_SPECS = {
|
|
|
63
127
|
apiRoute: "GET (presigned)",
|
|
64
128
|
params: [
|
|
65
129
|
objectKey("cuadernillos/{{payload.orderId}}.pdf"),
|
|
66
|
-
|
|
67
|
-
name: "expiresIn",
|
|
68
|
-
label: "Link valid for (seconds)",
|
|
69
|
-
type: "number",
|
|
70
|
-
required: true,
|
|
71
|
-
default: 3600,
|
|
72
|
-
min: 60,
|
|
73
|
-
/* El tope de SigV4 es de siete días y no se puede pasar: firmar por
|
|
74
|
-
más tiempo produce una URL que el proveedor rechaza, y el error
|
|
75
|
-
llega tarde y sin explicación. */
|
|
76
|
-
max: 604800,
|
|
77
|
-
description: "From 60 s up to 7 days, the maximum the signature allows. Past that, the link stops working.",
|
|
78
|
-
},
|
|
130
|
+
signatureTtl(),
|
|
79
131
|
{
|
|
80
132
|
name: "downloadAs",
|
|
81
133
|
label: "Download as (filename)",
|
|
@@ -113,24 +165,110 @@ export const BUCKET_OPERATION_SPECS = {
|
|
|
113
165
|
label: "Check object exists",
|
|
114
166
|
description: "Checks whether an object exists, and its size, without downloading it.",
|
|
115
167
|
apiRoute: "HEAD /{bucket}/{key}",
|
|
168
|
+
params: [objectKey("cuadernillos/{{payload.orderId}}.pdf"), failIfMissing()],
|
|
169
|
+
},
|
|
170
|
+
deleteObject: {
|
|
171
|
+
label: "Delete object",
|
|
172
|
+
description: "Deletes an object from the bucket. It cannot be undone.",
|
|
173
|
+
apiRoute: "DELETE /{bucket}/{key}",
|
|
174
|
+
params: [objectKey("temporales/{{payload.id}}.tmp")],
|
|
175
|
+
},
|
|
176
|
+
listObjects: {
|
|
177
|
+
label: "List objects",
|
|
178
|
+
description: "Lists the files and folders under a prefix. The node below runs once per file; folders travel alongside the list, they are not iterated. An empty prefix means zero runs, not an error.",
|
|
179
|
+
apiRoute: "GET /{bucket}?list-type=2",
|
|
116
180
|
params: [
|
|
117
|
-
objectKey(
|
|
181
|
+
/* NO usa `objectKey()`, y no es un descuido: ese ayudante marca
|
|
182
|
+
`required: true` y la validación de clave rechaza la cadena vacía. Un
|
|
183
|
+
prefijo puede ser vacío —el bucket entero— y puede acabar en barra,
|
|
184
|
+
que es justo lo que lo hace un prefijo y no una clave. */
|
|
118
185
|
{
|
|
119
|
-
name: "
|
|
120
|
-
label: "
|
|
186
|
+
name: "prefix",
|
|
187
|
+
label: "Prefix",
|
|
188
|
+
type: "template",
|
|
189
|
+
placeholder: "cuadernillos/2026/",
|
|
190
|
+
description: "Only keys starting with this. No leading slash. Leave it empty to list the whole bucket. Supports {{payload.x}}.",
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
name: "groupIntoFolders",
|
|
194
|
+
label: "Group into folders",
|
|
121
195
|
type: "booleanSelect",
|
|
122
196
|
options: [
|
|
123
|
-
{ value: "true", label: "Yes —
|
|
124
|
-
{ value: "false", label: "No —
|
|
197
|
+
{ value: "true", label: "Yes — one level, with folders" },
|
|
198
|
+
{ value: "false", label: "No — every key under the prefix" },
|
|
125
199
|
],
|
|
126
|
-
description: 'With "
|
|
200
|
+
description: 'With "Yes" you get the files at this level plus the folder names below it. With "No" you get every key under the prefix, however deep. Defaults to Yes.',
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
name: "maxKeys",
|
|
204
|
+
label: "Maximum objects",
|
|
205
|
+
type: "number",
|
|
206
|
+
default: 1000,
|
|
207
|
+
min: 1,
|
|
208
|
+
max: 5000,
|
|
209
|
+
/* El proveedor devuelve 1.000 por llamada como mucho, así que de 1.000
|
|
210
|
+
en adelante el ejecutor pagina por dentro. El techo de 5.000 no es
|
|
211
|
+
timidez: el contenedor viaja como JSON al nodo de abajo y se guarda
|
|
212
|
+
en el historial, y ahí ya hay un recorte a 256 KB por payload. */
|
|
213
|
+
description: "Up to 5000, paginated for you. When there are more, the step still succeeds: truncated says so and nextToken lets the next run carry on from there.",
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
name: "startAfter",
|
|
217
|
+
label: "Continue after",
|
|
218
|
+
type: "template",
|
|
219
|
+
advanced: true,
|
|
220
|
+
placeholder: "{{payload.nextToken}}",
|
|
221
|
+
description: "Resumes a listing: feed it the nextToken of the previous run. Leave it empty to start from the top.",
|
|
127
222
|
},
|
|
128
223
|
],
|
|
129
224
|
},
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
225
|
+
/* El tope de lectura NO es un parámetro: es del ejecutor, no una preferencia
|
|
226
|
+
por nodo. Y se dice en la descripción, no se descubre — al superarlo el
|
|
227
|
+
paso FALLA con el tamaño real y un puntero a `presignDownload`; nunca
|
|
228
|
+
devuelve un `content` a medias. Un contenido truncado se parsea, se manda
|
|
229
|
+
por correo y nadie se entera. */
|
|
230
|
+
getObject: {
|
|
231
|
+
label: "Read object",
|
|
232
|
+
description: "Downloads an object and puts its contents in the payload. Up to 5 MB; past that the step fails and points you at Sign download link.",
|
|
233
|
+
apiRoute: "GET /{bucket}/{key}",
|
|
234
|
+
params: [
|
|
235
|
+
objectKey("recibos/{{payload.orderId}}.json"),
|
|
236
|
+
{
|
|
237
|
+
name: "as",
|
|
238
|
+
label: "Read as",
|
|
239
|
+
type: "select",
|
|
240
|
+
required: true,
|
|
241
|
+
default: "text",
|
|
242
|
+
options: [
|
|
243
|
+
{ value: "text", label: "Text — UTF-8 into content" },
|
|
244
|
+
{ value: "json", label: "JSON — parsed into content" },
|
|
245
|
+
{ value: "base64", label: "Base64 — bytes into contentBase64" },
|
|
246
|
+
],
|
|
247
|
+
/* Lo binario se PIDE, no se adivina: un PDF leído como texto llega
|
|
248
|
+
como UTF-8 roto y el fallo aparece tres nodos más abajo. */
|
|
249
|
+
description: "Binary files need Base64 — read as text they arrive corrupted, and Base64 weighs a third more than the file. With JSON, a body that does not parse fails the step. Over 256 KB the contents still flow on whole, but the run history stores them trimmed.",
|
|
250
|
+
},
|
|
251
|
+
failIfMissing(),
|
|
252
|
+
],
|
|
253
|
+
},
|
|
254
|
+
presignUpload: {
|
|
255
|
+
label: "Sign upload link",
|
|
256
|
+
description: "Returns a temporary URL for someone else to PUT a file into the bucket, without giving them the credential.",
|
|
257
|
+
apiRoute: "PUT (presigned)",
|
|
258
|
+
params: [
|
|
259
|
+
objectKey("subidas/{{payload.orderId}}.pdf"),
|
|
260
|
+
signatureTtl(),
|
|
261
|
+
{
|
|
262
|
+
name: "contentType",
|
|
263
|
+
label: "Content type",
|
|
264
|
+
type: "template",
|
|
265
|
+
default: "application/octet-stream",
|
|
266
|
+
placeholder: "application/pdf",
|
|
267
|
+
/* NO se firma. Meterlo en `signedHeaders` obliga al que sube a mandar
|
|
268
|
+
esa cabecera exacta o comerse un 403 que no puede depurar, y quien
|
|
269
|
+
recibe el enlace no suele controlar su cliente HTTP. */
|
|
270
|
+
description: "What the object will be stored as. It is not enforced — the bucket stores whatever arrives.",
|
|
271
|
+
},
|
|
272
|
+
],
|
|
135
273
|
},
|
|
136
274
|
};
|
|
@@ -33,6 +33,28 @@
|
|
|
33
33
|
* Los `slot` siguen haciendo falta: separan la identidad del documento de los
|
|
34
34
|
* campos de la operación, aunque ahora las dos vayan debajo del selector. Un
|
|
35
35
|
* bucle único ordenaría por el array de params y no por ese criterio.
|
|
36
|
+
*
|
|
37
|
+
* ── Qué exige el motor (y por qué está escrito aquí) ──
|
|
38
|
+
* Este esquema declaraba CERO obligatorios en las cinco operaciones, y el
|
|
39
|
+
* ejecutor —`api/src/nodes/docs-actions/docs-actions.service.ts`— rechaza cinco
|
|
40
|
+
* cosas. Esa distancia no se veía como un error de validación sino como un
|
|
41
|
+
* nodo que no guía: sin ningún obligatorio no hay nada que esperar, ningún paso
|
|
42
|
+
* desbloquea al siguiente, y el guiado que el panel SÍ monta no se nota.
|
|
43
|
+
*
|
|
44
|
+
* Los cinco 400 del ejecutor, uno por línea, que son los cinco `required` de
|
|
45
|
+
* este fichero:
|
|
46
|
+
*
|
|
47
|
+
* :323 readDoc documentId 'Document ID is required'
|
|
48
|
+
* :440 appendText documentId 'Document ID is required'
|
|
49
|
+
* :447 appendText contentTemplate 'Content template is required'
|
|
50
|
+
* :498 replaceText documentId 'Document ID is required'
|
|
51
|
+
* :505 replaceText searchText 'Search text is required'
|
|
52
|
+
* :560 insertTable documentId 'Document ID is required'
|
|
53
|
+
*
|
|
54
|
+
* Y los cuatro que NO se marcan, con el defecto que los salva: `documentTitle`
|
|
55
|
+
* cae en `entity.name`, `contentTemplate` de `createDoc` en un documento vacío,
|
|
56
|
+
* `tableRows`/`tableCols` en `|| 3`, y `replaceWith` vacío es un BORRADO, no un
|
|
57
|
+
* hueco. Cada uno lo explica en su sitio.
|
|
36
58
|
*/
|
|
37
59
|
export declare const DOCS_OPERATIONS: readonly ["readDoc", "createDoc", "appendText", "replaceText", "insertTable"];
|
|
38
60
|
export type DocsOperation = (typeof DOCS_OPERATIONS)[number];
|
|
@@ -67,6 +89,23 @@ export interface DocsParamSpec {
|
|
|
67
89
|
label: string;
|
|
68
90
|
type: DocsParamType;
|
|
69
91
|
slot: DocsParamSlot;
|
|
92
|
+
/**
|
|
93
|
+
* Lo exige el EJECUTOR: sin él, `docs-actions.service.ts` devuelve un 400 y
|
|
94
|
+
* el nodo no corre.
|
|
95
|
+
*
|
|
96
|
+
* No es «lo que conviene rellenar», y la diferencia importa en las dos
|
|
97
|
+
* direcciones. De menos, un obligatorio que falta deja la familia con cero y
|
|
98
|
+
* `esGuiable` (`check-cuantos-faltan-por-guiar.mjs`) la echa del censo: el
|
|
99
|
+
* panel monta `useGuidedConfig`, pero sin nada que esperar ningún paso
|
|
100
|
+
* desbloquea al siguiente y el guiado se ve como si no existiera — que es el
|
|
101
|
+
* fallo que este campo arregla. De más, marcar algo que el ejecutor SÍ acepta
|
|
102
|
+
* vacío esconde tras un hueco imposible un nodo que habría funcionado, porque
|
|
103
|
+
* `frena()` sólo mira `obligatorio && !contestado`.
|
|
104
|
+
*
|
|
105
|
+
* Por eso cada `required: true` de este fichero apunta a la línea que lo
|
|
106
|
+
* exige, y los que no lo llevan dicen dónde el ejecutor pone su defecto.
|
|
107
|
+
*/
|
|
108
|
+
required?: boolean;
|
|
70
109
|
/** Texto de ayuda. Llano: el paquete no lleva React. */
|
|
71
110
|
description?: string;
|
|
72
111
|
placeholder?: string;
|
|
@@ -33,6 +33,28 @@
|
|
|
33
33
|
* Los `slot` siguen haciendo falta: separan la identidad del documento de los
|
|
34
34
|
* campos de la operación, aunque ahora las dos vayan debajo del selector. Un
|
|
35
35
|
* bucle único ordenaría por el array de params y no por ese criterio.
|
|
36
|
+
*
|
|
37
|
+
* ── Qué exige el motor (y por qué está escrito aquí) ──
|
|
38
|
+
* Este esquema declaraba CERO obligatorios en las cinco operaciones, y el
|
|
39
|
+
* ejecutor —`api/src/nodes/docs-actions/docs-actions.service.ts`— rechaza cinco
|
|
40
|
+
* cosas. Esa distancia no se veía como un error de validación sino como un
|
|
41
|
+
* nodo que no guía: sin ningún obligatorio no hay nada que esperar, ningún paso
|
|
42
|
+
* desbloquea al siguiente, y el guiado que el panel SÍ monta no se nota.
|
|
43
|
+
*
|
|
44
|
+
* Los cinco 400 del ejecutor, uno por línea, que son los cinco `required` de
|
|
45
|
+
* este fichero:
|
|
46
|
+
*
|
|
47
|
+
* :323 readDoc documentId 'Document ID is required'
|
|
48
|
+
* :440 appendText documentId 'Document ID is required'
|
|
49
|
+
* :447 appendText contentTemplate 'Content template is required'
|
|
50
|
+
* :498 replaceText documentId 'Document ID is required'
|
|
51
|
+
* :505 replaceText searchText 'Search text is required'
|
|
52
|
+
* :560 insertTable documentId 'Document ID is required'
|
|
53
|
+
*
|
|
54
|
+
* Y los cuatro que NO se marcan, con el defecto que los salva: `documentTitle`
|
|
55
|
+
* cae en `entity.name`, `contentTemplate` de `createDoc` en un documento vacío,
|
|
56
|
+
* `tableRows`/`tableCols` en `|| 3`, y `replaceWith` vacío es un BORRADO, no un
|
|
57
|
+
* hueco. Cada uno lo explica en su sitio.
|
|
36
58
|
*/
|
|
37
59
|
export const DOCS_OPERATIONS = [
|
|
38
60
|
'readDoc', // leer el contenido como texto plano
|
|
@@ -46,19 +68,42 @@ export function isDocsOperation(value) {
|
|
|
46
68
|
return typeof value === 'string' && DOCS_OPERATIONS.includes(value);
|
|
47
69
|
}
|
|
48
70
|
/* Los campos que comparten varias operaciones. */
|
|
71
|
+
/**
|
|
72
|
+
* `required: true` va aquí y no operación por operación porque las CUATRO que
|
|
73
|
+
* lo usan lo exigen: `executeRead` (:323), `executeAppend` (:440),
|
|
74
|
+
* `executeReplace` (:498) y `executeInsertTable` (:560) devuelven todas el
|
|
75
|
+
* mismo 400, «Document ID is required». La quinta, `createDoc`, no llama a este
|
|
76
|
+
* ayudante — el documento aún no existe.
|
|
77
|
+
*/
|
|
49
78
|
const documentId = () => ({
|
|
50
79
|
name: 'documentId',
|
|
51
80
|
label: 'Or paste Document ID / template',
|
|
52
81
|
type: 'docPicker',
|
|
53
82
|
slot: 'document',
|
|
83
|
+
required: true,
|
|
54
84
|
placeholder: '1BxiMVs0XRA5nFMdKvBd... or {{payload.documentId}}',
|
|
55
85
|
});
|
|
56
|
-
|
|
86
|
+
/**
|
|
87
|
+
* Éste NO puede llevar `required` en el ayudante, y es la única asimetría del
|
|
88
|
+
* fichero: sus dos usuarios no piden lo mismo.
|
|
89
|
+
*
|
|
90
|
+
* `appendText` lo exige (`executeAppend` :447, «Content template is required»):
|
|
91
|
+
* añadir la cadena vacía al final de un documento no es una operación, es un
|
|
92
|
+
* viaje a Google para no hacer nada. `createDoc` no (`executeCreate` :362 lo
|
|
93
|
+
* lee con un ternario y :375 sólo inserta `if (content)`): un documento nuevo y
|
|
94
|
+
* vacío es un resultado legítimo, y bloquearlo sería inventarse un requisito
|
|
95
|
+
* que la api no tiene.
|
|
96
|
+
*
|
|
97
|
+
* De ahí el `over`, como el `columnMapping` de Sheets: el que lo exige lo dice
|
|
98
|
+
* en su sitio.
|
|
99
|
+
*/
|
|
100
|
+
const contentTemplate = (over = {}) => ({
|
|
57
101
|
name: 'contentTemplate',
|
|
58
102
|
label: 'Content',
|
|
59
103
|
type: 'htmlTemplate',
|
|
60
104
|
slot: 'config',
|
|
61
105
|
placeholder: 'Text to insert... supports {{payload.x}} templates',
|
|
106
|
+
...over,
|
|
62
107
|
});
|
|
63
108
|
export const DOCS_OPERATION_SPECS = {
|
|
64
109
|
readDoc: {
|
|
@@ -73,13 +118,22 @@ export const DOCS_OPERATION_SPECS = {
|
|
|
73
118
|
labelShort: 'Create',
|
|
74
119
|
description: 'Create a new Google Doc',
|
|
75
120
|
/* La única SIN `documentId`: aquí el documento no existe todavía. Eso es lo
|
|
76
|
-
que en la página eran dos compuertas negadas (`!== "createDoc"`).
|
|
121
|
+
que en la página eran dos compuertas negadas (`!== "createDoc"`).
|
|
122
|
+
|
|
123
|
+
Y la única con CERO obligatorios, porque el ejecutor no exige ninguno:
|
|
124
|
+
`executeCreate` no tiene un solo `return 400`. Un `createDoc` sin nada
|
|
125
|
+
contestado corre y crea un documento vacío con el nombre del nodo. */
|
|
77
126
|
params: [
|
|
78
127
|
{
|
|
79
128
|
name: 'documentTitle',
|
|
80
129
|
label: 'Document Title',
|
|
81
130
|
type: 'template',
|
|
82
131
|
slot: 'document',
|
|
132
|
+
/* Opcional a propósito: `executeCreate` (:361) lo lee como
|
|
133
|
+
`render(entity.documentTitle || entity.name)`. Sin título el
|
|
134
|
+
documento sale con el NOMBRE DEL NODO, que es un defecto de verdad y
|
|
135
|
+
no un descuido. Marcarlo obligatorio pararía la guía delante de un
|
|
136
|
+
campo que la api sabe rellenar sola. */
|
|
83
137
|
placeholder: 'Report — {{payload.date}} or My Document',
|
|
84
138
|
},
|
|
85
139
|
contentTemplate(),
|
|
@@ -89,7 +143,7 @@ export const DOCS_OPERATION_SPECS = {
|
|
|
89
143
|
label: 'Append Text',
|
|
90
144
|
labelShort: 'Append',
|
|
91
145
|
description: 'Add text to end of document',
|
|
92
|
-
params: [documentId(), contentTemplate()],
|
|
146
|
+
params: [documentId(), contentTemplate({ required: true })],
|
|
93
147
|
},
|
|
94
148
|
replaceText: {
|
|
95
149
|
label: 'Replace Text',
|
|
@@ -102,6 +156,9 @@ export const DOCS_OPERATION_SPECS = {
|
|
|
102
156
|
label: 'Search Text',
|
|
103
157
|
type: 'template',
|
|
104
158
|
slot: 'config',
|
|
159
|
+
/* `executeReplace` (:505): «Search text is required». Sin qué buscar,
|
|
160
|
+
`replaceAllText` no tiene nada que hacer. */
|
|
161
|
+
required: true,
|
|
105
162
|
placeholder: '{{PLACEHOLDER}} or text to find',
|
|
106
163
|
},
|
|
107
164
|
{
|
|
@@ -109,6 +166,11 @@ export const DOCS_OPERATION_SPECS = {
|
|
|
109
166
|
label: 'Replace With',
|
|
110
167
|
type: 'template',
|
|
111
168
|
slot: 'config',
|
|
169
|
+
/* Opcional, y aquí el vacío SIGNIFICA algo: `executeReplace` lo pasa
|
|
170
|
+
tal cual a `replaceAllText`, así que dejarlo en blanco BORRA todas
|
|
171
|
+
las apariciones de `searchText`. Es la única forma de pedir un
|
|
172
|
+
borrado, y por eso el ejecutor no lo comprueba: hacerlo obligatorio
|
|
173
|
+
quitaría una operación que hoy funciona. */
|
|
112
174
|
placeholder: '{{payload.value}} or replacement text',
|
|
113
175
|
},
|
|
114
176
|
],
|
|
@@ -119,7 +181,15 @@ export const DOCS_OPERATION_SPECS = {
|
|
|
119
181
|
description: 'Insert a table at end of document',
|
|
120
182
|
params: [
|
|
121
183
|
documentId(),
|
|
122
|
-
/* Los topes son distintos y no es un descuido: 50 filas, 20 columnas.
|
|
184
|
+
/* Los topes son distintos y no es un descuido: 50 filas, 20 columnas.
|
|
185
|
+
|
|
186
|
+
Y los dos siguen OPCIONALES teniendo `default: 3`, que es justo el
|
|
187
|
+
caso que el contrato vigila: `executeInsertTable` (:555) los lee con
|
|
188
|
+
`entity.tableRows || 3`, o sea que el 3×3 lo pone la api tanto si el
|
|
189
|
+
formulario lo manda como si no. Un obligatorio con `default` sería
|
|
190
|
+
además la trampa de la regla 10 — `defectoPintable` le borra el
|
|
191
|
+
`default` para no pintar como contestado algo que nadie eligió, y
|
|
192
|
+
estos dos sí quieren pintarlo. */
|
|
123
193
|
{ name: 'tableRows', label: 'Rows', type: 'number', slot: 'config', min: 1, max: 50, default: 3 },
|
|
124
194
|
{ name: 'tableCols', label: 'Columns', type: 'number', slot: 'config', min: 1, max: 20, default: 3 },
|
|
125
195
|
],
|
package/package.json
CHANGED