@hostwebhook/node-sdk 0.3.0 → 0.5.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/acceso-al-workspace.d.ts +51 -0
- package/dist/acceso-al-workspace.js +99 -0
- package/dist/base-node.service.d.ts +120 -0
- package/dist/base-node.service.js +409 -0
- package/dist/cascade-cleanup.d.ts +13 -0
- package/dist/cascade-cleanup.js +93 -0
- package/dist/credential-references.d.ts +131 -0
- package/dist/credential-references.js +210 -0
- package/dist/dto-utils.d.ts +11 -0
- package/dist/dto-utils.js +22 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +9 -0
- package/dist/limpiar-marcador-detach.d.ts +63 -0
- package/dist/limpiar-marcador-detach.js +92 -0
- package/dist/olvidar-la-colocacion.d.ts +23 -0
- package/dist/olvidar-la-colocacion.js +60 -0
- package/dist/sync-connections.d.ts +73 -0
- package/dist/sync-connections.js +120 -0
- package/dist/unique-name.d.ts +6 -0
- package/dist/unique-name.js +32 -0
- package/package.json +22 -14
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Dónde guarda un nodo la referencia a una credencial — TODAS las formas.
|
|
4
|
+
*
|
|
5
|
+
* ## Por qué existe
|
|
6
|
+
*
|
|
7
|
+
* `findConsumers` buscaba en dos campos, `credentialId` y `memoryCredentialId`,
|
|
8
|
+
* y con eso decidía si una credencial estaba en uso. Los nodos que pueden usar
|
|
9
|
+
* VARIAS credenciales no las guardan así:
|
|
10
|
+
*
|
|
11
|
+
* - `socialMediaAction.credentialIds` — un mapa `plataforma → id | id[]`;
|
|
12
|
+
* - `socialMediaAction.extraCredentialIds` — un array;
|
|
13
|
+
* - `trigger.serviceConfig.social_media.credentialIds` — el mismo mapa,
|
|
14
|
+
* pero anidado dentro de `serviceConfig`.
|
|
15
|
+
*
|
|
16
|
+
* Así que borrar una credencial usada SÓLO por esas vías encontraba cero
|
|
17
|
+
* consumidores: ni 409, ni desatacado, ni aviso. La credencial desaparecía y el
|
|
18
|
+
* nodo se quedaba con un id que ya no resuelve — que es como Ariel perdió la de
|
|
19
|
+
* Mastodon el 2026-08-12, y el trigger siguió mostrando `…c0fac6` sin decir
|
|
20
|
+
* nada.
|
|
21
|
+
*
|
|
22
|
+
* No es la primera vez: la cabecera de `remove()` cuenta que esta misma clase
|
|
23
|
+
* de fallo dejó «19 nodos vivos en 5 workspaces apuntando a tres credenciales
|
|
24
|
+
* que no existían». Aquel arreglo cubrió los dos campos planos y se olvidó de
|
|
25
|
+
* los múltiples, porque la lista de campos estaba escrita a mano en la consulta.
|
|
26
|
+
* Ahora está declarada aquí, y `credential-references.spec.ts` la prueba con la
|
|
27
|
+
* forma real de cada documento.
|
|
28
|
+
*
|
|
29
|
+
* ## Cómo se compara
|
|
30
|
+
*
|
|
31
|
+
* Siempre por `String(...)`. Estos campos son `Mixed` en Mongoose, así que el
|
|
32
|
+
* mismo id vive como `ObjectId` en unos documentos y como `string` en otros —
|
|
33
|
+
* ver `reference-id.ts`. Comparar por valor fallaría en la mitad.
|
|
34
|
+
*/
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.UNSET_MARCADOR_DETACH = exports.CAMPOS_MARCADOR_DETACH = exports.CREDENTIAL_REF_SHAPES = void 0;
|
|
37
|
+
exports.leerCamino = leerCamino;
|
|
38
|
+
exports.valorReferencia = valorReferencia;
|
|
39
|
+
exports.formasQueReferencian = formasQueReferencian;
|
|
40
|
+
exports.valorTrasDesatacar = valorTrasDesatacar;
|
|
41
|
+
exports.valorTieneCredencial = valorTieneCredencial;
|
|
42
|
+
exports.tieneAlgunaCredencial = tieneAlgunaCredencial;
|
|
43
|
+
exports.marcadorEsFosil = marcadorEsFosil;
|
|
44
|
+
exports.dtoTocaCredencial = dtoTocaCredencial;
|
|
45
|
+
/**
|
|
46
|
+
* Las cinco formas que existen hoy.
|
|
47
|
+
*
|
|
48
|
+
* Se barren en TODAS las colecciones de nodos, no sólo en las que declaran el
|
|
49
|
+
* campo: una consulta por un campo que la colección no tiene simplemente no
|
|
50
|
+
* devuelve nada, y así añadir el campo a otro nodo no obliga a tocar esta lista.
|
|
51
|
+
*/
|
|
52
|
+
exports.CREDENTIAL_REF_SHAPES = [
|
|
53
|
+
{ kind: 'scalar', path: 'credentialId' },
|
|
54
|
+
{ kind: 'scalar', path: 'memoryCredentialId' },
|
|
55
|
+
{ kind: 'array', path: 'extraCredentialIds' },
|
|
56
|
+
{ kind: 'map', path: 'credentialIds' },
|
|
57
|
+
{ kind: 'map', path: 'serviceConfig.social_media.credentialIds' },
|
|
58
|
+
];
|
|
59
|
+
const mismoId = (v, id) => v !== null && v !== undefined && String(v) === id;
|
|
60
|
+
/** Lee un camino con puntos. Devuelve `undefined` si algo del medio falta. */
|
|
61
|
+
function leerCamino(doc, path) {
|
|
62
|
+
let actual = doc;
|
|
63
|
+
for (const parte of path.split('.')) {
|
|
64
|
+
if (actual === null || typeof actual !== 'object')
|
|
65
|
+
return undefined;
|
|
66
|
+
actual = actual[parte];
|
|
67
|
+
}
|
|
68
|
+
return actual;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* ¿El valor de este campo referencia la credencial?
|
|
72
|
+
*
|
|
73
|
+
* Un mapa puede tener el id como string suelto o dentro de un array: las dos
|
|
74
|
+
* formas conviven en datos reales (`credentialIds` está declarado como
|
|
75
|
+
* `Record<string, string[] | string>` justamente por eso).
|
|
76
|
+
*/
|
|
77
|
+
function valorReferencia(valor, id, kind) {
|
|
78
|
+
if (valor === null || valor === undefined)
|
|
79
|
+
return false;
|
|
80
|
+
if (kind === 'scalar')
|
|
81
|
+
return mismoId(valor, id);
|
|
82
|
+
if (kind === 'array')
|
|
83
|
+
return Array.isArray(valor) && valor.some((v) => mismoId(v, id));
|
|
84
|
+
// map
|
|
85
|
+
if (typeof valor !== 'object' || Array.isArray(valor))
|
|
86
|
+
return false;
|
|
87
|
+
return Object.values(valor).some((v) => Array.isArray(v) ? v.some((x) => mismoId(x, id)) : mismoId(v, id));
|
|
88
|
+
}
|
|
89
|
+
/** Las formas por las que ESTE documento referencia la credencial. */
|
|
90
|
+
function formasQueReferencian(doc, id) {
|
|
91
|
+
return exports.CREDENTIAL_REF_SHAPES.filter((forma) => valorReferencia(leerCamino(doc, forma.path), id, forma.kind));
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* El valor que hay que dejar en el campo al quitar la credencial.
|
|
95
|
+
*
|
|
96
|
+
* Devuelve `null` cuando el campo se queda sin nada, para que el documento no
|
|
97
|
+
* conserve un `{}` o un `[]` que se lee como «configurado, pero vacío».
|
|
98
|
+
*
|
|
99
|
+
* En el mapa, una plataforma que se queda sin cuentas **desaparece**: dejarla
|
|
100
|
+
* con `[]` haría que el trigger siguiera anunciando que vigila Mastodon sin
|
|
101
|
+
* ninguna cuenta con la que hacerlo.
|
|
102
|
+
*/
|
|
103
|
+
function valorTrasDesatacar(valor, id, kind) {
|
|
104
|
+
if (kind === 'scalar')
|
|
105
|
+
return null;
|
|
106
|
+
if (kind === 'array') {
|
|
107
|
+
const resto = Array.isArray(valor)
|
|
108
|
+
? valor.filter((v) => !mismoId(v, id))
|
|
109
|
+
: [];
|
|
110
|
+
return resto.length > 0 ? resto : null;
|
|
111
|
+
}
|
|
112
|
+
// map
|
|
113
|
+
if (valor === null || typeof valor !== 'object' || Array.isArray(valor))
|
|
114
|
+
return null;
|
|
115
|
+
const salida = {};
|
|
116
|
+
for (const [clave, v] of Object.entries(valor)) {
|
|
117
|
+
if (Array.isArray(v)) {
|
|
118
|
+
const resto = v.filter((x) => !mismoId(x, id));
|
|
119
|
+
if (resto.length > 0)
|
|
120
|
+
salida[clave] = resto;
|
|
121
|
+
}
|
|
122
|
+
else if (!mismoId(v, id)) {
|
|
123
|
+
salida[clave] = v;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return Object.keys(salida).length > 0 ? salida : null;
|
|
127
|
+
}
|
|
128
|
+
/* ── El marcador de «tu credencial se borró» ────────────────────── */
|
|
129
|
+
/**
|
|
130
|
+
* Los dos campos que `credentials.service.remove` escribe en cada nodo que se
|
|
131
|
+
* queda huérfano, para que la pantalla pueda decir QUÉ credencial era y
|
|
132
|
+
* cuándo desapareció (después de desatacar ya no queda id al que preguntar).
|
|
133
|
+
*/
|
|
134
|
+
exports.CAMPOS_MARCADOR_DETACH = [
|
|
135
|
+
'credentialDetachedAt',
|
|
136
|
+
'credentialDetachedName',
|
|
137
|
+
];
|
|
138
|
+
/** El `$unset` que los quita. */
|
|
139
|
+
exports.UNSET_MARCADOR_DETACH = {
|
|
140
|
+
credentialDetachedAt: '',
|
|
141
|
+
credentialDetachedName: '',
|
|
142
|
+
};
|
|
143
|
+
/** ¿Este valor trae al menos una credencial puesta? */
|
|
144
|
+
function valorTieneCredencial(valor, kind) {
|
|
145
|
+
if (valor === null || valor === undefined || valor === '')
|
|
146
|
+
return false;
|
|
147
|
+
if (kind === 'scalar')
|
|
148
|
+
return true;
|
|
149
|
+
if (kind === 'array')
|
|
150
|
+
return Array.isArray(valor) && valor.length > 0;
|
|
151
|
+
// map
|
|
152
|
+
if (typeof valor !== 'object' || Array.isArray(valor))
|
|
153
|
+
return false;
|
|
154
|
+
return Object.values(valor).some((v) => Array.isArray(v) ? v.length > 0 : v !== null && v !== undefined && v !== '');
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* ¿El nodo tiene AHORA alguna credencial, por cualquiera de las cinco formas?
|
|
158
|
+
*
|
|
159
|
+
* Es la pregunta que decide si el aviso «se borró tu credencial» sigue
|
|
160
|
+
* teniendo sentido. El dashboard hace este mismo cálculo para ocultarlo
|
|
161
|
+
* (`hooks/useNodeErrors.ts`); aquí sirve para BORRAR el marcador, que es lo
|
|
162
|
+
* que faltaba: ocultarlo lo dejaba vivo, y volvía a salir en cuanto el nodo
|
|
163
|
+
* se quedaba sin credencial otra vez — aunque fuera por un «Disconnect» a
|
|
164
|
+
* mano, y enseñando la fecha de un borrado de días atrás.
|
|
165
|
+
*/
|
|
166
|
+
function tieneAlgunaCredencial(doc) {
|
|
167
|
+
if (!doc || typeof doc !== 'object')
|
|
168
|
+
return false;
|
|
169
|
+
return exports.CREDENTIAL_REF_SHAPES.some((forma) => valorTieneCredencial(leerCamino(doc, forma.path), forma.kind));
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* ¿Hay que quitarle el marcador a este documento?
|
|
173
|
+
*
|
|
174
|
+
* Sólo cuando el marcador está Y el nodo volvió a tener credencial. Si sigue
|
|
175
|
+
* huérfano el aviso es legítimo y se queda: alguien le borró la credencial de
|
|
176
|
+
* verdad y todavía no ha elegido otra.
|
|
177
|
+
*/
|
|
178
|
+
function marcadorEsFosil(doc, tocoCredencial = false) {
|
|
179
|
+
if (!doc || typeof doc !== 'object')
|
|
180
|
+
return false;
|
|
181
|
+
const nombre = doc.credentialDetachedName;
|
|
182
|
+
if (typeof nombre !== 'string' || !nombre)
|
|
183
|
+
return false;
|
|
184
|
+
return tieneAlgunaCredencial(doc) || tocoCredencial;
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* ¿Este dto toca el campo de la credencial, sea para poner una o para
|
|
188
|
+
* quitarla?
|
|
189
|
+
*
|
|
190
|
+
* ⚠️ Ésta es la mitad que faltaba, y se vio en cuanto llegó a producción.
|
|
191
|
+
*
|
|
192
|
+
* «Es fósil si el nodo VOLVIÓ a tener credencial» arregla el caso de elegir
|
|
193
|
+
* otra, pero deja fuera el que lo destapó: Ariel pulsó «Disconnect» y el nodo
|
|
194
|
+
* se quedó SIN credencial, así que seguía contando como huérfano legítimo y
|
|
195
|
+
* el aviso del día 20 seguía en pantalla. Su queja fue literal: «y sigue».
|
|
196
|
+
*
|
|
197
|
+
* La distinción buena no es «tiene credencial» sino «¿sigue siendo verdad que
|
|
198
|
+
* te la quitaron sin que tú hicieras nada?». En cuanto la persona toca ese
|
|
199
|
+
* campo —ponga una o la vacíe— ya vio el aviso y actuó: a partir de ahí el
|
|
200
|
+
* estado lo eligió ella, y seguir diciéndole que se lo borraron el día 20 es
|
|
201
|
+
* hablarle de otra cosa.
|
|
202
|
+
*
|
|
203
|
+
* `!== undefined` y no un truthy: `credentialId: null` es exactamente el caso
|
|
204
|
+
* de desconectar, y es el que hay que reconocer.
|
|
205
|
+
*/
|
|
206
|
+
function dtoTocaCredencial(dto) {
|
|
207
|
+
if (!dto || typeof dto !== 'object')
|
|
208
|
+
return false;
|
|
209
|
+
return exports.CREDENTIAL_REF_SHAPES.some((forma) => leerCamino(dto, forma.path) !== undefined);
|
|
210
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Filters out undefined values from an object.
|
|
3
|
+
* Useful for DTO → Mongoose update where undefined own properties
|
|
4
|
+
* would overwrite existing values.
|
|
5
|
+
*/
|
|
6
|
+
export declare function definedProps<T extends object>(obj: T): Partial<T>;
|
|
7
|
+
/**
|
|
8
|
+
* Throws ForbiddenException if the current count has reached the plan limit.
|
|
9
|
+
* A limit of -1 means unlimited.
|
|
10
|
+
*/
|
|
11
|
+
export declare function checkPlanLimit(count: number, limit: number, resource: string): void;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.definedProps = definedProps;
|
|
4
|
+
exports.checkPlanLimit = checkPlanLimit;
|
|
5
|
+
const common_1 = require("@nestjs/common");
|
|
6
|
+
/**
|
|
7
|
+
* Filters out undefined values from an object.
|
|
8
|
+
* Useful for DTO → Mongoose update where undefined own properties
|
|
9
|
+
* would overwrite existing values.
|
|
10
|
+
*/
|
|
11
|
+
function definedProps(obj) {
|
|
12
|
+
return Object.fromEntries(Object.entries(obj).filter(([, v]) => v !== undefined));
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Throws ForbiddenException if the current count has reached the plan limit.
|
|
16
|
+
* A limit of -1 means unlimited.
|
|
17
|
+
*/
|
|
18
|
+
function checkPlanLimit(count, limit, resource) {
|
|
19
|
+
if (limit !== -1 && count >= limit) {
|
|
20
|
+
throw new common_1.ForbiddenException(`${resource} limit reached (${count}/${limit}). Upgrade your plan.`);
|
|
21
|
+
}
|
|
22
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -50,3 +50,12 @@ export * from "./swallow";
|
|
|
50
50
|
export * from "./template-render";
|
|
51
51
|
export * from "./try-parse";
|
|
52
52
|
export * from "./workspace-payloads";
|
|
53
|
+
export * from "./base-node.service";
|
|
54
|
+
export * from "./cascade-cleanup";
|
|
55
|
+
export * from "./dto-utils";
|
|
56
|
+
export * from "./sync-connections";
|
|
57
|
+
export * from "./unique-name";
|
|
58
|
+
export * from "./credential-references";
|
|
59
|
+
export * from "./limpiar-marcador-detach";
|
|
60
|
+
export * from "./olvidar-la-colocacion";
|
|
61
|
+
export * from "./acceso-al-workspace";
|
package/dist/index.js
CHANGED
|
@@ -72,3 +72,12 @@ __exportStar(require("./swallow"), exports);
|
|
|
72
72
|
__exportStar(require("./template-render"), exports);
|
|
73
73
|
__exportStar(require("./try-parse"), exports);
|
|
74
74
|
__exportStar(require("./workspace-payloads"), exports);
|
|
75
|
+
__exportStar(require("./base-node.service"), exports);
|
|
76
|
+
__exportStar(require("./cascade-cleanup"), exports);
|
|
77
|
+
__exportStar(require("./dto-utils"), exports);
|
|
78
|
+
__exportStar(require("./sync-connections"), exports);
|
|
79
|
+
__exportStar(require("./unique-name"), exports);
|
|
80
|
+
__exportStar(require("./credential-references"), exports);
|
|
81
|
+
__exportStar(require("./limpiar-marcador-detach"), exports);
|
|
82
|
+
__exportStar(require("./olvidar-la-colocacion"), exports);
|
|
83
|
+
__exportStar(require("./acceso-al-workspace"), exports);
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Quita el aviso «se borró tu credencial» cuando el nodo ya tiene otra.
|
|
3
|
+
*
|
|
4
|
+
* ## El defecto que arregla
|
|
5
|
+
*
|
|
6
|
+
* `credentials.service.remove` escribe dos marcadores en cada nodo que deja
|
|
7
|
+
* huérfano —`credentialDetachedName` y `credentialDetachedAt`— para que la
|
|
8
|
+
* pantalla pueda decir qué credencial era y cuándo desapareció. Hasta ahora
|
|
9
|
+
* eran un `$set` sin pareja: **ningún sitio de la api los borraba**.
|
|
10
|
+
*
|
|
11
|
+
* El dashboard sólo los OCULTA mientras el nodo tenga alguna credencial
|
|
12
|
+
* (`hooks/useNodeErrors.ts`). Ocultar no es borrar: el marcador seguía vivo
|
|
13
|
+
* debajo y volvía a salir en cuanto el nodo se quedaba sin credencial otra
|
|
14
|
+
* vez — incluso pulsando «Disconnect», que sólo hace `onChange(null, null)`
|
|
15
|
+
* y no borra ninguna credencial.
|
|
16
|
+
*
|
|
17
|
+
* Lo reportó Ariel el 2026-08-23: desconectó una credencial de Drive y la
|
|
18
|
+
* pantalla le dijo *«The credential "Google - …" was deleted on Aug 20, 2026»*.
|
|
19
|
+
* La fecha es la prueba de que el marcador era viejo: si lo hubiera escrito su
|
|
20
|
+
* clic, diría el 23. Un fósil de otro día conviviendo con el estado de hoy, y
|
|
21
|
+
* ganando él.
|
|
22
|
+
*
|
|
23
|
+
* ## Por qué esta condición y no otra
|
|
24
|
+
*
|
|
25
|
+
* Se miran las DOS cosas, y hicieron falta las dos:
|
|
26
|
+
*
|
|
27
|
+
* - el DOCUMENTO tiene credencial → el aviso ya no describe nada; cubre
|
|
28
|
+
* también al nodo que sólo cambió de nombre y arrastra el fósil;
|
|
29
|
+
* - el DTO tocaba el campo de la credencial → la persona acaba de actuar
|
|
30
|
+
* sobre él, ponga una o la vacíe.
|
|
31
|
+
*
|
|
32
|
+
* ⚠️ La segunda llegó tarde. Con sólo la primera, pulsar «Disconnect» dejaba
|
|
33
|
+
* el nodo sin credencial, o sea contando como huérfano legítimo, y el aviso
|
|
34
|
+
* del día 20 seguía en pantalla — que es exactamente el caso que abrió todo
|
|
35
|
+
* esto. Ariel lo dijo en dos palabras al probarlo: «y sigue».
|
|
36
|
+
*
|
|
37
|
+
* Y sólo se limpia si el nodo VOLVIÓ a tener credencial: si sigue huérfano el
|
|
38
|
+
* aviso es legítimo —alguien le borró la credencial de verdad y todavía no ha
|
|
39
|
+
* elegido otra— y tiene que seguir viéndose.
|
|
40
|
+
*
|
|
41
|
+
* ## Coste
|
|
42
|
+
*
|
|
43
|
+
* Cero en el caso normal. `marcadorEsFosil` mira dos campos en memoria y sale;
|
|
44
|
+
* sólo hay escritura cuando de verdad hay un fósil que quitar, que es raro.
|
|
45
|
+
*
|
|
46
|
+
* El fallo no aborta el PATCH: el usuario venía a guardar su nodo, y que no se
|
|
47
|
+
* pueda limpiar un aviso cosmético no es motivo para devolverle un 500. Pero
|
|
48
|
+
* se registra con nombre — ver `common/swallow`.
|
|
49
|
+
*/
|
|
50
|
+
export declare function limpiarMarcadorDetachSiEsFosil(model: {
|
|
51
|
+
updateOne(filtro: unknown, update: unknown): {
|
|
52
|
+
exec?: () => Promise<unknown>;
|
|
53
|
+
} | Promise<unknown>;
|
|
54
|
+
}, doc: unknown, opts?: {
|
|
55
|
+
/**
|
|
56
|
+
* ¿El PATCH que trajo hasta aquí tocaba el campo de la credencial?
|
|
57
|
+
*
|
|
58
|
+
* Cuando sí, el aviso se retira aunque el nodo se quede SIN credencial:
|
|
59
|
+
* desconectar a mano es un acto de la persona, y el marcador sólo tiene
|
|
60
|
+
* sentido mientras describa algo que le pasó SIN que ella hiciera nada.
|
|
61
|
+
*/
|
|
62
|
+
tocoCredencial?: boolean;
|
|
63
|
+
}): Promise<void>;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.limpiarMarcadorDetachSiEsFosil = limpiarMarcadorDetachSiEsFosil;
|
|
4
|
+
const common_1 = require("@nestjs/common");
|
|
5
|
+
const swallow_1 = require("./swallow");
|
|
6
|
+
const credential_references_1 = require("./credential-references");
|
|
7
|
+
const LOGGER = new common_1.Logger('LimpiarMarcadorDetach');
|
|
8
|
+
/**
|
|
9
|
+
* Quita el aviso «se borró tu credencial» cuando el nodo ya tiene otra.
|
|
10
|
+
*
|
|
11
|
+
* ## El defecto que arregla
|
|
12
|
+
*
|
|
13
|
+
* `credentials.service.remove` escribe dos marcadores en cada nodo que deja
|
|
14
|
+
* huérfano —`credentialDetachedName` y `credentialDetachedAt`— para que la
|
|
15
|
+
* pantalla pueda decir qué credencial era y cuándo desapareció. Hasta ahora
|
|
16
|
+
* eran un `$set` sin pareja: **ningún sitio de la api los borraba**.
|
|
17
|
+
*
|
|
18
|
+
* El dashboard sólo los OCULTA mientras el nodo tenga alguna credencial
|
|
19
|
+
* (`hooks/useNodeErrors.ts`). Ocultar no es borrar: el marcador seguía vivo
|
|
20
|
+
* debajo y volvía a salir en cuanto el nodo se quedaba sin credencial otra
|
|
21
|
+
* vez — incluso pulsando «Disconnect», que sólo hace `onChange(null, null)`
|
|
22
|
+
* y no borra ninguna credencial.
|
|
23
|
+
*
|
|
24
|
+
* Lo reportó Ariel el 2026-08-23: desconectó una credencial de Drive y la
|
|
25
|
+
* pantalla le dijo *«The credential "Google - …" was deleted on Aug 20, 2026»*.
|
|
26
|
+
* La fecha es la prueba de que el marcador era viejo: si lo hubiera escrito su
|
|
27
|
+
* clic, diría el 23. Un fósil de otro día conviviendo con el estado de hoy, y
|
|
28
|
+
* ganando él.
|
|
29
|
+
*
|
|
30
|
+
* ## Por qué esta condición y no otra
|
|
31
|
+
*
|
|
32
|
+
* Se miran las DOS cosas, y hicieron falta las dos:
|
|
33
|
+
*
|
|
34
|
+
* - el DOCUMENTO tiene credencial → el aviso ya no describe nada; cubre
|
|
35
|
+
* también al nodo que sólo cambió de nombre y arrastra el fósil;
|
|
36
|
+
* - el DTO tocaba el campo de la credencial → la persona acaba de actuar
|
|
37
|
+
* sobre él, ponga una o la vacíe.
|
|
38
|
+
*
|
|
39
|
+
* ⚠️ La segunda llegó tarde. Con sólo la primera, pulsar «Disconnect» dejaba
|
|
40
|
+
* el nodo sin credencial, o sea contando como huérfano legítimo, y el aviso
|
|
41
|
+
* del día 20 seguía en pantalla — que es exactamente el caso que abrió todo
|
|
42
|
+
* esto. Ariel lo dijo en dos palabras al probarlo: «y sigue».
|
|
43
|
+
*
|
|
44
|
+
* Y sólo se limpia si el nodo VOLVIÓ a tener credencial: si sigue huérfano el
|
|
45
|
+
* aviso es legítimo —alguien le borró la credencial de verdad y todavía no ha
|
|
46
|
+
* elegido otra— y tiene que seguir viéndose.
|
|
47
|
+
*
|
|
48
|
+
* ## Coste
|
|
49
|
+
*
|
|
50
|
+
* Cero en el caso normal. `marcadorEsFosil` mira dos campos en memoria y sale;
|
|
51
|
+
* sólo hay escritura cuando de verdad hay un fósil que quitar, que es raro.
|
|
52
|
+
*
|
|
53
|
+
* El fallo no aborta el PATCH: el usuario venía a guardar su nodo, y que no se
|
|
54
|
+
* pueda limpiar un aviso cosmético no es motivo para devolverle un 500. Pero
|
|
55
|
+
* se registra con nombre — ver `common/swallow`.
|
|
56
|
+
*/
|
|
57
|
+
async function limpiarMarcadorDetachSiEsFosil(
|
|
58
|
+
/*
|
|
59
|
+
* ⚠️ `updateOne` va como MÉTODO abreviado y no como propiedad con tipo
|
|
60
|
+
* función, y la diferencia no es de estilo.
|
|
61
|
+
*
|
|
62
|
+
* Con `strictFunctionTypes`, una propiedad cuyo tipo es una función se
|
|
63
|
+
* comprueba de forma CONTRAVARIANTE en los parámetros: un `updateOne` real
|
|
64
|
+
* de Mongoose, que declara `filter: _QueryFilter<T>`, no encaja en uno que
|
|
65
|
+
* declara `filtro: unknown`. Un método abreviado se comprueba de forma
|
|
66
|
+
* bivariante y sí encaja.
|
|
67
|
+
*
|
|
68
|
+
* Salió al mudar esto al paquete: en la api compilaba con mongoose 9.2.1 y
|
|
69
|
+
* aquí falla con 9.9.4, que estrechó `_QueryFilter`. Como el `peerDependency`
|
|
70
|
+
* dice `mongoose >=8`, la firma tiene que aguantar las dos — y la bivarianza
|
|
71
|
+
* es segura aquí porque esta función sólo LLAMA al método, con valores que
|
|
72
|
+
* construye ella misma.
|
|
73
|
+
*/
|
|
74
|
+
model, doc, opts) {
|
|
75
|
+
if (!(0, credential_references_1.marcadorEsFosil)(doc, opts?.tocoCredencial))
|
|
76
|
+
return;
|
|
77
|
+
const id = doc._id;
|
|
78
|
+
if (!id)
|
|
79
|
+
return;
|
|
80
|
+
const resultado = model.updateOne({ _id: id }, { $unset: credential_references_1.UNSET_MARCADOR_DETACH });
|
|
81
|
+
const promesa = typeof resultado.exec === 'function'
|
|
82
|
+
? resultado.exec()
|
|
83
|
+
: resultado;
|
|
84
|
+
await Promise.resolve(promesa).catch((0, swallow_1.onFailure)(LOGGER, `limpiar el marcador de credencial borrada del nodo ${String(id)}`));
|
|
85
|
+
/* También en el documento que se va a devolver.
|
|
86
|
+
Sin esto el PATCH responde con el marcador todavía puesto, la pantalla lo
|
|
87
|
+
pinta una última vez y sólo desaparece al recargar — que es exactamente la
|
|
88
|
+
clase de «se arregló pero no se ve» que hace dudar de si se arregló. */
|
|
89
|
+
const d = doc;
|
|
90
|
+
delete d.credentialDetachedAt;
|
|
91
|
+
delete d.credentialDetachedName;
|
|
92
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { Connection } from 'mongoose';
|
|
2
|
+
/**
|
|
3
|
+
* Borrar un nodo le quita también su sitio en el lienzo.
|
|
4
|
+
*
|
|
5
|
+
* Sin esto, la clave se queda para siempre. Es literalmente de donde salió el
|
|
6
|
+
* mapa viejo: `users.flowLayout` tenía **1 003 de 1 284 nodos referidos que ya
|
|
7
|
+
* no existían** —el 78 %— porque no había ningún camino que quitara la entrada
|
|
8
|
+
* al borrar el nodo. Sólo crecía.
|
|
9
|
+
*
|
|
10
|
+
* Y ya estaba volviendo a pasar: dos horas después de migrar, `flow_layouts`
|
|
11
|
+
* tenía 55 posiciones para 53 nodos. Las dos de más eran un Chat Trigger y el
|
|
12
|
+
* nodo AI que nace con él, creados y borrados por el camino.
|
|
13
|
+
*
|
|
14
|
+
* Va por la conexión y no por el servicio a propósito: quien borra un nodo es
|
|
15
|
+
* `BaseNodeService`, del que heredan los 45 tipos, y meterle una dependencia
|
|
16
|
+
* nueva al constructor obliga a tocar los 45 módulos. `cascadeCleanupNodeReferences`
|
|
17
|
+
* —que limpia las conexiones que apuntaban al nodo— resuelve lo mismo por el
|
|
18
|
+
* mismo sitio.
|
|
19
|
+
*
|
|
20
|
+
* No lanza. Que un nodo no se pueda borrar porque su posición se resistió sería
|
|
21
|
+
* peor que la posición huérfana que esto viene a evitar.
|
|
22
|
+
*/
|
|
23
|
+
export declare function olvidarLaColocacion(connection: Connection, nodeType: string, nodeId: string, workspaceId: string | undefined): Promise<number>;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.olvidarLaColocacion = olvidarLaColocacion;
|
|
4
|
+
const common_1 = require("@nestjs/common");
|
|
5
|
+
const mongoose_1 = require("mongoose");
|
|
6
|
+
const node_types_1 = require("@hostwebhook/node-types");
|
|
7
|
+
const logger = new common_1.Logger('FlowLayouts');
|
|
8
|
+
/**
|
|
9
|
+
* Borrar un nodo le quita también su sitio en el lienzo.
|
|
10
|
+
*
|
|
11
|
+
* Sin esto, la clave se queda para siempre. Es literalmente de donde salió el
|
|
12
|
+
* mapa viejo: `users.flowLayout` tenía **1 003 de 1 284 nodos referidos que ya
|
|
13
|
+
* no existían** —el 78 %— porque no había ningún camino que quitara la entrada
|
|
14
|
+
* al borrar el nodo. Sólo crecía.
|
|
15
|
+
*
|
|
16
|
+
* Y ya estaba volviendo a pasar: dos horas después de migrar, `flow_layouts`
|
|
17
|
+
* tenía 55 posiciones para 53 nodos. Las dos de más eran un Chat Trigger y el
|
|
18
|
+
* nodo AI que nace con él, creados y borrados por el camino.
|
|
19
|
+
*
|
|
20
|
+
* Va por la conexión y no por el servicio a propósito: quien borra un nodo es
|
|
21
|
+
* `BaseNodeService`, del que heredan los 45 tipos, y meterle una dependencia
|
|
22
|
+
* nueva al constructor obliga a tocar los 45 módulos. `cascadeCleanupNodeReferences`
|
|
23
|
+
* —que limpia las conexiones que apuntaban al nodo— resuelve lo mismo por el
|
|
24
|
+
* mismo sitio.
|
|
25
|
+
*
|
|
26
|
+
* No lanza. Que un nodo no se pueda borrar porque su posición se resistió sería
|
|
27
|
+
* peor que la posición huérfana que esto viene a evitar.
|
|
28
|
+
*/
|
|
29
|
+
async function olvidarLaColocacion(connection, nodeType, nodeId, workspaceId) {
|
|
30
|
+
if (!workspaceId || !mongoose_1.Types.ObjectId.isValid(workspaceId)) {
|
|
31
|
+
/* El nodo se borró igual; sólo queda su posición. Decirlo, porque el rastro
|
|
32
|
+
es lo único que distingue «no había workspace» de «no se limpió». */
|
|
33
|
+
logger.warn(`olvidarLaColocacion: ${nodeType}/${nodeId} sin workspaceId; su posición se queda`);
|
|
34
|
+
return 0;
|
|
35
|
+
}
|
|
36
|
+
const prefijo = node_types_1.NODE_REGISTRY[nodeType]?.prefix;
|
|
37
|
+
if (!prefijo) {
|
|
38
|
+
logger.warn(`olvidarLaColocacion: '${nodeType}' no está en el registro; no sé cómo se llama en el lienzo`);
|
|
39
|
+
return 0;
|
|
40
|
+
}
|
|
41
|
+
/* La clave del lienzo, que es `<prefijo>-<ObjectId>`. Se comprueba en vez de
|
|
42
|
+
interpolar a ciegas: va dentro de una ruta de `$unset`, donde un punto baja
|
|
43
|
+
un nivel y un `$` es un operador. */
|
|
44
|
+
if (!/^[0-9a-fA-F]{24}$/.test(nodeId)) {
|
|
45
|
+
logger.warn(`olvidarLaColocacion: '${nodeId}' no es un id de nodo`);
|
|
46
|
+
return 0;
|
|
47
|
+
}
|
|
48
|
+
try {
|
|
49
|
+
/* `updateMany` y sin `userId`: la colocación es de cada persona, y quien
|
|
50
|
+
borra el nodo no sabe —ni tiene por qué— quién lo había puesto dónde. */
|
|
51
|
+
const r = await connection
|
|
52
|
+
.collection('flow_layouts')
|
|
53
|
+
.updateMany({ workspaceId: new mongoose_1.Types.ObjectId(workspaceId) }, { $unset: { [`positions.${prefijo}-${nodeId}`]: '' } });
|
|
54
|
+
return r.modifiedCount ?? 0;
|
|
55
|
+
}
|
|
56
|
+
catch (err) {
|
|
57
|
+
logger.error(`olvidarLaColocacion: no pude quitar ${prefijo}-${nodeId} del workspace ${workspaceId}:`, err);
|
|
58
|
+
return 0;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { Connection, Types } from 'mongoose';
|
|
2
|
+
/**
|
|
3
|
+
* Reject a node wired to itself.
|
|
4
|
+
*
|
|
5
|
+
* A canvas drag from a node's own output dot to its own input dot produced a
|
|
6
|
+
* real, persisted edge: source and target were the same id. Nothing stopped it
|
|
7
|
+
* — the dashboard assumed the registry prevented invalid connections, and the
|
|
8
|
+
* registry answers a different question. `canReceiveFrom('transform',
|
|
9
|
+
* 'transform')` is TRUE, and correctly so: chaining two different Transform
|
|
10
|
+
* nodes is ordinary. What the type pair cannot express is "not this same
|
|
11
|
+
* instance", because ids are not types.
|
|
12
|
+
*
|
|
13
|
+
* The one legitimate self-edge is the Loop node's loop-back, and the registry
|
|
14
|
+
* already declares it as `special.hasLoopBack`. So the exception is read from
|
|
15
|
+
* there rather than hardcoded here — a future node that needs to re-enter
|
|
16
|
+
* itself declares the flag and both the canvas and this check follow.
|
|
17
|
+
*/
|
|
18
|
+
export declare function assertNoSelfConnection(nodeType: string, nodeId: Types.ObjectId | string, nodes: Array<{
|
|
19
|
+
nodeType: string;
|
|
20
|
+
nodeId: any;
|
|
21
|
+
}>): void;
|
|
22
|
+
/**
|
|
23
|
+
* Rechaza una conexión entre tipos que no pueden conectarse.
|
|
24
|
+
*
|
|
25
|
+
* Hasta ahora la compatibilidad de tipos era cosa del lienzo, y estaba escrito
|
|
26
|
+
* aquí al lado: *"Type compatibility is the registry's job and is checked on
|
|
27
|
+
* the canvas"*. Eso valía mientras el lienzo fuera el único que escribe, y no
|
|
28
|
+
* lo es: `/agent-context` le explica a un agente, con ejemplos, que meta
|
|
29
|
+
* `{ nodeType, nodeId }` a mano en `inputNodes`/`outputNodes`, y cualquier
|
|
30
|
+
* cliente con la clave y el scope puede hacer lo mismo.
|
|
31
|
+
*
|
|
32
|
+
* Lo que se colaba por ahí no revienta: **desaparece**. `dispatchOutputNodes`
|
|
33
|
+
* cae en su rama por defecto, `getNodeHandler('trigger')` no devuelve nada y
|
|
34
|
+
* suelta un `logger.warn` —sin evento de telemetría, porque el `telemetry.emit`
|
|
35
|
+
* está en el `catch` y esto no lanza—. O sea una arista dibujada en el lienzo
|
|
36
|
+
* que no dispara nunca y que no aparece en ningún sitio donde mirarla.
|
|
37
|
+
*
|
|
38
|
+
* ⚠️ **La regla es `fromNodes`/`toNodes` de `NODE_UI`, y no las listas de
|
|
39
|
+
* `NODE_CONNECTIONS`.** La tentación es usar `acceptsInputFrom`/`canOutputTo`,
|
|
40
|
+
* que están ahí mismo, las declaran los 43 tipos y parecen más precisas. Están
|
|
41
|
+
* podridas: medidas contra producción el 2026-08-19 rechazaban **111 de 364**
|
|
42
|
+
* conexiones que funcionan hoy — `trigger → ai`, `ai → telegramAction`,
|
|
43
|
+
* `sheetsAction → filter`. `fromNodes`/`toNodes` rechazaba **0**, porque es el
|
|
44
|
+
* mismo dato que decide qué puntos pinta la tarjeta. Endurecer con eso no
|
|
45
|
+
* puede romper nada que el usuario haya podido crear desde el lienzo, que es
|
|
46
|
+
* justo la propiedad que se busca: la api no debe ser más estricta que la
|
|
47
|
+
* pantalla, sólo igual de estricta en los caminos que la pantalla no cubre.
|
|
48
|
+
*
|
|
49
|
+
* De un tipo que el registro no conoce no se opina. Un alias viejo se
|
|
50
|
+
* normaliza primero (`voiceCall` → `voiceAgent`); lo que ni así aparezca se
|
|
51
|
+
* deja pasar, porque un 400 sobre datos heredados con autoguardado deja la
|
|
52
|
+
* página sin poder guardar nunca más.
|
|
53
|
+
*/
|
|
54
|
+
export declare function assertConnectableTypes(ownType: string, refs: {
|
|
55
|
+
inputNodes?: Array<{
|
|
56
|
+
nodeType: string;
|
|
57
|
+
}>;
|
|
58
|
+
outputNodes?: Array<{
|
|
59
|
+
nodeType: string;
|
|
60
|
+
}>;
|
|
61
|
+
}): void;
|
|
62
|
+
/**
|
|
63
|
+
* Validate that all target nodes belong to the same workspace as the source node.
|
|
64
|
+
* Throws BadRequestException if any cross-workspace connection is detected.
|
|
65
|
+
*
|
|
66
|
+
* This is the ONLY backend connection logic. The frontend owns connection
|
|
67
|
+
* creation/deletion — it updates both sides (source.outputNodes + target.inputNodes).
|
|
68
|
+
* The backend just saves what it receives and validates workspace boundaries.
|
|
69
|
+
*/
|
|
70
|
+
export declare function validateConnectionWorkspaces(connection: Connection, sourceWorkspaceId: Types.ObjectId | string | null | undefined, nodes: Array<{
|
|
71
|
+
nodeType: string;
|
|
72
|
+
nodeId: any;
|
|
73
|
+
}>): Promise<void>;
|