@hostwebhook/node-types 1.62.0 → 1.64.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/connections.js +4 -2
- package/dist/credentials.js +45 -0
- package/dist/dispatch.js +2 -0
- package/dist/github-operations.d.ts +147 -0
- package/dist/github-operations.js +555 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +14 -2
- package/dist/registry.js +28 -0
- package/dist/shopify-operations.d.ts +95 -0
- package/dist/shopify-operations.js +263 -0
- package/dist/types.d.ts +1 -1
- package/dist/ui.js +4 -0
- package/package.json +1 -1
package/dist/connections.js
CHANGED
|
@@ -18,7 +18,7 @@ exports.isNodeType = isNodeType;
|
|
|
18
18
|
const PROCESSING_OUTPUTS = [
|
|
19
19
|
'webhook', 'router', 'filter', 'transform', 'cache', 'code', 'rateLimiter',
|
|
20
20
|
'aggregator', 'conditional', 'delay', 'schemaValidator', 'split', 'markdown', 'fileTransform', 'limit',
|
|
21
|
-
'emailAction', 'gmailAction', 'httpAction', 'mongoAction', 'postgresAction', 'notificationAction', 'sheetsAction', 'calendarAction', 'docsAction', 'driveAction', 'firecrawlAction', 'telegramAction', 'whatsappAction', 'discordAction', 'slackAction', 'googleContactsAction', 'googleAnalyticsAction', 'notionAction', 'vectorStore', 'rssAction', 'socialMediaAction', 'mailchimpAction', 'loop',
|
|
21
|
+
'emailAction', 'gmailAction', 'httpAction', 'mongoAction', 'postgresAction', 'notificationAction', 'sheetsAction', 'calendarAction', 'docsAction', 'driveAction', 'firecrawlAction', 'telegramAction', 'whatsappAction', 'discordAction', 'slackAction', 'googleContactsAction', 'googleAnalyticsAction', 'notionAction', 'vectorStore', 'rssAction', 'socialMediaAction', 'mailchimpAction', 'shopifyAction', 'githubAction', 'loop',
|
|
22
22
|
];
|
|
23
23
|
/** Standard processing input sources */
|
|
24
24
|
const STANDARD_INPUTS = [
|
|
@@ -26,7 +26,7 @@ const STANDARD_INPUTS = [
|
|
|
26
26
|
'cache', 'code', 'rateLimiter', 'aggregator', 'conditional', 'delay', 'schemaValidator', 'split', 'loop', 'markdown', 'fileTransform', 'limit',
|
|
27
27
|
];
|
|
28
28
|
/** Action nodes that can chain (non-terminal) */
|
|
29
|
-
const CHAINABLE_ACTIONS = ['httpAction', 'mongoAction', 'postgresAction', 'sheetsAction', 'calendarAction', 'docsAction', 'driveAction', 'firecrawlAction', 'telegramAction', 'whatsappAction', 'discordAction', 'slackAction', 'googleContactsAction', 'googleAnalyticsAction', 'notionAction', 'vectorStore', 'rssAction', 'socialMediaAction', 'mailchimpAction'];
|
|
29
|
+
const CHAINABLE_ACTIONS = ['httpAction', 'mongoAction', 'postgresAction', 'sheetsAction', 'calendarAction', 'docsAction', 'driveAction', 'firecrawlAction', 'telegramAction', 'whatsappAction', 'discordAction', 'slackAction', 'googleContactsAction', 'googleAnalyticsAction', 'notionAction', 'vectorStore', 'rssAction', 'socialMediaAction', 'mailchimpAction', 'shopifyAction', 'githubAction'];
|
|
30
30
|
/** Full input list for action nodes (standard + chainable actions) */
|
|
31
31
|
const ACTION_INPUTS = [...STANDARD_INPUTS, ...CHAINABLE_ACTIONS];
|
|
32
32
|
exports.NODE_CONNECTIONS = {
|
|
@@ -78,6 +78,8 @@ exports.NODE_CONNECTIONS = {
|
|
|
78
78
|
discordAction: { acceptsInputFrom: ACTION_INPUTS, canOutputTo: PROCESSING_OUTPUTS },
|
|
79
79
|
slackAction: { acceptsInputFrom: ACTION_INPUTS, canOutputTo: PROCESSING_OUTPUTS },
|
|
80
80
|
mailchimpAction: { acceptsInputFrom: ACTION_INPUTS, canOutputTo: PROCESSING_OUTPUTS },
|
|
81
|
+
shopifyAction: { acceptsInputFrom: ACTION_INPUTS, canOutputTo: PROCESSING_OUTPUTS },
|
|
82
|
+
githubAction: { acceptsInputFrom: ACTION_INPUTS, canOutputTo: PROCESSING_OUTPUTS },
|
|
81
83
|
googleContactsAction: { acceptsInputFrom: ACTION_INPUTS, canOutputTo: PROCESSING_OUTPUTS },
|
|
82
84
|
googleAnalyticsAction: { acceptsInputFrom: ACTION_INPUTS, canOutputTo: PROCESSING_OUTPUTS },
|
|
83
85
|
notionAction: { acceptsInputFrom: ACTION_INPUTS, canOutputTo: PROCESSING_OUTPUTS },
|
package/dist/credentials.js
CHANGED
|
@@ -96,6 +96,51 @@ exports.CREDENTIAL_TYPES = [
|
|
|
96
96
|
// Renombrarlo ahora rompería las credenciales que hayan llegado a existir:
|
|
97
97
|
// el `type` es la clave por la que los nodos las referencian.
|
|
98
98
|
{ type: 'mailchimp_oauth2' },
|
|
99
|
+
// El OAuth de HostWebhook contra Shopify.
|
|
100
|
+
//
|
|
101
|
+
// Se anota aquí ANTES de escribir el servicio, precisamente por lo que
|
|
102
|
+
// cuenta el bloque de Mailchimp de arriba: la api puede tener el flujo
|
|
103
|
+
// entero y bien, y aun así el `create` de la credencial muere con un enum
|
|
104
|
+
// de Mongoose que TypeScript no ve. Es el mismo agujero, y ya sabemos que
|
|
105
|
+
// no avisa al compilar.
|
|
106
|
+
//
|
|
107
|
+
// Lleva `_oauth2` por coherencia con `mailchimp_oauth2`, y porque el
|
|
108
|
+
// `scopeFilter` del dashboard filtra por esta cadena exacta.
|
|
109
|
+
//
|
|
110
|
+
// Una nota sobre lo que este tipo GUARDA, que aquí no se ve: el dominio de
|
|
111
|
+
// la tienda (`{shop}.myshopify.com`) viaja DENTRO del blob cifrado, igual
|
|
112
|
+
// que el `dc` de Mailchimp. Es parte del host de toda llamada y de la URL
|
|
113
|
+
// de autorización, así que una credencial de Shopify sin su dominio es
|
|
114
|
+
// inservible — no un detalle de presentación.
|
|
115
|
+
{ type: 'shopify_oauth2' },
|
|
116
|
+
// La instalación de la GitHub App de HostWebhook. El camino PRINCIPAL: el
|
|
117
|
+
// usuario pulsa "Connect GitHub", elige en qué repos entra, y vuelve.
|
|
118
|
+
//
|
|
119
|
+
// Lo que guarda es un `installationId` y NINGÚN secreto — la private key es
|
|
120
|
+
// de la app y vive en el entorno (`GITHUB_APP_PRIVATE_KEY_BASE64`). El token
|
|
121
|
+
// de instalación es corto y se re-emite firmando con esa llave, así que aquí
|
|
122
|
+
// no hay nada que caduque en la base de datos.
|
|
123
|
+
//
|
|
124
|
+
// Va aparte de `github_pat` por lo mismo que `discord_oauth` va aparte de
|
|
125
|
+
// `discord_bot`: lo que guardan es distinto.
|
|
126
|
+
//
|
|
127
|
+
// ⚠️ No confundir con `GITHUB_OAUTH_CLIENT_ID`/`_SECRET`, que ya existían y
|
|
128
|
+
// son de una OAuth App DISTINTA — la que usa `MCP_OAUTH_PRESETS.github` para
|
|
129
|
+
// llegar al servidor MCP de GitHub. Dos registros, dos pares de credenciales,
|
|
130
|
+
// y conviven a propósito: el MCP es la cola larga dentro del AI Node, esto
|
|
131
|
+
// son los rieles del nodo.
|
|
132
|
+
{ type: 'github_app' },
|
|
133
|
+
// Un fine-grained personal access token pegado por el usuario. La salida de
|
|
134
|
+
// emergencia, detrás de "Use a personal access token instead".
|
|
135
|
+
//
|
|
136
|
+
// Aquí SÍ hay un secreto del usuario, y además caduca: GitHub obliga a
|
|
137
|
+
// ponerles fecha. Un aviso antes de que muera es trabajo pendiente, no de
|
|
138
|
+
// esta versión.
|
|
139
|
+
//
|
|
140
|
+
// Ejecuta exactamente las mismas 14 operaciones que `github_app`. Lo único
|
|
141
|
+
// que cambia es de dónde sale la lista de repositorios del formulario: los
|
|
142
|
+
// del usuario, en vez de los que abarca una instalación.
|
|
143
|
+
{ type: 'github_pat' },
|
|
99
144
|
];
|
|
100
145
|
/**
|
|
101
146
|
* Plain string array — what the Mongoose `enum` field and the
|
package/dist/dispatch.js
CHANGED
|
@@ -53,6 +53,8 @@ exports.NODE_DISPATCH = {
|
|
|
53
53
|
whatsappAction: { service: 'whatsappActionsService', collection: 'whatsappactions', hasFilters: true, outputFields: ['outputNodes'], customDispatch: true, pipelineDispatch: 'excluded' },
|
|
54
54
|
discordAction: { service: 'discordActionsService', collection: 'discordactions', hasFilters: true, outputFields: ['outputNodes'], customDispatch: true, pipelineDispatch: 'excluded' },
|
|
55
55
|
mailchimpAction: { service: 'mailchimpActionsService', collection: 'mailchimpactions', hasFilters: true, outputFields: ['outputNodes'], customDispatch: true, pipelineDispatch: 'excluded' },
|
|
56
|
+
shopifyAction: { service: 'shopifyActionsService', collection: 'shopifyactions', hasFilters: true, outputFields: ['outputNodes'], customDispatch: true, pipelineDispatch: 'excluded' },
|
|
57
|
+
githubAction: { service: 'githubActionsService', collection: 'githubactions', hasFilters: true, outputFields: ['outputNodes'], customDispatch: true, pipelineDispatch: 'excluded' },
|
|
56
58
|
slackAction: { service: 'slackActionsService', collection: 'slackactions', hasFilters: true, outputFields: ['outputNodes'], customDispatch: true, pipelineDispatch: 'excluded' },
|
|
57
59
|
googleContactsAction: { service: 'googleContactsActionsService', collection: 'googlecontactsactions', hasFilters: true, outputFields: ['outputNodes'], customDispatch: true, pipelineDispatch: 'excluded' },
|
|
58
60
|
googleAnalyticsAction: { service: 'googleAnalyticsActionsService', collection: 'googleanalyticsactions', hasFilters: true, outputFields: ['outputNodes'], customDispatch: true, pipelineDispatch: 'excluded' },
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub operation enum — única fuente de verdad para la api, el dashboard y
|
|
3
|
+
* el Message Broker. Lo usan:
|
|
4
|
+
* - la entidad y el DTO de `githubAction` (api): campo `operation` + validación
|
|
5
|
+
* - la página de detalle del dashboard, que pinta el formulario desde los specs
|
|
6
|
+
* - specs de toolkit MCP, si algún día el nodo gana modo herramienta
|
|
7
|
+
*
|
|
8
|
+
* Mismo patrón que `notion-operations.ts` y `shopify-operations.ts`.
|
|
9
|
+
*
|
|
10
|
+
* ── Qué credencial hay debajo ──
|
|
11
|
+
*
|
|
12
|
+
* Dos tipos, y la diferencia se nota en UNA cosa que no es una operación:
|
|
13
|
+
* de dónde sale la lista de repositorios.
|
|
14
|
+
*
|
|
15
|
+
* - `github_app` — la instalación de la GitHub App. El camino principal.
|
|
16
|
+
* Los repos son los que abarca esa instalación.
|
|
17
|
+
* - `github_pat` — un fine-grained token pegado por el usuario. La salida
|
|
18
|
+
* de emergencia, detrás de "Use a personal access token instead".
|
|
19
|
+
* Los repos son los del usuario.
|
|
20
|
+
*
|
|
21
|
+
* Las 14 operaciones de abajo se ejecutan IGUAL con las dos. Lo que cambia es
|
|
22
|
+
* el listado que rellena el formulario, y eso vive en el controlador, no aquí.
|
|
23
|
+
*
|
|
24
|
+
* ── Por qué 14 y no la API entera ──
|
|
25
|
+
*
|
|
26
|
+
* Paridad con lo que la gente encadena en un flujo, no con los docs de GitHub.
|
|
27
|
+
* Abrir un issue, comentarlo, mover un PR, leer o escribir un archivo, disparar
|
|
28
|
+
* un workflow. Lo demás —releases, gists, administración de la organización,
|
|
29
|
+
* búsqueda de código— lo cubre el MCP oficial de GitHub dentro del AI Node, que
|
|
30
|
+
* ya existe y no hay que construir.
|
|
31
|
+
*
|
|
32
|
+
* Y la regla que dejó escrita Notion se respeta al pie: **ninguna operación
|
|
33
|
+
* declarada sin implementar**. El enum ES el contrato de la pantalla — una op
|
|
34
|
+
* declarada se pinta en el desplegable y luego revienta al ejecutarse. Entran
|
|
35
|
+
* cuando entre su código.
|
|
36
|
+
*
|
|
37
|
+
* ── Lo que NO es un parámetro, a propósito ──
|
|
38
|
+
*
|
|
39
|
+
* El `sha`. Actualizar un archivo y crear una rama lo necesitan, y GitHub no
|
|
40
|
+
* perdona uno equivocado. Pedírselo al usuario es pedirle que se equivoque: lo
|
|
41
|
+
* resuelve el nodo, desde la ruta del archivo o el nombre de la rama base. Es
|
|
42
|
+
* el mismo criterio por el que un id de Notion se elige de una lista en vez de
|
|
43
|
+
* teclearse.
|
|
44
|
+
*
|
|
45
|
+
* Tampoco son operaciones listar repos, ramas, etiquetas, asignables o
|
|
46
|
+
* workflows. Una operación existe para producir payload en una corrida; eso
|
|
47
|
+
* otro es para rellenar el formulario, y son endpoints del controlador — como
|
|
48
|
+
* Drive alimenta su `DriveFilePicker`.
|
|
49
|
+
*
|
|
50
|
+
* ── Límites de GitHub que condicionan el diseño ──
|
|
51
|
+
*
|
|
52
|
+
* - 5.000 peticiones/hora autenticadas. Suficiente salvo en bucles largos.
|
|
53
|
+
* - `searchIssues` NO comparte ese cubo: la API de búsqueda va por su cuenta,
|
|
54
|
+
* del orden de 30 por minuto. Por eso está marcada aparte más abajo.
|
|
55
|
+
* - Las escrituras tienen además un límite secundario por ráfaga, y GitHub
|
|
56
|
+
* responde con `Retry-After`. Se respeta con `common/rate-limiter.ts` y
|
|
57
|
+
* `common/retry-transient.ts`, que ya existen. No escribir otro backoff.
|
|
58
|
+
*/
|
|
59
|
+
export declare const GITHUB_OPERATIONS: readonly ["createIssue", "updateIssue", "getIssue", "commentIssue", "listIssues", "createPullRequest", "getPullRequest", "mergePullRequest", "listPullRequests", "getFile", "createOrUpdateFile", "createBranch", "dispatchWorkflow", "searchIssues"];
|
|
60
|
+
export type GithubOperation = (typeof GITHUB_OPERATIONS)[number];
|
|
61
|
+
/** Type guard — para validar entrada no fiable (DTOs, tool calls). */
|
|
62
|
+
export declare function isGithubOperation(value: unknown): value is GithubOperation;
|
|
63
|
+
/**
|
|
64
|
+
* Las operaciones que devuelven un ARRAY y por tanto obligan a marcar
|
|
65
|
+
* `_meta.iterable` en `getOutputPayload`.
|
|
66
|
+
*
|
|
67
|
+
* Está aquí y no sólo en la api porque el error de olvidarlo no falla al
|
|
68
|
+
* compilar: falla en producción, callado, con el nodo de abajo recibiendo el
|
|
69
|
+
* sobre entero en vez de un elemento, y el PayloadViewer pintando rutas con
|
|
70
|
+
* corchetes que nadie puede usar. Que la lista viva en el paquete permite que
|
|
71
|
+
* el test de la api la recorra en vez de repetirla a mano.
|
|
72
|
+
*/
|
|
73
|
+
export declare const GITHUB_ITERABLE_OPERATIONS: readonly GithubOperation[];
|
|
74
|
+
export type GithubParamType =
|
|
75
|
+
/** Selector vivo de repositorios que alcanza la credencial. Guarda
|
|
76
|
+
* `owner/repo`, que es como lo consumen todas las rutas. */
|
|
77
|
+
'githubRepo'
|
|
78
|
+
/** Selector vivo de ramas del repo elegido. Admite escribir una que aún no
|
|
79
|
+
* existe: `createBranch` la crea, y una plantilla puede resolverla. */
|
|
80
|
+
| 'githubBranch'
|
|
81
|
+
/** Etiquetas del repo elegido, selección múltiple. */
|
|
82
|
+
| 'githubLabels'
|
|
83
|
+
/** Usuarios asignables en el repo elegido, selección múltiple. */
|
|
84
|
+
| 'githubAssignees'
|
|
85
|
+
/** Workflows del repo que declaran `workflow_dispatch`. Los demás no se
|
|
86
|
+
* pueden disparar, así que no se ofrecen. */
|
|
87
|
+
| 'githubWorkflow'
|
|
88
|
+
/** Texto con autocompletado de `{{payload.*}}`. */
|
|
89
|
+
| 'template'
|
|
90
|
+
/** Área de texto larga, también con autocompletado. */
|
|
91
|
+
| 'textarea'
|
|
92
|
+
/** `<select>` normal; las etiquetas no son los valores, `options` obliga. */
|
|
93
|
+
| 'select' | 'number'
|
|
94
|
+
/**
|
|
95
|
+
* `<select>` de `''` / `'true'` / `'false'` que se guarda como BOOLEANO, o
|
|
96
|
+
* no se guarda si es `''`. "— sin tocar —" tiene que significar que la clave
|
|
97
|
+
* no viaja, no que viaja en `false`.
|
|
98
|
+
*/
|
|
99
|
+
| 'booleanSelect'
|
|
100
|
+
/** Área de texto para JSON crudo. */
|
|
101
|
+
| 'json';
|
|
102
|
+
export interface GithubParamSpec {
|
|
103
|
+
/** La clave de `operationConfig`. Es lo que lee la api, no un nombre de UI. */
|
|
104
|
+
name: string;
|
|
105
|
+
label: string;
|
|
106
|
+
type: GithubParamType;
|
|
107
|
+
required?: boolean;
|
|
108
|
+
/** Ayuda bajo el campo. Texto plano: el paquete no lleva React. */
|
|
109
|
+
description?: string;
|
|
110
|
+
placeholder?: string;
|
|
111
|
+
/** Prerrellenado cuando la config guardada no trae valor para esta clave. */
|
|
112
|
+
default?: string | number;
|
|
113
|
+
/** Obligatorio en `select` y `booleanSelect` — etiqueta y valor difieren. */
|
|
114
|
+
options?: ReadonlyArray<{
|
|
115
|
+
value: string;
|
|
116
|
+
label: string;
|
|
117
|
+
}>;
|
|
118
|
+
min?: number;
|
|
119
|
+
max?: number;
|
|
120
|
+
/** Se pinta en la pestaña "Advanced" en vez de en "Config". */
|
|
121
|
+
advanced?: boolean;
|
|
122
|
+
/** Sólo visible cuando un campo hermano vale uno de estos. */
|
|
123
|
+
showWhen?: {
|
|
124
|
+
field: string;
|
|
125
|
+
in: ReadonlyArray<string>;
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
export interface GithubOperationSpec {
|
|
129
|
+
/** Etiqueta de la fila en el desplegable de operación. */
|
|
130
|
+
label: string;
|
|
131
|
+
/** Sub-línea bajo la etiqueta. */
|
|
132
|
+
description: string;
|
|
133
|
+
/** Ruta a la que mapea, tal cual, para poder cotejarla con los docs. */
|
|
134
|
+
apiRoute: string;
|
|
135
|
+
/** Esquema de parámetros. El orden ES el orden en pantalla. */
|
|
136
|
+
params: GithubParamSpec[];
|
|
137
|
+
}
|
|
138
|
+
export declare const GITHUB_OPERATION_SPECS: Record<GithubOperation, GithubOperationSpec>;
|
|
139
|
+
/**
|
|
140
|
+
* Lo que se ofrece en el desplegable de operación.
|
|
141
|
+
*
|
|
142
|
+
* Hoy son todas. Existe como constante aparte —y no como alias de
|
|
143
|
+
* `GITHUB_OPERATIONS`— porque es exactamente el punto donde otros nodos han
|
|
144
|
+
* necesitado esconder una op sin sacarla del enum: retirarla del enum rompe
|
|
145
|
+
* las que ya estaban guardadas en la base de datos.
|
|
146
|
+
*/
|
|
147
|
+
export declare const GITHUB_DROPDOWN_OPERATIONS: readonly GithubOperation[];
|
|
@@ -0,0 +1,555 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.GITHUB_DROPDOWN_OPERATIONS = exports.GITHUB_OPERATION_SPECS = exports.GITHUB_ITERABLE_OPERATIONS = exports.GITHUB_OPERATIONS = void 0;
|
|
4
|
+
exports.isGithubOperation = isGithubOperation;
|
|
5
|
+
/**
|
|
6
|
+
* GitHub operation enum — única fuente de verdad para la api, el dashboard y
|
|
7
|
+
* el Message Broker. Lo usan:
|
|
8
|
+
* - la entidad y el DTO de `githubAction` (api): campo `operation` + validación
|
|
9
|
+
* - la página de detalle del dashboard, que pinta el formulario desde los specs
|
|
10
|
+
* - specs de toolkit MCP, si algún día el nodo gana modo herramienta
|
|
11
|
+
*
|
|
12
|
+
* Mismo patrón que `notion-operations.ts` y `shopify-operations.ts`.
|
|
13
|
+
*
|
|
14
|
+
* ── Qué credencial hay debajo ──
|
|
15
|
+
*
|
|
16
|
+
* Dos tipos, y la diferencia se nota en UNA cosa que no es una operación:
|
|
17
|
+
* de dónde sale la lista de repositorios.
|
|
18
|
+
*
|
|
19
|
+
* - `github_app` — la instalación de la GitHub App. El camino principal.
|
|
20
|
+
* Los repos son los que abarca esa instalación.
|
|
21
|
+
* - `github_pat` — un fine-grained token pegado por el usuario. La salida
|
|
22
|
+
* de emergencia, detrás de "Use a personal access token instead".
|
|
23
|
+
* Los repos son los del usuario.
|
|
24
|
+
*
|
|
25
|
+
* Las 14 operaciones de abajo se ejecutan IGUAL con las dos. Lo que cambia es
|
|
26
|
+
* el listado que rellena el formulario, y eso vive en el controlador, no aquí.
|
|
27
|
+
*
|
|
28
|
+
* ── Por qué 14 y no la API entera ──
|
|
29
|
+
*
|
|
30
|
+
* Paridad con lo que la gente encadena en un flujo, no con los docs de GitHub.
|
|
31
|
+
* Abrir un issue, comentarlo, mover un PR, leer o escribir un archivo, disparar
|
|
32
|
+
* un workflow. Lo demás —releases, gists, administración de la organización,
|
|
33
|
+
* búsqueda de código— lo cubre el MCP oficial de GitHub dentro del AI Node, que
|
|
34
|
+
* ya existe y no hay que construir.
|
|
35
|
+
*
|
|
36
|
+
* Y la regla que dejó escrita Notion se respeta al pie: **ninguna operación
|
|
37
|
+
* declarada sin implementar**. El enum ES el contrato de la pantalla — una op
|
|
38
|
+
* declarada se pinta en el desplegable y luego revienta al ejecutarse. Entran
|
|
39
|
+
* cuando entre su código.
|
|
40
|
+
*
|
|
41
|
+
* ── Lo que NO es un parámetro, a propósito ──
|
|
42
|
+
*
|
|
43
|
+
* El `sha`. Actualizar un archivo y crear una rama lo necesitan, y GitHub no
|
|
44
|
+
* perdona uno equivocado. Pedírselo al usuario es pedirle que se equivoque: lo
|
|
45
|
+
* resuelve el nodo, desde la ruta del archivo o el nombre de la rama base. Es
|
|
46
|
+
* el mismo criterio por el que un id de Notion se elige de una lista en vez de
|
|
47
|
+
* teclearse.
|
|
48
|
+
*
|
|
49
|
+
* Tampoco son operaciones listar repos, ramas, etiquetas, asignables o
|
|
50
|
+
* workflows. Una operación existe para producir payload en una corrida; eso
|
|
51
|
+
* otro es para rellenar el formulario, y son endpoints del controlador — como
|
|
52
|
+
* Drive alimenta su `DriveFilePicker`.
|
|
53
|
+
*
|
|
54
|
+
* ── Límites de GitHub que condicionan el diseño ──
|
|
55
|
+
*
|
|
56
|
+
* - 5.000 peticiones/hora autenticadas. Suficiente salvo en bucles largos.
|
|
57
|
+
* - `searchIssues` NO comparte ese cubo: la API de búsqueda va por su cuenta,
|
|
58
|
+
* del orden de 30 por minuto. Por eso está marcada aparte más abajo.
|
|
59
|
+
* - Las escrituras tienen además un límite secundario por ráfaga, y GitHub
|
|
60
|
+
* responde con `Retry-After`. Se respeta con `common/rate-limiter.ts` y
|
|
61
|
+
* `common/retry-transient.ts`, que ya existen. No escribir otro backoff.
|
|
62
|
+
*/
|
|
63
|
+
exports.GITHUB_OPERATIONS = [
|
|
64
|
+
// Issues
|
|
65
|
+
'createIssue', // POST /repos/{o}/{r}/issues
|
|
66
|
+
'updateIssue', // PATCH /repos/{o}/{r}/issues/{n} (cubre cerrar y reabrir)
|
|
67
|
+
'getIssue', // GET /repos/{o}/{r}/issues/{n}
|
|
68
|
+
'commentIssue', // POST /repos/{o}/{r}/issues/{n}/comments
|
|
69
|
+
'listIssues', // GET /repos/{o}/{r}/issues → iterable
|
|
70
|
+
// Pull requests
|
|
71
|
+
'createPullRequest', // POST /repos/{o}/{r}/pulls
|
|
72
|
+
'getPullRequest', // GET /repos/{o}/{r}/pulls/{n}
|
|
73
|
+
'mergePullRequest', // PUT /repos/{o}/{r}/pulls/{n}/merge
|
|
74
|
+
'listPullRequests', // GET /repos/{o}/{r}/pulls → iterable
|
|
75
|
+
// Contenido del repositorio
|
|
76
|
+
'getFile', // GET /repos/{o}/{r}/contents/{path}
|
|
77
|
+
'createOrUpdateFile', // PUT /repos/{o}/{r}/contents/{path}
|
|
78
|
+
'createBranch', // POST /repos/{o}/{r}/git/refs
|
|
79
|
+
// Automatización
|
|
80
|
+
'dispatchWorkflow', // POST /repos/{o}/{r}/actions/workflows/{id}/dispatches
|
|
81
|
+
// Transversal
|
|
82
|
+
'searchIssues', // GET /search/issues → iterable, cubo propio
|
|
83
|
+
];
|
|
84
|
+
/** Type guard — para validar entrada no fiable (DTOs, tool calls). */
|
|
85
|
+
function isGithubOperation(value) {
|
|
86
|
+
return (typeof value === 'string' &&
|
|
87
|
+
exports.GITHUB_OPERATIONS.includes(value));
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Las operaciones que devuelven un ARRAY y por tanto obligan a marcar
|
|
91
|
+
* `_meta.iterable` en `getOutputPayload`.
|
|
92
|
+
*
|
|
93
|
+
* Está aquí y no sólo en la api porque el error de olvidarlo no falla al
|
|
94
|
+
* compilar: falla en producción, callado, con el nodo de abajo recibiendo el
|
|
95
|
+
* sobre entero en vez de un elemento, y el PayloadViewer pintando rutas con
|
|
96
|
+
* corchetes que nadie puede usar. Que la lista viva en el paquete permite que
|
|
97
|
+
* el test de la api la recorra en vez de repetirla a mano.
|
|
98
|
+
*/
|
|
99
|
+
exports.GITHUB_ITERABLE_OPERATIONS = [
|
|
100
|
+
'listIssues',
|
|
101
|
+
'listPullRequests',
|
|
102
|
+
'searchIssues',
|
|
103
|
+
];
|
|
104
|
+
/* Ayudantes — casi toda operación empieza eligiendo repositorio, y repetir el
|
|
105
|
+
mismo objeto catorce veces es cómo se desincronizan las descripciones. */
|
|
106
|
+
const repo = () => ({
|
|
107
|
+
name: 'repo',
|
|
108
|
+
label: 'Repository',
|
|
109
|
+
type: 'githubRepo',
|
|
110
|
+
required: true,
|
|
111
|
+
description: 'De la lista de repos que alcanza esta credencial. Se guarda como owner/repo.',
|
|
112
|
+
});
|
|
113
|
+
const issueNumber = (label = 'Issue number') => ({
|
|
114
|
+
name: 'issueNumber',
|
|
115
|
+
label,
|
|
116
|
+
type: 'template',
|
|
117
|
+
required: true,
|
|
118
|
+
placeholder: '{{payload.issue.number}}',
|
|
119
|
+
description: 'El número que se ve en la URL del issue, no su id interno.',
|
|
120
|
+
});
|
|
121
|
+
const pullNumber = () => ({
|
|
122
|
+
name: 'pullNumber',
|
|
123
|
+
label: 'Pull request number',
|
|
124
|
+
type: 'template',
|
|
125
|
+
required: true,
|
|
126
|
+
placeholder: '{{payload.pull_request.number}}',
|
|
127
|
+
description: 'El número que se ve en la URL del PR, no su id interno.',
|
|
128
|
+
});
|
|
129
|
+
const perPage = () => ({
|
|
130
|
+
name: 'perPage',
|
|
131
|
+
label: 'How many at most',
|
|
132
|
+
type: 'number',
|
|
133
|
+
default: 30,
|
|
134
|
+
min: 1,
|
|
135
|
+
max: 100,
|
|
136
|
+
advanced: true,
|
|
137
|
+
description: 'GitHub no devuelve más de 100 por página.',
|
|
138
|
+
});
|
|
139
|
+
exports.GITHUB_OPERATION_SPECS = {
|
|
140
|
+
/* ── Issues ────────────────────────────────────────────────────────────── */
|
|
141
|
+
createIssue: {
|
|
142
|
+
label: 'Create issue',
|
|
143
|
+
description: 'Abre un issue nuevo en el repositorio.',
|
|
144
|
+
apiRoute: 'POST /repos/{owner}/{repo}/issues',
|
|
145
|
+
params: [
|
|
146
|
+
repo(),
|
|
147
|
+
{
|
|
148
|
+
name: 'title',
|
|
149
|
+
label: 'Title',
|
|
150
|
+
type: 'template',
|
|
151
|
+
required: true,
|
|
152
|
+
placeholder: 'Error en el pago: {{payload.order.id}}',
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
name: 'body',
|
|
156
|
+
label: 'Body',
|
|
157
|
+
type: 'textarea',
|
|
158
|
+
placeholder: 'Lo que llegó:\n\n{{payload.message}}',
|
|
159
|
+
description: 'Admite Markdown, que es como GitHub lo va a pintar.',
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
name: 'labels',
|
|
163
|
+
label: 'Labels',
|
|
164
|
+
type: 'githubLabels',
|
|
165
|
+
description: 'De las que ya existen en el repo.',
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
name: 'assignees',
|
|
169
|
+
label: 'Assignees',
|
|
170
|
+
type: 'githubAssignees',
|
|
171
|
+
description: 'Sólo se puede asignar a quien tiene acceso al repo.',
|
|
172
|
+
},
|
|
173
|
+
],
|
|
174
|
+
},
|
|
175
|
+
updateIssue: {
|
|
176
|
+
label: 'Update issue',
|
|
177
|
+
description: 'Cambia título, cuerpo, estado, etiquetas o asignados.',
|
|
178
|
+
apiRoute: 'PATCH /repos/{owner}/{repo}/issues/{issue_number}',
|
|
179
|
+
params: [
|
|
180
|
+
repo(),
|
|
181
|
+
issueNumber(),
|
|
182
|
+
{
|
|
183
|
+
name: 'title',
|
|
184
|
+
label: 'New title',
|
|
185
|
+
type: 'template',
|
|
186
|
+
description: 'Se deja vacío para no tocarlo.',
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
name: 'body',
|
|
190
|
+
label: 'New body',
|
|
191
|
+
type: 'textarea',
|
|
192
|
+
description: 'Se deja vacío para no tocarlo. Reemplaza el cuerpo entero.',
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
name: 'state',
|
|
196
|
+
label: 'State',
|
|
197
|
+
type: 'select',
|
|
198
|
+
options: [
|
|
199
|
+
{ value: '', label: '— sin tocar —' },
|
|
200
|
+
{ value: 'open', label: 'Open (reabrir)' },
|
|
201
|
+
{ value: 'closed', label: 'Closed (cerrar)' },
|
|
202
|
+
],
|
|
203
|
+
description: 'Cerrar y reabrir se hacen aquí; no hay operación aparte.',
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
name: 'stateReason',
|
|
207
|
+
label: 'Why closed',
|
|
208
|
+
type: 'select',
|
|
209
|
+
showWhen: { field: 'state', in: ['closed'] },
|
|
210
|
+
options: [
|
|
211
|
+
{ value: 'completed', label: 'Completed — se resolvió' },
|
|
212
|
+
{ value: 'not_planned', label: 'Not planned — no se va a hacer' },
|
|
213
|
+
],
|
|
214
|
+
description: 'Es lo que decide qué icono pinta GitHub al cerrarlo.',
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
name: 'labels',
|
|
218
|
+
label: 'Labels',
|
|
219
|
+
type: 'githubLabels',
|
|
220
|
+
description: 'Ojo: REEMPLAZA las etiquetas del issue, no las añade. Vacío las deja como están.',
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
name: 'assignees',
|
|
224
|
+
label: 'Assignees',
|
|
225
|
+
type: 'githubAssignees',
|
|
226
|
+
description: 'Igual que las etiquetas: reemplaza, no añade.',
|
|
227
|
+
},
|
|
228
|
+
],
|
|
229
|
+
},
|
|
230
|
+
getIssue: {
|
|
231
|
+
label: 'Get issue',
|
|
232
|
+
description: 'Lee un issue por su número.',
|
|
233
|
+
apiRoute: 'GET /repos/{owner}/{repo}/issues/{issue_number}',
|
|
234
|
+
params: [repo(), issueNumber()],
|
|
235
|
+
},
|
|
236
|
+
commentIssue: {
|
|
237
|
+
label: 'Comment on issue',
|
|
238
|
+
description: 'Añade un comentario a un issue o a un pull request.',
|
|
239
|
+
apiRoute: 'POST /repos/{owner}/{repo}/issues/{issue_number}/comments',
|
|
240
|
+
params: [
|
|
241
|
+
repo(),
|
|
242
|
+
issueNumber('Issue or PR number'),
|
|
243
|
+
{
|
|
244
|
+
name: 'body',
|
|
245
|
+
label: 'Comment',
|
|
246
|
+
type: 'textarea',
|
|
247
|
+
required: true,
|
|
248
|
+
placeholder: 'Desplegado en producción: {{payload.deployment.url}}',
|
|
249
|
+
description: 'Vale igual para un PR: en la API de GitHub un pull request ES un issue.',
|
|
250
|
+
},
|
|
251
|
+
],
|
|
252
|
+
},
|
|
253
|
+
listIssues: {
|
|
254
|
+
label: 'List issues',
|
|
255
|
+
description: 'Los issues del repositorio, filtrables. Devuelve una lista.',
|
|
256
|
+
apiRoute: 'GET /repos/{owner}/{repo}/issues',
|
|
257
|
+
params: [
|
|
258
|
+
repo(),
|
|
259
|
+
{
|
|
260
|
+
name: 'state',
|
|
261
|
+
label: 'State',
|
|
262
|
+
type: 'select',
|
|
263
|
+
default: 'open',
|
|
264
|
+
options: [
|
|
265
|
+
{ value: 'open', label: 'Open' },
|
|
266
|
+
{ value: 'closed', label: 'Closed' },
|
|
267
|
+
{ value: 'all', label: 'Todos' },
|
|
268
|
+
],
|
|
269
|
+
},
|
|
270
|
+
{
|
|
271
|
+
name: 'labels',
|
|
272
|
+
label: 'With these labels',
|
|
273
|
+
type: 'githubLabels',
|
|
274
|
+
description: 'Un issue tiene que llevarlas TODAS para salir.',
|
|
275
|
+
},
|
|
276
|
+
{
|
|
277
|
+
name: 'assignee',
|
|
278
|
+
label: 'Assigned to',
|
|
279
|
+
type: 'githubAssignees',
|
|
280
|
+
description: 'Una sola persona. Vacío no filtra.',
|
|
281
|
+
},
|
|
282
|
+
perPage(),
|
|
283
|
+
],
|
|
284
|
+
},
|
|
285
|
+
/* ── Pull requests ─────────────────────────────────────────────────────── */
|
|
286
|
+
createPullRequest: {
|
|
287
|
+
label: 'Create pull request',
|
|
288
|
+
description: 'Abre un PR de una rama contra otra.',
|
|
289
|
+
apiRoute: 'POST /repos/{owner}/{repo}/pulls',
|
|
290
|
+
params: [
|
|
291
|
+
repo(),
|
|
292
|
+
{
|
|
293
|
+
name: 'title',
|
|
294
|
+
label: 'Title',
|
|
295
|
+
type: 'template',
|
|
296
|
+
required: true,
|
|
297
|
+
placeholder: 'Actualiza dependencias — {{payload.date}}',
|
|
298
|
+
},
|
|
299
|
+
{
|
|
300
|
+
name: 'head',
|
|
301
|
+
label: 'From branch',
|
|
302
|
+
type: 'githubBranch',
|
|
303
|
+
required: true,
|
|
304
|
+
description: 'La rama con los cambios. La que se quiere fusionar.',
|
|
305
|
+
},
|
|
306
|
+
{
|
|
307
|
+
name: 'base',
|
|
308
|
+
label: 'Into branch',
|
|
309
|
+
type: 'githubBranch',
|
|
310
|
+
required: true,
|
|
311
|
+
description: 'La rama de destino. Normalmente main.',
|
|
312
|
+
},
|
|
313
|
+
{ name: 'body', label: 'Description', type: 'textarea' },
|
|
314
|
+
{
|
|
315
|
+
name: 'draft',
|
|
316
|
+
label: 'Open as draft',
|
|
317
|
+
type: 'booleanSelect',
|
|
318
|
+
options: [
|
|
319
|
+
{ value: '', label: '— sin tocar —' },
|
|
320
|
+
{ value: 'true', label: 'Sí, en borrador' },
|
|
321
|
+
{ value: 'false', label: 'No, listo para revisar' },
|
|
322
|
+
],
|
|
323
|
+
advanced: true,
|
|
324
|
+
},
|
|
325
|
+
],
|
|
326
|
+
},
|
|
327
|
+
getPullRequest: {
|
|
328
|
+
label: 'Get pull request',
|
|
329
|
+
description: 'Lee un PR por su número, con su estado de fusión.',
|
|
330
|
+
apiRoute: 'GET /repos/{owner}/{repo}/pulls/{pull_number}',
|
|
331
|
+
params: [repo(), pullNumber()],
|
|
332
|
+
},
|
|
333
|
+
mergePullRequest: {
|
|
334
|
+
label: 'Merge pull request',
|
|
335
|
+
description: 'Fusiona un PR abierto.',
|
|
336
|
+
apiRoute: 'PUT /repos/{owner}/{repo}/pulls/{pull_number}/merge',
|
|
337
|
+
params: [
|
|
338
|
+
repo(),
|
|
339
|
+
pullNumber(),
|
|
340
|
+
{
|
|
341
|
+
name: 'mergeMethod',
|
|
342
|
+
label: 'How',
|
|
343
|
+
type: 'select',
|
|
344
|
+
default: 'merge',
|
|
345
|
+
options: [
|
|
346
|
+
{ value: 'merge', label: 'Merge commit' },
|
|
347
|
+
{ value: 'squash', label: 'Squash and merge' },
|
|
348
|
+
{ value: 'rebase', label: 'Rebase and merge' },
|
|
349
|
+
],
|
|
350
|
+
description: 'El repo puede tener métodos deshabilitados en sus ajustes; si el elegido no está permitido, GitHub responde 405.',
|
|
351
|
+
},
|
|
352
|
+
{
|
|
353
|
+
name: 'commitTitle',
|
|
354
|
+
label: 'Commit title',
|
|
355
|
+
type: 'template',
|
|
356
|
+
advanced: true,
|
|
357
|
+
description: 'Vacío deja el que GitHub genera solo.',
|
|
358
|
+
},
|
|
359
|
+
{
|
|
360
|
+
name: 'commitMessage',
|
|
361
|
+
label: 'Commit message',
|
|
362
|
+
type: 'textarea',
|
|
363
|
+
advanced: true,
|
|
364
|
+
},
|
|
365
|
+
],
|
|
366
|
+
},
|
|
367
|
+
listPullRequests: {
|
|
368
|
+
label: 'List pull requests',
|
|
369
|
+
description: 'Los PRs del repositorio, filtrables. Devuelve una lista.',
|
|
370
|
+
apiRoute: 'GET /repos/{owner}/{repo}/pulls',
|
|
371
|
+
params: [
|
|
372
|
+
repo(),
|
|
373
|
+
{
|
|
374
|
+
name: 'state',
|
|
375
|
+
label: 'State',
|
|
376
|
+
type: 'select',
|
|
377
|
+
default: 'open',
|
|
378
|
+
options: [
|
|
379
|
+
{ value: 'open', label: 'Open' },
|
|
380
|
+
{ value: 'closed', label: 'Closed' },
|
|
381
|
+
{ value: 'all', label: 'Todos' },
|
|
382
|
+
],
|
|
383
|
+
},
|
|
384
|
+
{
|
|
385
|
+
name: 'base',
|
|
386
|
+
label: 'Into branch',
|
|
387
|
+
type: 'githubBranch',
|
|
388
|
+
description: 'Sólo los que apuntan a esta rama. Vacío no filtra.',
|
|
389
|
+
},
|
|
390
|
+
perPage(),
|
|
391
|
+
],
|
|
392
|
+
},
|
|
393
|
+
/* ── Contenido del repositorio ─────────────────────────────────────────── */
|
|
394
|
+
getFile: {
|
|
395
|
+
label: 'Get file',
|
|
396
|
+
description: 'Lee un archivo del repositorio, ya decodificado.',
|
|
397
|
+
apiRoute: 'GET /repos/{owner}/{repo}/contents/{path}',
|
|
398
|
+
params: [
|
|
399
|
+
repo(),
|
|
400
|
+
{
|
|
401
|
+
name: 'path',
|
|
402
|
+
label: 'Path',
|
|
403
|
+
type: 'template',
|
|
404
|
+
required: true,
|
|
405
|
+
placeholder: 'package.json',
|
|
406
|
+
description: 'Ruta desde la raíz del repo, sin barra inicial.',
|
|
407
|
+
},
|
|
408
|
+
{
|
|
409
|
+
name: 'ref',
|
|
410
|
+
label: 'Branch',
|
|
411
|
+
type: 'githubBranch',
|
|
412
|
+
description: 'Vacío usa la rama por defecto del repo.',
|
|
413
|
+
},
|
|
414
|
+
],
|
|
415
|
+
},
|
|
416
|
+
createOrUpdateFile: {
|
|
417
|
+
label: 'Create or update file',
|
|
418
|
+
description: 'Escribe un archivo y commitea el cambio.',
|
|
419
|
+
apiRoute: 'PUT /repos/{owner}/{repo}/contents/{path}',
|
|
420
|
+
params: [
|
|
421
|
+
repo(),
|
|
422
|
+
{
|
|
423
|
+
name: 'path',
|
|
424
|
+
label: 'Path',
|
|
425
|
+
type: 'template',
|
|
426
|
+
required: true,
|
|
427
|
+
placeholder: 'data/report.md',
|
|
428
|
+
description: 'Si el archivo ya existe se actualiza; si no, se crea. El sha lo resuelve el nodo — no hace falta dárselo.',
|
|
429
|
+
},
|
|
430
|
+
{
|
|
431
|
+
name: 'content',
|
|
432
|
+
label: 'Content',
|
|
433
|
+
type: 'textarea',
|
|
434
|
+
required: true,
|
|
435
|
+
description: 'Texto plano. El nodo lo codifica antes de mandarlo.',
|
|
436
|
+
},
|
|
437
|
+
{
|
|
438
|
+
name: 'commitMessage',
|
|
439
|
+
label: 'Commit message',
|
|
440
|
+
type: 'template',
|
|
441
|
+
required: true,
|
|
442
|
+
placeholder: 'Actualiza el informe de {{payload.date}}',
|
|
443
|
+
},
|
|
444
|
+
{
|
|
445
|
+
name: 'branch',
|
|
446
|
+
label: 'Branch',
|
|
447
|
+
type: 'githubBranch',
|
|
448
|
+
description: 'Vacío commitea en la rama por defecto del repo.',
|
|
449
|
+
},
|
|
450
|
+
],
|
|
451
|
+
},
|
|
452
|
+
createBranch: {
|
|
453
|
+
label: 'Create branch',
|
|
454
|
+
description: 'Crea una rama a partir de otra.',
|
|
455
|
+
apiRoute: 'POST /repos/{owner}/{repo}/git/refs',
|
|
456
|
+
params: [
|
|
457
|
+
repo(),
|
|
458
|
+
{
|
|
459
|
+
name: 'newBranch',
|
|
460
|
+
label: 'New branch name',
|
|
461
|
+
type: 'template',
|
|
462
|
+
required: true,
|
|
463
|
+
placeholder: 'fix/{{payload.issue.number}}',
|
|
464
|
+
description: 'Sin el prefijo refs/heads/ — lo pone el nodo.',
|
|
465
|
+
},
|
|
466
|
+
{
|
|
467
|
+
name: 'fromBranch',
|
|
468
|
+
label: 'From branch',
|
|
469
|
+
type: 'githubBranch',
|
|
470
|
+
required: true,
|
|
471
|
+
description: 'De dónde parte. El sha de su punta lo resuelve el nodo desde este nombre.',
|
|
472
|
+
},
|
|
473
|
+
],
|
|
474
|
+
},
|
|
475
|
+
/* ── Automatización ────────────────────────────────────────────────────── */
|
|
476
|
+
dispatchWorkflow: {
|
|
477
|
+
label: 'Run workflow',
|
|
478
|
+
description: 'Dispara un workflow de GitHub Actions.',
|
|
479
|
+
apiRoute: 'POST /repos/{owner}/{repo}/actions/workflows/{workflow_id}/dispatches',
|
|
480
|
+
params: [
|
|
481
|
+
repo(),
|
|
482
|
+
{
|
|
483
|
+
name: 'workflow',
|
|
484
|
+
label: 'Workflow',
|
|
485
|
+
type: 'githubWorkflow',
|
|
486
|
+
required: true,
|
|
487
|
+
description: 'Sólo salen los que declaran workflow_dispatch: el resto no se puede disparar desde fuera.',
|
|
488
|
+
},
|
|
489
|
+
{
|
|
490
|
+
name: 'ref',
|
|
491
|
+
label: 'Branch or tag',
|
|
492
|
+
type: 'githubBranch',
|
|
493
|
+
required: true,
|
|
494
|
+
description: 'Desde dónde se corre el workflow.',
|
|
495
|
+
},
|
|
496
|
+
{
|
|
497
|
+
name: 'inputs',
|
|
498
|
+
label: 'Inputs',
|
|
499
|
+
type: 'json',
|
|
500
|
+
advanced: true,
|
|
501
|
+
placeholder: '{ "environment": "production" }',
|
|
502
|
+
description: 'Los inputs que declara el workflow. Objeto plano: GitHub sólo acepta cadenas como valores.',
|
|
503
|
+
},
|
|
504
|
+
],
|
|
505
|
+
},
|
|
506
|
+
/* ── Transversal ───────────────────────────────────────────────────────── */
|
|
507
|
+
searchIssues: {
|
|
508
|
+
label: 'Search issues and PRs',
|
|
509
|
+
description: 'Busca en todo GitHub con su sintaxis de búsqueda. Devuelve una lista.',
|
|
510
|
+
apiRoute: 'GET /search/issues',
|
|
511
|
+
params: [
|
|
512
|
+
{
|
|
513
|
+
name: 'query',
|
|
514
|
+
label: 'Query',
|
|
515
|
+
type: 'template',
|
|
516
|
+
required: true,
|
|
517
|
+
placeholder: 'repo:hostwebhook/dashboard is:open label:bug',
|
|
518
|
+
description: 'La misma sintaxis de la barra de búsqueda de GitHub. Sin repo: busca en todo lo que alcance la credencial.',
|
|
519
|
+
},
|
|
520
|
+
{
|
|
521
|
+
name: 'sort',
|
|
522
|
+
label: 'Sort by',
|
|
523
|
+
type: 'select',
|
|
524
|
+
options: [
|
|
525
|
+
{ value: '', label: 'Relevancia' },
|
|
526
|
+
{ value: 'created', label: 'Fecha de creación' },
|
|
527
|
+
{ value: 'updated', label: 'Última actualización' },
|
|
528
|
+
{ value: 'comments', label: 'Número de comentarios' },
|
|
529
|
+
],
|
|
530
|
+
advanced: true,
|
|
531
|
+
},
|
|
532
|
+
{
|
|
533
|
+
name: 'order',
|
|
534
|
+
label: 'Order',
|
|
535
|
+
type: 'select',
|
|
536
|
+
default: 'desc',
|
|
537
|
+
options: [
|
|
538
|
+
{ value: 'desc', label: 'Descendente' },
|
|
539
|
+
{ value: 'asc', label: 'Ascendente' },
|
|
540
|
+
],
|
|
541
|
+
advanced: true,
|
|
542
|
+
},
|
|
543
|
+
perPage(),
|
|
544
|
+
],
|
|
545
|
+
},
|
|
546
|
+
};
|
|
547
|
+
/**
|
|
548
|
+
* Lo que se ofrece en el desplegable de operación.
|
|
549
|
+
*
|
|
550
|
+
* Hoy son todas. Existe como constante aparte —y no como alias de
|
|
551
|
+
* `GITHUB_OPERATIONS`— porque es exactamente el punto donde otros nodos han
|
|
552
|
+
* necesitado esconder una op sin sacarla del enum: retirarla del enum rompe
|
|
553
|
+
* las que ya estaban guardadas en la base de datos.
|
|
554
|
+
*/
|
|
555
|
+
exports.GITHUB_DROPDOWN_OPERATIONS = exports.GITHUB_OPERATIONS;
|
package/dist/index.d.ts
CHANGED
|
@@ -26,6 +26,10 @@ export { DISCORD_OPERATIONS, DISCORD_OPERATION_SPECS, isDiscordOperation, } from
|
|
|
26
26
|
export type { DiscordOperation, DiscordParamSpec, DiscordOperationSpec, } from './discord-operations';
|
|
27
27
|
export { MAILCHIMP_OPERATIONS, MAILCHIMP_OPERATION_SPECS, MAILCHIMP_CONTACT_STATUSES, isMailchimpOperation, } from './mailchimp-operations';
|
|
28
28
|
export type { MailchimpOperation, MailchimpContactStatus, MailchimpParamSpec, MailchimpOperationSpec, } from './mailchimp-operations';
|
|
29
|
+
export { SHOPIFY_OPERATIONS, SHOPIFY_OPERATION_SPECS, SHOPIFY_TAGGABLE_RESOURCES, SHOPIFY_SEARCHABLE_RESOURCES, isShopifyOperation, } from './shopify-operations';
|
|
30
|
+
export type { ShopifyOperation, ShopifyTaggableResource, ShopifySearchableResource, ShopifyParamSpec, ShopifyOperationSpec, } from './shopify-operations';
|
|
31
|
+
export { GITHUB_OPERATIONS, GITHUB_OPERATION_SPECS, GITHUB_DROPDOWN_OPERATIONS, GITHUB_ITERABLE_OPERATIONS, isGithubOperation, } from './github-operations';
|
|
32
|
+
export type { GithubOperation, GithubParamType, GithubParamSpec, GithubOperationSpec, } from './github-operations';
|
|
29
33
|
export { SLACK_OPERATIONS, SLACK_OPERATION_SPECS, isSlackOperation, } from './slack-operations';
|
|
30
34
|
export type { SlackOperation, SlackParamSpec, SlackOperationSpec, } from './slack-operations';
|
|
31
35
|
export { SHEETS_OPERATIONS, SHEETS_OPERATION_SPECS, isSheetsOperation, } from './sheets-operations';
|
package/dist/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.DRIVE_OPERATION_SPECS = exports.DRIVE_OPERATIONS = exports.GOOGLE_CALENDAR_TOOLKIT_BY_TOOL_NAME = exports.GOOGLE_CALENDAR_TOOLKIT_SPECS = exports.isGoogleCalendarOperation = exports.GOOGLE_CALENDAR_OPERATION_SPECS = exports.GOOGLE_CALENDAR_OPERATIONS = exports.isGmailOperation = exports.resolveGmailSendFields = exports.GMAIL_SEND_LEGACY_FIELDS = exports.NATIVE_EMAIL_TOOLKIT_BY_TOOL_NAME = exports.GMAIL_TOOLKIT_BY_TOOL_NAME = exports.NATIVE_EMAIL_TOOLKIT_SPECS = exports.GMAIL_SEND_AND_WAIT_TOOL_SPEC = exports.GMAIL_ALL_TOOLKIT_SPECS = exports.GMAIL_TOOLKIT_SPECS = exports.GMAIL_TOOLKIT_OPERATIONS = exports.GMAIL_DROPDOWN_OPERATIONS = exports.GMAIL_OPERATION_GROUPS = exports.GMAIL_OPERATION_SPECS = exports.GMAIL_OPERATIONS = exports.versionCatalogErrors = exports.fieldsLost = exports.fieldsLostBetween = exports.currentVersion = exports.versionSpec = exports.versionsOf = exports.isVersioned = exports.NODE_TYPE_TO_PREFIX = exports.PREFIX_TO_NODE_TYPE = exports.NODE_STATE_KEYS = exports.NODE_COLORS = exports.NODE_DETAIL_PATHS = exports.getNodeRegistryEntry = exports.NODE_REGISTRY = exports.getNodeDispatchConfig = exports.getAllNodeCollections = exports.NODE_DISPATCH = exports.resolveNodeId = exports.PREFIX_TO_TYPE = exports.NODE_UI = exports.ALL_NODE_TYPES = exports.isNodeType = exports.isTerminal = exports.canSendToNodes = exports.canReceiveFromNodes = exports.canReceiveFrom = exports.NODE_CONNECTIONS = exports.iterableMeta = exports.singleMeta = void 0;
|
|
4
|
-
exports.
|
|
5
|
-
exports.isCredentialType = exports.getCredentialType = exports.credentialTypeValues = exports.CREDENTIAL_TYPE_VALUES = exports.CREDENTIAL_TYPES = exports.getModelLabel = exports.getDefaultModel = exports.getModelsFor = exports.MODEL_CONTEXT_WINDOWS = exports.LLM_MODELS = exports.LLM_PROVIDERS = exports.DOCS_TOOLKIT_DEFAULTABLE = exports.DOCS_TOOLKIT_BY_TOOL_NAME = exports.DOCS_TOOLKIT_SPECS = exports.isDocsOperation = void 0;
|
|
4
|
+
exports.NOTION_DROPDOWN_OPERATIONS = exports.NOTION_OPERATION_SPECS = exports.NOTION_OPERATIONS = exports.isGoogleAnalyticsOperation = exports.GOOGLE_ANALYTICS_DROPDOWN_OPERATIONS = exports.GOOGLE_ANALYTICS_OPERATION_SPECS = exports.GOOGLE_ANALYTICS_OPERATIONS = exports.isGoogleContactsOperation = exports.GOOGLE_CONTACTS_DEFAULT_PERSON_FIELDS = exports.GOOGLE_CONTACTS_OPERATION_GROUPS = exports.GOOGLE_CONTACTS_OPERATION_SPECS = exports.GOOGLE_CONTACTS_OPERATIONS_V2 = exports.GOOGLE_CONTACTS_OPERATIONS_V1 = exports.GOOGLE_CONTACTS_OPERATIONS = exports.SHEETS_TOOLKIT_DEFAULTABLE = exports.SHEETS_TOOLKIT_BY_TOOL_NAME = exports.SHEETS_TOOLKIT_SPECS = exports.isSheetsOperation = exports.SHEETS_OPERATION_SPECS = exports.SHEETS_OPERATIONS = exports.isSlackOperation = exports.SLACK_OPERATION_SPECS = exports.SLACK_OPERATIONS = exports.isGithubOperation = exports.GITHUB_ITERABLE_OPERATIONS = exports.GITHUB_DROPDOWN_OPERATIONS = exports.GITHUB_OPERATION_SPECS = exports.GITHUB_OPERATIONS = exports.isShopifyOperation = exports.SHOPIFY_SEARCHABLE_RESOURCES = exports.SHOPIFY_TAGGABLE_RESOURCES = exports.SHOPIFY_OPERATION_SPECS = exports.SHOPIFY_OPERATIONS = exports.isMailchimpOperation = exports.MAILCHIMP_CONTACT_STATUSES = exports.MAILCHIMP_OPERATION_SPECS = exports.MAILCHIMP_OPERATIONS = exports.isDiscordOperation = exports.DISCORD_OPERATION_SPECS = exports.DISCORD_OPERATIONS = exports.isWhatsAppOperation = exports.WHATSAPP_OPERATIONS = exports.TELEGRAM_TOOLKIT_BY_TOOL_NAME = exports.TELEGRAM_TOOLKIT_SPECS = exports.isTelegramOperation = exports.TELEGRAM_OPERATION_SPECS = exports.TELEGRAM_OPERATIONS = exports.DRIVE_TOOLKIT_BY_TOOL_NAME = exports.DRIVE_TOOLKIT_SPECS = exports.isDriveOperation = void 0;
|
|
5
|
+
exports.isCredentialType = exports.getCredentialType = exports.credentialTypeValues = exports.CREDENTIAL_TYPE_VALUES = exports.CREDENTIAL_TYPES = exports.getModelLabel = exports.getDefaultModel = exports.getModelsFor = exports.MODEL_CONTEXT_WINDOWS = exports.LLM_MODELS = exports.LLM_PROVIDERS = exports.DOCS_TOOLKIT_DEFAULTABLE = exports.DOCS_TOOLKIT_BY_TOOL_NAME = exports.DOCS_TOOLKIT_SPECS = exports.isDocsOperation = exports.DOCS_OPERATION_SPECS = exports.DOCS_OPERATIONS = exports.isMongoOperation = exports.MONGO_OPERATION_SPECS = exports.MONGO_OPERATIONS = exports.isPostgresOperation = exports.POSTGRES_OPERATION_SPECS = exports.POSTGRES_MODES = exports.POSTGRES_OPERATIONS = exports.isNotionOperation = void 0;
|
|
6
6
|
var types_1 = require("./types");
|
|
7
7
|
Object.defineProperty(exports, "singleMeta", { enumerable: true, get: function () { return types_1.singleMeta; } });
|
|
8
8
|
Object.defineProperty(exports, "iterableMeta", { enumerable: true, get: function () { return types_1.iterableMeta; } });
|
|
@@ -92,6 +92,18 @@ Object.defineProperty(exports, "MAILCHIMP_OPERATIONS", { enumerable: true, get:
|
|
|
92
92
|
Object.defineProperty(exports, "MAILCHIMP_OPERATION_SPECS", { enumerable: true, get: function () { return mailchimp_operations_1.MAILCHIMP_OPERATION_SPECS; } });
|
|
93
93
|
Object.defineProperty(exports, "MAILCHIMP_CONTACT_STATUSES", { enumerable: true, get: function () { return mailchimp_operations_1.MAILCHIMP_CONTACT_STATUSES; } });
|
|
94
94
|
Object.defineProperty(exports, "isMailchimpOperation", { enumerable: true, get: function () { return mailchimp_operations_1.isMailchimpOperation; } });
|
|
95
|
+
var shopify_operations_1 = require("./shopify-operations");
|
|
96
|
+
Object.defineProperty(exports, "SHOPIFY_OPERATIONS", { enumerable: true, get: function () { return shopify_operations_1.SHOPIFY_OPERATIONS; } });
|
|
97
|
+
Object.defineProperty(exports, "SHOPIFY_OPERATION_SPECS", { enumerable: true, get: function () { return shopify_operations_1.SHOPIFY_OPERATION_SPECS; } });
|
|
98
|
+
Object.defineProperty(exports, "SHOPIFY_TAGGABLE_RESOURCES", { enumerable: true, get: function () { return shopify_operations_1.SHOPIFY_TAGGABLE_RESOURCES; } });
|
|
99
|
+
Object.defineProperty(exports, "SHOPIFY_SEARCHABLE_RESOURCES", { enumerable: true, get: function () { return shopify_operations_1.SHOPIFY_SEARCHABLE_RESOURCES; } });
|
|
100
|
+
Object.defineProperty(exports, "isShopifyOperation", { enumerable: true, get: function () { return shopify_operations_1.isShopifyOperation; } });
|
|
101
|
+
var github_operations_1 = require("./github-operations");
|
|
102
|
+
Object.defineProperty(exports, "GITHUB_OPERATIONS", { enumerable: true, get: function () { return github_operations_1.GITHUB_OPERATIONS; } });
|
|
103
|
+
Object.defineProperty(exports, "GITHUB_OPERATION_SPECS", { enumerable: true, get: function () { return github_operations_1.GITHUB_OPERATION_SPECS; } });
|
|
104
|
+
Object.defineProperty(exports, "GITHUB_DROPDOWN_OPERATIONS", { enumerable: true, get: function () { return github_operations_1.GITHUB_DROPDOWN_OPERATIONS; } });
|
|
105
|
+
Object.defineProperty(exports, "GITHUB_ITERABLE_OPERATIONS", { enumerable: true, get: function () { return github_operations_1.GITHUB_ITERABLE_OPERATIONS; } });
|
|
106
|
+
Object.defineProperty(exports, "isGithubOperation", { enumerable: true, get: function () { return github_operations_1.isGithubOperation; } });
|
|
95
107
|
var slack_operations_1 = require("./slack-operations");
|
|
96
108
|
Object.defineProperty(exports, "SLACK_OPERATIONS", { enumerable: true, get: function () { return slack_operations_1.SLACK_OPERATIONS; } });
|
|
97
109
|
Object.defineProperty(exports, "SLACK_OPERATION_SPECS", { enumerable: true, get: function () { return slack_operations_1.SLACK_OPERATION_SPECS; } });
|
package/dist/registry.js
CHANGED
|
@@ -273,6 +273,34 @@ exports.NODE_REGISTRY = {
|
|
|
273
273
|
// light canvas. The brand value (#ffe01b) reads as a highlighter there.
|
|
274
274
|
color: '#c9a227', testable: true,
|
|
275
275
|
},
|
|
276
|
+
shopifyAction: {
|
|
277
|
+
type: 'shopifyAction', prefix: 'shp', label: 'Shopify',
|
|
278
|
+
group: 'Actions', detailPath: '/dashboard/shopify-actions', apiPath: '/shopify-actions',
|
|
279
|
+
stateKey: 'shopifyActions', allStateKey: 'allShopifyActions',
|
|
280
|
+
// Shopify's green, abierto un punto. El de marca (#008060) es más oscuro
|
|
281
|
+
// que cualquier otro color de este registro y se hunde contra el lienzo
|
|
282
|
+
// en modo oscuro; éste sigue leyéndose como Shopify y aguanta en los dos
|
|
283
|
+
// temas. Mismo criterio que el amarillo de Mailchimp, al revés.
|
|
284
|
+
color: '#0a9e70', testable: true,
|
|
285
|
+
},
|
|
286
|
+
githubAction: {
|
|
287
|
+
type: 'githubAction', prefix: 'gh', label: 'GitHub',
|
|
288
|
+
group: 'Actions', detailPath: '/dashboard/github-actions', apiPath: '/github-actions',
|
|
289
|
+
stateKey: 'githubActions', allStateKey: 'allGithubActions',
|
|
290
|
+
// El morado de "merged" de GitHub. Su marca real es acromática, y ahí no
|
|
291
|
+
// hay elección posible: en el minimapa lo único que distingue a un nodo es
|
|
292
|
+
// el color, el gris cálido ya lo ocupa Notion y el frío lo llevan tres.
|
|
293
|
+
//
|
|
294
|
+
// ⚠️ Es la banda MÁS disputada de este registro — ocho nodos en morado,
|
|
295
|
+
// con #7c3aed y #a855f7 como vecinos inmediatos. Se eligió por ser el
|
|
296
|
+
// color que de verdad dice GitHub, pero **la decisión se cierra mirando el
|
|
297
|
+
// minimapa**, no leyendo este comentario. Si choca, el plan B es un
|
|
298
|
+
// grafito claro, sabiendo que compite con Notion.
|
|
299
|
+
color: '#8957e5', testable: true,
|
|
300
|
+
/* Sin isToolOnly: la v1 no trae modo toolkit de IA, y aquí pesa más que en
|
|
301
|
+
ningún otro nodo — el MCP oficial de GitHub ya cubre ese caso dentro del
|
|
302
|
+
AI Node. Los rieles son este nodo; la improvisación es el MCP. */
|
|
303
|
+
},
|
|
276
304
|
telegramAction: {
|
|
277
305
|
type: 'telegramAction', prefix: 'tg', label: 'Telegram Action',
|
|
278
306
|
group: 'Actions', detailPath: '/dashboard/telegram-actions', apiPath: '/telegram-actions',
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shopify Admin API operations — single source of truth across the api, the
|
|
3
|
+
* dashboard and downstream consumers (MCP server).
|
|
4
|
+
*
|
|
5
|
+
* ## Four, out of several hundred
|
|
6
|
+
*
|
|
7
|
+
* The Admin API has hundreds of mutations. This node exposes four, and the
|
|
8
|
+
* choice is argued in `api/docs/ADR-0007-shopify.md`: a menu of everything is
|
|
9
|
+
* how a node becomes unusable, and these four cover what people actually
|
|
10
|
+
* automate — put a customer in the store, tag something, sync stock, and read
|
|
11
|
+
* back what the store already knows.
|
|
12
|
+
*
|
|
13
|
+
* Product creation and order fulfilment were considered and left out of v1.
|
|
14
|
+
* A product means variants, media and prices; a fulfilment means a location, a
|
|
15
|
+
* fulfilment service and a state machine. Each is a configuration surface of
|
|
16
|
+
* its own, not a field.
|
|
17
|
+
*
|
|
18
|
+
* ## GraphQL only, and the version is not ours to drift on
|
|
19
|
+
*
|
|
20
|
+
* REST went legacy in October 2024 and public apps have been GraphQL-only
|
|
21
|
+
* since April 2025, so there is no REST path here to fall back to. Every
|
|
22
|
+
* `apiRoute` below is a GraphQL mutation or query name, checkable against
|
|
23
|
+
* `shopify.dev/docs/api/admin-graphql`. The API version lives in ONE constant
|
|
24
|
+
* on the api side (`SHOPIFY_API_VERSION`) — never interpolated per call site.
|
|
25
|
+
*
|
|
26
|
+
* ## What was verified, and what was not (2026-08-25)
|
|
27
|
+
*
|
|
28
|
+
* Checked against the Admin GraphQL reference, not from memory:
|
|
29
|
+
*
|
|
30
|
+
* - `tagsAdd(id: ID!, tags: [String!]!)` and `tagsRemove` with the same
|
|
31
|
+
* shape. Both accept Order, DraftOrder, Customer, Product, Article and
|
|
32
|
+
* DiscountNode.
|
|
33
|
+
* - `customerCreate(input: CustomerInput!)` and `customerUpdate(input:
|
|
34
|
+
* CustomerInput!)` — for the update, **the id goes inside the input**, not
|
|
35
|
+
* as a sibling argument.
|
|
36
|
+
* - `inventoryAdjustQuantities(input: InventoryAdjustQuantitiesInput!)`, whose
|
|
37
|
+
* input requires `reason`, `name` and a `changes[]` of
|
|
38
|
+
* `{ delta, inventoryItemId, locationId }`.
|
|
39
|
+
*
|
|
40
|
+
* ⚠️ **NOT verified, and deliberately left loose below** — do not turn either
|
|
41
|
+
* into a hard-coded dropdown until someone reads the enumeration:
|
|
42
|
+
*
|
|
43
|
+
* - The full vocabulary of `reason`. Only `"correction"` is documented by
|
|
44
|
+
* example.
|
|
45
|
+
* - The full vocabulary of `name`. Only `"available"` is documented by
|
|
46
|
+
* example; the docs mention `on_hand` moving as a consequence, which is not
|
|
47
|
+
* the same as it being a valid input.
|
|
48
|
+
* - The exact search syntax for finding a customer by email
|
|
49
|
+
* (`customers(query: "email:…")`). It is the obvious shape and almost
|
|
50
|
+
* certainly right, which is exactly why it is worth confirming rather than
|
|
51
|
+
* assuming.
|
|
52
|
+
*
|
|
53
|
+
* ⚠️ **`@idempotent` is mandatory.** As of API version 2026-04,
|
|
54
|
+
* `inventoryAdjustQuantities` must be sent with the `@idempotent` directive
|
|
55
|
+
* and an idempotency key. A stock adjustment is the one operation here where a
|
|
56
|
+
* silent retry is a real inventory error, so this is not boilerplate.
|
|
57
|
+
*/
|
|
58
|
+
export declare const SHOPIFY_OPERATIONS: readonly ["upsertCustomer", "setTags", "adjustInventory", "findRecords"];
|
|
59
|
+
export type ShopifyOperation = (typeof SHOPIFY_OPERATIONS)[number];
|
|
60
|
+
/** Type guard — for DTOs and AI tool calls, where the input is untrusted. */
|
|
61
|
+
export declare function isShopifyOperation(value: unknown): value is ShopifyOperation;
|
|
62
|
+
/**
|
|
63
|
+
* The resources `tagsAdd` / `tagsRemove` accept. Taken from the mutation
|
|
64
|
+
* reference — this list is the API's, not a subset we chose.
|
|
65
|
+
*
|
|
66
|
+
* `article` and `discountNode` are left out of the node's own picker on
|
|
67
|
+
* purpose: neither is something a HostWebhook flow tags in practice, and every
|
|
68
|
+
* extra option is a field the user has to read past.
|
|
69
|
+
*/
|
|
70
|
+
export declare const SHOPIFY_TAGGABLE_RESOURCES: readonly ["order", "customer", "product", "draftOrder"];
|
|
71
|
+
export type ShopifyTaggableResource = (typeof SHOPIFY_TAGGABLE_RESOURCES)[number];
|
|
72
|
+
/** What `findRecords` can read back. */
|
|
73
|
+
export declare const SHOPIFY_SEARCHABLE_RESOURCES: readonly ["orders", "customers", "products"];
|
|
74
|
+
export type ShopifySearchableResource = (typeof SHOPIFY_SEARCHABLE_RESOURCES)[number];
|
|
75
|
+
export interface ShopifyParamSpec {
|
|
76
|
+
/** Field key — also the property name on operationConfig. */
|
|
77
|
+
name: string;
|
|
78
|
+
label: string;
|
|
79
|
+
/** Param type, so the dashboard renders the right control. `location` is a
|
|
80
|
+
* picker backed by a live lookup of the store's locations; `gid` is a
|
|
81
|
+
* Shopify global id, which is almost always templated from the payload
|
|
82
|
+
* rather than typed. */
|
|
83
|
+
type: 'gid' | 'location' | 'email' | 'string' | 'number' | 'tags' | 'taggableResource' | 'searchableResource' | 'json' | 'boolean';
|
|
84
|
+
required?: boolean;
|
|
85
|
+
description: string;
|
|
86
|
+
placeholder?: string;
|
|
87
|
+
}
|
|
88
|
+
export interface ShopifyOperationSpec {
|
|
89
|
+
label: string;
|
|
90
|
+
description: string;
|
|
91
|
+
/** The GraphQL operation this maps to, so the mapping is checkable. */
|
|
92
|
+
apiRoute: string;
|
|
93
|
+
params: ShopifyParamSpec[];
|
|
94
|
+
}
|
|
95
|
+
export declare const SHOPIFY_OPERATION_SPECS: Record<ShopifyOperation, ShopifyOperationSpec>;
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Shopify Admin API operations — single source of truth across the api, the
|
|
4
|
+
* dashboard and downstream consumers (MCP server).
|
|
5
|
+
*
|
|
6
|
+
* ## Four, out of several hundred
|
|
7
|
+
*
|
|
8
|
+
* The Admin API has hundreds of mutations. This node exposes four, and the
|
|
9
|
+
* choice is argued in `api/docs/ADR-0007-shopify.md`: a menu of everything is
|
|
10
|
+
* how a node becomes unusable, and these four cover what people actually
|
|
11
|
+
* automate — put a customer in the store, tag something, sync stock, and read
|
|
12
|
+
* back what the store already knows.
|
|
13
|
+
*
|
|
14
|
+
* Product creation and order fulfilment were considered and left out of v1.
|
|
15
|
+
* A product means variants, media and prices; a fulfilment means a location, a
|
|
16
|
+
* fulfilment service and a state machine. Each is a configuration surface of
|
|
17
|
+
* its own, not a field.
|
|
18
|
+
*
|
|
19
|
+
* ## GraphQL only, and the version is not ours to drift on
|
|
20
|
+
*
|
|
21
|
+
* REST went legacy in October 2024 and public apps have been GraphQL-only
|
|
22
|
+
* since April 2025, so there is no REST path here to fall back to. Every
|
|
23
|
+
* `apiRoute` below is a GraphQL mutation or query name, checkable against
|
|
24
|
+
* `shopify.dev/docs/api/admin-graphql`. The API version lives in ONE constant
|
|
25
|
+
* on the api side (`SHOPIFY_API_VERSION`) — never interpolated per call site.
|
|
26
|
+
*
|
|
27
|
+
* ## What was verified, and what was not (2026-08-25)
|
|
28
|
+
*
|
|
29
|
+
* Checked against the Admin GraphQL reference, not from memory:
|
|
30
|
+
*
|
|
31
|
+
* - `tagsAdd(id: ID!, tags: [String!]!)` and `tagsRemove` with the same
|
|
32
|
+
* shape. Both accept Order, DraftOrder, Customer, Product, Article and
|
|
33
|
+
* DiscountNode.
|
|
34
|
+
* - `customerCreate(input: CustomerInput!)` and `customerUpdate(input:
|
|
35
|
+
* CustomerInput!)` — for the update, **the id goes inside the input**, not
|
|
36
|
+
* as a sibling argument.
|
|
37
|
+
* - `inventoryAdjustQuantities(input: InventoryAdjustQuantitiesInput!)`, whose
|
|
38
|
+
* input requires `reason`, `name` and a `changes[]` of
|
|
39
|
+
* `{ delta, inventoryItemId, locationId }`.
|
|
40
|
+
*
|
|
41
|
+
* ⚠️ **NOT verified, and deliberately left loose below** — do not turn either
|
|
42
|
+
* into a hard-coded dropdown until someone reads the enumeration:
|
|
43
|
+
*
|
|
44
|
+
* - The full vocabulary of `reason`. Only `"correction"` is documented by
|
|
45
|
+
* example.
|
|
46
|
+
* - The full vocabulary of `name`. Only `"available"` is documented by
|
|
47
|
+
* example; the docs mention `on_hand` moving as a consequence, which is not
|
|
48
|
+
* the same as it being a valid input.
|
|
49
|
+
* - The exact search syntax for finding a customer by email
|
|
50
|
+
* (`customers(query: "email:…")`). It is the obvious shape and almost
|
|
51
|
+
* certainly right, which is exactly why it is worth confirming rather than
|
|
52
|
+
* assuming.
|
|
53
|
+
*
|
|
54
|
+
* ⚠️ **`@idempotent` is mandatory.** As of API version 2026-04,
|
|
55
|
+
* `inventoryAdjustQuantities` must be sent with the `@idempotent` directive
|
|
56
|
+
* and an idempotency key. A stock adjustment is the one operation here where a
|
|
57
|
+
* silent retry is a real inventory error, so this is not boilerplate.
|
|
58
|
+
*/
|
|
59
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
60
|
+
exports.SHOPIFY_OPERATION_SPECS = exports.SHOPIFY_SEARCHABLE_RESOURCES = exports.SHOPIFY_TAGGABLE_RESOURCES = exports.SHOPIFY_OPERATIONS = void 0;
|
|
61
|
+
exports.isShopifyOperation = isShopifyOperation;
|
|
62
|
+
exports.SHOPIFY_OPERATIONS = [
|
|
63
|
+
'upsertCustomer',
|
|
64
|
+
'setTags',
|
|
65
|
+
'adjustInventory',
|
|
66
|
+
'findRecords',
|
|
67
|
+
];
|
|
68
|
+
/** Type guard — for DTOs and AI tool calls, where the input is untrusted. */
|
|
69
|
+
function isShopifyOperation(value) {
|
|
70
|
+
return (typeof value === 'string' &&
|
|
71
|
+
exports.SHOPIFY_OPERATIONS.includes(value));
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* The resources `tagsAdd` / `tagsRemove` accept. Taken from the mutation
|
|
75
|
+
* reference — this list is the API's, not a subset we chose.
|
|
76
|
+
*
|
|
77
|
+
* `article` and `discountNode` are left out of the node's own picker on
|
|
78
|
+
* purpose: neither is something a HostWebhook flow tags in practice, and every
|
|
79
|
+
* extra option is a field the user has to read past.
|
|
80
|
+
*/
|
|
81
|
+
exports.SHOPIFY_TAGGABLE_RESOURCES = [
|
|
82
|
+
'order',
|
|
83
|
+
'customer',
|
|
84
|
+
'product',
|
|
85
|
+
'draftOrder',
|
|
86
|
+
];
|
|
87
|
+
/** What `findRecords` can read back. */
|
|
88
|
+
exports.SHOPIFY_SEARCHABLE_RESOURCES = [
|
|
89
|
+
'orders',
|
|
90
|
+
'customers',
|
|
91
|
+
'products',
|
|
92
|
+
];
|
|
93
|
+
exports.SHOPIFY_OPERATION_SPECS = {
|
|
94
|
+
upsertCustomer: {
|
|
95
|
+
label: 'Add or update customer',
|
|
96
|
+
description: 'Look the customer up by email, then create them or update the fields you set.',
|
|
97
|
+
apiRoute: 'mutation customerCreate / customerUpdate (lookup by email first)',
|
|
98
|
+
params: [
|
|
99
|
+
{
|
|
100
|
+
name: 'email',
|
|
101
|
+
label: 'Email address',
|
|
102
|
+
type: 'email',
|
|
103
|
+
required: true,
|
|
104
|
+
description: 'Identifies the customer. This is what decides between creating and updating — there is no separate "id" to keep.',
|
|
105
|
+
placeholder: '{{payload.email}}',
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
name: 'firstName',
|
|
109
|
+
label: 'First name',
|
|
110
|
+
type: 'string',
|
|
111
|
+
description: 'Left untouched on an existing customer if you leave it empty.',
|
|
112
|
+
placeholder: '{{payload.firstName}}',
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
name: 'lastName',
|
|
116
|
+
label: 'Last name',
|
|
117
|
+
type: 'string',
|
|
118
|
+
description: 'Left untouched on an existing customer if you leave it empty.',
|
|
119
|
+
placeholder: '{{payload.lastName}}',
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
name: 'phone',
|
|
123
|
+
label: 'Phone',
|
|
124
|
+
type: 'string',
|
|
125
|
+
description: 'Shopify wants it in E.164 (+52…). A malformed number fails the whole mutation, not just the field.',
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
name: 'tags',
|
|
129
|
+
label: 'Tags',
|
|
130
|
+
type: 'tags',
|
|
131
|
+
description: 'Tags to set on the customer. Note these REPLACE the customer tags — to add without losing the existing ones, use the tag operation instead.',
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
name: 'note',
|
|
135
|
+
label: 'Note',
|
|
136
|
+
type: 'string',
|
|
137
|
+
description: 'Internal note, visible to staff in the Shopify admin.',
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
name: 'emailMarketingConsent',
|
|
141
|
+
label: 'Email marketing consent',
|
|
142
|
+
type: 'json',
|
|
143
|
+
description: 'Leave this EMPTY unless the customer actually consented. Omitting it leaves their consent exactly as it was; writing a subscribed state for someone who never opted in is a legal problem, not a technical one. Shape: {"marketingState":"SUBSCRIBED","marketingOptInLevel":"SINGLE_OPT_IN"}.',
|
|
144
|
+
},
|
|
145
|
+
],
|
|
146
|
+
},
|
|
147
|
+
setTags: {
|
|
148
|
+
label: 'Add or remove tags',
|
|
149
|
+
description: 'Tag an order, a customer or a product. Adding and removing are separate calls, so one node does one of the two.',
|
|
150
|
+
apiRoute: 'mutation tagsAdd / tagsRemove',
|
|
151
|
+
params: [
|
|
152
|
+
{
|
|
153
|
+
name: 'resource',
|
|
154
|
+
label: 'What to tag',
|
|
155
|
+
type: 'taggableResource',
|
|
156
|
+
required: true,
|
|
157
|
+
description: 'Shopify tags many resource types through the same mutation; this only picks which id you are passing.',
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
name: 'resourceId',
|
|
161
|
+
label: 'Resource id',
|
|
162
|
+
type: 'gid',
|
|
163
|
+
required: true,
|
|
164
|
+
description: 'The Shopify global id, e.g. gid://shopify/Order/1234. A Shopify trigger gives you this directly in the payload.',
|
|
165
|
+
placeholder: '{{payload.admin_graphql_api_id}}',
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
name: 'tags',
|
|
169
|
+
label: 'Tags',
|
|
170
|
+
type: 'tags',
|
|
171
|
+
required: true,
|
|
172
|
+
description: 'One or more tags.',
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
name: 'remove',
|
|
176
|
+
label: 'Remove instead of add',
|
|
177
|
+
type: 'boolean',
|
|
178
|
+
description: 'Off adds the tags (tagsAdd); on removes them (tagsRemove). Unlike Mailchimp, Shopify cannot do both in one call.',
|
|
179
|
+
},
|
|
180
|
+
],
|
|
181
|
+
},
|
|
182
|
+
adjustInventory: {
|
|
183
|
+
label: 'Adjust inventory',
|
|
184
|
+
description: 'Move stock by a delta at one location. For the case where the real stock lives in another system.',
|
|
185
|
+
apiRoute: 'mutation inventoryAdjustQuantities',
|
|
186
|
+
params: [
|
|
187
|
+
{
|
|
188
|
+
name: 'inventoryItemId',
|
|
189
|
+
label: 'Inventory item id',
|
|
190
|
+
type: 'gid',
|
|
191
|
+
required: true,
|
|
192
|
+
description: 'gid://shopify/InventoryItem/1234. This is the inventory item, NOT the product or the variant — a variant points at one.',
|
|
193
|
+
placeholder: '{{payload.inventory_item_id}}',
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
name: 'locationId',
|
|
197
|
+
label: 'Location',
|
|
198
|
+
type: 'location',
|
|
199
|
+
required: true,
|
|
200
|
+
description: 'Which location the stock moves at. A store with one location still has to name it.',
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
name: 'delta',
|
|
204
|
+
label: 'Change by',
|
|
205
|
+
type: 'number',
|
|
206
|
+
required: true,
|
|
207
|
+
description: 'A DELTA, not a total. 5 adds five units and -5 removes five. Sending the absolute stock level here is the mistake that silently multiplies inventory.',
|
|
208
|
+
placeholder: '{{payload.delta}}',
|
|
209
|
+
},
|
|
210
|
+
{
|
|
211
|
+
name: 'name',
|
|
212
|
+
label: 'Quantity type',
|
|
213
|
+
type: 'string',
|
|
214
|
+
required: true,
|
|
215
|
+
description: 'Which quantity is being changed. "available" is the documented value; check the reference before using another.',
|
|
216
|
+
placeholder: 'available',
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
name: 'reason',
|
|
220
|
+
label: 'Reason',
|
|
221
|
+
type: 'string',
|
|
222
|
+
required: true,
|
|
223
|
+
description: 'Why the stock moved — it lands in the store\'s inventory history. "correction" is the documented value; check the reference before using another.',
|
|
224
|
+
placeholder: 'correction',
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
name: 'referenceDocumentUri',
|
|
228
|
+
label: 'Reference',
|
|
229
|
+
type: 'string',
|
|
230
|
+
description: 'Optional audit-trail link back to whatever caused the change, so someone reading Shopify\'s inventory history can find it.',
|
|
231
|
+
},
|
|
232
|
+
],
|
|
233
|
+
},
|
|
234
|
+
findRecords: {
|
|
235
|
+
label: 'Find orders, customers or products',
|
|
236
|
+
description: 'Read back what the store knows, to enrich a payload that came from somewhere else.',
|
|
237
|
+
apiRoute: 'query orders / customers / products (with search syntax)',
|
|
238
|
+
params: [
|
|
239
|
+
{
|
|
240
|
+
name: 'resource',
|
|
241
|
+
label: 'What to look for',
|
|
242
|
+
type: 'searchableResource',
|
|
243
|
+
required: true,
|
|
244
|
+
description: 'Which collection to search.',
|
|
245
|
+
},
|
|
246
|
+
{
|
|
247
|
+
name: 'query',
|
|
248
|
+
label: 'Search',
|
|
249
|
+
type: 'string',
|
|
250
|
+
required: true,
|
|
251
|
+
description: 'Shopify search syntax, e.g. email:someone@example.com or financial_status:paid. Empty would return the newest records, which is rarely what anyone means.',
|
|
252
|
+
placeholder: 'email:{{payload.email}}',
|
|
253
|
+
},
|
|
254
|
+
{
|
|
255
|
+
name: 'first',
|
|
256
|
+
label: 'How many',
|
|
257
|
+
type: 'number',
|
|
258
|
+
description: 'Defaults to 1, which is the enrichment case. Asking for many costs more against the query-cost budget, not more calls.',
|
|
259
|
+
placeholder: '1',
|
|
260
|
+
},
|
|
261
|
+
],
|
|
262
|
+
},
|
|
263
|
+
};
|
package/dist/types.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** All valid node type identifiers */
|
|
2
|
-
export type NodeType = 'webhook' | 'scheduledWorkflow' | 'chatTrigger' | 'trigger' | 'voiceAgent' | 'filter' | 'transform' | 'schemaValidator' | 'conditional' | 'delay' | 'rateLimiter' | 'aggregator' | 'cache' | 'code' | 'ai' | 'merge' | 'approval' | 'split' | 'loop' | 'markdown' | 'fileTransform' | 'limit' | 'router' | 'emailAction' | 'gmailAction' | 'httpAction' | 'mongoAction' | 'postgresAction' | 'notificationAction' | 'sheetsAction' | 'calendarAction' | 'docsAction' | 'driveAction' | 'firecrawlAction' | 'telegramAction' | 'whatsappAction' | 'discordAction' | 'slackAction' | 'googleContactsAction' | 'googleAnalyticsAction' | 'notionAction' | 'vectorStore' | 'rssAction' | 'socialMediaAction' | 'mailchimpAction' | 'stickyNote';
|
|
2
|
+
export type NodeType = 'webhook' | 'scheduledWorkflow' | 'chatTrigger' | 'trigger' | 'voiceAgent' | 'filter' | 'transform' | 'schemaValidator' | 'conditional' | 'delay' | 'rateLimiter' | 'aggregator' | 'cache' | 'code' | 'ai' | 'merge' | 'approval' | 'split' | 'loop' | 'markdown' | 'fileTransform' | 'limit' | 'router' | 'emailAction' | 'gmailAction' | 'httpAction' | 'mongoAction' | 'postgresAction' | 'notificationAction' | 'sheetsAction' | 'calendarAction' | 'docsAction' | 'driveAction' | 'firecrawlAction' | 'telegramAction' | 'whatsappAction' | 'discordAction' | 'slackAction' | 'googleContactsAction' | 'googleAnalyticsAction' | 'notionAction' | 'vectorStore' | 'rssAction' | 'socialMediaAction' | 'mailchimpAction' | 'shopifyAction' | 'githubAction' | 'stickyNote';
|
|
3
3
|
/** Node role in the pipeline */
|
|
4
4
|
export type NodeRole = 'source' | 'processing' | 'flowControl' | 'routing' | 'action' | 'monitoring';
|
|
5
5
|
/** Handle positions on the canvas node */
|
package/dist/ui.js
CHANGED
|
@@ -76,6 +76,8 @@ exports.NODE_UI = {
|
|
|
76
76
|
whatsappAction: { fromNodes: true, toNodes: true, inputHandles: ['left'], dotHandles: { input: ['left-in'], output: ['right-out'] }, special: { loopBackAllowed: true, routerTargetLabel: 'whatsapp' } },
|
|
77
77
|
discordAction: { fromNodes: true, toNodes: true, inputHandles: ['left'], dotHandles: { input: ['left-in'], output: ['right-out'] }, special: { loopBackAllowed: true, routerTargetLabel: 'discord' } },
|
|
78
78
|
mailchimpAction: { fromNodes: true, toNodes: true, inputHandles: ['left'], dotHandles: { input: ['left-in'], output: ['right-out'] }, special: { loopBackAllowed: true, routerTargetLabel: 'mailchimp' } },
|
|
79
|
+
shopifyAction: { fromNodes: true, toNodes: true, inputHandles: ['left'], dotHandles: { input: ['left-in'], output: ['right-out'] }, special: { loopBackAllowed: true, routerTargetLabel: 'shopify' } },
|
|
80
|
+
githubAction: { fromNodes: true, toNodes: true, inputHandles: ['left'], dotHandles: { input: ['left-in'], output: ['right-out'] }, special: { loopBackAllowed: true, routerTargetLabel: 'github' } },
|
|
79
81
|
slackAction: { fromNodes: true, toNodes: true, inputHandles: ['left'], dotHandles: { input: ['left-in'], output: ['right-out'] }, special: { loopBackAllowed: true, routerTargetLabel: 'slack' } },
|
|
80
82
|
googleContactsAction: { fromNodes: true, toNodes: true, inputHandles: ['left'], dotHandles: { input: ['left-in'], output: ['right-out'] }, special: { loopBackAllowed: true } },
|
|
81
83
|
googleAnalyticsAction: { fromNodes: true, toNodes: true, inputHandles: ['left'], dotHandles: { input: ['left-in'], output: ['right-out'] }, special: { loopBackAllowed: true } },
|
|
@@ -100,6 +102,8 @@ exports.PREFIX_TO_TYPE = [
|
|
|
100
102
|
{ prefix: 'dc-', type: 'discordAction', canvasType: 'discordAction' },
|
|
101
103
|
{ prefix: 'sl-', type: 'slackAction', canvasType: 'slackAction' },
|
|
102
104
|
{ prefix: 'mc-', type: 'mailchimpAction', canvasType: 'mailchimpAction' },
|
|
105
|
+
{ prefix: 'shp-', type: 'shopifyAction', canvasType: 'shopifyAction' },
|
|
106
|
+
{ prefix: 'gh-', type: 'githubAction', canvasType: 'githubAction' },
|
|
103
107
|
{ prefix: 'gc-', type: 'googleContactsAction', canvasType: 'googleContactsAction' },
|
|
104
108
|
{ prefix: 'gaa-', type: 'googleAnalyticsAction', canvasType: 'googleAnalyticsAction' },
|
|
105
109
|
{ prefix: 'ntn-', type: 'notionAction', canvasType: 'notionAction' },
|
package/package.json
CHANGED