Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 1 addition & 84 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,6 @@ The package is published as `@directus/n8n-nodes-directus` on npm.
npm install @directus/n8n-nodes-directus
```

**Note**: This package is not yet published to npm. For development and testing, see the [Development](#development) section below.

## Usage

### Getting Started
Expand Down Expand Up @@ -143,64 +141,8 @@ npm run build

4. **Create workflows**: Use the Directus nodes in your workflows

### Project Structure

```
├── credentials/ # Directus API credentials
├── nodes/ # n8n nodes (Directus and DirectusTrigger)
├── __tests__/ # Test files
├── dist/ # Built/compiled files (generated)
└── package.json # Package configuration
```

### Available Commands

```bash
# Development
npm run build # Build the project (TypeScript compilation + assets)
npm run dev # Watch mode for TypeScript compilation
npm run dev:n8n # Start n8n with your node loaded for testing
npm run build:n8n # Build nodes and credentials using n8n-node CLI

# Code Quality
npm run lint # Check code style (repo root; tests are ignored by config)
npm run lintfix # Fix code style issues
npm run format # Format code using Prettier

# Testing
npm run test # Run test suite
npm run test:watch # Run tests in watch mode
npm run test:coverage # Run tests with coverage report

# Publishing
npm run release # Publish to npm using n8n-node CLI
```

### Testing

#### Basic Node Testing

1. **Start n8n with your node loaded**:

```bash
npm run dev:n8n
```

2. **Access n8n**: Open http://localhost:5678 in your browser

3. **Configure credentials**:
- Go to **Credentials** → **Add Credential**
- Search for "Directus API" and add your credentials
- Test the connection

4. **Test operations**:
- Create a new workflow
- Add a Directus node
- Test various operations:
- **Items**: Create, Get, Update, Delete items in collections
- **Users**: Invite, Get, Update, Delete users
- **Files**: Upload (requires binary data from previous node), Import (from URL), Get, Update, Delete files

#### Webhook Testing (Requires ngrok)

For testing the **Directus Trigger** node, you need to expose n8n via a public URL since Directus cannot reach localhost:
Expand Down Expand Up @@ -246,32 +188,7 @@ For testing the **Directus Trigger** node, you need to expose n8n via a public U

**Note**: The manual URL replacement step is required because Directus cannot reach localhost URLs directly.

### Troubleshooting

#### Common Issues

1. **n8n not starting**:
- Ensure Node.js 22+ is installed
- Run `pnpm install` to install dependencies
- Check if port 5678 is available

2. **Node not appearing in n8n**:
- Run `npm run build` first
- Restart `npm run dev:n8n`
- Check the terminal for any error messages

3. **Webhook not triggering**:
- Ensure ngrok is running and accessible
- Verify the webhook URL in Directus flows
- Check n8n workflow is activated
- Test the ngrok URL directly in browser

4. **Build errors**:
- Run `npm run lint` to check for code issues
- Run `npm run lintfix` to auto-fix issues
- Ensure TypeScript compilation passes

#### Getting Help
### Getting Help

- Check the [GitHub Issues](https://github.com/directus/n8n-nodes-directus/issues) for known problems
- Run `pnpm test` to verify everything works
Expand Down
108 changes: 55 additions & 53 deletions __tests__/Directus.node.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,9 @@ describe('Directus Node', () => {
.mockReturnValueOnce('user')
.mockReturnValueOnce('update')
.mockReturnValueOnce('user-1')
.mockReturnValueOnce({ fields: { field: [{ name: 'email', value: 'updated@example.com' }] } });
.mockReturnValueOnce({
fields: { field: [{ name: 'email', value: 'updated@example.com' }] },
});

mockExecuteFunctions.helpers.httpRequest.mockResolvedValue({
data: { id: 'user-1', email: 'updated@example.com' },
Expand All @@ -244,74 +246,74 @@ describe('Directus Node', () => {

describe('File Operations', () => {
it('should handle file upload operations', async () => {
mockExecuteFunctions.getNodeParameter
.mockReturnValueOnce('file')
.mockReturnValueOnce('upload');

const mockBinaryData = {
file: {
data: Buffer.from('test file content').toString('base64'),
mockExecuteFunctions.getNodeParameter
.mockReturnValueOnce('file')
.mockReturnValueOnce('upload');

const mockBinaryData = {
file: {
data: Buffer.from('test file content').toString('base64'),
fileName: 'test.txt',
mimeType: 'text/plain',
},
};
mockExecuteFunctions.getInputData.mockReturnValue([{ binary: mockBinaryData }]);
mockExecuteFunctions.helpers.assertBinaryData.mockReturnValue({
fileName: 'test.txt',
mimeType: 'text/plain',
},
};
mockExecuteFunctions.getInputData.mockReturnValue([{ binary: mockBinaryData }]);
mockExecuteFunctions.helpers.assertBinaryData.mockReturnValue({
fileName: 'test.txt',
mimeType: 'text/plain',
});
mockExecuteFunctions.helpers.getBinaryDataBuffer.mockResolvedValue(
Buffer.from('test file content'),
);
mockExecuteFunctions.helpers.request = vi.fn().mockResolvedValue({
data: { id: 'file-1', filename_download: 'test.txt' },
});
mockExecuteFunctions.helpers.getBinaryDataBuffer.mockResolvedValue(
Buffer.from('test file content'),
);
mockExecuteFunctions.helpers.request = vi.fn().mockResolvedValue({
data: { id: 'file-1', filename_download: 'test.txt' },
});

const result = await node.execute.call(mockExecuteFunctions);

expect(result[0][0].json).toHaveProperty('id', 'file-1');
expect(mockExecuteFunctions.helpers.request).toHaveBeenCalled();
});

const result = await node.execute.call(mockExecuteFunctions);
it('should handle file import operations', async () => {
mockExecuteFunctions.getNodeParameter
.mockReturnValueOnce('file')
.mockReturnValueOnce('import')
.mockReturnValueOnce('https://example.com/image.jpg');

expect(result[0][0].json).toHaveProperty('id', 'file-1');
expect(mockExecuteFunctions.helpers.request).toHaveBeenCalled();
});
mockExecuteFunctions.helpers.httpRequest.mockResolvedValue({
data: { id: 'file-2', filename_download: 'image.jpg' },
});

it('should handle file import operations', async () => {
mockExecuteFunctions.getNodeParameter
.mockReturnValueOnce('file')
.mockReturnValueOnce('import')
.mockReturnValueOnce('https://example.com/image.jpg');
const result = await node.execute.call(mockExecuteFunctions);

mockExecuteFunctions.helpers.httpRequest.mockResolvedValue({
data: { id: 'file-2', filename_download: 'image.jpg' },
expect(result[0][0].json).toHaveProperty('id', 'file-2');
});

const result = await node.execute.call(mockExecuteFunctions);
it('should handle file get operations', async () => {
mockExecuteFunctions.getNodeParameter
.mockReturnValueOnce('file')
.mockReturnValueOnce('get')
.mockReturnValueOnce('file-3');

expect(result[0][0].json).toHaveProperty('id', 'file-2');
});
mockExecuteFunctions.helpers.httpRequest.mockResolvedValue({
data: { id: 'file-3', filename_download: 'document.pdf' },
});

it('should handle file get operations', async () => {
mockExecuteFunctions.getNodeParameter
.mockReturnValueOnce('file')
.mockReturnValueOnce('get')
.mockReturnValueOnce('file-3');
const result = await node.execute.call(mockExecuteFunctions);

mockExecuteFunctions.helpers.httpRequest.mockResolvedValue({
data: { id: 'file-3', filename_download: 'document.pdf' },
expect(result[0][0].json).toHaveProperty('id', 'file-3');
});

const result = await node.execute.call(mockExecuteFunctions);

expect(result[0][0].json).toHaveProperty('id', 'file-3');
});

it('should handle file delete operations', async () => {
mockExecuteFunctions.getNodeParameter
.mockReturnValueOnce('file')
.mockReturnValueOnce('delete')
.mockReturnValueOnce('file-4');
it('should handle file delete operations', async () => {
mockExecuteFunctions.getNodeParameter
.mockReturnValueOnce('file')
.mockReturnValueOnce('delete')
.mockReturnValueOnce('file-4');

mockExecuteFunctions.helpers.httpRequest.mockResolvedValue({});
mockExecuteFunctions.helpers.httpRequest.mockResolvedValue({});

const result = await node.execute.call(mockExecuteFunctions);
const result = await node.execute.call(mockExecuteFunctions);

expect(result[0][0].json).toEqual({ deleted: true, id: 'file-4' });
});
Expand Down
19 changes: 10 additions & 9 deletions __tests__/DirectusTrigger.node.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ describe('DirectusTrigger Node', () => {
.mockReturnValueOnce('create')
.mockReturnValueOnce('users');
mockWebhookFunctions.getWorkflowStaticData.mockReturnValue({});
mockWebhookFunctions.helpers.httpRequest
mockWebhookFunctions.helpers.httpRequestWithAuthentication
.mockResolvedValueOnce({ data: { data: [] } })
.mockResolvedValueOnce({ data: { id: 'flow-id' } })
.mockResolvedValueOnce({ data: { id: 'operation-id' } })
Expand All @@ -38,7 +38,7 @@ describe('DirectusTrigger Node', () => {
const result = await node.webhookMethods!.default!.create.call(mockWebhookFunctions);

expect(result).toBe(true);
expect(mockWebhookFunctions.helpers.httpRequest).toHaveBeenCalledTimes(4);
expect(mockWebhookFunctions.helpers.httpRequestWithAuthentication).toHaveBeenCalledTimes(4);
});

it('should create webhook flow with script filter for user.update', async () => {
Expand All @@ -47,7 +47,7 @@ describe('DirectusTrigger Node', () => {
.mockReturnValueOnce('update')
.mockReturnValueOnce('directus_users');
mockWebhookFunctions.getWorkflowStaticData.mockReturnValue({});
mockWebhookFunctions.helpers.httpRequest
mockWebhookFunctions.helpers.httpRequestWithAuthentication
.mockResolvedValueOnce({ data: { data: [] } })
.mockResolvedValueOnce({ data: { id: 'flow-id' } })
.mockResolvedValueOnce({ data: { id: 'webhook-op-id' } })
Expand All @@ -57,19 +57,19 @@ describe('DirectusTrigger Node', () => {
const result = await node.webhookMethods!.default!.create.call(mockWebhookFunctions);

expect(result).toBe(true);
expect(mockWebhookFunctions.helpers.httpRequest).toHaveBeenCalledTimes(5);
expect(mockWebhookFunctions.helpers.httpRequestWithAuthentication).toHaveBeenCalledTimes(5);
// Verify script operation was created with correct code
const scriptCall = mockWebhookFunctions.helpers.httpRequest.mock.calls.find(
(call: any[]) => call[0]?.body?.type === 'exec',
const scriptCall = mockWebhookFunctions.helpers.httpRequestWithAuthentication.mock.calls.find(
(call: any[]) => call[1]?.body?.type === 'exec',
);
expect(scriptCall).toBeDefined();
expect(scriptCall[0].body.options.code).toContain('last_page');
expect(scriptCall?.[1]?.body.options.code).toContain('last_page');
});

it('should delete webhook flow', async () => {
const staticData = { flowId: 'flow-1' };
mockWebhookFunctions.getWorkflowStaticData.mockReturnValue(staticData);
mockWebhookFunctions.helpers.httpRequest.mockResolvedValue({});
mockWebhookFunctions.helpers.httpRequestWithAuthentication.mockResolvedValue({});
mockWebhookFunctions.getCredentials.mockResolvedValue({
url: 'https://example.com',
token: 'test-token',
Expand All @@ -78,7 +78,8 @@ describe('DirectusTrigger Node', () => {
const result = await node.webhookMethods!.default!.delete.call(mockWebhookFunctions);

expect(result).toBe(true);
expect(mockWebhookFunctions.helpers.httpRequest).toHaveBeenCalledWith(
expect(mockWebhookFunctions.helpers.httpRequestWithAuthentication).toHaveBeenCalledWith(
'directusApi',
expect.objectContaining({
method: 'DELETE',
}),
Expand Down
6 changes: 6 additions & 0 deletions __tests__/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,12 @@ export function createMockWebhookFunctions(
id: 'flow-id-123',
},
}),
httpRequestWithAuthentication: vi.fn<any>().mockResolvedValue({
data: {
data: [{ id: 1, name: 'Test User' }],
id: 'flow-id-123',
},
}),
},
getCredentials: vi.fn<any>().mockResolvedValue(mockDirectusCredentials()),
getWorkflowStaticData: vi.fn(() => ({ flowId: undefined })),
Expand Down
5 changes: 3 additions & 2 deletions nodes/Directus/Directus.node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
INodeExecutionData,
INodeType,
INodeTypeDescription,
NodeConnectionTypes,
NodeOperationError,
IHttpRequestOptions,
IDataObject,
Expand Down Expand Up @@ -48,8 +49,8 @@ export class Directus implements INodeType {
defaults: {
name: 'Directus',
},
inputs: ['main'],
outputs: ['main'],
inputs: [NodeConnectionTypes.Main],
outputs: [NodeConnectionTypes.Main],
usableAsTool: true,
credentials: [
{
Expand Down
Loading