Compare commits

...

10 commits

Author SHA1 Message Date
Jesse Freitas
d1725d4af9 chore(release): standardize publish and update workflow
Some checks are pending
CI / build (push) Waiting to run
2026-03-16 15:27:07 -03:00
Jesse Freitas
365906a58d fix(contact): show id for label operations 2026-03-16 15:17:27 -03:00
Jesse Freitas
0bc2b4702c docs: clarify json field behavior 2026-03-16 10:11:33 -03:00
Jesse Freitas
381e361814 fix: normalize json payload fields across nodes 2026-03-16 09:14:03 -03:00
Jesse Freitas
6bb07609a3 fix: normalize optional json payload fields 2026-03-16 09:06:02 -03:00
Jesse Freitas
7cdba2e77e chore: release version 0.4.9 2026-03-12 14:25:17 -03:00
Jesse Freitas
3d120e7460 feat: add message attachment support 2026-03-12 14:12:35 -03:00
Jesse Freitas
34e8c52ceb chore: add python install helper 2026-03-12 13:59:12 -03:00
Jesse Freitas
e9eb2f7c1e chore: release version 0.4.7 2026-03-12 13:57:42 -03:00
Jesse Freitas
c3d6c7da5b feat: add combined conversation message flow 2026-03-12 13:48:10 -03:00
11 changed files with 1099 additions and 129 deletions

23
AGENTS.md Normal file
View file

@ -0,0 +1,23 @@
# AGENTS
## Publicacao e atualizacao do pacote
- Sempre que a tarefa envolver publicar ou atualizar este community node, use `python scripts/publish_and_update.py`.
- Nunca rode `npm publish` diretamente, exceto se o usuario pedir explicitamente para ignorar esse fluxo.
- Para atualizar uma instalacao local do n8n baseada em diretorio, use `--target`.
- Para atualizar uma instalacao em Docker Compose, use `--docker-service` e, quando necessario, `--docker-compose-file`.
- Se houver bloqueio externo de politica, credencial, npm, Docker ou ambiente, explique isso claramente ao usuario e mantenha o script como fluxo padrao.
## Exemplos
Publicar e atualizar um n8n em diretorio local:
```bash
python scripts/publish_and_update.py --token "<NPM_TOKEN>" --target "C:\\caminho\\do\\n8n"
```
Publicar e atualizar um n8n em Docker Compose:
```bash
python scripts/publish_and_update.py --token "<NPM_TOKEN>" --docker-service n8n --docker-compose-file "C:\\caminho\\docker-compose.yml"
```

View file

@ -17,7 +17,7 @@ Node de comunidade para [n8n](https://n8n.io) para trabalhar com a API do Mega.
- `Canned Response -> Get Many`, `Create`, `Update`, and `Delete` operations
- `Custom Filter -> Get Many`, `Create`, `Get`, `Update`, and `Delete` operations
- `Contact -> Get Many`, `Create`, `Create Note`, `Get`, `Update`, `Delete`, `Delete Note`, `Get Conversations`, `Search`, `Filter`, `Create Inbox`, `Get Contactable Inboxes`, and `Merge` operations
- `Conversation -> Get Counts`, `Get Many`, `Create`, `Filter`, `Get`, `Update`, `Toggle Status`, `Toggle Priority`, `Toggle Typing Status`, `Set Custom Attributes`, `Get Labels`, `Set Labels`, `Get Reporting Events`, and `Assign` operations
- `Conversation -> Get Counts`, `Get Many`, `Create`, `Create and Send Message`, `Filter`, `Get`, `Update`, `Toggle Status`, `Toggle Priority`, `Toggle Typing Status`, `Set Custom Attributes`, `Get Labels`, `Set Labels`, `Get Reporting Events`, and `Assign` operations
- `Custom Attribute -> Get Many`, `Create`, `Get`, `Update`, and `Delete` operations
- `Inbox -> Get Many`, `Get`, `Create`, `Update`, `Get Agent Bot`, `Set Agent Bot`, `Get Agents`, `Add Agent`, `Remove Agent`, and `Update Agents` operations
- `Integration -> Get Many`, `Create`, `Update`, and `Delete` operations
@ -57,6 +57,32 @@ Para instalar o pacote publicado no n8n:
npm install @jessefreitas/n8n-nodes-mega
```
## Publicacao e update
O fluxo padrao para publicar uma nova versao e atualizar uma instalacao do n8n deve usar:
```bash
python scripts/publish_and_update.py
```
Exemplo para publicar e atualizar um n8n local por diretorio:
```bash
python scripts/publish_and_update.py --token "<NPM_TOKEN>" --target "C:\\caminho\\do\\n8n"
```
Exemplo para publicar e atualizar um n8n em Docker Compose:
```bash
python scripts/publish_and_update.py --token "<NPM_TOKEN>" --docker-service n8n --docker-compose-file "C:\\caminho\\docker-compose.yml"
```
Se voce ja publicou a versao e quer apenas atualizar o ambiente:
```bash
python scripts/publish_and_update.py --skip-publish --version 0.4.12 --target "C:\\caminho\\do\\n8n"
```
## Credenciais
Crie uma credencial `Mega API` no n8n com:
@ -125,6 +151,22 @@ Importante:
- `Mega Client` usa identificadores publicos como `inbox_identifier`, `contact_identifier`, and `conversation_id`
- `CSAT Survey` usa uma rota publica `conversation_uuid` fora do padrao `/public/api/v1/inboxes/*`
## Campos JSON
Os campos do tipo `json` nos nodes `Mega`, `Mega Client` e `Mega Platform` esperam objetos JSON validos quando o payload do endpoint for estruturado. Exemplos de valor para o proprio campo:
```json
{"name":"Maria"}
```
ou:
```json
{"crm_id":"123"}
```
Quando o campo estiver vazio, o node envia `{}` ou omite o atributo opcional conforme o endpoint, evitando enviar strings como `"{}"` para a API.
## Operacoes
@ -866,6 +908,8 @@ Campos suportados:
- `After Message ID`
- `Before Message ID`
- `Content`
- `Attachments Source`
- `Attachment Binary Properties`
- `Message Type`
- `Private`
- `Content Type`
@ -873,6 +917,13 @@ Campos suportados:
- `Campaign ID`
- `Template Params`
`Message -> Create` supports optional multipart uploads using n8n binary properties.
- Use `Attachment Binary Properties` with a JSON array such as `["data", "audio", "pdf"]`
- Multiple attachments are supported in the same message
- Message text becomes optional when at least one attachment is provided
- Files are sent as `attachments[]` in `multipart/form-data`
### Scheduled Message
Operacoes suportadas:
@ -1000,6 +1051,7 @@ Operacoes suportadas:
- `Get Counts`
- `Get Many`
- `Create`
- `Create and Send Message`
- `Filter`
- `Get`
- `Update`
@ -1023,6 +1075,7 @@ POST /api/v1/accounts/{accountId}/conversations
POST /api/v1/accounts/{accountId}/conversations/filter
GET /api/v1/accounts/{accountId}/conversations/{id}
PATCH /api/v1/accounts/{accountId}/conversations/{id}
POST /api/v1/accounts/{accountId}/conversations/{id}/messages
POST /api/v1/accounts/{accountId}/conversations/{id}/toggle_status
POST /api/v1/accounts/{accountId}/conversations/{id}/toggle_priority
POST /api/v1/accounts/{accountId}/conversations/{id}/toggle_typing_status
@ -1050,6 +1103,10 @@ Campos suportados:
- `Contact ID`
- `Assignee ID`
- `Initial Message`
- `Message Content`
- `Message Visibility`
- `Attachments Source`
- `Attachment Binary Properties`
- `Additional Attributes`
- `Custom Attributes`
- `Priority`
@ -1059,6 +1116,21 @@ Campos suportados:
- `Typing Status`
- `Private Note`
`Create` can send an optional normal first message in the conversation creation request.
`Create and Send Message` is the combined flow:
- `Normal` sends the message in `POST /conversations`
- `Private` creates the conversation first and then sends a private note in `POST /conversations/{id}/messages`
When attachments are provided in `Create and Send Message`, the node creates the conversation first and then sends the message in a second request to `POST /conversations/{id}/messages`, for both `Normal` and `Private`.
- Use `Attachment Binary Properties` with a JSON array such as `["data", "audio", "pdf"]`
- Multiple attachments are supported in the same message
- Message text becomes optional when at least one attachment is provided
- Files are sent as `attachments[]` in `multipart/form-data`
- `Create a new message` also exposes optional advanced payload fields such as `Content Type`, `Content Attributes`, `Campaign ID`, and `Template Params`
## Validacao local
```bash

View file

@ -1,4 +1,5 @@
import type {
IBinaryData,
IDataObject,
IExecuteFunctions,
INodeExecutionData,
@ -22,7 +23,7 @@ const conversationCreateProperties: INodeProperties[] = [
displayOptions: {
show: {
resource: ['conversation'],
operation: ['create'],
operation: ['create', 'createAndSendMessage'],
},
},
},
@ -39,7 +40,7 @@ const conversationCreateProperties: INodeProperties[] = [
displayOptions: {
show: {
resource: ['conversation'],
operation: ['create'],
operation: ['create', 'createAndSendMessage'],
},
},
},
@ -56,7 +57,7 @@ const conversationCreateProperties: INodeProperties[] = [
displayOptions: {
show: {
resource: ['conversation'],
operation: ['create'],
operation: ['create', 'createAndSendMessage'],
},
},
},
@ -75,7 +76,7 @@ const conversationCreateProperties: INodeProperties[] = [
displayOptions: {
show: {
resource: ['conversation'],
operation: ['create'],
operation: ['create', 'createAndSendMessage'],
},
},
},
@ -91,7 +92,7 @@ const conversationCreateProperties: INodeProperties[] = [
displayOptions: {
show: {
resource: ['conversation'],
operation: ['create'],
operation: ['create', 'createAndSendMessage'],
},
},
},
@ -107,7 +108,7 @@ const conversationCreateProperties: INodeProperties[] = [
displayOptions: {
show: {
resource: ['conversation'],
operation: ['create'],
operation: ['create', 'createAndSendMessage'],
},
},
},
@ -133,7 +134,70 @@ const conversationCreateProperties: INodeProperties[] = [
displayOptions: {
show: {
resource: ['conversation'],
operation: ['create'],
operation: ['create', 'createAndSendMessage'],
},
},
},
];
const conversationCreateAndSendProperties: INodeProperties[] = [
{
displayName: 'Message Content',
name: 'conversationCombinedMessageContent',
type: 'string',
typeOptions: {
rows: 4,
},
default: '',
description: 'Message content to send after creating the conversation. Optional when attachments are provided.',
displayOptions: {
show: {
resource: ['conversation'],
operation: ['createAndSendMessage'],
},
},
},
{
displayName: 'Message Visibility',
name: 'conversationCombinedMessageVisibility',
type: 'options',
options: [
{ name: 'Normal', value: 'normal' },
{ name: 'Private', value: 'private' },
],
default: 'normal',
description: 'Whether to send a normal message or a private note',
displayOptions: {
show: {
resource: ['conversation'],
operation: ['createAndSendMessage'],
},
},
},
{
displayName: 'Attachments Source',
name: 'conversationCombinedAttachmentsSource',
type: 'options',
options: [{ name: 'Binary Properties', value: 'binaryProperties' }],
default: 'binaryProperties',
description: 'Where to read attachments from',
displayOptions: {
show: {
resource: ['conversation'],
operation: ['createAndSendMessage'],
},
},
},
{
displayName: 'Attachment Binary Properties',
name: 'conversationCombinedAttachmentBinaryProperties',
type: 'json',
default: '[]',
description: 'JSON array with binary property names, for example ["data", "audio", "pdf"]',
displayOptions: {
show: {
resource: ['conversation'],
operation: ['createAndSendMessage'],
},
},
},
@ -1709,6 +1773,7 @@ const contactIdProperty: INodeProperties = {
show: {
resource: ['contact'],
operation: [
'addLabels',
'createNote',
'createInbox',
'delete',
@ -1716,6 +1781,9 @@ const contactIdProperty: INodeProperties = {
'get',
'getContactableInboxes',
'getConversations',
'getLabels',
'removeLabels',
'setLabels',
'update',
],
},
@ -3108,8 +3176,34 @@ const messageCreateProperties: INodeProperties[] = [
rows: 4,
},
default: '',
required: true,
description: 'Content of the message',
description: 'Content of the message. Optional when attachments are provided.',
displayOptions: {
show: {
resource: ['message'],
operation: ['create'],
},
},
},
{
displayName: 'Attachments Source',
name: 'messageCreateAttachmentsSource',
type: 'options',
options: [{ name: 'Binary Properties', value: 'binaryProperties' }],
default: 'binaryProperties',
description: 'Where to read attachments from',
displayOptions: {
show: {
resource: ['message'],
operation: ['create'],
},
},
},
{
displayName: 'Attachment Binary Properties',
name: 'messageCreateAttachmentBinaryProperties',
type: 'json',
default: '[]',
description: 'JSON array with binary property names, for example ["data", "audio", "pdf"]',
displayOptions: {
show: {
resource: ['message'],
@ -4482,6 +4576,12 @@ export class Mega implements INodeType {
description: 'Create a conversation',
action: 'Create a conversation',
},
{
name: 'Create and Send Message',
value: 'createAndSendMessage',
description: 'Create a conversation and send a message',
action: 'Create a conversation and send a message',
},
{
name: 'Filter',
value: 'filter',
@ -4913,6 +5013,7 @@ export class Mega implements INodeType {
...conversationListProperties,
conversationFilterProperty,
...conversationCreateProperties,
...conversationCreateAndSendProperties,
...conversationUpdateProperties,
...conversationToggleStatusProperties,
conversationTogglePriorityProperty,
@ -4929,6 +5030,270 @@ export class Mega implements INodeType {
const returnData: INodeExecutionData[] = [];
const credentials = await this.getCredentials('megaApi');
const accountId = credentials.accountId as string;
const runtimeGlobals = globalThis as typeof globalThis & {
Blob?: new (parts?: Array<ArrayBuffer | ArrayBufferView | string>, options?: { type?: string }) => unknown;
FormData?: new () => { append(name: string, value: unknown, fileName?: string): void };
};
const createRuntimeFormData = (): {
append(name: string, value: unknown, fileName?: string): void;
} => {
if (typeof runtimeGlobals.FormData === 'undefined') {
throw new NodeOperationError(this.getNode(), 'FormData is not available in this runtime');
}
return new runtimeGlobals.FormData();
};
const createRuntimeBlob = (buffer: Uint8Array, mimeType: string): unknown => {
if (typeof runtimeGlobals.Blob === 'undefined') {
throw new NodeOperationError(this.getNode(), 'Blob is not available in this runtime');
}
return new runtimeGlobals.Blob([buffer], { type: mimeType });
};
const parseAttachmentPropertyNames = (
itemIndex: number,
parameterName: string,
): string[] => {
const rawValue = this.getNodeParameter(parameterName, itemIndex, '[]') as string | string[];
let parsedValue: unknown;
try {
parsedValue =
typeof rawValue === 'string'
? (JSON.parse(rawValue || '[]') as unknown)
: (rawValue as unknown);
} catch {
throw new NodeOperationError(
this.getNode(),
`${parameterName} must be a valid JSON array of strings`,
{ itemIndex },
);
}
if (!Array.isArray(parsedValue)) {
throw new NodeOperationError(
this.getNode(),
`${parameterName} must be a JSON array of strings`,
{ itemIndex },
);
}
return parsedValue.map((value, index) => {
if (typeof value !== 'string' || !value.trim()) {
throw new NodeOperationError(
this.getNode(),
`${parameterName}[${index}] must be a non-empty string`,
{ itemIndex },
);
}
return value.trim();
});
};
const parseOptionalObject = (
value: unknown,
itemIndex: number,
parameterName: string,
): IDataObject => {
if (value === undefined || value === null || value === '') {
return {};
}
let parsedValue = value;
if (typeof parsedValue === 'string') {
try {
parsedValue = JSON.parse(parsedValue);
} catch {
throw new NodeOperationError(
this.getNode(),
`${parameterName} must be a valid JSON object`,
{ itemIndex },
);
}
}
if (typeof parsedValue !== 'object' || Array.isArray(parsedValue)) {
throw new NodeOperationError(
this.getNode(),
`${parameterName} must be a JSON object`,
{ itemIndex },
);
}
return parsedValue as IDataObject;
};
const appendFormValue = (
formData: { append(name: string, value: unknown, fileName?: string): void },
key: string,
value: unknown,
): void => {
if (value === undefined || value === null) {
return;
}
if (typeof value === 'object') {
formData.append(key, JSON.stringify(value));
return;
}
formData.append(key, String(value));
};
const buildMultipartFormData = async (
itemIndex: number,
fields: Record<string, unknown>,
attachmentPropertyNames: string[],
): Promise<{ append(name: string, value: unknown, fileName?: string): void }> => {
const formData = createRuntimeFormData();
for (const [key, value] of Object.entries(fields)) {
appendFormValue(formData, key, value);
}
for (const propertyName of attachmentPropertyNames) {
const binaryData = this.helpers.assertBinaryData(itemIndex, propertyName) as IBinaryData;
const buffer = await this.helpers.getBinaryDataBuffer(itemIndex, propertyName);
const mimeType = binaryData.mimeType || 'application/octet-stream';
const fileName = binaryData.fileName || propertyName;
formData.append('attachments[]', createRuntimeBlob(buffer, mimeType), fileName);
}
return formData;
};
const createMessagePayload = (
itemIndex: number,
content: string,
privateMessage: boolean,
): IDataObject => {
const body: IDataObject = {
message_type: this.getNodeParameter('messageCreateType', itemIndex, 'outgoing') as string,
private: privateMessage,
content_type: this.getNodeParameter('messageCreateContentType', itemIndex, 'text') as string,
};
const trimmedContent = content.trim();
const contentAttributes = this.getNodeParameter(
'messageCreateContentAttributes',
itemIndex,
{},
);
const templateParams = this.getNodeParameter('messageCreateTemplateParams', itemIndex, {});
const campaignId = this.getNodeParameter('messageCreateCampaignId', itemIndex, 0) as number;
const parsedContentAttributes = parseOptionalObject(
contentAttributes,
itemIndex,
'messageCreateContentAttributes',
);
const parsedTemplateParams = parseOptionalObject(
templateParams,
itemIndex,
'messageCreateTemplateParams',
);
if (trimmedContent) {
body.content = trimmedContent;
}
if (Object.keys(parsedContentAttributes).length > 0) {
body.content_attributes = parsedContentAttributes;
}
if (Object.keys(parsedTemplateParams).length > 0) {
body.template_params = parsedTemplateParams;
}
if (campaignId > 0) {
body.campaign_id = campaignId;
}
return body;
};
const sendConversationMessage = async (
itemIndex: number,
conversationId: number,
content: string,
privateMessage: boolean,
attachmentPropertyNames: string[],
messageOverrides?: Partial<IDataObject>,
): Promise<IDataObject> => {
const basePayload: IDataObject = {
content_type: 'text',
message_type: 'outgoing',
private: privateMessage,
...(messageOverrides ?? {}),
};
const trimmedContent = content.trim();
if (trimmedContent) {
basePayload.content = trimmedContent;
}
if (attachmentPropertyNames.length === 0) {
return (await megaApiRequest.call(
this,
'POST',
`/api/v1/accounts/${accountId}/conversations/${conversationId}/messages`,
basePayload,
)) as IDataObject;
}
const formData = await buildMultipartFormData(itemIndex, basePayload, attachmentPropertyNames);
return (await megaApiRequest.call(
this,
'POST',
`/api/v1/accounts/${accountId}/conversations/${conversationId}/messages`,
formData,
)) as IDataObject;
};
const buildConversationCreateBody = (itemIndex: number, messageContent = ''): IDataObject => {
const body: IDataObject = {
source_id: this.getNodeParameter('sourceId', itemIndex) as string,
inbox_id: this.getNodeParameter('inboxId', itemIndex) as number,
contact_id: this.getNodeParameter('contactId', itemIndex) as number,
status: this.getNodeParameter('status', itemIndex) as string,
};
const assigneeId = this.getNodeParameter('assigneeId', itemIndex) as number;
const additionalAttributes = this.getNodeParameter(
'additionalAttributes',
itemIndex,
{},
);
const customAttributes = this.getNodeParameter('customAttributes', itemIndex, {});
const parsedAdditionalAttributes = parseOptionalObject(
additionalAttributes,
itemIndex,
'additionalAttributes',
);
const parsedCustomAttributes = parseOptionalObject(
customAttributes,
itemIndex,
'customAttributes',
);
if (assigneeId > 0) {
body.assignee_id = assigneeId;
}
if (messageContent.trim()) {
body.message = {
content: messageContent,
};
}
if (Object.keys(parsedAdditionalAttributes).length > 0) {
body.additional_attributes = parsedAdditionalAttributes;
}
if (Object.keys(parsedCustomAttributes).length > 0) {
body.custom_attributes = parsedCustomAttributes;
}
return body;
};
for (let itemIndex = 0; itemIndex < items.length; itemIndex++) {
try {
@ -5401,44 +5766,47 @@ export class Mega implements INodeType {
)) as IDataObject;
} else if (resource === 'message' && operation === 'create') {
const conversationId = this.getNodeParameter('messageConversationId', itemIndex) as number;
const body: IDataObject = {
content: this.getNodeParameter('messageCreateContent', itemIndex) as string,
message_type: this.getNodeParameter('messageCreateType', itemIndex, 'outgoing') as string,
private: this.getNodeParameter('messageCreatePrivate', itemIndex, false) as boolean,
content_type: this.getNodeParameter(
'messageCreateContentType',
const messageContent = this.getNodeParameter('messageCreateContent', itemIndex, '') as string;
const privateMessage = this.getNodeParameter(
'messageCreatePrivate',
itemIndex,
false,
) as boolean;
const attachmentPropertyNames = parseAttachmentPropertyNames(
itemIndex,
'messageCreateAttachmentBinaryProperties',
);
if (!messageContent.trim() && attachmentPropertyNames.length === 0) {
throw new NodeOperationError(
this.getNode(),
'Content must be provided when no attachments are sent',
{ itemIndex },
);
}
const body = createMessagePayload(itemIndex, messageContent, privateMessage);
if (attachmentPropertyNames.length === 0) {
response = (await megaApiRequest.call(
this,
'POST',
`/api/v1/accounts/${accountId}/conversations/${conversationId}/messages`,
body,
)) as IDataObject;
} else {
const formData = await buildMultipartFormData(
itemIndex,
'text',
) as string,
};
const contentAttributes = this.getNodeParameter(
'messageCreateContentAttributes',
itemIndex,
{},
) as IDataObject;
const templateParams = this.getNodeParameter(
'messageCreateTemplateParams',
itemIndex,
{},
) as IDataObject;
const campaignId = this.getNodeParameter('messageCreateCampaignId', itemIndex, 0) as number;
if (Object.keys(contentAttributes).length > 0) {
body.content_attributes = contentAttributes;
body as Record<string, unknown>,
attachmentPropertyNames,
);
response = (await megaApiRequest.call(
this,
'POST',
`/api/v1/accounts/${accountId}/conversations/${conversationId}/messages`,
formData,
)) as IDataObject;
}
if (Object.keys(templateParams).length > 0) {
body.template_params = templateParams;
}
if (campaignId > 0) {
body.campaign_id = campaignId;
}
response = (await megaApiRequest.call(
this,
'POST',
`/api/v1/accounts/${accountId}/conversations/${conversationId}/messages`,
body,
)) as IDataObject;
} else if (resource === 'message' && operation === 'delete') {
const conversationId = this.getNodeParameter('messageConversationId', itemIndex) as number;
const messageId = this.getNodeParameter('messageId', itemIndex) as number;
@ -5537,7 +5905,12 @@ export class Mega implements INodeType {
'scheduledMessageTemplateParams',
itemIndex,
{},
) as IDataObject;
);
const parsedTemplateParams = parseOptionalObject(
templateParams,
itemIndex,
'scheduledMessageTemplateParams',
);
const recurrenceType = this.getNodeParameter(
'scheduledMessageRecurrenceType',
itemIndex,
@ -5550,8 +5923,8 @@ export class Mega implements INodeType {
if (title.trim()) {
body.title = title;
}
if (Object.keys(templateParams).length > 0) {
body.template_params = templateParams;
if (Object.keys(parsedTemplateParams).length > 0) {
body.template_params = parsedTemplateParams;
}
if (recurrenceType !== 'none') {
@ -5676,7 +6049,11 @@ export class Mega implements INodeType {
body.scheduled_at = updateFields.scheduledAt;
}
if (updateFields.templateParams !== undefined) {
body.template_params = updateFields.templateParams;
body.template_params = parseOptionalObject(
updateFields.templateParams,
itemIndex,
'scheduledMessageUpdateFields.values.templateParams',
);
}
if (updateFields.title !== undefined && updateFields.title !== '') {
body.title = updateFields.title;
@ -5828,23 +6205,33 @@ export class Mega implements INodeType {
'contactAdditionalAttributes',
itemIndex,
{},
) as IDataObject;
);
const customAttributes = this.getNodeParameter(
'contactCustomAttributes',
itemIndex,
{},
) as IDataObject;
);
const parsedAdditionalAttributes = parseOptionalObject(
additionalAttributes,
itemIndex,
'contactAdditionalAttributes',
);
const parsedCustomAttributes = parseOptionalObject(
customAttributes,
itemIndex,
'contactCustomAttributes',
);
if (name.trim()) body.name = name;
if (email.trim()) body.email = email;
if (phoneNumber.trim()) body.phone_number = phoneNumber;
if (avatarUrl.trim()) body.avatar_url = avatarUrl;
if (identifier.trim()) body.identifier = identifier;
if (Object.keys(additionalAttributes).length > 0) {
body.additional_attributes = additionalAttributes;
if (Object.keys(parsedAdditionalAttributes).length > 0) {
body.additional_attributes = parsedAdditionalAttributes;
}
if (Object.keys(customAttributes).length > 0) {
body.custom_attributes = customAttributes;
if (Object.keys(parsedCustomAttributes).length > 0) {
body.custom_attributes = parsedCustomAttributes;
}
response = (await megaApiRequest.call(
@ -5870,7 +6257,11 @@ export class Mega implements INodeType {
const body: IDataObject = {};
if (updateFields.additionalAttributes !== undefined) {
body.additional_attributes = updateFields.additionalAttributes;
body.additional_attributes = parseOptionalObject(
updateFields.additionalAttributes,
itemIndex,
'contactUpdateFields.values.additionalAttributes',
);
}
if (updateFields.avatarUrl !== undefined && updateFields.avatarUrl !== '') {
body.avatar_url = updateFields.avatarUrl;
@ -5879,7 +6270,11 @@ export class Mega implements INodeType {
body.blocked = updateFields.blocked;
}
if (updateFields.customAttributes !== undefined) {
body.custom_attributes = updateFields.customAttributes;
body.custom_attributes = parseOptionalObject(
updateFields.customAttributes,
itemIndex,
'contactUpdateFields.values.customAttributes',
);
}
if (updateFields.email !== undefined && updateFields.email !== '') {
body.email = updateFields.email;
@ -6149,10 +6544,15 @@ export class Mega implements INodeType {
'campaignTemplateParams',
itemIndex,
{},
) as IDataObject;
);
const parsedTemplateParams = parseOptionalObject(
templateParams,
itemIndex,
'campaignTemplateParams',
);
if (Object.keys(templateParams).length > 0) {
body.template_params = templateParams;
if (Object.keys(parsedTemplateParams).length > 0) {
body.template_params = parsedTemplateParams;
}
response = (await megaApiRequest.call(
@ -6315,14 +6715,19 @@ export class Mega implements INodeType {
'chatRoomMessageContentAttributes',
itemIndex,
{},
) as IDataObject;
);
const parsedContentAttributes = parseOptionalObject(
contentAttributes,
itemIndex,
'chatRoomMessageContentAttributes',
);
if (echoId.trim()) {
chatRoomMessage.echo_id = echoId;
}
if (Object.keys(contentAttributes).length > 0) {
chatRoomMessage.content_attributes = contentAttributes;
if (Object.keys(parsedContentAttributes).length > 0) {
chatRoomMessage.content_attributes = parsedContentAttributes;
}
response = (await megaApiRequest.call(
@ -6806,43 +7211,8 @@ export class Mega implements INodeType {
`/api/v1/accounts/${accountId}/conversations/${conversationId}`,
)) as IDataObject;
} else if (resource === 'conversation' && operation === 'create') {
const body: IDataObject = {
source_id: this.getNodeParameter('sourceId', itemIndex) as string,
inbox_id: this.getNodeParameter('inboxId', itemIndex) as number,
contact_id: this.getNodeParameter('contactId', itemIndex) as number,
status: this.getNodeParameter('status', itemIndex) as string,
};
const assigneeId = this.getNodeParameter('assigneeId', itemIndex) as number;
const messageContent = this.getNodeParameter('messageContent', itemIndex) as string;
const additionalAttributes = this.getNodeParameter(
'additionalAttributes',
itemIndex,
{},
) as IDataObject;
const customAttributes = this.getNodeParameter(
'customAttributes',
itemIndex,
{},
) as IDataObject;
if (assigneeId > 0) {
body.assignee_id = assigneeId;
}
if (messageContent.trim()) {
body.message = {
content: messageContent,
};
}
if (Object.keys(additionalAttributes).length > 0) {
body.additional_attributes = additionalAttributes;
}
if (Object.keys(customAttributes).length > 0) {
body.custom_attributes = customAttributes;
}
const body = buildConversationCreateBody(itemIndex, messageContent);
response = (await megaApiRequest.call(
this,
@ -6850,6 +7220,72 @@ export class Mega implements INodeType {
`/api/v1/accounts/${accountId}/conversations`,
body,
)) as IDataObject;
} else if (resource === 'conversation' && operation === 'createAndSendMessage') {
const messageContent = this.getNodeParameter(
'conversationCombinedMessageContent',
itemIndex,
'',
) as string;
const messageVisibility = this.getNodeParameter(
'conversationCombinedMessageVisibility',
itemIndex,
'normal',
) as string;
const attachmentPropertyNames = parseAttachmentPropertyNames(
itemIndex,
'conversationCombinedAttachmentBinaryProperties',
);
if (!messageContent.trim() && attachmentPropertyNames.length === 0) {
throw new NodeOperationError(
this.getNode(),
'Message Content must be provided when no attachments are sent',
{ itemIndex },
);
}
if (messageVisibility === 'normal' && attachmentPropertyNames.length === 0) {
const body = buildConversationCreateBody(itemIndex, messageContent);
response = (await megaApiRequest.call(
this,
'POST',
`/api/v1/accounts/${accountId}/conversations`,
body,
)) as IDataObject;
} else {
const conversationResponse = (await megaApiRequest.call(
this,
'POST',
`/api/v1/accounts/${accountId}/conversations`,
buildConversationCreateBody(itemIndex),
)) as IDataObject;
const conversationId = Number(conversationResponse.id ?? 0);
if (conversationId <= 0) {
throw new Error(
'Conversation was created but no conversation ID was returned for the private message step',
);
}
try {
const messageResponse = await sendConversationMessage(
itemIndex,
conversationId,
messageContent,
messageVisibility === 'private',
attachmentPropertyNames,
);
response = {
conversation: conversationResponse,
message: messageResponse,
};
} catch (error) {
throw new Error(
`Conversation ${conversationId} was created but sending the ${messageVisibility} message failed: ${(error as Error).message}`,
);
}
}
} else if (resource === 'conversation' && operation === 'update') {
const conversationId = this.getNodeParameter('conversationId', itemIndex) as number;
const body: IDataObject = {
@ -6923,13 +7359,18 @@ export class Mega implements INodeType {
'conversationCustomAttributesPayload',
itemIndex,
{},
) as IDataObject;
);
const parsedCustomAttributes = parseOptionalObject(
customAttributes,
itemIndex,
'conversationCustomAttributesPayload',
);
response = (await megaApiRequest.call(
this,
'POST',
`/api/v1/accounts/${accountId}/conversations/${conversationId}/custom_attributes`,
{
custom_attributes: customAttributes,
custom_attributes: parsedCustomAttributes,
},
)) as IDataObject;
} else if (resource === 'conversation' && operation === 'getLabels') {

View file

@ -22,8 +22,9 @@ export async function megaApiRequest(
this: RequestContext,
method: IHttpRequestMethods,
route: string,
body?: IDataObject,
body?: IHttpRequestOptions['body'],
qs?: IDataObject,
requestOptions?: Partial<IHttpRequestOptions>,
) {
const credentials = await this.getCredentials('megaApi');
const baseUrl = normalizeBaseUrl(credentials.baseUrl as string);
@ -31,10 +32,19 @@ export async function megaApiRequest(
const options: IHttpRequestOptions = {
method,
url: `${baseUrl}${route}`,
body,
qs,
json: true,
...(body !== undefined ? { body } : {}),
...(qs !== undefined ? { qs } : {}),
...requestOptions,
};
if (options.json === undefined) {
options.json = !(
options.body &&
typeof options.body === 'object' &&
'append' in options.body &&
options.body.constructor?.name === 'FormData'
);
}
return this.helpers.httpRequestWithAuthentication.call(this, 'megaApi', options);
}

View file

@ -424,6 +424,38 @@ export class MegaClient implements INodeType {
const returnData: INodeExecutionData[] = [];
const credentials = await this.getCredentials('megaClientApi');
const inboxIdentifier = credentials.inboxIdentifier as string;
const parseOptionalObject = (
value: unknown,
itemIndex: number,
parameterName: string,
): IDataObject => {
if (value === undefined || value === null || value === '') {
return {};
}
let parsedValue = value;
if (typeof parsedValue === 'string') {
try {
parsedValue = JSON.parse(parsedValue);
} catch {
throw new NodeOperationError(
this.getNode(),
`${parameterName} must be a valid JSON object`,
{ itemIndex },
);
}
}
if (typeof parsedValue !== 'object' || Array.isArray(parsedValue)) {
throw new NodeOperationError(
this.getNode(),
`${parameterName} must be a JSON object`,
{ itemIndex },
);
}
return parsedValue as IDataObject;
};
for (let itemIndex = 0; itemIndex < items.length; itemIndex++) {
try {
@ -442,14 +474,19 @@ export class MegaClient implements INodeType {
'clientContactCustomAttributes',
itemIndex,
{},
) as IDataObject;
);
const parsedCustomAttributes = parseOptionalObject(
customAttributes,
itemIndex,
'clientContactCustomAttributes',
);
if (identifier.trim()) body.identifier = identifier;
if (identifierHash.trim()) body.identifier_hash = identifierHash;
if (email.trim()) body.email = email;
if (name.trim()) body.name = name;
if (phoneNumber.trim()) body.phone_number = phoneNumber;
if (Object.keys(customAttributes).length > 0) body.custom_attributes = customAttributes;
if (Object.keys(parsedCustomAttributes).length > 0) body.custom_attributes = parsedCustomAttributes;
response = (await megaClientApiRequest.call(
this,
@ -476,14 +513,19 @@ export class MegaClient implements INodeType {
'clientContactCustomAttributes',
itemIndex,
{},
) as IDataObject;
);
const parsedCustomAttributes = parseOptionalObject(
customAttributes,
itemIndex,
'clientContactCustomAttributes',
);
if (identifier.trim()) body.identifier = identifier;
if (identifierHash.trim()) body.identifier_hash = identifierHash;
if (email.trim()) body.email = email;
if (name.trim()) body.name = name;
if (phoneNumber.trim()) body.phone_number = phoneNumber;
if (Object.keys(customAttributes).length > 0) body.custom_attributes = customAttributes;
if (Object.keys(parsedCustomAttributes).length > 0) body.custom_attributes = parsedCustomAttributes;
response = (await megaClientApiRequest.call(
this,
@ -504,12 +546,17 @@ export class MegaClient implements INodeType {
'clientConversationCustomAttributes',
itemIndex,
{},
) as IDataObject;
);
const parsedCustomAttributes = parseOptionalObject(
customAttributes,
itemIndex,
'clientConversationCustomAttributes',
);
response = (await megaClientApiRequest.call(
this,
'POST',
`/public/api/v1/inboxes/${inboxIdentifier}/contacts/${contactIdentifier}/conversations`,
{ custom_attributes: customAttributes },
{ custom_attributes: parsedCustomAttributes },
)) as IDataObject;
} else if (resource === 'conversation' && operation === 'get') {
const contactIdentifier = this.getNodeParameter('clientContactIdentifier', itemIndex) as string;

View file

@ -791,6 +791,38 @@ export class MegaPlatform implements INodeType {
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const items = this.getInputData();
const returnData: INodeExecutionData[] = [];
const parseOptionalObject = (
value: unknown,
itemIndex: number,
parameterName: string,
): IDataObject => {
if (value === undefined || value === null || value === '') {
return {};
}
let parsedValue = value;
if (typeof parsedValue === 'string') {
try {
parsedValue = JSON.parse(parsedValue);
} catch {
throw new NodeOperationError(
this.getNode(),
`${parameterName} must be a valid JSON object`,
{ itemIndex },
);
}
}
if (typeof parsedValue !== 'object' || Array.isArray(parsedValue)) {
throw new NodeOperationError(
this.getNode(),
`${parameterName} must be a JSON object`,
{ itemIndex },
);
}
return parsedValue as IDataObject;
};
for (let itemIndex = 0; itemIndex < items.length; itemIndex++) {
try {
@ -799,6 +831,16 @@ export class MegaPlatform implements INodeType {
let response: IDataObject;
if (resource === 'account' && operation === 'create') {
const limits = parseOptionalObject(
this.getNodeParameter('platformAccountLimits', itemIndex, {}),
itemIndex,
'platformAccountLimits',
);
const customAttributes = parseOptionalObject(
this.getNodeParameter('platformAccountCustomAttributes', itemIndex, {}),
itemIndex,
'platformAccountCustomAttributes',
);
response = (await megaPlatformApiRequest.call(this, 'POST', '/platform/api/v1/accounts', {
name: this.getNodeParameter('platformAccountName', itemIndex) as string,
locale: this.getNodeParameter('platformAccountLocale', itemIndex, 'en') as string,
@ -809,12 +851,8 @@ export class MegaPlatform implements INodeType {
'',
) as string,
status: this.getNodeParameter('platformAccountStatus', itemIndex, 'active') as string,
limits: this.getNodeParameter('platformAccountLimits', itemIndex, {}) as IDataObject,
custom_attributes: this.getNodeParameter(
'platformAccountCustomAttributes',
itemIndex,
{},
) as IDataObject,
limits,
custom_attributes: customAttributes,
})) as IDataObject;
} else if (resource === 'account' && operation === 'get') {
const accountId = this.getNodeParameter('platformAccountId', itemIndex) as number;
@ -832,9 +870,21 @@ export class MegaPlatform implements INodeType {
) as IDataObject;
const body: IDataObject = {};
if (updateFields.customAttributes !== undefined) body.custom_attributes = updateFields.customAttributes;
if (updateFields.customAttributes !== undefined) {
body.custom_attributes = parseOptionalObject(
updateFields.customAttributes,
itemIndex,
'platformAccountUpdateFields.values.customAttributes',
);
}
if (updateFields.domain !== undefined) body.domain = updateFields.domain;
if (updateFields.limits !== undefined) body.limits = updateFields.limits;
if (updateFields.limits !== undefined) {
body.limits = parseOptionalObject(
updateFields.limits,
itemIndex,
'platformAccountUpdateFields.values.limits',
);
}
if (updateFields.locale !== undefined) body.locale = updateFields.locale;
if (updateFields.name !== undefined) body.name = updateFields.name;
if (updateFields.status !== undefined) body.status = updateFields.status;
@ -946,16 +996,17 @@ export class MegaPlatform implements INodeType {
);
response = { success: true, id: agentBotId };
} else if (resource === 'user' && operation === 'create') {
const customAttributes = parseOptionalObject(
this.getNodeParameter('platformUserCustomAttributes', itemIndex, {}),
itemIndex,
'platformUserCustomAttributes',
);
response = (await megaPlatformApiRequest.call(this, 'POST', '/platform/api/v1/users', {
name: this.getNodeParameter('platformUserName', itemIndex) as string,
display_name: this.getNodeParameter('platformUserDisplayName', itemIndex, '') as string,
email: this.getNodeParameter('platformUserEmail', itemIndex) as string,
password: this.getNodeParameter('platformUserPassword', itemIndex) as string,
custom_attributes: this.getNodeParameter(
'platformUserCustomAttributes',
itemIndex,
{},
) as IDataObject,
custom_attributes: customAttributes,
})) as IDataObject;
} else if (resource === 'user' && operation === 'get') {
const userId = this.getNodeParameter('platformUserId', itemIndex) as number;
@ -969,7 +1020,13 @@ export class MegaPlatform implements INodeType {
const updateFields = this.getNodeParameter('platformUserUpdateFields.values', itemIndex, {}) as IDataObject;
const body: IDataObject = {};
if (updateFields.customAttributes !== undefined) body.custom_attributes = updateFields.customAttributes;
if (updateFields.customAttributes !== undefined) {
body.custom_attributes = parseOptionalObject(
updateFields.customAttributes,
itemIndex,
'platformUserUpdateFields.values.customAttributes',
);
}
if (updateFields.displayName !== undefined) body.display_name = updateFields.displayName;
if (updateFields.email !== undefined) body.email = updateFields.email;
if (updateFields.name !== undefined) body.name = updateFields.name;

4
package-lock.json generated
View file

@ -1,12 +1,12 @@
{
"name": "@jessefreitas/n8n-nodes-mega",
"version": "0.4.5",
"version": "0.4.12",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@jessefreitas/n8n-nodes-mega",
"version": "0.4.5",
"version": "0.4.12",
"license": "MIT",
"devDependencies": {
"@n8n/node-cli": "0.23.0",

View file

@ -1,6 +1,6 @@
{
"name": "@jessefreitas/n8n-nodes-mega",
"version": "0.4.5",
"version": "0.4.12",
"description": "Trabalhe com a API do Mega",
"license": "MIT",
"homepage": "https://github.com/jessefreitas/n8n_community_mega",

View file

@ -0,0 +1,51 @@
import argparse
import shutil
import subprocess
import sys
from pathlib import Path
PACKAGE_NAME = "@jessefreitas/n8n-nodes-mega"
DEFAULT_VERSION = "0.4.7"
def resolve_npm() -> str:
if sys.platform == "win32":
return shutil.which("npm.cmd") or shutil.which("npm") or "npm"
return shutil.which("npm") or "npm"
def main() -> int:
parser = argparse.ArgumentParser(
description="Install the published Mega n8n community package using npm.",
)
parser.add_argument(
"--version",
default=DEFAULT_VERSION,
help=f"Package version to install. Defaults to {DEFAULT_VERSION}.",
)
parser.add_argument(
"--target",
default=".",
help="Directory where npm install should run. Defaults to the current directory.",
)
args = parser.parse_args()
target = Path(args.target).resolve()
if not target.exists():
print(f"Target directory does not exist: {target}", file=sys.stderr)
return 2
npm = resolve_npm()
package_spec = f"{PACKAGE_NAME}@{args.version}"
result = subprocess.run(
[npm, "install", package_spec],
cwd=target,
check=False,
)
return result.returncode
if __name__ == "__main__":
raise SystemExit(main())

View file

@ -0,0 +1,179 @@
import argparse
import getpass
import os
import shutil
import subprocess
import sys
import tempfile
from pathlib import Path
PACKAGE_NAME = "@jessefreitas/n8n-nodes-mega"
def resolve_npm() -> str:
if sys.platform == "win32":
return shutil.which("npm.cmd") or shutil.which("npm") or "npm"
return shutil.which("npm") or "npm"
def run(command: list[str], cwd: Path, env: dict[str, str] | None = None) -> None:
result = subprocess.run(command, cwd=cwd, env=env, check=False)
if result.returncode != 0:
raise SystemExit(result.returncode)
def build_temp_npmrc(token: str) -> tuple[Path, Path]:
temp_dir = Path(tempfile.mkdtemp(prefix="npm-publish-"))
npmrc = temp_dir / ".npmrc"
npmrc.write_text(
f"//registry.npmjs.org/:_authToken={token}\nregistry=https://registry.npmjs.org/\n",
encoding="ascii",
)
return temp_dir, npmrc
def read_package_version(repo_root: Path) -> str:
package_json = repo_root / "package.json"
content = package_json.read_text(encoding="utf-8")
marker = '"version": "'
start = content.find(marker)
if start == -1:
raise RuntimeError(f"Could not find version in {package_json}")
start += len(marker)
end = content.find('"', start)
if end == -1:
raise RuntimeError(f"Could not parse version in {package_json}")
return content[start:end]
def publish_package(repo_root: Path, token: str, skip_whoami: bool) -> str:
npm = resolve_npm()
version = read_package_version(repo_root)
temp_dir, npmrc = build_temp_npmrc(token)
env = os.environ.copy()
env["NPM_CONFIG_USERCONFIG"] = str(npmrc)
try:
if not skip_whoami:
run([npm, "whoami"], cwd=repo_root, env=env)
run(
[npm, "publish", "--access", "public", "--ignore-scripts"],
cwd=repo_root,
env=env,
)
run([npm, "view", PACKAGE_NAME, "version"], cwd=repo_root, env=env)
finally:
shutil.rmtree(temp_dir, ignore_errors=True)
return version
def update_directory(target: Path, version: str) -> None:
if not target.exists():
raise RuntimeError(f"Target directory does not exist: {target}")
npm = resolve_npm()
run([npm, "install", f"{PACKAGE_NAME}@{version}"], cwd=target)
def update_docker(service: str, version: str, compose_file: Path | None, project_dir: Path) -> None:
docker = shutil.which("docker")
if docker is None:
raise RuntimeError("Docker is not available in PATH")
command = [docker, "compose"]
if compose_file is not None:
command.extend(["-f", str(compose_file)])
command.extend(["exec", "-T", service, "npm", "install", f"{PACKAGE_NAME}@{version}"])
run(command, cwd=project_dir)
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Publish the Mega n8n package and update a target n8n installation.",
)
parser.add_argument(
"--token",
default=os.environ.get("NPM_TOKEN", "").strip(),
help="Granular npm token with publish permission. Defaults to NPM_TOKEN.",
)
parser.add_argument(
"--skip-publish",
action="store_true",
help="Skip npm publish and only run the update step.",
)
parser.add_argument(
"--version",
default="",
help="Version to install on the target. Defaults to package.json version.",
)
parser.add_argument(
"--skip-whoami",
action="store_true",
help="Skip npm whoami before publish.",
)
parser.add_argument(
"--target",
default="",
help="Directory where npm install should run for the n8n update step.",
)
parser.add_argument(
"--docker-service",
default="",
help="Docker Compose service name to update with npm install inside the container.",
)
parser.add_argument(
"--docker-compose-file",
default="",
help="Optional docker-compose file to use with --docker-service.",
)
parser.add_argument(
"--docker-project-dir",
default="",
help="Working directory for docker compose. Defaults to the compose file directory or current directory.",
)
return parser.parse_args()
def main() -> int:
args = parse_args()
repo_root = Path(__file__).resolve().parent.parent
version = args.version.strip() or read_package_version(repo_root)
if not args.skip_publish:
token = args.token
if not token:
token = getpass.getpass("NPM token: ").strip()
if not token:
print("Missing npm token. Pass --token, set NPM_TOKEN, or enter it when prompted.", file=sys.stderr)
return 2
version = publish_package(repo_root, token, args.skip_whoami)
if args.target.strip():
update_directory(Path(args.target).resolve(), version)
if args.docker_service.strip():
compose_file = Path(args.docker_compose_file).resolve() if args.docker_compose_file.strip() else None
if args.docker_project_dir.strip():
project_dir = Path(args.docker_project_dir).resolve()
elif compose_file is not None:
project_dir = compose_file.parent
else:
project_dir = Path.cwd()
update_docker(args.docker_service.strip(), version, compose_file, project_dir)
if not args.target.strip() and not args.docker_service.strip():
print("Publish completed. No update target was provided.")
else:
print(f"Completed publish/update flow for {PACKAGE_NAME}@{version}")
return 0
if __name__ == "__main__":
raise SystemExit(main())

90
scripts/publish_npm.py Normal file
View file

@ -0,0 +1,90 @@
import argparse
import getpass
import os
import shutil
import subprocess
import sys
import tempfile
from pathlib import Path
PACKAGE_NAME = "@jessefreitas/n8n-nodes-mega"
def run(command: list[str], cwd: Path, userconfig: Path | None = None) -> None:
env = os.environ.copy()
if userconfig is not None:
env["NPM_CONFIG_USERCONFIG"] = str(userconfig)
executable = command[0]
if os.name == "nt" and executable == "npm":
executable = shutil.which("npm.cmd") or shutil.which("npm") or executable
command = [executable, *command[1:]]
result = subprocess.run(command, cwd=cwd, env=env, check=False)
if result.returncode != 0:
raise SystemExit(result.returncode)
def build_temp_npmrc(token: str) -> Path:
temp_dir = Path(tempfile.mkdtemp(prefix="npm-publish-"))
npmrc = temp_dir / ".npmrc"
npmrc.write_text(
f"//registry.npmjs.org/:_authToken={token}\nregistry=https://registry.npmjs.org/\n",
encoding="ascii",
)
return npmrc
def main() -> int:
parser = argparse.ArgumentParser(
description="Publish @jessefreitas/n8n-nodes-mega using a temporary npm config.",
)
parser.add_argument(
"--token",
default=os.environ.get("NPM_TOKEN", ""),
help="Granular npm token with publish permission. Defaults to NPM_TOKEN.",
)
parser.add_argument(
"--skip-whoami",
action="store_true",
help="Skip the npm whoami check before publishing.",
)
parser.add_argument(
"--skip-view",
action="store_true",
help="Skip the final npm view version check after publishing.",
)
args = parser.parse_args()
token = args.token.strip()
if not token:
token = getpass.getpass("NPM token: ").strip()
if not token:
print("Missing npm token. Pass --token, set NPM_TOKEN, or enter it when prompted.", file=sys.stderr)
return 2
repo_root = Path(__file__).resolve().parent.parent
npmrc = build_temp_npmrc(token)
try:
if not args.skip_whoami:
run(["npm", "whoami"], cwd=repo_root, userconfig=npmrc)
run(
["npm", "publish", "--access", "public", "--ignore-scripts"],
cwd=repo_root,
userconfig=npmrc,
)
if not args.skip_view:
run(["npm", "view", PACKAGE_NAME, "version"], cwd=repo_root, userconfig=npmrc)
finally:
shutil.rmtree(npmrc.parent, ignore_errors=True)
return 0
if __name__ == "__main__":
raise SystemExit(main())